Skip to main content

openai_tools/common/
models.rs

1//! OpenAI Model Types
2//!
3//! This module provides strongly-typed enums for specifying OpenAI models
4//! across different APIs. Using enums instead of strings provides:
5//!
6//! - Compile-time validation of model names
7//! - IDE autocompletion support
8//! - Prevention of typos in model names
9//! - Clear documentation of available models
10//!
11//! # Model Categories
12//!
13//! - [`ChatModel`]: Models for Chat Completions and Responses APIs
14//! - [`EmbeddingModel`]: Models for text embeddings
15//! - [`RealtimeModel`]: Models for real-time audio/text interactions
16//! - [`FineTuningModel`]: Base models that can be fine-tuned
17//!
18//! # Example
19//!
20//! ```rust,no_run
21//! use openai_tools::common::models::{ChatModel, EmbeddingModel};
22//! use openai_tools::chat::request::ChatCompletion;
23//! use openai_tools::embedding::request::Embedding;
24//!
25//! # #[tokio::main]
26//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
27//! // Using ChatModel enum
28//! let mut chat = ChatCompletion::new();
29//! chat.model(ChatModel::Gpt4oMini);
30//!
31//! // Using EmbeddingModel enum
32//! let mut embedding = Embedding::new()?;
33//! embedding.model(EmbeddingModel::TextEmbedding3Small);
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! # References
39//!
40//! - [OpenAI Models Documentation](https://platform.openai.com/docs/models)
41//! - [Model Deprecations](https://platform.openai.com/docs/deprecations)
42
43use serde::{Deserialize, Serialize};
44
45// ============================================================================
46// Parameter Restriction Types
47// ============================================================================
48
49/// Defines how a parameter is restricted for a model.
50///
51/// This enum is used to specify whether a parameter can accept any value,
52/// only a fixed value, or is not supported at all.
53#[derive(Debug, Clone, PartialEq)]
54pub enum ParameterRestriction {
55    /// Parameter accepts any value within its valid range
56    Any,
57    /// Parameter only supports a specific fixed value
58    FixedValue(f64),
59    /// Parameter is not supported by this model
60    NotSupported,
61}
62
63/// Parameter support information for a model.
64///
65/// This struct provides detailed information about which parameters are
66/// supported by a model and any restrictions that apply.
67///
68/// # Example
69///
70/// ```rust
71/// use openai_tools::common::models::{ChatModel, ParameterRestriction};
72///
73/// let model = ChatModel::O3Mini;
74/// let support = model.parameter_support();
75///
76/// // Reasoning models only support temperature = 1.0
77/// assert_eq!(support.temperature, ParameterRestriction::FixedValue(1.0));
78///
79/// // Reasoning models don't support logprobs
80/// assert!(!support.logprobs);
81/// ```
82#[derive(Debug, Clone)]
83pub struct ParameterSupport {
84    /// Temperature parameter restriction (Chat & Responses API)
85    pub temperature: ParameterRestriction,
86    /// Frequency penalty parameter restriction (Chat API only)
87    pub frequency_penalty: ParameterRestriction,
88    /// Presence penalty parameter restriction (Chat API only)
89    pub presence_penalty: ParameterRestriction,
90    /// Whether logprobs parameter is supported (Chat API only)
91    pub logprobs: bool,
92    /// Whether top_logprobs parameter is supported (Chat & Responses API)
93    pub top_logprobs: bool,
94    /// Whether logit_bias parameter is supported (Chat API only)
95    pub logit_bias: bool,
96    /// Whether n > 1 (multiple completions) is supported (Chat API only)
97    pub n_multiple: bool,
98    /// Top P parameter restriction (Responses API only)
99    pub top_p: ParameterRestriction,
100    /// Whether reasoning parameter is supported (Responses API only, reasoning models)
101    pub reasoning: bool,
102}
103
104impl ParameterSupport {
105    /// Creates parameter support info for standard (non-reasoning) models.
106    ///
107    /// Standard models support all parameters with full range.
108    pub fn standard_model() -> Self {
109        Self {
110            temperature: ParameterRestriction::Any,
111            frequency_penalty: ParameterRestriction::Any,
112            presence_penalty: ParameterRestriction::Any,
113            logprobs: true,
114            top_logprobs: true,
115            logit_bias: true,
116            n_multiple: true,
117            top_p: ParameterRestriction::Any,
118            reasoning: false,
119        }
120    }
121
122    /// Creates parameter support info for reasoning models (GPT-5, o-series).
123    ///
124    /// Reasoning models have restricted parameter support:
125    /// - temperature: only 1.0
126    /// - top_p: only 1.0
127    /// - frequency_penalty: only 0
128    /// - presence_penalty: only 0
129    /// - logprobs, top_logprobs, logit_bias: not supported
130    /// - n: only 1
131    /// - reasoning: supported
132    pub fn reasoning_model() -> Self {
133        Self {
134            temperature: ParameterRestriction::FixedValue(1.0),
135            frequency_penalty: ParameterRestriction::FixedValue(0.0),
136            presence_penalty: ParameterRestriction::FixedValue(0.0),
137            logprobs: false,
138            top_logprobs: false,
139            logit_bias: false,
140            n_multiple: false,
141            top_p: ParameterRestriction::FixedValue(1.0),
142            reasoning: true,
143        }
144    }
145
146    /// Creates parameter support info for web-search models
147    /// (`gpt-5-search-api`, `gpt-4o-search-preview`, ...).
148    ///
149    /// These reject the whole sampling parameter set the same way reasoning
150    /// models do, but they expose no `reasoning` parameter - the API rejects
151    /// it with "Model incompatible request argument supplied".
152    pub fn search_model() -> Self {
153        Self { reasoning: false, ..Self::reasoning_model() }
154    }
155}
156
157/// Models available for Chat Completions and Responses APIs.
158///
159/// This enum covers all models that can be used with the Chat Completions API
160/// (`/v1/chat/completions`) and the Responses API (`/v1/responses`).
161///
162/// # Model Categories
163///
164/// ## GPT-5 Series (Latest Flagship)
165/// - [`Gpt5_2`]: GPT-5.2 Thinking - flagship model for coding and agentic tasks
166/// - [`Gpt5_2ChatLatest`]: GPT-5.2 Instant - fast workhorse for everyday work
167/// - [`Gpt5_2Pro`]: GPT-5.2 Pro - smartest for difficult questions (Responses API only)
168/// - [`Gpt5_1`]: GPT-5.1 - configurable reasoning and non-reasoning
169/// - [`Gpt5_1CodexMax`]: GPT-5.1 Codex Max - powers Codex CLI
170/// - [`Gpt5Mini`]: GPT-5 Mini - smaller, faster variant
171///
172/// ## GPT-4.1 Series
173/// - [`Gpt4_1`]: 1M context window flagship
174/// - [`Gpt4_1Mini`]: Balanced performance and cost
175/// - [`Gpt4_1Nano`]: Fastest and most cost-efficient
176///
177/// ## GPT-4o Series
178/// - [`Gpt4o`]: High-intelligence flagship model
179/// - [`Gpt4oMini`]: Cost-effective GPT-4o variant
180/// - [`Gpt4oAudioPreview`]: Audio-capable GPT-4o
181///
182/// ## Reasoning Models (o-series)
183/// - [`O1`], [`O1Pro`]: Full reasoning models
184/// - [`O3`], [`O3Mini`]: Latest reasoning models
185/// - [`O4Mini`]: Fast, cost-efficient reasoning
186///
187/// # Reasoning Model Restrictions
188///
189/// Reasoning models (GPT-5 series, o1, o3, o4 series) have parameter restrictions:
190/// - `temperature`: Only 1.0 supported
191/// - `top_p`: Only 1.0 supported
192/// - `frequency_penalty`: Only 0 supported
193/// - `presence_penalty`: Only 0 supported
194///
195/// GPT-5 models support `reasoning.effort` parameter:
196/// - `none`: No reasoning (GPT-5.1 default)
197/// - `minimal`: Very few reasoning tokens
198/// - `low`, `medium`, `high`: Increasing reasoning depth
199/// - `xhigh`: Maximum reasoning (GPT-5.2 Pro, GPT-5.1 Codex Max)
200///
201/// # Example
202///
203/// ```rust
204/// use openai_tools::common::models::ChatModel;
205///
206/// // Check if a model is a reasoning model
207/// let model = ChatModel::O3Mini;
208/// assert!(model.is_reasoning_model());
209///
210/// // GPT-5 models are also reasoning models
211/// let gpt5 = ChatModel::Gpt5_2;
212/// assert!(gpt5.is_reasoning_model());
213///
214/// // Get the API model ID string
215/// assert_eq!(model.as_str(), "o3-mini");
216/// ```
217#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
218pub enum ChatModel {
219    // === GPT-5.6 Series (Latest Flagship) ===
220    /// GPT-5.6 - Alias routing to GPT-5.6 Sol
221    #[serde(rename = "gpt-5.6")]
222    Gpt5_6,
223
224    /// GPT-5.6 Sol - Frontier model for complex professional work
225    ///
226    /// - Context: 1.05M tokens, 128K max output
227    /// - Knowledge cutoff: 2026-02-16
228    /// - Supports: reasoning.effort
229    #[serde(rename = "gpt-5.6-sol")]
230    Gpt5_6Sol,
231
232    /// GPT-5.6 Terra - Balanced model for everyday work
233    ///
234    /// - Context: 1.05M tokens, 128K max output
235    /// - Supports: reasoning.effort
236    #[serde(rename = "gpt-5.6-terra")]
237    Gpt5_6Terra,
238
239    /// GPT-5.6 Luna - Most cost-efficient GPT-5.6 variant
240    ///
241    /// - Context: 1.05M tokens, 128K max output
242    /// - Supports: reasoning.effort
243    #[serde(rename = "gpt-5.6-luna")]
244    Gpt5_6Luna,
245
246    // === GPT-5.5 Series ===
247    /// GPT-5.5 - Frontier model with 1M context
248    ///
249    /// - Context: 1.05M tokens, 128K max output
250    /// - Supports: reasoning.effort (none, low, medium (default), high, xhigh)
251    #[serde(rename = "gpt-5.5")]
252    Gpt5_5,
253
254    /// GPT-5.5 Pro - Uses more compute for consistently better answers
255    ///
256    /// - Available in Responses API only (no Chat Completions)
257    /// - Supports: reasoning.effort (medium, high (default), xhigh)
258    #[serde(rename = "gpt-5.5-pro")]
259    Gpt5_5Pro,
260
261    // === GPT-5.4 Series ===
262    /// GPT-5.4 - Frontier model for complex professional work
263    ///
264    /// - Context: 1.05M tokens
265    /// - Supports: reasoning.effort (none (default), low, medium, high, xhigh)
266    #[serde(rename = "gpt-5.4")]
267    Gpt5_4,
268
269    /// GPT-5.4 Pro - Uses more compute for consistently better answers
270    ///
271    /// - Available in Responses API only (no Chat Completions)
272    /// - Supports: reasoning.effort (medium (default), high, xhigh)
273    #[serde(rename = "gpt-5.4-pro")]
274    Gpt5_4Pro,
275
276    /// GPT-5.4 Mini - Strong mini model for coding, computer use and subagents
277    ///
278    /// - Context: 400K tokens
279    /// - Supports: reasoning.effort
280    #[serde(rename = "gpt-5.4-mini")]
281    Gpt5_4Mini,
282
283    /// GPT-5.4 Nano - Cheapest GPT-5.4-class model for high-volume tasks
284    ///
285    /// - Context: 400K tokens
286    /// - Supports: reasoning.effort (none (default), low, medium, high, xhigh)
287    #[serde(rename = "gpt-5.4-nano")]
288    Gpt5_4Nano,
289
290    // === GPT-5.3 Series ===
291    /// GPT-5.3 Instant - Non-reasoning chat model
292    ///
293    /// Points to the GPT-5.3 Instant snapshot used in ChatGPT.
294    ///
295    /// **Deprecated by OpenAI** - GPT-5.6 is recommended for most API usage.
296    #[serde(rename = "gpt-5.3-chat-latest")]
297    Gpt5_3ChatLatest,
298
299    // === Codex Series (Responses API only) ===
300    /// GPT-5.3 Codex - Most capable agentic coding model
301    ///
302    /// - Available in Responses API only (no Chat Completions)
303    /// - Context: 400K tokens, 128K max output
304    /// - Supports: reasoning.effort (low, medium, high, xhigh)
305    #[serde(rename = "gpt-5.3-codex")]
306    Gpt5_3Codex,
307
308    /// GPT-5.2 Codex - GPT-5.2 optimized for agentic coding tasks
309    ///
310    /// - Available in Responses API only (no Chat Completions)
311    /// - Context: 400K tokens
312    /// - Supports: reasoning.effort (low, medium, high, xhigh)
313    #[serde(rename = "gpt-5.2-codex")]
314    Gpt5_2Codex,
315
316    /// GPT-5.1 Codex - GPT-5 optimized for agentic coding tasks
317    ///
318    /// - Available in Responses API only (no Chat Completions)
319    #[serde(rename = "gpt-5.1-codex")]
320    Gpt5_1Codex,
321
322    // === Web Search Models ===
323    /// GPT-5 Search API - GPT-5 with built-in web search
324    ///
325    /// Rejects sampling parameters and exposes no `reasoning` parameter.
326    #[serde(rename = "gpt-5-search-api")]
327    Gpt5SearchApi,
328
329    /// GPT-4o Search Preview - GPT-4o with built-in web search
330    #[serde(rename = "gpt-4o-search-preview")]
331    Gpt4oSearchPreview,
332
333    /// GPT-4o Mini Search Preview - GPT-4o Mini with built-in web search
334    #[serde(rename = "gpt-4o-mini-search-preview")]
335    Gpt4oMiniSearchPreview,
336
337    // === Audio Chat Models ===
338    /// GPT Audio - audio-capable chat model
339    ///
340    /// Requires an audio input content part or an audio output modality.
341    #[serde(rename = "gpt-audio")]
342    GptAudio,
343
344    /// GPT Audio 1.5 - newer audio-capable chat model
345    #[serde(rename = "gpt-audio-1.5")]
346    GptAudio1_5,
347
348    /// GPT Audio Mini - cost-efficient audio-capable chat model
349    #[serde(rename = "gpt-audio-mini")]
350    GptAudioMini,
351
352    // === GPT-5 Series ===
353    /// GPT-5 - Original GPT-5 flagship reasoning model
354    #[serde(rename = "gpt-5")]
355    Gpt5,
356
357    /// GPT-5 Pro - GPT-5 with more compute for difficult questions
358    ///
359    /// - Available in Responses API only (no Chat Completions)
360    #[serde(rename = "gpt-5-pro")]
361    Gpt5Pro,
362
363    /// GPT-5.2 Thinking - Flagship model for coding and agentic tasks
364    ///
365    /// - Context: 128K tokens (256K with thinking)
366    /// - Supports: reasoning.effort (none, minimal, low, medium, high, xhigh)
367    /// - Supports: verbosity parameter (low, medium, high)
368    #[serde(rename = "gpt-5.2")]
369    Gpt5_2,
370
371    /// GPT-5.2 Instant - Fast workhorse for everyday work
372    ///
373    /// Points to the GPT-5.2 Instant snapshot used in ChatGPT. This is a
374    /// non-reasoning model, so it accepts the full standard parameter set.
375    ///
376    /// **Deprecated by OpenAI** - GPT-5.6 is recommended for most API usage.
377    #[serde(rename = "gpt-5.2-chat-latest")]
378    Gpt5_2ChatLatest,
379
380    /// GPT-5.2 Pro - Smartest for difficult questions
381    ///
382    /// - Available in Responses API only
383    /// - Supports: xhigh reasoning effort
384    #[serde(rename = "gpt-5.2-pro")]
385    Gpt5_2Pro,
386
387    /// GPT-5.1 - Configurable reasoning and non-reasoning
388    ///
389    /// - Defaults to no reasoning (effort: none)
390    /// - Supports: reasoning.effort (none, low, medium, high)
391    #[serde(rename = "gpt-5.1")]
392    Gpt5_1,
393
394    /// GPT-5.1 Instant - Chat-optimized GPT-5.1
395    ///
396    /// Points to the GPT-5.1 Instant snapshot used in ChatGPT. This is a
397    /// non-reasoning model, so it accepts the full standard parameter set.
398    #[serde(rename = "gpt-5.1-chat-latest")]
399    Gpt5_1ChatLatest,
400
401    /// GPT-5.1 Codex Max - Powers Codex and Codex CLI
402    ///
403    /// - Available in Responses API only
404    /// - Supports: reasoning.effort (none, medium, high, xhigh)
405    #[serde(rename = "gpt-5.1-codex-max")]
406    Gpt5_1CodexMax,
407
408    /// GPT-5 Mini - Smaller, faster GPT-5 variant
409    #[serde(rename = "gpt-5-mini")]
410    Gpt5Mini,
411
412    /// GPT-5 Nano - Fastest, most cost-efficient GPT-5 variant
413    #[serde(rename = "gpt-5-nano")]
414    Gpt5Nano,
415
416    // === GPT-4.1 Series ===
417    /// GPT-4.1 - Smartest non-reasoning model with 1M token context
418    #[serde(rename = "gpt-4.1")]
419    Gpt4_1,
420
421    /// GPT-4.1 Mini - Balanced performance and cost
422    #[serde(rename = "gpt-4.1-mini")]
423    Gpt4_1Mini,
424
425    /// GPT-4.1 Nano - Fastest and most cost-efficient
426    #[serde(rename = "gpt-4.1-nano")]
427    Gpt4_1Nano,
428
429    // === GPT-4o Series ===
430    /// GPT-4o - High-intelligence flagship model (multimodal)
431    #[serde(rename = "gpt-4o")]
432    Gpt4o,
433
434    /// GPT-4o Mini - Cost-effective GPT-4o variant
435    #[serde(rename = "gpt-4o-mini")]
436    #[default]
437    Gpt4oMini,
438
439    /// GPT-4o Audio Preview - Audio-capable GPT-4o
440    #[serde(rename = "gpt-4o-audio-preview")]
441    Gpt4oAudioPreview,
442
443    // === GPT-4 Series ===
444    /// GPT-4 Turbo - High capability with faster responses
445    #[serde(rename = "gpt-4-turbo")]
446    Gpt4Turbo,
447
448    /// GPT-4 - Original GPT-4 model
449    #[serde(rename = "gpt-4")]
450    Gpt4,
451
452    // === GPT-3.5 Series ===
453    /// GPT-3.5 Turbo - Fast and cost-effective
454    #[serde(rename = "gpt-3.5-turbo")]
455    Gpt3_5Turbo,
456
457    /// GPT-3.5 Turbo 16K - legacy extended-context GPT-3.5 Turbo
458    #[serde(rename = "gpt-3.5-turbo-16k")]
459    Gpt3_5Turbo16k,
460
461    // === Reasoning Models (o-series) ===
462    /// O1 - Full reasoning model for complex tasks
463    #[serde(rename = "o1")]
464    O1,
465
466    /// O1 Pro - O1 with more compute for complex problems
467    #[serde(rename = "o1-pro")]
468    O1Pro,
469
470    /// O3 - Latest full reasoning model
471    #[serde(rename = "o3")]
472    O3,
473
474    /// O3 Pro - O3 with more compute
475    ///
476    /// - Available in Responses API only (no Chat Completions)
477    #[serde(rename = "o3-pro")]
478    O3Pro,
479
480    /// O3 Mini - Smaller, faster reasoning model
481    #[serde(rename = "o3-mini")]
482    O3Mini,
483
484    /// O4 Mini - Fast, cost-efficient reasoning model
485    #[serde(rename = "o4-mini")]
486    O4Mini,
487
488    // === Custom Model ===
489    /// Custom model ID for fine-tuned models or new models not yet in enum
490    #[serde(untagged)]
491    Custom(String),
492}
493
494impl ChatModel {
495    /// Returns the model identifier string for API requests.
496    ///
497    /// # Example
498    ///
499    /// ```rust
500    /// use openai_tools::common::models::ChatModel;
501    ///
502    /// assert_eq!(ChatModel::Gpt4oMini.as_str(), "gpt-4o-mini");
503    /// assert_eq!(ChatModel::O3Mini.as_str(), "o3-mini");
504    /// assert_eq!(ChatModel::Gpt5_2.as_str(), "gpt-5.2");
505    /// ```
506    pub fn as_str(&self) -> &str {
507        match self {
508            // GPT-5.6 Series
509            Self::Gpt5_6 => "gpt-5.6",
510            Self::Gpt5_6Sol => "gpt-5.6-sol",
511            Self::Gpt5_6Terra => "gpt-5.6-terra",
512            Self::Gpt5_6Luna => "gpt-5.6-luna",
513            // GPT-5.5 Series
514            Self::Gpt5_5 => "gpt-5.5",
515            Self::Gpt5_5Pro => "gpt-5.5-pro",
516            // GPT-5.4 Series
517            Self::Gpt5_4 => "gpt-5.4",
518            Self::Gpt5_4Pro => "gpt-5.4-pro",
519            Self::Gpt5_4Mini => "gpt-5.4-mini",
520            Self::Gpt5_4Nano => "gpt-5.4-nano",
521            // GPT-5.3 Series
522            Self::Gpt5_3ChatLatest => "gpt-5.3-chat-latest",
523            // Codex Series
524            Self::Gpt5_3Codex => "gpt-5.3-codex",
525            Self::Gpt5_2Codex => "gpt-5.2-codex",
526            Self::Gpt5_1Codex => "gpt-5.1-codex",
527            // Web Search Models
528            Self::Gpt5SearchApi => "gpt-5-search-api",
529            Self::Gpt4oSearchPreview => "gpt-4o-search-preview",
530            Self::Gpt4oMiniSearchPreview => "gpt-4o-mini-search-preview",
531            // Audio Chat Models
532            Self::GptAudio => "gpt-audio",
533            Self::GptAudio1_5 => "gpt-audio-1.5",
534            Self::GptAudioMini => "gpt-audio-mini",
535            // GPT-5 Series
536            Self::Gpt5 => "gpt-5",
537            Self::Gpt5Pro => "gpt-5-pro",
538            Self::Gpt5_2 => "gpt-5.2",
539            Self::Gpt5_2ChatLatest => "gpt-5.2-chat-latest",
540            Self::Gpt5_2Pro => "gpt-5.2-pro",
541            Self::Gpt5_1 => "gpt-5.1",
542            Self::Gpt5_1ChatLatest => "gpt-5.1-chat-latest",
543            Self::Gpt5_1CodexMax => "gpt-5.1-codex-max",
544            Self::Gpt5Mini => "gpt-5-mini",
545            Self::Gpt5Nano => "gpt-5-nano",
546            // GPT-4.1 Series
547            Self::Gpt4_1 => "gpt-4.1",
548            Self::Gpt4_1Mini => "gpt-4.1-mini",
549            Self::Gpt4_1Nano => "gpt-4.1-nano",
550            // GPT-4o Series
551            Self::Gpt4o => "gpt-4o",
552            Self::Gpt4oMini => "gpt-4o-mini",
553            Self::Gpt4oAudioPreview => "gpt-4o-audio-preview",
554            // GPT-4 Series
555            Self::Gpt4Turbo => "gpt-4-turbo",
556            Self::Gpt4 => "gpt-4",
557            // GPT-3.5 Series
558            Self::Gpt3_5Turbo => "gpt-3.5-turbo",
559            Self::Gpt3_5Turbo16k => "gpt-3.5-turbo-16k",
560            // Reasoning Models
561            Self::O1 => "o1",
562            Self::O1Pro => "o1-pro",
563            Self::O3 => "o3",
564            Self::O3Pro => "o3-pro",
565            Self::O3Mini => "o3-mini",
566            Self::O4Mini => "o4-mini",
567            // Custom
568            Self::Custom(s) => s.as_str(),
569        }
570    }
571
572    /// Checks if this is a reasoning model with parameter restrictions.
573    ///
574    /// Reasoning models (GPT-5 series, o1, o3, o4 series) only support:
575    /// - `temperature = 1.0`
576    /// - `top_p = 1.0`
577    /// - `frequency_penalty = 0`
578    /// - `presence_penalty = 0`
579    ///
580    /// # Example
581    ///
582    /// ```rust
583    /// use openai_tools::common::models::ChatModel;
584    ///
585    /// assert!(ChatModel::O3Mini.is_reasoning_model());
586    /// assert!(ChatModel::Gpt5_2.is_reasoning_model());
587    /// assert!(!ChatModel::Gpt4oMini.is_reasoning_model());
588    /// assert!(!ChatModel::Gpt4_1.is_reasoning_model());
589    /// ```
590    pub fn is_reasoning_model(&self) -> bool {
591        matches!(
592            self,
593            // GPT-5.6 series
594            Self::Gpt5_6 | Self::Gpt5_6Sol | Self::Gpt5_6Terra | Self::Gpt5_6Luna |
595            // GPT-5.5 series
596            Self::Gpt5_5 | Self::Gpt5_5Pro |
597            // GPT-5.4 series
598            Self::Gpt5_4 | Self::Gpt5_4Pro | Self::Gpt5_4Mini | Self::Gpt5_4Nano |
599            // Codex series
600            Self::Gpt5_3Codex | Self::Gpt5_2Codex | Self::Gpt5_1Codex | Self::Gpt5_1CodexMax |
601            // GPT-5 series
602            Self::Gpt5 | Self::Gpt5Pro | Self::Gpt5_2 | Self::Gpt5_2Pro | Self::Gpt5_1 | Self::Gpt5Mini | Self::Gpt5Nano |
603            // O-series reasoning models
604            Self::O1 | Self::O1Pro | Self::O3 | Self::O3Pro | Self::O3Mini | Self::O4Mini
605        ) || matches!(
606            self,
607            // The `*-chat-latest` aliases point at the non-reasoning "Instant"
608            // snapshots, and the search models expose no `reasoning` parameter,
609            // so neither must be caught by the prefix heuristic.
610            Self::Custom(s) if !s.ends_with("-chat-latest")
611                && !s.contains("-search")
612                && (s.starts_with("gpt-5") || s.starts_with("o1") || s.starts_with("o3") || s.starts_with("o4"))
613        )
614    }
615
616    /// Checks if this is a web-search model.
617    ///
618    /// Search models (`gpt-5-search-api`, `gpt-4o-search-preview`,
619    /// `gpt-4o-mini-search-preview`) reject the whole sampling parameter set -
620    /// `temperature`, `top_p`, `n`, `logprobs` and the penalties - but unlike
621    /// reasoning models they expose no `reasoning` parameter.
622    ///
623    /// # Example
624    ///
625    /// ```rust
626    /// use openai_tools::common::models::ChatModel;
627    ///
628    /// assert!(ChatModel::Gpt4oSearchPreview.is_search_model());
629    /// assert!(!ChatModel::Gpt4oSearchPreview.is_reasoning_model());
630    /// assert!(!ChatModel::Gpt4oMini.is_search_model());
631    /// ```
632    pub fn is_search_model(&self) -> bool {
633        matches!(self, Self::Gpt5SearchApi | Self::Gpt4oSearchPreview | Self::Gpt4oMiniSearchPreview)
634            || matches!(self, Self::Custom(s) if s.contains("-search"))
635    }
636
637    /// Returns parameter support information for this model.
638    ///
639    /// This method provides detailed information about which parameters
640    /// are supported by the model and any restrictions that apply.
641    ///
642    /// # Example
643    ///
644    /// ```rust
645    /// use openai_tools::common::models::{ChatModel, ParameterRestriction};
646    ///
647    /// // Standard model supports all parameters
648    /// let standard = ChatModel::Gpt4oMini;
649    /// let support = standard.parameter_support();
650    /// assert_eq!(support.temperature, ParameterRestriction::Any);
651    /// assert!(support.logprobs);
652    ///
653    /// // Reasoning model has restrictions
654    /// let reasoning = ChatModel::O3Mini;
655    /// let support = reasoning.parameter_support();
656    /// assert_eq!(support.temperature, ParameterRestriction::FixedValue(1.0));
657    /// assert!(!support.logprobs);
658    /// assert!(support.reasoning);
659    /// ```
660    pub fn parameter_support(&self) -> ParameterSupport {
661        if self.is_search_model() {
662            ParameterSupport::search_model()
663        } else if self.is_reasoning_model() {
664            ParameterSupport::reasoning_model()
665        } else {
666            ParameterSupport::standard_model()
667        }
668    }
669
670    /// Creates a custom model from a string.
671    ///
672    /// Use this for fine-tuned models or new models not yet in the enum.
673    ///
674    /// # Example
675    ///
676    /// ```rust
677    /// use openai_tools::common::models::ChatModel;
678    ///
679    /// let model = ChatModel::custom("ft:gpt-4o-mini:my-org::abc123");
680    /// assert_eq!(model.as_str(), "ft:gpt-4o-mini:my-org::abc123");
681    /// ```
682    pub fn custom(model_id: impl Into<String>) -> Self {
683        Self::Custom(model_id.into())
684    }
685}
686
687impl std::fmt::Display for ChatModel {
688    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
689        write!(f, "{}", self.as_str())
690    }
691}
692
693impl From<&str> for ChatModel {
694    fn from(s: &str) -> Self {
695        match s {
696            // GPT-5.6 Series
697            "gpt-5.6" => Self::Gpt5_6,
698            "gpt-5.6-sol" => Self::Gpt5_6Sol,
699            "gpt-5.6-terra" => Self::Gpt5_6Terra,
700            "gpt-5.6-luna" => Self::Gpt5_6Luna,
701            // GPT-5.5 Series
702            "gpt-5.5" => Self::Gpt5_5,
703            "gpt-5.5-pro" => Self::Gpt5_5Pro,
704            // GPT-5.4 Series
705            "gpt-5.4" => Self::Gpt5_4,
706            "gpt-5.4-pro" => Self::Gpt5_4Pro,
707            "gpt-5.4-mini" => Self::Gpt5_4Mini,
708            "gpt-5.4-nano" => Self::Gpt5_4Nano,
709            // GPT-5.3 Series
710            "gpt-5.3-chat-latest" => Self::Gpt5_3ChatLatest,
711            // Codex Series
712            "gpt-5.3-codex" => Self::Gpt5_3Codex,
713            "gpt-5.2-codex" => Self::Gpt5_2Codex,
714            "gpt-5.1-codex" => Self::Gpt5_1Codex,
715            // Web Search Models
716            "gpt-5-search-api" => Self::Gpt5SearchApi,
717            "gpt-4o-search-preview" => Self::Gpt4oSearchPreview,
718            "gpt-4o-mini-search-preview" => Self::Gpt4oMiniSearchPreview,
719            // Audio Chat Models
720            "gpt-audio" => Self::GptAudio,
721            "gpt-audio-1.5" => Self::GptAudio1_5,
722            "gpt-audio-mini" => Self::GptAudioMini,
723            // GPT-5 Series
724            "gpt-5" => Self::Gpt5,
725            "gpt-5-pro" => Self::Gpt5Pro,
726            "gpt-5.2" => Self::Gpt5_2,
727            "gpt-5.2-chat-latest" => Self::Gpt5_2ChatLatest,
728            "gpt-5.2-pro" => Self::Gpt5_2Pro,
729            "gpt-5.1" => Self::Gpt5_1,
730            "gpt-5.1-chat-latest" => Self::Gpt5_1ChatLatest,
731            "gpt-5.1-codex-max" => Self::Gpt5_1CodexMax,
732            "gpt-5-mini" => Self::Gpt5Mini,
733            "gpt-5-nano" => Self::Gpt5Nano,
734            // GPT-4.1 Series
735            "gpt-4.1" => Self::Gpt4_1,
736            "gpt-4.1-mini" => Self::Gpt4_1Mini,
737            "gpt-4.1-nano" => Self::Gpt4_1Nano,
738            // GPT-4o Series
739            "gpt-4o" => Self::Gpt4o,
740            "gpt-4o-mini" => Self::Gpt4oMini,
741            "gpt-4o-audio-preview" => Self::Gpt4oAudioPreview,
742            // GPT-4 Series
743            "gpt-4-turbo" => Self::Gpt4Turbo,
744            "gpt-4" => Self::Gpt4,
745            // GPT-3.5 Series
746            "gpt-3.5-turbo" => Self::Gpt3_5Turbo,
747            "gpt-3.5-turbo-16k" => Self::Gpt3_5Turbo16k,
748            // Reasoning Models
749            "o1" => Self::O1,
750            "o1-pro" => Self::O1Pro,
751            "o3" => Self::O3,
752            "o3-pro" => Self::O3Pro,
753            "o3-mini" => Self::O3Mini,
754            "o4-mini" => Self::O4Mini,
755            // Custom
756            other => Self::Custom(other.to_string()),
757        }
758    }
759}
760
761impl From<String> for ChatModel {
762    fn from(s: String) -> Self {
763        Self::from(s.as_str())
764    }
765}
766
767// ============================================================================
768// Embedding Models
769// ============================================================================
770
771/// Models available for the Embeddings API.
772///
773/// This enum covers all models that can be used with the Embeddings API
774/// (`/v1/embeddings`) for converting text into vector representations.
775///
776/// # Available Models
777///
778/// - [`TextEmbedding3Small`]: Improved, performant model (default)
779/// - [`TextEmbedding3Large`]: Most capable model for English and non-English
780/// - [`TextEmbeddingAda002`]: Legacy model (not recommended for new projects)
781///
782/// # Example
783///
784/// ```rust
785/// use openai_tools::common::models::EmbeddingModel;
786///
787/// let model = EmbeddingModel::TextEmbedding3Small;
788/// assert_eq!(model.as_str(), "text-embedding-3-small");
789/// assert_eq!(model.dimensions(), 1536);
790/// ```
791///
792/// # Reference
793///
794/// See [OpenAI Embeddings Guide](https://platform.openai.com/docs/guides/embeddings)
795#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
796pub enum EmbeddingModel {
797    /// text-embedding-3-small - Improved, more performant embedding model
798    ///
799    /// - Dimensions: 1536
800    /// - Max input: 8191 tokens
801    /// - Recommended for most use cases
802    #[serde(rename = "text-embedding-3-small")]
803    #[default]
804    TextEmbedding3Small,
805
806    /// text-embedding-3-large - Most capable embedding model
807    ///
808    /// - Dimensions: 3072
809    /// - Max input: 8191 tokens
810    /// - Best for high-accuracy tasks
811    #[serde(rename = "text-embedding-3-large")]
812    TextEmbedding3Large,
813
814    /// text-embedding-ada-002 - Legacy embedding model
815    ///
816    /// - Dimensions: 1536
817    /// - Max input: 8191 tokens
818    /// - Not recommended for new projects
819    #[serde(rename = "text-embedding-ada-002")]
820    TextEmbeddingAda002,
821}
822
823impl EmbeddingModel {
824    /// Returns the model identifier string for API requests.
825    pub fn as_str(&self) -> &str {
826        match self {
827            Self::TextEmbedding3Small => "text-embedding-3-small",
828            Self::TextEmbedding3Large => "text-embedding-3-large",
829            Self::TextEmbeddingAda002 => "text-embedding-ada-002",
830        }
831    }
832
833    /// Returns the default output dimensions for this model.
834    ///
835    /// Note: For `text-embedding-3-*` models, you can request fewer dimensions
836    /// via the API's `dimensions` parameter. This returns the default/maximum.
837    pub fn dimensions(&self) -> usize {
838        match self {
839            Self::TextEmbedding3Small => 1536,
840            Self::TextEmbedding3Large => 3072,
841            Self::TextEmbeddingAda002 => 1536,
842        }
843    }
844}
845
846impl std::fmt::Display for EmbeddingModel {
847    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
848        write!(f, "{}", self.as_str())
849    }
850}
851
852impl From<&str> for EmbeddingModel {
853    fn from(s: &str) -> Self {
854        match s {
855            "text-embedding-3-small" => Self::TextEmbedding3Small,
856            "text-embedding-3-large" => Self::TextEmbedding3Large,
857            "text-embedding-ada-002" => Self::TextEmbeddingAda002,
858            _ => Self::TextEmbedding3Small, // Default fallback
859        }
860    }
861}
862
863// ============================================================================
864// Realtime Models
865// ============================================================================
866
867/// Models available for the Realtime API.
868///
869/// This enum covers all models that can be used with the Realtime API
870/// for real-time audio and text interactions via WebSocket.
871///
872/// # Available Models
873///
874/// - [`GptRealtime_2025_08_28`]: GPT Realtime model (default)
875///
876/// # Example
877///
878/// ```rust
879/// use openai_tools::common::models::RealtimeModel;
880///
881/// let model = RealtimeModel::GptRealtime_2025_08_28;
882/// assert_eq!(model.as_str(), "gpt-realtime-2025-08-28");
883/// ```
884///
885/// # Reference
886///
887/// See [OpenAI Realtime API Documentation](https://platform.openai.com/docs/guides/realtime)
888#[allow(non_camel_case_types)]
889#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
890pub enum RealtimeModel {
891    /// gpt-realtime-2.1 - Reasoning speech-to-speech model with tool use
892    ///
893    /// - Context: 128K tokens, 32K max output
894    /// - Improved alphanumeric recognition, silence/noise handling and
895    ///   interruption behavior
896    #[serde(rename = "gpt-realtime-2.1")]
897    GptRealtime2_1,
898
899    /// gpt-realtime-2.1-mini - Smaller, cheaper gpt-realtime-2.1
900    #[serde(rename = "gpt-realtime-2.1-mini")]
901    GptRealtime2_1Mini,
902
903    /// gpt-realtime-2 - Previous generation realtime voice model
904    #[serde(rename = "gpt-realtime-2")]
905    GptRealtime2,
906
907    /// gpt-realtime - Alias tracking the current realtime voice model
908    #[serde(rename = "gpt-realtime")]
909    GptRealtime,
910
911    /// gpt-realtime-mini - Cost-efficient realtime voice model
912    #[serde(rename = "gpt-realtime-mini")]
913    GptRealtimeMini,
914
915    /// gpt-realtime-1.5 - Earlier realtime voice model
916    #[serde(rename = "gpt-realtime-1.5")]
917    GptRealtime1_5,
918
919    /// gpt-realtime-translate - Streaming speech-to-speech translation
920    ///
921    /// Uses the `v1/realtime/translations` endpoint rather than `v1/realtime`.
922    #[serde(rename = "gpt-realtime-translate")]
923    GptRealtimeTranslate,
924
925    /// gpt-realtime-2025-08-28 - GPT Realtime model (default)
926    ///
927    /// Kept as the default so that existing callers of `RealtimeClient::new()`
928    /// keep reaching the same model. Select a newer model explicitly with
929    /// [`RealtimeClient::model`](crate::realtime::RealtimeClient::model).
930    #[serde(rename = "gpt-realtime-2025-08-28")]
931    #[default]
932    GptRealtime_2025_08_28,
933
934    /// Custom model ID for new models not yet in enum
935    #[serde(untagged)]
936    Custom(String),
937}
938
939impl RealtimeModel {
940    /// Returns the model identifier string for API requests.
941    pub fn as_str(&self) -> &str {
942        match self {
943            Self::GptRealtime2_1 => "gpt-realtime-2.1",
944            Self::GptRealtime2_1Mini => "gpt-realtime-2.1-mini",
945            Self::GptRealtime2 => "gpt-realtime-2",
946            Self::GptRealtime => "gpt-realtime",
947            Self::GptRealtimeMini => "gpt-realtime-mini",
948            Self::GptRealtime1_5 => "gpt-realtime-1.5",
949            Self::GptRealtimeTranslate => "gpt-realtime-translate",
950            Self::GptRealtime_2025_08_28 => "gpt-realtime-2025-08-28",
951            Self::Custom(s) => s.as_str(),
952        }
953    }
954
955    /// Creates a custom model from a string.
956    pub fn custom(model_id: impl Into<String>) -> Self {
957        Self::Custom(model_id.into())
958    }
959}
960
961impl std::fmt::Display for RealtimeModel {
962    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
963        write!(f, "{}", self.as_str())
964    }
965}
966
967impl From<&str> for RealtimeModel {
968    fn from(s: &str) -> Self {
969        match s {
970            "gpt-realtime-2.1" => Self::GptRealtime2_1,
971            "gpt-realtime-2.1-mini" => Self::GptRealtime2_1Mini,
972            "gpt-realtime-2" => Self::GptRealtime2,
973            "gpt-realtime" => Self::GptRealtime,
974            "gpt-realtime-mini" => Self::GptRealtimeMini,
975            "gpt-realtime-1.5" => Self::GptRealtime1_5,
976            "gpt-realtime-translate" => Self::GptRealtimeTranslate,
977            "gpt-realtime-2025-08-28" => Self::GptRealtime_2025_08_28,
978            other => Self::Custom(other.to_string()),
979        }
980    }
981}
982
983// ============================================================================
984// Fine-tuning Models
985// ============================================================================
986
987/// Base models that can be used for fine-tuning.
988///
989/// This enum covers all models that can be fine-tuned via the Fine-tuning API
990/// (`/v1/fine_tuning/jobs`). Note that fine-tuning requires specific dated
991/// model versions.
992///
993/// # Available Models
994///
995/// ## GPT-4.1 Series (Latest)
996/// - [`Gpt41_2025_04_14`]: GPT-4.1 for fine-tuning
997/// - [`Gpt41Mini_2025_04_14`]: GPT-4.1 Mini for fine-tuning
998/// - [`Gpt41Nano_2025_04_14`]: GPT-4.1 Nano for fine-tuning
999///
1000/// ## GPT-4o Series
1001/// - [`Gpt4oMini_2024_07_18`]: GPT-4o Mini for fine-tuning
1002/// - [`Gpt4o_2024_08_06`]: GPT-4o for fine-tuning
1003///
1004/// ## GPT-4 Series
1005/// - [`Gpt4_0613`]: GPT-4 for fine-tuning
1006///
1007/// ## GPT-3.5 Series
1008/// - [`Gpt35Turbo_0125`]: GPT-3.5 Turbo for fine-tuning
1009///
1010/// # Example
1011///
1012/// ```rust
1013/// use openai_tools::common::models::FineTuningModel;
1014///
1015/// let model = FineTuningModel::Gpt4oMini_2024_07_18;
1016/// assert_eq!(model.as_str(), "gpt-4o-mini-2024-07-18");
1017/// ```
1018///
1019/// # Reference
1020///
1021/// See [OpenAI Fine-tuning Guide](https://platform.openai.com/docs/guides/fine-tuning)
1022#[allow(non_camel_case_types)]
1023#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1024pub enum FineTuningModel {
1025    // === GPT-4.1 Series ===
1026    /// gpt-4.1-2025-04-14 - GPT-4.1 for fine-tuning
1027    #[serde(rename = "gpt-4.1-2025-04-14")]
1028    Gpt41_2025_04_14,
1029
1030    /// gpt-4.1-mini-2025-04-14 - GPT-4.1 Mini for fine-tuning
1031    #[serde(rename = "gpt-4.1-mini-2025-04-14")]
1032    Gpt41Mini_2025_04_14,
1033
1034    /// gpt-4.1-nano-2025-04-14 - GPT-4.1 Nano for fine-tuning
1035    #[serde(rename = "gpt-4.1-nano-2025-04-14")]
1036    Gpt41Nano_2025_04_14,
1037
1038    // === GPT-4o Series ===
1039    /// gpt-4o-mini-2024-07-18 - GPT-4o Mini for fine-tuning
1040    #[serde(rename = "gpt-4o-mini-2024-07-18")]
1041    #[default]
1042    Gpt4oMini_2024_07_18,
1043
1044    /// gpt-4o-2024-08-06 - GPT-4o for fine-tuning
1045    #[serde(rename = "gpt-4o-2024-08-06")]
1046    Gpt4o_2024_08_06,
1047
1048    // === GPT-4 Series ===
1049    /// gpt-4-0613 - GPT-4 for fine-tuning
1050    #[serde(rename = "gpt-4-0613")]
1051    Gpt4_0613,
1052
1053    // === GPT-3.5 Series ===
1054    /// gpt-3.5-turbo-0125 - GPT-3.5 Turbo for fine-tuning
1055    #[serde(rename = "gpt-3.5-turbo-0125")]
1056    Gpt35Turbo_0125,
1057
1058    /// gpt-3.5-turbo-1106 - GPT-3.5 Turbo (older version)
1059    #[serde(rename = "gpt-3.5-turbo-1106")]
1060    Gpt35Turbo_1106,
1061
1062    /// gpt-3.5-turbo-0613 - GPT-3.5 Turbo (legacy)
1063    #[serde(rename = "gpt-3.5-turbo-0613")]
1064    Gpt35Turbo_0613,
1065
1066    /// babbage-002 - Legacy base model for fine-tuning
1067    #[serde(rename = "babbage-002")]
1068    Babbage002,
1069
1070    /// davinci-002 - Legacy base model for fine-tuning
1071    #[serde(rename = "davinci-002")]
1072    Davinci002,
1073}
1074
1075impl FineTuningModel {
1076    /// Returns the model identifier string for API requests.
1077    pub fn as_str(&self) -> &str {
1078        match self {
1079            // GPT-4.1 Series
1080            Self::Gpt41_2025_04_14 => "gpt-4.1-2025-04-14",
1081            Self::Gpt41Mini_2025_04_14 => "gpt-4.1-mini-2025-04-14",
1082            Self::Gpt41Nano_2025_04_14 => "gpt-4.1-nano-2025-04-14",
1083            // GPT-4o Series
1084            Self::Gpt4oMini_2024_07_18 => "gpt-4o-mini-2024-07-18",
1085            Self::Gpt4o_2024_08_06 => "gpt-4o-2024-08-06",
1086            // GPT-4 Series
1087            Self::Gpt4_0613 => "gpt-4-0613",
1088            // GPT-3.5 Series
1089            Self::Gpt35Turbo_0125 => "gpt-3.5-turbo-0125",
1090            Self::Gpt35Turbo_1106 => "gpt-3.5-turbo-1106",
1091            Self::Gpt35Turbo_0613 => "gpt-3.5-turbo-0613",
1092            Self::Babbage002 => "babbage-002",
1093            Self::Davinci002 => "davinci-002",
1094        }
1095    }
1096}
1097
1098impl std::fmt::Display for FineTuningModel {
1099    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1100        write!(f, "{}", self.as_str())
1101    }
1102}
1103
1104#[cfg(test)]
1105mod tests {
1106    use super::*;
1107
1108    #[test]
1109    fn test_chat_model_as_str() {
1110        assert_eq!(ChatModel::Gpt4oMini.as_str(), "gpt-4o-mini");
1111        assert_eq!(ChatModel::O3Mini.as_str(), "o3-mini");
1112        assert_eq!(ChatModel::Gpt4_1.as_str(), "gpt-4.1");
1113        // GPT-5 models
1114        assert_eq!(ChatModel::Gpt5_2.as_str(), "gpt-5.2");
1115        assert_eq!(ChatModel::Gpt5_2ChatLatest.as_str(), "gpt-5.2-chat-latest");
1116        assert_eq!(ChatModel::Gpt5_2Pro.as_str(), "gpt-5.2-pro");
1117        assert_eq!(ChatModel::Gpt5_1.as_str(), "gpt-5.1");
1118        assert_eq!(ChatModel::Gpt5_1CodexMax.as_str(), "gpt-5.1-codex-max");
1119        assert_eq!(ChatModel::Gpt5Mini.as_str(), "gpt-5-mini");
1120    }
1121
1122    #[test]
1123    fn test_chat_model_is_reasoning() {
1124        // O-series reasoning models
1125        assert!(ChatModel::O1.is_reasoning_model());
1126        assert!(ChatModel::O3.is_reasoning_model());
1127        assert!(ChatModel::O3Mini.is_reasoning_model());
1128        assert!(ChatModel::O4Mini.is_reasoning_model());
1129        // GPT-5 series are also reasoning models, except the `*-chat-latest`
1130        // aliases, which point at the non-reasoning Instant snapshots.
1131        assert!(ChatModel::Gpt5_2.is_reasoning_model());
1132        assert!(!ChatModel::Gpt5_2ChatLatest.is_reasoning_model());
1133        assert!(ChatModel::Gpt5_2Pro.is_reasoning_model());
1134        assert!(ChatModel::Gpt5_1.is_reasoning_model());
1135        assert!(ChatModel::Gpt5_1CodexMax.is_reasoning_model());
1136        assert!(ChatModel::Gpt5Mini.is_reasoning_model());
1137        // Non-reasoning models
1138        assert!(!ChatModel::Gpt4oMini.is_reasoning_model());
1139        assert!(!ChatModel::Gpt4_1.is_reasoning_model());
1140    }
1141
1142    #[test]
1143    fn test_chat_model_from_str() {
1144        assert_eq!(ChatModel::from("gpt-4o-mini"), ChatModel::Gpt4oMini);
1145        assert_eq!(ChatModel::from("o3-mini"), ChatModel::O3Mini);
1146        // GPT-5 models
1147        assert_eq!(ChatModel::from("gpt-5.2"), ChatModel::Gpt5_2);
1148        assert_eq!(ChatModel::from("gpt-5.2-chat-latest"), ChatModel::Gpt5_2ChatLatest);
1149        assert_eq!(ChatModel::from("gpt-5.2-pro"), ChatModel::Gpt5_2Pro);
1150        assert_eq!(ChatModel::from("gpt-5.1"), ChatModel::Gpt5_1);
1151        assert_eq!(ChatModel::from("gpt-5.1-codex-max"), ChatModel::Gpt5_1CodexMax);
1152        assert_eq!(ChatModel::from("gpt-5-mini"), ChatModel::Gpt5Mini);
1153        // Unknown models become Custom
1154        assert!(matches!(ChatModel::from("unknown-model"), ChatModel::Custom(_)));
1155    }
1156
1157    #[test]
1158    fn test_chat_model_custom() {
1159        let custom = ChatModel::custom("ft:gpt-4o-mini:org::123");
1160        assert_eq!(custom.as_str(), "ft:gpt-4o-mini:org::123");
1161    }
1162
1163    #[test]
1164    fn test_chat_model_custom_gpt5_is_reasoning() {
1165        // Custom GPT-5 models should also be detected as reasoning models
1166        let custom_gpt5 = ChatModel::custom("gpt-5.3-preview");
1167        assert!(custom_gpt5.is_reasoning_model());
1168    }
1169
1170    #[test]
1171    fn test_embedding_model_dimensions() {
1172        assert_eq!(EmbeddingModel::TextEmbedding3Small.dimensions(), 1536);
1173        assert_eq!(EmbeddingModel::TextEmbedding3Large.dimensions(), 3072);
1174    }
1175
1176    #[test]
1177    fn test_realtime_model_as_str() {
1178        assert_eq!(RealtimeModel::GptRealtime_2025_08_28.as_str(), "gpt-realtime-2025-08-28");
1179    }
1180
1181    #[test]
1182    fn test_fine_tuning_model_as_str() {
1183        assert_eq!(FineTuningModel::Gpt4oMini_2024_07_18.as_str(), "gpt-4o-mini-2024-07-18");
1184        assert_eq!(FineTuningModel::Gpt41_2025_04_14.as_str(), "gpt-4.1-2025-04-14");
1185    }
1186
1187    #[test]
1188    fn test_chat_model_serialization() {
1189        let model = ChatModel::Gpt4oMini;
1190        let json = serde_json::to_string(&model).unwrap();
1191        assert_eq!(json, "\"gpt-4o-mini\"");
1192        // GPT-5 serialization
1193        let gpt52 = ChatModel::Gpt5_2;
1194        let json = serde_json::to_string(&gpt52).unwrap();
1195        assert_eq!(json, "\"gpt-5.2\"");
1196    }
1197
1198    #[test]
1199    fn test_chat_model_deserialization() {
1200        let model: ChatModel = serde_json::from_str("\"gpt-4o-mini\"").unwrap();
1201        assert_eq!(model, ChatModel::Gpt4oMini);
1202        // GPT-5 deserialization
1203        let gpt52: ChatModel = serde_json::from_str("\"gpt-5.2\"").unwrap();
1204        assert_eq!(gpt52, ChatModel::Gpt5_2);
1205    }
1206
1207    #[test]
1208    fn test_parameter_support_standard_model() {
1209        let model = ChatModel::Gpt4oMini;
1210        let support = model.parameter_support();
1211
1212        // Standard models support all parameters
1213        assert_eq!(support.temperature, ParameterRestriction::Any);
1214        assert_eq!(support.frequency_penalty, ParameterRestriction::Any);
1215        assert_eq!(support.presence_penalty, ParameterRestriction::Any);
1216        assert_eq!(support.top_p, ParameterRestriction::Any);
1217        assert!(support.logprobs);
1218        assert!(support.top_logprobs);
1219        assert!(support.logit_bias);
1220        assert!(support.n_multiple);
1221        assert!(!support.reasoning); // Standard models don't support reasoning
1222    }
1223
1224    #[test]
1225    fn test_parameter_support_reasoning_model() {
1226        let model = ChatModel::O3Mini;
1227        let support = model.parameter_support();
1228
1229        // Reasoning models have restrictions
1230        assert_eq!(support.temperature, ParameterRestriction::FixedValue(1.0));
1231        assert_eq!(support.frequency_penalty, ParameterRestriction::FixedValue(0.0));
1232        assert_eq!(support.presence_penalty, ParameterRestriction::FixedValue(0.0));
1233        assert_eq!(support.top_p, ParameterRestriction::FixedValue(1.0));
1234        assert!(!support.logprobs);
1235        assert!(!support.top_logprobs);
1236        assert!(!support.logit_bias);
1237        assert!(!support.n_multiple);
1238        assert!(support.reasoning); // Reasoning models support reasoning
1239    }
1240
1241    #[test]
1242    fn test_parameter_support_gpt5_model() {
1243        // GPT-5 models are also reasoning models
1244        let model = ChatModel::Gpt5_2;
1245        let support = model.parameter_support();
1246
1247        assert_eq!(support.temperature, ParameterRestriction::FixedValue(1.0));
1248        assert!(!support.logprobs);
1249        assert!(support.reasoning);
1250    }
1251
1252    // =============================================================================
1253    // Comprehensive Reasoning Model Detection Tests
1254    // =============================================================================
1255
1256    #[test]
1257    fn test_all_o_series_models_are_reasoning() {
1258        // All defined o-series models should be detected as reasoning models
1259        let o_series = vec![ChatModel::O1, ChatModel::O1Pro, ChatModel::O3, ChatModel::O3Mini, ChatModel::O4Mini];
1260
1261        for model in o_series {
1262            assert!(model.is_reasoning_model(), "Expected {} to be a reasoning model", model.as_str());
1263        }
1264    }
1265
1266    #[test]
1267    fn test_all_gpt5_models_are_reasoning() {
1268        // All GPT-5 series models should be detected as reasoning models,
1269        // except the `*-chat-latest` aliases (covered separately by
1270        // `test_chat_latest_models_are_not_reasoning_models`).
1271        let gpt5_series =
1272            vec![ChatModel::Gpt5_2, ChatModel::Gpt5_2Pro, ChatModel::Gpt5_1, ChatModel::Gpt5_1CodexMax, ChatModel::Gpt5Mini, ChatModel::Gpt5Nano];
1273
1274        for model in gpt5_series {
1275            assert!(model.is_reasoning_model(), "Expected {} to be a reasoning model", model.as_str());
1276        }
1277    }
1278
1279    #[test]
1280    fn test_all_standard_models_are_not_reasoning() {
1281        // Standard models should NOT be detected as reasoning models
1282        let standard_models = vec![
1283            ChatModel::Gpt4oMini,
1284            ChatModel::Gpt4o,
1285            ChatModel::Gpt4oAudioPreview,
1286            ChatModel::Gpt4Turbo,
1287            ChatModel::Gpt4,
1288            ChatModel::Gpt3_5Turbo,
1289            ChatModel::Gpt4_1,
1290            ChatModel::Gpt4_1Mini,
1291            ChatModel::Gpt4_1Nano,
1292        ];
1293
1294        for model in standard_models {
1295            assert!(!model.is_reasoning_model(), "Expected {} to NOT be a reasoning model", model.as_str());
1296        }
1297    }
1298
1299    // =============================================================================
1300    // Custom Model Reasoning Detection Tests
1301    // =============================================================================
1302
1303    #[test]
1304    fn test_custom_o1_models_are_reasoning() {
1305        let custom_o1_variants = vec!["o1-mini", "o1-preview", "o1-pro-2025", "o1-high"];
1306
1307        for model_str in custom_o1_variants {
1308            let model = ChatModel::custom(model_str);
1309            assert!(model.is_reasoning_model(), "Expected custom model '{}' to be a reasoning model", model_str);
1310        }
1311    }
1312
1313    #[test]
1314    fn test_custom_o3_models_are_reasoning() {
1315        let custom_o3_variants = vec!["o3-preview", "o3-high", "o3-2025-01-15"];
1316
1317        for model_str in custom_o3_variants {
1318            let model = ChatModel::custom(model_str);
1319            assert!(model.is_reasoning_model(), "Expected custom model '{}' to be a reasoning model", model_str);
1320        }
1321    }
1322
1323    #[test]
1324    fn test_custom_o4_models_are_reasoning() {
1325        let custom_o4_variants = vec!["o4-preview", "o4-mini-2025", "o4-high"];
1326
1327        for model_str in custom_o4_variants {
1328            let model = ChatModel::custom(model_str);
1329            assert!(model.is_reasoning_model(), "Expected custom model '{}' to be a reasoning model", model_str);
1330        }
1331    }
1332
1333    #[test]
1334    fn test_custom_gpt5_models_are_reasoning() {
1335        let custom_gpt5_variants = vec!["gpt-5.3", "gpt-5.3-preview", "gpt-5-turbo", "gpt-5.0"];
1336
1337        for model_str in custom_gpt5_variants {
1338            let model = ChatModel::custom(model_str);
1339            assert!(model.is_reasoning_model(), "Expected custom model '{}' to be a reasoning model", model_str);
1340        }
1341    }
1342
1343    #[test]
1344    fn test_custom_standard_models_are_not_reasoning() {
1345        let custom_standard_variants = vec![
1346            "ft:gpt-4o-mini:org::123",
1347            "gpt-4o-2025-01-15",
1348            "gpt-4-turbo-preview",
1349            "gpt-3.5-turbo-instruct",
1350            "text-davinci-003",
1351            "claude-3-opus", // Non-OpenAI model
1352        ];
1353
1354        for model_str in custom_standard_variants {
1355            let model = ChatModel::custom(model_str);
1356            assert!(!model.is_reasoning_model(), "Expected custom model '{}' to NOT be a reasoning model", model_str);
1357        }
1358    }
1359
1360    // =============================================================================
1361    // Parameter Support Tests for Each Model Generation
1362    // =============================================================================
1363
1364    #[test]
1365    fn test_parameter_support_all_o_series() {
1366        let o_series = vec![ChatModel::O1, ChatModel::O1Pro, ChatModel::O3, ChatModel::O3Mini, ChatModel::O4Mini];
1367
1368        for model in o_series {
1369            let support = model.parameter_support();
1370
1371            assert_eq!(support.temperature, ParameterRestriction::FixedValue(1.0), "{} should only support temperature=1.0", model.as_str());
1372            assert_eq!(
1373                support.frequency_penalty,
1374                ParameterRestriction::FixedValue(0.0),
1375                "{} should only support frequency_penalty=0.0",
1376                model.as_str()
1377            );
1378            assert_eq!(
1379                support.presence_penalty,
1380                ParameterRestriction::FixedValue(0.0),
1381                "{} should only support presence_penalty=0.0",
1382                model.as_str()
1383            );
1384            assert_eq!(support.top_p, ParameterRestriction::FixedValue(1.0), "{} should only support top_p=1.0", model.as_str());
1385            assert!(!support.logprobs, "{} should not support logprobs", model.as_str());
1386            assert!(!support.top_logprobs, "{} should not support top_logprobs", model.as_str());
1387            assert!(!support.logit_bias, "{} should not support logit_bias", model.as_str());
1388            assert!(!support.n_multiple, "{} should only support n=1", model.as_str());
1389            assert!(support.reasoning, "{} should support reasoning parameter", model.as_str());
1390        }
1391    }
1392
1393    #[test]
1394    fn test_parameter_support_all_gpt5_series() {
1395        // The `*-chat-latest` aliases are excluded: they are non-reasoning
1396        // models and accept the full standard parameter set.
1397        let gpt5_series =
1398            vec![ChatModel::Gpt5_2, ChatModel::Gpt5_2Pro, ChatModel::Gpt5_1, ChatModel::Gpt5_1CodexMax, ChatModel::Gpt5Mini, ChatModel::Gpt5Nano];
1399
1400        for model in gpt5_series {
1401            let support = model.parameter_support();
1402
1403            assert_eq!(support.temperature, ParameterRestriction::FixedValue(1.0), "{} should only support temperature=1.0", model.as_str());
1404            assert!(support.reasoning, "{} should support reasoning parameter", model.as_str());
1405        }
1406    }
1407
1408    #[test]
1409    fn test_parameter_support_all_standard_gpt4_series() {
1410        let gpt4_series = vec![
1411            ChatModel::Gpt4oMini,
1412            ChatModel::Gpt4o,
1413            ChatModel::Gpt4Turbo,
1414            ChatModel::Gpt4,
1415            ChatModel::Gpt4_1,
1416            ChatModel::Gpt4_1Mini,
1417            ChatModel::Gpt4_1Nano,
1418        ];
1419
1420        for model in gpt4_series {
1421            let support = model.parameter_support();
1422
1423            assert_eq!(support.temperature, ParameterRestriction::Any, "{} should support any temperature", model.as_str());
1424            assert_eq!(support.frequency_penalty, ParameterRestriction::Any, "{} should support any frequency_penalty", model.as_str());
1425            assert_eq!(support.presence_penalty, ParameterRestriction::Any, "{} should support any presence_penalty", model.as_str());
1426            assert!(support.logprobs, "{} should support logprobs", model.as_str());
1427            assert!(support.top_logprobs, "{} should support top_logprobs", model.as_str());
1428            assert!(support.logit_bias, "{} should support logit_bias", model.as_str());
1429            assert!(support.n_multiple, "{} should support n > 1", model.as_str());
1430            assert!(!support.reasoning, "{} should NOT support reasoning parameter", model.as_str());
1431        }
1432    }
1433
1434    // =============================================================================
1435    // ParameterRestriction Enum Tests
1436    // =============================================================================
1437
1438    #[test]
1439    fn test_parameter_restriction_equality() {
1440        assert_eq!(ParameterRestriction::Any, ParameterRestriction::Any);
1441        assert_eq!(ParameterRestriction::NotSupported, ParameterRestriction::NotSupported);
1442        assert_eq!(ParameterRestriction::FixedValue(1.0), ParameterRestriction::FixedValue(1.0));
1443
1444        assert_ne!(ParameterRestriction::Any, ParameterRestriction::NotSupported);
1445        assert_ne!(ParameterRestriction::FixedValue(1.0), ParameterRestriction::FixedValue(0.0));
1446    }
1447
1448    #[test]
1449    fn test_parameter_support_factory_methods() {
1450        let standard = ParameterSupport::standard_model();
1451        assert_eq!(standard.temperature, ParameterRestriction::Any);
1452        assert!(standard.logprobs);
1453        assert!(!standard.reasoning);
1454
1455        let reasoning = ParameterSupport::reasoning_model();
1456        assert_eq!(reasoning.temperature, ParameterRestriction::FixedValue(1.0));
1457        assert!(!reasoning.logprobs);
1458        assert!(reasoning.reasoning);
1459    }
1460
1461    // =============================================================================
1462    // Model String Conversion Tests
1463    // =============================================================================
1464
1465    #[test]
1466    fn test_all_gpt5_model_string_roundtrip() {
1467        let gpt5_models = vec![
1468            ("gpt-5.2", ChatModel::Gpt5_2),
1469            ("gpt-5.2-chat-latest", ChatModel::Gpt5_2ChatLatest),
1470            ("gpt-5.2-pro", ChatModel::Gpt5_2Pro),
1471            ("gpt-5.1", ChatModel::Gpt5_1),
1472            ("gpt-5.1-chat-latest", ChatModel::Gpt5_1ChatLatest),
1473            ("gpt-5.1-codex-max", ChatModel::Gpt5_1CodexMax),
1474            ("gpt-5-mini", ChatModel::Gpt5Mini),
1475            ("gpt-5-nano", ChatModel::Gpt5Nano),
1476        ];
1477
1478        for (model_str, expected_model) in gpt5_models {
1479            // Test from string
1480            let parsed = ChatModel::from(model_str);
1481            assert_eq!(parsed, expected_model, "Failed to parse '{}'", model_str);
1482
1483            // Test to string
1484            assert_eq!(expected_model.as_str(), model_str, "Failed to convert {:?} to string", expected_model);
1485
1486            // Test serialization roundtrip
1487            let json = serde_json::to_string(&expected_model).unwrap();
1488            let deserialized: ChatModel = serde_json::from_str(&json).unwrap();
1489            assert_eq!(deserialized, expected_model, "Serialization roundtrip failed for {}", model_str);
1490        }
1491    }
1492
1493    #[test]
1494    fn test_all_o_series_model_string_roundtrip() {
1495        let o_series_models = vec![
1496            ("o1", ChatModel::O1),
1497            ("o1-pro", ChatModel::O1Pro),
1498            ("o3", ChatModel::O3),
1499            ("o3-mini", ChatModel::O3Mini),
1500            ("o4-mini", ChatModel::O4Mini),
1501        ];
1502
1503        for (model_str, expected_model) in o_series_models {
1504            let parsed = ChatModel::from(model_str);
1505            assert_eq!(parsed, expected_model, "Failed to parse '{}'", model_str);
1506            assert_eq!(expected_model.as_str(), model_str, "Failed to convert {:?} to string", expected_model);
1507        }
1508    }
1509
1510    // =============================================================================
1511    // Embedding Model Tests
1512    // =============================================================================
1513
1514    #[test]
1515    fn test_embedding_model_string_roundtrip() {
1516        let embedding_models = vec![
1517            ("text-embedding-3-small", EmbeddingModel::TextEmbedding3Small),
1518            ("text-embedding-3-large", EmbeddingModel::TextEmbedding3Large),
1519            ("text-embedding-ada-002", EmbeddingModel::TextEmbeddingAda002),
1520        ];
1521
1522        for (model_str, expected_model) in embedding_models {
1523            let parsed = EmbeddingModel::from(model_str);
1524            assert_eq!(parsed, expected_model, "Failed to parse '{}'", model_str);
1525            assert_eq!(expected_model.as_str(), model_str, "Failed to convert {:?} to string", expected_model);
1526        }
1527    }
1528
1529    #[test]
1530    fn test_embedding_model_all_dimensions() {
1531        assert_eq!(EmbeddingModel::TextEmbedding3Small.dimensions(), 1536);
1532        assert_eq!(EmbeddingModel::TextEmbedding3Large.dimensions(), 3072);
1533        assert_eq!(EmbeddingModel::TextEmbeddingAda002.dimensions(), 1536);
1534    }
1535
1536    // =============================================================================
1537    // Realtime Model Tests
1538    // =============================================================================
1539
1540    #[test]
1541    fn test_realtime_model_string_roundtrip() {
1542        let realtime_models = vec![("gpt-realtime-2025-08-28", RealtimeModel::GptRealtime_2025_08_28)];
1543
1544        for (model_str, expected_model) in realtime_models {
1545            let parsed = RealtimeModel::from(model_str);
1546            assert_eq!(parsed, expected_model, "Failed to parse '{}'", model_str);
1547            assert_eq!(expected_model.as_str(), model_str, "Failed to convert {:?} to string", expected_model);
1548        }
1549    }
1550
1551    #[test]
1552    fn test_realtime_model_custom() {
1553        let custom = RealtimeModel::custom("gpt-4o-realtime-2025");
1554        assert_eq!(custom.as_str(), "gpt-4o-realtime-2025");
1555        assert!(matches!(custom, RealtimeModel::Custom(_)));
1556    }
1557
1558    // =============================================================================
1559    // Fine-tuning Model Tests
1560    // =============================================================================
1561
1562    #[test]
1563    fn test_fine_tuning_model_as_str_all_variants() {
1564        let fine_tuning_models = vec![
1565            ("gpt-4.1-2025-04-14", FineTuningModel::Gpt41_2025_04_14),
1566            ("gpt-4.1-mini-2025-04-14", FineTuningModel::Gpt41Mini_2025_04_14),
1567            ("gpt-4.1-nano-2025-04-14", FineTuningModel::Gpt41Nano_2025_04_14),
1568            ("gpt-4o-mini-2024-07-18", FineTuningModel::Gpt4oMini_2024_07_18),
1569            ("gpt-4o-2024-08-06", FineTuningModel::Gpt4o_2024_08_06),
1570            ("gpt-4-0613", FineTuningModel::Gpt4_0613),
1571            ("gpt-3.5-turbo-0125", FineTuningModel::Gpt35Turbo_0125),
1572            ("gpt-3.5-turbo-1106", FineTuningModel::Gpt35Turbo_1106),
1573            ("gpt-3.5-turbo-0613", FineTuningModel::Gpt35Turbo_0613),
1574        ];
1575
1576        for (model_str, expected_model) in fine_tuning_models {
1577            assert_eq!(expected_model.as_str(), model_str, "Failed to convert {:?} to string", expected_model);
1578        }
1579    }
1580
1581    #[test]
1582    fn test_fine_tuning_model_serialization_roundtrip() {
1583        let models = vec![FineTuningModel::Gpt41_2025_04_14, FineTuningModel::Gpt4oMini_2024_07_18, FineTuningModel::Gpt35Turbo_0125];
1584
1585        for model in models {
1586            let json = serde_json::to_string(&model).unwrap();
1587            let deserialized: FineTuningModel = serde_json::from_str(&json).unwrap();
1588            assert_eq!(deserialized, model, "Serialization roundtrip failed for {:?}", model);
1589        }
1590    }
1591
1592    // ========================================================================
1593    // GPT-5.3 / 5.4 / 5.5 / 5.6 series and Codex models
1594    //
1595    // Model IDs and capabilities verified against the OpenAI API reference
1596    // (https://developers.openai.com/api/docs/models), August 2026.
1597    // ========================================================================
1598
1599    /// Every model ID added for the GPT-5.3 through GPT-5.6 generations, paired
1600    /// with the exact string the API expects.
1601    fn gpt5_3_to_5_6_models() -> Vec<(ChatModel, &'static str)> {
1602        vec![
1603            // GPT-5.6 series: `gpt-5.6` is a documented alias routing to Sol.
1604            (ChatModel::Gpt5_6, "gpt-5.6"),
1605            (ChatModel::Gpt5_6Sol, "gpt-5.6-sol"),
1606            (ChatModel::Gpt5_6Terra, "gpt-5.6-terra"),
1607            (ChatModel::Gpt5_6Luna, "gpt-5.6-luna"),
1608            // GPT-5.5 series
1609            (ChatModel::Gpt5_5, "gpt-5.5"),
1610            (ChatModel::Gpt5_5Pro, "gpt-5.5-pro"),
1611            // GPT-5.4 series
1612            (ChatModel::Gpt5_4, "gpt-5.4"),
1613            (ChatModel::Gpt5_4Pro, "gpt-5.4-pro"),
1614            (ChatModel::Gpt5_4Mini, "gpt-5.4-mini"),
1615            (ChatModel::Gpt5_4Nano, "gpt-5.4-nano"),
1616            // GPT-5.3 series
1617            (ChatModel::Gpt5_3ChatLatest, "gpt-5.3-chat-latest"),
1618            // Codex series
1619            (ChatModel::Gpt5_3Codex, "gpt-5.3-codex"),
1620            (ChatModel::Gpt5_2Codex, "gpt-5.2-codex"),
1621            (ChatModel::Gpt5_1Codex, "gpt-5.1-codex"),
1622        ]
1623    }
1624
1625    #[test]
1626    fn test_new_chat_models_as_str() {
1627        for (model, expected) in gpt5_3_to_5_6_models() {
1628            assert_eq!(model.as_str(), expected, "Wrong model ID for {:?}", model);
1629        }
1630    }
1631
1632    #[test]
1633    fn test_new_chat_models_from_str_roundtrip() {
1634        for (model, model_str) in gpt5_3_to_5_6_models() {
1635            assert_eq!(ChatModel::from(model_str), model, "Failed to parse {}", model_str);
1636            // A known model must never fall through to Custom.
1637            assert_eq!(ChatModel::from(model_str).as_str(), model_str, "Roundtrip failed for {}", model_str);
1638        }
1639    }
1640
1641    #[test]
1642    fn test_new_chat_models_serialization_roundtrip() {
1643        for (model, model_str) in gpt5_3_to_5_6_models() {
1644            let json = serde_json::to_string(&model).unwrap();
1645            assert_eq!(json, format!("\"{}\"", model_str), "Wrong serialization for {:?}", model);
1646            let deserialized: ChatModel = serde_json::from_str(&json).unwrap();
1647            assert_eq!(deserialized, model, "Serialization roundtrip failed for {:?}", model);
1648        }
1649    }
1650
1651    /// Every GPT-5.4/5.5/5.6 and Codex model documents "reasoning token
1652    /// support", so they carry the reasoning-model parameter restrictions.
1653    #[test]
1654    fn test_new_gpt5_models_are_reasoning_models() {
1655        let reasoning = vec![
1656            ChatModel::Gpt5_6,
1657            ChatModel::Gpt5_6Sol,
1658            ChatModel::Gpt5_6Terra,
1659            ChatModel::Gpt5_6Luna,
1660            ChatModel::Gpt5_5,
1661            ChatModel::Gpt5_5Pro,
1662            ChatModel::Gpt5_4,
1663            ChatModel::Gpt5_4Pro,
1664            ChatModel::Gpt5_4Mini,
1665            ChatModel::Gpt5_4Nano,
1666            ChatModel::Gpt5_3Codex,
1667            ChatModel::Gpt5_2Codex,
1668            ChatModel::Gpt5_1Codex,
1669        ];
1670
1671        for model in reasoning {
1672            assert!(model.is_reasoning_model(), "Expected {} to be a reasoning model", model.as_str());
1673            let support = model.parameter_support();
1674            assert_eq!(support.temperature, ParameterRestriction::FixedValue(1.0), "{} should pin temperature to 1.0", model.as_str());
1675            assert!(support.reasoning, "{} should support the reasoning parameter", model.as_str());
1676        }
1677    }
1678
1679    /// The `*-chat-latest` models point at the non-reasoning "Instant"
1680    /// snapshots used in ChatGPT. The API reference lists no reasoning token
1681    /// support for them, so they accept the full standard parameter set.
1682    #[test]
1683    fn test_chat_latest_models_are_not_reasoning_models() {
1684        let chat_latest = vec![ChatModel::Gpt5_3ChatLatest, ChatModel::Gpt5_2ChatLatest, ChatModel::Gpt5_1ChatLatest];
1685
1686        for model in chat_latest {
1687            assert!(!model.is_reasoning_model(), "Expected {} to NOT be a reasoning model", model.as_str());
1688            let support = model.parameter_support();
1689            assert_eq!(support.temperature, ParameterRestriction::Any, "{} should accept any temperature", model.as_str());
1690            assert_eq!(support.top_p, ParameterRestriction::Any, "{} should accept any top_p", model.as_str());
1691            assert!(!support.reasoning, "{} should NOT support the reasoning parameter", model.as_str());
1692        }
1693    }
1694
1695    /// The `Custom` fallback infers reasoning support from the model ID prefix.
1696    /// A future `gpt-5.x-chat-latest` must not be caught by that heuristic.
1697    #[test]
1698    fn test_custom_chat_latest_is_not_reasoning_model() {
1699        assert!(!ChatModel::custom("gpt-5.7-chat-latest").is_reasoning_model());
1700        // Unknown GPT-5 models still default to the reasoning restrictions.
1701        assert!(ChatModel::custom("gpt-5.7").is_reasoning_model());
1702        assert!(ChatModel::custom("gpt-5.7-codex").is_reasoning_model());
1703    }
1704
1705    // ========================================================================
1706    // Realtime models
1707    // ========================================================================
1708
1709    fn new_realtime_models() -> Vec<(RealtimeModel, &'static str)> {
1710        vec![
1711            (RealtimeModel::GptRealtime2_1, "gpt-realtime-2.1"),
1712            (RealtimeModel::GptRealtime2_1Mini, "gpt-realtime-2.1-mini"),
1713            (RealtimeModel::GptRealtime2, "gpt-realtime-2"),
1714            (RealtimeModel::GptRealtime1_5, "gpt-realtime-1.5"),
1715            (RealtimeModel::GptRealtimeTranslate, "gpt-realtime-translate"),
1716        ]
1717    }
1718
1719    #[test]
1720    fn test_new_realtime_models_as_str() {
1721        for (model, expected) in new_realtime_models() {
1722            assert_eq!(model.as_str(), expected, "Wrong model ID for {:?}", model);
1723        }
1724    }
1725
1726    #[test]
1727    fn test_new_realtime_models_from_str_roundtrip() {
1728        for (model, model_str) in new_realtime_models() {
1729            assert_eq!(RealtimeModel::from(model_str), model, "Failed to parse {}", model_str);
1730        }
1731    }
1732
1733    /// `RealtimeClient::new()` picks up `RealtimeModel::default()`, so the
1734    /// default must not drift when new models are added - that would silently
1735    /// change which model existing callers connect to.
1736    #[test]
1737    fn test_realtime_model_default_is_unchanged() {
1738        assert_eq!(RealtimeModel::default(), RealtimeModel::GptRealtime_2025_08_28);
1739    }
1740
1741    #[test]
1742    fn test_new_realtime_models_serialization_roundtrip() {
1743        for (model, model_str) in new_realtime_models() {
1744            let json = serde_json::to_string(&model).unwrap();
1745            assert_eq!(json, format!("\"{}\"", model_str), "Wrong serialization for {:?}", model);
1746            let deserialized: RealtimeModel = serde_json::from_str(&json).unwrap();
1747            assert_eq!(deserialized, model, "Serialization roundtrip failed for {:?}", model);
1748        }
1749    }
1750
1751    // ========================================================================
1752    // Models present in the live /v1/models listing but previously missing
1753    // from the enums. Classification verified empirically against the API
1754    // (August 2026): reasoning models reject `temperature`, search models
1755    // reject the whole sampling parameter set but expose no `reasoning` param.
1756    // ========================================================================
1757
1758    fn previously_missing_chat_models() -> Vec<(ChatModel, &'static str)> {
1759        vec![
1760            (ChatModel::Gpt5, "gpt-5"),
1761            (ChatModel::Gpt5Pro, "gpt-5-pro"),
1762            (ChatModel::O3Pro, "o3-pro"),
1763            (ChatModel::Gpt5SearchApi, "gpt-5-search-api"),
1764            (ChatModel::Gpt4oSearchPreview, "gpt-4o-search-preview"),
1765            (ChatModel::Gpt4oMiniSearchPreview, "gpt-4o-mini-search-preview"),
1766            (ChatModel::GptAudio, "gpt-audio"),
1767            (ChatModel::GptAudio1_5, "gpt-audio-1.5"),
1768            (ChatModel::GptAudioMini, "gpt-audio-mini"),
1769            (ChatModel::Gpt3_5Turbo16k, "gpt-3.5-turbo-16k"),
1770        ]
1771    }
1772
1773    #[test]
1774    fn test_previously_missing_chat_models_as_str() {
1775        for (model, expected) in previously_missing_chat_models() {
1776            assert_eq!(model.as_str(), expected, "Wrong model ID for {:?}", model);
1777        }
1778    }
1779
1780    #[test]
1781    fn test_previously_missing_chat_models_from_str_roundtrip() {
1782        for (model, model_str) in previously_missing_chat_models() {
1783            assert_eq!(ChatModel::from(model_str), model, "Failed to parse {}", model_str);
1784            assert_eq!(ChatModel::from(model_str).as_str(), model_str, "Roundtrip failed for {}", model_str);
1785        }
1786    }
1787
1788    #[test]
1789    fn test_previously_missing_chat_models_serialization_roundtrip() {
1790        for (model, model_str) in previously_missing_chat_models() {
1791            let json = serde_json::to_string(&model).unwrap();
1792            assert_eq!(json, format!("\"{}\"", model_str), "Wrong serialization for {:?}", model);
1793            let deserialized: ChatModel = serde_json::from_str(&json).unwrap();
1794            assert_eq!(deserialized, model, "Serialization roundtrip failed for {:?}", model);
1795        }
1796    }
1797
1798    /// `gpt-5`, `gpt-5-pro` and `o3-pro` reject `temperature`, so they carry
1799    /// the reasoning restrictions.
1800    #[test]
1801    fn test_gpt5_base_and_pro_models_are_reasoning() {
1802        for model in [ChatModel::Gpt5, ChatModel::Gpt5Pro, ChatModel::O3Pro] {
1803            assert!(model.is_reasoning_model(), "Expected {} to be a reasoning model", model.as_str());
1804            assert!(model.parameter_support().reasoning, "{} should support the reasoning parameter", model.as_str());
1805        }
1806    }
1807
1808    /// The audio chat models accept the standard sampling parameters.
1809    #[test]
1810    fn test_audio_chat_models_are_standard() {
1811        for model in [ChatModel::GptAudio, ChatModel::GptAudio1_5, ChatModel::GptAudioMini, ChatModel::Gpt3_5Turbo16k] {
1812            assert!(!model.is_reasoning_model(), "Expected {} to NOT be a reasoning model", model.as_str());
1813            let support = model.parameter_support();
1814            assert_eq!(support.temperature, ParameterRestriction::Any, "{} should accept any temperature", model.as_str());
1815            assert!(support.n_multiple, "{} should support n > 1", model.as_str());
1816        }
1817    }
1818
1819    /// Search models reject temperature/top_p/n/logprobs/penalties, but they
1820    /// are NOT reasoning models - they expose no `reasoning` parameter.
1821    #[test]
1822    fn test_search_models_reject_sampling_but_have_no_reasoning() {
1823        let search_models = [ChatModel::Gpt5SearchApi, ChatModel::Gpt4oSearchPreview, ChatModel::Gpt4oMiniSearchPreview];
1824
1825        for model in search_models {
1826            assert!(model.is_search_model(), "Expected {} to be a search model", model.as_str());
1827            assert!(!model.is_reasoning_model(), "Search model {} must not be classed as reasoning", model.as_str());
1828
1829            let support = model.parameter_support();
1830            assert_eq!(support.temperature, ParameterRestriction::FixedValue(1.0), "{} should reject custom temperature", model.as_str());
1831            assert_eq!(support.top_p, ParameterRestriction::FixedValue(1.0), "{} should reject custom top_p", model.as_str());
1832            assert_eq!(support.frequency_penalty, ParameterRestriction::FixedValue(0.0), "{} should reject frequency_penalty", model.as_str());
1833            assert_eq!(support.presence_penalty, ParameterRestriction::FixedValue(0.0), "{} should reject presence_penalty", model.as_str());
1834            assert!(!support.logprobs, "{} should reject logprobs", model.as_str());
1835            assert!(!support.n_multiple, "{} should reject n > 1", model.as_str());
1836            assert!(!support.reasoning, "{} exposes no reasoning parameter", model.as_str());
1837        }
1838    }
1839
1840    /// Non-search models must not be caught by the search classification.
1841    #[test]
1842    fn test_non_search_models_are_not_search_models() {
1843        for model in [ChatModel::Gpt5_6Sol, ChatModel::Gpt4oMini, ChatModel::O3Mini, ChatModel::Gpt5_2ChatLatest] {
1844            assert!(!model.is_search_model(), "Expected {} to NOT be a search model", model.as_str());
1845        }
1846    }
1847
1848    #[test]
1849    fn test_previously_missing_realtime_models() {
1850        for (model, expected) in [(RealtimeModel::GptRealtime, "gpt-realtime"), (RealtimeModel::GptRealtimeMini, "gpt-realtime-mini")] {
1851            assert_eq!(model.as_str(), expected, "Wrong model ID for {:?}", model);
1852            assert_eq!(RealtimeModel::from(expected), model, "Failed to parse {}", expected);
1853            let json = serde_json::to_string(&model).unwrap();
1854            assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", model);
1855        }
1856    }
1857
1858    #[test]
1859    fn test_legacy_fine_tuning_base_models() {
1860        for (model, expected) in [(FineTuningModel::Babbage002, "babbage-002"), (FineTuningModel::Davinci002, "davinci-002")] {
1861            assert_eq!(model.as_str(), expected, "Wrong model ID for {:?}", model);
1862            let json = serde_json::to_string(&model).unwrap();
1863            assert_eq!(json, format!("\"{}\"", expected), "Wrong serialization for {:?}", model);
1864        }
1865    }
1866}