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