Skip to main content

rainy_sdk/
models.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::cmp::Ordering;
4use std::collections::HashMap;
5
6fn map_is_empty(value: &HashMap<String, serde_json::Value>) -> bool {
7    value.is_empty()
8}
9
10/// Represents a single message in a chat conversation.
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct ChatMessage {
13    /// The role of the message author.
14    pub role: MessageRole,
15    /// The content of the message.
16    pub content: String,
17}
18
19/// The role of a message's author.
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21#[serde(rename_all = "lowercase")]
22pub enum MessageRole {
23    /// A message from the system, setting the context or instructions for the assistant.
24    System,
25    /// A message from the user.
26    User,
27    /// A message from the assistant.
28    Assistant,
29}
30
31/// The role of an OpenAI-compatible chat message author.
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33#[serde(rename_all = "lowercase")]
34pub enum OpenAIMessageRole {
35    /// A message from the system.
36    System,
37    /// A message from the user.
38    User,
39    /// A message from the assistant.
40    Assistant,
41    /// A tool result message.
42    Tool,
43}
44
45/// OpenAI-compatible chat message content.
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47#[serde(untagged)]
48pub enum OpenAIMessageContent {
49    /// Plain text content.
50    Text(String),
51    /// Multimodal content parts.
52    Parts(Vec<OpenAIContentPart>),
53}
54
55/// OpenAI-compatible multimodal content part.
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57#[serde(tag = "type", rename_all = "snake_case")]
58pub enum OpenAIContentPart {
59    /// Text content part.
60    Text {
61        /// The text content.
62        text: String,
63    },
64    /// Image URL content part.
65    ImageUrl {
66        /// Image URL payload.
67        image_url: OpenAIImageUrl,
68    },
69}
70
71/// OpenAI-compatible image URL payload.
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73pub struct OpenAIImageUrl {
74    /// Image URL or data URI.
75    pub url: String,
76    /// Optional detail level hint.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub detail: Option<String>,
79}
80
81/// OpenAI-compatible function call payload.
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub struct OpenAIFunctionCall {
84    /// Function name.
85    pub name: String,
86    /// JSON-encoded function arguments.
87    pub arguments: String,
88}
89
90/// OpenAI-compatible tool call payload.
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub struct OpenAIToolCall {
93    /// Tool call ID.
94    pub id: String,
95    /// Tool type (typically `function`).
96    pub r#type: String,
97    /// Optional provider-specific metadata.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub extra_content: Option<serde_json::Value>,
100    /// Function call details.
101    pub function: OpenAIFunctionCall,
102}
103
104/// OpenAI-compatible chat message with full tool-call replay support.
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
106pub struct OpenAIChatMessage {
107    /// The role of the message author.
108    pub role: OpenAIMessageRole,
109    /// Message content. Assistant tool-call messages may omit content.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub content: Option<OpenAIMessageContent>,
112    /// Optional display name.
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub name: Option<String>,
115    /// Assistant tool calls attached to this message.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub tool_calls: Option<Vec<OpenAIToolCall>>,
118    /// Tool call ID associated with a `tool` role message.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub tool_call_id: Option<String>,
121}
122
123/// The search provider to use for web research.
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
125#[serde(rename_all = "lowercase")]
126pub enum ResearchProvider {
127    /// Use Exa (formerly Metaphor) for high-quality semantic search.
128    #[default]
129    Exa,
130    /// Use Tavily for comprehensive web search and content extraction.
131    Tavily,
132    /// Automatically select the best provider based on the query.
133    Auto,
134}
135
136/// The depth of the research operation.
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
138#[serde(rename_all = "lowercase")]
139pub enum ResearchDepth {
140    /// Basic search (faster, lower cost).
141    #[default]
142    Basic,
143    /// Deep search (more thorough, higher cost, includes more context).
144    Advanced,
145}
146
147/// Represents a request to create a chat completion.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct ChatCompletionRequest {
150    /// The identifier of the model to use for the completion (e.g., "gpt-4o", "claude-sonnet-4").
151    pub model: String,
152
153    /// A list of messages that form the conversation history.
154    pub messages: Vec<ChatMessage>,
155
156    /// The sampling temperature to use, between 0.0 and 2.0. Higher values will make the output
157    /// more random, while lower values will make it more focused and deterministic.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub temperature: Option<f32>,
160
161    /// The maximum number of tokens to generate in the completion.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub max_tokens: Option<u32>,
164
165    /// The nucleus sampling parameter. The model considers the results of the tokens with `top_p`
166    /// probability mass. So, 0.1 means only the tokens comprising the top 10% probability mass are considered.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub top_p: Option<f32>,
169
170    /// A penalty applied to new tokens based on their frequency in the text so far.
171    /// It decreases the model's likelihood to repeat the same line verbatim.
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub frequency_penalty: Option<f32>,
174
175    /// A penalty applied to new tokens based on whether they appear in the text so far.
176    /// It increases the model's likelihood to talk about new topics.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub presence_penalty: Option<f32>,
179
180    /// A list of sequences that will cause the model to stop generating further tokens.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub stop: Option<Vec<String>>,
183
184    /// A unique identifier representing your end-user, which can help in monitoring and
185    /// tracking conversations.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub user: Option<String>,
188
189    /// A hint to the router about which provider to use for the model.
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub provider: Option<String>,
192
193    /// If set to `true`, the response will be streamed as a series of events.
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub stream: Option<bool>,
196
197    /// Modify the likelihood of specified tokens appearing in the completion.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub logit_bias: Option<serde_json::Value>,
200
201    /// Whether to return log probabilities of the output tokens.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub logprobs: Option<bool>,
204
205    /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub top_logprobs: Option<u32>,
208
209    /// How many chat completion choices to generate for each input message.
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub n: Option<u32>,
212
213    /// An object specifying the format that the model must output.
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub response_format: Option<ResponseFormat>,
216
217    /// A list of tools the model may call.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub tools: Option<Vec<Tool>>,
220
221    /// Controls which (if any) tool is called by the model.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub tool_choice: Option<ToolChoice>,
224
225    /// Configuration for thinking capabilities (Gemini 3 and 2.5 series).
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub thinking_config: Option<ThinkingConfig>,
228}
229
230/// OpenAI-compatible request payload with full message replay support.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct OpenAIChatCompletionRequest {
233    /// The identifier of the model to use for the completion.
234    pub model: String,
235
236    /// Full OpenAI-compatible message history.
237    pub messages: Vec<OpenAIChatMessage>,
238
239    /// The sampling temperature to use.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub temperature: Option<f32>,
242
243    /// The maximum number of tokens to generate in the completion.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub max_tokens: Option<u32>,
246
247    /// Nucleus sampling parameter.
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub top_p: Option<f32>,
250
251    /// Frequency penalty.
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub frequency_penalty: Option<f32>,
254
255    /// Presence penalty.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub presence_penalty: Option<f32>,
258
259    /// Stop sequences.
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub stop: Option<Vec<String>>,
262
263    /// End-user identifier.
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub user: Option<String>,
266
267    /// Router/provider hint.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub provider: Option<String>,
270
271    /// If true, the response will be streamed as SSE events.
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub stream: Option<bool>,
274
275    /// Logit bias map.
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub logit_bias: Option<serde_json::Value>,
278
279    /// Whether to return log probabilities.
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub logprobs: Option<bool>,
282
283    /// Number of top log probabilities to return.
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub top_logprobs: Option<u32>,
286
287    /// Number of completion choices to generate.
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub n: Option<u32>,
290
291    /// Structured response format.
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub response_format: Option<ResponseFormat>,
294
295    /// Tools available to the model.
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub tools: Option<Vec<Tool>>,
298
299    /// Tool selection strategy.
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub tool_choice: Option<ToolChoice>,
302
303    /// Gemini thinking configuration.
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub thinking_config: Option<ThinkingConfig>,
306
307    /// Anthropic extended-thinking configuration (`thinking.budget_tokens`).
308    /// Serialised as the `thinking` top-level field so it is passed through to
309    /// OpenRouter/Anthropic as `{"type":"enabled","budget_tokens":N}`.
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub thinking: Option<serde_json::Value>,
312}
313
314/// Represents the response from a chat completion request.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct ChatCompletionResponse {
317    /// A unique identifier for the chat completion.
318    pub id: String,
319
320    /// The type of object, which is always "chat.completion".
321    pub object: String,
322
323    /// The Unix timestamp (in seconds) of when the completion was created.
324    pub created: u64,
325
326    /// The model that was used for the completion.
327    pub model: String,
328
329    /// A list of chat completion choices.
330    pub choices: Vec<ChatChoice>,
331
332    /// Information about the token usage for this completion.
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub usage: Option<Usage>,
335}
336
337/// OpenAI-compatible chat completion response with tool-call aware messages.
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct OpenAIChatCompletionResponse {
340    /// A unique identifier for the chat completion.
341    pub id: String,
342
343    /// The type of object, which is always `chat.completion`.
344    pub object: String,
345
346    /// The Unix timestamp (in seconds) of when the completion was created.
347    pub created: u64,
348
349    /// The model that was used for the completion.
350    pub model: String,
351
352    /// A list of chat completion choices.
353    pub choices: Vec<OpenAIChatChoice>,
354
355    /// Token usage information for this completion.
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub usage: Option<Usage>,
358}
359
360/// Represents a chunk of a streaming chat completion response.
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct ChatCompletionChunk {
363    /// A unique identifier for the chat completion.
364    pub id: String,
365
366    /// The type of object, which is always "chat.completion.chunk".
367    pub object: String,
368
369    /// The Unix timestamp (in seconds) of when the completion was created.
370    pub created: u64,
371
372    /// The model that was used for the completion.
373    pub model: String,
374
375    /// A list of chat completion choices.
376    pub choices: Vec<ChatCompletionChunkChoice>,
377}
378
379/// Represents a single choice in a streaming chat completion response.
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct ChatCompletionChunkChoice {
382    /// The index of the choice in the list of choices.
383    pub index: u32,
384
385    /// A delta payload with the content that has changed since the last chunk.
386    pub delta: ChatCompletionChunkDelta,
387
388    /// The reason the model stopped generating tokens.
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub finish_reason: Option<String>,
391}
392
393/// Represents the delta payload of a streaming chat completion chunk.
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct ChatCompletionChunkDelta {
396    /// The role of the message author.
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub role: Option<MessageRole>,
399
400    /// The content of the message.
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub content: Option<String>,
403
404    /// The thinking content (for Gemini 3 models).
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub thought: Option<String>,
407}
408
409/// Represents a single choice in a chat completion response.
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct ChatChoice {
412    /// The index of the choice in the list of choices.
413    pub index: u32,
414
415    /// The message generated by the model.
416    pub message: ChatMessage,
417
418    /// The reason the model stopped generating tokens.
419    pub finish_reason: String,
420}
421
422/// Represents a single choice in an OpenAI-compatible chat completion response.
423#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct OpenAIChatChoice {
425    /// The index of the choice in the list of choices.
426    pub index: u32,
427
428    /// The tool-call aware message generated by the model.
429    pub message: OpenAIChatMessage,
430
431    /// The reason the model stopped generating tokens.
432    pub finish_reason: String,
433}
434
435/// Represents the token usage statistics for a chat completion.
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct Usage {
438    /// The number of tokens in the prompt.
439    pub prompt_tokens: u32,
440
441    /// The number of tokens in the generated completion.
442    pub completion_tokens: u32,
443
444    /// The total number of tokens used in the request (prompt + completion).
445    pub total_tokens: u32,
446}
447
448/// Represents the health status of the Rainy API.
449#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct HealthStatus {
451    /// The overall status of the API (e.g., "healthy", "degraded").
452    pub status: String,
453
454    /// The timestamp of when the health check was performed.
455    pub timestamp: String,
456
457    /// The uptime of the system in seconds.
458    pub uptime: f64,
459
460    /// The status of individual services.
461    pub services: ServiceStatus,
462}
463
464/// Represents the status of individual backend services.
465#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct ServiceStatus {
467    /// The status of the database connection.
468    pub database: bool,
469
470    /// The status of the Redis connection, if applicable.
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub redis: Option<bool>,
473
474    /// The overall status of the connections to AI providers.
475    pub providers: bool,
476}
477
478/// Represents the available models and providers.
479#[derive(Debug, Clone, Serialize, Deserialize, Default)]
480pub struct AvailableModels {
481    /// A map where keys are provider names and values are lists of model names.
482    #[serde(default)]
483    pub providers: HashMap<String, Vec<String>>,
484
485    /// The total number of available models across all providers.
486    #[serde(default)]
487    pub total_models: usize,
488
489    /// A list of provider names that are currently active and available.
490    #[serde(default)]
491    pub active_providers: Vec<String>,
492}
493
494/// Represents information about credit usage for a request.
495#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct CreditInfo {
497    /// The number of credits available before the request.
498    pub current_credits: f64,
499
500    /// The estimated number of credits that the request will cost.
501    pub estimated_cost: f64,
502
503    /// The estimated number of credits remaining after the request.
504    pub credits_after_request: f64,
505
506    /// The date when the credit balance is next scheduled to be reset.
507    pub reset_date: String,
508}
509
510/// Represents metadata extracted from the response headers of an API request.
511#[derive(Debug, Clone)]
512pub struct RequestMetadata {
513    /// The time taken for the request to complete, in milliseconds.
514    pub response_time: Option<u64>,
515
516    /// The AI provider that handled the request.
517    pub provider: Option<String>,
518
519    /// The number of tokens used in the request.
520    pub tokens_used: Option<u32>,
521
522    /// The number of credits used for the request.
523    pub credits_used: Option<f64>,
524
525    /// The number of credits remaining after the request.
526    pub credits_remaining: Option<f64>,
527
528    /// The unique ID of the request, for tracking and debugging.
529    pub request_id: Option<String>,
530
531    /// Count of non-blocking compatibility warnings returned by Rainy.
532    pub compat_warnings: Option<u32>,
533
534    /// Response mode selected by Rainy (`raw` or `envelope`).
535    pub response_mode: Option<String>,
536
537    /// Billing plan used for this request.
538    pub billing_plan: Option<String>,
539
540    /// Credits charged for the request.
541    pub rainy_credits_charged: Option<f64>,
542
543    /// Markup percent applied by gateway pricing.
544    pub rainy_markup_percent: Option<f64>,
545
546    /// Remaining daily credits reported by Rainy.
547    pub rainy_daily_credits_remaining: Option<String>,
548}
549
550/// OpenRouter/Rainy Responses API request payload.
551#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct ResponsesRequest {
553    /// The identifier of the model to use.
554    pub model: String,
555
556    /// Input payload accepted by the Responses API (string, object, or array).
557    pub input: serde_json::Value,
558
559    /// If true, the response will be streamed as SSE events.
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub stream: Option<bool>,
562
563    /// Responses tool definitions and/or custom tools.
564    #[serde(skip_serializing_if = "Option::is_none")]
565    pub tools: Option<Vec<serde_json::Value>>,
566
567    /// Tool selection strategy.
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub tool_choice: Option<serde_json::Value>,
570
571    /// Structured output format.
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub response_format: Option<serde_json::Value>,
574
575    /// Sampling temperature.
576    #[serde(skip_serializing_if = "Option::is_none")]
577    pub temperature: Option<f32>,
578
579    /// Top-p nucleus sampling.
580    #[serde(skip_serializing_if = "Option::is_none")]
581    pub top_p: Option<f32>,
582
583    /// Maximum number of output tokens.
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub max_output_tokens: Option<u32>,
586
587    /// End-user identifier (legacy fallback accepted by Rainy).
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub user: Option<String>,
590
591    /// Prompt cache key for routing/cache optimization.
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub prompt_cache_key: Option<String>,
594
595    /// Reasoning configuration object (provider/model dependent).
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub reasoning: Option<serde_json::Value>,
598
599    /// Forward-compatible extra parameters.
600    #[serde(flatten, skip_serializing_if = "map_is_empty", default)]
601    pub extra: HashMap<String, serde_json::Value>,
602}
603
604impl ResponsesRequest {
605    /// Creates a new Responses request from an arbitrary input payload.
606    pub fn new(model: impl Into<String>, input: serde_json::Value) -> Self {
607        Self {
608            model: model.into(),
609            input,
610            stream: None,
611            tools: None,
612            tool_choice: None,
613            response_format: None,
614            temperature: None,
615            top_p: None,
616            max_output_tokens: None,
617            user: None,
618            prompt_cache_key: None,
619            reasoning: None,
620            extra: HashMap::new(),
621        }
622    }
623
624    /// Convenience constructor for plain text input.
625    pub fn text(model: impl Into<String>, input_text: impl Into<String>) -> Self {
626        Self::new(model, serde_json::Value::String(input_text.into()))
627    }
628
629    /// Sets streaming mode.
630    pub fn with_stream(mut self, stream: bool) -> Self {
631        self.stream = Some(stream);
632        self
633    }
634
635    /// Sets reasoning configuration object.
636    pub fn with_reasoning(mut self, reasoning: serde_json::Value) -> Self {
637        self.reasoning = Some(reasoning);
638        self
639    }
640
641    /// Convenience helper to set reasoning effort (`low`, `medium`, `high`).
642    pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
643        self.reasoning = Some(serde_json::json!({ "effort": effort.into() }));
644        self
645    }
646
647    /// Sets max output tokens.
648    pub fn with_max_output_tokens(mut self, max_output_tokens: u32) -> Self {
649        self.max_output_tokens = Some(max_output_tokens);
650        self
651    }
652
653    /// Sets prompt cache key.
654    pub fn with_prompt_cache_key(mut self, prompt_cache_key: impl Into<String>) -> Self {
655        self.prompt_cache_key = Some(prompt_cache_key.into());
656        self
657    }
658
659    /// Sets user identifier.
660    pub fn with_user(mut self, user: impl Into<String>) -> Self {
661        self.user = Some(user.into());
662        self
663    }
664
665    /// Sets tool definitions array directly.
666    pub fn with_tools(mut self, tools: Vec<serde_json::Value>) -> Self {
667        self.tools = Some(tools);
668        self
669    }
670
671    /// Adds a function tool using Responses-style shape.
672    pub fn add_function_tool(
673        mut self,
674        name: impl Into<String>,
675        description: impl Into<String>,
676        parameters: serde_json::Value,
677    ) -> Self {
678        let mut tools = self.tools.unwrap_or_default();
679        tools.push(serde_json::json!({
680            "type": "function",
681            "name": name.into(),
682            "description": description.into(),
683            "parameters": parameters
684        }));
685        self.tools = Some(tools);
686        self
687    }
688
689    /// Adds a custom extra parameter for forward compatibility.
690    pub fn with_extra(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
691        self.extra.insert(key.into(), value);
692        self
693    }
694}
695
696/// Responses API usage object (partial, forward-compatible).
697#[derive(Debug, Clone, Serialize, Deserialize, Default)]
698pub struct ResponsesUsage {
699    /// Number of input tokens consumed.
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub input_tokens: Option<u32>,
702    /// Number of output tokens generated.
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub output_tokens: Option<u32>,
705    /// Number of tokens used for cache creation.
706    #[serde(skip_serializing_if = "Option::is_none")]
707    pub cache_creation_input_tokens: Option<u32>,
708    /// Number of tokens read from cache.
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub cache_read_input_tokens: Option<u32>,
711    /// Detailed breakdown of output tokens.
712    #[serde(skip_serializing_if = "Option::is_none")]
713    pub output_tokens_details: Option<serde_json::Value>,
714    /// Detailed breakdown of completion tokens.
715    #[serde(skip_serializing_if = "Option::is_none")]
716    pub completion_tokens_details: Option<serde_json::Value>,
717    /// Additional provider-specific usage fields.
718    #[serde(flatten, default)]
719    pub extra: HashMap<String, serde_json::Value>,
720}
721
722/// Responses API raw response payload.
723#[derive(Debug, Clone, Serialize, Deserialize, Default)]
724pub struct ResponsesApiResponse {
725    /// Unique identifier for the response.
726    #[serde(skip_serializing_if = "Option::is_none")]
727    pub id: Option<String>,
728    /// Object type identifier.
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub object: Option<String>,
731    /// Model used for the response.
732    #[serde(skip_serializing_if = "Option::is_none")]
733    pub model: Option<String>,
734    /// Plain text output content.
735    #[serde(skip_serializing_if = "Option::is_none")]
736    pub output_text: Option<String>,
737    /// Structured output items.
738    #[serde(skip_serializing_if = "Option::is_none")]
739    pub output: Option<Vec<serde_json::Value>>,
740    /// Token usage information.
741    #[serde(skip_serializing_if = "Option::is_none")]
742    pub usage: Option<ResponsesUsage>,
743    /// Additional provider-specific response fields.
744    #[serde(flatten, default)]
745    pub extra: HashMap<String, serde_json::Value>,
746}
747
748/// Non-blocking compatibility warning emitted by Rainy in envelope mode.
749#[derive(Debug, Clone, Serialize, Deserialize)]
750pub struct CompatWarning {
751    /// Warning code identifier.
752    pub code: String,
753    /// Human-readable warning message.
754    pub message: String,
755    /// JSON path to the field that triggered the warning.
756    #[serde(skip_serializing_if = "Option::is_none")]
757    pub path: Option<String>,
758}
759
760/// Features used by request (reported in envelope mode).
761#[derive(Debug, Clone, Serialize, Deserialize)]
762pub struct FeaturesUsed {
763    /// Whether reasoning/thinking was used.
764    pub reasoning: bool,
765    /// Whether image input was provided.
766    #[serde(rename = "imageInput")]
767    pub image_input: bool,
768    /// Whether tool calling was used.
769    pub tools: bool,
770    /// Whether structured output was requested.
771    #[serde(rename = "structuredOutput")]
772    pub structured_output: bool,
773}
774
775/// Reasoning summary metadata reported by Rainy in envelope mode.
776#[derive(Debug, Clone, Serialize, Deserialize)]
777pub struct ReasoningMeta {
778    /// Whether reasoning was present in the response.
779    pub present: bool,
780    /// Whether a reasoning summary was provided.
781    pub summary_present: bool,
782    /// Number of tokens used for reasoning.
783    #[serde(skip_serializing_if = "Option::is_none")]
784    pub tokens: Option<u32>,
785}
786
787/// Rainy envelope metadata (partial, forward-compatible).
788#[derive(Debug, Clone, Serialize, Deserialize, Default)]
789pub struct RainyEnvelopeMeta {
790    /// Billing plan identifier.
791    #[serde(
792        rename = "billingPlan",
793        alias = "billing_plan",
794        skip_serializing_if = "Option::is_none"
795    )]
796    pub billing_plan: Option<String>,
797    /// Credits charged for the request.
798    #[serde(
799        rename = "creditsCharged",
800        alias = "credits_charged",
801        skip_serializing_if = "Option::is_none"
802    )]
803    pub credits_charged: Option<f64>,
804    /// Markup percentage applied to pricing.
805    #[serde(
806        rename = "markupPercent",
807        alias = "markup_percent",
808        skip_serializing_if = "Option::is_none"
809    )]
810    pub markup_percent: Option<f64>,
811    /// Daily credits remaining for the user.
812    #[serde(
813        rename = "dailyCreditsRemaining",
814        alias = "daily_credits_remaining",
815        skip_serializing_if = "Option::is_none"
816    )]
817    pub daily_credits_remaining: Option<String>,
818    /// Compatibility warnings emitted during processing.
819    #[serde(
820        rename = "compatWarnings",
821        alias = "compat_warnings",
822        skip_serializing_if = "Option::is_none"
823    )]
824    pub compat_warnings: Option<Vec<CompatWarning>>,
825    /// Features used by the request.
826    #[serde(
827        rename = "featuresUsed",
828        alias = "features_used",
829        skip_serializing_if = "Option::is_none"
830    )]
831    pub features_used: Option<FeaturesUsed>,
832    /// Reasoning metadata for the response.
833    #[serde(skip_serializing_if = "Option::is_none")]
834    pub reasoning: Option<ReasoningMeta>,
835    /// Additional envelope metadata fields.
836    #[serde(flatten, default)]
837    pub extra: HashMap<String, serde_json::Value>,
838}
839
840/// Standard Rainy success envelope.
841#[derive(Debug, Clone, Serialize, Deserialize)]
842pub struct RainyEnvelope<T> {
843    /// Whether the request was successful.
844    pub success: bool,
845    /// The response data payload.
846    pub data: T,
847    /// Optional envelope metadata.
848    #[serde(skip_serializing_if = "Option::is_none")]
849    pub meta: Option<RainyEnvelopeMeta>,
850}
851
852/// Response stream SSE event payload (dynamic by design).
853pub type ResponsesStreamEvent = serde_json::Value;
854
855/// Model architecture metadata returned by `/models/catalog`.
856#[derive(Debug, Clone, Serialize, Deserialize, Default)]
857pub struct ModelArchitecture {
858    /// Supported input modalities (e.g., "text", "image").
859    #[serde(skip_serializing_if = "Option::is_none")]
860    pub input_modalities: Option<Vec<String>>,
861    /// Supported output modalities (e.g., "text").
862    #[serde(skip_serializing_if = "Option::is_none")]
863    pub output_modalities: Option<Vec<String>>,
864    /// Tokenizer used by the model.
865    #[serde(skip_serializing_if = "Option::is_none")]
866    pub tokenizer: Option<String>,
867    /// Instruction type supported by the model.
868    #[serde(skip_serializing_if = "Option::is_none")]
869    pub instruct_type: Option<String>,
870}
871
872/// Capability flag can be boolean or `"unknown"`.
873#[derive(Debug, Clone, Serialize, Deserialize)]
874#[serde(untagged)]
875pub enum CapabilityFlag {
876    /// Boolean capability flag.
877    Bool(bool),
878    /// Text-based capability flag (e.g., "unknown").
879    Text(String),
880}
881
882/// Rainy capability hints returned by `/models/catalog`.
883#[derive(Debug, Clone, Serialize, Deserialize, Default)]
884pub struct RainyCapabilities {
885    /// Whether the model supports reasoning/thinking.
886    #[serde(skip_serializing_if = "Option::is_none")]
887    pub reasoning: Option<CapabilityFlag>,
888    /// Whether the model supports image input.
889    #[serde(skip_serializing_if = "Option::is_none")]
890    pub image_input: Option<CapabilityFlag>,
891    /// Whether the model supports tool calling.
892    #[serde(skip_serializing_if = "Option::is_none")]
893    pub tools: Option<CapabilityFlag>,
894    /// Whether the model supports structured output formats.
895    #[serde(skip_serializing_if = "Option::is_none")]
896    pub response_format: Option<CapabilityFlag>,
897}
898
899/// Provider-specific reasoning profile.
900#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
901#[serde(rename_all = "lowercase")]
902pub enum ReasoningProvider {
903    /// OpenAI provider.
904    Openai,
905    /// Google provider.
906    Google,
907    /// Anthropic provider.
908    Anthropic,
909    /// Other providers.
910    Other,
911}
912
913/// Thinking budget range metadata.
914#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
915pub struct ThinkingBudget {
916    /// Minimum budget value.
917    pub min: i32,
918    /// Maximum budget value.
919    pub max: i32,
920    /// Dynamic budget value if applicable.
921    #[serde(skip_serializing_if = "Option::is_none")]
922    pub dynamic_value: Option<i32>,
923    /// Value that disables thinking budget.
924    #[serde(skip_serializing_if = "Option::is_none")]
925    pub disable_value: Option<i32>,
926}
927
928/// Reasoning controls available for a model.
929#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
930pub struct ReasoningControls {
931    /// Parameters observed for reasoning control.
932    #[serde(skip_serializing_if = "Option::is_none")]
933    pub observed_parameters: Option<Vec<String>>,
934    /// Whether reasoning toggle is supported.
935    #[serde(skip_serializing_if = "Option::is_none")]
936    pub reasoning_toggle: Option<bool>,
937    /// Whether reasoning effort control is supported.
938    #[serde(skip_serializing_if = "Option::is_none")]
939    pub reasoning_effort: Option<bool>,
940    /// Available effort levels.
941    #[serde(skip_serializing_if = "Option::is_none")]
942    pub effort: Option<Vec<String>>,
943    /// Available thinking levels.
944    #[serde(skip_serializing_if = "Option::is_none")]
945    pub thinking_level: Option<Vec<String>>,
946    /// Thinking budget configuration.
947    #[serde(skip_serializing_if = "Option::is_none")]
948    pub thinking_budget: Option<ThinkingBudget>,
949}
950
951/// Provider profile for reasoning/thinking.
952#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
953pub struct ReasoningProfile {
954    /// The provider this profile applies to.
955    pub provider: ReasoningProvider,
956    /// JSON path to the reasoning parameter.
957    pub parameter_path: String,
958    /// Available values for the parameter.
959    #[serde(skip_serializing_if = "Option::is_none")]
960    pub values: Option<Vec<String>>,
961    /// Additional notes about this profile.
962    #[serde(skip_serializing_if = "Option::is_none")]
963    pub notes: Option<String>,
964}
965
966/// Reasoning toggle paths for clients.
967#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
968pub struct ReasoningToggle {
969    /// Parameter path to enable reasoning.
970    #[serde(skip_serializing_if = "Option::is_none")]
971    pub enable_param: Option<String>,
972    /// Parameter path to include reasoning in response.
973    #[serde(skip_serializing_if = "Option::is_none")]
974    pub include_reasoning_param: Option<String>,
975}
976
977/// Reasoning capability block in `rainy_capabilities_v2`.
978#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
979pub struct RainyReasoningCapabilitiesV2 {
980    /// Whether reasoning is supported.
981    pub supported: bool,
982    /// Available reasoning controls.
983    #[serde(skip_serializing_if = "Option::is_none")]
984    pub controls: Option<ReasoningControls>,
985    /// Provider-specific reasoning profiles.
986    #[serde(default)]
987    pub profiles: Vec<ReasoningProfile>,
988    /// Reasoning toggle configuration.
989    #[serde(skip_serializing_if = "Option::is_none")]
990    pub toggle: Option<ReasoningToggle>,
991}
992
993/// Multimodal capability block in `rainy_capabilities_v2`.
994#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
995pub struct RainyMultimodalCapabilitiesV2 {
996    /// Supported input modalities.
997    #[serde(default)]
998    pub input: Vec<String>,
999    /// Supported output modalities.
1000    #[serde(default)]
1001    pub output: Vec<String>,
1002}
1003
1004/// Parameter capability block in `rainy_capabilities_v2`.
1005#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1006pub struct RainyParametersCapabilitiesV2 {
1007    /// Accepted parameter names.
1008    #[serde(default)]
1009    pub accepted: Vec<String>,
1010}
1011
1012/// Full v2 capability block returned by `/models/catalog`.
1013#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1014pub struct RainyCapabilitiesV2 {
1015    /// Multimodal capabilities.
1016    pub multimodal: RainyMultimodalCapabilitiesV2,
1017    /// Reasoning capabilities.
1018    pub reasoning: RainyReasoningCapabilitiesV2,
1019    /// Parameter capabilities.
1020    pub parameters: RainyParametersCapabilitiesV2,
1021}
1022
1023/// Pricing metadata for model ranking helpers.
1024#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1025pub struct ModelPricing {
1026    /// Prompt token pricing.
1027    #[serde(skip_serializing_if = "Option::is_none")]
1028    pub prompt: Option<String>,
1029    /// Completion token pricing.
1030    #[serde(skip_serializing_if = "Option::is_none")]
1031    pub completion: Option<String>,
1032}
1033
1034/// Model entry returned by `/models/catalog`.
1035#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1036pub struct ModelCatalogItem {
1037    /// Unique model identifier.
1038    pub id: String,
1039    /// Human-readable model name.
1040    #[serde(skip_serializing_if = "Option::is_none")]
1041    pub name: Option<String>,
1042    /// Maximum context length in tokens.
1043    #[serde(skip_serializing_if = "Option::is_none")]
1044    pub context_length: Option<u32>,
1045    /// Model pricing information.
1046    #[serde(skip_serializing_if = "Option::is_none")]
1047    pub pricing: Option<ModelPricing>,
1048    /// Supported API parameters.
1049    #[serde(skip_serializing_if = "Option::is_none")]
1050    pub supported_parameters: Option<Vec<String>>,
1051    /// Model architecture metadata.
1052    #[serde(skip_serializing_if = "Option::is_none")]
1053    pub architecture: Option<ModelArchitecture>,
1054    /// Rainy capability hints (v1).
1055    #[serde(skip_serializing_if = "Option::is_none")]
1056    pub rainy_capabilities: Option<RainyCapabilities>,
1057    /// Rainy capability hints (v2).
1058    #[serde(skip_serializing_if = "Option::is_none")]
1059    pub rainy_capabilities_v2: Option<RainyCapabilitiesV2>,
1060    /// Additional model metadata.
1061    #[serde(flatten, default)]
1062    pub extra: HashMap<String, serde_json::Value>,
1063}
1064
1065/// Reasoning mode expected by the caller when selecting models.
1066#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1067#[serde(rename_all = "snake_case")]
1068pub enum ReasoningMode {
1069    /// Effort-based reasoning control.
1070    Effort,
1071    /// Thinking level-based reasoning control.
1072    ThinkingLevel,
1073    /// Thinking budget-based reasoning control.
1074    ThinkingBudget,
1075}
1076
1077/// Selector criteria for model discovery from `/models/catalog`.
1078#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1079pub struct ModelSelectionCriteria {
1080    /// Required input modalities.
1081    #[serde(default)]
1082    pub required_input_modalities: Vec<String>,
1083    /// Required output modalities.
1084    #[serde(default)]
1085    pub required_output_modalities: Vec<String>,
1086    /// Whether tool calling is required.
1087    #[serde(skip_serializing_if = "Option::is_none")]
1088    pub require_tools: Option<bool>,
1089    /// Whether structured output is required.
1090    #[serde(skip_serializing_if = "Option::is_none")]
1091    pub require_structured_output: Option<bool>,
1092    /// Required reasoning mode.
1093    #[serde(skip_serializing_if = "Option::is_none")]
1094    pub reasoning_mode: Option<ReasoningMode>,
1095    /// Reasoning value to match.
1096    #[serde(skip_serializing_if = "Option::is_none")]
1097    pub reasoning_value: Option<String>,
1098}
1099
1100/// Builder preference for reasoning payload generation.
1101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1102pub struct ReasoningPreference {
1103    /// Reasoning mode to use.
1104    pub mode: ReasoningMode,
1105    /// Reasoning value to apply.
1106    #[serde(skip_serializing_if = "Option::is_none")]
1107    pub value: Option<String>,
1108    /// Thinking budget value.
1109    #[serde(skip_serializing_if = "Option::is_none")]
1110    pub budget: Option<i32>,
1111}
1112
1113fn parse_price(value: Option<&str>) -> f64 {
1114    value
1115        .and_then(|raw| raw.parse::<f64>().ok())
1116        .filter(|v| v.is_finite())
1117        .unwrap_or(f64::MAX)
1118}
1119
1120fn has_required_modalities(available: &[String], required: &[String]) -> bool {
1121    if required.is_empty() {
1122        return true;
1123    }
1124
1125    required.iter().all(|modality| {
1126        available
1127            .iter()
1128            .any(|candidate| candidate.eq_ignore_ascii_case(modality))
1129    })
1130}
1131
1132fn supports_reasoning_preference(
1133    capabilities: &RainyCapabilitiesV2,
1134    mode: &ReasoningMode,
1135    reasoning_value: Option<&str>,
1136) -> bool {
1137    if !capabilities.reasoning.supported {
1138        return false;
1139    }
1140
1141    let controls = capabilities.reasoning.controls.as_ref();
1142    match mode {
1143        ReasoningMode::Effort => controls
1144            .map(|c| {
1145                c.reasoning_effort == Some(true) || c.effort.as_ref().is_some_and(|v| !v.is_empty())
1146            })
1147            .filter(|supported| *supported)
1148            .map(|_| {
1149                controls
1150                    .and_then(|c| c.effort.as_ref())
1151                    .map(|values| {
1152                        reasoning_value.is_none_or(|value| {
1153                            values
1154                                .iter()
1155                                .any(|candidate| candidate.eq_ignore_ascii_case(value))
1156                        })
1157                    })
1158                    .unwrap_or(reasoning_value.is_none())
1159            })
1160            .unwrap_or(false),
1161        ReasoningMode::ThinkingLevel => controls
1162            .and_then(|c| c.thinking_level.as_ref())
1163            .map(|values| {
1164                reasoning_value.is_none_or(|value| {
1165                    values
1166                        .iter()
1167                        .any(|candidate| candidate.eq_ignore_ascii_case(value))
1168                })
1169            })
1170            .unwrap_or(false),
1171        ReasoningMode::ThinkingBudget => {
1172            controls.and_then(|c| c.thinking_budget.as_ref()).is_some()
1173        }
1174    }
1175}
1176
1177fn catalog_item_supports(item: &ModelCatalogItem, parameter: &str) -> bool {
1178    if let Some(v2) = &item.rainy_capabilities_v2 {
1179        return v2
1180            .parameters
1181            .accepted
1182            .iter()
1183            .any(|candidate| candidate == parameter);
1184    }
1185
1186    item.supported_parameters
1187        .as_ref()
1188        .map(|params| params.iter().any(|candidate| candidate == parameter))
1189        .unwrap_or(false)
1190}
1191
1192/// Select models from catalog and rank by prompt price, completion price, then context length desc.
1193pub fn select_models(
1194    models: &[ModelCatalogItem],
1195    criteria: &ModelSelectionCriteria,
1196) -> Vec<ModelCatalogItem> {
1197    let required_inputs: Vec<String> = criteria
1198        .required_input_modalities
1199        .iter()
1200        .map(|v| v.to_lowercase())
1201        .collect();
1202    let required_outputs: Vec<String> = criteria
1203        .required_output_modalities
1204        .iter()
1205        .map(|v| v.to_lowercase())
1206        .collect();
1207
1208    let mut filtered: Vec<ModelCatalogItem> = models
1209        .iter()
1210        .filter(|item| {
1211            let Some(v2) = item.rainy_capabilities_v2.as_ref() else {
1212                return false;
1213            };
1214            let input: Vec<String> = v2
1215                .multimodal
1216                .input
1217                .iter()
1218                .map(|v| v.to_lowercase())
1219                .collect();
1220            let output: Vec<String> = v2
1221                .multimodal
1222                .output
1223                .iter()
1224                .map(|v| v.to_lowercase())
1225                .collect();
1226
1227            if !has_required_modalities(&input, &required_inputs) {
1228                return false;
1229            }
1230
1231            if !has_required_modalities(&output, &required_outputs) {
1232                return false;
1233            }
1234
1235            if criteria.require_tools == Some(true) && !catalog_item_supports(item, "tools") {
1236                return false;
1237            }
1238
1239            if criteria.require_structured_output == Some(true)
1240                && !catalog_item_supports(item, "response_format")
1241                && !catalog_item_supports(item, "structured_outputs")
1242            {
1243                return false;
1244            }
1245
1246            if let Some(mode) = &criteria.reasoning_mode {
1247                let reasoning_value = criteria.reasoning_value.as_deref();
1248                if !supports_reasoning_preference(v2, mode, reasoning_value) {
1249                    return false;
1250                }
1251            }
1252
1253            true
1254        })
1255        .cloned()
1256        .collect();
1257
1258    filtered.sort_by(|a, b| {
1259        let a_prompt = parse_price(a.pricing.as_ref().and_then(|p| p.prompt.as_deref()));
1260        let b_prompt = parse_price(b.pricing.as_ref().and_then(|p| p.prompt.as_deref()));
1261        let prompt_cmp = a_prompt.partial_cmp(&b_prompt).unwrap_or(Ordering::Equal);
1262        if prompt_cmp != Ordering::Equal {
1263            return prompt_cmp;
1264        }
1265
1266        let a_completion = parse_price(a.pricing.as_ref().and_then(|p| p.completion.as_deref()));
1267        let b_completion = parse_price(b.pricing.as_ref().and_then(|p| p.completion.as_deref()));
1268        let completion_cmp = a_completion
1269            .partial_cmp(&b_completion)
1270            .unwrap_or(Ordering::Equal);
1271        if completion_cmp != Ordering::Equal {
1272            return completion_cmp;
1273        }
1274
1275        let a_context = a.context_length.unwrap_or_default();
1276        let b_context = b.context_length.unwrap_or_default();
1277        b_context.cmp(&a_context)
1278    });
1279
1280    filtered
1281}
1282
1283/// Build provider-aware reasoning payload from `rainy_capabilities_v2`.
1284pub fn build_reasoning_config(
1285    model: &ModelCatalogItem,
1286    preference: &ReasoningPreference,
1287) -> Option<serde_json::Value> {
1288    let v2 = model.rainy_capabilities_v2.as_ref()?;
1289    if !v2.reasoning.supported {
1290        return None;
1291    }
1292
1293    let profiles = &v2.reasoning.profiles;
1294    let controls = v2.reasoning.controls.as_ref();
1295    match preference.mode {
1296        ReasoningMode::Effort => {
1297            let value = preference.value.clone()?;
1298            let supports_effort = controls
1299                .map(|c| {
1300                    c.reasoning_effort == Some(true)
1301                        || c.effort.as_ref().is_some_and(|v| !v.is_empty())
1302                })
1303                .unwrap_or(false);
1304            if !supports_effort {
1305                return None;
1306            }
1307            if let Some(efforts) = controls.and_then(|c| c.effort.as_ref()) {
1308                if !efforts.iter().any(|v| v.eq_ignore_ascii_case(&value)) {
1309                    return None;
1310                }
1311            }
1312
1313            let effort_profile = profiles
1314                .iter()
1315                .find(|p| p.parameter_path == "reasoning.effort")?;
1316            match effort_profile.parameter_path.as_str() {
1317                "reasoning.effort" => Some(serde_json::json!({
1318                    "reasoning": { "effort": value }
1319                })),
1320                _ => None,
1321            }
1322        }
1323        ReasoningMode::ThinkingLevel => {
1324            let value = preference.value.clone()?;
1325            let supports = controls
1326                .and_then(|c| c.thinking_level.as_ref())
1327                .map(|levels| levels.iter().any(|v| v.eq_ignore_ascii_case(&value)))
1328                .unwrap_or(false);
1329            if !supports {
1330                return None;
1331            }
1332            let level_profile = profiles
1333                .iter()
1334                .find(|p| p.parameter_path == "thinking_config.thinking_level")?;
1335            if let Some(values) = &level_profile.values {
1336                if !values.iter().any(|v| v.eq_ignore_ascii_case(&value)) {
1337                    return None;
1338                }
1339            }
1340            Some(serde_json::json!({
1341                "thinking_config": { "thinking_level": value }
1342            }))
1343        }
1344        ReasoningMode::ThinkingBudget => {
1345            let budget = preference.budget?;
1346            let supports = controls.and_then(|c| c.thinking_budget.as_ref())?;
1347            if budget < supports.min || budget > supports.max {
1348                return None;
1349            }
1350            let budget_profile = profiles.iter().find(|p| {
1351                p.parameter_path == "thinking.budget_tokens"
1352                    || p.parameter_path == "thinking_config.thinking_budget"
1353            })?;
1354
1355            if budget_profile.parameter_path == "thinking.budget_tokens" {
1356                return Some(serde_json::json!({
1357                    "thinking": { "budget_tokens": budget }
1358                }));
1359            }
1360            if budget_profile.parameter_path == "thinking_config.thinking_budget" {
1361                return Some(serde_json::json!({
1362                "thinking_config": { "thinking_budget": budget }
1363                }));
1364            }
1365            None
1366        }
1367    }
1368}
1369
1370/// A collection of predefined model constants for convenience.
1371/// All models listed here are confirmed to be 100% OpenAI-compatible without parameter adaptations.
1372pub mod model_constants {
1373    // OpenAI models (fully compatible)
1374    /// Constant for the GPT-4o model.
1375    pub const OPENAI_GPT_4O: &str = "gpt-4o";
1376    /// Constant for the GPT-5 model.
1377    pub const OPENAI_GPT_5: &str = "gpt-5";
1378    /// Constant for the GPT-5 Pro model.
1379    pub const OPENAI_GPT_5_PRO: &str = "gpt-5-pro";
1380    /// Constant for the O3 model.
1381    pub const OPENAI_O3: &str = "o3";
1382    /// Constant for the O4 Mini model.
1383    pub const OPENAI_O4_MINI: &str = "o4-mini";
1384
1385    // Google Gemini models (fully compatible via official compatibility layer)
1386    /// Constant for the Gemini 2.5 Pro model.
1387    pub const GOOGLE_GEMINI_2_5_PRO: &str = "gemini-2.5-pro";
1388    /// Constant for the Gemini 2.5 Flash model.
1389    pub const GOOGLE_GEMINI_2_5_FLASH: &str = "gemini-2.5-flash";
1390    /// Constant for the Gemini 2.5 Flash Lite model.
1391    pub const GOOGLE_GEMINI_2_5_FLASH_LITE: &str = "gemini-2.5-flash-lite";
1392
1393    // Gemini 3 series - Advanced reasoning models with thinking capabilities
1394    /// Constant for the Gemini 3 Pro model with advanced reasoning.
1395    pub const GOOGLE_GEMINI_3_PRO: &str = "gemini-3-pro-preview";
1396    /// Constant for the Gemini 3 Flash model with thinking capabilities.
1397    pub const GOOGLE_GEMINI_3_FLASH: &str = "gemini-3-flash-preview";
1398    /// Constant for the Gemini 3 Pro Image model with multimodal reasoning.
1399    pub const GOOGLE_GEMINI_3_PRO_IMAGE: &str = "gemini-3-pro-image-preview";
1400
1401    // Groq models (fully compatible)
1402    /// Constant for the Llama 3.1 8B Instant model.
1403    pub const GROQ_LLAMA_3_1_8B_INSTANT: &str = "llama-3.1-8b-instant";
1404    /// Constant for the Llama 3.3 70B Versatile model.
1405    pub const GROQ_LLAMA_3_3_70B_VERSATILE: &str = "llama-3.3-70b-versatile";
1406    /// Constant for the moonshotai/kimi-k2-instruct-0905 Instant model.
1407    pub const KIMI_K2_0925: &str = "moonshotai/kimi-k2-instruct-0905";
1408
1409    // Cerebras models (fully compatible)
1410    /// Constant for the Llama3.1 8B model.
1411    pub const CEREBRAS_LLAMA3_1_8B: &str = "cerebras/llama3.1-8b";
1412
1413    // Enosis Labs models (fully compatible)
1414    /// Constant for the Astronomer 1 model.
1415    pub const ASTRONOMER_1: &str = "astronomer-1";
1416    /// Constant for the Astronomer 1 Max model.
1417    pub const ASTRONOMER_1_MAX: &str = "astronomer-1-max";
1418    /// Constant for the Astronomer 1.5 model.
1419    pub const ASTRONOMER_1_5: &str = "astronomer-1.5";
1420    /// Constant for the Astronomer 2 model.
1421    pub const ASTRONOMER_2: &str = "astronomer-2";
1422    /// Constant for the Astronomer 2 Pro model.
1423    pub const ASTRONOMER_2_PRO: &str = "astronomer-2-pro";
1424
1425    // Legacy aliases for backward compatibility (deprecated - use provider-prefixed versions above)
1426    /// Legacy constant for the GPT-4o model (use OPENAI_GPT_4O instead).
1427    #[deprecated(note = "Use OPENAI_GPT_4O instead for OpenAI compatibility")]
1428    pub const GPT_4O: &str = "openai/gpt-4o";
1429    /// Legacy constant for the GPT-5 model (use OPENAI_GPT_5 instead).
1430    #[deprecated(note = "Use OPENAI_GPT_5 instead for OpenAI compatibility")]
1431    pub const GPT_5: &str = "openai/gpt-5";
1432    /// Legacy constant for the Gemini 2.5 Pro model (use GOOGLE_GEMINI_2_5_PRO instead).
1433    #[deprecated(note = "Use GOOGLE_GEMINI_2_5_PRO instead for OpenAI compatibility")]
1434    pub const GEMINI_2_5_PRO: &str = "google/gemini-2.5-pro";
1435    /// Legacy constant for the Gemini 2.5 Flash model (use GOOGLE_GEMINI_2_5_FLASH instead).
1436    #[deprecated(note = "Use GOOGLE_GEMINI_2_5_FLASH instead for OpenAI compatibility")]
1437    pub const GEMINI_2_5_FLASH: &str = "google/gemini-2.5-flash";
1438    /// Legacy constant for the Gemini 2.5 Flash Lite model (use GOOGLE_GEMINI_2_5_FLASH_LITE instead).
1439    #[deprecated(note = "Use GOOGLE_GEMINI_2_5_FLASH_LITE instead for OpenAI compatibility")]
1440    pub const GEMINI_2_5_FLASH_LITE: &str = "google/gemini-2.5-flash-lite";
1441    /// Legacy constant for the Llama 3.1 8B Instant model (use GROQ_LLAMA_3_1_8B_INSTANT instead).
1442    #[deprecated(note = "Use GROQ_LLAMA_3_1_8B_INSTANT instead for OpenAI compatibility")]
1443    pub const LLAMA_3_1_8B_INSTANT: &str = "groq/llama-3.1-8b-instant";
1444    /// Legacy constant for the Llama3.1 8B model (use CEREBRAS_LLAMA3_1_8B instead).
1445    #[deprecated(note = "Use CEREBRAS_LLAMA3_1_8B instead for OpenAI compatibility")]
1446    pub const LLAMA3_1_8B: &str = "cerebras/llama3.1-8b";
1447}
1448
1449/// A collection of predefined provider name constants for convenience.
1450pub mod providers {
1451    /// Constant for the OpenAI provider.
1452    pub const OPENAI: &str = "openai";
1453    /// Constant for the Anthropic provider.
1454    pub const ANTHROPIC: &str = "anthropic";
1455    /// Constant for the Groq provider.
1456    pub const GROQ: &str = "groq";
1457    /// Constant for the Cerebras provider.
1458    pub const CEREBRAS: &str = "cerebras";
1459    /// Constant for the Gemini provider.
1460    pub const GEMINI: &str = "gemini";
1461    /// Constant for the Enosis Labs provider.
1462    pub const ENOSISLABS: &str = "enosislabs";
1463}
1464
1465impl ChatCompletionRequest {
1466    /// Creates a new `ChatCompletionRequest` with the given model and messages.
1467    ///
1468    /// # Arguments
1469    ///
1470    /// * `model` - The identifier of the model to use.
1471    /// * `messages` - The list of messages for the conversation.
1472    pub fn new(model: impl Into<String>, messages: Vec<ChatMessage>) -> Self {
1473        Self {
1474            model: model.into(),
1475            messages,
1476            temperature: None,
1477            max_tokens: None,
1478            top_p: None,
1479            frequency_penalty: None,
1480            presence_penalty: None,
1481            stop: None,
1482            user: None,
1483            provider: None,
1484            stream: None,
1485            logit_bias: None,
1486            logprobs: None,
1487            top_logprobs: None,
1488            n: None,
1489            response_format: None,
1490            tools: None,
1491            tool_choice: None,
1492            thinking_config: None,
1493        }
1494    }
1495
1496    /// Sets the temperature for the chat completion.
1497    ///
1498    /// The temperature is clamped between 0.0 and 2.0.
1499    ///
1500    /// # Arguments
1501    ///
1502    /// * `temperature` - The sampling temperature.
1503    pub fn with_temperature(mut self, temperature: f32) -> Self {
1504        self.temperature = Some(temperature.clamp(0.0, 2.0));
1505        self
1506    }
1507
1508    /// Sets the maximum number of tokens to generate.
1509    ///
1510    /// # Arguments
1511    ///
1512    /// * `max_tokens` - The maximum number of tokens.
1513    pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
1514        self.max_tokens = Some(max_tokens);
1515        self
1516    }
1517
1518    /// Sets the user identifier for the chat completion.
1519    ///
1520    /// # Arguments
1521    ///
1522    /// * `user` - A unique identifier for the end-user.
1523    pub fn with_user(mut self, user: impl Into<String>) -> Self {
1524        self.user = Some(user.into());
1525        self
1526    }
1527
1528    /// Sets a provider hint for the request.
1529    ///
1530    /// # Arguments
1531    ///
1532    /// * `provider` - The name of the provider to use.
1533    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
1534        self.provider = Some(provider.into());
1535        self
1536    }
1537
1538    /// Enables or disables streaming for the response.
1539    ///
1540    /// # Arguments
1541    ///
1542    /// * `stream` - `true` to enable streaming, `false` to disable.
1543    pub fn with_stream(mut self, stream: bool) -> Self {
1544        self.stream = Some(stream);
1545        self
1546    }
1547
1548    /// Sets the logit bias for the chat completion.
1549    ///
1550    /// # Arguments
1551    ///
1552    /// * `logit_bias` - A map of token IDs to bias values.
1553    pub fn with_logit_bias(mut self, logit_bias: serde_json::Value) -> Self {
1554        self.logit_bias = Some(logit_bias);
1555        self
1556    }
1557
1558    /// Enables or disables log probabilities for the response.
1559    ///
1560    /// # Arguments
1561    ///
1562    /// * `logprobs` - `true` to include log probabilities.
1563    pub fn with_logprobs(mut self, logprobs: bool) -> Self {
1564        self.logprobs = Some(logprobs);
1565        self
1566    }
1567
1568    /// Sets the number of most likely tokens to return at each position.
1569    ///
1570    /// # Arguments
1571    ///
1572    /// * `top_logprobs` - The number of top log probabilities to return.
1573    pub fn with_top_logprobs(mut self, top_logprobs: u32) -> Self {
1574        self.top_logprobs = Some(top_logprobs);
1575        self
1576    }
1577
1578    /// Sets the number of chat completion choices to generate.
1579    ///
1580    /// # Arguments
1581    ///
1582    /// * `n` - The number of completions to generate.
1583    pub fn with_n(mut self, n: u32) -> Self {
1584        self.n = Some(n);
1585        self
1586    }
1587
1588    /// Sets the response format for the chat completion.
1589    ///
1590    /// # Arguments
1591    ///
1592    /// * `response_format` - The format the model must output.
1593    pub fn with_response_format(mut self, response_format: ResponseFormat) -> Self {
1594        self.response_format = Some(response_format);
1595        self
1596    }
1597
1598    /// Sets the tools available to the model.
1599    ///
1600    /// # Arguments
1601    ///
1602    /// * `tools` - A list of tools the model can use.
1603    pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
1604        self.tools = Some(tools);
1605        self
1606    }
1607
1608    /// Sets the tool choice for the chat completion.
1609    ///
1610    /// # Arguments
1611    ///
1612    /// * `tool_choice` - Controls which tool the model uses.
1613    pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
1614        self.tool_choice = Some(tool_choice);
1615        self
1616    }
1617
1618    /// Sets the thinking configuration for Gemini 3 and 2.5 series models.
1619    ///
1620    /// # Arguments
1621    ///
1622    /// * `thinking_config` - Configuration for thinking capabilities.
1623    pub fn with_thinking_config(mut self, thinking_config: ThinkingConfig) -> Self {
1624        self.thinking_config = Some(thinking_config);
1625        self
1626    }
1627
1628    /// Enables thought summaries in the response (Gemini 3 and 2.5 series).
1629    ///
1630    /// # Arguments
1631    ///
1632    /// * `include_thoughts` - Whether to include thought summaries.
1633    pub fn with_include_thoughts(mut self, include_thoughts: bool) -> Self {
1634        let mut config = self.thinking_config.unwrap_or_default();
1635        config.include_thoughts = Some(include_thoughts);
1636        self.thinking_config = Some(config);
1637        self
1638    }
1639
1640    /// Sets the thinking level for Gemini 3 models.
1641    ///
1642    /// # Arguments
1643    ///
1644    /// * `thinking_level` - The thinking level (minimal, low, medium, high).
1645    pub fn with_thinking_level(mut self, thinking_level: ThinkingLevel) -> Self {
1646        let mut config = self.thinking_config.unwrap_or_default();
1647        config.thinking_level = Some(thinking_level);
1648        self.thinking_config = Some(config);
1649        self
1650    }
1651
1652    /// Sets the thinking budget for Gemini 2.5 models.
1653    ///
1654    /// # Arguments
1655    ///
1656    /// * `thinking_budget` - Number of thinking tokens (-1 for dynamic, 0 to disable).
1657    pub fn with_thinking_budget(mut self, thinking_budget: i32) -> Self {
1658        let mut config = self.thinking_config.unwrap_or_default();
1659        config.thinking_budget = Some(thinking_budget);
1660        self.thinking_config = Some(config);
1661        self
1662    }
1663
1664    /// Validates that the request parameters are compatible with OpenAI standards.
1665    ///
1666    /// This method checks parameter ranges and values to ensure they match OpenAI's API specifications.
1667    /// Also validates Gemini 3 specific parameters like thinking configuration.
1668    ///
1669    /// # Returns
1670    ///
1671    /// A `Result` indicating whether the request is valid for OpenAI compatibility.
1672    pub fn validate_openai_compatibility(&self) -> Result<(), String> {
1673        // Validate temperature
1674        if let Some(temp) = self.temperature {
1675            if !(0.0..=2.0).contains(&temp) {
1676                return Err(format!(
1677                    "Temperature must be between 0.0 and 2.0, got {}",
1678                    temp
1679                ));
1680            }
1681        }
1682
1683        // Validate top_p
1684        if let Some(top_p) = self.top_p {
1685            if !(0.0..=1.0).contains(&top_p) {
1686                return Err(format!("Top-p must be between 0.0 and 1.0, got {}", top_p));
1687            }
1688        }
1689
1690        // Validate frequency_penalty
1691        if let Some(fp) = self.frequency_penalty {
1692            if !(-2.0..=2.0).contains(&fp) {
1693                return Err(format!(
1694                    "Frequency penalty must be between -2.0 and 2.0, got {}",
1695                    fp
1696                ));
1697            }
1698        }
1699
1700        // Validate presence_penalty
1701        if let Some(pp) = self.presence_penalty {
1702            if !(-2.0..=2.0).contains(&pp) {
1703                return Err(format!(
1704                    "Presence penalty must be between -2.0 and 2.0, got {}",
1705                    pp
1706                ));
1707            }
1708        }
1709
1710        // Validate max_tokens
1711        if let Some(mt) = self.max_tokens {
1712            if mt == 0 {
1713                return Err("Max tokens must be greater than 0".to_string());
1714            }
1715        }
1716
1717        // Validate top_logprobs
1718        if let Some(tlp) = self.top_logprobs {
1719            if !(0..=20).contains(&tlp) {
1720                return Err(format!(
1721                    "Top logprobs must be between 0 and 20, got {}",
1722                    tlp
1723                ));
1724            }
1725        }
1726
1727        // Validate n
1728        if let Some(n) = self.n {
1729            if n == 0 {
1730                return Err("n must be greater than 0".to_string());
1731            }
1732        }
1733
1734        // Validate stop sequences
1735        if let Some(stop) = &self.stop {
1736            if stop.len() > 4 {
1737                return Err("Cannot have more than 4 stop sequences".to_string());
1738            }
1739            for seq in stop {
1740                if seq.is_empty() {
1741                    return Err("Stop sequences cannot be empty".to_string());
1742                }
1743                if seq.len() > 64 {
1744                    return Err("Stop sequences cannot be longer than 64 characters".to_string());
1745                }
1746            }
1747        }
1748
1749        // Validate thinking configuration for Gemini models
1750        if let Some(thinking_config) = &self.thinking_config {
1751            self.validate_thinking_config(thinking_config)?;
1752        }
1753
1754        Ok(())
1755    }
1756
1757    /// Validates thinking configuration parameters for Gemini models.
1758    fn validate_thinking_config(&self, config: &ThinkingConfig) -> Result<(), String> {
1759        let is_gemini_3 = self.model.contains("gemini-3");
1760        let is_gemini_2_5 = self.model.contains("gemini-2.5");
1761        let is_gemini_3_pro = self.model.contains("gemini-3-pro");
1762
1763        // Validate thinking level (Gemini 3 only)
1764        if let Some(level) = &config.thinking_level {
1765            if !is_gemini_3 {
1766                return Err("thinking_level is only supported for Gemini 3 models".to_string());
1767            }
1768
1769            match level {
1770                ThinkingLevel::Minimal | ThinkingLevel::Medium => {
1771                    if is_gemini_3_pro {
1772                        return Err(
1773                            "Gemini 3 Pro only supports 'low' and 'high' thinking levels"
1774                                .to_string(),
1775                        );
1776                    }
1777                }
1778                _ => {}
1779            }
1780        }
1781
1782        // Validate thinking budget (Gemini 2.5 only)
1783        if let Some(budget) = config.thinking_budget {
1784            if !is_gemini_2_5 {
1785                return Err("thinking_budget is only supported for Gemini 2.5 models".to_string());
1786            }
1787
1788            // Validate budget ranges based on model
1789            if self.model.contains("2.5-pro") {
1790                if budget != -1 && !(128..=32768).contains(&budget) {
1791                    return Err(
1792                        "Gemini 2.5 Pro thinking budget must be -1 (dynamic) or between 128-32768"
1793                            .to_string(),
1794                    );
1795                }
1796            } else if self.model.contains("2.5-flash")
1797                && budget != -1
1798                && !(0..=24576).contains(&budget)
1799            {
1800                return Err(
1801                    "Gemini 2.5 Flash thinking budget must be -1 (dynamic) or between 0-24576"
1802                        .to_string(),
1803                );
1804            }
1805        }
1806
1807        // Warn about conflicting parameters
1808        if config.thinking_level.is_some() && config.thinking_budget.is_some() {
1809            return Err("Cannot specify both thinking_level (Gemini 3) and thinking_budget (Gemini 2.5) in the same request".to_string());
1810        }
1811
1812        Ok(())
1813    }
1814
1815    /// Checks if the model supports thinking capabilities.
1816    pub fn supports_thinking(&self) -> bool {
1817        self.model.contains("gemini-3") || self.model.contains("gemini-2.5")
1818    }
1819
1820    /// Checks if the model requires thought signatures for function calling.
1821    pub fn requires_thought_signatures(&self) -> bool {
1822        self.model.contains("gemini-3")
1823    }
1824}
1825
1826impl OpenAIChatCompletionRequest {
1827    /// Creates a new OpenAI-compatible chat completion request.
1828    pub fn new(model: impl Into<String>, messages: Vec<OpenAIChatMessage>) -> Self {
1829        Self {
1830            model: model.into(),
1831            messages,
1832            temperature: None,
1833            max_tokens: None,
1834            top_p: None,
1835            frequency_penalty: None,
1836            presence_penalty: None,
1837            stop: None,
1838            user: None,
1839            provider: None,
1840            stream: None,
1841            logit_bias: None,
1842            logprobs: None,
1843            top_logprobs: None,
1844            n: None,
1845            response_format: None,
1846            tools: None,
1847            tool_choice: None,
1848            thinking_config: None,
1849            thinking: None,
1850        }
1851    }
1852
1853    /// Sets the sampling temperature.
1854    pub fn with_temperature(mut self, temperature: f32) -> Self {
1855        self.temperature = Some(temperature.clamp(0.0, 2.0));
1856        self
1857    }
1858
1859    /// Sets the maximum number of tokens to generate.
1860    pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
1861        self.max_tokens = Some(max_tokens);
1862        self
1863    }
1864
1865    /// Sets the end-user identifier.
1866    pub fn with_user(mut self, user: impl Into<String>) -> Self {
1867        self.user = Some(user.into());
1868        self
1869    }
1870
1871    /// Sets a provider hint.
1872    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
1873        self.provider = Some(provider.into());
1874        self
1875    }
1876
1877    /// Enables or disables streaming.
1878    pub fn with_stream(mut self, stream: bool) -> Self {
1879        self.stream = Some(stream);
1880        self
1881    }
1882
1883    /// Sets nucleus sampling.
1884    pub fn with_top_p(mut self, top_p: f32) -> Self {
1885        self.top_p = Some(top_p.clamp(0.0, 1.0));
1886        self
1887    }
1888
1889    /// Sets frequency penalty.
1890    pub fn with_frequency_penalty(mut self, frequency_penalty: f32) -> Self {
1891        self.frequency_penalty = Some(frequency_penalty.clamp(-2.0, 2.0));
1892        self
1893    }
1894
1895    /// Sets presence penalty.
1896    pub fn with_presence_penalty(mut self, presence_penalty: f32) -> Self {
1897        self.presence_penalty = Some(presence_penalty.clamp(-2.0, 2.0));
1898        self
1899    }
1900
1901    /// Sets stop sequences.
1902    pub fn with_stop(mut self, stop: Vec<String>) -> Self {
1903        self.stop = Some(stop);
1904        self
1905    }
1906
1907    /// Sets logit bias.
1908    pub fn with_logit_bias(mut self, logit_bias: serde_json::Value) -> Self {
1909        self.logit_bias = Some(logit_bias);
1910        self
1911    }
1912
1913    /// Enables or disables log probabilities.
1914    pub fn with_logprobs(mut self, logprobs: bool) -> Self {
1915        self.logprobs = Some(logprobs);
1916        self
1917    }
1918
1919    /// Sets the top log probabilities count.
1920    pub fn with_top_logprobs(mut self, top_logprobs: u32) -> Self {
1921        self.top_logprobs = Some(top_logprobs);
1922        self
1923    }
1924
1925    /// Sets the number of choices to generate.
1926    pub fn with_n(mut self, n: u32) -> Self {
1927        self.n = Some(n);
1928        self
1929    }
1930
1931    /// Sets the response format.
1932    pub fn with_response_format(mut self, response_format: ResponseFormat) -> Self {
1933        self.response_format = Some(response_format);
1934        self
1935    }
1936
1937    /// Sets the available tools.
1938    pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
1939        self.tools = Some(tools);
1940        self
1941    }
1942
1943    /// Sets the tool choice strategy.
1944    pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
1945        self.tool_choice = Some(tool_choice);
1946        self
1947    }
1948
1949    /// Sets the Gemini thinking configuration.
1950    pub fn with_thinking_config(mut self, thinking_config: ThinkingConfig) -> Self {
1951        self.thinking_config = Some(thinking_config);
1952        self
1953    }
1954
1955    /// Enables or disables thought summaries.
1956    pub fn with_include_thoughts(mut self, include_thoughts: bool) -> Self {
1957        let mut config = self.thinking_config.unwrap_or_default();
1958        config.include_thoughts = Some(include_thoughts);
1959        self.thinking_config = Some(config);
1960        self
1961    }
1962
1963    /// Sets the Gemini 3 thinking level.
1964    pub fn with_thinking_level(mut self, thinking_level: ThinkingLevel) -> Self {
1965        let mut config = self.thinking_config.unwrap_or_default();
1966        config.thinking_level = Some(thinking_level);
1967        self.thinking_config = Some(config);
1968        self
1969    }
1970
1971    /// Sets the Gemini 2.5 thinking budget.
1972    pub fn with_thinking_budget(mut self, thinking_budget: i32) -> Self {
1973        let mut config = self.thinking_config.unwrap_or_default();
1974        config.thinking_budget = Some(thinking_budget);
1975        self.thinking_config = Some(config);
1976        self
1977    }
1978
1979    /// Sets Anthropic extended-thinking configuration.
1980    ///
1981    /// Serialised as `thinking: {"type":"enabled","budget_tokens":N}` — the format
1982    /// expected by Anthropic's API via OpenRouter/Rainy API.
1983    pub fn with_anthropic_thinking(mut self, budget_tokens: i32) -> Self {
1984        self.thinking =
1985            Some(serde_json::json!({"type": "enabled", "budget_tokens": budget_tokens}));
1986        self
1987    }
1988
1989    /// Validates compatibility using the same parameter rules as the simple chat request.
1990    pub fn validate_openai_compatibility(&self) -> Result<(), String> {
1991        ChatCompletionRequest {
1992            model: self.model.clone(),
1993            messages: vec![],
1994            temperature: self.temperature,
1995            max_tokens: self.max_tokens,
1996            top_p: self.top_p,
1997            frequency_penalty: self.frequency_penalty,
1998            presence_penalty: self.presence_penalty,
1999            stop: self.stop.clone(),
2000            user: self.user.clone(),
2001            provider: self.provider.clone(),
2002            stream: self.stream,
2003            logit_bias: self.logit_bias.clone(),
2004            logprobs: self.logprobs,
2005            top_logprobs: self.top_logprobs,
2006            n: self.n,
2007            response_format: self.response_format.clone(),
2008            tools: self.tools.clone(),
2009            tool_choice: self.tool_choice.clone(),
2010            thinking_config: self.thinking_config.clone(),
2011        }
2012        .validate_openai_compatibility()
2013    }
2014
2015    /// Checks whether the selected model supports thinking features.
2016    pub fn supports_thinking(&self) -> bool {
2017        self.model.contains("gemini-3") || self.model.contains("gemini-2.5")
2018    }
2019
2020    /// Checks whether the selected model requires thought signatures for function calling.
2021    pub fn requires_thought_signatures(&self) -> bool {
2022        self.model.contains("gemini-3")
2023    }
2024}
2025
2026impl ChatMessage {
2027    /// Creates a new message with the `System` role.
2028    ///
2029    /// # Arguments
2030    ///
2031    /// * `content` - The content of the system message.
2032    pub fn system(content: impl Into<String>) -> Self {
2033        Self {
2034            role: MessageRole::System,
2035            content: content.into(),
2036        }
2037    }
2038
2039    /// Creates a new message with the `User` role.
2040    ///
2041    /// # Arguments
2042    ///
2043    /// * `content` - The content of the user message.
2044    pub fn user(content: impl Into<String>) -> Self {
2045        Self {
2046            role: MessageRole::User,
2047            content: content.into(),
2048        }
2049    }
2050
2051    /// Creates a new message with the `Assistant` role.
2052    ///
2053    /// # Arguments
2054    ///
2055    /// * `content` - The content of the assistant message.
2056    pub fn assistant(content: impl Into<String>) -> Self {
2057        Self {
2058            role: MessageRole::Assistant,
2059            content: content.into(),
2060        }
2061    }
2062}
2063
2064impl OpenAIMessageContent {
2065    /// Creates text content.
2066    pub fn text(content: impl Into<String>) -> Self {
2067        Self::Text(content.into())
2068    }
2069
2070    /// Creates multimodal content parts.
2071    pub fn parts(parts: Vec<OpenAIContentPart>) -> Self {
2072        Self::Parts(parts)
2073    }
2074}
2075
2076impl OpenAIContentPart {
2077    /// Creates a text content part.
2078    pub fn text(content: impl Into<String>) -> Self {
2079        Self::Text {
2080            text: content.into(),
2081        }
2082    }
2083
2084    /// Creates an image URL part.
2085    pub fn image_url(url: impl Into<String>) -> Self {
2086        Self::ImageUrl {
2087            image_url: OpenAIImageUrl {
2088                url: url.into(),
2089                detail: None,
2090            },
2091        }
2092    }
2093
2094    /// Creates an image URL part with a specific detail hint.
2095    pub fn image_url_with_detail(url: impl Into<String>, detail: impl Into<String>) -> Self {
2096        Self::ImageUrl {
2097            image_url: OpenAIImageUrl {
2098                url: url.into(),
2099                detail: Some(detail.into()),
2100            },
2101        }
2102    }
2103}
2104
2105impl OpenAIChatMessage {
2106    /// Creates a new system message.
2107    pub fn system(content: impl Into<OpenAIMessageContent>) -> Self {
2108        Self {
2109            role: OpenAIMessageRole::System,
2110            content: Some(content.into()),
2111            name: None,
2112            tool_calls: None,
2113            tool_call_id: None,
2114        }
2115    }
2116
2117    /// Creates a new user message.
2118    pub fn user(content: impl Into<OpenAIMessageContent>) -> Self {
2119        Self {
2120            role: OpenAIMessageRole::User,
2121            content: Some(content.into()),
2122            name: None,
2123            tool_calls: None,
2124            tool_call_id: None,
2125        }
2126    }
2127
2128    /// Creates a new assistant message.
2129    pub fn assistant(content: impl Into<OpenAIMessageContent>) -> Self {
2130        Self {
2131            role: OpenAIMessageRole::Assistant,
2132            content: Some(content.into()),
2133            name: None,
2134            tool_calls: None,
2135            tool_call_id: None,
2136        }
2137    }
2138
2139    /// Creates an assistant message that only carries tool calls.
2140    pub fn assistant_with_tool_calls(tool_calls: Vec<OpenAIToolCall>) -> Self {
2141        Self {
2142            role: OpenAIMessageRole::Assistant,
2143            content: None,
2144            name: None,
2145            tool_calls: Some(tool_calls),
2146            tool_call_id: None,
2147        }
2148    }
2149
2150    /// Creates a tool result message.
2151    pub fn tool(tool_call_id: impl Into<String>, content: impl Into<OpenAIMessageContent>) -> Self {
2152        Self {
2153            role: OpenAIMessageRole::Tool,
2154            content: Some(content.into()),
2155            name: None,
2156            tool_calls: None,
2157            tool_call_id: Some(tool_call_id.into()),
2158        }
2159    }
2160
2161    /// Creates a message with full control over optional OpenAI-compatible fields.
2162    pub fn with_parts(
2163        role: OpenAIMessageRole,
2164        content: Option<OpenAIMessageContent>,
2165        tool_calls: Option<Vec<OpenAIToolCall>>,
2166        tool_call_id: Option<String>,
2167    ) -> Self {
2168        Self {
2169            role,
2170            content,
2171            name: None,
2172            tool_calls,
2173            tool_call_id,
2174        }
2175    }
2176}
2177
2178impl From<String> for OpenAIMessageContent {
2179    fn from(value: String) -> Self {
2180        Self::Text(value)
2181    }
2182}
2183
2184impl From<&str> for OpenAIMessageContent {
2185    fn from(value: &str) -> Self {
2186        Self::Text(value.to_string())
2187    }
2188}
2189
2190// Legacy compatibility types - keep existing types for backward compatibility
2191use uuid::Uuid;
2192
2193/// Represents a user account (legacy).
2194#[derive(Debug, Clone, Serialize, Deserialize)]
2195pub struct User {
2196    /// The unique ID of the user.
2197    pub id: Uuid,
2198    /// The user's identifier string.
2199    pub user_id: String,
2200    /// The name of the user's subscription plan.
2201    pub plan_name: String,
2202    /// The user's current credit balance.
2203    pub current_credits: f64,
2204    /// The amount of credits the user has used in the current month.
2205    pub credits_used_this_month: f64,
2206    /// The date when the user's credits will reset.
2207    pub credits_reset_date: DateTime<Utc>,
2208    /// Indicates if the user account is active.
2209    pub is_active: bool,
2210    /// The timestamp of when the user account was created.
2211    pub created_at: DateTime<Utc>,
2212}
2213
2214/// Represents an API key (legacy).
2215#[derive(Debug, Clone, Serialize, Deserialize)]
2216pub struct ApiKey {
2217    /// The unique ID of the API key.
2218    pub id: Uuid,
2219    /// The API key string.
2220    pub key: String,
2221    /// The ID of the user who owns the key.
2222    pub owner_id: Uuid,
2223    /// Indicates if the API key is active.
2224    pub is_active: bool,
2225    /// The timestamp of when the key was created.
2226    pub created_at: DateTime<Utc>,
2227    /// The expiration date of the key, if any.
2228    pub expires_at: Option<DateTime<Utc>>,
2229    /// A description of the key.
2230    pub description: Option<String>,
2231    /// The timestamp of when the key was last used.
2232    pub last_used_at: Option<DateTime<Utc>>,
2233}
2234
2235/// Represents usage statistics over a period (legacy).
2236#[derive(Debug, Clone, Serialize, Deserialize)]
2237pub struct UsageStats {
2238    /// The number of days in the usage period.
2239    pub period_days: u32,
2240    /// A list of daily usage data.
2241    pub daily_usage: Vec<DailyUsage>,
2242    /// A list of recent credit transactions.
2243    pub recent_transactions: Vec<CreditTransaction>,
2244    /// The total number of requests made in the period.
2245    pub total_requests: u64,
2246    /// The total number of tokens used in the period.
2247    pub total_tokens: u64,
2248}
2249
2250/// Represents usage data for a single day (legacy).
2251#[derive(Debug, Clone, Serialize, Deserialize)]
2252pub struct DailyUsage {
2253    /// The date for the usage data.
2254    pub date: String,
2255    /// The number of credits used on this day.
2256    pub credits_used: f64,
2257    /// The number of requests made on this day.
2258    pub requests: u64,
2259    /// The number of tokens used on this day.
2260    pub tokens: u64,
2261}
2262
2263/// Represents a single credit transaction (legacy).
2264#[derive(Debug, Clone, Serialize, Deserialize)]
2265pub struct CreditTransaction {
2266    /// The unique ID of the transaction.
2267    pub id: Uuid,
2268    /// The type of the transaction.
2269    pub transaction_type: TransactionType,
2270    /// The amount of credits involved in the transaction.
2271    pub credits_amount: f64,
2272    /// The credit balance after the transaction.
2273    pub credits_balance_after: f64,
2274    /// The provider associated with the transaction, if any.
2275    pub provider: Option<String>,
2276    /// The model associated with the transaction, if any.
2277    pub model: Option<String>,
2278    /// A description of the transaction.
2279    pub description: String,
2280    /// The timestamp of when the transaction occurred.
2281    pub created_at: DateTime<Utc>,
2282}
2283
2284/// The type of credit transaction (legacy).
2285#[derive(Debug, Clone, Serialize, Deserialize)]
2286#[serde(rename_all = "lowercase")]
2287pub enum TransactionType {
2288    /// A transaction for API usage.
2289    Usage,
2290    /// A transaction for a credit reset.
2291    Reset,
2292    /// A transaction for a credit purchase.
2293    Purchase,
2294    /// A transaction for a credit refund.
2295    Refund,
2296}
2297
2298// Legacy aliases for backward compatibility
2299/// A legacy type alias for `MessageRole`.
2300pub type ChatRole = MessageRole;
2301/// A legacy type alias for `Usage`.
2302pub type ChatUsage = Usage;
2303/// A legacy type alias for `HealthStatus`.
2304pub type HealthCheck = HealthStatus;
2305
2306/// Represents the status of backend services (legacy).
2307#[derive(Debug, Clone, Serialize, Deserialize)]
2308pub struct HealthServices {
2309    /// The status of the database connection.
2310    pub database: bool,
2311    /// The status of the Redis connection.
2312    pub redis: bool,
2313    /// The overall status of AI providers.
2314    pub providers: bool,
2315}
2316
2317/// The health status of the API (legacy).
2318#[derive(Debug, Clone, Serialize, Deserialize)]
2319#[serde(rename_all = "lowercase")]
2320pub enum HealthStatusEnum {
2321    /// The API is healthy.
2322    Healthy,
2323    /// The API is in a degraded state.
2324    Degraded,
2325    /// The API is unhealthy.
2326    Unhealthy,
2327    /// The API needs initialization.
2328    NeedsInit,
2329}
2330
2331/// Represents the format that the model must output.
2332#[derive(Debug, Clone, Serialize, Deserialize)]
2333#[serde(rename_all = "snake_case")]
2334pub enum ResponseFormat {
2335    /// The model can return text.
2336    Text,
2337    /// The model must return a valid JSON object.
2338    JsonObject,
2339    /// The model must return a JSON object that matches the provided schema.
2340    JsonSchema {
2341        /// The JSON Schema that the model's output must conform to.
2342        json_schema: serde_json::Value,
2343    },
2344}
2345
2346/// Represents a tool that the model can use.
2347#[derive(Debug, Clone, Serialize, Deserialize)]
2348pub struct Tool {
2349    /// The type of the tool (currently only "function" is supported).
2350    pub r#type: ToolType,
2351    /// The function definition describing the tool's capabilities.
2352    pub function: FunctionDefinition,
2353}
2354
2355/// The type of tool.
2356#[derive(Debug, Clone, Serialize, Deserialize)]
2357#[serde(rename_all = "snake_case")]
2358pub enum ToolType {
2359    /// A function tool.
2360    Function,
2361}
2362
2363/// Represents a function definition for a tool.
2364#[derive(Debug, Clone, Serialize, Deserialize)]
2365pub struct FunctionDefinition {
2366    /// The name of the function.
2367    pub name: String,
2368    /// A description of what the function does.
2369    #[serde(skip_serializing_if = "Option::is_none")]
2370    pub description: Option<String>,
2371    /// The parameters the function accepts, described as a JSON Schema object.
2372    #[serde(skip_serializing_if = "Option::is_none")]
2373    pub parameters: Option<serde_json::Value>,
2374}
2375
2376/// Controls which tool is called by the model.
2377#[derive(Debug, Clone, Serialize, Deserialize)]
2378#[serde(untagged)]
2379pub enum ToolChoice {
2380    /// No tool is called.
2381    None,
2382    /// The model chooses which tool to call.
2383    Auto,
2384    /// A specific tool is called.
2385    Tool {
2386        /// The type of the tool being called.
2387        r#type: ToolType,
2388        /// The function to call within the tool.
2389        function: ToolFunction,
2390    },
2391}
2392
2393/// Represents a tool function call.
2394#[derive(Debug, Clone, Serialize, Deserialize)]
2395pub struct ToolFunction {
2396    /// The name of the function to call.
2397    pub name: String,
2398}
2399
2400/// Configuration for thinking capabilities in Gemini 3 and 2.5 series models.
2401#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2402pub struct ThinkingConfig {
2403    /// Whether to include thought summaries in the response.
2404    #[serde(skip_serializing_if = "Option::is_none")]
2405    pub include_thoughts: Option<bool>,
2406
2407    /// The thinking level for Gemini 3 models (low, high for Pro; minimal, low, medium, high for Flash).
2408    #[serde(skip_serializing_if = "Option::is_none")]
2409    pub thinking_level: Option<ThinkingLevel>,
2410
2411    /// The thinking budget for Gemini 2.5 models (number of thinking tokens).
2412    #[serde(skip_serializing_if = "Option::is_none")]
2413    pub thinking_budget: Option<i32>,
2414}
2415
2416/// Thinking levels for Gemini 3 models.
2417#[derive(Debug, Clone, Serialize, Deserialize)]
2418#[serde(rename_all = "lowercase")]
2419pub enum ThinkingLevel {
2420    /// Minimal thinking (Gemini 3 Flash only) - model likely won't think.
2421    Minimal,
2422    /// Low thinking level - faster responses with basic reasoning.
2423    Low,
2424    /// Medium thinking level (Gemini 3 Flash only) - balanced reasoning and speed.
2425    Medium,
2426    /// High thinking level - deep reasoning for complex tasks (default).
2427    High,
2428}
2429
2430/// Represents a content part that may include thought signatures.
2431#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2432pub struct ContentPart {
2433    /// The text content of the part.
2434    #[serde(skip_serializing_if = "Option::is_none")]
2435    pub text: Option<String>,
2436
2437    /// Function call information if this part contains a function call.
2438    #[serde(skip_serializing_if = "Option::is_none")]
2439    pub function_call: Option<FunctionCall>,
2440
2441    /// Function response information if this part contains a function response.
2442    #[serde(skip_serializing_if = "Option::is_none")]
2443    pub function_response: Option<FunctionResponse>,
2444
2445    /// Indicates if this part contains thought content.
2446    #[serde(skip_serializing_if = "Option::is_none")]
2447    pub thought: Option<bool>,
2448
2449    /// Encrypted thought signature for preserving reasoning context across turns.
2450    #[serde(skip_serializing_if = "Option::is_none")]
2451    pub thought_signature: Option<String>,
2452}
2453
2454/// Represents a function call in the content.
2455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2456pub struct FunctionCall {
2457    /// The name of the function being called.
2458    pub name: String,
2459    /// The arguments for the function call as a JSON object.
2460    pub args: serde_json::Value,
2461}
2462
2463/// Represents a function response in the content.
2464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2465pub struct FunctionResponse {
2466    /// The name of the function that was called.
2467    pub name: String,
2468    /// The response from the function call.
2469    pub response: serde_json::Value,
2470}
2471
2472/// Enhanced chat message that supports Gemini 3 thinking capabilities.
2473#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2474pub struct EnhancedChatMessage {
2475    /// The role of the message author.
2476    pub role: MessageRole,
2477    /// The content parts of the message (supports text, function calls, and thought signatures).
2478    pub parts: Vec<ContentPart>,
2479}
2480
2481/// Enhanced usage statistics that include thinking tokens.
2482#[derive(Debug, Clone, Serialize, Deserialize)]
2483pub struct EnhancedUsage {
2484    /// The number of tokens in the prompt.
2485    pub prompt_tokens: u32,
2486    /// The number of tokens in the generated completion.
2487    pub completion_tokens: u32,
2488    /// The total number of tokens used in the request (prompt + completion).
2489    pub total_tokens: u32,
2490    /// The number of thinking tokens used (Gemini 3 and 2.5 series).
2491    #[serde(skip_serializing_if = "Option::is_none")]
2492    pub thoughts_token_count: Option<u32>,
2493}
2494
2495impl ThinkingConfig {
2496    /// Creates a new thinking configuration with default values.
2497    pub fn new() -> Self {
2498        Self::default()
2499    }
2500
2501    /// Creates a configuration for Gemini 3 models with specified thinking level.
2502    ///
2503    /// # Arguments
2504    ///
2505    /// * `level` - The thinking level to use.
2506    /// * `include_thoughts` - Whether to include thought summaries.
2507    pub fn gemini_3(level: ThinkingLevel, include_thoughts: bool) -> Self {
2508        Self {
2509            thinking_level: Some(level),
2510            include_thoughts: Some(include_thoughts),
2511            thinking_budget: None,
2512        }
2513    }
2514
2515    /// Creates a configuration for Gemini 2.5 models with specified thinking budget.
2516    ///
2517    /// # Arguments
2518    ///
2519    /// * `budget` - The thinking budget (-1 for dynamic, 0 to disable, or specific token count).
2520    /// * `include_thoughts` - Whether to include thought summaries.
2521    pub fn gemini_2_5(budget: i32, include_thoughts: bool) -> Self {
2522        Self {
2523            thinking_budget: Some(budget),
2524            include_thoughts: Some(include_thoughts),
2525            thinking_level: None,
2526        }
2527    }
2528
2529    /// Creates a configuration optimized for complex reasoning tasks.
2530    pub fn high_reasoning() -> Self {
2531        Self {
2532            thinking_level: Some(ThinkingLevel::High),
2533            include_thoughts: Some(true),
2534            thinking_budget: Some(-1), // Dynamic for 2.5 models
2535        }
2536    }
2537
2538    /// Creates a configuration optimized for fast responses.
2539    pub fn fast_response() -> Self {
2540        Self {
2541            thinking_level: Some(ThinkingLevel::Low),
2542            include_thoughts: Some(false),
2543            thinking_budget: Some(512), // Low budget for 2.5 models
2544        }
2545    }
2546}
2547
2548impl ContentPart {
2549    /// Creates a new text content part.
2550    pub fn text(content: impl Into<String>) -> Self {
2551        Self {
2552            text: Some(content.into()),
2553            function_call: None,
2554            function_response: None,
2555            thought: None,
2556            thought_signature: None,
2557        }
2558    }
2559
2560    /// Creates a new function call content part.
2561    pub fn function_call(name: impl Into<String>, args: serde_json::Value) -> Self {
2562        Self {
2563            text: None,
2564            function_call: Some(FunctionCall {
2565                name: name.into(),
2566                args,
2567            }),
2568            function_response: None,
2569            thought: None,
2570            thought_signature: None,
2571        }
2572    }
2573
2574    /// Creates a new function response content part.
2575    pub fn function_response(name: impl Into<String>, response: serde_json::Value) -> Self {
2576        Self {
2577            text: None,
2578            function_call: None,
2579            function_response: Some(FunctionResponse {
2580                name: name.into(),
2581                response,
2582            }),
2583            thought: None,
2584            thought_signature: None,
2585        }
2586    }
2587
2588    /// Adds a thought signature to this content part.
2589    pub fn with_thought_signature(mut self, signature: impl Into<String>) -> Self {
2590        self.thought_signature = Some(signature.into());
2591        self
2592    }
2593
2594    /// Marks this content part as containing thought content.
2595    pub fn as_thought(mut self) -> Self {
2596        self.thought = Some(true);
2597        self
2598    }
2599}
2600
2601impl EnhancedChatMessage {
2602    /// Creates a new enhanced message with the `System` role.
2603    pub fn system(content: impl Into<String>) -> Self {
2604        Self {
2605            role: MessageRole::System,
2606            parts: vec![ContentPart::text(content)],
2607        }
2608    }
2609
2610    /// Creates a new enhanced message with the `User` role.
2611    pub fn user(content: impl Into<String>) -> Self {
2612        Self {
2613            role: MessageRole::User,
2614            parts: vec![ContentPart::text(content)],
2615        }
2616    }
2617
2618    /// Creates a new enhanced message with the `Assistant` role.
2619    pub fn assistant(content: impl Into<String>) -> Self {
2620        Self {
2621            role: MessageRole::Assistant,
2622            parts: vec![ContentPart::text(content)],
2623        }
2624    }
2625
2626    /// Creates a new enhanced message with multiple content parts.
2627    pub fn with_parts(role: MessageRole, parts: Vec<ContentPart>) -> Self {
2628        Self { role, parts }
2629    }
2630}
2631
2632/// Represents a streaming chat completion response (OpenAI delta format).
2633#[derive(Debug, Clone, Serialize, Deserialize)]
2634pub struct ChatCompletionStreamResponse {
2635    /// A unique identifier for the chat completion.
2636    pub id: String,
2637    /// The type of object, which is always "chat.completion.chunk".
2638    pub object: String,
2639    /// The Unix timestamp (in seconds) of when the completion was created.
2640    pub created: u64,
2641    /// The model that was used for the completion.
2642    pub model: String,
2643    /// A list of chat completion choices.
2644    pub choices: Vec<ChatCompletionStreamChoice>,
2645    /// Information about the token usage for this completion (only present in the final chunk).
2646    #[serde(skip_serializing_if = "Option::is_none")]
2647    pub usage: Option<Usage>,
2648}
2649
2650/// Represents a single choice in a streaming chat completion response.
2651#[derive(Debug, Clone, Serialize, Deserialize)]
2652pub struct ChatCompletionStreamChoice {
2653    /// The index of the choice in the list of choices.
2654    pub index: u32,
2655    /// The delta containing the new content for this choice.
2656    pub delta: ChatCompletionStreamDelta,
2657    /// The reason the model stopped generating tokens (only present in the final chunk).
2658    #[serde(skip_serializing_if = "Option::is_none")]
2659    pub finish_reason: Option<String>,
2660}
2661
2662/// Represents the delta (change) in a streaming chat completion response.
2663#[derive(Debug, Clone, Serialize, Deserialize)]
2664pub struct ChatCompletionStreamDelta {
2665    /// The role of the message (only present in the first chunk).
2666    #[serde(skip_serializing_if = "Option::is_none")]
2667    pub role: Option<String>,
2668    /// The new content for this chunk.
2669    #[serde(skip_serializing_if = "Option::is_none")]
2670    pub content: Option<String>,
2671    /// The thinking/reasoning content for this chunk (if any).
2672    #[serde(skip_serializing_if = "Option::is_none")]
2673    pub thought: Option<String>,
2674    /// Tool calls for this chunk (if any).
2675    #[serde(skip_serializing_if = "Option::is_none")]
2676    pub tool_calls: Option<Vec<ToolCall>>,
2677}
2678
2679/// Represents a tool call in a streaming response.
2680#[derive(Debug, Clone, Serialize, Deserialize)]
2681pub struct ToolCall {
2682    /// The index of the tool call.
2683    pub index: u32,
2684    /// The ID of the tool call.
2685    #[serde(skip_serializing_if = "Option::is_none")]
2686    pub id: Option<String>,
2687    /// The type of the tool call.
2688    #[serde(skip_serializing_if = "Option::is_none")]
2689    pub r#type: Option<String>,
2690    /// The function being called.
2691    #[serde(skip_serializing_if = "Option::is_none")]
2692    pub function: Option<ToolCallFunction>,
2693}
2694
2695/// Represents a function call in a tool call.
2696#[derive(Debug, Clone, Serialize, Deserialize)]
2697pub struct ToolCallFunction {
2698    /// The name of the function.
2699    #[serde(skip_serializing_if = "Option::is_none")]
2700    pub name: Option<String>,
2701    /// The arguments for the function.
2702    #[serde(skip_serializing_if = "Option::is_none")]
2703    pub arguments: Option<String>,
2704}