use serde::{Deserialize, Serialize};
use super::candidate::PricingSku;
use super::capabilities::{CapabilityState, RouteCapabilities};
use super::ids::{ModelId, ProviderId, WireModelId};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouteLimits {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_tokens: Option<u64>,
}
impl RouteLimits {
#[must_use]
pub const fn has_known_limit(self) -> bool {
self.context_tokens.is_some() || self.input_tokens.is_some() || self.output_tokens.is_some()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProviderModelOffering {
pub provider: ProviderId,
pub canonical_model: Option<ModelId>,
pub wire_model_id: WireModelId,
pub endpoint_key: String,
pub default_for_provider: bool,
pub limits: RouteLimits,
pub capabilities: RouteCapabilities,
pub pricing: PricingSku,
}
const MODELSTUDIO_TEXT_MODELS: &[&str] = &[
"qwen3.8-max",
"qwen3.8-max-preview",
"qwen3.7-plus",
"qwen3.7-max",
"qwen3.6-flash",
"deepseek-v4-pro",
"deepseek-v4-flash-0731",
"glm-5.2",
];
pub(crate) const OPENCODE_ZEN_RESPONSES_MODELS: &[&str] = &[
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.4",
"gpt-5.4-pro",
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-5.3-codex",
"gpt-5.3-codex-spark",
"gpt-5.2",
"gpt-5.2-codex",
"gpt-5.1",
"gpt-5.1-codex",
"gpt-5.1-codex-max",
"gpt-5.1-codex-mini",
"gpt-5",
"gpt-5-codex",
"gpt-5-nano",
];
pub(crate) const OPENCODE_ZEN_MESSAGES_MODELS: &[&str] = &[
"claude-fable-5",
"claude-opus-4-8",
"claude-opus-4-7",
"claude-opus-4-6",
"claude-opus-4-5",
"claude-sonnet-5",
"claude-sonnet-4-6",
"claude-sonnet-4-5",
"claude-haiku-4-5",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.6-plus",
"qwen3.5-plus",
];
pub(crate) const OPENCODE_ZEN_CHAT_MODELS: &[&str] = &[
"deepseek-v4-pro",
"deepseek-v4-flash",
"minimax-m3",
"minimax-m2.7",
"minimax-m2.5",
"glm-5.2",
"glm-5.1",
"glm-5",
"kimi-k2.5",
"kimi-k2.6",
"kimi-k2.7-code",
"grok-4.5",
"grok-build-0.1",
"big-pickle",
"mimo-v2.5-free",
"north-mini-code-free",
"nemotron-3-ultra-free",
"deepseek-v4-flash-free",
];
#[must_use]
pub fn bundled_offerings() -> Vec<ProviderModelOffering> {
let deepseek = ProviderId::from("deepseek");
let documented_capabilities = RouteCapabilities {
image_input: CapabilityState::Unsupported,
reasoning: CapabilityState::Supported,
native_tool_calls: CapabilityState::Supported,
structured_output: CapabilityState::Supported,
parallel_tool_calls: CapabilityState::Supported,
streaming: CapabilityState::Supported,
prompt_caching: CapabilityState::Supported,
server_side_web_search: CapabilityState::Unknown,
..RouteCapabilities::default()
};
let documented_limits = RouteLimits {
context_tokens: Some(1_000_000),
input_tokens: None,
output_tokens: Some(384_000),
};
let mut offerings = vec![
ProviderModelOffering {
provider: deepseek.clone(),
canonical_model: Some(ModelId::from("deepseek-v4-pro")),
wire_model_id: WireModelId::from("deepseek-v4-pro"),
endpoint_key: "chat".to_string(),
default_for_provider: true,
limits: documented_limits,
capabilities: documented_capabilities,
pricing: PricingSku::UnknownOrStale,
},
ProviderModelOffering {
provider: deepseek,
canonical_model: Some(ModelId::from("deepseek-v4-flash")),
wire_model_id: WireModelId::from("deepseek-v4-flash"),
endpoint_key: "responses".to_string(),
default_for_provider: false,
limits: documented_limits,
capabilities: documented_capabilities,
pricing: PricingSku::UnknownOrStale,
},
];
let provider = ProviderId::from("opencode-zen");
let groups = [
("responses", OPENCODE_ZEN_RESPONSES_MODELS),
("messages", OPENCODE_ZEN_MESSAGES_MODELS),
("chat", OPENCODE_ZEN_CHAT_MODELS),
];
offerings.extend(groups.into_iter().flat_map(|(endpoint_key, models)| {
let provider = provider.clone();
models.iter().map(move |model| ProviderModelOffering {
provider: provider.clone(),
canonical_model: (*model == "gpt-5.6-sol").then(|| ModelId::from("gpt-5.6")),
wire_model_id: WireModelId::from(*model),
endpoint_key: endpoint_key.to_string(),
default_for_provider: *model == "gpt-5.6-sol",
limits: RouteLimits::default(),
capabilities: RouteCapabilities::default(),
pricing: PricingSku::UnknownOrStale,
})
}));
fn ms_capabilities(model: &str) -> RouteCapabilities {
let image_input = match model {
"qwen3.8-max" | "qwen3.8-max-preview" | "qwen3.7-plus" | "qwen3.6-flash" => {
CapabilityState::Supported
}
_ => CapabilityState::Unsupported,
};
RouteCapabilities {
reasoning: CapabilityState::Supported,
native_tool_calls: CapabilityState::Supported,
structured_output: CapabilityState::Supported,
streaming: CapabilityState::Supported,
image_input,
..RouteCapabilities::default()
}
}
fn ms_limits(model: &str) -> RouteLimits {
let (context_tokens, output_tokens) = match model {
"qwen3.8-max" | "qwen3.8-max-preview" => (1_000_000, 131_072),
"qwen3.7-plus" | "qwen3.7-max" => (1_000_000, 65_536),
"qwen3.6-flash" => (1_000_000, 65_536),
"deepseek-v4-pro" | "deepseek-v4-flash-0731" => (1_000_000, 384_000),
"glm-5.2" => (1_000_000, 131_072),
_ => (1_000_000, 131_072),
};
RouteLimits {
context_tokens: Some(context_tokens),
input_tokens: None,
output_tokens: Some(output_tokens),
}
}
let plan = ProviderId::from("modelstudio-token-plan");
offerings.extend(
MODELSTUDIO_TEXT_MODELS
.iter()
.enumerate()
.map(|(i, model)| ProviderModelOffering {
provider: plan.clone(),
canonical_model: None,
wire_model_id: WireModelId::from(*model),
endpoint_key: "chat".to_string(),
default_for_provider: i == 0,
limits: ms_limits(model),
capabilities: ms_capabilities(model),
pricing: PricingSku::UnknownOrStale,
}),
);
offerings
}