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 bundled, network-free catalog snapshot.
216    ///
217    /// This is the curated reference set every install carries; it needs no
218    /// credentials or network and is always available.
219    #[must_use]
220    pub fn bundled() -> Self {
221        Self::from_offerings(&bundled_catalog_offerings())
222    }
223
224    /// All cards, in stable `(provider, model id)` order.
225    #[must_use]
226    pub fn cards(&self) -> &[ModelReferenceCard] {
227        &self.cards
228    }
229
230    /// Number of cards.
231    #[must_use]
232    pub fn len(&self) -> usize {
233        self.cards.len()
234    }
235
236    /// Whether the database is empty.
237    #[must_use]
238    pub fn is_empty(&self) -> bool {
239        self.cards.is_empty()
240    }
241
242    /// Distinct provider ids present, sorted.
243    #[must_use]
244    pub fn providers(&self) -> Vec<&str> {
245        self.cards
246            .iter()
247            .map(|card| card.provider.as_str())
248            .collect::<BTreeSet<_>>()
249            .into_iter()
250            .collect()
251    }
252
253    /// All cards served by one provider id.
254    #[must_use]
255    pub fn for_provider(&self, provider: &str) -> Vec<&ModelReferenceCard> {
256        self.cards
257            .iter()
258            .filter(|card| card.provider == provider)
259            .collect()
260    }
261
262    /// Find a card by `(provider, model id)`.
263    #[must_use]
264    pub fn find(&self, provider: &str, model_id: &str) -> Option<&ModelReferenceCard> {
265        self.cards
266            .iter()
267            .find(|card| card.provider == provider && card.model_id == model_id)
268    }
269}
270
271/// Round a token count to a short human label (`"1M"`, `"203K"`, `"512"`), or
272/// `"unknown"` for an absent count. Used for display only; callers needing the
273/// exact value read the `Option<u64>` field directly.
274fn humanize_tokens(tokens: Option<u64>) -> String {
275    let Some(tokens) = tokens else {
276        return "unknown".to_string();
277    };
278    if tokens >= 1_000_000 {
279        let millions = tokens as f64 / 1_000_000.0;
280        let rendered = format!("{millions:.2}");
281        let trimmed = rendered.trim_end_matches('0').trim_end_matches('.');
282        format!("{trimmed}M")
283    } else if tokens >= 1_000 {
284        format!("{}K", (tokens as f64 / 1_000.0).round() as u64)
285    } else {
286        tokens.to_string()
287    }
288}
289
290fn currency_symbol(currency: &Currency) -> &'static str {
291    match currency {
292        Currency::Usd => "$",
293        Currency::Cny => "¥",
294        Currency::Other(_) => "",
295    }
296}
297
298fn currency_suffix(currency: &Currency) -> String {
299    match currency {
300        Currency::Usd | Currency::Cny => String::new(),
301        Currency::Other(code) => format!(" {code}"),
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use crate::models_dev::{ModelsDevCost, ModelsDevLimit};
309
310    fn offering(provider: &str, wire: &str) -> CatalogOffering {
311        CatalogOffering {
312            provider: provider.to_string(),
313            wire_model_id: wire.to_string(),
314            endpoint_key: "chat".to_string(),
315            source: CatalogSource::Bundled,
316            ..Default::default()
317        }
318    }
319
320    #[test]
321    fn modality_text_multimodal_and_unknown() {
322        assert_eq!(Modality::from_modalities(None), Modality::Unknown);
323        assert_eq!(
324            Modality::from_modalities(Some(&ModelsDevModalities::default())),
325            Modality::Unknown,
326            "an empty modality block is unknown, not text-only"
327        );
328        assert_eq!(
329            Modality::from_modalities(Some(&ModelsDevModalities {
330                input: vec!["text".to_string()],
331                output: vec!["text".to_string()],
332            })),
333            Modality::Text
334        );
335        assert_eq!(
336            Modality::from_modalities(Some(&ModelsDevModalities {
337                input: vec!["text".to_string(), "image".to_string()],
338                output: vec!["text".to_string()],
339            })),
340            Modality::Multimodal
341        );
342        // Case-insensitive and tolerant of an output-only non-text modality.
343        assert_eq!(
344            Modality::from_modalities(Some(&ModelsDevModalities {
345                input: vec!["TEXT".to_string()],
346                output: vec!["Audio".to_string()],
347            })),
348            Modality::Multimodal
349        );
350    }
351
352    #[test]
353    fn card_projects_stated_facts() {
354        let row = CatalogOffering {
355            family: Some("deepseek".to_string()),
356            limit: Some(ModelsDevLimit {
357                context: Some(1_000_000),
358                input: None,
359                output: Some(384_000),
360            }),
361            cost: Some(ModelsDevCost {
362                input: Some(0.3),
363                output: Some(1.2),
364                cache_read: Some(0.06),
365                cache_write: None,
366            }),
367            modalities: Some(ModelsDevModalities {
368                input: vec!["text".to_string()],
369                output: vec!["text".to_string()],
370            }),
371            ..offering("deepseek", "deepseek-v4-pro")
372        };
373        let card = ModelReferenceCard::from_offering(&row);
374
375        assert_eq!(card.provider, "deepseek");
376        assert_eq!(card.provider_kind, Some(ProviderKind::Deepseek));
377        assert_eq!(card.provider_kind_label(), "deepseek");
378        assert_eq!(card.model_id, "deepseek-v4-pro");
379        assert_eq!(card.family.as_deref(), Some("deepseek"));
380        assert_eq!(card.context_window, Some(1_000_000));
381        assert_eq!(card.context_window_label(), "1M");
382        assert_eq!(card.max_output, Some(384_000));
383        assert_eq!(card.max_output_label(), "384K");
384        assert_eq!(card.modality, Modality::Text);
385        assert_eq!(card.price_label(), "$0.30 / $1.20 per Mtok");
386    }
387
388    #[test]
389    fn custom_local_row_is_all_unknown_but_keeps_model_id_verbatim() {
390        // A user-named custom endpoint with no catalog facts: provider kind,
391        // context window, modality, and price are all unknown — never guessed —
392        // and the model id is preserved exactly.
393        let row = CatalogOffering {
394            source: CatalogSource::UserOverride,
395            ..offering("my-local-llm", "Vendor/Custom-Model_v1")
396        };
397        let card = ModelReferenceCard::from_offering(&row);
398
399        assert_eq!(card.provider_kind, None);
400        assert_eq!(card.provider_kind_label(), "unknown");
401        assert_eq!(card.model_id, "Vendor/Custom-Model_v1");
402        assert_eq!(card.context_window, None);
403        assert_eq!(card.context_window_label(), "unknown");
404        assert_eq!(card.max_output_label(), "unknown");
405        assert_eq!(card.modality, Modality::Unknown);
406        assert_eq!(card.price_label(), "unknown");
407    }
408
409    #[test]
410    fn unpriced_and_cache_only_rows_report_unknown_price_never_zero() {
411        // No cost block at all.
412        let unpriced = ModelReferenceCard::from_offering(&offering("deepseek", "deepseek-v4-pro"));
413        assert_eq!(unpriced.price_label(), "unknown");
414        assert!(unpriced.pricing.is_none());
415
416        // A cost object priced only on cache classes is still unknown for the
417        // headline input/output rate label.
418        let cache_only = CatalogOffering {
419            cost: Some(ModelsDevCost {
420                input: None,
421                output: None,
422                cache_read: Some(0.05),
423                cache_write: None,
424            }),
425            ..offering("acme", "house-model")
426        };
427        assert_eq!(
428            ModelReferenceCard::from_offering(&cache_only).price_label(),
429            "unknown"
430        );
431    }
432
433    #[test]
434    fn partial_price_renders_known_rate_and_marks_the_other_unknown() {
435        let row = CatalogOffering {
436            cost: Some(ModelsDevCost {
437                input: Some(5.0),
438                output: None,
439                cache_read: None,
440                cache_write: None,
441            }),
442            ..offering("openai", "gpt-5.5")
443        };
444        assert_eq!(
445            ModelReferenceCard::from_offering(&row).price_label(),
446            "$5.00 / ? per Mtok"
447        );
448    }
449
450    #[test]
451    fn database_is_sorted_deduped_and_queryable() {
452        let rows = vec![
453            CatalogOffering {
454                limit: Some(ModelsDevLimit {
455                    context: Some(1),
456                    input: None,
457                    output: None,
458                }),
459                ..offering("zai", "GLM-5.2")
460            },
461            offering("deepseek", "deepseek-v4-pro"),
462            // Duplicate identity with a higher context wins (last-write).
463            CatalogOffering {
464                limit: Some(ModelsDevLimit {
465                    context: Some(1_000_000),
466                    input: None,
467                    output: None,
468                }),
469                ..offering("zai", "GLM-5.2")
470            },
471        ];
472        let db = ModelReferenceDatabase::from_offerings(&rows);
473
474        assert_eq!(db.len(), 2, "duplicate (provider, model) collapses to one");
475        // Sorted by (provider, model id): deepseek before zai.
476        assert_eq!(db.cards()[0].provider, "deepseek");
477        assert_eq!(db.cards()[1].provider, "zai");
478        assert_eq!(db.providers(), vec!["deepseek", "zai"]);
479        assert_eq!(db.for_provider("zai").len(), 1);
480        assert_eq!(
481            db.find("zai", "GLM-5.2")
482                .and_then(|card| card.context_window),
483            Some(1_000_000),
484            "last-write-wins kept the richer row"
485        );
486        assert!(db.find("zai", "missing").is_none());
487    }
488
489    #[test]
490    fn bundled_database_is_nonempty_and_honest() {
491        let db = ModelReferenceDatabase::bundled();
492        assert!(!db.is_empty());
493        assert!(
494            db.len() >= 20,
495            "bundled snapshot should carry the curated offerings, got {}",
496            db.len()
497        );
498
499        // Every card preserves a non-empty model id and resolves a known kind
500        // for the bundled (first-class) providers.
501        for card in db.cards() {
502            assert!(!card.model_id.is_empty());
503            assert!(
504                card.provider_kind.is_some(),
505                "bundled provider {} should map to a known kind",
506                card.provider
507            );
508        }
509
510        // A DeepSeek-native row: context window known, price honestly unknown
511        // (the bundled snapshot omits DeepSeek-native per-token pricing).
512        let deepseek = db
513            .find("deepseek", "deepseek-v4-pro")
514            .expect("bundled deepseek row");
515        assert_eq!(deepseek.context_window, Some(1_000_000));
516        assert_eq!(deepseek.modality, Modality::Text);
517        assert_eq!(deepseek.price_label(), "unknown");
518
519        // A priced row surfaces its stated per-token rate.
520        let minimax = db
521            .find("minimax", "MiniMax-M3")
522            .expect("bundled minimax row");
523        assert_eq!(minimax.price_label(), "$0.30 / $1.20 per Mtok");
524    }
525
526    #[test]
527    fn humanize_tokens_shapes() {
528        assert_eq!(humanize_tokens(None), "unknown");
529        assert_eq!(humanize_tokens(Some(512)), "512");
530        assert_eq!(humanize_tokens(Some(131_072)), "131K");
531        assert_eq!(humanize_tokens(Some(1_000_000)), "1M");
532        assert_eq!(humanize_tokens(Some(1_050_000)), "1.05M");
533    }
534}