Skip to main content

llm_kernel/provider/
mapping.rs

1//! Provider-id mapping between the embedded catalog and the
2//! [models.dev](https://models.dev) upstream catalog.
3//!
4//! `resolve` answers "which models.dev provider key holds the canonical model
5//! list for a given catalog provider id". This drives the catalog sync tooling
6//! ([`crate::provider::sync`]) and runtime enrichment of discovery results
7//! ([`crate::discovery`]).
8
9/// How a catalog provider id maps to a models.dev provider key.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Mapping {
12    /// The catalog id is the exact models.dev provider key.
13    Exact,
14    /// The catalog id maps to a different models.dev provider key.
15    Aliased(&'static str),
16    /// The catalog id has no models.dev counterpart — keep its models as-is.
17    Manual,
18}
19
20impl Mapping {
21    /// Return the models.dev provider key this mapping resolves to, or `None`
22    /// for [`Mapping::Manual`].
23    #[must_use]
24    pub fn upstream_key(&self) -> Option<&'static str> {
25        match self {
26            Mapping::Exact => None,
27            Mapping::Aliased(k) => Some(k),
28            Mapping::Manual => None,
29        }
30    }
31}
32
33/// Resolve a catalog provider id to its models.dev counterpart.
34///
35/// Returns [`Mapping::Exact`] when the id matches a models.dev key directly,
36/// [`Mapping::Aliased`] with the upstream key when an alias is known, and
37/// [`Mapping::Manual`] for catalog providers absent from models.dev (local
38/// engines and curated passthroughs such as `native`). Callers should keep
39/// [`Mapping::Manual`] providers untouched during sync.
40///
41/// When adding a provider to `catalog.json`, decide its mapping here so the
42/// table stays in sync — otherwise it silently resolves to [`Mapping::Manual`].
43#[must_use]
44pub fn resolve(catalog_id: &str) -> Mapping {
45    match catalog_id {
46        // Exact matches (catalog id == models.dev provider key).
47        "openai" | "zai" | "minimax" | "minimax-cn" | "deepseek" | "alibaba" | "alibaba-cn"
48        | "lmstudio" => Mapping::Exact,
49        // Aliased matches (catalog id → different models.dev provider key).
50        "gemini" => Mapping::Aliased("google"),
51        "zai-cn" => Mapping::Aliased("zai"),
52        "zai-coding" | "zai-cn-coding" => Mapping::Aliased("zai-coding-plan"),
53        "kimi" => Mapping::Aliased("kimi-for-coding"),
54        "moonshot" => Mapping::Aliased("moonshotai"),
55        "alibaba-us" => Mapping::Aliased("alibaba"),
56        // Everything else (native, ve, mimo, ollama, llamacpp, unknowns) is manual.
57        _ => Mapping::Manual,
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    const EXACT: &[&str] = &[
66        "openai",
67        "zai",
68        "minimax",
69        "minimax-cn",
70        "deepseek",
71        "alibaba",
72        "alibaba-cn",
73        "lmstudio",
74    ];
75    const MANUAL: &[&str] = &["native", "ve", "mimo", "ollama", "llamacpp"];
76
77    #[test]
78    fn test_exact_matches() {
79        for &id in EXACT {
80            assert_eq!(resolve(id), Mapping::Exact, "{id} should be Exact");
81        }
82    }
83
84    #[test]
85    fn test_aliased_matches() {
86        assert_eq!(resolve("gemini"), Mapping::Aliased("google"));
87        assert_eq!(resolve("zai-cn"), Mapping::Aliased("zai"));
88        assert_eq!(resolve("zai-coding"), Mapping::Aliased("zai-coding-plan"));
89        assert_eq!(
90            resolve("zai-cn-coding"),
91            Mapping::Aliased("zai-coding-plan")
92        );
93        assert_eq!(resolve("kimi"), Mapping::Aliased("kimi-for-coding"));
94        assert_eq!(resolve("moonshot"), Mapping::Aliased("moonshotai"));
95        assert_eq!(resolve("alibaba-us"), Mapping::Aliased("alibaba"));
96    }
97
98    #[test]
99    fn test_manual_matches() {
100        for &id in MANUAL {
101            assert_eq!(resolve(id), Mapping::Manual, "{id} should be Manual");
102        }
103        assert_eq!(resolve("totally-unknown"), Mapping::Manual);
104    }
105
106    #[test]
107    fn test_upstream_key_helper() {
108        assert_eq!(resolve("openai").upstream_key(), None); // Exact → use the id itself
109        assert_eq!(resolve("gemini").upstream_key(), Some("google"));
110        assert_eq!(resolve("ollama").upstream_key(), None); // Manual → no upstream
111    }
112
113    #[test]
114    fn test_every_embedded_provider_is_classified() {
115        // Guard against drift: every provider currently in catalog.json must
116        // be explicitly classified as Exact, Aliased, or Manual. If a new
117        // provider is added without a mapping decision, this test surfaces it
118        // by showing an unexpected Manual for a provider that should sync.
119        let ids = crate::provider::ProviderIndex::embedded().ids();
120        let aliased: &[&str] = &[
121            "gemini",
122            "zai-cn",
123            "zai-coding",
124            "zai-cn-coding",
125            "kimi",
126            "moonshot",
127            "alibaba-us",
128        ];
129        for id in &ids {
130            let m = resolve(id);
131            let is_exact = EXACT.contains(&id.as_str());
132            let is_aliased = aliased.contains(&id.as_str());
133            let is_manual = MANUAL.contains(&id.as_str());
134            assert!(
135                match m {
136                    Mapping::Exact => is_exact,
137                    Mapping::Aliased(_) => is_aliased,
138                    Mapping::Manual => is_manual,
139                },
140                "provider {id} resolved to {m:?} but is not in any known bucket — \
141                 add a mapping decision in resolve()"
142            );
143        }
144    }
145}