1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::cmp::Ordering;
4use std::collections::HashMap;
5
6fn map_is_empty(value: &HashMap<String, serde_json::Value>) -> bool {
7 value.is_empty()
8}
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct ChatMessage {
13 pub role: MessageRole,
15 pub content: String,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21#[serde(rename_all = "lowercase")]
22pub enum MessageRole {
23 System,
25 User,
27 Assistant,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33#[serde(rename_all = "lowercase")]
34pub enum OpenAIMessageRole {
35 System,
37 User,
39 Assistant,
41 Tool,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47#[serde(untagged)]
48pub enum OpenAIMessageContent {
49 Text(String),
51 Parts(Vec<OpenAIContentPart>),
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57#[serde(tag = "type", rename_all = "snake_case")]
58pub enum OpenAIContentPart {
59 Text {
61 text: String,
63 },
64 ImageUrl {
66 image_url: OpenAIImageUrl,
68 },
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73pub struct OpenAIImageUrl {
74 pub url: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub detail: Option<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub struct OpenAIFunctionCall {
84 pub name: String,
86 pub arguments: String,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92pub struct OpenAIToolCall {
93 pub id: String,
95 pub r#type: String,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub extra_content: Option<serde_json::Value>,
100 pub function: OpenAIFunctionCall,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
106pub struct OpenAIChatMessage {
107 pub role: OpenAIMessageRole,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub content: Option<OpenAIMessageContent>,
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub name: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub tool_calls: Option<Vec<OpenAIToolCall>>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub tool_call_id: Option<String>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
125#[serde(rename_all = "lowercase")]
126pub enum ResearchProvider {
127 #[default]
129 Exa,
130 Tavily,
132 Auto,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
138#[serde(rename_all = "lowercase")]
139pub enum ResearchDepth {
140 #[default]
142 Basic,
143 Advanced,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct ChatCompletionRequest {
150 pub model: String,
152
153 pub messages: Vec<ChatMessage>,
155
156 #[serde(skip_serializing_if = "Option::is_none")]
159 pub temperature: Option<f32>,
160
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub max_tokens: Option<u32>,
164
165 #[serde(skip_serializing_if = "Option::is_none")]
168 pub top_p: Option<f32>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
173 pub frequency_penalty: Option<f32>,
174
175 #[serde(skip_serializing_if = "Option::is_none")]
178 pub presence_penalty: Option<f32>,
179
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub stop: Option<Vec<String>>,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
187 pub user: Option<String>,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub provider: Option<String>,
192
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub stream: Option<bool>,
196
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub logit_bias: Option<serde_json::Value>,
200
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub logprobs: Option<bool>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub top_logprobs: Option<u32>,
208
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub n: Option<u32>,
212
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub response_format: Option<ResponseFormat>,
216
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub tools: Option<Vec<Tool>>,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub tool_choice: Option<ToolChoice>,
224
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub thinking_config: Option<ThinkingConfig>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct OpenAIChatCompletionRequest {
233 pub model: String,
235
236 pub messages: Vec<OpenAIChatMessage>,
238
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub temperature: Option<f32>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub max_tokens: Option<u32>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub top_p: Option<f32>,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub frequency_penalty: Option<f32>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub presence_penalty: Option<f32>,
258
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub stop: Option<Vec<String>>,
262
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub user: Option<String>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub provider: Option<String>,
270
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub stream: Option<bool>,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub logit_bias: Option<serde_json::Value>,
278
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub logprobs: Option<bool>,
282
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub top_logprobs: Option<u32>,
286
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub n: Option<u32>,
290
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub response_format: Option<ResponseFormat>,
294
295 #[serde(skip_serializing_if = "Option::is_none")]
297 pub tools: Option<Vec<Tool>>,
298
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub tool_choice: Option<ToolChoice>,
302
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub thinking_config: Option<ThinkingConfig>,
306
307 #[serde(skip_serializing_if = "Option::is_none")]
311 pub thinking: Option<serde_json::Value>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct ChatCompletionResponse {
317 pub id: String,
319
320 pub object: String,
322
323 pub created: u64,
325
326 pub model: String,
328
329 pub choices: Vec<ChatChoice>,
331
332 #[serde(skip_serializing_if = "Option::is_none")]
334 pub usage: Option<Usage>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct OpenAIChatCompletionResponse {
340 pub id: String,
342
343 pub object: String,
345
346 pub created: u64,
348
349 pub model: String,
351
352 pub choices: Vec<OpenAIChatChoice>,
354
355 #[serde(skip_serializing_if = "Option::is_none")]
357 pub usage: Option<Usage>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct ChatCompletionChunk {
363 pub id: String,
365
366 pub object: String,
368
369 pub created: u64,
371
372 pub model: String,
374
375 pub choices: Vec<ChatCompletionChunkChoice>,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct ChatCompletionChunkChoice {
382 pub index: u32,
384
385 pub delta: ChatCompletionChunkDelta,
387
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub finish_reason: Option<String>,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct ChatCompletionChunkDelta {
396 #[serde(skip_serializing_if = "Option::is_none")]
398 pub role: Option<MessageRole>,
399
400 #[serde(skip_serializing_if = "Option::is_none")]
402 pub content: Option<String>,
403
404 #[serde(skip_serializing_if = "Option::is_none")]
406 pub thought: Option<String>,
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct ChatChoice {
412 pub index: u32,
414
415 pub message: ChatMessage,
417
418 pub finish_reason: String,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct OpenAIChatChoice {
425 pub index: u32,
427
428 pub message: OpenAIChatMessage,
430
431 pub finish_reason: String,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct Usage {
438 pub prompt_tokens: u32,
440
441 pub completion_tokens: u32,
443
444 pub total_tokens: u32,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct HealthStatus {
451 pub status: String,
453
454 pub timestamp: String,
456
457 pub uptime: f64,
459
460 pub services: ServiceStatus,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct ServiceStatus {
467 pub database: bool,
469
470 #[serde(skip_serializing_if = "Option::is_none")]
472 pub redis: Option<bool>,
473
474 pub providers: bool,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize, Default)]
480pub struct AvailableModels {
481 #[serde(default)]
483 pub providers: HashMap<String, Vec<String>>,
484
485 #[serde(default)]
487 pub total_models: usize,
488
489 #[serde(default)]
491 pub active_providers: Vec<String>,
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct CreditInfo {
497 pub current_credits: f64,
499
500 pub estimated_cost: f64,
502
503 pub credits_after_request: f64,
505
506 pub reset_date: String,
508}
509
510#[derive(Debug, Clone)]
512pub struct RequestMetadata {
513 pub response_time: Option<u64>,
515
516 pub provider: Option<String>,
518
519 pub tokens_used: Option<u32>,
521
522 pub credits_used: Option<f64>,
524
525 pub credits_remaining: Option<f64>,
527
528 pub request_id: Option<String>,
530
531 pub compat_warnings: Option<u32>,
533
534 pub response_mode: Option<String>,
536
537 pub billing_plan: Option<String>,
539
540 pub rainy_credits_charged: Option<f64>,
542
543 pub rainy_markup_percent: Option<f64>,
545
546 pub rainy_daily_credits_remaining: Option<String>,
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct ResponsesRequest {
553 pub model: String,
555
556 pub input: serde_json::Value,
558
559 #[serde(skip_serializing_if = "Option::is_none")]
561 pub stream: Option<bool>,
562
563 #[serde(skip_serializing_if = "Option::is_none")]
565 pub tools: Option<Vec<serde_json::Value>>,
566
567 #[serde(skip_serializing_if = "Option::is_none")]
569 pub tool_choice: Option<serde_json::Value>,
570
571 #[serde(skip_serializing_if = "Option::is_none")]
573 pub response_format: Option<serde_json::Value>,
574
575 #[serde(skip_serializing_if = "Option::is_none")]
577 pub temperature: Option<f32>,
578
579 #[serde(skip_serializing_if = "Option::is_none")]
581 pub top_p: Option<f32>,
582
583 #[serde(skip_serializing_if = "Option::is_none")]
585 pub max_output_tokens: Option<u32>,
586
587 #[serde(skip_serializing_if = "Option::is_none")]
589 pub user: Option<String>,
590
591 #[serde(skip_serializing_if = "Option::is_none")]
593 pub prompt_cache_key: Option<String>,
594
595 #[serde(skip_serializing_if = "Option::is_none")]
597 pub reasoning: Option<serde_json::Value>,
598
599 #[serde(flatten, skip_serializing_if = "map_is_empty", default)]
601 pub extra: HashMap<String, serde_json::Value>,
602}
603
604impl ResponsesRequest {
605 pub fn new(model: impl Into<String>, input: serde_json::Value) -> Self {
607 Self {
608 model: model.into(),
609 input,
610 stream: None,
611 tools: None,
612 tool_choice: None,
613 response_format: None,
614 temperature: None,
615 top_p: None,
616 max_output_tokens: None,
617 user: None,
618 prompt_cache_key: None,
619 reasoning: None,
620 extra: HashMap::new(),
621 }
622 }
623
624 pub fn text(model: impl Into<String>, input_text: impl Into<String>) -> Self {
626 Self::new(model, serde_json::Value::String(input_text.into()))
627 }
628
629 pub fn with_stream(mut self, stream: bool) -> Self {
631 self.stream = Some(stream);
632 self
633 }
634
635 pub fn with_reasoning(mut self, reasoning: serde_json::Value) -> Self {
637 self.reasoning = Some(reasoning);
638 self
639 }
640
641 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
643 self.reasoning = Some(serde_json::json!({ "effort": effort.into() }));
644 self
645 }
646
647 pub fn with_max_output_tokens(mut self, max_output_tokens: u32) -> Self {
649 self.max_output_tokens = Some(max_output_tokens);
650 self
651 }
652
653 pub fn with_prompt_cache_key(mut self, prompt_cache_key: impl Into<String>) -> Self {
655 self.prompt_cache_key = Some(prompt_cache_key.into());
656 self
657 }
658
659 pub fn with_user(mut self, user: impl Into<String>) -> Self {
661 self.user = Some(user.into());
662 self
663 }
664
665 pub fn with_tools(mut self, tools: Vec<serde_json::Value>) -> Self {
667 self.tools = Some(tools);
668 self
669 }
670
671 pub fn add_function_tool(
673 mut self,
674 name: impl Into<String>,
675 description: impl Into<String>,
676 parameters: serde_json::Value,
677 ) -> Self {
678 let mut tools = self.tools.unwrap_or_default();
679 tools.push(serde_json::json!({
680 "type": "function",
681 "name": name.into(),
682 "description": description.into(),
683 "parameters": parameters
684 }));
685 self.tools = Some(tools);
686 self
687 }
688
689 pub fn with_extra(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
691 self.extra.insert(key.into(), value);
692 self
693 }
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize, Default)]
698pub struct ResponsesUsage {
699 #[serde(skip_serializing_if = "Option::is_none")]
701 pub input_tokens: Option<u32>,
702 #[serde(skip_serializing_if = "Option::is_none")]
704 pub output_tokens: Option<u32>,
705 #[serde(skip_serializing_if = "Option::is_none")]
707 pub cache_creation_input_tokens: Option<u32>,
708 #[serde(skip_serializing_if = "Option::is_none")]
710 pub cache_read_input_tokens: Option<u32>,
711 #[serde(skip_serializing_if = "Option::is_none")]
713 pub output_tokens_details: Option<serde_json::Value>,
714 #[serde(skip_serializing_if = "Option::is_none")]
716 pub completion_tokens_details: Option<serde_json::Value>,
717 #[serde(flatten, default)]
719 pub extra: HashMap<String, serde_json::Value>,
720}
721
722#[derive(Debug, Clone, Serialize, Deserialize, Default)]
724pub struct ResponsesApiResponse {
725 #[serde(skip_serializing_if = "Option::is_none")]
727 pub id: Option<String>,
728 #[serde(skip_serializing_if = "Option::is_none")]
730 pub object: Option<String>,
731 #[serde(skip_serializing_if = "Option::is_none")]
733 pub model: Option<String>,
734 #[serde(skip_serializing_if = "Option::is_none")]
736 pub output_text: Option<String>,
737 #[serde(skip_serializing_if = "Option::is_none")]
739 pub output: Option<Vec<serde_json::Value>>,
740 #[serde(skip_serializing_if = "Option::is_none")]
742 pub usage: Option<ResponsesUsage>,
743 #[serde(flatten, default)]
745 pub extra: HashMap<String, serde_json::Value>,
746}
747
748#[derive(Debug, Clone, Serialize, Deserialize)]
750pub struct CompatWarning {
751 pub code: String,
753 pub message: String,
755 #[serde(skip_serializing_if = "Option::is_none")]
757 pub path: Option<String>,
758}
759
760#[derive(Debug, Clone, Serialize, Deserialize)]
762pub struct FeaturesUsed {
763 pub reasoning: bool,
765 #[serde(rename = "imageInput")]
767 pub image_input: bool,
768 pub tools: bool,
770 #[serde(rename = "structuredOutput")]
772 pub structured_output: bool,
773}
774
775#[derive(Debug, Clone, Serialize, Deserialize)]
777pub struct ReasoningMeta {
778 pub present: bool,
780 pub summary_present: bool,
782 #[serde(skip_serializing_if = "Option::is_none")]
784 pub tokens: Option<u32>,
785}
786
787#[derive(Debug, Clone, Serialize, Deserialize, Default)]
789pub struct RainyEnvelopeMeta {
790 #[serde(
792 rename = "billingPlan",
793 alias = "billing_plan",
794 skip_serializing_if = "Option::is_none"
795 )]
796 pub billing_plan: Option<String>,
797 #[serde(
799 rename = "creditsCharged",
800 alias = "credits_charged",
801 skip_serializing_if = "Option::is_none"
802 )]
803 pub credits_charged: Option<f64>,
804 #[serde(
806 rename = "markupPercent",
807 alias = "markup_percent",
808 skip_serializing_if = "Option::is_none"
809 )]
810 pub markup_percent: Option<f64>,
811 #[serde(
813 rename = "dailyCreditsRemaining",
814 alias = "daily_credits_remaining",
815 skip_serializing_if = "Option::is_none"
816 )]
817 pub daily_credits_remaining: Option<String>,
818 #[serde(
820 rename = "compatWarnings",
821 alias = "compat_warnings",
822 skip_serializing_if = "Option::is_none"
823 )]
824 pub compat_warnings: Option<Vec<CompatWarning>>,
825 #[serde(
827 rename = "featuresUsed",
828 alias = "features_used",
829 skip_serializing_if = "Option::is_none"
830 )]
831 pub features_used: Option<FeaturesUsed>,
832 #[serde(skip_serializing_if = "Option::is_none")]
834 pub reasoning: Option<ReasoningMeta>,
835 #[serde(flatten, default)]
837 pub extra: HashMap<String, serde_json::Value>,
838}
839
840#[derive(Debug, Clone, Serialize, Deserialize)]
842pub struct RainyEnvelope<T> {
843 pub success: bool,
845 pub data: T,
847 #[serde(skip_serializing_if = "Option::is_none")]
849 pub meta: Option<RainyEnvelopeMeta>,
850}
851
852pub type ResponsesStreamEvent = serde_json::Value;
854
855#[derive(Debug, Clone, Serialize, Deserialize, Default)]
857pub struct ModelArchitecture {
858 #[serde(skip_serializing_if = "Option::is_none")]
860 pub input_modalities: Option<Vec<String>>,
861 #[serde(skip_serializing_if = "Option::is_none")]
863 pub output_modalities: Option<Vec<String>>,
864 #[serde(skip_serializing_if = "Option::is_none")]
866 pub tokenizer: Option<String>,
867 #[serde(skip_serializing_if = "Option::is_none")]
869 pub instruct_type: Option<String>,
870}
871
872#[derive(Debug, Clone, Serialize, Deserialize)]
874#[serde(untagged)]
875pub enum CapabilityFlag {
876 Bool(bool),
878 Text(String),
880}
881
882#[derive(Debug, Clone, Serialize, Deserialize, Default)]
884pub struct RainyCapabilities {
885 #[serde(skip_serializing_if = "Option::is_none")]
887 pub reasoning: Option<CapabilityFlag>,
888 #[serde(skip_serializing_if = "Option::is_none")]
890 pub image_input: Option<CapabilityFlag>,
891 #[serde(skip_serializing_if = "Option::is_none")]
893 pub tools: Option<CapabilityFlag>,
894 #[serde(skip_serializing_if = "Option::is_none")]
896 pub response_format: Option<CapabilityFlag>,
897}
898
899#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
901#[serde(rename_all = "lowercase")]
902pub enum ReasoningProvider {
903 Openai,
905 Google,
907 Anthropic,
909 Other,
911}
912
913#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
915pub struct ThinkingBudget {
916 pub min: i32,
918 pub max: i32,
920 #[serde(skip_serializing_if = "Option::is_none")]
922 pub dynamic_value: Option<i32>,
923 #[serde(skip_serializing_if = "Option::is_none")]
925 pub disable_value: Option<i32>,
926}
927
928#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
930pub struct ReasoningControls {
931 #[serde(skip_serializing_if = "Option::is_none")]
933 pub observed_parameters: Option<Vec<String>>,
934 #[serde(skip_serializing_if = "Option::is_none")]
936 pub reasoning_toggle: Option<bool>,
937 #[serde(skip_serializing_if = "Option::is_none")]
939 pub reasoning_effort: Option<bool>,
940 #[serde(skip_serializing_if = "Option::is_none")]
942 pub effort: Option<Vec<String>>,
943 #[serde(skip_serializing_if = "Option::is_none")]
945 pub thinking_level: Option<Vec<String>>,
946 #[serde(skip_serializing_if = "Option::is_none")]
948 pub thinking_budget: Option<ThinkingBudget>,
949}
950
951#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
953pub struct ReasoningProfile {
954 pub provider: ReasoningProvider,
956 pub parameter_path: String,
958 #[serde(skip_serializing_if = "Option::is_none")]
960 pub values: Option<Vec<String>>,
961 #[serde(skip_serializing_if = "Option::is_none")]
963 pub notes: Option<String>,
964}
965
966#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
968pub struct ReasoningToggle {
969 #[serde(skip_serializing_if = "Option::is_none")]
971 pub enable_param: Option<String>,
972 #[serde(skip_serializing_if = "Option::is_none")]
974 pub include_reasoning_param: Option<String>,
975}
976
977#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
979pub struct RainyReasoningCapabilitiesV2 {
980 pub supported: bool,
982 #[serde(skip_serializing_if = "Option::is_none")]
984 pub controls: Option<ReasoningControls>,
985 #[serde(default)]
987 pub profiles: Vec<ReasoningProfile>,
988 #[serde(skip_serializing_if = "Option::is_none")]
990 pub toggle: Option<ReasoningToggle>,
991}
992
993#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
995pub struct RainyMultimodalCapabilitiesV2 {
996 #[serde(default)]
998 pub input: Vec<String>,
999 #[serde(default)]
1001 pub output: Vec<String>,
1002}
1003
1004#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1006pub struct RainyParametersCapabilitiesV2 {
1007 #[serde(default)]
1009 pub accepted: Vec<String>,
1010}
1011
1012#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1014pub struct RainyCapabilitiesV2 {
1015 pub multimodal: RainyMultimodalCapabilitiesV2,
1017 pub reasoning: RainyReasoningCapabilitiesV2,
1019 pub parameters: RainyParametersCapabilitiesV2,
1021}
1022
1023#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1025pub struct ModelPricing {
1026 #[serde(skip_serializing_if = "Option::is_none")]
1028 pub prompt: Option<String>,
1029 #[serde(skip_serializing_if = "Option::is_none")]
1031 pub completion: Option<String>,
1032}
1033
1034#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1036pub struct ModelCatalogItem {
1037 pub id: String,
1039 #[serde(skip_serializing_if = "Option::is_none")]
1041 pub name: Option<String>,
1042 #[serde(skip_serializing_if = "Option::is_none")]
1044 pub context_length: Option<u32>,
1045 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub pricing: Option<ModelPricing>,
1048 #[serde(skip_serializing_if = "Option::is_none")]
1050 pub supported_parameters: Option<Vec<String>>,
1051 #[serde(skip_serializing_if = "Option::is_none")]
1053 pub architecture: Option<ModelArchitecture>,
1054 #[serde(skip_serializing_if = "Option::is_none")]
1056 pub rainy_capabilities: Option<RainyCapabilities>,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1059 pub rainy_capabilities_v2: Option<RainyCapabilitiesV2>,
1060 #[serde(flatten, default)]
1062 pub extra: HashMap<String, serde_json::Value>,
1063}
1064
1065#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1067#[serde(rename_all = "snake_case")]
1068pub enum ReasoningMode {
1069 Effort,
1071 ThinkingLevel,
1073 ThinkingBudget,
1075}
1076
1077#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
1079pub struct ModelSelectionCriteria {
1080 #[serde(default)]
1082 pub required_input_modalities: Vec<String>,
1083 #[serde(default)]
1085 pub required_output_modalities: Vec<String>,
1086 #[serde(skip_serializing_if = "Option::is_none")]
1088 pub require_tools: Option<bool>,
1089 #[serde(skip_serializing_if = "Option::is_none")]
1091 pub require_structured_output: Option<bool>,
1092 #[serde(skip_serializing_if = "Option::is_none")]
1094 pub reasoning_mode: Option<ReasoningMode>,
1095 #[serde(skip_serializing_if = "Option::is_none")]
1097 pub reasoning_value: Option<String>,
1098}
1099
1100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1102pub struct ReasoningPreference {
1103 pub mode: ReasoningMode,
1105 #[serde(skip_serializing_if = "Option::is_none")]
1107 pub value: Option<String>,
1108 #[serde(skip_serializing_if = "Option::is_none")]
1110 pub budget: Option<i32>,
1111}
1112
1113fn parse_price(value: Option<&str>) -> f64 {
1114 value
1115 .and_then(|raw| raw.parse::<f64>().ok())
1116 .filter(|v| v.is_finite())
1117 .unwrap_or(f64::MAX)
1118}
1119
1120fn has_required_modalities(available: &[String], required: &[String]) -> bool {
1121 if required.is_empty() {
1122 return true;
1123 }
1124
1125 required.iter().all(|modality| {
1126 available
1127 .iter()
1128 .any(|candidate| candidate.eq_ignore_ascii_case(modality))
1129 })
1130}
1131
1132fn supports_reasoning_preference(
1133 capabilities: &RainyCapabilitiesV2,
1134 mode: &ReasoningMode,
1135 reasoning_value: Option<&str>,
1136) -> bool {
1137 if !capabilities.reasoning.supported {
1138 return false;
1139 }
1140
1141 let controls = capabilities.reasoning.controls.as_ref();
1142 match mode {
1143 ReasoningMode::Effort => controls
1144 .map(|c| {
1145 c.reasoning_effort == Some(true) || c.effort.as_ref().is_some_and(|v| !v.is_empty())
1146 })
1147 .filter(|supported| *supported)
1148 .map(|_| {
1149 controls
1150 .and_then(|c| c.effort.as_ref())
1151 .map(|values| {
1152 reasoning_value.is_none_or(|value| {
1153 values
1154 .iter()
1155 .any(|candidate| candidate.eq_ignore_ascii_case(value))
1156 })
1157 })
1158 .unwrap_or(reasoning_value.is_none())
1159 })
1160 .unwrap_or(false),
1161 ReasoningMode::ThinkingLevel => controls
1162 .and_then(|c| c.thinking_level.as_ref())
1163 .map(|values| {
1164 reasoning_value.is_none_or(|value| {
1165 values
1166 .iter()
1167 .any(|candidate| candidate.eq_ignore_ascii_case(value))
1168 })
1169 })
1170 .unwrap_or(false),
1171 ReasoningMode::ThinkingBudget => {
1172 controls.and_then(|c| c.thinking_budget.as_ref()).is_some()
1173 }
1174 }
1175}
1176
1177fn catalog_item_supports(item: &ModelCatalogItem, parameter: &str) -> bool {
1178 if let Some(v2) = &item.rainy_capabilities_v2 {
1179 return v2
1180 .parameters
1181 .accepted
1182 .iter()
1183 .any(|candidate| candidate == parameter);
1184 }
1185
1186 item.supported_parameters
1187 .as_ref()
1188 .map(|params| params.iter().any(|candidate| candidate == parameter))
1189 .unwrap_or(false)
1190}
1191
1192pub fn select_models(
1194 models: &[ModelCatalogItem],
1195 criteria: &ModelSelectionCriteria,
1196) -> Vec<ModelCatalogItem> {
1197 let required_inputs: Vec<String> = criteria
1198 .required_input_modalities
1199 .iter()
1200 .map(|v| v.to_lowercase())
1201 .collect();
1202 let required_outputs: Vec<String> = criteria
1203 .required_output_modalities
1204 .iter()
1205 .map(|v| v.to_lowercase())
1206 .collect();
1207
1208 let mut filtered: Vec<ModelCatalogItem> = models
1209 .iter()
1210 .filter(|item| {
1211 let Some(v2) = item.rainy_capabilities_v2.as_ref() else {
1212 return false;
1213 };
1214 let input: Vec<String> = v2
1215 .multimodal
1216 .input
1217 .iter()
1218 .map(|v| v.to_lowercase())
1219 .collect();
1220 let output: Vec<String> = v2
1221 .multimodal
1222 .output
1223 .iter()
1224 .map(|v| v.to_lowercase())
1225 .collect();
1226
1227 if !has_required_modalities(&input, &required_inputs) {
1228 return false;
1229 }
1230
1231 if !has_required_modalities(&output, &required_outputs) {
1232 return false;
1233 }
1234
1235 if criteria.require_tools == Some(true) && !catalog_item_supports(item, "tools") {
1236 return false;
1237 }
1238
1239 if criteria.require_structured_output == Some(true)
1240 && !catalog_item_supports(item, "response_format")
1241 && !catalog_item_supports(item, "structured_outputs")
1242 {
1243 return false;
1244 }
1245
1246 if let Some(mode) = &criteria.reasoning_mode {
1247 let reasoning_value = criteria.reasoning_value.as_deref();
1248 if !supports_reasoning_preference(v2, mode, reasoning_value) {
1249 return false;
1250 }
1251 }
1252
1253 true
1254 })
1255 .cloned()
1256 .collect();
1257
1258 filtered.sort_by(|a, b| {
1259 let a_prompt = parse_price(a.pricing.as_ref().and_then(|p| p.prompt.as_deref()));
1260 let b_prompt = parse_price(b.pricing.as_ref().and_then(|p| p.prompt.as_deref()));
1261 let prompt_cmp = a_prompt.partial_cmp(&b_prompt).unwrap_or(Ordering::Equal);
1262 if prompt_cmp != Ordering::Equal {
1263 return prompt_cmp;
1264 }
1265
1266 let a_completion = parse_price(a.pricing.as_ref().and_then(|p| p.completion.as_deref()));
1267 let b_completion = parse_price(b.pricing.as_ref().and_then(|p| p.completion.as_deref()));
1268 let completion_cmp = a_completion
1269 .partial_cmp(&b_completion)
1270 .unwrap_or(Ordering::Equal);
1271 if completion_cmp != Ordering::Equal {
1272 return completion_cmp;
1273 }
1274
1275 let a_context = a.context_length.unwrap_or_default();
1276 let b_context = b.context_length.unwrap_or_default();
1277 b_context.cmp(&a_context)
1278 });
1279
1280 filtered
1281}
1282
1283pub fn build_reasoning_config(
1285 model: &ModelCatalogItem,
1286 preference: &ReasoningPreference,
1287) -> Option<serde_json::Value> {
1288 let v2 = model.rainy_capabilities_v2.as_ref()?;
1289 if !v2.reasoning.supported {
1290 return None;
1291 }
1292
1293 let profiles = &v2.reasoning.profiles;
1294 let controls = v2.reasoning.controls.as_ref();
1295 match preference.mode {
1296 ReasoningMode::Effort => {
1297 let value = preference.value.clone()?;
1298 let supports_effort = controls
1299 .map(|c| {
1300 c.reasoning_effort == Some(true)
1301 || c.effort.as_ref().is_some_and(|v| !v.is_empty())
1302 })
1303 .unwrap_or(false);
1304 if !supports_effort {
1305 return None;
1306 }
1307 if let Some(efforts) = controls.and_then(|c| c.effort.as_ref()) {
1308 if !efforts.iter().any(|v| v.eq_ignore_ascii_case(&value)) {
1309 return None;
1310 }
1311 }
1312
1313 let effort_profile = profiles
1314 .iter()
1315 .find(|p| p.parameter_path == "reasoning.effort")?;
1316 match effort_profile.parameter_path.as_str() {
1317 "reasoning.effort" => Some(serde_json::json!({
1318 "reasoning": { "effort": value }
1319 })),
1320 _ => None,
1321 }
1322 }
1323 ReasoningMode::ThinkingLevel => {
1324 let value = preference.value.clone()?;
1325 let supports = controls
1326 .and_then(|c| c.thinking_level.as_ref())
1327 .map(|levels| levels.iter().any(|v| v.eq_ignore_ascii_case(&value)))
1328 .unwrap_or(false);
1329 if !supports {
1330 return None;
1331 }
1332 let level_profile = profiles
1333 .iter()
1334 .find(|p| p.parameter_path == "thinking_config.thinking_level")?;
1335 if let Some(values) = &level_profile.values {
1336 if !values.iter().any(|v| v.eq_ignore_ascii_case(&value)) {
1337 return None;
1338 }
1339 }
1340 Some(serde_json::json!({
1341 "thinking_config": { "thinking_level": value }
1342 }))
1343 }
1344 ReasoningMode::ThinkingBudget => {
1345 let budget = preference.budget?;
1346 let supports = controls.and_then(|c| c.thinking_budget.as_ref())?;
1347 if budget < supports.min || budget > supports.max {
1348 return None;
1349 }
1350 let budget_profile = profiles.iter().find(|p| {
1351 p.parameter_path == "thinking.budget_tokens"
1352 || p.parameter_path == "thinking_config.thinking_budget"
1353 })?;
1354
1355 if budget_profile.parameter_path == "thinking.budget_tokens" {
1356 return Some(serde_json::json!({
1357 "thinking": { "budget_tokens": budget }
1358 }));
1359 }
1360 if budget_profile.parameter_path == "thinking_config.thinking_budget" {
1361 return Some(serde_json::json!({
1362 "thinking_config": { "thinking_budget": budget }
1363 }));
1364 }
1365 None
1366 }
1367 }
1368}
1369
1370pub mod model_constants {
1373 pub const OPENAI_GPT_4O: &str = "gpt-4o";
1376 pub const OPENAI_GPT_5: &str = "gpt-5";
1378 pub const OPENAI_GPT_5_PRO: &str = "gpt-5-pro";
1380 pub const OPENAI_O3: &str = "o3";
1382 pub const OPENAI_O4_MINI: &str = "o4-mini";
1384
1385 pub const GOOGLE_GEMINI_2_5_PRO: &str = "gemini-2.5-pro";
1388 pub const GOOGLE_GEMINI_2_5_FLASH: &str = "gemini-2.5-flash";
1390 pub const GOOGLE_GEMINI_2_5_FLASH_LITE: &str = "gemini-2.5-flash-lite";
1392
1393 pub const GOOGLE_GEMINI_3_PRO: &str = "gemini-3-pro-preview";
1396 pub const GOOGLE_GEMINI_3_FLASH: &str = "gemini-3-flash-preview";
1398 pub const GOOGLE_GEMINI_3_PRO_IMAGE: &str = "gemini-3-pro-image-preview";
1400
1401 pub const GROQ_LLAMA_3_1_8B_INSTANT: &str = "llama-3.1-8b-instant";
1404 pub const GROQ_LLAMA_3_3_70B_VERSATILE: &str = "llama-3.3-70b-versatile";
1406 pub const KIMI_K2_0925: &str = "moonshotai/kimi-k2-instruct-0905";
1408
1409 pub const CEREBRAS_LLAMA3_1_8B: &str = "cerebras/llama3.1-8b";
1412
1413 pub const ASTRONOMER_1: &str = "astronomer-1";
1416 pub const ASTRONOMER_1_MAX: &str = "astronomer-1-max";
1418 pub const ASTRONOMER_1_5: &str = "astronomer-1.5";
1420 pub const ASTRONOMER_2: &str = "astronomer-2";
1422 pub const ASTRONOMER_2_PRO: &str = "astronomer-2-pro";
1424
1425 #[deprecated(note = "Use OPENAI_GPT_4O instead for OpenAI compatibility")]
1428 pub const GPT_4O: &str = "openai/gpt-4o";
1429 #[deprecated(note = "Use OPENAI_GPT_5 instead for OpenAI compatibility")]
1431 pub const GPT_5: &str = "openai/gpt-5";
1432 #[deprecated(note = "Use GOOGLE_GEMINI_2_5_PRO instead for OpenAI compatibility")]
1434 pub const GEMINI_2_5_PRO: &str = "google/gemini-2.5-pro";
1435 #[deprecated(note = "Use GOOGLE_GEMINI_2_5_FLASH instead for OpenAI compatibility")]
1437 pub const GEMINI_2_5_FLASH: &str = "google/gemini-2.5-flash";
1438 #[deprecated(note = "Use GOOGLE_GEMINI_2_5_FLASH_LITE instead for OpenAI compatibility")]
1440 pub const GEMINI_2_5_FLASH_LITE: &str = "google/gemini-2.5-flash-lite";
1441 #[deprecated(note = "Use GROQ_LLAMA_3_1_8B_INSTANT instead for OpenAI compatibility")]
1443 pub const LLAMA_3_1_8B_INSTANT: &str = "groq/llama-3.1-8b-instant";
1444 #[deprecated(note = "Use CEREBRAS_LLAMA3_1_8B instead for OpenAI compatibility")]
1446 pub const LLAMA3_1_8B: &str = "cerebras/llama3.1-8b";
1447}
1448
1449pub mod providers {
1451 pub const OPENAI: &str = "openai";
1453 pub const ANTHROPIC: &str = "anthropic";
1455 pub const GROQ: &str = "groq";
1457 pub const CEREBRAS: &str = "cerebras";
1459 pub const GEMINI: &str = "gemini";
1461 pub const ENOSISLABS: &str = "enosislabs";
1463}
1464
1465impl ChatCompletionRequest {
1466 pub fn new(model: impl Into<String>, messages: Vec<ChatMessage>) -> Self {
1473 Self {
1474 model: model.into(),
1475 messages,
1476 temperature: None,
1477 max_tokens: None,
1478 top_p: None,
1479 frequency_penalty: None,
1480 presence_penalty: None,
1481 stop: None,
1482 user: None,
1483 provider: None,
1484 stream: None,
1485 logit_bias: None,
1486 logprobs: None,
1487 top_logprobs: None,
1488 n: None,
1489 response_format: None,
1490 tools: None,
1491 tool_choice: None,
1492 thinking_config: None,
1493 }
1494 }
1495
1496 pub fn with_temperature(mut self, temperature: f32) -> Self {
1504 self.temperature = Some(temperature.clamp(0.0, 2.0));
1505 self
1506 }
1507
1508 pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
1514 self.max_tokens = Some(max_tokens);
1515 self
1516 }
1517
1518 pub fn with_user(mut self, user: impl Into<String>) -> Self {
1524 self.user = Some(user.into());
1525 self
1526 }
1527
1528 pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
1534 self.provider = Some(provider.into());
1535 self
1536 }
1537
1538 pub fn with_stream(mut self, stream: bool) -> Self {
1544 self.stream = Some(stream);
1545 self
1546 }
1547
1548 pub fn with_logit_bias(mut self, logit_bias: serde_json::Value) -> Self {
1554 self.logit_bias = Some(logit_bias);
1555 self
1556 }
1557
1558 pub fn with_logprobs(mut self, logprobs: bool) -> Self {
1564 self.logprobs = Some(logprobs);
1565 self
1566 }
1567
1568 pub fn with_top_logprobs(mut self, top_logprobs: u32) -> Self {
1574 self.top_logprobs = Some(top_logprobs);
1575 self
1576 }
1577
1578 pub fn with_n(mut self, n: u32) -> Self {
1584 self.n = Some(n);
1585 self
1586 }
1587
1588 pub fn with_response_format(mut self, response_format: ResponseFormat) -> Self {
1594 self.response_format = Some(response_format);
1595 self
1596 }
1597
1598 pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
1604 self.tools = Some(tools);
1605 self
1606 }
1607
1608 pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
1614 self.tool_choice = Some(tool_choice);
1615 self
1616 }
1617
1618 pub fn with_thinking_config(mut self, thinking_config: ThinkingConfig) -> Self {
1624 self.thinking_config = Some(thinking_config);
1625 self
1626 }
1627
1628 pub fn with_include_thoughts(mut self, include_thoughts: bool) -> Self {
1634 let mut config = self.thinking_config.unwrap_or_default();
1635 config.include_thoughts = Some(include_thoughts);
1636 self.thinking_config = Some(config);
1637 self
1638 }
1639
1640 pub fn with_thinking_level(mut self, thinking_level: ThinkingLevel) -> Self {
1646 let mut config = self.thinking_config.unwrap_or_default();
1647 config.thinking_level = Some(thinking_level);
1648 self.thinking_config = Some(config);
1649 self
1650 }
1651
1652 pub fn with_thinking_budget(mut self, thinking_budget: i32) -> Self {
1658 let mut config = self.thinking_config.unwrap_or_default();
1659 config.thinking_budget = Some(thinking_budget);
1660 self.thinking_config = Some(config);
1661 self
1662 }
1663
1664 pub fn validate_openai_compatibility(&self) -> Result<(), String> {
1673 if let Some(temp) = self.temperature {
1675 if !(0.0..=2.0).contains(&temp) {
1676 return Err(format!(
1677 "Temperature must be between 0.0 and 2.0, got {}",
1678 temp
1679 ));
1680 }
1681 }
1682
1683 if let Some(top_p) = self.top_p {
1685 if !(0.0..=1.0).contains(&top_p) {
1686 return Err(format!("Top-p must be between 0.0 and 1.0, got {}", top_p));
1687 }
1688 }
1689
1690 if let Some(fp) = self.frequency_penalty {
1692 if !(-2.0..=2.0).contains(&fp) {
1693 return Err(format!(
1694 "Frequency penalty must be between -2.0 and 2.0, got {}",
1695 fp
1696 ));
1697 }
1698 }
1699
1700 if let Some(pp) = self.presence_penalty {
1702 if !(-2.0..=2.0).contains(&pp) {
1703 return Err(format!(
1704 "Presence penalty must be between -2.0 and 2.0, got {}",
1705 pp
1706 ));
1707 }
1708 }
1709
1710 if let Some(mt) = self.max_tokens {
1712 if mt == 0 {
1713 return Err("Max tokens must be greater than 0".to_string());
1714 }
1715 }
1716
1717 if let Some(tlp) = self.top_logprobs {
1719 if !(0..=20).contains(&tlp) {
1720 return Err(format!(
1721 "Top logprobs must be between 0 and 20, got {}",
1722 tlp
1723 ));
1724 }
1725 }
1726
1727 if let Some(n) = self.n {
1729 if n == 0 {
1730 return Err("n must be greater than 0".to_string());
1731 }
1732 }
1733
1734 if let Some(stop) = &self.stop {
1736 if stop.len() > 4 {
1737 return Err("Cannot have more than 4 stop sequences".to_string());
1738 }
1739 for seq in stop {
1740 if seq.is_empty() {
1741 return Err("Stop sequences cannot be empty".to_string());
1742 }
1743 if seq.len() > 64 {
1744 return Err("Stop sequences cannot be longer than 64 characters".to_string());
1745 }
1746 }
1747 }
1748
1749 if let Some(thinking_config) = &self.thinking_config {
1751 self.validate_thinking_config(thinking_config)?;
1752 }
1753
1754 Ok(())
1755 }
1756
1757 fn validate_thinking_config(&self, config: &ThinkingConfig) -> Result<(), String> {
1759 let is_gemini_3 = self.model.contains("gemini-3");
1760 let is_gemini_2_5 = self.model.contains("gemini-2.5");
1761 let is_gemini_3_pro = self.model.contains("gemini-3-pro");
1762
1763 if let Some(level) = &config.thinking_level {
1765 if !is_gemini_3 {
1766 return Err("thinking_level is only supported for Gemini 3 models".to_string());
1767 }
1768
1769 match level {
1770 ThinkingLevel::Minimal | ThinkingLevel::Medium => {
1771 if is_gemini_3_pro {
1772 return Err(
1773 "Gemini 3 Pro only supports 'low' and 'high' thinking levels"
1774 .to_string(),
1775 );
1776 }
1777 }
1778 _ => {}
1779 }
1780 }
1781
1782 if let Some(budget) = config.thinking_budget {
1784 if !is_gemini_2_5 {
1785 return Err("thinking_budget is only supported for Gemini 2.5 models".to_string());
1786 }
1787
1788 if self.model.contains("2.5-pro") {
1790 if budget != -1 && !(128..=32768).contains(&budget) {
1791 return Err(
1792 "Gemini 2.5 Pro thinking budget must be -1 (dynamic) or between 128-32768"
1793 .to_string(),
1794 );
1795 }
1796 } else if self.model.contains("2.5-flash")
1797 && budget != -1
1798 && !(0..=24576).contains(&budget)
1799 {
1800 return Err(
1801 "Gemini 2.5 Flash thinking budget must be -1 (dynamic) or between 0-24576"
1802 .to_string(),
1803 );
1804 }
1805 }
1806
1807 if config.thinking_level.is_some() && config.thinking_budget.is_some() {
1809 return Err("Cannot specify both thinking_level (Gemini 3) and thinking_budget (Gemini 2.5) in the same request".to_string());
1810 }
1811
1812 Ok(())
1813 }
1814
1815 pub fn supports_thinking(&self) -> bool {
1817 self.model.contains("gemini-3") || self.model.contains("gemini-2.5")
1818 }
1819
1820 pub fn requires_thought_signatures(&self) -> bool {
1822 self.model.contains("gemini-3")
1823 }
1824}
1825
1826impl OpenAIChatCompletionRequest {
1827 pub fn new(model: impl Into<String>, messages: Vec<OpenAIChatMessage>) -> Self {
1829 Self {
1830 model: model.into(),
1831 messages,
1832 temperature: None,
1833 max_tokens: None,
1834 top_p: None,
1835 frequency_penalty: None,
1836 presence_penalty: None,
1837 stop: None,
1838 user: None,
1839 provider: None,
1840 stream: None,
1841 logit_bias: None,
1842 logprobs: None,
1843 top_logprobs: None,
1844 n: None,
1845 response_format: None,
1846 tools: None,
1847 tool_choice: None,
1848 thinking_config: None,
1849 thinking: None,
1850 }
1851 }
1852
1853 pub fn with_temperature(mut self, temperature: f32) -> Self {
1855 self.temperature = Some(temperature.clamp(0.0, 2.0));
1856 self
1857 }
1858
1859 pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
1861 self.max_tokens = Some(max_tokens);
1862 self
1863 }
1864
1865 pub fn with_user(mut self, user: impl Into<String>) -> Self {
1867 self.user = Some(user.into());
1868 self
1869 }
1870
1871 pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
1873 self.provider = Some(provider.into());
1874 self
1875 }
1876
1877 pub fn with_stream(mut self, stream: bool) -> Self {
1879 self.stream = Some(stream);
1880 self
1881 }
1882
1883 pub fn with_top_p(mut self, top_p: f32) -> Self {
1885 self.top_p = Some(top_p.clamp(0.0, 1.0));
1886 self
1887 }
1888
1889 pub fn with_frequency_penalty(mut self, frequency_penalty: f32) -> Self {
1891 self.frequency_penalty = Some(frequency_penalty.clamp(-2.0, 2.0));
1892 self
1893 }
1894
1895 pub fn with_presence_penalty(mut self, presence_penalty: f32) -> Self {
1897 self.presence_penalty = Some(presence_penalty.clamp(-2.0, 2.0));
1898 self
1899 }
1900
1901 pub fn with_stop(mut self, stop: Vec<String>) -> Self {
1903 self.stop = Some(stop);
1904 self
1905 }
1906
1907 pub fn with_logit_bias(mut self, logit_bias: serde_json::Value) -> Self {
1909 self.logit_bias = Some(logit_bias);
1910 self
1911 }
1912
1913 pub fn with_logprobs(mut self, logprobs: bool) -> Self {
1915 self.logprobs = Some(logprobs);
1916 self
1917 }
1918
1919 pub fn with_top_logprobs(mut self, top_logprobs: u32) -> Self {
1921 self.top_logprobs = Some(top_logprobs);
1922 self
1923 }
1924
1925 pub fn with_n(mut self, n: u32) -> Self {
1927 self.n = Some(n);
1928 self
1929 }
1930
1931 pub fn with_response_format(mut self, response_format: ResponseFormat) -> Self {
1933 self.response_format = Some(response_format);
1934 self
1935 }
1936
1937 pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
1939 self.tools = Some(tools);
1940 self
1941 }
1942
1943 pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
1945 self.tool_choice = Some(tool_choice);
1946 self
1947 }
1948
1949 pub fn with_thinking_config(mut self, thinking_config: ThinkingConfig) -> Self {
1951 self.thinking_config = Some(thinking_config);
1952 self
1953 }
1954
1955 pub fn with_include_thoughts(mut self, include_thoughts: bool) -> Self {
1957 let mut config = self.thinking_config.unwrap_or_default();
1958 config.include_thoughts = Some(include_thoughts);
1959 self.thinking_config = Some(config);
1960 self
1961 }
1962
1963 pub fn with_thinking_level(mut self, thinking_level: ThinkingLevel) -> Self {
1965 let mut config = self.thinking_config.unwrap_or_default();
1966 config.thinking_level = Some(thinking_level);
1967 self.thinking_config = Some(config);
1968 self
1969 }
1970
1971 pub fn with_thinking_budget(mut self, thinking_budget: i32) -> Self {
1973 let mut config = self.thinking_config.unwrap_or_default();
1974 config.thinking_budget = Some(thinking_budget);
1975 self.thinking_config = Some(config);
1976 self
1977 }
1978
1979 pub fn with_anthropic_thinking(mut self, budget_tokens: i32) -> Self {
1984 self.thinking =
1985 Some(serde_json::json!({"type": "enabled", "budget_tokens": budget_tokens}));
1986 self
1987 }
1988
1989 pub fn validate_openai_compatibility(&self) -> Result<(), String> {
1991 ChatCompletionRequest {
1992 model: self.model.clone(),
1993 messages: vec![],
1994 temperature: self.temperature,
1995 max_tokens: self.max_tokens,
1996 top_p: self.top_p,
1997 frequency_penalty: self.frequency_penalty,
1998 presence_penalty: self.presence_penalty,
1999 stop: self.stop.clone(),
2000 user: self.user.clone(),
2001 provider: self.provider.clone(),
2002 stream: self.stream,
2003 logit_bias: self.logit_bias.clone(),
2004 logprobs: self.logprobs,
2005 top_logprobs: self.top_logprobs,
2006 n: self.n,
2007 response_format: self.response_format.clone(),
2008 tools: self.tools.clone(),
2009 tool_choice: self.tool_choice.clone(),
2010 thinking_config: self.thinking_config.clone(),
2011 }
2012 .validate_openai_compatibility()
2013 }
2014
2015 pub fn supports_thinking(&self) -> bool {
2017 self.model.contains("gemini-3") || self.model.contains("gemini-2.5")
2018 }
2019
2020 pub fn requires_thought_signatures(&self) -> bool {
2022 self.model.contains("gemini-3")
2023 }
2024}
2025
2026impl ChatMessage {
2027 pub fn system(content: impl Into<String>) -> Self {
2033 Self {
2034 role: MessageRole::System,
2035 content: content.into(),
2036 }
2037 }
2038
2039 pub fn user(content: impl Into<String>) -> Self {
2045 Self {
2046 role: MessageRole::User,
2047 content: content.into(),
2048 }
2049 }
2050
2051 pub fn assistant(content: impl Into<String>) -> Self {
2057 Self {
2058 role: MessageRole::Assistant,
2059 content: content.into(),
2060 }
2061 }
2062}
2063
2064impl OpenAIMessageContent {
2065 pub fn text(content: impl Into<String>) -> Self {
2067 Self::Text(content.into())
2068 }
2069
2070 pub fn parts(parts: Vec<OpenAIContentPart>) -> Self {
2072 Self::Parts(parts)
2073 }
2074}
2075
2076impl OpenAIContentPart {
2077 pub fn text(content: impl Into<String>) -> Self {
2079 Self::Text {
2080 text: content.into(),
2081 }
2082 }
2083
2084 pub fn image_url(url: impl Into<String>) -> Self {
2086 Self::ImageUrl {
2087 image_url: OpenAIImageUrl {
2088 url: url.into(),
2089 detail: None,
2090 },
2091 }
2092 }
2093
2094 pub fn image_url_with_detail(url: impl Into<String>, detail: impl Into<String>) -> Self {
2096 Self::ImageUrl {
2097 image_url: OpenAIImageUrl {
2098 url: url.into(),
2099 detail: Some(detail.into()),
2100 },
2101 }
2102 }
2103}
2104
2105impl OpenAIChatMessage {
2106 pub fn system(content: impl Into<OpenAIMessageContent>) -> Self {
2108 Self {
2109 role: OpenAIMessageRole::System,
2110 content: Some(content.into()),
2111 name: None,
2112 tool_calls: None,
2113 tool_call_id: None,
2114 }
2115 }
2116
2117 pub fn user(content: impl Into<OpenAIMessageContent>) -> Self {
2119 Self {
2120 role: OpenAIMessageRole::User,
2121 content: Some(content.into()),
2122 name: None,
2123 tool_calls: None,
2124 tool_call_id: None,
2125 }
2126 }
2127
2128 pub fn assistant(content: impl Into<OpenAIMessageContent>) -> Self {
2130 Self {
2131 role: OpenAIMessageRole::Assistant,
2132 content: Some(content.into()),
2133 name: None,
2134 tool_calls: None,
2135 tool_call_id: None,
2136 }
2137 }
2138
2139 pub fn assistant_with_tool_calls(tool_calls: Vec<OpenAIToolCall>) -> Self {
2141 Self {
2142 role: OpenAIMessageRole::Assistant,
2143 content: None,
2144 name: None,
2145 tool_calls: Some(tool_calls),
2146 tool_call_id: None,
2147 }
2148 }
2149
2150 pub fn tool(tool_call_id: impl Into<String>, content: impl Into<OpenAIMessageContent>) -> Self {
2152 Self {
2153 role: OpenAIMessageRole::Tool,
2154 content: Some(content.into()),
2155 name: None,
2156 tool_calls: None,
2157 tool_call_id: Some(tool_call_id.into()),
2158 }
2159 }
2160
2161 pub fn with_parts(
2163 role: OpenAIMessageRole,
2164 content: Option<OpenAIMessageContent>,
2165 tool_calls: Option<Vec<OpenAIToolCall>>,
2166 tool_call_id: Option<String>,
2167 ) -> Self {
2168 Self {
2169 role,
2170 content,
2171 name: None,
2172 tool_calls,
2173 tool_call_id,
2174 }
2175 }
2176}
2177
2178impl From<String> for OpenAIMessageContent {
2179 fn from(value: String) -> Self {
2180 Self::Text(value)
2181 }
2182}
2183
2184impl From<&str> for OpenAIMessageContent {
2185 fn from(value: &str) -> Self {
2186 Self::Text(value.to_string())
2187 }
2188}
2189
2190use uuid::Uuid;
2192
2193#[derive(Debug, Clone, Serialize, Deserialize)]
2195pub struct User {
2196 pub id: Uuid,
2198 pub user_id: String,
2200 pub plan_name: String,
2202 pub current_credits: f64,
2204 pub credits_used_this_month: f64,
2206 pub credits_reset_date: DateTime<Utc>,
2208 pub is_active: bool,
2210 pub created_at: DateTime<Utc>,
2212}
2213
2214#[derive(Debug, Clone, Serialize, Deserialize)]
2216pub struct ApiKey {
2217 pub id: Uuid,
2219 pub key: String,
2221 pub owner_id: Uuid,
2223 pub is_active: bool,
2225 pub created_at: DateTime<Utc>,
2227 pub expires_at: Option<DateTime<Utc>>,
2229 pub description: Option<String>,
2231 pub last_used_at: Option<DateTime<Utc>>,
2233}
2234
2235#[derive(Debug, Clone, Serialize, Deserialize)]
2237pub struct UsageStats {
2238 pub period_days: u32,
2240 pub daily_usage: Vec<DailyUsage>,
2242 pub recent_transactions: Vec<CreditTransaction>,
2244 pub total_requests: u64,
2246 pub total_tokens: u64,
2248}
2249
2250#[derive(Debug, Clone, Serialize, Deserialize)]
2252pub struct DailyUsage {
2253 pub date: String,
2255 pub credits_used: f64,
2257 pub requests: u64,
2259 pub tokens: u64,
2261}
2262
2263#[derive(Debug, Clone, Serialize, Deserialize)]
2265pub struct CreditTransaction {
2266 pub id: Uuid,
2268 pub transaction_type: TransactionType,
2270 pub credits_amount: f64,
2272 pub credits_balance_after: f64,
2274 pub provider: Option<String>,
2276 pub model: Option<String>,
2278 pub description: String,
2280 pub created_at: DateTime<Utc>,
2282}
2283
2284#[derive(Debug, Clone, Serialize, Deserialize)]
2286#[serde(rename_all = "lowercase")]
2287pub enum TransactionType {
2288 Usage,
2290 Reset,
2292 Purchase,
2294 Refund,
2296}
2297
2298pub type ChatRole = MessageRole;
2301pub type ChatUsage = Usage;
2303pub type HealthCheck = HealthStatus;
2305
2306#[derive(Debug, Clone, Serialize, Deserialize)]
2308pub struct HealthServices {
2309 pub database: bool,
2311 pub redis: bool,
2313 pub providers: bool,
2315}
2316
2317#[derive(Debug, Clone, Serialize, Deserialize)]
2319#[serde(rename_all = "lowercase")]
2320pub enum HealthStatusEnum {
2321 Healthy,
2323 Degraded,
2325 Unhealthy,
2327 NeedsInit,
2329}
2330
2331#[derive(Debug, Clone, Serialize, Deserialize)]
2333#[serde(rename_all = "snake_case")]
2334pub enum ResponseFormat {
2335 Text,
2337 JsonObject,
2339 JsonSchema {
2341 json_schema: serde_json::Value,
2343 },
2344}
2345
2346#[derive(Debug, Clone, Serialize, Deserialize)]
2348pub struct Tool {
2349 pub r#type: ToolType,
2351 pub function: FunctionDefinition,
2353}
2354
2355#[derive(Debug, Clone, Serialize, Deserialize)]
2357#[serde(rename_all = "snake_case")]
2358pub enum ToolType {
2359 Function,
2361}
2362
2363#[derive(Debug, Clone, Serialize, Deserialize)]
2365pub struct FunctionDefinition {
2366 pub name: String,
2368 #[serde(skip_serializing_if = "Option::is_none")]
2370 pub description: Option<String>,
2371 #[serde(skip_serializing_if = "Option::is_none")]
2373 pub parameters: Option<serde_json::Value>,
2374}
2375
2376#[derive(Debug, Clone, Serialize, Deserialize)]
2378#[serde(untagged)]
2379pub enum ToolChoice {
2380 None,
2382 Auto,
2384 Tool {
2386 r#type: ToolType,
2388 function: ToolFunction,
2390 },
2391}
2392
2393#[derive(Debug, Clone, Serialize, Deserialize)]
2395pub struct ToolFunction {
2396 pub name: String,
2398}
2399
2400#[derive(Debug, Clone, Serialize, Deserialize, Default)]
2402pub struct ThinkingConfig {
2403 #[serde(skip_serializing_if = "Option::is_none")]
2405 pub include_thoughts: Option<bool>,
2406
2407 #[serde(skip_serializing_if = "Option::is_none")]
2409 pub thinking_level: Option<ThinkingLevel>,
2410
2411 #[serde(skip_serializing_if = "Option::is_none")]
2413 pub thinking_budget: Option<i32>,
2414}
2415
2416#[derive(Debug, Clone, Serialize, Deserialize)]
2418#[serde(rename_all = "lowercase")]
2419pub enum ThinkingLevel {
2420 Minimal,
2422 Low,
2424 Medium,
2426 High,
2428}
2429
2430#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2432pub struct ContentPart {
2433 #[serde(skip_serializing_if = "Option::is_none")]
2435 pub text: Option<String>,
2436
2437 #[serde(skip_serializing_if = "Option::is_none")]
2439 pub function_call: Option<FunctionCall>,
2440
2441 #[serde(skip_serializing_if = "Option::is_none")]
2443 pub function_response: Option<FunctionResponse>,
2444
2445 #[serde(skip_serializing_if = "Option::is_none")]
2447 pub thought: Option<bool>,
2448
2449 #[serde(skip_serializing_if = "Option::is_none")]
2451 pub thought_signature: Option<String>,
2452}
2453
2454#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2456pub struct FunctionCall {
2457 pub name: String,
2459 pub args: serde_json::Value,
2461}
2462
2463#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2465pub struct FunctionResponse {
2466 pub name: String,
2468 pub response: serde_json::Value,
2470}
2471
2472#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2474pub struct EnhancedChatMessage {
2475 pub role: MessageRole,
2477 pub parts: Vec<ContentPart>,
2479}
2480
2481#[derive(Debug, Clone, Serialize, Deserialize)]
2483pub struct EnhancedUsage {
2484 pub prompt_tokens: u32,
2486 pub completion_tokens: u32,
2488 pub total_tokens: u32,
2490 #[serde(skip_serializing_if = "Option::is_none")]
2492 pub thoughts_token_count: Option<u32>,
2493}
2494
2495impl ThinkingConfig {
2496 pub fn new() -> Self {
2498 Self::default()
2499 }
2500
2501 pub fn gemini_3(level: ThinkingLevel, include_thoughts: bool) -> Self {
2508 Self {
2509 thinking_level: Some(level),
2510 include_thoughts: Some(include_thoughts),
2511 thinking_budget: None,
2512 }
2513 }
2514
2515 pub fn gemini_2_5(budget: i32, include_thoughts: bool) -> Self {
2522 Self {
2523 thinking_budget: Some(budget),
2524 include_thoughts: Some(include_thoughts),
2525 thinking_level: None,
2526 }
2527 }
2528
2529 pub fn high_reasoning() -> Self {
2531 Self {
2532 thinking_level: Some(ThinkingLevel::High),
2533 include_thoughts: Some(true),
2534 thinking_budget: Some(-1), }
2536 }
2537
2538 pub fn fast_response() -> Self {
2540 Self {
2541 thinking_level: Some(ThinkingLevel::Low),
2542 include_thoughts: Some(false),
2543 thinking_budget: Some(512), }
2545 }
2546}
2547
2548impl ContentPart {
2549 pub fn text(content: impl Into<String>) -> Self {
2551 Self {
2552 text: Some(content.into()),
2553 function_call: None,
2554 function_response: None,
2555 thought: None,
2556 thought_signature: None,
2557 }
2558 }
2559
2560 pub fn function_call(name: impl Into<String>, args: serde_json::Value) -> Self {
2562 Self {
2563 text: None,
2564 function_call: Some(FunctionCall {
2565 name: name.into(),
2566 args,
2567 }),
2568 function_response: None,
2569 thought: None,
2570 thought_signature: None,
2571 }
2572 }
2573
2574 pub fn function_response(name: impl Into<String>, response: serde_json::Value) -> Self {
2576 Self {
2577 text: None,
2578 function_call: None,
2579 function_response: Some(FunctionResponse {
2580 name: name.into(),
2581 response,
2582 }),
2583 thought: None,
2584 thought_signature: None,
2585 }
2586 }
2587
2588 pub fn with_thought_signature(mut self, signature: impl Into<String>) -> Self {
2590 self.thought_signature = Some(signature.into());
2591 self
2592 }
2593
2594 pub fn as_thought(mut self) -> Self {
2596 self.thought = Some(true);
2597 self
2598 }
2599}
2600
2601impl EnhancedChatMessage {
2602 pub fn system(content: impl Into<String>) -> Self {
2604 Self {
2605 role: MessageRole::System,
2606 parts: vec![ContentPart::text(content)],
2607 }
2608 }
2609
2610 pub fn user(content: impl Into<String>) -> Self {
2612 Self {
2613 role: MessageRole::User,
2614 parts: vec![ContentPart::text(content)],
2615 }
2616 }
2617
2618 pub fn assistant(content: impl Into<String>) -> Self {
2620 Self {
2621 role: MessageRole::Assistant,
2622 parts: vec![ContentPart::text(content)],
2623 }
2624 }
2625
2626 pub fn with_parts(role: MessageRole, parts: Vec<ContentPart>) -> Self {
2628 Self { role, parts }
2629 }
2630}
2631
2632#[derive(Debug, Clone, Serialize, Deserialize)]
2634pub struct ChatCompletionStreamResponse {
2635 pub id: String,
2637 pub object: String,
2639 pub created: u64,
2641 pub model: String,
2643 pub choices: Vec<ChatCompletionStreamChoice>,
2645 #[serde(skip_serializing_if = "Option::is_none")]
2647 pub usage: Option<Usage>,
2648}
2649
2650#[derive(Debug, Clone, Serialize, Deserialize)]
2652pub struct ChatCompletionStreamChoice {
2653 pub index: u32,
2655 pub delta: ChatCompletionStreamDelta,
2657 #[serde(skip_serializing_if = "Option::is_none")]
2659 pub finish_reason: Option<String>,
2660}
2661
2662#[derive(Debug, Clone, Serialize, Deserialize)]
2664pub struct ChatCompletionStreamDelta {
2665 #[serde(skip_serializing_if = "Option::is_none")]
2667 pub role: Option<String>,
2668 #[serde(skip_serializing_if = "Option::is_none")]
2670 pub content: Option<String>,
2671 #[serde(skip_serializing_if = "Option::is_none")]
2673 pub thought: Option<String>,
2674 #[serde(skip_serializing_if = "Option::is_none")]
2676 pub tool_calls: Option<Vec<ToolCall>>,
2677}
2678
2679#[derive(Debug, Clone, Serialize, Deserialize)]
2681pub struct ToolCall {
2682 pub index: u32,
2684 #[serde(skip_serializing_if = "Option::is_none")]
2686 pub id: Option<String>,
2687 #[serde(skip_serializing_if = "Option::is_none")]
2689 pub r#type: Option<String>,
2690 #[serde(skip_serializing_if = "Option::is_none")]
2692 pub function: Option<ToolCallFunction>,
2693}
2694
2695#[derive(Debug, Clone, Serialize, Deserialize)]
2697pub struct ToolCallFunction {
2698 #[serde(skip_serializing_if = "Option::is_none")]
2700 pub name: Option<String>,
2701 #[serde(skip_serializing_if = "Option::is_none")]
2703 pub arguments: Option<String>,
2704}