claude_codex/providers/codex/translate/
model_allowlist.rs1use 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
115pub 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}