Skip to main content

rainy_sdk/
models.rs

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