Skip to main content

codewhale_config/
model_reference.rs

1//! Factual model reference database (#3205, #2300).
2//!
3//! A browsable, read-only projection of the compiled catalog into per-offering
4//! "fact cards": the model id as-is, the serving provider and its kind, the
5//! context window, the price, and the modality (text vs multimodal). It exists
6//! to answer "what are this model's stated attributes?", nothing more.
7//!
8//! This layer is **labels only**. It performs no selection, routing, tiering,
9//! or ranking — it never decides which model to use, and it carries no
10//! `strong`/`balanced`/`fast` or role concept. It is a superset-free view over
11//! [`crate::catalog::CatalogOffering`] rows.
12//!
13//! Honesty rule (shared with #2608 / #3085): an attribute the catalog layer did
14//! not state is reported as **unknown**, never guessed. A local/custom endpoint
15//! with no catalog facts yields `Unknown` modality, `None` context window, and
16//! an unknown price — its model id is still preserved verbatim. Nothing here is
17//! inferred from a model-id prefix.
18
19use std::collections::{BTreeMap, BTreeSet};
20
21use serde::{Deserialize, Serialize};
22
23use crate::ProviderKind;
24use crate::catalog::{CatalogOffering, CatalogSnapshot, CatalogSource, bundled_catalog_offerings};
25use crate::models_dev::ModelsDevModalities;
26use crate::pricing::{Currency, OfferingPricing};
27
28/// Coarse, factual input/output modality label for a model.
29///
30/// `text` vs `multimodal` is derived from the union of stated input/output
31/// modalities. Absent modality metadata is [`Modality::Unknown`], distinct from
32/// a stated text-only model — "we were not told" is not "text only".
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
34#[serde(rename_all = "snake_case")]
35pub enum Modality {
36    /// Every stated modality is text.
37    Text,
38    /// At least one stated modality is non-text (image/audio/video/…).
39    Multimodal,
40    /// No modality metadata was stated for this row.
41    #[default]
42    Unknown,
43}
44
45impl Modality {
46    /// Classify the modality from a Models.dev-shaped modality block.
47    ///
48    /// Returns [`Modality::Unknown`] for absent metadata or an empty list,
49    /// [`Modality::Multimodal`] when any stated input/output modality is not
50    /// `text`, and [`Modality::Text`] when the only stated modalities are text.
51    #[must_use]
52    pub fn from_modalities(modalities: Option<&ModelsDevModalities>) -> Self {
53        let Some(modalities) = modalities else {
54            return Self::Unknown;
55        };
56        let mut saw_any = false;
57        for modality in modalities.input.iter().chain(modalities.output.iter()) {
58            let trimmed = modality.trim();
59            if trimmed.is_empty() {
60                continue;
61            }
62            saw_any = true;
63            if !trimmed.eq_ignore_ascii_case("text") {
64                return Self::Multimodal;
65            }
66        }
67        if saw_any { Self::Text } else { Self::Unknown }
68    }
69
70    /// Stable lowercase label.
71    #[must_use]
72    pub fn as_str(self) -> &'static str {
73        match self {
74            Self::Text => "text",
75            Self::Multimodal => "multimodal",
76            Self::Unknown => "unknown",
77        }
78    }
79}
80
81/// A factual reference card for one provider offering.
82///
83/// Every field is either a stated fact or an explicit unknown. This is a
84/// labels-only projection: it carries no routing, tier, or selection concept.
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct ModelReferenceCard {
87    /// Provider id serving this offering, exactly as the catalog row states it.
88    pub provider: String,
89    /// Resolved built-in provider kind, when the provider id maps to one.
90    ///
91    /// `None` for an unrecognized / user-named custom provider — an unknown
92    /// kind, not a guess.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub provider_kind: Option<ProviderKind>,
95    /// The provider wire model id, verbatim. Never normalized or prefixed.
96    pub model_id: String,
97    /// Canonical model identity, only when the row carried an explicit join.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub canonical_model: Option<String>,
100    /// Model family / series, when stated.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub family: Option<String>,
103    /// Context-window tokens, when stated.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub context_window: Option<u64>,
106    /// Max-output tokens, when stated.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub max_output: Option<u64>,
109    /// Text vs multimodal, or unknown.
110    pub modality: Modality,
111    /// Per-token pricing facts, when priced. `None` is unknown, never free.
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub pricing: Option<OfferingPricing>,
114    /// Provenance of the underlying catalog row (bundled / live / override).
115    pub source: CatalogSource,
116}
117
118impl ModelReferenceCard {
119    /// Project a catalog offering into its factual reference card.
120    #[must_use]
121    pub fn from_offering(offering: &CatalogOffering) -> Self {
122        Self {
123            provider: offering.provider.clone(),
124            provider_kind: ProviderKind::parse(&offering.provider),
125            model_id: offering.wire_model_id.clone(),
126            canonical_model: offering.canonical_model.clone(),
127            family: offering.family.clone(),
128            context_window: offering.limit.as_ref().and_then(|limit| limit.context),
129            max_output: offering.limit.as_ref().and_then(|limit| limit.output),
130            modality: Modality::from_modalities(offering.modalities.as_ref()),
131            pricing: OfferingPricing::from_catalog_offering(offering),
132            source: offering.source.clone(),
133        }
134    }
135
136    /// Label for the resolved provider kind, or `"unknown"`.
137    #[must_use]
138    pub fn provider_kind_label(&self) -> &'static str {
139        self.provider_kind.map_or("unknown", ProviderKind::as_str)
140    }
141
142    /// Human context-window label such as `"1M"`, `"131K"`, `"512"`, or
143    /// `"unknown"`. The exact token count remains on [`Self::context_window`].
144    #[must_use]
145    pub fn context_window_label(&self) -> String {
146        humanize_tokens(self.context_window)
147    }
148
149    /// Human max-output label, same shape as [`Self::context_window_label`].
150    #[must_use]
151    pub fn max_output_label(&self) -> String {
152        humanize_tokens(self.max_output)
153    }
154
155    /// Short factual price label, e.g. `"$0.30 / $1.20 per Mtok"`, or
156    /// `"unknown"` when no per-token input/output rate is sourced.
157    ///
158    /// A `?` in one slot means that single rate is unknown while the other is
159    /// stated; a fully unknown price collapses to `"unknown"` rather than a
160    /// fabricated zero.
161    #[must_use]
162    pub fn price_label(&self) -> String {
163        let Some(pricing) = self.pricing.as_ref() else {
164            return "unknown".to_string();
165        };
166        if pricing.input_per_million.is_none() && pricing.output_per_million.is_none() {
167            return "unknown".to_string();
168        }
169        let symbol = currency_symbol(&pricing.currency);
170        let render = |value: Option<f64>| match value {
171            Some(rate) => format!("{symbol}{rate:.2}"),
172            None => "?".to_string(),
173        };
174        let suffix = currency_suffix(&pricing.currency);
175        format!(
176            "{} / {} per Mtok{suffix}",
177            render(pricing.input_per_million),
178            render(pricing.output_per_million),
179        )
180    }
181}
182
183/// A browsable, read-only factual reference database of model offerings.
184///
185/// Cards are sorted by `(provider, model id)` and de-duplicated on that
186/// identity, so the database is deterministic regardless of input order.
187#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
188pub struct ModelReferenceDatabase {
189    cards: Vec<ModelReferenceCard>,
190}
191
192impl ModelReferenceDatabase {
193    /// Build from raw catalog offerings.
194    ///
195    /// Rows are keyed by `(provider, model id)`; a later row with the same
196    /// identity replaces an earlier one, matching catalog merge semantics.
197    #[must_use]
198    pub fn from_offerings(offerings: &[CatalogOffering]) -> Self {
199        let mut by_identity: BTreeMap<(String, String), ModelReferenceCard> = BTreeMap::new();
200        for offering in offerings {
201            let card = ModelReferenceCard::from_offering(offering);
202            by_identity.insert((card.provider.clone(), card.model_id.clone()), card);
203        }
204        Self {
205            cards: by_identity.into_values().collect(),
206        }
207    }
208
209    /// Build from a compiled catalog snapshot (bundled < live < overrides).
210    #[must_use]
211    pub fn from_snapshot(snapshot: &CatalogSnapshot) -> Self {
212        Self::from_offerings(&snapshot.offerings)
213    }
214
215    /// Build from CodeWhale's offline/stale bundled catalog snapshot (#4188).
216    ///
217    /// Prefer a live/compiled [`CatalogSnapshot`] when available. The bundled
218    /// set needs no credentials or network and remains the offline fallback
219    /// every install carries.
220    #[must_use]
221    pub fn bundled() -> Self {
222        Self::from_offerings(&bundled_catalog_offerings())
223    }
224
225    /// All cards, in stable `(provider, model id)` order.
226    #[must_use]
227    pub fn cards(&self) -> &[ModelReferenceCard] {
228        &self.cards
229    }
230
231    /// Number of cards.
232    #[must_use]
233    pub fn len(&self) -> usize {
234        self.cards.len()
235    }
236
237    /// Whether the database is empty.
238    #[must_use]
239    pub fn is_empty(&self) -> bool {
240        self.cards.is_empty()
241    }
242
243    /// Distinct provider ids present, sorted.
244    #[must_use]
245    pub fn providers(&self) -> Vec<&str> {
246        self.cards
247            .iter()
248            .map(|card| card.provider.as_str())
249            .collect::<BTreeSet<_>>()
250            .into_iter()
251            .collect()
252    }
253
254    /// All cards served by one provider id.
255    #[must_use]
256    pub fn for_provider(&self, provider: &str) -> Vec<&ModelReferenceCard> {
257        self.cards
258            .iter()
259            .filter(|card| card.provider == provider)
260            .collect()
261    }
262
263    /// Find a card by `(provider, model id)`.
264    #[must_use]
265    pub fn find(&self, provider: &str, model_id: &str) -> Option<&ModelReferenceCard> {
266        self.cards
267            .iter()
268            .find(|card| card.provider == provider && card.model_id == model_id)
269    }
270}
271
272/// Round a token count to a short human label (`"1M"`, `"203K"`, `"512"`), or
273/// `"unknown"` for an absent count. Used for display only; callers needing the
274/// exact value read the `Option<u64>` field directly.
275fn humanize_tokens(tokens: Option<u64>) -> String {
276    let Some(tokens) = tokens else {
277        return "unknown".to_string();
278    };
279    if tokens >= 1_000_000 {
280        let millions = tokens as f64 / 1_000_000.0;
281        let rendered = format!("{millions:.2}");
282        let trimmed = rendered.trim_end_matches('0').trim_end_matches('.');
283        format!("{trimmed}M")
284    } else if tokens >= 1_000 {
285        format!("{}K", (tokens as f64 / 1_000.0).round() as u64)
286    } else {
287        tokens.to_string()
288    }
289}
290
291fn currency_symbol(currency: &Currency) -> &'static str {
292    match currency {
293        Currency::Usd => "$",
294        Currency::Cny => "¥",
295        Currency::Other(_) => "",
296    }
297}
298
299fn currency_suffix(currency: &Currency) -> String {
300    match currency {
301        Currency::Usd | Currency::Cny => String::new(),
302        Currency::Other(code) => format!(" {code}"),
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::models_dev::{ModelsDevCost, ModelsDevLimit};
310
311    fn offering(provider: &str, wire: &str) -> CatalogOffering {
312        CatalogOffering {
313            provider: provider.to_string(),
314            wire_model_id: wire.to_string(),
315            endpoint_key: "chat".to_string(),
316            source: CatalogSource::Bundled,
317            ..Default::default()
318        }
319    }
320
321    #[test]
322    fn modality_text_multimodal_and_unknown() {
323        assert_eq!(Modality::from_modalities(None), Modality::Unknown);
324        assert_eq!(
325            Modality::from_modalities(Some(&ModelsDevModalities::default())),
326            Modality::Unknown,
327            "an empty modality block is unknown, not text-only"
328        );
329        assert_eq!(
330            Modality::from_modalities(Some(&ModelsDevModalities {
331                input: vec!["text".to_string()],
332                output: vec!["text".to_string()],
333            })),
334            Modality::Text
335        );
336        assert_eq!(
337            Modality::from_modalities(Some(&ModelsDevModalities {
338                input: vec!["text".to_string(), "image".to_string()],
339                output: vec!["text".to_string()],
340            })),
341            Modality::Multimodal
342        );
343        // Case-insensitive and tolerant of an output-only non-text modality.
344        assert_eq!(
345            Modality::from_modalities(Some(&ModelsDevModalities {
346                input: vec!["TEXT".to_string()],
347                output: vec!["Audio".to_string()],
348            })),
349            Modality::Multimodal
350        );
351    }
352
353    #[test]
354    fn card_projects_stated_facts() {
355        let row = CatalogOffering {
356            family: Some("deepseek".to_string()),
357            limit: Some(ModelsDevLimit {
358                context: Some(1_000_000),
359                input: None,
360                output: Some(384_000),
361            }),
362            cost: Some(ModelsDevCost {
363                input: Some(0.3),
364                output: Some(1.2),
365                cache_read: Some(0.06),
366                cache_write: None,
367            }),
368            modalities: Some(ModelsDevModalities {
369                input: vec!["text".to_string()],
370                output: vec!["text".to_string()],
371            }),
372            ..offering("deepseek", "deepseek-v4-pro")
373        };
374        let card = ModelReferenceCard::from_offering(&row);
375
376        assert_eq!(card.provider, "deepseek");
377        assert_eq!(card.provider_kind, Some(ProviderKind::Deepseek));
378        assert_eq!(card.provider_kind_label(), "deepseek");
379        assert_eq!(card.model_id, "deepseek-v4-pro");
380        assert_eq!(card.family.as_deref(), Some("deepseek"));
381        assert_eq!(card.context_window, Some(1_000_000));
382        assert_eq!(card.context_window_label(), "1M");
383        assert_eq!(card.max_output, Some(384_000));
384        assert_eq!(card.max_output_label(), "384K");
385        assert_eq!(card.modality, Modality::Text);
386        assert_eq!(card.price_label(), "$0.30 / $1.20 per Mtok");
387    }
388
389    #[test]
390    fn custom_local_row_is_all_unknown_but_keeps_model_id_verbatim() {
391        // A user-named custom endpoint with no catalog facts: provider kind,
392        // context window, modality, and price are all unknown — never guessed —
393        // and the model id is preserved exactly.
394        let row = CatalogOffering {
395            source: CatalogSource::UserOverride,
396            ..offering("my-local-llm", "Vendor/Custom-Model_v1")
397        };
398        let card = ModelReferenceCard::from_offering(&row);
399
400        assert_eq!(card.provider_kind, None);
401        assert_eq!(card.provider_kind_label(), "unknown");
402        assert_eq!(card.model_id, "Vendor/Custom-Model_v1");
403        assert_eq!(card.context_window, None);
404        assert_eq!(card.context_window_label(), "unknown");
405        assert_eq!(card.max_output_label(), "unknown");
406        assert_eq!(card.modality, Modality::Unknown);
407        assert_eq!(card.price_label(), "unknown");
408    }
409
410    #[test]
411    fn unpriced_and_cache_only_rows_report_unknown_price_never_zero() {
412        // No cost block at all.
413        let unpriced = ModelReferenceCard::from_offering(&offering("deepseek", "deepseek-v4-pro"));
414        assert_eq!(unpriced.price_label(), "unknown");
415        assert!(unpriced.pricing.is_none());
416
417        // A cost object priced only on cache classes is still unknown for the
418        // headline input/output rate label.
419        let cache_only = CatalogOffering {
420            cost: Some(ModelsDevCost {
421                input: None,
422                output: None,
423                cache_read: Some(0.05),
424                cache_write: None,
425            }),
426            ..offering("acme", "house-model")
427        };
428        assert_eq!(
429            ModelReferenceCard::from_offering(&cache_only).price_label(),
430            "unknown"
431        );
432    }
433
434    #[test]
435    fn partial_price_renders_known_rate_and_marks_the_other_unknown() {
436        let row = CatalogOffering {
437            cost: Some(ModelsDevCost {
438                input: Some(5.0),
439                output: None,
440                cache_read: None,
441                cache_write: None,
442            }),
443            ..offering("openai", "gpt-5.5")
444        };
445        assert_eq!(
446            ModelReferenceCard::from_offering(&row).price_label(),
447            "$5.00 / ? per Mtok"
448        );
449    }
450
451    #[test]
452    fn database_is_sorted_deduped_and_queryable() {
453        let rows = vec![
454            CatalogOffering {
455                limit: Some(ModelsDevLimit {
456                    context: Some(1),
457                    input: None,
458                    output: None,
459                }),
460                ..offering("zai", "GLM-5.2")
461            },
462            offering("deepseek", "deepseek-v4-pro"),
463            // Duplicate identity with a higher context wins (last-write).
464            CatalogOffering {
465                limit: Some(ModelsDevLimit {
466                    context: Some(1_000_000),
467                    input: None,
468                    output: None,
469                }),
470                ..offering("zai", "GLM-5.2")
471            },
472        ];
473        let db = ModelReferenceDatabase::from_offerings(&rows);
474
475        assert_eq!(db.len(), 2, "duplicate (provider, model) collapses to one");
476        // Sorted by (provider, model id): deepseek before zai.
477        assert_eq!(db.cards()[0].provider, "deepseek");
478        assert_eq!(db.cards()[1].provider, "zai");
479        assert_eq!(db.providers(), vec!["deepseek", "zai"]);
480        assert_eq!(db.for_provider("zai").len(), 1);
481        assert_eq!(
482            db.find("zai", "GLM-5.2")
483                .and_then(|card| card.context_window),
484            Some(1_000_000),
485            "last-write-wins kept the richer row"
486        );
487        assert!(db.find("zai", "missing").is_none());
488    }
489
490    #[test]
491    fn bundled_database_is_nonempty_and_honest() {
492        let db = ModelReferenceDatabase::bundled();
493        assert!(!db.is_empty());
494        assert!(
495            db.len() >= 20,
496            "bundled offline snapshot should carry seed offerings, got {}",
497            db.len()
498        );
499
500        // Every card preserves a non-empty model id and resolves a known kind
501        // for the bundled (first-class) providers.
502        for card in db.cards() {
503            assert!(!card.model_id.is_empty());
504            assert!(
505                card.provider_kind.is_some(),
506                "bundled provider {} should map to a known kind",
507                card.provider
508            );
509        }
510
511        // A DeepSeek-native row: context window known, price honestly unknown
512        // (the bundled snapshot omits DeepSeek-native per-token pricing).
513        let deepseek = db
514            .find("deepseek", "deepseek-v4-pro")
515            .expect("bundled deepseek row");
516        assert_eq!(deepseek.context_window, Some(1_000_000));
517        assert_eq!(deepseek.modality, Modality::Text);
518        assert_eq!(deepseek.price_label(), "unknown");
519
520        // A priced row surfaces its stated per-token rate.
521        let minimax = db
522            .find("minimax", "MiniMax-M2.7")
523            .expect("bundled minimax row");
524        assert_eq!(minimax.price_label(), "$0.30 / $1.20 per Mtok");
525
526        // MiniMax-M3 ships without verified per-token pricing, so its price
527        // stays honestly unknown rather than inheriting a sibling's rate.
528        let minimax_m3 = db
529            .find("minimax", "MiniMax-M3")
530            .expect("bundled minimax m3 row");
531        assert_eq!(minimax_m3.price_label(), "unknown");
532    }
533
534    #[test]
535    fn humanize_tokens_shapes() {
536        assert_eq!(humanize_tokens(None), "unknown");
537        assert_eq!(humanize_tokens(Some(512)), "512");
538        assert_eq!(humanize_tokens(Some(131_072)), "131K");
539        assert_eq!(humanize_tokens(Some(1_000_000)), "1M");
540        assert_eq!(humanize_tokens(Some(1_050_000)), "1.05M");
541    }
542}