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