Skip to main content

rai_sdk/
model.rs

1//! Typed model catalogs for every supported provider.
2//!
3//! [`Model`] is the provider-agnostic handle you pass to a client or request
4//! builder. It wraps one of the provider-specific catalogs — [`OpenAIModel`],
5//! [`AnthropicModel`], or [`OpenRouterModel`] — so the provider is always
6//! implied by the model you pick and can never be mismatched.
7//!
8//! Every catalog has a `Custom(String)` variant, so a model that shipped after
9//! this crate was released is still reachable without waiting for an update.
10//!
11//! # Examples
12//!
13//! ```no_run
14//! use rai_sdk::{AnthropicModel, Model, ProviderKind};
15//!
16//! // Convenience constructors for the common cases.
17//! let model = Model::gpt4o_mini();
18//! assert_eq!(model.as_str(), "gpt-4o-mini");
19//! assert_eq!(model.provider(), ProviderKind::OpenAI);
20//!
21//! // Or wrap a provider catalog entry directly.
22//! let claude = Model::Anthropic(AnthropicModel::ClaudeSonnet45);
23//! assert_eq!(claude.as_str(), "claude-sonnet-4-5");
24//!
25//! // Anything not in the catalog can still be named explicitly.
26//! let preview = Model::openai_custom("gpt-5-preview");
27//! assert_eq!(preview.as_str(), "gpt-5-preview");
28//! ```
29
30use serde::{Deserialize, Serialize};
31
32use crate::error::ProviderKind;
33
34/// Unified AI model selection across providers.
35///
36/// Each variant wraps a provider-specific model enum, which is what makes the
37/// provider unambiguous: picking a model also picks the API it is sent to.
38///
39/// Serializes as an internally tagged value with a `provider` tag and a `model`
40/// payload, so a selection can be round-tripped through configuration files.
41///
42/// # Examples
43///
44/// ```no_run
45/// use rai_sdk::{Model, OpenAIModel, ProviderKind};
46///
47/// let model = Model::OpenAI(OpenAIModel::Gpt5);
48/// assert_eq!(model.as_str(), "gpt-5");
49/// assert_eq!(model.provider(), ProviderKind::OpenAI);
50/// ```
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(tag = "provider", content = "model")]
53pub enum Model {
54    /// A model served by OpenAI's API.
55    OpenAI(OpenAIModel),
56    /// A model served by Anthropic's Messages API.
57    Anthropic(AnthropicModel),
58    /// A model served through OpenRouter's aggregating API.
59    OpenRouter(OpenRouterModel),
60    /// A model served by the client's configured OpenAI-compatible endpoint.
61    OpenAICompatible(OpenAICompatibleModel),
62}
63
64impl Model {
65    /// The wire identifier sent to the provider, e.g. `"gpt-4o-mini"`.
66    pub fn as_str(&self) -> &str {
67        match self {
68            Model::OpenAI(m) => m.as_str(),
69            Model::Anthropic(m) => m.as_str(),
70            Model::OpenRouter(m) => m.as_str(),
71            Model::OpenAICompatible(m) => m.as_str(),
72        }
73    }
74
75    /// The provider that will serve this model.
76    pub fn provider(&self) -> ProviderKind {
77        match self {
78            Model::OpenAI(_) => ProviderKind::OpenAI,
79            Model::Anthropic(_) => ProviderKind::Anthropic,
80            Model::OpenRouter(_) => ProviderKind::OpenRouter,
81            Model::OpenAICompatible(_) => ProviderKind::OpenAICompatible,
82        }
83    }
84
85    // ── OpenAI convenience constructors ──
86
87    /// Select [`OpenAIModel::Gpt4o`] (`gpt-4o`).
88    pub fn gpt4o() -> Self {
89        Model::OpenAI(OpenAIModel::Gpt4o)
90    }
91
92    /// Select [`OpenAIModel::Gpt4oMini`] (`gpt-4o-mini`).
93    pub fn gpt4o_mini() -> Self {
94        Model::OpenAI(OpenAIModel::Gpt4oMini)
95    }
96
97    /// Select [`OpenAIModel::Gpt4_1`] (`gpt-4.1`).
98    pub fn gpt4_1() -> Self {
99        Model::OpenAI(OpenAIModel::Gpt4_1)
100    }
101
102    /// Select [`OpenAIModel::O3Mini`] (`o3-mini`).
103    pub fn o3_mini() -> Self {
104        Model::OpenAI(OpenAIModel::O3Mini)
105    }
106
107    /// Select [`OpenAIModel::O3`] (`o3`).
108    pub fn o3() -> Self {
109        Model::OpenAI(OpenAIModel::O3)
110    }
111
112    /// Select [`OpenAIModel::O4Mini`] (`o4-mini`).
113    pub fn o4_mini() -> Self {
114        Model::OpenAI(OpenAIModel::O4Mini)
115    }
116
117    /// Select [`OpenAIModel::Gpt5`] (`gpt-5`).
118    pub fn gpt5() -> Self {
119        Model::OpenAI(OpenAIModel::Gpt5)
120    }
121
122    /// Select [`OpenAIModel::Gpt5Mini`] (`gpt-5-mini`).
123    pub fn gpt5_mini() -> Self {
124        Model::OpenAI(OpenAIModel::Gpt5Mini)
125    }
126
127    /// Select [`OpenAIModel::Gpt5Nano`] (`gpt-5-nano`).
128    pub fn gpt5_nano() -> Self {
129        Model::OpenAI(OpenAIModel::Gpt5Nano)
130    }
131
132    /// Select [`OpenAIModel::Gpt5Codex`] (`gpt-5-codex`).
133    pub fn gpt5_codex() -> Self {
134        Model::OpenAI(OpenAIModel::Gpt5Codex)
135    }
136
137    /// Select [`OpenAIModel::Gpt5_1`] (`gpt-5.1`).
138    pub fn gpt_5_1() -> Self {
139        Model::OpenAI(OpenAIModel::Gpt5_1)
140    }
141
142    /// Select [`OpenAIModel::Gpt5_2`] (`gpt-5.2`).
143    pub fn gpt_5_2() -> Self {
144        Model::OpenAI(OpenAIModel::Gpt5_2)
145    }
146
147    /// Select [`OpenAIModel::Gpt5_2Pro`] (`gpt-5.2-pro`).
148    pub fn gpt_5_2_pro() -> Self {
149        Model::OpenAI(OpenAIModel::Gpt5_2Pro)
150    }
151
152    /// Select [`OpenAIModel::Gpt5_3Chat`] (`gpt-5.3-chat`).
153    pub fn gpt_5_3_chat() -> Self {
154        Model::OpenAI(OpenAIModel::Gpt5_3Chat)
155    }
156
157    /// Select [`OpenAIModel::Gpt5_3Instant`] (`gpt-5.3-instant`).
158    pub fn gpt_5_3_instant() -> Self {
159        Model::OpenAI(OpenAIModel::Gpt5_3Instant)
160    }
161
162    /// Select [`OpenAIModel::Gpt5_4`] (`gpt-5.4`).
163    pub fn gpt_5_4() -> Self {
164        Model::OpenAI(OpenAIModel::Gpt5_4)
165    }
166
167    /// Select [`OpenAIModel::Gpt5_4Mini`] (`gpt-5.4-mini`).
168    pub fn gpt_5_4_mini() -> Self {
169        Model::OpenAI(OpenAIModel::Gpt5_4Mini)
170    }
171
172    /// Select [`OpenAIModel::Gpt5_4Nano`] (`gpt-5.4-nano`).
173    pub fn gpt_5_4_nano() -> Self {
174        Model::OpenAI(OpenAIModel::Gpt5_4Nano)
175    }
176
177    /// Select [`OpenAIModel::Gpt5_5`] (`gpt-5.5`).
178    pub fn gpt_5_5() -> Self {
179        Model::OpenAI(OpenAIModel::Gpt5_5)
180    }
181
182    // ── Anthropic convenience constructors ──
183
184    /// Select [`AnthropicModel::ClaudeFable5`] (`claude-fable-5`).
185    pub fn claude_fable_5() -> Self {
186        Model::Anthropic(AnthropicModel::ClaudeFable5)
187    }
188
189    /// Select [`AnthropicModel::ClaudeOpus48`] (`claude-opus-4-8`).
190    pub fn claude_opus_48() -> Self {
191        Model::Anthropic(AnthropicModel::ClaudeOpus48)
192    }
193
194    /// Select [`AnthropicModel::ClaudeOpus47`] (`claude-opus-4-7`).
195    pub fn claude_opus_47() -> Self {
196        Model::Anthropic(AnthropicModel::ClaudeOpus47)
197    }
198
199    /// Select [`AnthropicModel::ClaudeSonnet46`] (`claude-sonnet-4-6`).
200    pub fn claude_sonnet_46() -> Self {
201        Model::Anthropic(AnthropicModel::ClaudeSonnet46)
202    }
203
204    /// Select [`AnthropicModel::ClaudeOpus46`] (`claude-opus-4-6`).
205    pub fn claude_opus_46() -> Self {
206        Model::Anthropic(AnthropicModel::ClaudeOpus46)
207    }
208
209    /// Select [`AnthropicModel::ClaudeSonnet4`] (`claude-sonnet-4-0`).
210    pub fn claude_sonnet_4() -> Self {
211        Model::Anthropic(AnthropicModel::ClaudeSonnet4)
212    }
213
214    /// Select [`AnthropicModel::ClaudeOpus4`] (`claude-opus-4-0`).
215    pub fn claude_opus_4() -> Self {
216        Model::Anthropic(AnthropicModel::ClaudeOpus4)
217    }
218
219    /// Select [`AnthropicModel::ClaudeOpus41`] (`claude-opus-4-1`).
220    pub fn claude_opus_41() -> Self {
221        Model::Anthropic(AnthropicModel::ClaudeOpus41)
222    }
223
224    /// Select [`AnthropicModel::ClaudeSonnet45`] (`claude-sonnet-4-5`).
225    pub fn claude_sonnet_45() -> Self {
226        Model::Anthropic(AnthropicModel::ClaudeSonnet45)
227    }
228
229    /// Select [`AnthropicModel::ClaudeOpus45`] (`claude-opus-4-5`).
230    pub fn claude_opus_45() -> Self {
231        Model::Anthropic(AnthropicModel::ClaudeOpus45)
232    }
233
234    /// Select [`AnthropicModel::ClaudeHaiku45`] (`claude-haiku-4-5`).
235    pub fn claude_haiku_45() -> Self {
236        Model::Anthropic(AnthropicModel::ClaudeHaiku45)
237    }
238
239    /// Select [`AnthropicModel::Claude35Sonnet`] (`claude-3-5-sonnet-20241022`).
240    pub fn claude_35_sonnet() -> Self {
241        Model::Anthropic(AnthropicModel::Claude35Sonnet)
242    }
243
244    /// Select [`AnthropicModel::Claude35Haiku`] (`claude-3-5-haiku-20241022`).
245    pub fn claude_35_haiku() -> Self {
246        Model::Anthropic(AnthropicModel::Claude35Haiku)
247    }
248
249    /// Create a model from a custom OpenAI model string.
250    pub fn openai_custom(name: impl Into<String>) -> Self {
251        Model::OpenAI(OpenAIModel::Custom(name.into()))
252    }
253
254    /// Create a model from a custom Anthropic model string.
255    pub fn anthropic_custom(name: impl Into<String>) -> Self {
256        Model::Anthropic(AnthropicModel::Custom(name.into()))
257    }
258
259    /// Create a model from a custom OpenRouter model string.
260    pub fn openrouter_custom(name: impl Into<String>) -> Self {
261        Model::OpenRouter(OpenRouterModel::Custom(name.into()))
262    }
263
264    /// Name a model served by the client's OpenAI-compatible endpoint.
265    ///
266    /// There is no catalog to pick from: the identifier is whatever the
267    /// endpoint calls the model, such as `llama3.1:8b` on Ollama or a
268    /// filesystem path on vLLM. Which endpoint serves it is a property of the
269    /// client — see
270    /// [`ClientBuilder::openai_compatible_base_url`](crate::ClientBuilder::openai_compatible_base_url).
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// use rai_sdk::{Model, ProviderKind};
276    ///
277    /// let model = Model::openai_compatible("llama3.1:8b");
278    /// assert_eq!(model.as_str(), "llama3.1:8b");
279    /// assert_eq!(model.provider(), ProviderKind::OpenAICompatible);
280    /// ```
281    pub fn openai_compatible(name: impl Into<String>) -> Self {
282        Model::OpenAICompatible(OpenAICompatibleModel::new(name))
283    }
284
285    /// Select [`OpenRouterModel::Auto`] (`openrouter/auto`).
286    pub fn openrouter_auto() -> Self {
287        Model::OpenRouter(OpenRouterModel::Auto)
288    }
289
290    /// Select [`OpenRouterModel::Gpt5`] (`openai/gpt-5`).
291    pub fn openrouter_gpt5() -> Self {
292        Model::OpenRouter(OpenRouterModel::Gpt5)
293    }
294
295    /// Select [`OpenRouterModel::ClaudeSonnet4_5`] (`anthropic/claude-sonnet-4.5`).
296    pub fn openrouter_claude_sonnet_4_5() -> Self {
297        Model::OpenRouter(OpenRouterModel::ClaudeSonnet4_5)
298    }
299
300    /// Select [`OpenRouterModel::Gemini25Flash`] (`google/gemini-2.5-flash`).
301    pub fn openrouter_gemini_25_flash() -> Self {
302        Model::OpenRouter(OpenRouterModel::Gemini25Flash)
303    }
304
305    /// Select [`OpenRouterModel::DeepseekR1`] (`deepseek/deepseek-r1`).
306    pub fn openrouter_deepseek_r1() -> Self {
307        Model::OpenRouter(OpenRouterModel::DeepseekR1)
308    }
309
310    /// Select [`OpenRouterModel::Qwen3Coder`] (`qwen/qwen3-coder`).
311    pub fn openrouter_qwen3_coder() -> Self {
312        Model::OpenRouter(OpenRouterModel::Qwen3Coder)
313    }
314}
315
316/// OpenAI model variants.
317///
318/// Use [`OpenAIModel::Custom`] for any model string not listed here.
319#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
320pub enum OpenAIModel {
321    /// GPT-5.5 — latest flagship model for complex reasoning and coding
322    Gpt5_5,
323    /// GPT-5.4 — more affordable frontier model for coding and professional work
324    Gpt5_4,
325    /// GPT-5.4 Mini — stronger mini model for coding, computer use, and subagents
326    Gpt5_4Mini,
327    /// GPT-5.4 Nano — lowest-latency GPT-5.4 variant
328    Gpt5_4Nano,
329    /// GPT-5.3 Instant — real-time high-accuracy model
330    Gpt5_3Instant,
331    /// GPT-5.3 Chat — conversational tuning of GPT-5.3
332    Gpt5_3Chat,
333    /// GPT-5.2 Pro — highest-capability GPT-5.2 tier
334    Gpt5_2Pro,
335    /// GPT-5.2
336    Gpt5_2,
337    /// GPT-5.1
338    Gpt5_1,
339    /// GPT-5 — flagship GPT-5 model
340    Gpt5,
341    /// GPT-5 Mini — faster, cheaper GPT-5
342    Gpt5Mini,
343    /// GPT-5 Nano — smallest, fastest GPT-5
344    Gpt5Nano,
345    /// GPT-5 Codex — agentic coding-optimized GPT-5
346    Gpt5Codex,
347    /// GPT-4.1 — high-capability long-context model
348    Gpt4_1,
349    /// GPT-4o — multimodal model (previous generation)
350    Gpt4o,
351    /// GPT-4o Mini
352    Gpt4oMini,
353    /// GPT-4 Turbo
354    Gpt4Turbo,
355    /// O1 Preview — reasoning model
356    O1Preview,
357    /// O1 Mini
358    O1Mini,
359    /// O3 Mini
360    O3Mini,
361    /// O3
362    O3,
363    /// O4 Mini
364    O4Mini,
365    /// Custom model name
366    Custom(String),
367}
368
369impl OpenAIModel {
370    /// The model identifier OpenAI expects in the request body.
371    pub fn as_str(&self) -> &str {
372        match self {
373            // GPT-5 family
374            Self::Gpt5_5 => "gpt-5.5",
375            Self::Gpt5_4 => "gpt-5.4",
376            Self::Gpt5_4Mini => "gpt-5.4-mini",
377            Self::Gpt5_4Nano => "gpt-5.4-nano",
378            Self::Gpt5_3Instant => "gpt-5.3-instant",
379            Self::Gpt5_3Chat => "gpt-5.3-chat",
380            Self::Gpt5_2Pro => "gpt-5.2-pro",
381            Self::Gpt5_2 => "gpt-5.2",
382            Self::Gpt5_1 => "gpt-5.1",
383            Self::Gpt5 => "gpt-5",
384            Self::Gpt5Mini => "gpt-5-mini",
385            Self::Gpt5Nano => "gpt-5-nano",
386            Self::Gpt5Codex => "gpt-5-codex",
387            // GPT-4 family
388            Self::Gpt4_1 => "gpt-4.1",
389            Self::Gpt4o => "gpt-4o",
390            Self::Gpt4oMini => "gpt-4o-mini",
391            Self::Gpt4Turbo => "gpt-4-turbo",
392            // Reasoning (o-series)
393            Self::O1Preview => "o1-preview",
394            Self::O1Mini => "o1-mini",
395            Self::O3Mini => "o3-mini",
396            Self::O3 => "o3",
397            Self::O4Mini => "o4-mini",
398            Self::Custom(s) => s,
399        }
400    }
401
402    /// Whether this model is a reasoning model (o-series) that doesn't support temperature.
403    pub fn is_reasoning_model(&self) -> bool {
404        matches!(
405            self,
406            Self::O1Preview | Self::O1Mini | Self::O3Mini | Self::O3 | Self::O4Mini
407        )
408    }
409}
410
411/// Anthropic Claude model variants.
412///
413/// Use [`AnthropicModel::Custom`] for any model string not listed here.
414#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
415pub enum AnthropicModel {
416    /// Claude Fable 5 — most capable widely released Claude model
417    ClaudeFable5,
418    /// Claude Opus 4.8 — latest Opus-tier model for complex reasoning and coding
419    ClaudeOpus48,
420    /// Claude Opus 4.7 — legacy Opus-tier model
421    ClaudeOpus47,
422    /// Claude Opus 4.6 — legacy Opus-tier model with 1M context
423    ClaudeOpus46,
424    /// Claude Sonnet 4.6 — current best balance of intelligence, speed and cost
425    ClaudeSonnet46,
426    /// Claude Sonnet 4.5
427    ClaudeSonnet45,
428    /// Claude Sonnet 4
429    ClaudeSonnet4,
430    /// Claude Opus 4.5
431    ClaudeOpus45,
432    /// Claude Opus 4.1 — enhanced Claude 4 Opus
433    ClaudeOpus41,
434    /// Claude Opus 4 — legacy Claude 4 model
435    ClaudeOpus4,
436    /// Claude Haiku 4.5
437    ClaudeHaiku45,
438    /// Claude 3.5 Sonnet
439    Claude35Sonnet,
440    /// Claude 3.5 Haiku
441    Claude35Haiku,
442    /// Claude 3 Opus
443    Claude3Opus,
444    /// Claude 3 Sonnet
445    Claude3Sonnet,
446    /// Claude 3 Haiku
447    Claude3Haiku,
448    /// Custom model name
449    Custom(String),
450}
451
452impl AnthropicModel {
453    /// The model identifier Anthropic expects in the request body.
454    pub fn as_str(&self) -> &str {
455        match self {
456            // Claude 5 models
457            Self::ClaudeFable5 => "claude-fable-5",
458            // Claude 4.8 models
459            Self::ClaudeOpus48 => "claude-opus-4-8",
460            // Claude 4.7 models
461            Self::ClaudeOpus47 => "claude-opus-4-7",
462            // Claude 4.6 models
463            Self::ClaudeOpus46 => "claude-opus-4-6",
464            Self::ClaudeSonnet46 => "claude-sonnet-4-6",
465            // Claude 4.5 models
466            Self::ClaudeSonnet45 => "claude-sonnet-4-5",
467            Self::ClaudeOpus45 => "claude-opus-4-5",
468            Self::ClaudeHaiku45 => "claude-haiku-4-5",
469            // Claude 4 models
470            Self::ClaudeOpus41 => "claude-opus-4-1",
471            Self::ClaudeSonnet4 => "claude-sonnet-4-0",
472            Self::ClaudeOpus4 => "claude-opus-4-0",
473            // Claude 3.5 models
474            Self::Claude35Sonnet => "claude-3-5-sonnet-20241022",
475            Self::Claude35Haiku => "claude-3-5-haiku-20241022",
476            // Claude 3 models
477            Self::Claude3Opus => "claude-3-opus-20240229",
478            Self::Claude3Sonnet => "claude-3-sonnet-20240229",
479            Self::Claude3Haiku => "claude-3-haiku-20240307",
480            Self::Custom(s) => s,
481        }
482    }
483}
484
485/// OpenRouter model variants.
486///
487/// OpenRouter addresses models as `vendor/model` strings. The variants below
488/// cover the commonly used catalog entries; anything else can be named with
489/// [`OpenRouterModel::Custom`], which is also what
490/// [`FromStr`](std::str::FromStr) falls back to for unrecognized input.
491#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
492pub enum OpenRouterModel {
493    /// OpenRouter auto-router alias.
494    Auto,
495    /// OpenRouter free-tier alias.
496    Free,
497
498    // OpenAI models
499    /// OpenRouter model `openai/gpt-5`.
500    Gpt5,
501    /// OpenRouter model `openai/gpt-5-mini`.
502    Gpt5Mini,
503    /// OpenRouter model `openai/gpt-5-nano`.
504    Gpt5Nano,
505    /// OpenRouter model `openai/gpt-5-codex`.
506    Gpt5Codex,
507    /// OpenRouter model `openai/gpt-5.1`.
508    Gpt5_1,
509    /// OpenRouter model `openai/gpt-5.2`.
510    Gpt5_2,
511    /// OpenRouter model `openai/gpt-5.2-pro`.
512    Gpt5_2Pro,
513    /// OpenRouter model `openai/gpt-5.3-chat`.
514    Gpt5_3Chat,
515    /// OpenRouter model `openai/gpt-5.4`.
516    Gpt5_4,
517    /// OpenRouter model `openai/gpt-5.4-mini`.
518    Gpt5_4Mini,
519    /// OpenRouter model `openai/gpt-5.4-nano`.
520    Gpt5_4Nano,
521    /// OpenRouter model `openai/gpt-5.4-pro`.
522    Gpt5_4Pro,
523    /// OpenRouter model `openai/gpt-5.5`.
524    Gpt5_5,
525    /// OpenRouter model `openai/gpt-5.5-pro`.
526    Gpt5_5Pro,
527    /// OpenRouter model `openai/gpt-4.1`.
528    Gpt4_1,
529    /// OpenRouter model `openai/gpt-4o`.
530    Gpt4o,
531    /// OpenRouter model `openai/o3`.
532    O3,
533    /// OpenRouter model `openai/o3-pro`.
534    O3Pro,
535    /// OpenRouter model `openai/o3-deep-research`.
536    O3DeepResearch,
537    /// OpenRouter model `openai/o4-mini`.
538    O4Mini,
539    /// OpenRouter model `openai/gpt-oss-120b`.
540    GptOss120b,
541
542    // Anthropic models
543    /// OpenRouter model `anthropic/claude-fable-5`.
544    ClaudeFable5,
545    /// OpenRouter model `anthropic/claude-sonnet-4`.
546    ClaudeSonnet4,
547    /// OpenRouter model `anthropic/claude-sonnet-4.5`.
548    ClaudeSonnet4_5,
549    /// OpenRouter model `anthropic/claude-opus-4.1`.
550    ClaudeOpus4_1,
551    /// OpenRouter model `anthropic/claude-opus-4.5`.
552    ClaudeOpus4_5,
553    /// OpenRouter model `anthropic/claude-opus-4.6`.
554    ClaudeOpus4_6,
555    /// OpenRouter model `anthropic/claude-opus-4.6-fast`.
556    ClaudeOpus4_6Fast,
557    /// OpenRouter model `anthropic/claude-opus-4.7`.
558    ClaudeOpus4_7,
559    /// OpenRouter model `anthropic/claude-opus-4.7-fast`.
560    ClaudeOpus4_7Fast,
561    /// OpenRouter model `anthropic/claude-opus-4.8`.
562    ClaudeOpus4_8,
563    /// OpenRouter model `anthropic/claude-opus-4.8-fast`.
564    ClaudeOpus4_8Fast,
565    /// OpenRouter model `anthropic/claude-sonnet-4.6`.
566    ClaudeSonnet4_6,
567    /// OpenRouter model `anthropic/claude-haiku-4.5`.
568    ClaudeHaiku4_5,
569    /// OpenRouter model `anthropic/claude-3.7-sonnet`.
570    Claude3_7Sonnet,
571
572    // Google models
573    /// OpenRouter model `google/gemini-3.5-flash`.
574    Gemini35Flash,
575    /// OpenRouter model `google/gemini-3.1-pro-preview`.
576    Gemini31ProPreview,
577    /// OpenRouter model `google/gemini-3.1-pro-preview-customtools`.
578    Gemini31ProPreviewCustomTools,
579    /// OpenRouter model `google/gemini-3.1-flash-lite`.
580    Gemini31FlashLite,
581    /// OpenRouter model `google/gemini-3.1-flash-lite-preview`.
582    Gemini31FlashLitePreview,
583    /// OpenRouter model `google/gemini-3.1-flash-image-preview`.
584    Gemini31FlashImagePreview,
585    /// OpenRouter model `google/gemini-3-pro-image-preview`.
586    Gemini3ProImagePreview,
587    /// OpenRouter model `google/gemini-3-flash-preview`.
588    Gemini3FlashPreview,
589    /// OpenRouter model `google/gemini-2.5-pro`.
590    Gemini25Pro,
591    /// OpenRouter model `google/gemini-2.5-flash`.
592    Gemini25Flash,
593    /// OpenRouter model `google/gemini-2.5-flash-image`.
594    Gemini25FlashImage,
595
596    // xAI models
597    /// OpenRouter model `x-ai/grok-4.3`.
598    Grok4_3,
599    /// OpenRouter model `x-ai/grok-4.20`.
600    Grok4_20,
601    /// OpenRouter model `x-ai/grok-4.20-multi-agent`.
602    Grok4_20MultiAgent,
603    /// OpenRouter model `x-ai/grok-build-0.1`.
604    GrokBuild0_1,
605    /// OpenRouter model `x-ai/grok-4`.
606    Grok4,
607    /// OpenRouter model `x-ai/grok-4-fast`.
608    Grok4Fast,
609    /// OpenRouter model `x-ai/grok-4.1-fast`.
610    Grok4_1Fast,
611    /// OpenRouter model `x-ai/grok-code-fast-1`.
612    GrokCodeFast1,
613
614    // Meta / Llama models
615    /// OpenRouter model `meta-llama/llama-4-maverick`.
616    Llama4Maverick,
617    /// OpenRouter model `meta-llama/llama-4-scout`.
618    Llama4Scout,
619    /// OpenRouter model `meta-llama/llama-3.3-70b-instruct`.
620    Llama3_3_70bInstruct,
621    /// OpenRouter model `meta-llama/llama-3.2-11b-vision-instruct`.
622    Llama3_2_11bVisionInstruct,
623
624    // Qwen models
625    /// OpenRouter model `qwen/qwen3-max`.
626    Qwen3Max,
627    /// OpenRouter model `qwen/qwen3-max-thinking`.
628    Qwen3MaxThinking,
629    /// OpenRouter model `qwen/qwen3-coder`.
630    Qwen3Coder,
631    /// OpenRouter model `qwen/qwen3-coder-plus`.
632    Qwen3CoderPlus,
633    /// OpenRouter model `qwen/qwen3-235b-a22b`.
634    Qwen3_235bA22b,
635    /// OpenRouter model `qwen/qwen3-vl-235b-a22b-instruct`.
636    Qwen3Vl235bA22bInstruct,
637    /// OpenRouter model `qwen/qwen3-vl-235b-a22b-thinking`.
638    Qwen3Vl235bA22bThinking,
639    /// OpenRouter model `qwen/qwen3.7-max`.
640    Qwen3_7Max,
641    /// OpenRouter model `qwen/qwen3.7-plus`.
642    Qwen3_7Plus,
643    /// OpenRouter model `qwen/qwen3.6-max-preview`.
644    Qwen3_6MaxPreview,
645    /// OpenRouter model `qwen/qwen3.6-plus`.
646    Qwen3_6Plus,
647    /// OpenRouter model `qwen/qwen3.6-flash`.
648    Qwen3_6Flash,
649
650    // DeepSeek models
651    /// OpenRouter model `deepseek/deepseek-chat-v3.1`.
652    DeepseekChatV3_1,
653    /// OpenRouter model `deepseek/deepseek-r1`.
654    DeepseekR1,
655    /// OpenRouter model `deepseek/deepseek-v3.2`.
656    DeepseekV3_2,
657    /// OpenRouter model `deepseek/deepseek-v4-flash`.
658    DeepseekV4Flash,
659    /// OpenRouter model `deepseek/deepseek-v4-pro`.
660    DeepseekV4Pro,
661
662    // Mistral models
663    /// OpenRouter model `mistralai/mistral-large`.
664    MistralLarge,
665    /// OpenRouter model `mistralai/mistral-medium-3.1`.
666    MistralMedium3_1,
667    /// OpenRouter model `mistralai/codestral-2508`.
668    Codestral2508,
669    /// OpenRouter model `mistralai/devstral-medium`.
670    DevstralMedium,
671    /// OpenRouter model `mistralai/pixtral-large-2411`.
672    PixtralLarge2411,
673    /// OpenRouter model `mistralai/mistral-large-2512`.
674    MistralLarge2512,
675    /// OpenRouter model `mistralai/mistral-medium-3-5`.
676    MistralMedium3_5,
677    /// OpenRouter model `mistralai/devstral-2512`.
678    Devstral2512,
679    /// OpenRouter model `mistralai/ministral-14b-2512`.
680    Ministral14b2512,
681
682    // Perplexity models
683    /// OpenRouter model `perplexity/sonar-pro`.
684    SonarPro,
685    /// OpenRouter model `perplexity/sonar-reasoning-pro`.
686    SonarReasoningPro,
687    /// OpenRouter model `perplexity/sonar-deep-research`.
688    SonarDeepResearch,
689
690    // Cohere models
691    /// OpenRouter model `cohere/command-a`.
692    CommandA,
693
694    // Moonshot AI models
695    /// OpenRouter model `moonshotai/kimi-k2.5`.
696    KimiK2_5,
697    /// OpenRouter model `moonshotai/kimi-k2-thinking`.
698    KimiK2Thinking,
699    /// OpenRouter model `moonshotai/kimi-k2.6`.
700    KimiK2_6,
701    /// OpenRouter model `moonshotai/kimi-k2.7-code`.
702    KimiK2_7Code,
703
704    // Z.ai models
705    /// OpenRouter model `z-ai/glm-5`.
706    Glm5,
707    /// OpenRouter model `z-ai/glm-5.1`.
708    Glm5_1,
709    /// OpenRouter model `z-ai/glm-5-turbo`.
710    Glm5Turbo,
711    /// OpenRouter model `z-ai/glm-4.7`.
712    Glm4_7,
713
714    // Xiaomi models
715    /// OpenRouter model `xiaomi/mimo-v2-omni`.
716    MimoV2Omni,
717    /// OpenRouter model `xiaomi/mimo-v2-flash`.
718    MimoV2Flash,
719    /// OpenRouter model `xiaomi/mimo-v2.5`.
720    MimoV2_5,
721    /// OpenRouter model `xiaomi/mimo-v2.5-pro`.
722    MimoV2_5Pro,
723
724    /// Any other OpenRouter `vendor/model` string.
725    Custom(String),
726}
727
728impl OpenRouterModel {
729    /// The `vendor/model` identifier OpenRouter expects in the request body.
730    pub fn as_str(&self) -> &str {
731        match self {
732            Self::Auto => "openrouter/auto",
733            Self::Free => "openrouter/free",
734            Self::Gpt5 => "openai/gpt-5",
735            Self::Gpt5Mini => "openai/gpt-5-mini",
736            Self::Gpt5Nano => "openai/gpt-5-nano",
737            Self::Gpt5Codex => "openai/gpt-5-codex",
738            Self::Gpt5_1 => "openai/gpt-5.1",
739            Self::Gpt5_2 => "openai/gpt-5.2",
740            Self::Gpt5_2Pro => "openai/gpt-5.2-pro",
741            Self::Gpt5_3Chat => "openai/gpt-5.3-chat",
742            Self::Gpt5_4 => "openai/gpt-5.4",
743            Self::Gpt5_4Mini => "openai/gpt-5.4-mini",
744            Self::Gpt5_4Nano => "openai/gpt-5.4-nano",
745            Self::Gpt5_4Pro => "openai/gpt-5.4-pro",
746            Self::Gpt5_5 => "openai/gpt-5.5",
747            Self::Gpt5_5Pro => "openai/gpt-5.5-pro",
748            Self::Gpt4_1 => "openai/gpt-4.1",
749            Self::Gpt4o => "openai/gpt-4o",
750            Self::O3 => "openai/o3",
751            Self::O3Pro => "openai/o3-pro",
752            Self::O3DeepResearch => "openai/o3-deep-research",
753            Self::O4Mini => "openai/o4-mini",
754            Self::GptOss120b => "openai/gpt-oss-120b",
755            Self::ClaudeFable5 => "anthropic/claude-fable-5",
756            Self::ClaudeSonnet4 => "anthropic/claude-sonnet-4",
757            Self::ClaudeSonnet4_5 => "anthropic/claude-sonnet-4.5",
758            Self::ClaudeOpus4_1 => "anthropic/claude-opus-4.1",
759            Self::ClaudeOpus4_5 => "anthropic/claude-opus-4.5",
760            Self::ClaudeOpus4_6 => "anthropic/claude-opus-4.6",
761            Self::ClaudeOpus4_6Fast => "anthropic/claude-opus-4.6-fast",
762            Self::ClaudeOpus4_7 => "anthropic/claude-opus-4.7",
763            Self::ClaudeOpus4_7Fast => "anthropic/claude-opus-4.7-fast",
764            Self::ClaudeOpus4_8 => "anthropic/claude-opus-4.8",
765            Self::ClaudeOpus4_8Fast => "anthropic/claude-opus-4.8-fast",
766            Self::ClaudeSonnet4_6 => "anthropic/claude-sonnet-4.6",
767            Self::ClaudeHaiku4_5 => "anthropic/claude-haiku-4.5",
768            Self::Claude3_7Sonnet => "anthropic/claude-3.7-sonnet",
769            Self::Gemini35Flash => "google/gemini-3.5-flash",
770            Self::Gemini31ProPreview => "google/gemini-3.1-pro-preview",
771            Self::Gemini31ProPreviewCustomTools => "google/gemini-3.1-pro-preview-customtools",
772            Self::Gemini31FlashLite => "google/gemini-3.1-flash-lite",
773            Self::Gemini31FlashLitePreview => "google/gemini-3.1-flash-lite-preview",
774            Self::Gemini31FlashImagePreview => "google/gemini-3.1-flash-image-preview",
775            Self::Gemini3ProImagePreview => "google/gemini-3-pro-image-preview",
776            Self::Gemini3FlashPreview => "google/gemini-3-flash-preview",
777            Self::Gemini25Pro => "google/gemini-2.5-pro",
778            Self::Gemini25Flash => "google/gemini-2.5-flash",
779            Self::Gemini25FlashImage => "google/gemini-2.5-flash-image",
780            Self::Grok4_3 => "x-ai/grok-4.3",
781            Self::Grok4_20 => "x-ai/grok-4.20",
782            Self::Grok4_20MultiAgent => "x-ai/grok-4.20-multi-agent",
783            Self::GrokBuild0_1 => "x-ai/grok-build-0.1",
784            Self::Grok4 => "x-ai/grok-4",
785            Self::Grok4Fast => "x-ai/grok-4-fast",
786            Self::Grok4_1Fast => "x-ai/grok-4.1-fast",
787            Self::GrokCodeFast1 => "x-ai/grok-code-fast-1",
788            Self::Llama4Maverick => "meta-llama/llama-4-maverick",
789            Self::Llama4Scout => "meta-llama/llama-4-scout",
790            Self::Llama3_3_70bInstruct => "meta-llama/llama-3.3-70b-instruct",
791            Self::Llama3_2_11bVisionInstruct => "meta-llama/llama-3.2-11b-vision-instruct",
792            Self::Qwen3Max => "qwen/qwen3-max",
793            Self::Qwen3MaxThinking => "qwen/qwen3-max-thinking",
794            Self::Qwen3Coder => "qwen/qwen3-coder",
795            Self::Qwen3CoderPlus => "qwen/qwen3-coder-plus",
796            Self::Qwen3_235bA22b => "qwen/qwen3-235b-a22b",
797            Self::Qwen3Vl235bA22bInstruct => "qwen/qwen3-vl-235b-a22b-instruct",
798            Self::Qwen3Vl235bA22bThinking => "qwen/qwen3-vl-235b-a22b-thinking",
799            Self::Qwen3_7Max => "qwen/qwen3.7-max",
800            Self::Qwen3_7Plus => "qwen/qwen3.7-plus",
801            Self::Qwen3_6MaxPreview => "qwen/qwen3.6-max-preview",
802            Self::Qwen3_6Plus => "qwen/qwen3.6-plus",
803            Self::Qwen3_6Flash => "qwen/qwen3.6-flash",
804            Self::DeepseekChatV3_1 => "deepseek/deepseek-chat-v3.1",
805            Self::DeepseekR1 => "deepseek/deepseek-r1",
806            Self::DeepseekV3_2 => "deepseek/deepseek-v3.2",
807            Self::DeepseekV4Flash => "deepseek/deepseek-v4-flash",
808            Self::DeepseekV4Pro => "deepseek/deepseek-v4-pro",
809            Self::MistralLarge => "mistralai/mistral-large",
810            Self::MistralMedium3_1 => "mistralai/mistral-medium-3.1",
811            Self::Codestral2508 => "mistralai/codestral-2508",
812            Self::DevstralMedium => "mistralai/devstral-medium",
813            Self::PixtralLarge2411 => "mistralai/pixtral-large-2411",
814            Self::MistralLarge2512 => "mistralai/mistral-large-2512",
815            Self::MistralMedium3_5 => "mistralai/mistral-medium-3-5",
816            Self::Devstral2512 => "mistralai/devstral-2512",
817            Self::Ministral14b2512 => "mistralai/ministral-14b-2512",
818            Self::SonarPro => "perplexity/sonar-pro",
819            Self::SonarReasoningPro => "perplexity/sonar-reasoning-pro",
820            Self::SonarDeepResearch => "perplexity/sonar-deep-research",
821            Self::CommandA => "cohere/command-a",
822            Self::KimiK2_5 => "moonshotai/kimi-k2.5",
823            Self::KimiK2Thinking => "moonshotai/kimi-k2-thinking",
824            Self::KimiK2_6 => "moonshotai/kimi-k2.6",
825            Self::KimiK2_7Code => "moonshotai/kimi-k2.7-code",
826            Self::Glm5 => "z-ai/glm-5",
827            Self::Glm5_1 => "z-ai/glm-5.1",
828            Self::Glm5Turbo => "z-ai/glm-5-turbo",
829            Self::Glm4_7 => "z-ai/glm-4.7",
830            Self::MimoV2Omni => "xiaomi/mimo-v2-omni",
831            Self::MimoV2Flash => "xiaomi/mimo-v2-flash",
832            Self::MimoV2_5 => "xiaomi/mimo-v2.5",
833            Self::MimoV2_5Pro => "xiaomi/mimo-v2.5-pro",
834            Self::Custom(s) => s,
835        }
836    }
837}
838
839impl std::str::FromStr for OpenRouterModel {
840    type Err = std::convert::Infallible;
841
842    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
843        let model = match value {
844            "openrouter/auto" => Self::Auto,
845            "openrouter/free" => Self::Free,
846            "openai/gpt-5" => Self::Gpt5,
847            "openai/gpt-5-mini" => Self::Gpt5Mini,
848            "openai/gpt-5-nano" => Self::Gpt5Nano,
849            "openai/gpt-5-codex" => Self::Gpt5Codex,
850            "openai/gpt-5.1" => Self::Gpt5_1,
851            "openai/gpt-5.2" => Self::Gpt5_2,
852            "openai/gpt-5.2-pro" => Self::Gpt5_2Pro,
853            "openai/gpt-5.3-chat" => Self::Gpt5_3Chat,
854            "openai/gpt-5.4" => Self::Gpt5_4,
855            "openai/gpt-5.4-mini" => Self::Gpt5_4Mini,
856            "openai/gpt-5.4-nano" => Self::Gpt5_4Nano,
857            "openai/gpt-5.4-pro" => Self::Gpt5_4Pro,
858            "openai/gpt-5.5" => Self::Gpt5_5,
859            "openai/gpt-5.5-pro" => Self::Gpt5_5Pro,
860            "openai/gpt-4.1" => Self::Gpt4_1,
861            "openai/gpt-4o" => Self::Gpt4o,
862            "openai/o3" => Self::O3,
863            "openai/o3-pro" => Self::O3Pro,
864            "openai/o3-deep-research" => Self::O3DeepResearch,
865            "openai/o4-mini" => Self::O4Mini,
866            "openai/gpt-oss-120b" => Self::GptOss120b,
867            "anthropic/claude-fable-5" => Self::ClaudeFable5,
868            "anthropic/claude-sonnet-4" => Self::ClaudeSonnet4,
869            "anthropic/claude-sonnet-4.5" => Self::ClaudeSonnet4_5,
870            "anthropic/claude-opus-4.1" => Self::ClaudeOpus4_1,
871            "anthropic/claude-opus-4.5" => Self::ClaudeOpus4_5,
872            "anthropic/claude-opus-4.6" => Self::ClaudeOpus4_6,
873            "anthropic/claude-opus-4.6-fast" => Self::ClaudeOpus4_6Fast,
874            "anthropic/claude-opus-4.7" => Self::ClaudeOpus4_7,
875            "anthropic/claude-opus-4.7-fast" => Self::ClaudeOpus4_7Fast,
876            "anthropic/claude-opus-4.8" => Self::ClaudeOpus4_8,
877            "anthropic/claude-opus-4.8-fast" => Self::ClaudeOpus4_8Fast,
878            "anthropic/claude-sonnet-4.6" => Self::ClaudeSonnet4_6,
879            "anthropic/claude-haiku-4.5" => Self::ClaudeHaiku4_5,
880            "anthropic/claude-3.7-sonnet" => Self::Claude3_7Sonnet,
881            "google/gemini-3.5-flash" => Self::Gemini35Flash,
882            "google/gemini-3.1-pro-preview" => Self::Gemini31ProPreview,
883            "google/gemini-3.1-pro-preview-customtools" => Self::Gemini31ProPreviewCustomTools,
884            "google/gemini-3.1-flash-lite" => Self::Gemini31FlashLite,
885            "google/gemini-3.1-flash-lite-preview" => Self::Gemini31FlashLitePreview,
886            "google/gemini-3.1-flash-image-preview" => Self::Gemini31FlashImagePreview,
887            "google/gemini-3-pro-image-preview" => Self::Gemini3ProImagePreview,
888            "google/gemini-3-flash-preview" => Self::Gemini3FlashPreview,
889            "google/gemini-2.5-pro" => Self::Gemini25Pro,
890            "google/gemini-2.5-flash" => Self::Gemini25Flash,
891            "google/gemini-2.5-flash-image" => Self::Gemini25FlashImage,
892            "x-ai/grok-4.3" => Self::Grok4_3,
893            "x-ai/grok-4.20" => Self::Grok4_20,
894            "x-ai/grok-4.20-multi-agent" => Self::Grok4_20MultiAgent,
895            "x-ai/grok-build-0.1" => Self::GrokBuild0_1,
896            "x-ai/grok-4" => Self::Grok4,
897            "x-ai/grok-4-fast" => Self::Grok4Fast,
898            "x-ai/grok-4.1-fast" => Self::Grok4_1Fast,
899            "x-ai/grok-code-fast-1" => Self::GrokCodeFast1,
900            "meta-llama/llama-4-maverick" => Self::Llama4Maverick,
901            "meta-llama/llama-4-scout" => Self::Llama4Scout,
902            "meta-llama/llama-3.3-70b-instruct" => Self::Llama3_3_70bInstruct,
903            "meta-llama/llama-3.2-11b-vision-instruct" => Self::Llama3_2_11bVisionInstruct,
904            "qwen/qwen3-max" => Self::Qwen3Max,
905            "qwen/qwen3-max-thinking" => Self::Qwen3MaxThinking,
906            "qwen/qwen3-coder" => Self::Qwen3Coder,
907            "qwen/qwen3-coder-plus" => Self::Qwen3CoderPlus,
908            "qwen/qwen3-235b-a22b" => Self::Qwen3_235bA22b,
909            "qwen/qwen3-vl-235b-a22b-instruct" => Self::Qwen3Vl235bA22bInstruct,
910            "qwen/qwen3-vl-235b-a22b-thinking" => Self::Qwen3Vl235bA22bThinking,
911            "qwen/qwen3.7-max" => Self::Qwen3_7Max,
912            "qwen/qwen3.7-plus" => Self::Qwen3_7Plus,
913            "qwen/qwen3.6-max-preview" => Self::Qwen3_6MaxPreview,
914            "qwen/qwen3.6-plus" => Self::Qwen3_6Plus,
915            "qwen/qwen3.6-flash" => Self::Qwen3_6Flash,
916            "deepseek/deepseek-chat-v3.1" => Self::DeepseekChatV3_1,
917            "deepseek/deepseek-r1" => Self::DeepseekR1,
918            "deepseek/deepseek-v3.2" => Self::DeepseekV3_2,
919            "deepseek/deepseek-v4-flash" => Self::DeepseekV4Flash,
920            "deepseek/deepseek-v4-pro" => Self::DeepseekV4Pro,
921            "mistralai/mistral-large" => Self::MistralLarge,
922            "mistralai/mistral-medium-3.1" => Self::MistralMedium3_1,
923            "mistralai/codestral-2508" => Self::Codestral2508,
924            "mistralai/devstral-medium" => Self::DevstralMedium,
925            "mistralai/pixtral-large-2411" => Self::PixtralLarge2411,
926            "mistralai/mistral-large-2512" => Self::MistralLarge2512,
927            "mistralai/mistral-medium-3-5" => Self::MistralMedium3_5,
928            "mistralai/devstral-2512" => Self::Devstral2512,
929            "mistralai/ministral-14b-2512" => Self::Ministral14b2512,
930            "perplexity/sonar-pro" => Self::SonarPro,
931            "perplexity/sonar-reasoning-pro" => Self::SonarReasoningPro,
932            "perplexity/sonar-deep-research" => Self::SonarDeepResearch,
933            "cohere/command-a" => Self::CommandA,
934            "moonshotai/kimi-k2.5" => Self::KimiK2_5,
935            "moonshotai/kimi-k2-thinking" => Self::KimiK2Thinking,
936            "moonshotai/kimi-k2.6" => Self::KimiK2_6,
937            "moonshotai/kimi-k2.7-code" => Self::KimiK2_7Code,
938            "z-ai/glm-5" => Self::Glm5,
939            "z-ai/glm-5.1" => Self::Glm5_1,
940            "z-ai/glm-5-turbo" => Self::Glm5Turbo,
941            "z-ai/glm-4.7" => Self::Glm4_7,
942            "xiaomi/mimo-v2-omni" => Self::MimoV2Omni,
943            "xiaomi/mimo-v2-flash" => Self::MimoV2Flash,
944            "xiaomi/mimo-v2.5" => Self::MimoV2_5,
945            "xiaomi/mimo-v2.5-pro" => Self::MimoV2_5Pro,
946            other => Self::Custom(other.to_string()),
947        };
948
949        Ok(model)
950    }
951}
952
953/// A model identifier served by an OpenAI-compatible endpoint.
954///
955/// The other catalogs in this module enumerate a vendor's published models and
956/// keep a `Custom(String)` escape hatch. An OpenAI-compatible endpoint has no
957/// published catalog — the identifiers are whatever the operator loaded, like
958/// `llama3.1:8b`, `qwen2.5-coder:14b`, or `/models/Mistral-7B-Instruct` — so
959/// this type is the escape hatch and nothing else.
960///
961/// # Examples
962///
963/// ```
964/// use rai_sdk::OpenAICompatibleModel;
965///
966/// let model = OpenAICompatibleModel::new("llama3.1:8b");
967/// assert_eq!(model.as_str(), "llama3.1:8b");
968/// assert_eq!(model, "llama3.1:8b".parse().unwrap());
969/// ```
970#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
971#[serde(transparent)]
972pub struct OpenAICompatibleModel(String);
973
974impl OpenAICompatibleModel {
975    /// Name a model exactly as the endpoint knows it.
976    pub fn new(name: impl Into<String>) -> Self {
977        Self(name.into())
978    }
979
980    /// The model identifier sent in the request body.
981    pub fn as_str(&self) -> &str {
982        &self.0
983    }
984}
985
986impl std::fmt::Display for OpenAICompatibleModel {
987    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
988        f.write_str(&self.0)
989    }
990}
991
992impl std::str::FromStr for OpenAICompatibleModel {
993    type Err = std::convert::Infallible;
994
995    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
996        Ok(Self::new(value))
997    }
998}
999
1000impl From<String> for OpenAICompatibleModel {
1001    fn from(value: String) -> Self {
1002        Self::new(value)
1003    }
1004}
1005
1006impl From<&str> for OpenAICompatibleModel {
1007    fn from(value: &str) -> Self {
1008        Self::new(value)
1009    }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014    use super::*;
1015
1016    #[test]
1017    fn model_provider_mapping() {
1018        assert_eq!(Model::gpt4o_mini().provider(), ProviderKind::OpenAI);
1019        assert_eq!(
1020            Model::claude_sonnet_45().provider(),
1021            ProviderKind::Anthropic
1022        );
1023        assert_eq!(Model::gpt_5_3_instant().provider(), ProviderKind::OpenAI);
1024        assert_eq!(
1025            Model::claude_sonnet_46().provider(),
1026            ProviderKind::Anthropic
1027        );
1028        assert_eq!(Model::claude_opus_46().provider(), ProviderKind::Anthropic);
1029        assert_eq!(
1030            Model::openrouter_auto().provider(),
1031            ProviderKind::OpenRouter
1032        );
1033    }
1034
1035    #[test]
1036    fn model_string_identifiers() {
1037        assert_eq!(Model::gpt4o_mini().as_str(), "gpt-4o-mini");
1038        assert_eq!(Model::claude_sonnet_45().as_str(), "claude-sonnet-4-5");
1039        assert_eq!(Model::o3_mini().as_str(), "o3-mini");
1040        assert_eq!(Model::gpt_5_3_instant().as_str(), "gpt-5.3-instant");
1041        assert_eq!(Model::claude_sonnet_46().as_str(), "claude-sonnet-4-6");
1042        assert_eq!(Model::claude_opus_46().as_str(), "claude-opus-4-6");
1043        assert_eq!(Model::openrouter_auto().as_str(), "openrouter/auto");
1044        assert_eq!(Model::openrouter_gpt5().as_str(), "openai/gpt-5");
1045        assert_eq!(Model::openrouter_qwen3_coder().as_str(), "qwen/qwen3-coder");
1046    }
1047
1048    #[test]
1049    fn openrouter_model_from_str_handles_known_and_custom_models() {
1050        assert_eq!(
1051            "openrouter/auto".parse::<OpenRouterModel>(),
1052            Ok(OpenRouterModel::Auto)
1053        );
1054        assert_eq!(
1055            "acme/custom-model".parse::<OpenRouterModel>(),
1056            Ok(OpenRouterModel::Custom("acme/custom-model".to_string()))
1057        );
1058    }
1059
1060    #[test]
1061    fn reasoning_model_detection() {
1062        assert!(OpenAIModel::O3.is_reasoning_model());
1063        assert!(OpenAIModel::O4Mini.is_reasoning_model());
1064        assert!(!OpenAIModel::Gpt4oMini.is_reasoning_model());
1065    }
1066
1067    #[test]
1068    fn openai_gpt5_family_string_identifiers() {
1069        let cases = [
1070            (OpenAIModel::Gpt5_5, "gpt-5.5"),
1071            (OpenAIModel::Gpt5_4, "gpt-5.4"),
1072            (OpenAIModel::Gpt5_4Mini, "gpt-5.4-mini"),
1073            (OpenAIModel::Gpt5_4Nano, "gpt-5.4-nano"),
1074            (OpenAIModel::Gpt5, "gpt-5"),
1075            (OpenAIModel::Gpt5Mini, "gpt-5-mini"),
1076            (OpenAIModel::Gpt5Nano, "gpt-5-nano"),
1077            (OpenAIModel::Gpt5Codex, "gpt-5-codex"),
1078            (OpenAIModel::Gpt5_1, "gpt-5.1"),
1079            (OpenAIModel::Gpt5_2, "gpt-5.2"),
1080            (OpenAIModel::Gpt5_2Pro, "gpt-5.2-pro"),
1081            (OpenAIModel::Gpt5_3Instant, "gpt-5.3-instant"),
1082            (OpenAIModel::Gpt5_3Chat, "gpt-5.3-chat"),
1083            (OpenAIModel::Gpt4_1, "gpt-4.1"),
1084        ];
1085
1086        for (model, expected) in cases {
1087            assert_eq!(model.as_str(), expected);
1088            // GPT-5 / GPT-4.1 are not treated as o-series reasoning models.
1089            assert!(!model.is_reasoning_model());
1090        }
1091    }
1092
1093    #[test]
1094    fn model_gpt5_constructors_map_to_provider() {
1095        assert_eq!(Model::gpt5().as_str(), "gpt-5");
1096        assert_eq!(Model::gpt5().provider(), ProviderKind::OpenAI);
1097        assert_eq!(Model::gpt_5_5().as_str(), "gpt-5.5");
1098        assert_eq!(Model::gpt_5_4().as_str(), "gpt-5.4");
1099        assert_eq!(Model::gpt_5_4_mini().as_str(), "gpt-5.4-mini");
1100        assert_eq!(Model::gpt_5_4_nano().as_str(), "gpt-5.4-nano");
1101        assert_eq!(Model::gpt4_1().as_str(), "gpt-4.1");
1102    }
1103
1104    #[test]
1105    fn anthropic_latest_models_string_identifiers() {
1106        let cases = [
1107            (AnthropicModel::ClaudeFable5, "claude-fable-5"),
1108            (AnthropicModel::ClaudeOpus48, "claude-opus-4-8"),
1109            (AnthropicModel::ClaudeOpus47, "claude-opus-4-7"),
1110            (AnthropicModel::ClaudeOpus41, "claude-opus-4-1"),
1111        ];
1112
1113        for (model, expected) in cases {
1114            assert_eq!(model.as_str(), expected);
1115        }
1116
1117        assert_eq!(Model::claude_fable_5().as_str(), "claude-fable-5");
1118        assert_eq!(Model::claude_opus_48().as_str(), "claude-opus-4-8");
1119        assert_eq!(Model::claude_opus_47().as_str(), "claude-opus-4-7");
1120        assert_eq!(Model::claude_opus_41().provider(), ProviderKind::Anthropic);
1121    }
1122
1123    #[test]
1124    fn openrouter_latest_catalog_models_round_trip() {
1125        let cases = [
1126            (OpenRouterModel::Gpt5_5, "openai/gpt-5.5"),
1127            (OpenRouterModel::ClaudeFable5, "anthropic/claude-fable-5"),
1128            (OpenRouterModel::ClaudeOpus4_8, "anthropic/claude-opus-4.8"),
1129            (OpenRouterModel::Gemini35Flash, "google/gemini-3.5-flash"),
1130            (OpenRouterModel::Grok4_3, "x-ai/grok-4.3"),
1131            (OpenRouterModel::Qwen3_7Max, "qwen/qwen3.7-max"),
1132            (OpenRouterModel::DeepseekV4Pro, "deepseek/deepseek-v4-pro"),
1133            (OpenRouterModel::KimiK2_7Code, "moonshotai/kimi-k2.7-code"),
1134        ];
1135
1136        for (model, expected) in cases {
1137            assert_eq!(model.as_str(), expected);
1138            assert_eq!(expected.parse::<OpenRouterModel>(), Ok(model));
1139        }
1140    }
1141}