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