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 ("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
123pub 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}