返回 CodeWhale
offering.rs
根目录 / crates / config / src / 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
14 use serde::{Deserialize, Serialize};
15
16 use super::candidate::PricingSku;
17 use super::capabilities::RouteCapabilities;
18 use 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)]
26 pub 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
38 impl 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)]
51 pub 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 /// Endpoint key for a Zen catalog row Models.dev marks `deprecated`. It names
78 /// no protocol, so the resolver refuses the model locally with this reason
79 /// instead of sending a request Zen no longer serves.
80 pub const OPENCODE_ZEN_DEPRECATED_ENDPOINT_KEY: &str = "deprecated";
81
82 /// Endpoint key OpenCode Zen serves a model on, from the AI SDK package
83 /// OpenCode's own catalog names for it.
84 ///
85 /// Models.dev's `opencode` provider is Zen's published catalog: its provider
86 /// default is `@ai-sdk/openai-compatible`, and a model served over another
87 /// wire overrides that with `provider.npm`. The package is the wire fact, so it
88 /// is mapped exactly and never guessed from a model-id family, which does not
89 /// hold on Zen (qwen3.8-flash is Messages while qwen3.8-max is Chat). Google's
90 /// package maps to `"google"` and any other to `"unproven"`; neither resolves
91 /// to a protocol, so the resolver fails closed and names the endpoint.
92 #[must_use]
93 pub fn opencode_zen_endpoint_key_for_npm(npm: Option<&str>) -> &'static str {
94 match npm.map(str::trim) {
95 Some("@ai-sdk/openai") => "responses",
96 Some("@ai-sdk/anthropic") => "messages",
97 Some("@ai-sdk/openai-compatible") => "chat",
98 Some("@ai-sdk/google") => "google",
99 _ => "unproven",
100 }
101 }
102
103 /// Logical default plus every documented Zen wire id, for picker fallbacks
104 /// when Models.dev is stale or failed. `gpt-5.6` is the user-facing default;
105 /// `gpt-5.6-sol` is the proven Responses wire id.
106 #[must_use]
107 pub fn opencode_zen_picker_models() -> Vec<&'static str> {
108 crate::catalog::reviewed::constants::completion_names("opencode-zen").to_vec()
109 }
110
111 /// Codewhale API bootstrap rows used only when the account's live
112 /// `GET {base}/models` cannot be fetched.
113 ///
114 /// The account catalog is authoritative: it lists exactly the providers the
115 /// customer connected, and each row states its own protocol. These three rows
116 /// exist so a route can still be selected offline; every consumer that shows
117 /// models must say the list is a fallback, not the account's catalog.
118 pub use crate::catalog::reviewed::constants::CODEWHALE_FALLBACK_MODELS;
119
120 /// Endpoint key for one Codewhale API model id.
121 ///
122 /// The live catalog states the protocol per model in `codewhale.protocol`;
123 /// this is the offline inference used for the bootstrap rows and for a model
124 /// id the local catalog has never seen. Only the `anthropic/` namespace routes
125 /// to `{base}/messages`; everything else is OpenAI Chat Completions at
126 /// `{base}/chat/completions`. The id alone carries no signal for the
127 /// Responses surface — a `responses` row only ever comes from the catalog's
128 /// stated `codewhale.protocol`, never from a model name.
129 #[must_use]
130 pub fn codewhale_endpoint_key_for_model(model: &str) -> &'static str {
131 if model.trim().to_ascii_lowercase().starts_with("anthropic/") {
132 "messages"
133 } else {
134 "chat"
135 }
136 }
137
138 /// Return curated provider/model transport facts as owned offering rows.
139 ///
140 /// OpenCode Zen's official catalog serves models over three protocol families.
141 /// These rows intentionally carry no inferred limits, pricing, or canonical
142 /// identity: their sole claim is the documented wire model and endpoint key.
143 #[must_use]
144 pub fn bundled_offerings() -> Vec<ProviderModelOffering> {
145 crate::catalog::reviewed::bundled_reviewed()
146 .transports
147 .iter()
148 .map(crate::catalog::reviewed::ReviewedTransport::to_offering)
149 .collect()
150 }
151
151 lines RUST