use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApiProtocol {
AnthropicMessages,
OpenAiCompletions,
OpenAiResponses,
AzureOpenAiResponses,
GoogleGenerativeAi,
GoogleVertex,
BedrockConverseStream,
}
impl std::fmt::Display for ApiProtocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AnthropicMessages => write!(f, "anthropic_messages"),
Self::OpenAiCompletions => write!(f, "openai_completions"),
Self::OpenAiResponses => write!(f, "openai_responses"),
Self::AzureOpenAiResponses => write!(f, "azure_openai_responses"),
Self::GoogleGenerativeAi => write!(f, "google_generative_ai"),
Self::GoogleVertex => write!(f, "google_vertex"),
Self::BedrockConverseStream => write!(f, "bedrock_converse_stream"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostConfig {
pub input_per_million: f64,
pub output_per_million: f64,
#[serde(default)]
pub cache_read_per_million: f64,
#[serde(default)]
pub cache_write_per_million: f64,
}
impl CostConfig {
pub fn is_configured(&self) -> bool {
self.input_per_million != 0.0
|| self.output_per_million != 0.0
|| self.cache_read_per_million != 0.0
|| self.cache_write_per_million != 0.0
}
pub fn cost_usd(&self, usage: &crate::types::Usage) -> f64 {
(usage.input as f64 * self.input_per_million
+ usage.output as f64 * self.output_per_million
+ usage.cache_read as f64 * self.cache_read_per_million
+ usage.cache_write as f64 * self.cache_write_per_million)
/ 1_000_000.0
}
}
impl Default for CostConfig {
fn default() -> Self {
Self {
input_per_million: 0.0,
output_per_million: 0.0,
cache_read_per_million: 0.0,
cache_write_per_million: 0.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum MaxTokensField {
#[default]
MaxTokens,
MaxCompletionTokens,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ThinkingFormat {
#[default]
OpenAi,
Xai,
Qwen,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAiCompat {
pub supports_store: bool,
pub supports_developer_role: bool,
pub supports_reasoning_effort: bool,
#[serde(default)]
pub supports_thinking_control: bool,
pub supports_usage_in_streaming: bool,
pub max_tokens_field: MaxTokensField,
pub requires_tool_result_name: bool,
#[serde(default)]
pub requires_assistant_after_tool_result: bool,
pub thinking_format: ThinkingFormat,
}
impl Default for OpenAiCompat {
fn default() -> Self {
Self {
supports_store: false,
supports_developer_role: false,
supports_reasoning_effort: false,
supports_thinking_control: false,
supports_usage_in_streaming: true,
max_tokens_field: MaxTokensField::MaxTokens,
requires_tool_result_name: false,
requires_assistant_after_tool_result: false,
thinking_format: ThinkingFormat::OpenAi,
}
}
}
impl OpenAiCompat {
pub fn openai() -> Self {
Self {
supports_store: true,
supports_developer_role: true,
supports_reasoning_effort: true,
supports_usage_in_streaming: true,
max_tokens_field: MaxTokensField::MaxCompletionTokens,
..Default::default()
}
}
pub fn meta() -> Self {
Self {
supports_reasoning_effort: true,
supports_usage_in_streaming: true,
max_tokens_field: MaxTokensField::MaxCompletionTokens,
..Default::default()
}
}
pub fn xai() -> Self {
Self {
supports_usage_in_streaming: true,
thinking_format: ThinkingFormat::Xai,
..Default::default()
}
}
pub fn groq() -> Self {
Self {
supports_usage_in_streaming: true,
..Default::default()
}
}
pub fn cerebras() -> Self {
Self::default()
}
pub fn openrouter() -> Self {
Self {
supports_usage_in_streaming: true,
max_tokens_field: MaxTokensField::MaxCompletionTokens,
..Default::default()
}
}
pub fn mistral() -> Self {
Self {
supports_usage_in_streaming: true,
max_tokens_field: MaxTokensField::MaxTokens,
..Default::default()
}
}
pub fn deepseek() -> Self {
Self {
supports_reasoning_effort: true,
supports_thinking_control: true,
supports_usage_in_streaming: true,
max_tokens_field: MaxTokensField::MaxTokens,
..Default::default()
}
}
pub fn zai() -> Self {
Self {
supports_usage_in_streaming: true,
..Default::default()
}
}
pub fn minimax() -> Self {
Self {
supports_usage_in_streaming: true,
..Default::default()
}
}
pub fn qwen() -> Self {
Self {
supports_usage_in_streaming: true,
max_tokens_field: MaxTokensField::MaxTokens,
thinking_format: ThinkingFormat::Qwen,
..Default::default()
}
}
pub fn ollama() -> Self {
Self {
requires_assistant_after_tool_result: true,
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AnthropicCompat {
pub adaptive_thinking: bool,
pub bearer_auth: bool,
}
impl Default for AnthropicCompat {
fn default() -> Self {
Self {
adaptive_thinking: true,
bearer_auth: false,
}
}
}
impl AnthropicCompat {
pub fn legacy() -> Self {
Self {
adaptive_thinking: false,
bearer_auth: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OpenCodeGateway {
Zen,
Go,
}
impl OpenCodeGateway {
fn provider_name(self) -> &'static str {
match self {
Self::Zen => "opencode-zen",
Self::Go => "opencode-go",
}
}
fn base_url(self) -> &'static str {
match self {
Self::Zen => "https://opencode.ai/zen/v1",
Self::Go => "https://opencode.ai/zen/go/v1",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ModelConfig {
pub id: String,
pub name: String,
pub api: ApiProtocol,
pub provider: String,
pub base_url: String,
pub reasoning: bool,
pub context_window: u32,
pub max_tokens: u32,
#[serde(default)]
pub cost: CostConfig,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default)]
pub compat: Option<OpenAiCompat>,
#[serde(default)]
pub anthropic: Option<AnthropicCompat>,
}
impl ModelConfig {
pub fn mock() -> Self {
Self::custom(
ApiProtocol::AnthropicMessages,
"mock",
"http://mock.invalid",
"mock",
"Mock",
)
}
pub fn custom(
api: ApiProtocol,
provider: impl Into<String>,
base_url: impl Into<String>,
model_id: impl Into<String>,
name: impl Into<String>,
) -> Self {
Self {
id: model_id.into(),
name: name.into(),
api,
provider: provider.into(),
base_url: base_url.into(),
reasoning: false,
context_window: 128_000,
max_tokens: 16_000,
cost: CostConfig::default(),
headers: HashMap::new(),
compat: None,
anthropic: None,
}
}
pub fn anthropic(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
api: ApiProtocol::AnthropicMessages,
provider: "anthropic".into(),
base_url: "https://api.anthropic.com/v1".into(),
reasoning: true,
context_window: 200_000,
max_tokens: 16_000,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: None,
}
}
pub fn claude_fable_5() -> Self {
Self {
context_window: 1_000_000,
max_tokens: 64_000,
cost: CostConfig {
input_per_million: 10.0,
output_per_million: 50.0,
cache_read_per_million: 1.0,
cache_write_per_million: 12.5,
},
..Self::anthropic("claude-fable-5", "Claude Fable 5")
}
}
pub fn claude_opus_5() -> Self {
Self {
context_window: 1_000_000,
max_tokens: 64_000,
cost: CostConfig {
input_per_million: 5.0,
output_per_million: 25.0,
cache_read_per_million: 0.5,
cache_write_per_million: 6.25,
},
..Self::anthropic("claude-opus-5", "Claude Opus 5")
}
}
pub fn claude_opus_4_8() -> Self {
Self {
context_window: 1_000_000,
max_tokens: 64_000,
cost: CostConfig {
input_per_million: 5.0,
output_per_million: 25.0,
cache_read_per_million: 0.5,
cache_write_per_million: 6.25,
},
..Self::anthropic("claude-opus-4-8", "Claude Opus 4.8")
}
}
pub fn claude_sonnet_5() -> Self {
Self {
context_window: 1_000_000,
max_tokens: 64_000,
cost: CostConfig {
input_per_million: 3.0,
output_per_million: 15.0,
cache_read_per_million: 0.3,
cache_write_per_million: 3.75,
},
..Self::anthropic("claude-sonnet-5", "Claude Sonnet 5")
}
}
pub fn claude_haiku_4_5() -> Self {
Self {
context_window: 200_000,
max_tokens: 32_000,
cost: CostConfig {
input_per_million: 1.0,
output_per_million: 5.0,
cache_read_per_million: 0.1,
cache_write_per_million: 1.25,
},
..Self::anthropic("claude-haiku-4-5", "Claude Haiku 4.5")
}
}
pub fn gpt_5_5() -> Self {
Self {
reasoning: true,
context_window: 1_000_000,
max_tokens: 64_000,
cost: CostConfig {
input_per_million: 5.0,
output_per_million: 30.0,
cache_read_per_million: 0.5,
cache_write_per_million: 0.0,
},
..Self::openai("gpt-5.5", "GPT-5.5")
}
}
pub fn openai(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
api: ApiProtocol::OpenAiCompletions,
provider: "openai".into(),
base_url: "https://api.openai.com/v1".into(),
reasoning: false,
context_window: 128_000,
max_tokens: 4096,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: Some(OpenAiCompat::openai()),
}
}
pub fn local(base_url: impl Into<String>, model_id: impl Into<String>) -> Self {
Self {
id: model_id.into(),
name: "Local Model".into(),
api: ApiProtocol::OpenAiCompletions,
provider: "local".into(),
base_url: base_url.into(),
reasoning: false,
context_window: 128_000,
max_tokens: 4096,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: Some(OpenAiCompat::default()),
}
}
pub fn opencode_zen(model_id: impl Into<String>) -> Self {
Self::opencode(model_id.into(), OpenCodeGateway::Zen)
}
pub fn opencode_go(model_id: impl Into<String>) -> Self {
Self::opencode(model_id.into(), OpenCodeGateway::Go)
}
fn opencode(id: String, gateway: OpenCodeGateway) -> Self {
let lower = id.to_ascii_lowercase();
if lower.starts_with("gemini-") {
tracing::warn!(
"OpenCode serves Gemini models over a Google-native endpoint yoagent \
does not target; '{}' is routed to /chat/completions and will likely \
fail at request time",
id
);
}
let anthropic_protocol = match gateway {
OpenCodeGateway::Zen => lower.starts_with("claude-") || lower.starts_with("qwen"),
OpenCodeGateway::Go => lower.starts_with("qwen") || lower.starts_with("minimax-"),
};
let (api, reasoning, compat, anthropic) = if anthropic_protocol {
(
ApiProtocol::AnthropicMessages,
true,
None,
Some(AnthropicCompat {
adaptive_thinking: true,
bearer_auth: true,
}),
)
} else if gateway == OpenCodeGateway::Zen && lower.starts_with("gpt-") {
(ApiProtocol::OpenAiResponses, true, None, None)
} else {
(
ApiProtocol::OpenAiCompletions,
false,
Some(OpenAiCompat::default()),
None,
)
};
Self {
id: id.clone(),
name: id,
api,
provider: gateway.provider_name().into(),
base_url: gateway.base_url().into(),
reasoning,
context_window: 128_000,
max_tokens: 16_000,
cost: CostConfig::default(),
headers: HashMap::new(),
compat,
anthropic,
}
}
pub fn openai_compat(
base_url: impl Into<String>,
model_id: impl Into<String>,
provider: impl Into<String>,
compat: OpenAiCompat,
) -> Self {
let id = model_id.into();
Self {
id: id.clone(),
name: id,
api: ApiProtocol::OpenAiCompletions,
provider: provider.into(),
base_url: base_url.into(),
reasoning: false,
context_window: 128_000,
max_tokens: 4096,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: Some(compat),
}
}
pub fn ollama(base_url: impl Into<String>, model_id: impl Into<String>) -> Self {
let id = model_id.into();
Self {
id: id.clone(),
name: id,
api: ApiProtocol::OpenAiCompletions,
provider: "ollama".into(),
base_url: base_url.into(),
reasoning: false,
context_window: 128_000,
max_tokens: 4096,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: Some(OpenAiCompat::ollama()),
}
}
pub fn zai(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
api: ApiProtocol::OpenAiCompletions,
provider: "zai".into(),
base_url: "https://api.z.ai/api/paas/v4".into(),
reasoning: false,
context_window: 128_000,
max_tokens: 4096,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: Some(OpenAiCompat::zai()),
}
}
pub fn meta(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
api: ApiProtocol::OpenAiCompletions,
provider: "meta".into(),
base_url: "https://api.meta.ai/v1".into(),
reasoning: true,
context_window: 1_048_576,
max_tokens: 131_072,
cost: CostConfig {
input_per_million: 1.25,
output_per_million: 4.25,
cache_read_per_million: 0.15,
cache_write_per_million: 0.0,
},
headers: HashMap::new(),
anthropic: None,
compat: Some(OpenAiCompat::meta()),
}
}
pub fn minimax(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
api: ApiProtocol::OpenAiCompletions,
provider: "minimax".into(),
base_url: "https://api.minimaxi.chat/v1".into(),
reasoning: false,
context_window: 1_000_000,
max_tokens: 4096,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: Some(OpenAiCompat::minimax()),
}
}
pub fn qwen(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
api: ApiProtocol::OpenAiCompletions,
provider: "qwen".into(),
base_url: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1".into(),
reasoning: true,
context_window: 128_000,
max_tokens: 4096,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: Some(OpenAiCompat::qwen()),
}
}
pub fn xai(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
api: ApiProtocol::OpenAiCompletions,
provider: "xai".into(),
base_url: "https://api.x.ai/v1".into(),
reasoning: false,
context_window: 131_072,
max_tokens: 4096,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: Some(OpenAiCompat::xai()),
}
}
pub fn groq(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
api: ApiProtocol::OpenAiCompletions,
provider: "groq".into(),
base_url: "https://api.groq.com/openai/v1".into(),
reasoning: false,
context_window: 128_000,
max_tokens: 4096,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: Some(OpenAiCompat::groq()),
}
}
pub fn deepseek(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
api: ApiProtocol::OpenAiCompletions,
provider: "deepseek".into(),
base_url: "https://api.deepseek.com".into(),
reasoning: true,
context_window: 1_000_000,
max_tokens: 384_000,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: Some(OpenAiCompat::deepseek()),
}
}
pub fn mistral(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
api: ApiProtocol::OpenAiCompletions,
provider: "mistral".into(),
base_url: "https://api.mistral.ai/v1".into(),
reasoning: false,
context_window: 128_000,
max_tokens: 4096,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: Some(OpenAiCompat::mistral()),
}
}
pub fn google(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
api: ApiProtocol::GoogleGenerativeAi,
provider: "google".into(),
base_url: "https://generativelanguage.googleapis.com".into(),
reasoning: false,
context_window: 1_000_000,
max_tokens: 8192,
cost: CostConfig::default(),
headers: HashMap::new(),
anthropic: None,
compat: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn meta_preset_matches_launch_specs() {
let mc = ModelConfig::meta("muse-spark-1.1", "Muse Spark 1.1");
assert_eq!(mc.provider, "meta");
assert_eq!(mc.api, ApiProtocol::OpenAiCompletions);
assert_eq!(mc.base_url, "https://api.meta.ai/v1");
assert_eq!(mc.context_window, 1_048_576);
assert_eq!(mc.max_tokens, 131_072);
assert!(mc.cost.is_configured());
assert_eq!(mc.cost.input_per_million, 1.25);
assert_eq!(mc.cost.output_per_million, 4.25);
assert_eq!(mc.cost.cache_read_per_million, 0.15);
assert_eq!(mc.cost.cache_write_per_million, 0.0);
let compat = mc.compat.expect("compat flags set");
assert!(matches!(
compat.max_tokens_field,
MaxTokensField::MaxCompletionTokens
));
assert!(compat.supports_reasoning_effort);
assert!(compat.supports_usage_in_streaming);
}
#[test]
fn test_model_config_anthropic() {
let config = ModelConfig::anthropic("claude-sonnet-5", "Claude Sonnet 5");
assert_eq!(config.api, ApiProtocol::AnthropicMessages);
assert_eq!(config.provider, "anthropic");
assert_eq!(config.base_url, "https://api.anthropic.com/v1");
assert!(config.compat.is_none());
assert!(config.anthropic.is_none());
}
#[test]
fn test_cost_usd() {
let cost = CostConfig {
input_per_million: 3.0,
output_per_million: 15.0,
cache_read_per_million: 0.3,
cache_write_per_million: 3.75,
};
let usage = crate::types::Usage {
input: 1_000_000,
output: 100_000,
cache_read: 2_000_000,
cache_write: 400_000,
total_tokens: 0,
};
assert!((cost.cost_usd(&usage) - 6.6).abs() < 1e-9);
assert_eq!(CostConfig::default().cost_usd(&usage), 0.0);
}
#[test]
fn test_new_generation_presets() {
let fable = ModelConfig::claude_fable_5();
assert_eq!(fable.id, "claude-fable-5");
assert_eq!(fable.api, ApiProtocol::AnthropicMessages);
assert_eq!(fable.context_window, 1_000_000);
assert_eq!(fable.cost.input_per_million, 10.0);
assert_eq!(fable.cost.output_per_million, 50.0);
let opus_5 = ModelConfig::claude_opus_5();
assert_eq!(opus_5.id, "claude-opus-5");
assert_eq!(opus_5.api, ApiProtocol::AnthropicMessages);
assert_eq!(opus_5.context_window, 1_000_000);
assert_eq!(opus_5.max_tokens, 64_000);
assert_eq!(opus_5.cost.input_per_million, 5.0);
assert_eq!(opus_5.cost.output_per_million, 25.0);
assert_eq!(opus_5.cost.cache_read_per_million, 0.5);
assert_eq!(opus_5.cost.cache_write_per_million, 6.25);
let opus = ModelConfig::claude_opus_4_8();
assert_eq!(opus.id, "claude-opus-4-8");
assert_eq!(opus.context_window, 1_000_000);
assert_eq!(opus.cost.input_per_million, 5.0);
let sonnet = ModelConfig::claude_sonnet_5();
assert_eq!(sonnet.id, "claude-sonnet-5");
assert_eq!(sonnet.cost.output_per_million, 15.0);
let haiku = ModelConfig::claude_haiku_4_5();
assert_eq!(haiku.id, "claude-haiku-4-5");
assert_eq!(haiku.context_window, 200_000);
let gpt = ModelConfig::gpt_5_5();
assert_eq!(gpt.id, "gpt-5.5");
assert_eq!(gpt.api, ApiProtocol::OpenAiCompletions);
assert_eq!(gpt.context_window, 1_000_000);
assert_eq!(gpt.cost.output_per_million, 30.0);
assert!(gpt.compat.is_some());
}
#[test]
fn test_opencode_zen_protocol_selection() {
let gpt = ModelConfig::opencode_zen("gpt-5.5");
assert_eq!(gpt.api, ApiProtocol::OpenAiResponses);
assert_eq!(gpt.provider, "opencode-zen");
assert_eq!(gpt.base_url, "https://opencode.ai/zen/v1");
for id in ["claude-sonnet-5", "qwen3.7-max"] {
let config = ModelConfig::opencode_zen(id);
assert_eq!(config.api, ApiProtocol::AnthropicMessages, "{id}");
let compat = config.anthropic.expect("anthropic compat set");
assert!(compat.bearer_auth);
}
for id in ["deepseek-v4-pro", "minimax-m3", "glm-5.2", "kimi-k2.7-code"] {
let config = ModelConfig::opencode_zen(id);
assert_eq!(config.api, ApiProtocol::OpenAiCompletions, "{id}");
assert!(config.compat.is_some());
}
}
#[test]
fn test_opencode_go_protocol_selection() {
for id in ["qwen3.7-max", "minimax-m3"] {
let config = ModelConfig::opencode_go(id);
assert_eq!(config.api, ApiProtocol::AnthropicMessages, "{id}");
assert_eq!(config.base_url, "https://opencode.ai/zen/go/v1");
assert!(config.anthropic.expect("anthropic compat set").bearer_auth);
}
for id in [
"glm-5.2",
"kimi-k2.7-code",
"deepseek-v4-flash",
"mimo-v2.5",
] {
let config = ModelConfig::opencode_go(id);
assert_eq!(config.api, ApiProtocol::OpenAiCompletions, "{id}");
assert_eq!(config.provider, "opencode-go", "{id}");
}
}
#[test]
fn test_model_config_openai() {
let config = ModelConfig::openai("gpt-4o", "GPT-4o");
assert_eq!(config.api, ApiProtocol::OpenAiCompletions);
let compat = config.compat.unwrap();
assert!(compat.supports_store);
assert!(compat.supports_developer_role);
assert_eq!(compat.max_tokens_field, MaxTokensField::MaxCompletionTokens);
}
#[test]
fn test_openai_compat_variants() {
let xai = OpenAiCompat::xai();
assert_eq!(xai.thinking_format, ThinkingFormat::Xai);
assert!(!xai.supports_store);
let groq = OpenAiCompat::groq();
assert!(groq.supports_usage_in_streaming);
assert!(!groq.supports_store);
let deepseek = OpenAiCompat::deepseek();
assert_eq!(deepseek.max_tokens_field, MaxTokensField::MaxTokens);
assert!(deepseek.supports_reasoning_effort);
assert!(deepseek.supports_thinking_control);
let zai = OpenAiCompat::zai();
assert!(zai.supports_usage_in_streaming);
assert!(!zai.supports_store);
let minimax = OpenAiCompat::minimax();
assert!(minimax.supports_usage_in_streaming);
assert!(!minimax.supports_store);
let ollama = OpenAiCompat::ollama();
assert!(ollama.requires_assistant_after_tool_result);
assert!(!ollama.requires_tool_result_name);
let qwen = OpenAiCompat::qwen();
assert_eq!(qwen.thinking_format, ThinkingFormat::Qwen);
assert_eq!(qwen.max_tokens_field, MaxTokensField::MaxTokens);
assert!(qwen.supports_usage_in_streaming);
assert!(!qwen.supports_reasoning_effort);
assert!(!qwen.supports_thinking_control);
}
#[test]
fn test_model_config_deserializes_without_anthropic_field() {
let mut value = serde_json::to_value(ModelConfig::anthropic("m", "M")).unwrap();
value.as_object_mut().unwrap().remove("anthropic");
let config: ModelConfig = serde_json::from_value(value).unwrap();
assert!(config.anthropic.is_none());
}
#[test]
fn test_anthropic_compat_deserializes_from_partial_json() {
let compat: AnthropicCompat = serde_json::from_value(serde_json::json!({})).unwrap();
assert!(compat.adaptive_thinking);
assert!(!compat.bearer_auth);
let compat: AnthropicCompat =
serde_json::from_value(serde_json::json!({"bearer_auth": true})).unwrap();
assert!(compat.adaptive_thinking);
assert!(compat.bearer_auth);
}
#[test]
fn test_openai_compat_deserializes_without_assistant_after_tool_result_flag() {
let compat: OpenAiCompat = serde_json::from_value(serde_json::json!({
"supports_store": false,
"supports_developer_role": false,
"supports_reasoning_effort": false,
"supports_thinking_control": false,
"supports_usage_in_streaming": true,
"max_tokens_field": "max_tokens",
"requires_tool_result_name": false,
"thinking_format": "open_ai"
}))
.unwrap();
assert!(!compat.requires_assistant_after_tool_result);
}
#[test]
fn test_model_config_local_remains_neutral() {
let config = ModelConfig::local("http://localhost:1234/v1", "local-model");
assert_eq!(config.api, ApiProtocol::OpenAiCompletions);
assert_eq!(config.provider, "local");
assert_eq!(config.base_url, "http://localhost:1234/v1");
let compat = config.compat.unwrap();
assert!(!compat.requires_assistant_after_tool_result);
}
#[test]
fn test_model_config_ollama() {
let config = ModelConfig::ollama("http://localhost:11434/v1", "llama3.1:8b");
assert_eq!(config.api, ApiProtocol::OpenAiCompletions);
assert_eq!(config.provider, "ollama");
assert_eq!(config.id, "llama3.1:8b");
assert_eq!(config.name, "llama3.1:8b");
assert_eq!(config.base_url, "http://localhost:11434/v1");
let compat = config.compat.unwrap();
assert!(compat.requires_assistant_after_tool_result);
}
#[test]
fn test_model_config_openai_compat() {
let config = ModelConfig::openai_compat(
"http://localhost:1234/v1",
"qwen3-local",
"qwen",
OpenAiCompat::qwen(),
);
assert_eq!(config.api, ApiProtocol::OpenAiCompletions);
assert_eq!(config.provider, "qwen");
assert_eq!(config.id, "qwen3-local");
assert_eq!(config.name, "qwen3-local");
assert_eq!(config.base_url, "http://localhost:1234/v1");
let compat = config.compat.unwrap();
assert_eq!(compat.thinking_format, ThinkingFormat::Qwen);
}
#[test]
fn test_model_config_qwen() {
let config = ModelConfig::qwen("qwen3.6-plus", "Qwen 3.6 Plus");
assert_eq!(config.api, ApiProtocol::OpenAiCompletions);
assert_eq!(config.provider, "qwen");
assert_eq!(
config.base_url,
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
);
assert!(config.reasoning);
let compat = config.compat.unwrap();
assert_eq!(compat.thinking_format, ThinkingFormat::Qwen);
assert_eq!(compat.max_tokens_field, MaxTokensField::MaxTokens);
}
#[test]
fn test_model_config_zai() {
let config = ModelConfig::zai("glm-4.7", "GLM 4.7");
assert_eq!(config.api, ApiProtocol::OpenAiCompletions);
assert_eq!(config.provider, "zai");
assert_eq!(config.base_url, "https://api.z.ai/api/paas/v4");
assert!(config.compat.is_some());
}
#[test]
fn test_model_config_minimax() {
let config = ModelConfig::minimax("MiniMax-Text-01", "MiniMax Text 01");
assert_eq!(config.api, ApiProtocol::OpenAiCompletions);
assert_eq!(config.provider, "minimax");
assert_eq!(config.base_url, "https://api.minimaxi.chat/v1");
assert_eq!(config.context_window, 1_000_000);
assert!(config.compat.is_some());
}
#[test]
fn test_model_config_deepseek() {
let config = ModelConfig::deepseek("deepseek-v4-flash", "DeepSeek V4 Flash");
assert_eq!(config.api, ApiProtocol::OpenAiCompletions);
assert_eq!(config.provider, "deepseek");
assert_eq!(config.base_url, "https://api.deepseek.com");
assert_eq!(config.context_window, 1_000_000);
assert_eq!(config.max_tokens, 384_000);
assert!(config.reasoning);
assert!(config.compat.is_some());
}
#[test]
fn test_api_protocol_display() {
assert_eq!(
ApiProtocol::AnthropicMessages.to_string(),
"anthropic_messages"
);
assert_eq!(
ApiProtocol::OpenAiCompletions.to_string(),
"openai_completions"
);
assert_eq!(
ApiProtocol::GoogleGenerativeAi.to_string(),
"google_generative_ai"
);
}
#[test]
fn test_cost_config_default() {
let cost = CostConfig::default();
assert_eq!(cost.input_per_million, 0.0);
assert_eq!(cost.output_per_million, 0.0);
}
}