Skip to main content

claude_codex/providers/codex/translate/
model_allowlist.rs

1use std::collections::HashSet;
2
3use crate::config;
4
5use super::request::ServiceTier;
6
7pub const ALLOWED_MODELS: &[&str] = &[
8    "gpt-5.2",
9    "gpt-5.3-codex",
10    "gpt-5.3-codex-spark",
11    "gpt-5.4",
12    "gpt-5.4-mini",
13    "gpt-5.5",
14    "gpt-5.6-luna",
15    "gpt-5.6-sol",
16    "gpt-5.6-terra",
17];
18
19pub const MODEL_ALIASES: &[(&str, &str)] = &[
20    ("haiku", "gpt-5.6-luna"),
21    ("claude-haiku-4-5", "gpt-5.6-luna"),
22    ("claude-haiku-4-5-20251001", "gpt-5.6-luna"),
23    ("sonnet", "gpt-5.6-terra"),
24    ("claude-sonnet-4-6", "gpt-5.6-terra"),
25    ("claude-sonnet-5", "gpt-5.6-terra"),
26    ("opus", "gpt-5.6-sol"),
27    ("claude-opus-4-7", "gpt-5.6-sol"),
28    ("claude-opus-4-8", "gpt-5.6-sol"),
29    ("fable", "gpt-5.6-sol"),
30    ("claude-fable-5", "gpt-5.6-sol"),
31];
32
33#[derive(Debug, Clone)]
34pub struct ResolvedModel {
35    pub model: String,
36    pub service_tier: Option<ServiceTier>,
37}
38
39fn fast_model_aliases() -> HashSet<String> {
40    ALLOWED_MODELS.iter().map(|m| format!("{m}-fast")).collect()
41}
42
43fn resolve_fast_model_alias(model: &str) -> ResolvedModel {
44    let fast_set = fast_model_aliases();
45    if fast_set.contains(model) {
46        let base = model.trim_end_matches("-fast");
47        ResolvedModel {
48            model: base.to_string(),
49            service_tier: Some(ServiceTier::Priority),
50        }
51    } else {
52        ResolvedModel {
53            model: model.to_string(),
54            service_tier: None,
55        }
56    }
57}
58
59pub fn resolve_model_request(model: &str) -> ResolvedModel {
60    let alias = MODEL_ALIASES
61        .iter()
62        .find(|(alias, _)| *alias == model)
63        .map(|(_, target)| *target)
64        .unwrap_or(model);
65
66    let requested = resolve_fast_model_alias(alias);
67
68    let override_model = config::codex_model();
69    let resolved = match override_model {
70        Some(ref val) if !val.is_empty() => resolve_fast_model_alias(val),
71        _ => requested.clone(),
72    };
73
74    ResolvedModel {
75        model: resolved.model,
76        service_tier: if requested.service_tier == Some(ServiceTier::Priority)
77            || resolved.service_tier == Some(ServiceTier::Priority)
78        {
79            Some(ServiceTier::Priority)
80        } else {
81            resolved.service_tier
82        },
83    }
84}
85
86pub fn resolve_model(model: &str) -> String {
87    resolve_model_request(model).model
88}
89
90#[derive(Debug, Clone)]
91pub struct ModelNotAllowedError {
92    pub model: String,
93}
94
95impl std::fmt::Display for ModelNotAllowedError {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        write!(f, "Model not allowed: {}", self.model)
98    }
99}
100
101pub fn assert_allowed_model(model: &str) -> Result<(), ModelNotAllowedError> {
102    if ALLOWED_MODELS.contains(&model) {
103        Ok(())
104    } else {
105        Err(ModelNotAllowedError {
106            model: model.to_string(),
107        })
108    }
109}
110
111pub fn uses_responses_lite(model: &str) -> bool {
112    matches!(model, "gpt-5.6-luna" | "gpt-5.6-sol" | "gpt-5.6-terra")
113}
114
115/// `gpt-5.6-luna` exists only behind the Responses Lite lane; the full
116/// Responses API resolves it to a `-free` variant and returns 404 (Model not
117/// found gpt-5.6-luna-free-...). Hosted web_search requests must run on the
118/// full lane, so luna is upgraded to its nearest full-lane sibling.
119pub fn full_lane_web_search_model(model: &str) -> &str {
120    if model == "gpt-5.6-luna" {
121        "gpt-5.6-sol"
122    } else {
123        model
124    }
125}
126
127pub fn is_valid_model_for_codex(model: &str) -> bool {
128    if ALLOWED_MODELS.contains(&model) {
129        return true;
130    }
131    let fast_set = fast_model_aliases();
132    if fast_set.contains(model) {
133        return true;
134    }
135    MODEL_ALIASES.iter().any(|(alias, _)| *alias == model)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn haiku_resolves_to_luna() {
144        let r = resolve_model_request("haiku");
145        assert_eq!(r.model, "gpt-5.6-luna");
146    }
147
148    #[test]
149    fn web_search_upgrades_luna_to_full_lane_sibling() {
150        assert_eq!(full_lane_web_search_model("gpt-5.6-luna"), "gpt-5.6-sol");
151        assert_eq!(full_lane_web_search_model("gpt-5.6-sol"), "gpt-5.6-sol");
152        assert_eq!(full_lane_web_search_model("gpt-5.6-terra"), "gpt-5.6-terra");
153        assert_eq!(full_lane_web_search_model("gpt-5.4"), "gpt-5.4");
154    }
155
156    #[test]
157    fn sonnet_resolves_to_terra() {
158        let r = resolve_model_request("sonnet");
159        assert_eq!(r.model, "gpt-5.6-terra");
160    }
161
162    #[test]
163    fn sonnet_5_resolves_to_terra() {
164        let r = resolve_model_request("claude-sonnet-5");
165        assert_eq!(r.model, "gpt-5.6-terra");
166    }
167
168    #[test]
169    fn opus_resolves_to_sol() {
170        let r = resolve_model_request("opus");
171        assert_eq!(r.model, "gpt-5.6-sol");
172    }
173
174    #[test]
175    fn opus_4_8_resolves_to_sol() {
176        let r = resolve_model_request("claude-opus-4-8");
177        assert_eq!(r.model, "gpt-5.6-sol");
178    }
179
180    #[test]
181    fn fable_5_resolves_to_sol() {
182        for model in ["fable", "claude-fable-5"] {
183            let r = resolve_model_request(model);
184            assert_eq!(r.model, "gpt-5.6-sol");
185        }
186    }
187
188    #[test]
189    fn fast_suffix_adds_priority() {
190        let r = resolve_model_request("gpt-5.6-sol-fast");
191        assert_eq!(r.model, "gpt-5.6-sol");
192        assert_eq!(r.service_tier, Some(ServiceTier::Priority));
193    }
194
195    #[test]
196    fn allowed_models_accept_base() {
197        assert!(assert_allowed_model("gpt-5.4").is_ok());
198        assert!(assert_allowed_model("gpt-5.6-sol").is_ok());
199        assert!(assert_allowed_model("gpt-5.6-terra").is_ok());
200        assert!(assert_allowed_model("gpt-5.6-luna").is_ok());
201    }
202
203    #[test]
204    fn not_allowed_rejected() {
205        assert!(assert_allowed_model("gpt-7").is_err());
206    }
207}