Skip to main content

agent_sdk_providers/
model_features.rs

1//! Static feature metadata for `OpenAI` models.
2//!
3//! This registry complements [`crate::model_capabilities`] with API-shape
4//! information used to validate requests and choose an inference surface.
5
6/// `OpenAI` API surface on which a model can be used.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum ModelApiSurface {
10    ChatCompletions,
11    Responses,
12    Batch,
13}
14
15/// Input modality accepted by a model.
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17#[non_exhaustive]
18pub enum ModelInputModality {
19    Text,
20    Image,
21}
22
23/// Exact `OpenAI` `reasoning.effort` value.
24#[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/// Exact `OpenAI` `reasoning.mode` value.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38#[non_exhaustive]
39pub enum ModelReasoningMode {
40    Standard,
41    Pro,
42}
43
44/// Exact `OpenAI` `reasoning.context` value.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum ModelReasoningContext {
48    Auto,
49    CurrentTurn,
50    AllTurns,
51}
52
53/// Exact `OpenAI` `reasoning.summary` value.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum ModelReasoningSummary {
57    Auto,
58    Concise,
59    Detailed,
60}
61
62/// Mechanism available for continuing opaque reasoning state.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64#[non_exhaustive]
65pub enum ModelReasoningStateReplay {
66    PreviousResponseId,
67    ManualOutputItems,
68    EncryptedContent,
69}
70
71/// Exact GPT-5.6 `prompt_cache_options.mode` value.
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73#[non_exhaustive]
74pub enum ModelPromptCacheMode {
75    Implicit,
76    Explicit,
77}
78
79/// Exact GPT-5.6 `prompt_cache_options.ttl` value.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum ModelPromptCacheTtl {
83    ThirtyMinutes,
84}
85
86/// Tool-selection policy accepted by `OpenAI` function calling.
87#[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/// Values supported by a feature and the request surfaces that accept them.
98#[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/// Reasoning controls supported by a model.
106#[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/// Prompt-cache controls supported by a model.
117#[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/// Function-tool controls supported by a model.
129#[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/// API feature profile for one exact `OpenAI` model ID or alias.
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138#[non_exhaustive]
139pub struct ModelFeatures {
140    /// Exact request model ID.
141    pub model_id: &'static str,
142    /// Canonical model selected by this alias, when applicable.
143    pub alias_of: Option<&'static str>,
144    /// Total input-plus-output context capacity.
145    pub context_window: u32,
146    /// Maximum input tokens accepted in one request.
147    pub max_input_tokens: u32,
148    /// Maximum output-token budget accepted in one request.
149    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/// Return the static feature profile for an exact model ID or alias.
455#[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/// Return all model feature profiles bundled with this SDK release.
463#[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}