Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/cablehead_yoke.json
T

1 line
68 KiB
JSON

{"repo": "cablehead/yoke", "files": {"yoagent/src/provider/model.rs": {"90b5fb91b4c513fb0851fc808e61c5c94ba940bf": {"t": 1785746230.3369272, "n": 17169, "h": "4af92d106dda6ff5", "c": "//! Model configuration and provider compatibility flags.\n\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\n\n/// Which API protocol a model uses.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum ApiProtocol {\n AnthropicMessages,\n OpenAiCompletions,\n OpenAiResponses,\n AzureOpenAiResponses,\n GoogleGenerativeAi,\n GoogleVertex,\n BedrockConverseStream,\n}\n\nimpl std::fmt::Display for ApiProtocol {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n Self::AnthropicMessages => write!(f, \"anthropic_messages\"),\n Self::OpenAiCompletions => write!(f, \"openai_completions\"),\n Self::OpenAiResponses => write!(f, \"openai_responses\"),\n Self::AzureOpenAiResponses => write!(f, \"azure_openai_responses\"),\n Self::GoogleGenerativeAi => write!(f, \"google_generative_ai\"),\n Self::GoogleVertex => write!(f, \"google_vertex\"),\n Self::BedrockConverseStream => write!(f, \"bedrock_converse_stream\"),\n }\n }\n}\n\n/// Cost per million tokens (input/output).\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CostConfig {\n pub input_per_million: f64,\n pub output_per_million: f64,\n #[serde(default)]\n pub cache_read_per_million: f64,\n #[serde(default)]\n pub cache_write_per_million: f64,\n}\n\nimpl Default for CostConfig {\n fn default() -> Self {\n Self {\n input_per_million: 0.0,\n output_per_million: 0.0,\n cache_read_per_million: 0.0,\n cache_write_per_million: 0.0,\n }\n }\n}\n\n/// How a provider handles the `max_tokens` field.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]\n#[serde(rename_all = \"snake_case\")]\npub enum MaxTokensField {\n #[default]\n MaxTokens,\n MaxCompletionTokens,\n}\n\n/// How an OpenAI-compatible endpoint accepts reasoning/effort controls.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]\n#[serde(rename_all = \"snake_case\")]\npub enum ReasoningStyle {\n /// Endpoint ignores reasoning params; send nothing.\n #[default]\n None,\n /// OpenAI chat-completions `reasoning_effort` string (no explicit disable).\n Effort,\n /// OpenRouter unified `reasoning: { effort }` object (supports `\"none\"` to disable).\n Object,\n}\n\n/// How a provider formats thinking/reasoning output.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]\n#[serde(rename_all = \"snake_case\")]\npub enum ThinkingFormat {\n #[default]\n OpenAi,\n Xai,\n Qwen,\n}\n\n/// Compatibility flags for OpenAI-compatible providers.\n/// Different providers have different quirks even though they share the same base API.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct OpenAiCompat {\n /// Supports the `store` parameter for conversation persistence.\n pub supports_store: bool,\n /// Supports `developer` role (system-level instructions).\n pub supports_developer_role: bool,\n /// How this endpoint accepts reasoning/effort controls.\n pub reasoning_style: ReasoningStyle,\n /// Includes usage data in streaming responses.\n pub supports_usage_in_streaming: bool,\n /// Which field name to use for max tokens.\n pub max_tokens_field: MaxTokensField,\n /// Tool results must include a `name` field.\n pub requires_tool_result_name: bool,\n /// Must insert an assistant message after tool results.\n pub requires_assistant_after_tool_result: bool,\n /// How thinking/reasoning content is formatted in streaming.\n pub thinking_format: ThinkingFormat,\n}\n\nimpl Default for OpenAiCompat {\n fn default() -> Self {\n Self {\n supports_store: false,\n supports_developer_role: false,\n reasoning_style: ReasoningStyle::None,\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxTokens,\n requires_tool_result_name: false,\n requires_assistant_after_tool_result: false,\n thinking_format: ThinkingFormat::OpenAi,\n }\n }\n}\n\nimpl OpenAiCompat {\n /// Compat flags for native OpenAI.\n pub fn openai() -> Self {\n Self {\n supports_store: true,\n supports_developer_role: true,\n reasoning_style: ReasoningStyle::Effort,\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for xAI (Grok).\n pub fn xai() -> Self {\n Self {\n supports_usage_in_streaming: true,\n thinking_format: ThinkingFormat::Xai,\n ..Default::default()\n }\n }\n\n /// Compat flags for Groq.\n pub fn groq() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n\n /// Compat flags for Cerebras.\n pub fn cerebras() -> Self {\n Self::default()\n }\n\n /// Compat flags for OpenRouter.\n pub fn openrouter() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n reasoning_style: ReasoningStyle::Object,\n ..Default::default()\n }\n }\n\n /// Compat flags for Mistral.\n pub fn mistral() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for DeepSeek.\n pub fn deepseek() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for Z.ai (Zhipu AI).\n pub fn zai() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n\n /// Compat flags for MiniMax.\n pub fn minimax() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n}\n\n/// Full model configuration. Knows everything needed to make API calls.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ModelConfig {\n /// Model identifier sent to the API (e.g. \"gpt-4o\", \"claude-sonnet-4-20250514\").\n pub id: String,\n /// Human-friendly name.\n pub name: String,\n /// Which API protocol to use.\n pub api: ApiProtocol,\n /// Provider name (e.g. \"openai\", \"anthropic\", \"xai\").\n pub provider: String,\n /// Base URL for API requests (without trailing slash).\n pub base_url: String,\n /// Whether this model supports reasoning/thinking.\n pub reasoning: bool,\n /// Context window size in tokens.\n pub context_window: u32,\n /// Default max output tokens.\n pub max_tokens: u32,\n /// Cost configuration.\n #[serde(default)]\n pub cost: CostConfig,\n /// Additional headers to send with requests.\n #[serde(default)]\n pub headers: HashMap<String, String>,\n /// OpenAI-compat quirk flags (only for OpenAiCompletions protocol).\n #[serde(default)]\n pub compat: Option<OpenAiCompat>,\n}\n\nimpl ModelConfig {\n /// Create a new Anthropic model config.\n pub fn anthropic(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::AnthropicMessages,\n provider: \"anthropic\".into(),\n base_url: \"https://api.anthropic.com\".into(),\n reasoning: false,\n context_window: 200_000,\n max_tokens: 8192,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: None,\n }\n }\n\n /// Create a new OpenAI model config.\n pub fn openai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"openai\".into(),\n base_url: \"https://api.openai.com/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::openai()),\n }\n }\n\n /// Create a config for a local OpenAI-compatible server (LM Studio, Ollama, etc.).\n /// No API key required — sends an empty Bearer token.\n pub fn local(base_url: impl Into<String>, model_id: impl Into<String>) -> Self {\n Self {\n id: model_id.into(),\n name: \"Local Model\".into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"local\".into(),\n base_url: base_url.into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::default()),\n }\n }\n\n /// Create a new Z.ai (Zhipu AI) model config.\n ///\n /// Models: `glm-4.7`, `glm-4.5-air`, `glm-5`, etc.\n pub fn zai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"zai\".into(),\n base_url: \"https://api.z.ai/api/paas/v4\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::zai()),\n }\n }\n\n /// Create a new MiniMax model config.\n ///\n /// Models: `MiniMax-Text-01`, `MiniMax-M1`, etc.\n pub fn minimax(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"minimax\".into(),\n base_url: \"https://api.minimaxi.chat/v1\".into(),\n reasoning: false,\n context_window: 1_000_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::minimax()),\n }\n }\n\n /// Create a new xAI (Grok) model config.\n ///\n /// Models: `grok-3-mini`, `grok-3`, etc.\n pub fn xai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"xai\".into(),\n base_url: \"https://api.x.ai/v1\".into(),\n reasoning: false,\n context_window: 131_072,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::xai()),\n }\n }\n\n /// Create a new Groq model config.\n ///\n /// Models: `llama-3.3-70b-versatile`, `mixtral-8x7b-32768`, etc.\n pub fn groq(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"groq\".into(),\n base_url: \"https://api.groq.com/openai/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::groq()),\n }\n }\n\n /// Create a new DeepSeek model config.\n ///\n /// Models: `deepseek-chat`, `deepseek-reasoner`, etc.\n pub fn deepseek(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"deepseek\".into(),\n base_url: \"https://api.deepseek.com/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::deepseek()),\n }\n }\n\n /// Create a new OpenRouter model config.\n ///\n /// Models: `openai/gpt-4o-mini`, `anthropic/claude-3.5-sonnet`, etc.\n pub fn openrouter(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"openrouter\".into(),\n base_url: \"https://openrouter.ai/api/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::openrouter()),\n }\n }\n\n /// Create a new Mistral model config.\n ///\n /// Models: `mistral-large-latest`, `mistral-small-latest`, etc.\n pub fn mistral(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"mistral\".into(),\n base_url: \"https://api.mistral.ai/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::mistral()),\n }\n }\n\n /// Create a new Google Generative AI (Gemini) model config.\n pub fn google(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::GoogleGenerativeAi,\n provider: \"google\".into(),\n base_url: \"https://generativelanguage.googleapis.com\".into(),\n reasoning: false,\n context_window: 1_000_000,\n max_tokens: 8192,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: None,\n }\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_model_config_anthropic() {\n let config = ModelConfig::anthropic(\"claude-sonnet-4-20250514\", \"Claude Sonnet 4\");\n assert_eq!(config.api, ApiProtocol::AnthropicMessages);\n assert_eq!(config.provider, \"anthropic\");\n assert!(config.compat.is_none());\n }\n\n #[test]\n fn test_model_config_openai() {\n let config = ModelConfig::openai(\"gpt-4o\", \"GPT-4o\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n let compat = config.compat.unwrap();\n assert!(compat.supports_store);\n assert!(compat.supports_developer_role);\n assert_eq!(compat.max_tokens_field, MaxTokensField::MaxCompletionTokens);\n }\n\n #[test]\n fn test_openai_compat_variants() {\n let xai = OpenAiCompat::xai();\n assert_eq!(xai.thinking_format, ThinkingFormat::Xai);\n assert!(!xai.supports_store);\n\n let groq = OpenAiCompat::groq();\n assert!(groq.supports_usage_in_streaming);\n assert!(!groq.supports_store);\n\n let deepseek = OpenAiCompat::deepseek();\n assert_eq!(\n deepseek.max_tokens_field,\n MaxTokensField::MaxCompletionTokens\n );\n\n let zai = OpenAiCompat::zai();\n assert!(zai.supports_usage_in_streaming);\n assert!(!zai.supports_store);\n\n let minimax = OpenAiCompat::minimax();\n assert!(minimax.supports_usage_in_streaming);\n assert!(!minimax.supports_store);\n }\n\n #[test]\n fn test_model_config_zai() {\n let config = ModelConfig::zai(\"glm-4.7\", \"GLM 4.7\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n assert_eq!(config.provider, \"zai\");\n assert_eq!(config.base_url, \"https://api.z.ai/api/paas/v4\");\n assert!(config.compat.is_some());\n }\n\n #[test]\n fn test_model_config_minimax() {\n let config = ModelConfig::minimax(\"MiniMax-Text-01\", \"MiniMax Text 01\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n assert_eq!(config.provider, \"minimax\");\n assert_eq!(config.base_url, \"https://api.minimaxi.chat/v1\");\n assert_eq!(config.context_window, 1_000_000);\n assert!(config.compat.is_some());\n }\n\n #[test]\n fn test_api_protocol_display() {\n assert_eq!(\n ApiProtocol::AnthropicMessages.to_string(),\n \"anthropic_messages\"\n );\n assert_eq!(\n ApiProtocol::OpenAiCompletions.to_string(),\n \"openai_completions\"\n );\n assert_eq!(\n ApiProtocol::GoogleGenerativeAi.to_string(),\n \"google_generative_ai\"\n );\n }\n\n #[test]\n fn test_cost_config_default() {\n let cost = CostConfig::default();\n assert_eq!(cost.input_per_million, 0.0);\n assert_eq!(cost.output_per_million, 0.0);\n }\n}\n"}, "35f70a6c6d8d761fc81d65b0225e721918ddb6cb": {"t": 1785760848.607692, "n": 17169, "h": "4af92d106dda6ff5", "c": "//! Model configuration and provider compatibility flags.\n\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\n\n/// Which API protocol a model uses.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum ApiProtocol {\n AnthropicMessages,\n OpenAiCompletions,\n OpenAiResponses,\n AzureOpenAiResponses,\n GoogleGenerativeAi,\n GoogleVertex,\n BedrockConverseStream,\n}\n\nimpl std::fmt::Display for ApiProtocol {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n Self::AnthropicMessages => write!(f, \"anthropic_messages\"),\n Self::OpenAiCompletions => write!(f, \"openai_completions\"),\n Self::OpenAiResponses => write!(f, \"openai_responses\"),\n Self::AzureOpenAiResponses => write!(f, \"azure_openai_responses\"),\n Self::GoogleGenerativeAi => write!(f, \"google_generative_ai\"),\n Self::GoogleVertex => write!(f, \"google_vertex\"),\n Self::BedrockConverseStream => write!(f, \"bedrock_converse_stream\"),\n }\n }\n}\n\n/// Cost per million tokens (input/output).\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CostConfig {\n pub input_per_million: f64,\n pub output_per_million: f64,\n #[serde(default)]\n pub cache_read_per_million: f64,\n #[serde(default)]\n pub cache_write_per_million: f64,\n}\n\nimpl Default for CostConfig {\n fn default() -> Self {\n Self {\n input_per_million: 0.0,\n output_per_million: 0.0,\n cache_read_per_million: 0.0,\n cache_write_per_million: 0.0,\n }\n }\n}\n\n/// How a provider handles the `max_tokens` field.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]\n#[serde(rename_all = \"snake_case\")]\npub enum MaxTokensField {\n #[default]\n MaxTokens,\n MaxCompletionTokens,\n}\n\n/// How an OpenAI-compatible endpoint accepts reasoning/effort controls.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]\n#[serde(rename_all = \"snake_case\")]\npub enum ReasoningStyle {\n /// Endpoint ignores reasoning params; send nothing.\n #[default]\n None,\n /// OpenAI chat-completions `reasoning_effort` string (no explicit disable).\n Effort,\n /// OpenRouter unified `reasoning: { effort }` object (supports `\"none\"` to disable).\n Object,\n}\n\n/// How a provider formats thinking/reasoning output.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]\n#[serde(rename_all = \"snake_case\")]\npub enum ThinkingFormat {\n #[default]\n OpenAi,\n Xai,\n Qwen,\n}\n\n/// Compatibility flags for OpenAI-compatible providers.\n/// Different providers have different quirks even though they share the same base API.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct OpenAiCompat {\n /// Supports the `store` parameter for conversation persistence.\n pub supports_store: bool,\n /// Supports `developer` role (system-level instructions).\n pub supports_developer_role: bool,\n /// How this endpoint accepts reasoning/effort controls.\n pub reasoning_style: ReasoningStyle,\n /// Includes usage data in streaming responses.\n pub supports_usage_in_streaming: bool,\n /// Which field name to use for max tokens.\n pub max_tokens_field: MaxTokensField,\n /// Tool results must include a `name` field.\n pub requires_tool_result_name: bool,\n /// Must insert an assistant message after tool results.\n pub requires_assistant_after_tool_result: bool,\n /// How thinking/reasoning content is formatted in streaming.\n pub thinking_format: ThinkingFormat,\n}\n\nimpl Default for OpenAiCompat {\n fn default() -> Self {\n Self {\n supports_store: false,\n supports_developer_role: false,\n reasoning_style: ReasoningStyle::None,\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxTokens,\n requires_tool_result_name: false,\n requires_assistant_after_tool_result: false,\n thinking_format: ThinkingFormat::OpenAi,\n }\n }\n}\n\nimpl OpenAiCompat {\n /// Compat flags for native OpenAI.\n pub fn openai() -> Self {\n Self {\n supports_store: true,\n supports_developer_role: true,\n reasoning_style: ReasoningStyle::Effort,\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for xAI (Grok).\n pub fn xai() -> Self {\n Self {\n supports_usage_in_streaming: true,\n thinking_format: ThinkingFormat::Xai,\n ..Default::default()\n }\n }\n\n /// Compat flags for Groq.\n pub fn groq() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n\n /// Compat flags for Cerebras.\n pub fn cerebras() -> Self {\n Self::default()\n }\n\n /// Compat flags for OpenRouter.\n pub fn openrouter() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n reasoning_style: ReasoningStyle::Object,\n ..Default::default()\n }\n }\n\n /// Compat flags for Mistral.\n pub fn mistral() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for DeepSeek.\n pub fn deepseek() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for Z.ai (Zhipu AI).\n pub fn zai() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n\n /// Compat flags for MiniMax.\n pub fn minimax() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n}\n\n/// Full model configuration. Knows everything needed to make API calls.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ModelConfig {\n /// Model identifier sent to the API (e.g. \"gpt-4o\", \"claude-sonnet-4-20250514\").\n pub id: String,\n /// Human-friendly name.\n pub name: String,\n /// Which API protocol to use.\n pub api: ApiProtocol,\n /// Provider name (e.g. \"openai\", \"anthropic\", \"xai\").\n pub provider: String,\n /// Base URL for API requests (without trailing slash).\n pub base_url: String,\n /// Whether this model supports reasoning/thinking.\n pub reasoning: bool,\n /// Context window size in tokens.\n pub context_window: u32,\n /// Default max output tokens.\n pub max_tokens: u32,\n /// Cost configuration.\n #[serde(default)]\n pub cost: CostConfig,\n /// Additional headers to send with requests.\n #[serde(default)]\n pub headers: HashMap<String, String>,\n /// OpenAI-compat quirk flags (only for OpenAiCompletions protocol).\n #[serde(default)]\n pub compat: Option<OpenAiCompat>,\n}\n\nimpl ModelConfig {\n /// Create a new Anthropic model config.\n pub fn anthropic(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::AnthropicMessages,\n provider: \"anthropic\".into(),\n base_url: \"https://api.anthropic.com\".into(),\n reasoning: false,\n context_window: 200_000,\n max_tokens: 8192,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: None,\n }\n }\n\n /// Create a new OpenAI model config.\n pub fn openai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"openai\".into(),\n base_url: \"https://api.openai.com/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::openai()),\n }\n }\n\n /// Create a config for a local OpenAI-compatible server (LM Studio, Ollama, etc.).\n /// No API key required — sends an empty Bearer token.\n pub fn local(base_url: impl Into<String>, model_id: impl Into<String>) -> Self {\n Self {\n id: model_id.into(),\n name: \"Local Model\".into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"local\".into(),\n base_url: base_url.into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::default()),\n }\n }\n\n /// Create a new Z.ai (Zhipu AI) model config.\n ///\n /// Models: `glm-4.7`, `glm-4.5-air`, `glm-5`, etc.\n pub fn zai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"zai\".into(),\n base_url: \"https://api.z.ai/api/paas/v4\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::zai()),\n }\n }\n\n /// Create a new MiniMax model config.\n ///\n /// Models: `MiniMax-Text-01`, `MiniMax-M1`, etc.\n pub fn minimax(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"minimax\".into(),\n base_url: \"https://api.minimaxi.chat/v1\".into(),\n reasoning: false,\n context_window: 1_000_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::minimax()),\n }\n }\n\n /// Create a new xAI (Grok) model config.\n ///\n /// Models: `grok-3-mini`, `grok-3`, etc.\n pub fn xai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"xai\".into(),\n base_url: \"https://api.x.ai/v1\".into(),\n reasoning: false,\n context_window: 131_072,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::xai()),\n }\n }\n\n /// Create a new Groq model config.\n ///\n /// Models: `llama-3.3-70b-versatile`, `mixtral-8x7b-32768`, etc.\n pub fn groq(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"groq\".into(),\n base_url: \"https://api.groq.com/openai/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::groq()),\n }\n }\n\n /// Create a new DeepSeek model config.\n ///\n /// Models: `deepseek-chat`, `deepseek-reasoner`, etc.\n pub fn deepseek(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"deepseek\".into(),\n base_url: \"https://api.deepseek.com/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::deepseek()),\n }\n }\n\n /// Create a new OpenRouter model config.\n ///\n /// Models: `openai/gpt-4o-mini`, `anthropic/claude-3.5-sonnet`, etc.\n pub fn openrouter(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"openrouter\".into(),\n base_url: \"https://openrouter.ai/api/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::openrouter()),\n }\n }\n\n /// Create a new Mistral model config.\n ///\n /// Models: `mistral-large-latest`, `mistral-small-latest`, etc.\n pub fn mistral(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"mistral\".into(),\n base_url: \"https://api.mistral.ai/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::mistral()),\n }\n }\n\n /// Create a new Google Generative AI (Gemini) model config.\n pub fn google(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::GoogleGenerativeAi,\n provider: \"google\".into(),\n base_url: \"https://generativelanguage.googleapis.com\".into(),\n reasoning: false,\n context_window: 1_000_000,\n max_tokens: 8192,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: None,\n }\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_model_config_anthropic() {\n let config = ModelConfig::anthropic(\"claude-sonnet-4-20250514\", \"Claude Sonnet 4\");\n assert_eq!(config.api, ApiProtocol::AnthropicMessages);\n assert_eq!(config.provider, \"anthropic\");\n assert!(config.compat.is_none());\n }\n\n #[test]\n fn test_model_config_openai() {\n let config = ModelConfig::openai(\"gpt-4o\", \"GPT-4o\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n let compat = config.compat.unwrap();\n assert!(compat.supports_store);\n assert!(compat.supports_developer_role);\n assert_eq!(compat.max_tokens_field, MaxTokensField::MaxCompletionTokens);\n }\n\n #[test]\n fn test_openai_compat_variants() {\n let xai = OpenAiCompat::xai();\n assert_eq!(xai.thinking_format, ThinkingFormat::Xai);\n assert!(!xai.supports_store);\n\n let groq = OpenAiCompat::groq();\n assert!(groq.supports_usage_in_streaming);\n assert!(!groq.supports_store);\n\n let deepseek = OpenAiCompat::deepseek();\n assert_eq!(\n deepseek.max_tokens_field,\n MaxTokensField::MaxCompletionTokens\n );\n\n let zai = OpenAiCompat::zai();\n assert!(zai.supports_usage_in_streaming);\n assert!(!zai.supports_store);\n\n let minimax = OpenAiCompat::minimax();\n assert!(minimax.supports_usage_in_streaming);\n assert!(!minimax.supports_store);\n }\n\n #[test]\n fn test_model_config_zai() {\n let config = ModelConfig::zai(\"glm-4.7\", \"GLM 4.7\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n assert_eq!(config.provider, \"zai\");\n assert_eq!(config.base_url, \"https://api.z.ai/api/paas/v4\");\n assert!(config.compat.is_some());\n }\n\n #[test]\n fn test_model_config_minimax() {\n let config = ModelConfig::minimax(\"MiniMax-Text-01\", \"MiniMax Text 01\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n assert_eq!(config.provider, \"minimax\");\n assert_eq!(config.base_url, \"https://api.minimaxi.chat/v1\");\n assert_eq!(config.context_window, 1_000_000);\n assert!(config.compat.is_some());\n }\n\n #[test]\n fn test_api_protocol_display() {\n assert_eq!(\n ApiProtocol::AnthropicMessages.to_string(),\n \"anthropic_messages\"\n );\n assert_eq!(\n ApiProtocol::OpenAiCompletions.to_string(),\n \"openai_completions\"\n );\n assert_eq!(\n ApiProtocol::GoogleGenerativeAi.to_string(),\n \"google_generative_ai\"\n );\n }\n\n #[test]\n fn test_cost_config_default() {\n let cost = CostConfig::default();\n assert_eq!(cost.input_per_million, 0.0);\n assert_eq!(cost.output_per_million, 0.0);\n }\n}\n"}, "e9554e618c13b17aecfab6319d72a36e0569cb42": {"t": 1785760849.7880807, "n": 16596, "h": "572fb1465fc0a017", "c": "//! Model configuration and provider compatibility flags.\n\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\n\n/// Which API protocol a model uses.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum ApiProtocol {\n AnthropicMessages,\n OpenAiCompletions,\n OpenAiResponses,\n AzureOpenAiResponses,\n GoogleGenerativeAi,\n GoogleVertex,\n BedrockConverseStream,\n}\n\nimpl std::fmt::Display for ApiProtocol {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n Self::AnthropicMessages => write!(f, \"anthropic_messages\"),\n Self::OpenAiCompletions => write!(f, \"openai_completions\"),\n Self::OpenAiResponses => write!(f, \"openai_responses\"),\n Self::AzureOpenAiResponses => write!(f, \"azure_openai_responses\"),\n Self::GoogleGenerativeAi => write!(f, \"google_generative_ai\"),\n Self::GoogleVertex => write!(f, \"google_vertex\"),\n Self::BedrockConverseStream => write!(f, \"bedrock_converse_stream\"),\n }\n }\n}\n\n/// Cost per million tokens (input/output).\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CostConfig {\n pub input_per_million: f64,\n pub output_per_million: f64,\n #[serde(default)]\n pub cache_read_per_million: f64,\n #[serde(default)]\n pub cache_write_per_million: f64,\n}\n\nimpl Default for CostConfig {\n fn default() -> Self {\n Self {\n input_per_million: 0.0,\n output_per_million: 0.0,\n cache_read_per_million: 0.0,\n cache_write_per_million: 0.0,\n }\n }\n}\n\n/// How a provider handles the `max_tokens` field.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]\n#[serde(rename_all = \"snake_case\")]\npub enum MaxTokensField {\n #[default]\n MaxTokens,\n MaxCompletionTokens,\n}\n\n/// How a provider formats thinking/reasoning output.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]\n#[serde(rename_all = \"snake_case\")]\npub enum ThinkingFormat {\n #[default]\n OpenAi,\n Xai,\n Qwen,\n}\n\n/// Compatibility flags for OpenAI-compatible providers.\n/// Different providers have different quirks even though they share the same base API.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct OpenAiCompat {\n /// Supports the `store` parameter for conversation persistence.\n pub supports_store: bool,\n /// Supports `developer` role (system-level instructions).\n pub supports_developer_role: bool,\n /// Supports `reasoning_effort` parameter.\n pub supports_reasoning_effort: bool,\n /// Includes usage data in streaming responses.\n pub supports_usage_in_streaming: bool,\n /// Which field name to use for max tokens.\n pub max_tokens_field: MaxTokensField,\n /// Tool results must include a `name` field.\n pub requires_tool_result_name: bool,\n /// Must insert an assistant message after tool results.\n pub requires_assistant_after_tool_result: bool,\n /// How thinking/reasoning content is formatted in streaming.\n pub thinking_format: ThinkingFormat,\n}\n\nimpl Default for OpenAiCompat {\n fn default() -> Self {\n Self {\n supports_store: false,\n supports_developer_role: false,\n supports_reasoning_effort: false,\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxTokens,\n requires_tool_result_name: false,\n requires_assistant_after_tool_result: false,\n thinking_format: ThinkingFormat::OpenAi,\n }\n }\n}\n\nimpl OpenAiCompat {\n /// Compat flags for native OpenAI.\n pub fn openai() -> Self {\n Self {\n supports_store: true,\n supports_developer_role: true,\n supports_reasoning_effort: true,\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for xAI (Grok).\n pub fn xai() -> Self {\n Self {\n supports_usage_in_streaming: true,\n thinking_format: ThinkingFormat::Xai,\n ..Default::default()\n }\n }\n\n /// Compat flags for Groq.\n pub fn groq() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n\n /// Compat flags for Cerebras.\n pub fn cerebras() -> Self {\n Self::default()\n }\n\n /// Compat flags for OpenRouter.\n pub fn openrouter() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for Mistral.\n pub fn mistral() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for DeepSeek.\n pub fn deepseek() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for Z.ai (Zhipu AI).\n pub fn zai() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n\n /// Compat flags for MiniMax.\n pub fn minimax() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n}\n\n/// Full model configuration. Knows everything needed to make API calls.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ModelConfig {\n /// Model identifier sent to the API (e.g. \"gpt-4o\", \"claude-sonnet-4-20250514\").\n pub id: String,\n /// Human-friendly name.\n pub name: String,\n /// Which API protocol to use.\n pub api: ApiProtocol,\n /// Provider name (e.g. \"openai\", \"anthropic\", \"xai\").\n pub provider: String,\n /// Base URL for API requests (without trailing slash).\n pub base_url: String,\n /// Whether this model supports reasoning/thinking.\n pub reasoning: bool,\n /// Context window size in tokens.\n pub context_window: u32,\n /// Default max output tokens.\n pub max_tokens: u32,\n /// Cost configuration.\n #[serde(default)]\n pub cost: CostConfig,\n /// Additional headers to send with requests.\n #[serde(default)]\n pub headers: HashMap<String, String>,\n /// OpenAI-compat quirk flags (only for OpenAiCompletions protocol).\n #[serde(default)]\n pub compat: Option<OpenAiCompat>,\n}\n\nimpl ModelConfig {\n /// Create a new Anthropic model config.\n pub fn anthropic(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::AnthropicMessages,\n provider: \"anthropic\".into(),\n base_url: \"https://api.anthropic.com\".into(),\n reasoning: false,\n context_window: 200_000,\n max_tokens: 8192,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: None,\n }\n }\n\n /// Create a new OpenAI model config.\n pub fn openai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"openai\".into(),\n base_url: \"https://api.openai.com/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::openai()),\n }\n }\n\n /// Create a config for a local OpenAI-compatible server (LM Studio, Ollama, etc.).\n /// No API key required — sends an empty Bearer token.\n pub fn local(base_url: impl Into<String>, model_id: impl Into<String>) -> Self {\n Self {\n id: model_id.into(),\n name: \"Local Model\".into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"local\".into(),\n base_url: base_url.into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::default()),\n }\n }\n\n /// Create a new Z.ai (Zhipu AI) model config.\n ///\n /// Models: `glm-4.7`, `glm-4.5-air`, `glm-5`, etc.\n pub fn zai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"zai\".into(),\n base_url: \"https://api.z.ai/api/paas/v4\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::zai()),\n }\n }\n\n /// Create a new MiniMax model config.\n ///\n /// Models: `MiniMax-Text-01`, `MiniMax-M1`, etc.\n pub fn minimax(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"minimax\".into(),\n base_url: \"https://api.minimaxi.chat/v1\".into(),\n reasoning: false,\n context_window: 1_000_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::minimax()),\n }\n }\n\n /// Create a new xAI (Grok) model config.\n ///\n /// Models: `grok-3-mini`, `grok-3`, etc.\n pub fn xai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"xai\".into(),\n base_url: \"https://api.x.ai/v1\".into(),\n reasoning: false,\n context_window: 131_072,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::xai()),\n }\n }\n\n /// Create a new Groq model config.\n ///\n /// Models: `llama-3.3-70b-versatile`, `mixtral-8x7b-32768`, etc.\n pub fn groq(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"groq\".into(),\n base_url: \"https://api.groq.com/openai/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::groq()),\n }\n }\n\n /// Create a new DeepSeek model config.\n ///\n /// Models: `deepseek-chat`, `deepseek-reasoner`, etc.\n pub fn deepseek(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"deepseek\".into(),\n base_url: \"https://api.deepseek.com/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::deepseek()),\n }\n }\n\n /// Create a new OpenRouter model config.\n ///\n /// Models: `openai/gpt-4o-mini`, `anthropic/claude-3.5-sonnet`, etc.\n pub fn openrouter(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"openrouter\".into(),\n base_url: \"https://openrouter.ai/api/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::openrouter()),\n }\n }\n\n /// Create a new Mistral model config.\n ///\n /// Models: `mistral-large-latest`, `mistral-small-latest`, etc.\n pub fn mistral(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"mistral\".into(),\n base_url: \"https://api.mistral.ai/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::mistral()),\n }\n }\n\n /// Create a new Google Generative AI (Gemini) model config.\n pub fn google(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::GoogleGenerativeAi,\n provider: \"google\".into(),\n base_url: \"https://generativelanguage.googleapis.com\".into(),\n reasoning: false,\n context_window: 1_000_000,\n max_tokens: 8192,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: None,\n }\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_model_config_anthropic() {\n let config = ModelConfig::anthropic(\"claude-sonnet-4-20250514\", \"Claude Sonnet 4\");\n assert_eq!(config.api, ApiProtocol::AnthropicMessages);\n assert_eq!(config.provider, \"anthropic\");\n assert!(config.compat.is_none());\n }\n\n #[test]\n fn test_model_config_openai() {\n let config = ModelConfig::openai(\"gpt-4o\", \"GPT-4o\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n let compat = config.compat.unwrap();\n assert!(compat.supports_store);\n assert!(compat.supports_developer_role);\n assert_eq!(compat.max_tokens_field, MaxTokensField::MaxCompletionTokens);\n }\n\n #[test]\n fn test_openai_compat_variants() {\n let xai = OpenAiCompat::xai();\n assert_eq!(xai.thinking_format, ThinkingFormat::Xai);\n assert!(!xai.supports_store);\n\n let groq = OpenAiCompat::groq();\n assert!(groq.supports_usage_in_streaming);\n assert!(!groq.supports_store);\n\n let deepseek = OpenAiCompat::deepseek();\n assert_eq!(\n deepseek.max_tokens_field,\n MaxTokensField::MaxCompletionTokens\n );\n\n let zai = OpenAiCompat::zai();\n assert!(zai.supports_usage_in_streaming);\n assert!(!zai.supports_store);\n\n let minimax = OpenAiCompat::minimax();\n assert!(minimax.supports_usage_in_streaming);\n assert!(!minimax.supports_store);\n }\n\n #[test]\n fn test_model_config_zai() {\n let config = ModelConfig::zai(\"glm-4.7\", \"GLM 4.7\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n assert_eq!(config.provider, \"zai\");\n assert_eq!(config.base_url, \"https://api.z.ai/api/paas/v4\");\n assert!(config.compat.is_some());\n }\n\n #[test]\n fn test_model_config_minimax() {\n let config = ModelConfig::minimax(\"MiniMax-Text-01\", \"MiniMax Text 01\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n assert_eq!(config.provider, \"minimax\");\n assert_eq!(config.base_url, \"https://api.minimaxi.chat/v1\");\n assert_eq!(config.context_window, 1_000_000);\n assert!(config.compat.is_some());\n }\n\n #[test]\n fn test_api_protocol_display() {\n assert_eq!(\n ApiProtocol::AnthropicMessages.to_string(),\n \"anthropic_messages\"\n );\n assert_eq!(\n ApiProtocol::OpenAiCompletions.to_string(),\n \"openai_completions\"\n );\n assert_eq!(\n ApiProtocol::GoogleGenerativeAi.to_string(),\n \"google_generative_ai\"\n );\n }\n\n #[test]\n fn test_cost_config_default() {\n let cost = CostConfig::default();\n assert_eq!(cost.input_per_million, 0.0);\n assert_eq!(cost.output_per_million, 0.0);\n }\n}\n"}, "ff8834906450509555dee7e5bc670f9234b3669b": {"t": 1785760851.1347442, "n": 15916, "h": "bc38640dac19c66e", "c": "//! Model configuration and provider compatibility flags.\n\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\n\n/// Which API protocol a model uses.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]\n#[serde(rename_all = \"snake_case\")]\npub enum ApiProtocol {\n AnthropicMessages,\n OpenAiCompletions,\n OpenAiResponses,\n AzureOpenAiResponses,\n GoogleGenerativeAi,\n GoogleVertex,\n BedrockConverseStream,\n}\n\nimpl std::fmt::Display for ApiProtocol {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n match self {\n Self::AnthropicMessages => write!(f, \"anthropic_messages\"),\n Self::OpenAiCompletions => write!(f, \"openai_completions\"),\n Self::OpenAiResponses => write!(f, \"openai_responses\"),\n Self::AzureOpenAiResponses => write!(f, \"azure_openai_responses\"),\n Self::GoogleGenerativeAi => write!(f, \"google_generative_ai\"),\n Self::GoogleVertex => write!(f, \"google_vertex\"),\n Self::BedrockConverseStream => write!(f, \"bedrock_converse_stream\"),\n }\n }\n}\n\n/// Cost per million tokens (input/output).\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct CostConfig {\n pub input_per_million: f64,\n pub output_per_million: f64,\n #[serde(default)]\n pub cache_read_per_million: f64,\n #[serde(default)]\n pub cache_write_per_million: f64,\n}\n\nimpl Default for CostConfig {\n fn default() -> Self {\n Self {\n input_per_million: 0.0,\n output_per_million: 0.0,\n cache_read_per_million: 0.0,\n cache_write_per_million: 0.0,\n }\n }\n}\n\n/// How a provider handles the `max_tokens` field.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]\n#[serde(rename_all = \"snake_case\")]\npub enum MaxTokensField {\n #[default]\n MaxTokens,\n MaxCompletionTokens,\n}\n\n/// How a provider formats thinking/reasoning output.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]\n#[serde(rename_all = \"snake_case\")]\npub enum ThinkingFormat {\n #[default]\n OpenAi,\n Xai,\n Qwen,\n}\n\n/// Compatibility flags for OpenAI-compatible providers.\n/// Different providers have different quirks even though they share the same base API.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct OpenAiCompat {\n /// Supports the `store` parameter for conversation persistence.\n pub supports_store: bool,\n /// Supports `developer` role (system-level instructions).\n pub supports_developer_role: bool,\n /// Supports `reasoning_effort` parameter.\n pub supports_reasoning_effort: bool,\n /// Includes usage data in streaming responses.\n pub supports_usage_in_streaming: bool,\n /// Which field name to use for max tokens.\n pub max_tokens_field: MaxTokensField,\n /// Tool results must include a `name` field.\n pub requires_tool_result_name: bool,\n /// Must insert an assistant message after tool results.\n pub requires_assistant_after_tool_result: bool,\n /// How thinking/reasoning content is formatted in streaming.\n pub thinking_format: ThinkingFormat,\n}\n\nimpl Default for OpenAiCompat {\n fn default() -> Self {\n Self {\n supports_store: false,\n supports_developer_role: false,\n supports_reasoning_effort: false,\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxTokens,\n requires_tool_result_name: false,\n requires_assistant_after_tool_result: false,\n thinking_format: ThinkingFormat::OpenAi,\n }\n }\n}\n\nimpl OpenAiCompat {\n /// Compat flags for native OpenAI.\n pub fn openai() -> Self {\n Self {\n supports_store: true,\n supports_developer_role: true,\n supports_reasoning_effort: true,\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for xAI (Grok).\n pub fn xai() -> Self {\n Self {\n supports_usage_in_streaming: true,\n thinking_format: ThinkingFormat::Xai,\n ..Default::default()\n }\n }\n\n /// Compat flags for Groq.\n pub fn groq() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n\n /// Compat flags for Cerebras.\n pub fn cerebras() -> Self {\n Self::default()\n }\n\n /// Compat flags for OpenRouter.\n pub fn openrouter() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for Mistral.\n pub fn mistral() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for DeepSeek.\n pub fn deepseek() -> Self {\n Self {\n supports_usage_in_streaming: true,\n max_tokens_field: MaxTokensField::MaxCompletionTokens,\n ..Default::default()\n }\n }\n\n /// Compat flags for Z.ai (Zhipu AI).\n pub fn zai() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n\n /// Compat flags for MiniMax.\n pub fn minimax() -> Self {\n Self {\n supports_usage_in_streaming: true,\n ..Default::default()\n }\n }\n}\n\n/// Full model configuration. Knows everything needed to make API calls.\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ModelConfig {\n /// Model identifier sent to the API (e.g. \"gpt-4o\", \"claude-sonnet-4-20250514\").\n pub id: String,\n /// Human-friendly name.\n pub name: String,\n /// Which API protocol to use.\n pub api: ApiProtocol,\n /// Provider name (e.g. \"openai\", \"anthropic\", \"xai\").\n pub provider: String,\n /// Base URL for API requests (without trailing slash).\n pub base_url: String,\n /// Whether this model supports reasoning/thinking.\n pub reasoning: bool,\n /// Context window size in tokens.\n pub context_window: u32,\n /// Default max output tokens.\n pub max_tokens: u32,\n /// Cost configuration.\n #[serde(default)]\n pub cost: CostConfig,\n /// Additional headers to send with requests.\n #[serde(default)]\n pub headers: HashMap<String, String>,\n /// OpenAI-compat quirk flags (only for OpenAiCompletions protocol).\n #[serde(default)]\n pub compat: Option<OpenAiCompat>,\n}\n\nimpl ModelConfig {\n /// Create a new Anthropic model config.\n pub fn anthropic(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::AnthropicMessages,\n provider: \"anthropic\".into(),\n base_url: \"https://api.anthropic.com\".into(),\n reasoning: false,\n context_window: 200_000,\n max_tokens: 8192,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: None,\n }\n }\n\n /// Create a new OpenAI model config.\n pub fn openai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"openai\".into(),\n base_url: \"https://api.openai.com/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::openai()),\n }\n }\n\n /// Create a config for a local OpenAI-compatible server (LM Studio, Ollama, etc.).\n /// No API key required — sends an empty Bearer token.\n pub fn local(base_url: impl Into<String>, model_id: impl Into<String>) -> Self {\n Self {\n id: model_id.into(),\n name: \"Local Model\".into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"local\".into(),\n base_url: base_url.into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::default()),\n }\n }\n\n /// Create a new Z.ai (Zhipu AI) model config.\n ///\n /// Models: `glm-4.7`, `glm-4.5-air`, `glm-5`, etc.\n pub fn zai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"zai\".into(),\n base_url: \"https://api.z.ai/api/paas/v4\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::zai()),\n }\n }\n\n /// Create a new MiniMax model config.\n ///\n /// Models: `MiniMax-Text-01`, `MiniMax-M1`, etc.\n pub fn minimax(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"minimax\".into(),\n base_url: \"https://api.minimaxi.chat/v1\".into(),\n reasoning: false,\n context_window: 1_000_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::minimax()),\n }\n }\n\n /// Create a new xAI (Grok) model config.\n ///\n /// Models: `grok-3-mini`, `grok-3`, etc.\n pub fn xai(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"xai\".into(),\n base_url: \"https://api.x.ai/v1\".into(),\n reasoning: false,\n context_window: 131_072,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::xai()),\n }\n }\n\n /// Create a new Groq model config.\n ///\n /// Models: `llama-3.3-70b-versatile`, `mixtral-8x7b-32768`, etc.\n pub fn groq(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"groq\".into(),\n base_url: \"https://api.groq.com/openai/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::groq()),\n }\n }\n\n /// Create a new DeepSeek model config.\n ///\n /// Models: `deepseek-chat`, `deepseek-reasoner`, etc.\n pub fn deepseek(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"deepseek\".into(),\n base_url: \"https://api.deepseek.com/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::deepseek()),\n }\n }\n\n /// Create a new Mistral model config.\n ///\n /// Models: `mistral-large-latest`, `mistral-small-latest`, etc.\n pub fn mistral(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::OpenAiCompletions,\n provider: \"mistral\".into(),\n base_url: \"https://api.mistral.ai/v1\".into(),\n reasoning: false,\n context_window: 128_000,\n max_tokens: 4096,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: Some(OpenAiCompat::mistral()),\n }\n }\n\n /// Create a new Google Generative AI (Gemini) model config.\n pub fn google(id: impl Into<String>, name: impl Into<String>) -> Self {\n Self {\n id: id.into(),\n name: name.into(),\n api: ApiProtocol::GoogleGenerativeAi,\n provider: \"google\".into(),\n base_url: \"https://generativelanguage.googleapis.com\".into(),\n reasoning: false,\n context_window: 1_000_000,\n max_tokens: 8192,\n cost: CostConfig::default(),\n headers: HashMap::new(),\n compat: None,\n }\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_model_config_anthropic() {\n let config = ModelConfig::anthropic(\"claude-sonnet-4-20250514\", \"Claude Sonnet 4\");\n assert_eq!(config.api, ApiProtocol::AnthropicMessages);\n assert_eq!(config.provider, \"anthropic\");\n assert!(config.compat.is_none());\n }\n\n #[test]\n fn test_model_config_openai() {\n let config = ModelConfig::openai(\"gpt-4o\", \"GPT-4o\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n let compat = config.compat.unwrap();\n assert!(compat.supports_store);\n assert!(compat.supports_developer_role);\n assert_eq!(compat.max_tokens_field, MaxTokensField::MaxCompletionTokens);\n }\n\n #[test]\n fn test_openai_compat_variants() {\n let xai = OpenAiCompat::xai();\n assert_eq!(xai.thinking_format, ThinkingFormat::Xai);\n assert!(!xai.supports_store);\n\n let groq = OpenAiCompat::groq();\n assert!(groq.supports_usage_in_streaming);\n assert!(!groq.supports_store);\n\n let deepseek = OpenAiCompat::deepseek();\n assert_eq!(\n deepseek.max_tokens_field,\n MaxTokensField::MaxCompletionTokens\n );\n\n let zai = OpenAiCompat::zai();\n assert!(zai.supports_usage_in_streaming);\n assert!(!zai.supports_store);\n\n let minimax = OpenAiCompat::minimax();\n assert!(minimax.supports_usage_in_streaming);\n assert!(!minimax.supports_store);\n }\n\n #[test]\n fn test_model_config_zai() {\n let config = ModelConfig::zai(\"glm-4.7\", \"GLM 4.7\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n assert_eq!(config.provider, \"zai\");\n assert_eq!(config.base_url, \"https://api.z.ai/api/paas/v4\");\n assert!(config.compat.is_some());\n }\n\n #[test]\n fn test_model_config_minimax() {\n let config = ModelConfig::minimax(\"MiniMax-Text-01\", \"MiniMax Text 01\");\n assert_eq!(config.api, ApiProtocol::OpenAiCompletions);\n assert_eq!(config.provider, \"minimax\");\n assert_eq!(config.base_url, \"https://api.minimaxi.chat/v1\");\n assert_eq!(config.context_window, 1_000_000);\n assert!(config.compat.is_some());\n }\n\n #[test]\n fn test_api_protocol_display() {\n assert_eq!(\n ApiProtocol::AnthropicMessages.to_string(),\n \"anthropic_messages\"\n );\n assert_eq!(\n ApiProtocol::OpenAiCompletions.to_string(),\n \"openai_completions\"\n );\n assert_eq!(\n ApiProtocol::GoogleGenerativeAi.to_string(),\n \"google_generative_ai\"\n );\n }\n\n #[test]\n fn test_cost_config_default() {\n let cost = CostConfig::default();\n assert_eq!(cost.input_per_million, 0.0);\n assert_eq!(cost.output_per_million, 0.0);\n }\n}\n"}}}}