1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum ModelApiSurface {
10 ChatCompletions,
11 Responses,
12 Batch,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum ModelInputModality {
19 Text,
20 Image,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum ModelReasoningEffort {
27 None,
28 Minimal,
29 Low,
30 Medium,
31 High,
32 XHigh,
33 Max,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38#[non_exhaustive]
39pub enum ModelReasoningMode {
40 Standard,
41 Pro,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum ModelReasoningContext {
48 Auto,
49 CurrentTurn,
50 AllTurns,
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum ModelReasoningSummary {
57 Auto,
58 Concise,
59 Detailed,
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64#[non_exhaustive]
65pub enum ModelReasoningStateReplay {
66 PreviousResponseId,
67 ManualOutputItems,
68 EncryptedContent,
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73#[non_exhaustive]
74pub enum ModelPromptCacheMode {
75 Implicit,
76 Explicit,
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum ModelPromptCacheTtl {
83 ThirtyMinutes,
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88#[non_exhaustive]
89pub enum ModelToolChoice {
90 None,
91 Auto,
92 Required,
93 ForcedFunction,
94 AllowedTools,
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99#[non_exhaustive]
100pub struct ModelFeatureSet<T: 'static> {
101 pub values: &'static [T],
102 pub api_surfaces: &'static [ModelApiSurface],
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107#[non_exhaustive]
108pub struct ModelReasoningFeatures {
109 pub efforts: ModelFeatureSet<ModelReasoningEffort>,
110 pub modes: ModelFeatureSet<ModelReasoningMode>,
111 pub contexts: ModelFeatureSet<ModelReasoningContext>,
112 pub summaries: ModelFeatureSet<ModelReasoningSummary>,
113 pub state_replay: ModelFeatureSet<ModelReasoningStateReplay>,
114}
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118#[non_exhaustive]
119pub struct ModelPromptCacheFeatures {
120 pub automatic: &'static [ModelApiSurface],
121 pub prompt_cache_key: &'static [ModelApiSurface],
122 pub modes: ModelFeatureSet<ModelPromptCacheMode>,
123 pub ttls: ModelFeatureSet<ModelPromptCacheTtl>,
124 pub explicit_breakpoints: &'static [ModelApiSurface],
125 pub max_write_breakpoints: Option<u8>,
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
130#[non_exhaustive]
131pub struct ModelToolFeatures {
132 pub choices: ModelFeatureSet<ModelToolChoice>,
133 pub parallel_function_calls: &'static [ModelApiSurface],
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138#[non_exhaustive]
139pub struct ModelFeatures {
140 pub model_id: &'static str,
142 pub alias_of: Option<&'static str>,
144 pub context_window: u32,
146 pub max_input_tokens: u32,
148 pub max_output_tokens: u32,
150 pub api_surfaces: &'static [ModelApiSurface],
151 pub input_modalities: &'static [ModelInputModality],
152 pub reasoning: ModelReasoningFeatures,
153 pub prompt_cache: ModelPromptCacheFeatures,
154 pub tools: ModelToolFeatures,
155 pub source_urls: &'static [&'static str],
156}
157
158const CHAT_AND_RESPONSES: &[ModelApiSurface] =
159 &[ModelApiSurface::ChatCompletions, ModelApiSurface::Responses];
160const RESPONSES: &[ModelApiSurface] = &[ModelApiSurface::Responses];
161const ALL_SURFACES: &[ModelApiSurface] = &[
162 ModelApiSurface::ChatCompletions,
163 ModelApiSurface::Responses,
164 ModelApiSurface::Batch,
165];
166const TEXT_AND_IMAGE: &[ModelInputModality] =
167 &[ModelInputModality::Text, ModelInputModality::Image];
168
169const GPT56_EFFORTS: &[ModelReasoningEffort] = &[
170 ModelReasoningEffort::None,
171 ModelReasoningEffort::Low,
172 ModelReasoningEffort::Medium,
173 ModelReasoningEffort::High,
174 ModelReasoningEffort::XHigh,
175 ModelReasoningEffort::Max,
176];
177const GPT53_CODEX_EFFORTS: &[ModelReasoningEffort] = &[
178 ModelReasoningEffort::Low,
179 ModelReasoningEffort::Medium,
180 ModelReasoningEffort::High,
181 ModelReasoningEffort::XHigh,
182];
183const GPT52_PRO_EFFORTS: &[ModelReasoningEffort] = &[
184 ModelReasoningEffort::Medium,
185 ModelReasoningEffort::High,
186 ModelReasoningEffort::XHigh,
187];
188const GPT56_MODES: &[ModelReasoningMode] = &[ModelReasoningMode::Standard, ModelReasoningMode::Pro];
189const GPT56_CONTEXTS: &[ModelReasoningContext] = &[
190 ModelReasoningContext::Auto,
191 ModelReasoningContext::CurrentTurn,
192 ModelReasoningContext::AllTurns,
193];
194const AUTO_SUMMARY: &[ModelReasoningSummary] = &[ModelReasoningSummary::Auto];
195const REASONING_STATE_REPLAY: &[ModelReasoningStateReplay] = &[
196 ModelReasoningStateReplay::PreviousResponseId,
197 ModelReasoningStateReplay::ManualOutputItems,
198 ModelReasoningStateReplay::EncryptedContent,
199];
200const CACHE_MODES: &[ModelPromptCacheMode] = &[
201 ModelPromptCacheMode::Implicit,
202 ModelPromptCacheMode::Explicit,
203];
204const CACHE_TTLS: &[ModelPromptCacheTtl] = &[ModelPromptCacheTtl::ThirtyMinutes];
205const TOOL_CHOICES: &[ModelToolChoice] = &[
206 ModelToolChoice::None,
207 ModelToolChoice::Auto,
208 ModelToolChoice::Required,
209 ModelToolChoice::ForcedFunction,
210 ModelToolChoice::AllowedTools,
211];
212
213const GPT56_REASONING: ModelReasoningFeatures = ModelReasoningFeatures {
214 efforts: ModelFeatureSet {
215 values: GPT56_EFFORTS,
216 api_surfaces: CHAT_AND_RESPONSES,
217 },
218 modes: ModelFeatureSet {
219 values: GPT56_MODES,
220 api_surfaces: RESPONSES,
221 },
222 contexts: ModelFeatureSet {
223 values: GPT56_CONTEXTS,
224 api_surfaces: RESPONSES,
225 },
226 summaries: ModelFeatureSet {
227 values: AUTO_SUMMARY,
228 api_surfaces: RESPONSES,
229 },
230 state_replay: ModelFeatureSet {
231 values: REASONING_STATE_REPLAY,
232 api_surfaces: RESPONSES,
233 },
234};
235
236const GPT53_CODEX_REASONING: ModelReasoningFeatures = ModelReasoningFeatures {
237 efforts: ModelFeatureSet {
238 values: GPT53_CODEX_EFFORTS,
239 api_surfaces: RESPONSES,
240 },
241 modes: ModelFeatureSet {
242 values: &[],
243 api_surfaces: &[],
244 },
245 contexts: ModelFeatureSet {
246 values: &[],
247 api_surfaces: &[],
248 },
249 summaries: ModelFeatureSet {
250 values: &[],
251 api_surfaces: &[],
252 },
253 state_replay: ModelFeatureSet {
254 values: REASONING_STATE_REPLAY,
255 api_surfaces: RESPONSES,
256 },
257};
258
259const GPT52_PRO_REASONING: ModelReasoningFeatures = ModelReasoningFeatures {
260 efforts: ModelFeatureSet {
261 values: GPT52_PRO_EFFORTS,
262 api_surfaces: RESPONSES,
263 },
264 modes: ModelFeatureSet {
265 values: &[],
266 api_surfaces: &[],
267 },
268 contexts: ModelFeatureSet {
269 values: &[],
270 api_surfaces: &[],
271 },
272 summaries: ModelFeatureSet {
273 values: &[],
274 api_surfaces: &[],
275 },
276 state_replay: ModelFeatureSet {
277 values: REASONING_STATE_REPLAY,
278 api_surfaces: RESPONSES,
279 },
280};
281
282const GPT56_CACHE: ModelPromptCacheFeatures = ModelPromptCacheFeatures {
283 automatic: CHAT_AND_RESPONSES,
284 prompt_cache_key: CHAT_AND_RESPONSES,
285 modes: ModelFeatureSet {
286 values: CACHE_MODES,
287 api_surfaces: CHAT_AND_RESPONSES,
288 },
289 ttls: ModelFeatureSet {
290 values: CACHE_TTLS,
291 api_surfaces: CHAT_AND_RESPONSES,
292 },
293 explicit_breakpoints: CHAT_AND_RESPONSES,
294 max_write_breakpoints: Some(4),
295};
296
297const LEGACY_AUTOMATIC_CACHE: ModelPromptCacheFeatures = ModelPromptCacheFeatures {
298 automatic: RESPONSES,
299 prompt_cache_key: RESPONSES,
300 modes: ModelFeatureSet {
301 values: &[],
302 api_surfaces: &[],
303 },
304 ttls: ModelFeatureSet {
305 values: &[],
306 api_surfaces: &[],
307 },
308 explicit_breakpoints: &[],
309 max_write_breakpoints: None,
310};
311
312const FUNCTION_TOOLS: ModelToolFeatures = ModelToolFeatures {
313 choices: ModelFeatureSet {
314 values: TOOL_CHOICES,
315 api_surfaces: CHAT_AND_RESPONSES,
316 },
317 parallel_function_calls: CHAT_AND_RESPONSES,
318};
319
320const RESPONSES_FUNCTION_TOOLS: ModelToolFeatures = ModelToolFeatures {
321 choices: ModelFeatureSet {
322 values: TOOL_CHOICES,
323 api_surfaces: RESPONSES,
324 },
325 parallel_function_calls: RESPONSES,
326};
327
328const REASONING_URL: &str = "https://developers.openai.com/api/docs/guides/reasoning";
329const CACHE_URL: &str = "https://developers.openai.com/api/docs/guides/prompt-caching";
330const FUNCTION_CALLING_URL: &str = "https://developers.openai.com/api/docs/guides/function-calling";
331const GPT56_GUIDE_URL: &str = "https://developers.openai.com/api/docs/guides/latest-model";
332const GPT56_SOL_URL: &str = "https://developers.openai.com/api/docs/models/gpt-5.6-sol";
333const GPT56_TERRA_URL: &str = "https://developers.openai.com/api/docs/models/gpt-5.6-terra";
334const GPT56_LUNA_URL: &str = "https://developers.openai.com/api/docs/models/gpt-5.6-luna";
335const GPT53_CODEX_URL: &str = "https://developers.openai.com/api/docs/models/gpt-5.3-codex";
336const GPT52_PRO_URL: &str = "https://developers.openai.com/api/docs/models/gpt-5.2-pro";
337
338const GPT56_SOURCES: &[&str] = &[
339 GPT56_SOL_URL,
340 GPT56_GUIDE_URL,
341 REASONING_URL,
342 CACHE_URL,
343 FUNCTION_CALLING_URL,
344];
345const GPT56_SOL_SOURCES: &[&str] = GPT56_SOURCES;
346const GPT56_TERRA_SOURCES: &[&str] = &[
347 GPT56_TERRA_URL,
348 GPT56_GUIDE_URL,
349 REASONING_URL,
350 CACHE_URL,
351 FUNCTION_CALLING_URL,
352];
353const GPT56_LUNA_SOURCES: &[&str] = &[
354 GPT56_LUNA_URL,
355 GPT56_GUIDE_URL,
356 REASONING_URL,
357 CACHE_URL,
358 FUNCTION_CALLING_URL,
359];
360const GPT53_CODEX_SOURCES: &[&str] = &[
361 GPT53_CODEX_URL,
362 REASONING_URL,
363 CACHE_URL,
364 FUNCTION_CALLING_URL,
365];
366const GPT52_PRO_SOURCES: &[&str] = &[
367 GPT52_PRO_URL,
368 REASONING_URL,
369 CACHE_URL,
370 FUNCTION_CALLING_URL,
371];
372
373const MODEL_FEATURES: &[ModelFeatures] = &[
374 ModelFeatures {
375 model_id: "gpt-5.6",
376 alias_of: Some("gpt-5.6-sol"),
377 context_window: 1_050_000,
378 max_input_tokens: 922_000,
379 max_output_tokens: 128_000,
380 api_surfaces: ALL_SURFACES,
381 input_modalities: TEXT_AND_IMAGE,
382 reasoning: GPT56_REASONING,
383 prompt_cache: GPT56_CACHE,
384 tools: FUNCTION_TOOLS,
385 source_urls: GPT56_SOURCES,
386 },
387 ModelFeatures {
388 model_id: "gpt-5.6-sol",
389 alias_of: None,
390 context_window: 1_050_000,
391 max_input_tokens: 922_000,
392 max_output_tokens: 128_000,
393 api_surfaces: ALL_SURFACES,
394 input_modalities: TEXT_AND_IMAGE,
395 reasoning: GPT56_REASONING,
396 prompt_cache: GPT56_CACHE,
397 tools: FUNCTION_TOOLS,
398 source_urls: GPT56_SOL_SOURCES,
399 },
400 ModelFeatures {
401 model_id: "gpt-5.6-terra",
402 alias_of: None,
403 context_window: 1_050_000,
404 max_input_tokens: 922_000,
405 max_output_tokens: 128_000,
406 api_surfaces: ALL_SURFACES,
407 input_modalities: TEXT_AND_IMAGE,
408 reasoning: GPT56_REASONING,
409 prompt_cache: GPT56_CACHE,
410 tools: FUNCTION_TOOLS,
411 source_urls: GPT56_TERRA_SOURCES,
412 },
413 ModelFeatures {
414 model_id: "gpt-5.6-luna",
415 alias_of: None,
416 context_window: 1_050_000,
417 max_input_tokens: 922_000,
418 max_output_tokens: 128_000,
419 api_surfaces: ALL_SURFACES,
420 input_modalities: TEXT_AND_IMAGE,
421 reasoning: GPT56_REASONING,
422 prompt_cache: GPT56_CACHE,
423 tools: FUNCTION_TOOLS,
424 source_urls: GPT56_LUNA_SOURCES,
425 },
426 ModelFeatures {
427 model_id: "gpt-5.3-codex",
428 alias_of: None,
429 context_window: 400_000,
430 max_input_tokens: 272_000,
431 max_output_tokens: 128_000,
432 api_surfaces: RESPONSES,
433 input_modalities: TEXT_AND_IMAGE,
434 reasoning: GPT53_CODEX_REASONING,
435 prompt_cache: LEGACY_AUTOMATIC_CACHE,
436 tools: RESPONSES_FUNCTION_TOOLS,
437 source_urls: GPT53_CODEX_SOURCES,
438 },
439 ModelFeatures {
440 model_id: "gpt-5.2-pro",
441 alias_of: None,
442 context_window: 400_000,
443 max_input_tokens: 272_000,
444 max_output_tokens: 128_000,
445 api_surfaces: RESPONSES,
446 input_modalities: TEXT_AND_IMAGE,
447 reasoning: GPT52_PRO_REASONING,
448 prompt_cache: LEGACY_AUTOMATIC_CACHE,
449 tools: RESPONSES_FUNCTION_TOOLS,
450 source_urls: GPT52_PRO_SOURCES,
451 },
452];
453
454#[must_use]
456pub fn get_model_features(model_id: &str) -> Option<&'static ModelFeatures> {
457 MODEL_FEATURES
458 .iter()
459 .find(|features| features.model_id == model_id)
460}
461
462#[must_use]
464pub const fn supported_model_features() -> &'static [ModelFeatures] {
465 MODEL_FEATURES
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471 use anyhow::Context;
472
473 #[test]
474 fn registry_contains_every_official_row() -> anyhow::Result<()> {
475 for model_id in [
476 "gpt-5.6",
477 "gpt-5.6-sol",
478 "gpt-5.6-terra",
479 "gpt-5.6-luna",
480 "gpt-5.3-codex",
481 "gpt-5.2-pro",
482 ] {
483 get_model_features(model_id)
484 .with_context(|| format!("missing model feature row for {model_id}"))?;
485 }
486 assert!(get_model_features("unknown-model").is_none());
487 Ok(())
488 }
489
490 #[test]
491 fn gpt56_alias_preserves_exact_surface_and_reasoning_features() -> anyhow::Result<()> {
492 let features = get_model_features("gpt-5.6").context("missing gpt-5.6 features")?;
493
494 assert_eq!(features.alias_of, Some("gpt-5.6-sol"));
495 assert_eq!(features.context_window, 1_050_000);
496 assert_eq!(features.max_input_tokens, 922_000);
497 assert_eq!(features.max_output_tokens, 128_000);
498 assert_eq!(features.api_surfaces, ALL_SURFACES);
499 assert_eq!(features.input_modalities, TEXT_AND_IMAGE);
500 assert_eq!(features.reasoning.efforts.values, GPT56_EFFORTS);
501 assert_eq!(features.reasoning.modes.values, GPT56_MODES);
502 assert_eq!(features.reasoning.contexts.values, GPT56_CONTEXTS);
503 assert_eq!(features.reasoning.summaries.values, AUTO_SUMMARY);
504 assert_eq!(
505 features.reasoning.state_replay.values,
506 REASONING_STATE_REPLAY
507 );
508 Ok(())
509 }
510
511 #[test]
512 fn gpt56_exposes_explicit_cache_and_parallel_tool_controls() -> anyhow::Result<()> {
513 let features =
514 get_model_features("gpt-5.6-terra").context("missing gpt-5.6-terra features")?;
515
516 assert_eq!(features.prompt_cache.modes.values, CACHE_MODES);
517 assert_eq!(features.prompt_cache.ttls.values, CACHE_TTLS);
518 assert_eq!(features.prompt_cache.max_write_breakpoints, Some(4));
519 assert_eq!(features.tools.choices.values, TOOL_CHOICES);
520 assert_eq!(features.tools.parallel_function_calls, CHAT_AND_RESPONSES);
521 Ok(())
522 }
523
524 #[test]
525 fn gpt53_codex_keeps_legacy_boundaries() -> anyhow::Result<()> {
526 let features =
527 get_model_features("gpt-5.3-codex").context("missing gpt-5.3-codex features")?;
528
529 assert_eq!(features.reasoning.efforts.values, GPT53_CODEX_EFFORTS);
530 assert_eq!(features.api_surfaces, RESPONSES);
531 assert_eq!(features.context_window, 400_000);
532 assert_eq!(features.max_input_tokens, 272_000);
533 assert_eq!(features.max_output_tokens, 128_000);
534 assert_eq!(features.reasoning.efforts.api_surfaces, RESPONSES);
535 assert!(features.reasoning.modes.values.is_empty());
536 assert!(features.reasoning.contexts.values.is_empty());
537 assert!(features.reasoning.summaries.values.is_empty());
538 assert!(features.prompt_cache.modes.values.is_empty());
539 assert!(features.prompt_cache.explicit_breakpoints.is_empty());
540 assert_eq!(
541 features.reasoning.state_replay.values,
542 REASONING_STATE_REPLAY
543 );
544 Ok(())
545 }
546
547 #[test]
548 fn gpt52_pro_is_responses_only_with_verified_efforts() -> anyhow::Result<()> {
549 let features = get_model_features("gpt-5.2-pro").context("missing gpt-5.2-pro features")?;
550
551 assert_eq!(features.api_surfaces, RESPONSES);
552 assert_eq!(features.reasoning.efforts.values, GPT52_PRO_EFFORTS);
553 assert_eq!(features.reasoning.efforts.api_surfaces, RESPONSES);
554 assert_eq!(features.context_window, 400_000);
555 assert_eq!(features.max_output_tokens, 128_000);
556 Ok(())
557 }
558
559 #[test]
560 fn model_ids_are_unique() {
561 for (index, features) in supported_model_features().iter().enumerate() {
562 assert!(
563 supported_model_features()[index + 1..]
564 .iter()
565 .all(|candidate| candidate.model_id != features.model_id)
566 );
567 }
568 }
569}