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