Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:57:08.714Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

models.rs

1538 lines · 56.5 KB · rust
1use serde::{Deserialize, Serialize};
2use strum::EnumIter;
3
4#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
5#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
6pub enum BedrockAdaptiveThinkingEffort {
7    Low,
8    Medium,
9    #[default]
10    High,
11    XHigh,
12    Max,
13}
14
15impl BedrockAdaptiveThinkingEffort {
16    pub fn as_str(&self) -> &'static str {
17        match self {
18            Self::Low => "low",
19            Self::Medium => "medium",
20            Self::High => "high",
21            Self::XHigh => "xhigh",
22            Self::Max => "max",
23        }
24    }
25}
26
27#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
28#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
29pub enum BedrockModelMode {
30    #[default]
31    Default,
32    Thinking {
33        budget_tokens: Option<u64>,
34    },
35    AdaptiveThinking {
36        effort: BedrockAdaptiveThinkingEffort,
37    },
38}
39
40#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
41#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
42pub struct BedrockModelCacheConfiguration {
43    pub max_cache_anchors: usize,
44    pub min_total_token: u64,
45}
46
47#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
48#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
49pub enum ConverseModel {
50    // Anthropic Claude 4+ models
51    #[serde(
52        rename = "claude-fable-5",
53        alias = "claude-fable-5-latest",
54        alias = "claude-fable-5-thinking",
55        alias = "claude-fable-5-thinking-latest"
56    )]
57    ClaudeFable5,
58    #[serde(
59        rename = "claude-opus-5",
60        alias = "claude-opus-5-latest",
61        alias = "claude-opus-5-thinking",
62        alias = "claude-opus-5-thinking-latest"
63    )]
64    ClaudeOpus5,
65    #[serde(
66        rename = "claude-opus-4-8",
67        alias = "claude-opus-4-8-latest",
68        alias = "claude-opus-4-8-thinking",
69        alias = "claude-opus-4-8-thinking-latest"
70    )]
71    ClaudeOpus4_8,
72    #[serde(
73        rename = "claude-opus-4-7",
74        alias = "claude-opus-4-7-latest",
75        alias = "claude-opus-4-7-thinking",
76        alias = "claude-opus-4-7-thinking-latest"
77    )]
78    ClaudeOpus4_7,
79    #[serde(
80        rename = "claude-opus-4-6",
81        alias = "claude-opus-4-6-latest",
82        alias = "claude-opus-4-6-thinking",
83        alias = "claude-opus-4-6-thinking-latest"
84    )]
85    ClaudeOpus4_6,
86    #[serde(
87        rename = "claude-opus-4-5",
88        alias = "claude-opus-4-5-latest",
89        alias = "claude-opus-4-5-thinking",
90        alias = "claude-opus-4-5-thinking-latest"
91    )]
92    ClaudeOpus4_5,
93    #[serde(
94        rename = "claude-opus-4-1",
95        alias = "claude-opus-4-1-latest",
96        alias = "claude-opus-4-1-thinking",
97        alias = "claude-opus-4-1-thinking-latest"
98    )]
99    ClaudeOpus4_1,
100    #[serde(
101        rename = "claude-sonnet-5",
102        alias = "claude-sonnet-5-latest",
103        alias = "claude-sonnet-5-thinking",
104        alias = "claude-sonnet-5-thinking-latest"
105    )]
106    ClaudeSonnet5,
107    #[serde(
108        rename = "claude-sonnet-4-6",
109        alias = "claude-sonnet-4-6-latest",
110        alias = "claude-sonnet-4-6-thinking",
111        alias = "claude-sonnet-4-6-thinking-latest"
112    )]
113    ClaudeSonnet4_6,
114    #[default]
115    #[serde(
116        rename = "claude-sonnet-4-5",
117        alias = "claude-sonnet-4-5-latest",
118        alias = "claude-sonnet-4-5-thinking",
119        alias = "claude-sonnet-4-5-thinking-latest"
120    )]
121    ClaudeSonnet4_5,
122    #[serde(
123        rename = "claude-sonnet-4",
124        alias = "claude-sonnet-4-latest",
125        alias = "claude-sonnet-4-thinking",
126        alias = "claude-sonnet-4-thinking-latest"
127    )]
128    ClaudeSonnet4,
129    #[serde(rename = "claude-haiku-4-5", alias = "claude-haiku-4-5-latest")]
130    ClaudeHaiku4_5,
131
132    // Meta Llama 4 models
133    #[serde(rename = "llama-4-scout-17b")]
134    Llama4Scout17B,
135    #[serde(rename = "llama-4-maverick-17b")]
136    Llama4Maverick17B,
137
138    // Google Gemma 3 models
139    #[serde(rename = "gemma-3-4b")]
140    Gemma3_4B,
141    #[serde(rename = "gemma-3-12b")]
142    Gemma3_12B,
143    #[serde(rename = "gemma-3-27b")]
144    Gemma3_27B,
145
146    // Mistral models
147    #[serde(rename = "magistral-small")]
148    MagistralSmall,
149    #[serde(rename = "mistral-large-3")]
150    MistralLarge3,
151    #[serde(rename = "pixtral-large")]
152    PixtralLarge,
153    #[serde(rename = "devstral-2-123b")]
154    Devstral2_123B,
155    #[serde(rename = "ministral-14b")]
156    Ministral14B,
157
158    // Qwen models
159    #[serde(rename = "qwen3-32b")]
160    Qwen3_32B,
161    #[serde(rename = "qwen3-vl-235b")]
162    Qwen3VL235B,
163    #[serde(rename = "qwen3-235b")]
164    Qwen3_235B,
165    #[serde(rename = "qwen3-next-80b")]
166    Qwen3Next80B,
167    #[serde(rename = "qwen3-coder-30b")]
168    Qwen3Coder30B,
169    #[serde(rename = "qwen3-coder-next")]
170    Qwen3CoderNext,
171    #[serde(rename = "qwen3-coder-480b")]
172    Qwen3Coder480B,
173
174    // Amazon Nova models
175    #[serde(rename = "nova-lite")]
176    NovaLite,
177    #[serde(rename = "nova-pro")]
178    NovaPro,
179    #[serde(rename = "nova-premier")]
180    NovaPremier,
181    #[serde(rename = "nova-2-lite")]
182    Nova2Lite,
183
184    // OpenAI GPT OSS models
185    #[serde(rename = "gpt-oss-20b")]
186    GptOss20B,
187    #[serde(rename = "gpt-oss-120b")]
188    GptOss120B,
189
190    // NVIDIA Nemotron models
191    #[serde(rename = "nemotron-super-3-120b")]
192    NemotronSuper3_120B,
193    #[serde(rename = "nemotron-nano-3-30b")]
194    NemotronNano3_30B,
195
196    // MiniMax models
197    #[serde(rename = "minimax-m2")]
198    MiniMaxM2,
199    #[serde(rename = "minimax-m2-1")]
200    MiniMaxM2_1,
201    #[serde(rename = "minimax-m2-5")]
202    MiniMaxM2_5,
203
204    // Z.AI GLM models
205    #[serde(rename = "glm-5")]
206    GLM5,
207    #[serde(rename = "glm-4-7")]
208    GLM4_7,
209    #[serde(rename = "glm-4-7-flash")]
210    GLM4_7Flash,
211
212    // Moonshot models
213    #[serde(rename = "kimi-k2-thinking")]
214    KimiK2Thinking,
215    #[serde(rename = "kimi-k2-5")]
216    KimiK2_5,
217
218    // DeepSeek models
219    #[serde(rename = "deepseek-r1")]
220    DeepSeekR1,
221    #[serde(rename = "deepseek-v3")]
222    DeepSeekV3_1,
223    #[serde(rename = "deepseek-v3-2")]
224    DeepSeekV3_2,
225
226    #[serde(rename = "custom")]
227    Custom {
228        name: String,
229        max_tokens: u64,
230        display_name: Option<String>,
231        max_output_tokens: Option<u64>,
232        default_temperature: Option<f32>,
233        cache_configuration: Option<BedrockModelCacheConfiguration>,
234    },
235}
236
237impl ConverseModel {
238    pub fn default_fast(_region: &str) -> Self {
239        Self::ClaudeHaiku4_5
240    }
241
242    pub fn from_id(id: &str) -> anyhow::Result<Self> {
243        if id.starts_with("claude-fable-5") {
244            Ok(Self::ClaudeFable5)
245        } else if id.starts_with("claude-opus-5") {
246            Ok(Self::ClaudeOpus5)
247        } else if id.starts_with("claude-opus-4-8") {
248            Ok(Self::ClaudeOpus4_8)
249        } else if id.starts_with("claude-opus-4-7") {
250            Ok(Self::ClaudeOpus4_7)
251        } else if id.starts_with("claude-opus-4-6") {
252            Ok(Self::ClaudeOpus4_6)
253        } else if id.starts_with("claude-opus-4-5") {
254            Ok(Self::ClaudeOpus4_5)
255        } else if id.starts_with("claude-opus-4-1") {
256            Ok(Self::ClaudeOpus4_1)
257        } else if id.starts_with("claude-sonnet-5") {
258            Ok(Self::ClaudeSonnet5)
259        } else if id.starts_with("claude-sonnet-4-6") {
260            Ok(Self::ClaudeSonnet4_6)
261        } else if id.starts_with("claude-sonnet-4-5") {
262            Ok(Self::ClaudeSonnet4_5)
263        } else if id.starts_with("claude-sonnet-4") {
264            Ok(Self::ClaudeSonnet4)
265        } else if id.starts_with("claude-haiku-4-5") {
266            Ok(Self::ClaudeHaiku4_5)
267        } else {
268            anyhow::bail!("invalid model id {id}");
269        }
270    }
271
272    pub fn id(&self) -> &str {
273        match self {
274            Self::ClaudeFable5 => "claude-fable-5",
275            Self::ClaudeOpus5 => "claude-opus-5",
276            Self::ClaudeOpus4_8 => "claude-opus-4-8",
277            Self::ClaudeOpus4_7 => "claude-opus-4-7",
278            Self::ClaudeOpus4_6 => "claude-opus-4-6",
279            Self::ClaudeOpus4_5 => "claude-opus-4-5",
280            Self::ClaudeOpus4_1 => "claude-opus-4-1",
281            Self::ClaudeSonnet5 => "claude-sonnet-5",
282            Self::ClaudeSonnet4_6 => "claude-sonnet-4-6",
283            Self::ClaudeSonnet4_5 => "claude-sonnet-4-5",
284            Self::ClaudeSonnet4 => "claude-sonnet-4",
285            Self::ClaudeHaiku4_5 => "claude-haiku-4-5",
286            Self::Llama4Scout17B => "llama-4-scout-17b",
287            Self::Llama4Maverick17B => "llama-4-maverick-17b",
288            Self::Gemma3_4B => "gemma-3-4b",
289            Self::Gemma3_12B => "gemma-3-12b",
290            Self::Gemma3_27B => "gemma-3-27b",
291            Self::MagistralSmall => "magistral-small",
292            Self::MistralLarge3 => "mistral-large-3",
293            Self::PixtralLarge => "pixtral-large",
294            Self::Devstral2_123B => "devstral-2-123b",
295            Self::Ministral14B => "ministral-14b",
296            Self::Qwen3_32B => "qwen3-32b",
297            Self::Qwen3VL235B => "qwen3-vl-235b",
298            Self::Qwen3_235B => "qwen3-235b",
299            Self::Qwen3Next80B => "qwen3-next-80b",
300            Self::Qwen3Coder30B => "qwen3-coder-30b",
301            Self::Qwen3CoderNext => "qwen3-coder-next",
302            Self::Qwen3Coder480B => "qwen3-coder-480b",
303            Self::NovaLite => "nova-lite",
304            Self::NovaPro => "nova-pro",
305            Self::NovaPremier => "nova-premier",
306            Self::Nova2Lite => "nova-2-lite",
307            Self::GptOss20B => "gpt-oss-20b",
308            Self::GptOss120B => "gpt-oss-120b",
309            Self::NemotronSuper3_120B => "nemotron-super-3-120b",
310            Self::NemotronNano3_30B => "nemotron-nano-3-30b",
311            Self::MiniMaxM2 => "minimax-m2",
312            Self::MiniMaxM2_1 => "minimax-m2-1",
313            Self::MiniMaxM2_5 => "minimax-m2-5",
314            Self::GLM5 => "glm-5",
315            Self::GLM4_7 => "glm-4-7",
316            Self::GLM4_7Flash => "glm-4-7-flash",
317            Self::KimiK2Thinking => "kimi-k2-thinking",
318            Self::KimiK2_5 => "kimi-k2-5",
319            Self::DeepSeekR1 => "deepseek-r1",
320            Self::DeepSeekV3_1 => "deepseek-v3",
321            Self::DeepSeekV3_2 => "deepseek-v3-2",
322            Self::Custom { name, .. } => name,
323        }
324    }
325
326    pub fn request_id(&self) -> &str {
327        match self {
328            Self::ClaudeFable5 => "anthropic.claude-fable-5",
329            Self::ClaudeOpus5 => "anthropic.claude-opus-5",
330            Self::ClaudeOpus4_8 => "anthropic.claude-opus-4-8",
331            Self::ClaudeOpus4_7 => "anthropic.claude-opus-4-7",
332            Self::ClaudeOpus4_6 => "anthropic.claude-opus-4-6-v1",
333            Self::ClaudeOpus4_5 => "anthropic.claude-opus-4-5-20251101-v1:0",
334            Self::ClaudeOpus4_1 => "anthropic.claude-opus-4-1-20250805-v1:0",
335            Self::ClaudeSonnet5 => "anthropic.claude-sonnet-5",
336            Self::ClaudeSonnet4_6 => "anthropic.claude-sonnet-4-6",
337            Self::ClaudeSonnet4_5 => "anthropic.claude-sonnet-4-5-20250929-v1:0",
338            Self::ClaudeSonnet4 => "anthropic.claude-sonnet-4-20250514-v1:0",
339            Self::ClaudeHaiku4_5 => "anthropic.claude-haiku-4-5-20251001-v1:0",
340            Self::Llama4Scout17B => "meta.llama4-scout-17b-instruct-v1:0",
341            Self::Llama4Maverick17B => "meta.llama4-maverick-17b-instruct-v1:0",
342            Self::Gemma3_4B => "google.gemma-3-4b-it",
343            Self::Gemma3_12B => "google.gemma-3-12b-it",
344            Self::Gemma3_27B => "google.gemma-3-27b-it",
345            Self::MagistralSmall => "mistral.magistral-small-2509",
346            Self::MistralLarge3 => "mistral.mistral-large-3-675b-instruct",
347            Self::PixtralLarge => "mistral.pixtral-large-2502-v1:0",
348            Self::Devstral2_123B => "mistral.devstral-2-123b",
349            Self::Ministral14B => "mistral.ministral-3-14b-instruct",
350            Self::Qwen3VL235B => "qwen.qwen3-vl-235b-a22b",
351            Self::Qwen3_32B => "qwen.qwen3-32b-v1:0",
352            Self::Qwen3_235B => "qwen.qwen3-235b-a22b-2507-v1:0",
353            Self::Qwen3Next80B => "qwen.qwen3-next-80b-a3b",
354            Self::Qwen3Coder30B => "qwen.qwen3-coder-30b-a3b-v1:0",
355            Self::Qwen3CoderNext => "qwen.qwen3-coder-next",
356            Self::Qwen3Coder480B => "qwen.qwen3-coder-480b-a35b-v1:0",
357            Self::NovaLite => "amazon.nova-lite-v1:0",
358            Self::NovaPro => "amazon.nova-pro-v1:0",
359            Self::NovaPremier => "amazon.nova-premier-v1:0",
360            Self::Nova2Lite => "amazon.nova-2-lite-v1:0",
361            Self::GptOss20B => "openai.gpt-oss-20b-1:0",
362            Self::GptOss120B => "openai.gpt-oss-120b-1:0",
363            Self::NemotronSuper3_120B => "nvidia.nemotron-super-3-120b",
364            Self::NemotronNano3_30B => "nvidia.nemotron-nano-3-30b",
365            Self::MiniMaxM2 => "minimax.minimax-m2",
366            Self::MiniMaxM2_1 => "minimax.minimax-m2.1",
367            Self::MiniMaxM2_5 => "minimax.minimax-m2.5",
368            Self::GLM5 => "zai.glm-5",
369            Self::GLM4_7 => "zai.glm-4.7",
370            Self::GLM4_7Flash => "zai.glm-4.7-flash",
371            Self::KimiK2Thinking => "moonshot.kimi-k2-thinking",
372            Self::KimiK2_5 => "moonshotai.kimi-k2.5",
373            Self::DeepSeekR1 => "deepseek.r1-v1:0",
374            Self::DeepSeekV3_1 => "deepseek.v3-v1:0",
375            Self::DeepSeekV3_2 => "deepseek.v3.2",
376            Self::Custom { name, .. } => name,
377        }
378    }
379
380    pub fn display_name(&self) -> &str {
381        match self {
382            Self::ClaudeFable5 => "Claude Fable 5",
383            Self::ClaudeOpus5 => "Claude Opus 5",
384            Self::ClaudeOpus4_8 => "Claude Opus 4.8",
385            Self::ClaudeOpus4_7 => "Claude Opus 4.7",
386            Self::ClaudeOpus4_6 => "Claude Opus 4.6",
387            Self::ClaudeOpus4_5 => "Claude Opus 4.5",
388            Self::ClaudeOpus4_1 => "Claude Opus 4.1",
389            Self::ClaudeSonnet5 => "Claude Sonnet 5",
390            Self::ClaudeSonnet4_6 => "Claude Sonnet 4.6",
391            Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
392            Self::ClaudeSonnet4 => "Claude Sonnet 4",
393            Self::ClaudeHaiku4_5 => "Claude Haiku 4.5",
394            Self::Llama4Scout17B => "Llama 4 Scout 17B",
395            Self::Llama4Maverick17B => "Llama 4 Maverick 17B",
396            Self::Gemma3_4B => "Gemma 3 4B",
397            Self::Gemma3_12B => "Gemma 3 12B",
398            Self::Gemma3_27B => "Gemma 3 27B",
399            Self::MagistralSmall => "Magistral Small",
400            Self::MistralLarge3 => "Mistral Large 3",
401            Self::PixtralLarge => "Pixtral Large",
402            Self::Devstral2_123B => "Devstral 2 123B",
403            Self::Ministral14B => "Ministral 14B",
404            Self::Qwen3VL235B => "Qwen3 VL 235B",
405            Self::Qwen3_32B => "Qwen3 32B",
406            Self::Qwen3_235B => "Qwen3 235B",
407            Self::Qwen3Next80B => "Qwen3 Next 80B",
408            Self::Qwen3Coder30B => "Qwen3 Coder 30B",
409            Self::Qwen3CoderNext => "Qwen3 Coder Next",
410            Self::Qwen3Coder480B => "Qwen3 Coder 480B",
411            Self::NovaLite => "Amazon Nova Lite",
412            Self::NovaPro => "Amazon Nova Pro",
413            Self::NovaPremier => "Amazon Nova Premier",
414            Self::Nova2Lite => "Amazon Nova 2 Lite",
415            Self::GptOss20B => "GPT OSS 20B",
416            Self::GptOss120B => "GPT OSS 120B",
417            Self::NemotronSuper3_120B => "Nemotron Super 3 120B",
418            Self::NemotronNano3_30B => "Nemotron Nano 3 30B",
419            Self::MiniMaxM2 => "MiniMax M2",
420            Self::MiniMaxM2_1 => "MiniMax M2.1",
421            Self::MiniMaxM2_5 => "MiniMax M2.5",
422            Self::GLM5 => "GLM 5",
423            Self::GLM4_7 => "GLM 4.7",
424            Self::GLM4_7Flash => "GLM 4.7 Flash",
425            Self::KimiK2Thinking => "Kimi K2 Thinking",
426            Self::KimiK2_5 => "Kimi K2.5",
427            Self::DeepSeekR1 => "DeepSeek R1",
428            Self::DeepSeekV3_1 => "DeepSeek V3.1",
429            Self::DeepSeekV3_2 => "DeepSeek V3.2",
430            Self::Custom {
431                display_name, name, ..
432            } => display_name.as_deref().unwrap_or(name.as_str()),
433        }
434    }
435
436    pub fn max_token_count(&self) -> u64 {
437        match self {
438            Self::ClaudeFable5
439            | Self::ClaudeOpus5
440            | Self::ClaudeOpus4_8
441            | Self::ClaudeOpus4_7
442            | Self::ClaudeOpus4_6
443            | Self::ClaudeOpus4_5
444            | Self::ClaudeSonnet5
445            | Self::ClaudeSonnet4_6
446            | Self::ClaudeSonnet4_5
447            | Self::ClaudeSonnet4
448            | Self::ClaudeHaiku4_5 => 1_000_000,
449            Self::ClaudeOpus4_1 => 200_000,
450            Self::Llama4Scout17B | Self::Llama4Maverick17B => 128_000,
451            Self::Gemma3_4B | Self::Gemma3_12B | Self::Gemma3_27B => 128_000,
452            Self::MagistralSmall | Self::MistralLarge3 | Self::PixtralLarge => 128_000,
453            Self::Devstral2_123B | Self::Ministral14B => 256_000,
454            Self::Qwen3_32B
455            | Self::Qwen3VL235B
456            | Self::Qwen3_235B
457            | Self::Qwen3Next80B
458            | Self::Qwen3Coder30B
459            | Self::Qwen3CoderNext
460            | Self::Qwen3Coder480B => 128_000,
461            Self::NovaLite | Self::NovaPro => 300_000,
462            Self::NovaPremier => 1_000_000,
463            Self::Nova2Lite => 300_000,
464            Self::GptOss20B | Self::GptOss120B => 128_000,
465            Self::NemotronSuper3_120B | Self::NemotronNano3_30B => 262_000,
466            Self::MiniMaxM2 | Self::MiniMaxM2_1 | Self::MiniMaxM2_5 => 196_000,
467            Self::GLM5 | Self::GLM4_7 | Self::GLM4_7Flash => 203_000,
468            Self::KimiK2Thinking | Self::KimiK2_5 => 128_000,
469            Self::DeepSeekR1 | Self::DeepSeekV3_1 | Self::DeepSeekV3_2 => 128_000,
470            Self::Custom { max_tokens, .. } => *max_tokens,
471        }
472    }
473
474    pub fn max_output_tokens(&self) -> u64 {
475        match self {
476            Self::ClaudeFable5
477            | Self::ClaudeOpus5
478            | Self::ClaudeOpus4_8
479            | Self::ClaudeOpus4_7
480            | Self::ClaudeOpus4_6
481            | Self::ClaudeSonnet5 => 128_000,
482            Self::ClaudeOpus4_5
483            | Self::ClaudeSonnet4_6
484            | Self::ClaudeSonnet4_5
485            | Self::ClaudeSonnet4
486            | Self::ClaudeHaiku4_5 => 64_000,
487            Self::ClaudeOpus4_1 => 32_000,
488            Self::Llama4Scout17B
489            | Self::Llama4Maverick17B
490            | Self::Gemma3_4B
491            | Self::Gemma3_12B
492            | Self::Gemma3_27B
493            | Self::MagistralSmall
494            | Self::MistralLarge3
495            | Self::PixtralLarge => 8_192,
496            Self::Devstral2_123B | Self::Ministral14B => 131_000,
497            Self::Qwen3_32B
498            | Self::Qwen3VL235B
499            | Self::Qwen3_235B
500            | Self::Qwen3Next80B
501            | Self::Qwen3Coder30B
502            | Self::Qwen3CoderNext
503            | Self::Qwen3Coder480B => 8_192,
504            Self::NovaLite | Self::NovaPro | Self::NovaPremier | Self::Nova2Lite => 5_000,
505            Self::GptOss20B | Self::GptOss120B => 16_000,
506            Self::NemotronSuper3_120B | Self::NemotronNano3_30B => 131_000,
507            Self::MiniMaxM2 | Self::MiniMaxM2_1 | Self::MiniMaxM2_5 => 98_000,
508            Self::GLM5 | Self::GLM4_7 | Self::GLM4_7Flash => 101_000,
509            Self::KimiK2Thinking | Self::KimiK2_5 => 16_000,
510            Self::DeepSeekR1 | Self::DeepSeekV3_1 | Self::DeepSeekV3_2 => 16_000,
511            Self::Custom {
512                max_output_tokens, ..
513            } => max_output_tokens.unwrap_or(4_096),
514        }
515    }
516
517    pub fn default_temperature(&self) -> f32 {
518        match self {
519            Self::ClaudeFable5
520            | Self::ClaudeOpus5
521            | Self::ClaudeOpus4_8
522            | Self::ClaudeOpus4_7
523            | Self::ClaudeOpus4_6
524            | Self::ClaudeOpus4_5
525            | Self::ClaudeOpus4_1
526            | Self::ClaudeSonnet5
527            | Self::ClaudeSonnet4_6
528            | Self::ClaudeSonnet4_5
529            | Self::ClaudeSonnet4
530            | Self::ClaudeHaiku4_5 => 1.0,
531            Self::Custom {
532                default_temperature,
533                ..
534            } => default_temperature.unwrap_or(1.0),
535            _ => 1.0,
536        }
537    }
538
539    pub fn supports_tool_use(&self) -> bool {
540        match self {
541            Self::ClaudeFable5
542            | Self::ClaudeOpus5
543            | Self::ClaudeOpus4_8
544            | Self::ClaudeOpus4_7
545            | Self::ClaudeOpus4_6
546            | Self::ClaudeOpus4_5
547            | Self::ClaudeOpus4_1
548            | Self::ClaudeSonnet5
549            | Self::ClaudeSonnet4_6
550            | Self::ClaudeSonnet4_5
551            | Self::ClaudeSonnet4
552            | Self::ClaudeHaiku4_5 => true,
553            Self::NovaLite | Self::NovaPro | Self::NovaPremier | Self::Nova2Lite => true,
554            Self::MistralLarge3 | Self::PixtralLarge | Self::MagistralSmall => true,
555            Self::Devstral2_123B | Self::Ministral14B => true,
556            // Gemma accepts toolConfig without error but produces unreliable tool
557            // calls -- malformed JSON args, hallucinated tool names, dropped calls.
558            Self::Qwen3_32B
559            | Self::Qwen3VL235B
560            | Self::Qwen3_235B
561            | Self::Qwen3Next80B
562            | Self::Qwen3Coder30B
563            | Self::Qwen3CoderNext
564            | Self::Qwen3Coder480B => true,
565            Self::MiniMaxM2 | Self::MiniMaxM2_1 | Self::MiniMaxM2_5 => true,
566            Self::NemotronSuper3_120B | Self::NemotronNano3_30B => true,
567            Self::GLM5 | Self::GLM4_7 | Self::GLM4_7Flash => true,
568            Self::KimiK2Thinking | Self::KimiK2_5 => true,
569            Self::DeepSeekR1 | Self::DeepSeekV3_1 | Self::DeepSeekV3_2 => true,
570            _ => false,
571        }
572    }
573
574    pub fn supports_images(&self) -> bool {
575        match self {
576            Self::ClaudeFable5
577            | Self::ClaudeOpus5
578            | Self::ClaudeOpus4_8
579            | Self::ClaudeOpus4_7
580            | Self::ClaudeOpus4_6
581            | Self::ClaudeOpus4_5
582            | Self::ClaudeOpus4_1
583            | Self::ClaudeSonnet5
584            | Self::ClaudeSonnet4_6
585            | Self::ClaudeSonnet4_5
586            | Self::ClaudeSonnet4
587            | Self::ClaudeHaiku4_5 => true,
588            Self::NovaLite | Self::NovaPro => true,
589            Self::PixtralLarge => true,
590            Self::Qwen3VL235B => true,
591            Self::KimiK2_5 => true,
592            _ => false,
593        }
594    }
595
596    pub fn supports_caching(&self) -> bool {
597        match self {
598            Self::ClaudeFable5
599            | Self::ClaudeOpus5
600            | Self::ClaudeOpus4_8
601            | Self::ClaudeOpus4_7
602            | Self::ClaudeOpus4_6
603            | Self::ClaudeOpus4_5
604            | Self::ClaudeOpus4_1
605            | Self::ClaudeSonnet5
606            | Self::ClaudeSonnet4_6
607            | Self::ClaudeSonnet4_5
608            | Self::ClaudeSonnet4
609            | Self::ClaudeHaiku4_5 => true,
610            Self::Custom {
611                cache_configuration,
612                ..
613            } => cache_configuration.is_some(),
614            _ => false,
615        }
616    }
617
618    pub fn supports_thinking(&self) -> bool {
619        matches!(
620            self,
621            Self::ClaudeFable5
622                | Self::ClaudeOpus5
623                | Self::ClaudeOpus4_8
624                | Self::ClaudeOpus4_7
625                | Self::ClaudeOpus4_6
626                | Self::ClaudeOpus4_5
627                | Self::ClaudeOpus4_1
628                | Self::ClaudeSonnet5
629                | Self::ClaudeSonnet4_6
630                | Self::ClaudeSonnet4_5
631                | Self::ClaudeSonnet4
632                | Self::ClaudeHaiku4_5
633        )
634    }
635
636    pub fn supports_adaptive_thinking(&self) -> bool {
637        matches!(
638            self,
639            Self::ClaudeFable5
640                | Self::ClaudeOpus5
641                | Self::ClaudeOpus4_8
642                | Self::ClaudeOpus4_7
643                | Self::ClaudeOpus4_6
644                | Self::ClaudeSonnet5
645                | Self::ClaudeSonnet4_6
646        )
647    }
648
649    pub fn supports_xhigh_adaptive_thinking(&self) -> bool {
650        matches!(
651            self,
652            Self::ClaudeFable5 | Self::ClaudeOpus5 | Self::ClaudeOpus4_8 | Self::ClaudeSonnet5
653        )
654    }
655
656    pub fn thinking_mode(&self) -> BedrockModelMode {
657        if self.supports_adaptive_thinking() {
658            BedrockModelMode::AdaptiveThinking {
659                effort: BedrockAdaptiveThinkingEffort::default(),
660            }
661        } else if self.supports_thinking() {
662            BedrockModelMode::Thinking {
663                budget_tokens: Some(4096),
664            }
665        } else {
666            BedrockModelMode::Default
667        }
668    }
669
670    pub fn cross_region_inference_id(
671        &self,
672        region: &str,
673        allow_global: bool,
674    ) -> anyhow::Result<String> {
675        let model_id = self.request_id();
676
677        let supports_global = matches!(
678            self,
679            Self::ClaudeFable5
680                | Self::ClaudeOpus5
681                | Self::ClaudeOpus4_8
682                | Self::ClaudeOpus4_7
683                | Self::ClaudeOpus4_6
684                | Self::ClaudeOpus4_5
685                | Self::ClaudeSonnet5
686                | Self::ClaudeSonnet4_6
687                | Self::ClaudeSonnet4_5
688                | Self::ClaudeSonnet4
689                | Self::ClaudeHaiku4_5
690                | Self::Nova2Lite
691        );
692
693        // Determine region group based on AWS region
694        let region_group = if region.starts_with("us-gov-") {
695            "us-gov"
696        } else if region.starts_with("us-") || region.starts_with("sa-") {
697            if allow_global && supports_global {
698                "global"
699            } else {
700                "us"
701            }
702        } else if region.starts_with("ca-") {
703            if allow_global && supports_global {
704                "global"
705            } else {
706                "ca"
707            }
708        } else if region.starts_with("eu-") {
709            if allow_global && supports_global {
710                "global"
711            } else {
712                "eu"
713            }
714        } else if region == "ap-southeast-2" || region == "ap-southeast-4" {
715            // Australia
716            if allow_global && supports_global {
717                "global"
718            } else {
719                "au"
720            }
721        } else if region == "ap-northeast-1" || region == "ap-northeast-3" {
722            // Japan
723            if allow_global && supports_global {
724                "global"
725            } else {
726                "jp"
727            }
728        } else if region.starts_with("ap-") || region.starts_with("me-") {
729            if allow_global && supports_global {
730                "global"
731            } else {
732                "apac"
733            }
734        } else {
735            anyhow::bail!("Unsupported Region {region}");
736        };
737
738        match (self, region_group) {
739            (Self::Custom { .. }, _) => Ok(model_id.into()),
740
741            // Global inference profiles
742            (
743                Self::ClaudeFable5
744                | Self::ClaudeOpus5
745                | Self::ClaudeOpus4_8
746                | Self::ClaudeOpus4_7
747                | Self::ClaudeOpus4_6
748                | Self::ClaudeOpus4_5
749                | Self::ClaudeSonnet5
750                | Self::ClaudeSonnet4_6
751                | Self::ClaudeSonnet4_5
752                | Self::ClaudeSonnet4
753                | Self::ClaudeHaiku4_5
754                | Self::Nova2Lite,
755                "global",
756            ) => Ok(format!("{}.{}", region_group, model_id)),
757
758            // US Government region inference profiles
759            (Self::ClaudeSonnet4_5, "us-gov") => Ok(format!("{}.{}", region_group, model_id)),
760
761            // US region inference profiles
762            (
763                Self::ClaudeFable5
764                | Self::ClaudeOpus5
765                | Self::ClaudeOpus4_8
766                | Self::ClaudeOpus4_7
767                | Self::ClaudeOpus4_6
768                | Self::ClaudeOpus4_5
769                | Self::ClaudeOpus4_1
770                | Self::ClaudeSonnet5
771                | Self::ClaudeSonnet4_6
772                | Self::ClaudeSonnet4_5
773                | Self::ClaudeSonnet4
774                | Self::ClaudeHaiku4_5
775                | Self::Llama4Scout17B
776                | Self::Llama4Maverick17B
777                | Self::NovaLite
778                | Self::NovaPro
779                | Self::NovaPremier
780                | Self::Nova2Lite
781                | Self::PixtralLarge
782                | Self::DeepSeekR1,
783                "us",
784            ) => Ok(format!("{}.{}", region_group, model_id)),
785
786            // Canada region inference profiles
787            (Self::NovaLite, "ca") => Ok(format!("{}.{}", region_group, model_id)),
788
789            // Canada has no Claude-specific `ca.` profiles. AWS instead lists
790            // ca-central-1 and ca-west-1 as source regions of the US geo
791            // profile for these models, which keeps data within US and Canada
792            // regions. See the per-model cards, e.g.
793            // <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-5.html>.
794            (Self::ClaudeOpus5, "ca") => Ok(format!("us.{}", model_id)),
795
796            // EU region inference profiles
797            (
798                Self::ClaudeOpus5
799                | Self::ClaudeOpus4_8
800                | Self::ClaudeOpus4_7
801                | Self::ClaudeOpus4_6
802                | Self::ClaudeSonnet4_6
803                | Self::ClaudeSonnet4_5
804                | Self::ClaudeSonnet4
805                | Self::ClaudeHaiku4_5
806                | Self::NovaLite
807                | Self::NovaPro
808                | Self::Nova2Lite,
809                "eu",
810            ) => Ok(format!("{}.{}", region_group, model_id)),
811
812            // Australia region inference profiles
813            (
814                Self::ClaudeOpus5
815                | Self::ClaudeOpus4_8
816                | Self::ClaudeOpus4_7
817                | Self::ClaudeOpus4_6
818                | Self::ClaudeSonnet4_6
819                | Self::ClaudeSonnet4_5
820                | Self::ClaudeHaiku4_5,
821                "au",
822            ) => Ok(format!("{}.{}", region_group, model_id)),
823
824            // Japan region inference profiles. Claude Opus 5 is deliberately
825            // absent: its model card lists only `us.`/`eu.`/`au.` geo profiles
826            // (plus `global.`):
827            // <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-5.html>
828            (
829                Self::ClaudeSonnet4_6
830                | Self::ClaudeSonnet4_5
831                | Self::ClaudeHaiku4_5
832                | Self::Nova2Lite,
833                "jp",
834            ) => Ok(format!("{}.{}", region_group, model_id)),
835
836            // APAC region inference profiles (other than AU/JP)
837            (
838                Self::ClaudeSonnet4_5
839                | Self::ClaudeSonnet4
840                | Self::ClaudeHaiku4_5
841                | Self::NovaLite
842                | Self::NovaPro
843                | Self::Nova2Lite,
844                "apac",
845            ) => Ok(format!("{}.{}", region_group, model_id)),
846
847            (Self::ClaudeFable5 | Self::ClaudeSonnet5, _) => Ok(format!("global.{}", model_id)),
848
849            // Default: use model ID directly
850            _ => Ok(model_id.into()),
851        }
852    }
853}
854
855/// The wire protocol used to talk to a [`MantleModel`] on the `bedrock-mantle` endpoint.
856#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
857#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
858pub enum MantleProtocol {
859    /// The OpenAI Chat Completions API (`/chat/completions`).
860    #[default]
861    ChatCompletions,
862    /// The OpenAI Responses API (`/responses`).
863    Responses,
864}
865
866/// Models only reachable through the `bedrock-mantle` endpoint's
867/// OpenAI-compatible APIs, i.e. with no `Converse`/`Invoke` support on
868/// `bedrock-runtime`.
869#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
870#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, EnumIter)]
871pub enum MantleModel {
872    #[serde(rename = "gpt-5.6-sol")]
873    Gpt5_6Sol,
874    #[serde(rename = "gpt-5.6-terra")]
875    Gpt5_6Terra,
876    #[serde(rename = "gpt-5.6-luna")]
877    Gpt5_6Luna,
878    #[serde(rename = "gpt-5.5")]
879    Gpt5_5,
880    #[serde(rename = "gpt-5.4")]
881    Gpt5_4,
882    #[serde(rename = "grok-4.3")]
883    Grok4_3,
884    #[serde(rename = "custom")]
885    Custom {
886        name: String,
887        display_name: Option<String>,
888        max_tokens: u64,
889        max_output_tokens: Option<u64>,
890        protocol: MantleProtocol,
891        supports_tools: bool,
892        supports_images: bool,
893        supports_thinking: bool,
894    },
895}
896
897impl MantleModel {
898    /// The model id Zed uses internally (also used as the `name` in settings).
899    pub fn id(&self) -> &str {
900        match self {
901            Self::Gpt5_6Sol => "gpt-5.6-sol",
902            Self::Gpt5_6Terra => "gpt-5.6-terra",
903            Self::Gpt5_6Luna => "gpt-5.6-luna",
904            Self::Gpt5_5 => "gpt-5.5",
905            Self::Gpt5_4 => "gpt-5.4",
906            Self::Grok4_3 => "grok-4.3",
907            Self::Custom { name, .. } => name,
908        }
909    }
910
911    /// The model id as expected in Bedrock Mantle request bodies, e.g. `openai.gpt-5.5`.
912    pub fn request_id(&self) -> &str {
913        match self {
914            Self::Gpt5_6Sol => "openai.gpt-5.6-sol",
915            Self::Gpt5_6Terra => "openai.gpt-5.6-terra",
916            Self::Gpt5_6Luna => "openai.gpt-5.6-luna",
917            Self::Gpt5_5 => "openai.gpt-5.5",
918            Self::Gpt5_4 => "openai.gpt-5.4",
919            Self::Grok4_3 => "xai.grok-4.3",
920            Self::Custom { name, .. } => name,
921        }
922    }
923
924    pub fn display_name(&self) -> &str {
925        match self {
926            Self::Gpt5_6Sol => "GPT-5.6 Sol",
927            Self::Gpt5_6Terra => "GPT-5.6 Terra",
928            Self::Gpt5_6Luna => "GPT-5.6 Luna",
929            Self::Gpt5_5 => "GPT-5.5",
930            Self::Gpt5_4 => "GPT-5.4",
931            Self::Grok4_3 => "Grok 4.3",
932            Self::Custom {
933                display_name, name, ..
934            } => display_name.as_deref().unwrap_or(name.as_str()),
935        }
936    }
937
938    /// Which OpenAI-compatible API this model must be called through.
939    pub fn protocol(&self) -> MantleProtocol {
940        match self {
941            Self::Gpt5_6Sol
942            | Self::Gpt5_6Terra
943            | Self::Gpt5_6Luna
944            | Self::Gpt5_5
945            | Self::Gpt5_4
946            | Self::Grok4_3 => MantleProtocol::Responses,
947            Self::Custom { protocol, .. } => *protocol,
948        }
949    }
950
951    pub fn max_token_count(&self) -> u64 {
952        match self {
953            Self::Gpt5_6Sol
954            | Self::Gpt5_6Terra
955            | Self::Gpt5_6Luna
956            | Self::Gpt5_5
957            | Self::Gpt5_4 => 272_000,
958            Self::Grok4_3 => 1_000_000,
959            Self::Custom { max_tokens, .. } => *max_tokens,
960        }
961    }
962
963    pub fn max_output_tokens(&self) -> u64 {
964        match self {
965            // AWS doesn't document a hard cap for the GPT-5.x models on Mantle.
966            Self::Gpt5_6Sol
967            | Self::Gpt5_6Terra
968            | Self::Gpt5_6Luna
969            | Self::Gpt5_5
970            | Self::Gpt5_4 => 128_000,
971            Self::Grok4_3 => 131_072,
972            Self::Custom {
973                max_output_tokens, ..
974            } => max_output_tokens.unwrap_or(4_096),
975        }
976    }
977
978    pub fn supports_tools(&self) -> bool {
979        match self {
980            Self::Gpt5_6Sol
981            | Self::Gpt5_6Terra
982            | Self::Gpt5_6Luna
983            | Self::Gpt5_5
984            | Self::Gpt5_4
985            | Self::Grok4_3 => true,
986            Self::Custom { supports_tools, .. } => *supports_tools,
987        }
988    }
989
990    pub fn supports_images(&self) -> bool {
991        match self {
992            Self::Gpt5_6Sol
993            | Self::Gpt5_6Terra
994            | Self::Gpt5_6Luna
995            | Self::Gpt5_5
996            | Self::Gpt5_4
997            | Self::Grok4_3 => true,
998            Self::Custom {
999                supports_images, ..
1000            } => *supports_images,
1001        }
1002    }
1003
1004    pub fn supports_thinking(&self) -> bool {
1005        match self {
1006            Self::Gpt5_6Sol
1007            | Self::Gpt5_6Terra
1008            | Self::Gpt5_6Luna
1009            | Self::Gpt5_5
1010            | Self::Gpt5_4
1011            | Self::Grok4_3 => true,
1012            Self::Custom {
1013                supports_thinking, ..
1014            } => *supports_thinking,
1015        }
1016    }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021    use super::*;
1022
1023    #[test]
1024    fn test_builtin_mantle_models_use_responses_protocol() {
1025        assert_eq!(MantleModel::Gpt5_6Sol.protocol(), MantleProtocol::Responses);
1026        assert_eq!(
1027            MantleModel::Gpt5_6Terra.protocol(),
1028            MantleProtocol::Responses
1029        );
1030        assert_eq!(
1031            MantleModel::Gpt5_6Luna.protocol(),
1032            MantleProtocol::Responses
1033        );
1034        assert_eq!(MantleModel::Gpt5_5.protocol(), MantleProtocol::Responses);
1035        assert_eq!(MantleModel::Gpt5_4.protocol(), MantleProtocol::Responses);
1036        assert_eq!(MantleModel::Grok4_3.protocol(), MantleProtocol::Responses);
1037    }
1038
1039    #[test]
1040    fn test_gpt_5_6_mantle_model_metadata() {
1041        // Values mirror the AWS Bedrock model cards for GPT-5.6 Sol/Terra/Luna,
1042        // which are served only through the `bedrock-mantle` Responses API.
1043        for (model, id, request_id, display_name) in [
1044            (
1045                MantleModel::Gpt5_6Sol,
1046                "gpt-5.6-sol",
1047                "openai.gpt-5.6-sol",
1048                "GPT-5.6 Sol",
1049            ),
1050            (
1051                MantleModel::Gpt5_6Terra,
1052                "gpt-5.6-terra",
1053                "openai.gpt-5.6-terra",
1054                "GPT-5.6 Terra",
1055            ),
1056            (
1057                MantleModel::Gpt5_6Luna,
1058                "gpt-5.6-luna",
1059                "openai.gpt-5.6-luna",
1060                "GPT-5.6 Luna",
1061            ),
1062        ] {
1063            assert_eq!(model.id(), id);
1064            assert_eq!(model.request_id(), request_id);
1065            assert_eq!(model.display_name(), display_name);
1066            assert_eq!(model.max_token_count(), 272_000);
1067            assert!(model.supports_tools());
1068            assert!(model.supports_images());
1069            assert!(model.supports_thinking());
1070        }
1071    }
1072
1073    #[test]
1074    fn test_builtin_mantle_models_have_unique_ids_and_sane_token_limits() {
1075        use std::collections::HashSet;
1076        use strum::IntoEnumIterator;
1077
1078        let mut ids = HashSet::new();
1079        let mut request_ids = HashSet::new();
1080        for model in
1081            MantleModel::iter().filter(|model| !matches!(model, MantleModel::Custom { .. }))
1082        {
1083            assert!(
1084                ids.insert(model.id().to_string()),
1085                "duplicate MantleModel id: {}",
1086                model.id()
1087            );
1088            assert!(
1089                request_ids.insert(model.request_id().to_string()),
1090                "duplicate MantleModel request_id: {}",
1091                model.request_id()
1092            );
1093            assert!(
1094                model.max_output_tokens() <= model.max_token_count(),
1095                "{} has max_output_tokens ({}) greater than max_token_count ({})",
1096                model.id(),
1097                model.max_output_tokens(),
1098                model.max_token_count()
1099            );
1100        }
1101    }
1102
1103    #[test]
1104    fn test_us_region_inference_ids() -> anyhow::Result<()> {
1105        assert_eq!(
1106            ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("us-east-1", false)?,
1107            "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
1108        );
1109        assert_eq!(
1110            ConverseModel::ClaudeSonnet4.cross_region_inference_id("us-west-2", false)?,
1111            "us.anthropic.claude-sonnet-4-20250514-v1:0"
1112        );
1113        assert_eq!(
1114            ConverseModel::ClaudeFable5.cross_region_inference_id("us-east-1", false)?,
1115            "us.anthropic.claude-fable-5"
1116        );
1117        assert_eq!(
1118            ConverseModel::ClaudeSonnet5.cross_region_inference_id("us-east-1", false)?,
1119            "us.anthropic.claude-sonnet-5"
1120        );
1121        assert_eq!(
1122            ConverseModel::ClaudeOpus5.cross_region_inference_id("us-east-1", false)?,
1123            "us.anthropic.claude-opus-5"
1124        );
1125        assert_eq!(
1126            ConverseModel::NovaPro.cross_region_inference_id("us-east-2", false)?,
1127            "us.amazon.nova-pro-v1:0"
1128        );
1129        assert_eq!(
1130            ConverseModel::DeepSeekR1.cross_region_inference_id("us-east-1", false)?,
1131            "us.deepseek.r1-v1:0"
1132        );
1133        Ok(())
1134    }
1135
1136    #[test]
1137    fn test_eu_region_inference_ids() -> anyhow::Result<()> {
1138        assert_eq!(
1139            ConverseModel::ClaudeSonnet4.cross_region_inference_id("eu-west-1", false)?,
1140            "eu.anthropic.claude-sonnet-4-20250514-v1:0"
1141        );
1142        assert_eq!(
1143            ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", false)?,
1144            "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
1145        );
1146        assert_eq!(
1147            ConverseModel::NovaLite.cross_region_inference_id("eu-north-1", false)?,
1148            "eu.amazon.nova-lite-v1:0"
1149        );
1150        assert_eq!(
1151            ConverseModel::ClaudeOpus4_6.cross_region_inference_id("eu-west-1", false)?,
1152            "eu.anthropic.claude-opus-4-6-v1"
1153        );
1154        assert_eq!(
1155            ConverseModel::ClaudeOpus4_7.cross_region_inference_id("eu-west-1", false)?,
1156            "eu.anthropic.claude-opus-4-7"
1157        );
1158        assert_eq!(
1159            ConverseModel::ClaudeOpus4_8.cross_region_inference_id("eu-west-1", false)?,
1160            "eu.anthropic.claude-opus-4-8"
1161        );
1162        assert_eq!(
1163            ConverseModel::ClaudeOpus5.cross_region_inference_id("eu-west-1", false)?,
1164            "eu.anthropic.claude-opus-5"
1165        );
1166        Ok(())
1167    }
1168
1169    #[test]
1170    fn test_inference_profile_only_models_fall_back_to_global() -> anyhow::Result<()> {
1171        assert_eq!(
1172            ConverseModel::ClaudeFable5.cross_region_inference_id("eu-west-1", false)?,
1173            "global.anthropic.claude-fable-5"
1174        );
1175        assert_eq!(
1176            ConverseModel::ClaudeSonnet5.cross_region_inference_id("eu-west-1", false)?,
1177            "global.anthropic.claude-sonnet-5"
1178        );
1179        assert_eq!(
1180            ConverseModel::ClaudeFable5.cross_region_inference_id("ap-southeast-2", false)?,
1181            "global.anthropic.claude-fable-5"
1182        );
1183        assert_eq!(
1184            ConverseModel::ClaudeSonnet5.cross_region_inference_id("ap-northeast-1", false)?,
1185            "global.anthropic.claude-sonnet-5"
1186        );
1187        Ok(())
1188    }
1189
1190    #[test]
1191    fn test_apac_region_inference_ids() -> anyhow::Result<()> {
1192        assert_eq!(
1193            ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("ap-south-1", false)?,
1194            "apac.anthropic.claude-sonnet-4-5-20250929-v1:0"
1195        );
1196        assert_eq!(
1197            ConverseModel::NovaLite.cross_region_inference_id("ap-south-1", false)?,
1198            "apac.amazon.nova-lite-v1:0"
1199        );
1200        Ok(())
1201    }
1202
1203    #[test]
1204    fn test_au_region_inference_ids() -> anyhow::Result<()> {
1205        assert_eq!(
1206            ConverseModel::ClaudeHaiku4_5.cross_region_inference_id("ap-southeast-2", false)?,
1207            "au.anthropic.claude-haiku-4-5-20251001-v1:0"
1208        );
1209        assert_eq!(
1210            ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("ap-southeast-4", false)?,
1211            "au.anthropic.claude-sonnet-4-5-20250929-v1:0"
1212        );
1213        assert_eq!(
1214            ConverseModel::ClaudeOpus4_6.cross_region_inference_id("ap-southeast-2", false)?,
1215            "au.anthropic.claude-opus-4-6-v1"
1216        );
1217        assert_eq!(
1218            ConverseModel::ClaudeOpus4_7.cross_region_inference_id("ap-southeast-2", false)?,
1219            "au.anthropic.claude-opus-4-7"
1220        );
1221        assert_eq!(
1222            ConverseModel::ClaudeOpus4_8.cross_region_inference_id("ap-southeast-2", false)?,
1223            "au.anthropic.claude-opus-4-8"
1224        );
1225        assert_eq!(
1226            ConverseModel::ClaudeOpus5.cross_region_inference_id("ap-southeast-2", false)?,
1227            "au.anthropic.claude-opus-5"
1228        );
1229        Ok(())
1230    }
1231
1232    #[test]
1233    fn test_jp_region_inference_ids() -> anyhow::Result<()> {
1234        assert_eq!(
1235            ConverseModel::ClaudeHaiku4_5.cross_region_inference_id("ap-northeast-1", false)?,
1236            "jp.anthropic.claude-haiku-4-5-20251001-v1:0"
1237        );
1238        // Claude Opus 5 has no `jp.` geo profile, so it falls through to the
1239        // bare model id (which fails at request time with a region hint)
1240        // rather than fabricating an undocumented profile.
1241        assert_eq!(
1242            ConverseModel::ClaudeOpus5.cross_region_inference_id("ap-northeast-1", false)?,
1243            "anthropic.claude-opus-5"
1244        );
1245        assert_eq!(
1246            ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("ap-northeast-3", false)?,
1247            "jp.anthropic.claude-sonnet-4-5-20250929-v1:0"
1248        );
1249        assert_eq!(
1250            ConverseModel::Nova2Lite.cross_region_inference_id("ap-northeast-1", false)?,
1251            "jp.amazon.nova-2-lite-v1:0"
1252        );
1253        Ok(())
1254    }
1255
1256    #[test]
1257    fn test_ca_region_inference_ids() -> anyhow::Result<()> {
1258        assert_eq!(
1259            ConverseModel::NovaLite.cross_region_inference_id("ca-central-1", false)?,
1260            "ca.amazon.nova-lite-v1:0"
1261        );
1262        // Canadian regions are source regions of the US geo profile for
1263        // recent Opus models; there are no `ca.` Claude profiles.
1264        assert_eq!(
1265            ConverseModel::ClaudeOpus5.cross_region_inference_id("ca-central-1", false)?,
1266            "us.anthropic.claude-opus-5"
1267        );
1268        Ok(())
1269    }
1270
1271    #[test]
1272    fn test_gov_region_inference_ids() -> anyhow::Result<()> {
1273        assert_eq!(
1274            ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("us-gov-east-1", false)?,
1275            "us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0"
1276        );
1277        assert_eq!(
1278            ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("us-gov-west-1", false)?,
1279            "us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0"
1280        );
1281        Ok(())
1282    }
1283
1284    #[test]
1285    fn test_global_inference_ids() -> anyhow::Result<()> {
1286        assert_eq!(
1287            ConverseModel::ClaudeSonnet4.cross_region_inference_id("us-east-1", true)?,
1288            "global.anthropic.claude-sonnet-4-20250514-v1:0"
1289        );
1290        assert_eq!(
1291            ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", true)?,
1292            "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
1293        );
1294        assert_eq!(
1295            ConverseModel::ClaudeHaiku4_5.cross_region_inference_id("ap-south-1", true)?,
1296            "global.anthropic.claude-haiku-4-5-20251001-v1:0"
1297        );
1298        assert_eq!(
1299            ConverseModel::ClaudeOpus4_6.cross_region_inference_id("us-east-1", true)?,
1300            "global.anthropic.claude-opus-4-6-v1"
1301        );
1302        assert_eq!(
1303            ConverseModel::ClaudeOpus4_7.cross_region_inference_id("us-east-1", true)?,
1304            "global.anthropic.claude-opus-4-7"
1305        );
1306        assert_eq!(
1307            ConverseModel::ClaudeOpus4_8.cross_region_inference_id("us-east-1", true)?,
1308            "global.anthropic.claude-opus-4-8"
1309        );
1310        assert_eq!(
1311            ConverseModel::ClaudeOpus5.cross_region_inference_id("us-east-1", true)?,
1312            "global.anthropic.claude-opus-5"
1313        );
1314        assert_eq!(
1315            ConverseModel::ClaudeFable5.cross_region_inference_id("us-east-1", true)?,
1316            "global.anthropic.claude-fable-5"
1317        );
1318        assert_eq!(
1319            ConverseModel::ClaudeSonnet5.cross_region_inference_id("us-east-1", true)?,
1320            "global.anthropic.claude-sonnet-5"
1321        );
1322        assert_eq!(
1323            ConverseModel::Nova2Lite.cross_region_inference_id("us-east-1", true)?,
1324            "global.amazon.nova-2-lite-v1:0"
1325        );
1326
1327        // Models without global support fall back to regional
1328        assert_eq!(
1329            ConverseModel::NovaPro.cross_region_inference_id("us-east-1", true)?,
1330            "us.amazon.nova-pro-v1:0"
1331        );
1332        Ok(())
1333    }
1334
1335    #[test]
1336    fn test_models_without_cross_region() -> anyhow::Result<()> {
1337        // Models without cross-region support return their request_id directly
1338        assert_eq!(
1339            ConverseModel::Gemma3_4B.cross_region_inference_id("us-east-1", false)?,
1340            "google.gemma-3-4b-it"
1341        );
1342        assert_eq!(
1343            ConverseModel::MistralLarge3.cross_region_inference_id("eu-west-1", false)?,
1344            "mistral.mistral-large-3-675b-instruct"
1345        );
1346        assert_eq!(
1347            ConverseModel::Qwen3VL235B.cross_region_inference_id("ap-south-1", false)?,
1348            "qwen.qwen3-vl-235b-a22b"
1349        );
1350        assert_eq!(
1351            ConverseModel::GptOss120B.cross_region_inference_id("us-east-1", false)?,
1352            "openai.gpt-oss-120b-1:0"
1353        );
1354        assert_eq!(
1355            ConverseModel::MiniMaxM2.cross_region_inference_id("us-east-1", false)?,
1356            "minimax.minimax-m2"
1357        );
1358        assert_eq!(
1359            ConverseModel::KimiK2Thinking.cross_region_inference_id("us-east-1", false)?,
1360            "moonshot.kimi-k2-thinking"
1361        );
1362        Ok(())
1363    }
1364
1365    #[test]
1366    fn test_custom_model_inference_ids() -> anyhow::Result<()> {
1367        let custom_model = ConverseModel::Custom {
1368            name: "custom.my-model-v1:0".to_string(),
1369            max_tokens: 100000,
1370            display_name: Some("My Custom Model".to_string()),
1371            max_output_tokens: Some(8192),
1372            default_temperature: Some(0.7),
1373            cache_configuration: None,
1374        };
1375
1376        assert_eq!(
1377            custom_model.cross_region_inference_id("us-east-1", false)?,
1378            "custom.my-model-v1:0"
1379        );
1380        assert_eq!(
1381            custom_model.cross_region_inference_id("eu-west-1", true)?,
1382            "custom.my-model-v1:0"
1383        );
1384        Ok(())
1385    }
1386
1387    #[test]
1388    fn test_friendly_id_vs_request_id() {
1389        assert_eq!(ConverseModel::ClaudeSonnet4_5.id(), "claude-sonnet-4-5");
1390        assert_eq!(ConverseModel::NovaLite.id(), "nova-lite");
1391        assert_eq!(ConverseModel::DeepSeekR1.id(), "deepseek-r1");
1392        assert_eq!(ConverseModel::Llama4Scout17B.id(), "llama-4-scout-17b");
1393        assert_eq!(ConverseModel::ClaudeFable5.id(), "claude-fable-5");
1394        assert_eq!(ConverseModel::ClaudeOpus5.id(), "claude-opus-5");
1395        assert_eq!(ConverseModel::ClaudeSonnet5.id(), "claude-sonnet-5");
1396
1397        assert_eq!(
1398            ConverseModel::ClaudeSonnet4_5.request_id(),
1399            "anthropic.claude-sonnet-4-5-20250929-v1:0"
1400        );
1401        assert_eq!(
1402            ConverseModel::NovaLite.request_id(),
1403            "amazon.nova-lite-v1:0"
1404        );
1405        assert_eq!(ConverseModel::DeepSeekR1.request_id(), "deepseek.r1-v1:0");
1406        assert_eq!(
1407            ConverseModel::Llama4Scout17B.request_id(),
1408            "meta.llama4-scout-17b-instruct-v1:0"
1409        );
1410        assert_eq!(
1411            ConverseModel::ClaudeFable5.request_id(),
1412            "anthropic.claude-fable-5"
1413        );
1414        assert_eq!(
1415            ConverseModel::ClaudeOpus5.request_id(),
1416            "anthropic.claude-opus-5"
1417        );
1418        assert_eq!(
1419            ConverseModel::ClaudeSonnet5.request_id(),
1420            "anthropic.claude-sonnet-5"
1421        );
1422
1423        // Thinking aliases deserialize to the same model
1424        assert_eq!(ConverseModel::ClaudeSonnet4.id(), "claude-sonnet-4");
1425        assert_eq!(
1426            ConverseModel::from_id("claude-sonnet-4-thinking")
1427                .unwrap()
1428                .id(),
1429            "claude-sonnet-4"
1430        );
1431        assert_eq!(
1432            ConverseModel::from_id("claude-fable-5-thinking")
1433                .unwrap()
1434                .id(),
1435            "claude-fable-5"
1436        );
1437        assert_eq!(
1438            ConverseModel::from_id("claude-opus-5-thinking")
1439                .unwrap()
1440                .id(),
1441            "claude-opus-5"
1442        );
1443        assert_eq!(
1444            ConverseModel::from_id("claude-sonnet-5-thinking")
1445                .unwrap()
1446                .id(),
1447            "claude-sonnet-5"
1448        );
1449    }
1450
1451    #[test]
1452    fn test_thinking_modes() {
1453        assert!(ConverseModel::ClaudeHaiku4_5.supports_thinking());
1454        assert!(ConverseModel::ClaudeSonnet4.supports_thinking());
1455        assert!(ConverseModel::ClaudeSonnet4_5.supports_thinking());
1456        assert!(ConverseModel::ClaudeOpus4_6.supports_thinking());
1457        assert!(ConverseModel::ClaudeFable5.supports_thinking());
1458        assert!(ConverseModel::ClaudeOpus5.supports_thinking());
1459
1460        assert!(!ConverseModel::ClaudeSonnet4.supports_adaptive_thinking());
1461        assert!(ConverseModel::ClaudeOpus4_6.supports_adaptive_thinking());
1462        assert!(ConverseModel::ClaudeSonnet4_6.supports_adaptive_thinking());
1463        assert!(ConverseModel::ClaudeFable5.supports_adaptive_thinking());
1464        assert!(ConverseModel::ClaudeOpus5.supports_adaptive_thinking());
1465        assert!(ConverseModel::ClaudeSonnet5.supports_adaptive_thinking());
1466        assert!(!ConverseModel::ClaudeOpus4_7.supports_xhigh_adaptive_thinking());
1467        assert!(ConverseModel::ClaudeFable5.supports_xhigh_adaptive_thinking());
1468        assert!(ConverseModel::ClaudeOpus5.supports_xhigh_adaptive_thinking());
1469        assert!(ConverseModel::ClaudeSonnet5.supports_xhigh_adaptive_thinking());
1470        assert!(ConverseModel::ClaudeOpus4_8.supports_xhigh_adaptive_thinking());
1471        assert_eq!(BedrockAdaptiveThinkingEffort::XHigh.as_str(), "xhigh");
1472
1473        assert_eq!(
1474            ConverseModel::ClaudeSonnet4.thinking_mode(),
1475            BedrockModelMode::Thinking {
1476                budget_tokens: Some(4096)
1477            }
1478        );
1479        assert_eq!(
1480            ConverseModel::ClaudeOpus4_6.thinking_mode(),
1481            BedrockModelMode::AdaptiveThinking {
1482                effort: BedrockAdaptiveThinkingEffort::High
1483            }
1484        );
1485        assert_eq!(
1486            ConverseModel::ClaudeHaiku4_5.thinking_mode(),
1487            BedrockModelMode::Thinking {
1488                budget_tokens: Some(4096)
1489            }
1490        );
1491    }
1492
1493    #[test]
1494    fn test_max_token_count() {
1495        assert_eq!(ConverseModel::ClaudeSonnet4_5.max_token_count(), 1_000_000);
1496        assert_eq!(ConverseModel::ClaudeOpus4_6.max_token_count(), 1_000_000);
1497        assert_eq!(ConverseModel::ClaudeFable5.max_token_count(), 1_000_000);
1498        assert_eq!(ConverseModel::ClaudeOpus5.max_token_count(), 1_000_000);
1499        assert_eq!(ConverseModel::ClaudeSonnet5.max_token_count(), 1_000_000);
1500        assert_eq!(ConverseModel::Llama4Scout17B.max_token_count(), 128_000);
1501        assert_eq!(ConverseModel::NovaPremier.max_token_count(), 1_000_000);
1502    }
1503
1504    #[test]
1505    fn test_max_output_tokens() {
1506        assert_eq!(ConverseModel::ClaudeSonnet4_5.max_output_tokens(), 64_000);
1507        assert_eq!(ConverseModel::ClaudeOpus4_6.max_output_tokens(), 128_000);
1508        assert_eq!(ConverseModel::ClaudeFable5.max_output_tokens(), 128_000);
1509        assert_eq!(ConverseModel::ClaudeOpus5.max_output_tokens(), 128_000);
1510        assert_eq!(ConverseModel::ClaudeSonnet5.max_output_tokens(), 128_000);
1511        assert_eq!(ConverseModel::ClaudeOpus4_1.max_output_tokens(), 32_000);
1512        assert_eq!(ConverseModel::Gemma3_4B.max_output_tokens(), 8_192);
1513    }
1514
1515    #[test]
1516    fn test_supports_tool_use() {
1517        assert!(ConverseModel::ClaudeSonnet4_5.supports_tool_use());
1518        assert!(ConverseModel::ClaudeFable5.supports_tool_use());
1519        assert!(ConverseModel::NovaPro.supports_tool_use());
1520        assert!(ConverseModel::MistralLarge3.supports_tool_use());
1521        assert!(!ConverseModel::Gemma3_4B.supports_tool_use());
1522        assert!(ConverseModel::Qwen3_32B.supports_tool_use());
1523        assert!(ConverseModel::MiniMaxM2.supports_tool_use());
1524        assert!(ConverseModel::KimiK2_5.supports_tool_use());
1525        assert!(ConverseModel::DeepSeekR1.supports_tool_use());
1526        assert!(!ConverseModel::Llama4Scout17B.supports_tool_use());
1527    }
1528
1529    #[test]
1530    fn test_supports_caching() {
1531        assert!(ConverseModel::ClaudeSonnet4_5.supports_caching());
1532        assert!(ConverseModel::ClaudeOpus4_6.supports_caching());
1533        assert!(ConverseModel::ClaudeFable5.supports_caching());
1534        assert!(!ConverseModel::Llama4Scout17B.supports_caching());
1535        assert!(!ConverseModel::NovaPro.supports_caching());
1536    }
1537}
1538
Served at tenant.openagents/omega Member data and write actions are omitted.