Skip to main content

codewhale_config/route/
offering.rs

1//! Provider model offerings (#3084).
2//!
3//! A [`ProviderModelOffering`] binds a provider to a canonical model, the
4//! provider-owned wire id that serves it, and the endpoint key. This is the
5//! seam that proves the #2608 invariant: the SAME canonical model can be served
6//! by multiple providers under DIFFERENT wire ids (some aggregator-prefixed),
7//! and a prefix never implies provider ownership.
8//!
9//! Catalog-derived offerings from [`crate::catalog::bundled_catalog_offerings`]
10//! remain the general bundled source of truth. [`bundled_offerings`] contains
11//! only transport facts that Models.dev cannot express, such as a single
12//! provider routing different models over different wire protocols.
13
14use serde::{Deserialize, Serialize};
15
16use super::candidate::PricingSku;
17use super::capabilities::RouteCapabilities;
18use super::ids::{ModelId, ProviderId, WireModelId};
19
20/// Token limits for one resolved route/offering.
21///
22/// These are optional because hosted catalogs, local runtimes, and custom
23/// endpoints can legitimately omit some or all limit facts. Callers should
24/// treat `None` as unknown, not zero.
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
26pub struct RouteLimits {
27    /// Total context window (input + output), in tokens.
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub context_tokens: Option<u64>,
30    /// Input-token limit, when the provider reports it separately.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub input_tokens: Option<u64>,
33    /// Output-token cap for the route/offering, when known.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub output_tokens: Option<u64>,
36}
37
38impl RouteLimits {
39    /// Whether at least one limit fact is known.
40    #[must_use]
41    pub const fn has_known_limit(self) -> bool {
42        self.context_tokens.is_some() || self.input_tokens.is_some() || self.output_tokens.is_some()
43    }
44}
45
46/// One provider's way of serving a (possibly canonical) model.
47///
48/// `Eq` is intentionally NOT derived: [`PricingSku::Token`] carries `f64` rates,
49/// so the offering is only `PartialEq`. No caller keys a set/map on offerings.
50#[derive(Debug, Clone, PartialEq)]
51pub struct ProviderModelOffering {
52    /// Provider serving this offering.
53    pub provider: ProviderId,
54    /// Canonical model identity, if this offering maps to one.
55    pub canonical_model: Option<ModelId>,
56    /// Provider-owned wire id sent on the request (verbatim).
57    pub wire_model_id: WireModelId,
58    /// Endpoint key the offering is served on.
59    pub endpoint_key: String,
60    /// Whether this is the provider's default offering.
61    pub default_for_provider: bool,
62    /// Provider/offering-scoped token limits, when known.
63    pub limits: RouteLimits,
64    /// Provider/model-scoped capability facts. Unknown is preserved rather
65    /// than inferred from the wire protocol.
66    pub capabilities: RouteCapabilities,
67    /// Coarse route-facing pricing meter for this offering (#3085).
68    ///
69    /// Projected from the offering's sourced cost at the layer that owns it
70    /// (`CatalogOffering::to_offering` → [`crate::pricing::route_pricing_sku`]).
71    /// The resolver carries this verbatim onto the candidate; it is
72    /// [`PricingSku::UnknownOrStale`] whenever no price was sourced — never a
73    /// fabricated zero (the #2608 / #3085 honesty rule).
74    pub pricing: PricingSku,
75}
76
77// Transport snapshot verified against https://opencode.ai/docs/zen on
78// 2026-07-17. Gemini rows are intentionally absent because they use Google's
79// model-specific wire protocol, which CodeWhale does not currently implement.
80pub(crate) const OPENCODE_ZEN_RESPONSES_MODELS: &[&str] = &[
81    "gpt-5.6-sol",
82    "gpt-5.6-terra",
83    "gpt-5.6-luna",
84    "gpt-5.5",
85    "gpt-5.5-pro",
86    "gpt-5.4",
87    "gpt-5.4-pro",
88    "gpt-5.4-mini",
89    "gpt-5.4-nano",
90    "gpt-5.3-codex",
91    "gpt-5.3-codex-spark",
92    "gpt-5.2",
93    "gpt-5.2-codex",
94    "gpt-5.1",
95    "gpt-5.1-codex",
96    "gpt-5.1-codex-max",
97    "gpt-5.1-codex-mini",
98    "gpt-5",
99    "gpt-5-codex",
100    "gpt-5-nano",
101];
102
103pub(crate) const OPENCODE_ZEN_MESSAGES_MODELS: &[&str] = &[
104    "claude-fable-5",
105    "claude-opus-4-8",
106    "claude-opus-4-7",
107    "claude-opus-4-6",
108    "claude-opus-4-5",
109    "claude-sonnet-5",
110    "claude-sonnet-4-6",
111    "claude-sonnet-4-5",
112    "claude-haiku-4-5",
113    "qwen3.7-max",
114    "qwen3.7-plus",
115    "qwen3.6-plus",
116    "qwen3.5-plus",
117];
118
119pub(crate) const OPENCODE_ZEN_CHAT_MODELS: &[&str] = &[
120    "deepseek-v4-pro",
121    "deepseek-v4-flash",
122    "minimax-m3",
123    "minimax-m2.7",
124    "minimax-m2.5",
125    "glm-5.2",
126    "glm-5.1",
127    "glm-5",
128    "kimi-k2.5",
129    "kimi-k2.6",
130    "kimi-k2.7-code",
131    "grok-4.5",
132    "grok-build-0.1",
133    "big-pickle",
134    "mimo-v2.5-free",
135    "north-mini-code-free",
136    "nemotron-3-ultra-free",
137    "deepseek-v4-flash-free",
138];
139
140/// Return curated provider/model transport facts as owned offering rows.
141///
142/// OpenCode Zen's official catalog serves models over three protocol families.
143/// These rows intentionally carry no inferred limits, pricing, or canonical
144/// identity: their sole claim is the documented wire model and endpoint key.
145#[must_use]
146pub fn bundled_offerings() -> Vec<ProviderModelOffering> {
147    let provider = ProviderId::from("opencode-zen");
148    let groups = [
149        ("responses", OPENCODE_ZEN_RESPONSES_MODELS),
150        ("messages", OPENCODE_ZEN_MESSAGES_MODELS),
151        ("chat", OPENCODE_ZEN_CHAT_MODELS),
152    ];
153
154    groups
155        .into_iter()
156        .flat_map(|(endpoint_key, models)| {
157            let provider = provider.clone();
158            models.iter().map(move |model| ProviderModelOffering {
159                provider: provider.clone(),
160                canonical_model: None,
161                wire_model_id: WireModelId::from(*model),
162                endpoint_key: endpoint_key.to_string(),
163                default_for_provider: *model == "gpt-5.5",
164                limits: RouteLimits::default(),
165                capabilities: RouteCapabilities::default(),
166                pricing: PricingSku::UnknownOrStale,
167            })
168        })
169        .collect()
170}