models.rs

  1use serde::{Deserialize, Serialize};
  2use strum::EnumIter;
  3
  4#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
  5#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
  6pub enum BedrockModelMode {
  7    #[default]
  8    Default,
  9    Thinking {
 10        budget_tokens: Option<u64>,
 11    },
 12}
 13
 14#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 15#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
 16pub enum Model {
 17    // Anthropic models (already included)
 18    #[default]
 19    #[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
 20    ClaudeSonnet4,
 21    #[serde(
 22        rename = "claude-sonnet-4-thinking",
 23        alias = "claude-sonnet-4-thinking-latest"
 24    )]
 25    ClaudeSonnet4Thinking,
 26    #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
 27    ClaudeOpus4,
 28    #[serde(
 29        rename = "claude-opus-4-thinking",
 30        alias = "claude-opus-4-thinking-latest"
 31    )]
 32    ClaudeOpus4Thinking,
 33    #[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
 34    Claude3_5SonnetV2,
 35    #[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
 36    Claude3_7Sonnet,
 37    #[serde(
 38        rename = "claude-3-7-sonnet-thinking",
 39        alias = "claude-3-7-sonnet-thinking-latest"
 40    )]
 41    Claude3_7SonnetThinking,
 42    #[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
 43    Claude3Opus,
 44    #[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-latest")]
 45    Claude3Sonnet,
 46    #[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
 47    Claude3_5Haiku,
 48    Claude3_5Sonnet,
 49    Claude3Haiku,
 50    // Amazon Nova Models
 51    AmazonNovaLite,
 52    AmazonNovaMicro,
 53    AmazonNovaPro,
 54    AmazonNovaPremier,
 55    // AI21 models
 56    AI21J2GrandeInstruct,
 57    AI21J2JumboInstruct,
 58    AI21J2Mid,
 59    AI21J2MidV1,
 60    AI21J2Ultra,
 61    AI21J2UltraV1_8k,
 62    AI21J2UltraV1,
 63    AI21JambaInstructV1,
 64    AI21Jamba15LargeV1,
 65    AI21Jamba15MiniV1,
 66    // Cohere models
 67    CohereCommandTextV14_4k,
 68    CohereCommandRV1,
 69    CohereCommandRPlusV1,
 70    CohereCommandLightTextV14_4k,
 71    // DeepSeek
 72    DeepSeekR1,
 73    // Meta models
 74    MetaLlama38BInstructV1,
 75    MetaLlama370BInstructV1,
 76    MetaLlama318BInstructV1_128k,
 77    MetaLlama318BInstructV1,
 78    MetaLlama3170BInstructV1_128k,
 79    MetaLlama3170BInstructV1,
 80    MetaLlama3211BInstructV1,
 81    MetaLlama3290BInstructV1,
 82    MetaLlama321BInstructV1,
 83    MetaLlama323BInstructV1,
 84    // Mistral models
 85    MistralMistral7BInstructV0,
 86    MistralMixtral8x7BInstructV0,
 87    MistralMistralLarge2402V1,
 88    MistralMistralSmall2402V1,
 89    MistralPixtralLarge2502V1,
 90    // Writer models
 91    PalmyraWriterX5,
 92    PalmyraWriterX4,
 93    #[serde(rename = "custom")]
 94    Custom {
 95        name: String,
 96        max_tokens: usize,
 97        /// The name displayed in the UI, such as in the assistant panel model dropdown menu.
 98        display_name: Option<String>,
 99        max_output_tokens: Option<u32>,
100        default_temperature: Option<f32>,
101    },
102}
103
104impl Model {
105    pub fn default_fast() -> Self {
106        Self::Claude3_5Haiku
107    }
108
109    pub fn from_id(id: &str) -> anyhow::Result<Self> {
110        if id.starts_with("claude-3-5-sonnet-v2") {
111            Ok(Self::Claude3_5SonnetV2)
112        } else if id.starts_with("claude-3-opus") {
113            Ok(Self::Claude3Opus)
114        } else if id.starts_with("claude-3-sonnet") {
115            Ok(Self::Claude3Sonnet)
116        } else if id.starts_with("claude-3-5-haiku") {
117            Ok(Self::Claude3_5Haiku)
118        } else if id.starts_with("claude-3-7-sonnet") {
119            Ok(Self::Claude3_7Sonnet)
120        } else if id.starts_with("claude-3-7-sonnet-thinking") {
121            Ok(Self::Claude3_7SonnetThinking)
122        } else {
123            anyhow::bail!("invalid model id {id}");
124        }
125    }
126
127    pub fn id(&self) -> &str {
128        match self {
129            Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => {
130                "anthropic.claude-sonnet-4-20250514-v1:0"
131            }
132            Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => {
133                "anthropic.claude-opus-4-20250514-v1:0"
134            }
135            Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
136            Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
137            Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
138            Model::Claude3Sonnet => "anthropic.claude-3-sonnet-20240229-v1:0",
139            Model::Claude3Haiku => "anthropic.claude-3-haiku-20240307-v1:0",
140            Model::Claude3_5Haiku => "anthropic.claude-3-5-haiku-20241022-v1:0",
141            Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => {
142                "anthropic.claude-3-7-sonnet-20250219-v1:0"
143            }
144            Model::AmazonNovaLite => "amazon.nova-lite-v1:0",
145            Model::AmazonNovaMicro => "amazon.nova-micro-v1:0",
146            Model::AmazonNovaPro => "amazon.nova-pro-v1:0",
147            Model::AmazonNovaPremier => "amazon.nova-premier-v1:0",
148            Model::DeepSeekR1 => "us.deepseek.r1-v1:0",
149            Model::AI21J2GrandeInstruct => "ai21.j2-grande-instruct",
150            Model::AI21J2JumboInstruct => "ai21.j2-jumbo-instruct",
151            Model::AI21J2Mid => "ai21.j2-mid",
152            Model::AI21J2MidV1 => "ai21.j2-mid-v1",
153            Model::AI21J2Ultra => "ai21.j2-ultra",
154            Model::AI21J2UltraV1_8k => "ai21.j2-ultra-v1:0:8k",
155            Model::AI21J2UltraV1 => "ai21.j2-ultra-v1",
156            Model::AI21JambaInstructV1 => "ai21.jamba-instruct-v1:0",
157            Model::AI21Jamba15LargeV1 => "ai21.jamba-1-5-large-v1:0",
158            Model::AI21Jamba15MiniV1 => "ai21.jamba-1-5-mini-v1:0",
159            Model::CohereCommandTextV14_4k => "cohere.command-text-v14:7:4k",
160            Model::CohereCommandRV1 => "cohere.command-r-v1:0",
161            Model::CohereCommandRPlusV1 => "cohere.command-r-plus-v1:0",
162            Model::CohereCommandLightTextV14_4k => "cohere.command-light-text-v14:7:4k",
163            Model::MetaLlama38BInstructV1 => "meta.llama3-8b-instruct-v1:0",
164            Model::MetaLlama370BInstructV1 => "meta.llama3-70b-instruct-v1:0",
165            Model::MetaLlama318BInstructV1_128k => "meta.llama3-1-8b-instruct-v1:0:128k",
166            Model::MetaLlama318BInstructV1 => "meta.llama3-1-8b-instruct-v1:0",
167            Model::MetaLlama3170BInstructV1_128k => "meta.llama3-1-70b-instruct-v1:0:128k",
168            Model::MetaLlama3170BInstructV1 => "meta.llama3-1-70b-instruct-v1:0",
169            Model::MetaLlama3211BInstructV1 => "meta.llama3-2-11b-instruct-v1:0",
170            Model::MetaLlama3290BInstructV1 => "meta.llama3-2-90b-instruct-v1:0",
171            Model::MetaLlama321BInstructV1 => "meta.llama3-2-1b-instruct-v1:0",
172            Model::MetaLlama323BInstructV1 => "meta.llama3-2-3b-instruct-v1:0",
173            Model::MistralMistral7BInstructV0 => "mistral.mistral-7b-instruct-v0:2",
174            Model::MistralMixtral8x7BInstructV0 => "mistral.mixtral-8x7b-instruct-v0:1",
175            Model::MistralMistralLarge2402V1 => "mistral.mistral-large-2402-v1:0",
176            Model::MistralMistralSmall2402V1 => "mistral.mistral-small-2402-v1:0",
177            Model::MistralPixtralLarge2502V1 => "mistral.pixtral-large-2502-v1:0",
178            Model::PalmyraWriterX4 => "writer.palmyra-x4-v1:0",
179            Model::PalmyraWriterX5 => "writer.palmyra-x5-v1:0",
180            Self::Custom { name, .. } => name,
181        }
182    }
183
184    pub fn display_name(&self) -> &str {
185        match self {
186            Self::ClaudeSonnet4 => "Claude Sonnet 4",
187            Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
188            Self::ClaudeOpus4 => "Claude Opus 4",
189            Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
190            Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
191            Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
192            Self::Claude3Opus => "Claude 3 Opus",
193            Self::Claude3Sonnet => "Claude 3 Sonnet",
194            Self::Claude3Haiku => "Claude 3 Haiku",
195            Self::Claude3_5Haiku => "Claude 3.5 Haiku",
196            Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
197            Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
198            Self::AmazonNovaLite => "Amazon Nova Lite",
199            Self::AmazonNovaMicro => "Amazon Nova Micro",
200            Self::AmazonNovaPro => "Amazon Nova Pro",
201            Self::AmazonNovaPremier => "Amazon Nova Premier",
202            Self::DeepSeekR1 => "DeepSeek R1",
203            Self::AI21J2GrandeInstruct => "AI21 Jurassic2 Grande Instruct",
204            Self::AI21J2JumboInstruct => "AI21 Jurassic2 Jumbo Instruct",
205            Self::AI21J2Mid => "AI21 Jurassic2 Mid",
206            Self::AI21J2MidV1 => "AI21 Jurassic2 Mid V1",
207            Self::AI21J2Ultra => "AI21 Jurassic2 Ultra",
208            Self::AI21J2UltraV1_8k => "AI21 Jurassic2 Ultra V1 8K",
209            Self::AI21J2UltraV1 => "AI21 Jurassic2 Ultra V1",
210            Self::AI21JambaInstructV1 => "AI21 Jamba Instruct",
211            Self::AI21Jamba15LargeV1 => "AI21 Jamba 1.5 Large",
212            Self::AI21Jamba15MiniV1 => "AI21 Jamba 1.5 Mini",
213            Self::CohereCommandTextV14_4k => "Cohere Command Text V14 4K",
214            Self::CohereCommandRV1 => "Cohere Command R V1",
215            Self::CohereCommandRPlusV1 => "Cohere Command R Plus V1",
216            Self::CohereCommandLightTextV14_4k => "Cohere Command Light Text V14 4K",
217            Self::MetaLlama38BInstructV1 => "Meta Llama 3 8B Instruct V1",
218            Self::MetaLlama370BInstructV1 => "Meta Llama 3 70B Instruct V1",
219            Self::MetaLlama318BInstructV1_128k => "Meta Llama 3 1.8B Instruct V1 128K",
220            Self::MetaLlama318BInstructV1 => "Meta Llama 3 1.8B Instruct V1",
221            Self::MetaLlama3170BInstructV1_128k => "Meta Llama 3 1 70B Instruct V1 128K",
222            Self::MetaLlama3170BInstructV1 => "Meta Llama 3 1 70B Instruct V1",
223            Self::MetaLlama3211BInstructV1 => "Meta Llama 3 2 11B Instruct V1",
224            Self::MetaLlama3290BInstructV1 => "Meta Llama 3 2 90B Instruct V1",
225            Self::MetaLlama321BInstructV1 => "Meta Llama 3 2 1B Instruct V1",
226            Self::MetaLlama323BInstructV1 => "Meta Llama 3 2 3B Instruct V1",
227            Self::MistralMistral7BInstructV0 => "Mistral 7B Instruct V0",
228            Self::MistralMixtral8x7BInstructV0 => "Mistral Mixtral 8x7B Instruct V0",
229            Self::MistralMistralLarge2402V1 => "Mistral Large 2402 V1",
230            Self::MistralMistralSmall2402V1 => "Mistral Small 2402 V1",
231            Self::MistralPixtralLarge2502V1 => "Pixtral Large 25.02 V1",
232            Self::PalmyraWriterX5 => "Writer Palmyra X5",
233            Self::PalmyraWriterX4 => "Writer Palmyra X4",
234            Self::Custom {
235                display_name, name, ..
236            } => display_name.as_deref().unwrap_or(name),
237        }
238    }
239
240    pub fn max_token_count(&self) -> usize {
241        match self {
242            Self::Claude3_5SonnetV2
243            | Self::Claude3Opus
244            | Self::Claude3Sonnet
245            | Self::Claude3_5Haiku
246            | Self::Claude3_7Sonnet
247            | Self::ClaudeSonnet4
248            | Self::ClaudeOpus4 => 200_000,
249            Self::AmazonNovaPremier => 1_000_000,
250            Self::PalmyraWriterX5 => 1_000_000,
251            Self::PalmyraWriterX4 => 128_000,
252            Self::Custom { max_tokens, .. } => *max_tokens,
253            _ => 128_000,
254        }
255    }
256
257    pub fn max_output_tokens(&self) -> u32 {
258        match self {
259            Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
260            Self::Claude3_7Sonnet
261            | Self::Claude3_7SonnetThinking
262            | Self::ClaudeSonnet4
263            | Self::ClaudeSonnet4Thinking
264            | Self::ClaudeOpus4
265            | Model::ClaudeOpus4Thinking => 128_000,
266            Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
267            Self::Custom {
268                max_output_tokens, ..
269            } => max_output_tokens.unwrap_or(4_096),
270            _ => 4_096,
271        }
272    }
273
274    pub fn default_temperature(&self) -> f32 {
275        match self {
276            Self::Claude3_5SonnetV2
277            | Self::Claude3Opus
278            | Self::Claude3Sonnet
279            | Self::Claude3_5Haiku
280            | Self::Claude3_7Sonnet
281            | Self::ClaudeOpus4
282            | Self::ClaudeOpus4Thinking
283            | Self::ClaudeSonnet4
284            | Self::ClaudeSonnet4Thinking => 1.0,
285            Self::Custom {
286                default_temperature,
287                ..
288            } => default_temperature.unwrap_or(1.0),
289            _ => 1.0,
290        }
291    }
292
293    pub fn supports_tool_use(&self) -> bool {
294        match self {
295            // Anthropic Claude 3 models (all support tool use)
296            Self::Claude3Opus
297            | Self::Claude3Sonnet
298            | Self::Claude3_5Sonnet
299            | Self::Claude3_5SonnetV2
300            | Self::Claude3_7Sonnet
301            | Self::Claude3_7SonnetThinking
302            | Self::ClaudeOpus4
303            | Self::ClaudeOpus4Thinking
304            | Self::ClaudeSonnet4
305            | Self::ClaudeSonnet4Thinking
306            | Self::Claude3_5Haiku => true,
307
308            // Amazon Nova models (all support tool use)
309            Self::AmazonNovaPremier
310            | Self::AmazonNovaPro
311            | Self::AmazonNovaLite
312            | Self::AmazonNovaMicro => true,
313
314            // AI21 Jamba 1.5 models support tool use
315            Self::AI21Jamba15LargeV1 | Self::AI21Jamba15MiniV1 => true,
316
317            // Cohere Command R models support tool use
318            Self::CohereCommandRV1 | Self::CohereCommandRPlusV1 => true,
319
320            // All other models don't support tool use
321            // Including Meta Llama 3.2, AI21 Jurassic, and others
322            _ => false,
323        }
324    }
325
326    pub fn mode(&self) -> BedrockModelMode {
327        match self {
328            Model::Claude3_7SonnetThinking => BedrockModelMode::Thinking {
329                budget_tokens: Some(4096),
330            },
331            Model::ClaudeSonnet4Thinking => BedrockModelMode::Thinking {
332                budget_tokens: Some(4096),
333            },
334            Model::ClaudeOpus4Thinking => BedrockModelMode::Thinking {
335                budget_tokens: Some(4096),
336            },
337            _ => BedrockModelMode::Default,
338        }
339    }
340
341    pub fn cross_region_inference_id(&self, region: &str) -> anyhow::Result<String> {
342        let region_group = if region.starts_with("us-gov-") {
343            "us-gov"
344        } else if region.starts_with("us-") {
345            "us"
346        } else if region.starts_with("eu-") {
347            "eu"
348        } else if region.starts_with("ap-") || region == "me-central-1" || region == "me-south-1" {
349            "apac"
350        } else if region.starts_with("ca-") || region.starts_with("sa-") {
351            // Canada and South America regions - default to US profiles
352            "us"
353        } else {
354            anyhow::bail!("Unsupported Region {region}");
355        };
356
357        let model_id = self.id();
358
359        match (self, region_group) {
360            // Custom models can't have CRI IDs
361            (Model::Custom { .. }, _) => Ok(self.id().into()),
362
363            // Models with US Gov only
364            (Model::Claude3_5Sonnet, "us-gov") | (Model::Claude3Haiku, "us-gov") => {
365                Ok(format!("{}.{}", region_group, model_id))
366            }
367
368            // Models available only in US
369            (Model::Claude3Opus, "us")
370            | (Model::Claude3_5Haiku, "us")
371            | (Model::Claude3_7Sonnet, "us")
372            | (Model::ClaudeSonnet4, "us")
373            | (Model::ClaudeOpus4, "us")
374            | (Model::ClaudeSonnet4Thinking, "us")
375            | (Model::ClaudeOpus4Thinking, "us")
376            | (Model::Claude3_7SonnetThinking, "us")
377            | (Model::AmazonNovaPremier, "us")
378            | (Model::MistralPixtralLarge2502V1, "us") => {
379                Ok(format!("{}.{}", region_group, model_id))
380            }
381
382            // Models available in US, EU, and APAC
383            (Model::Claude3_5SonnetV2, "us")
384            | (Model::Claude3_5SonnetV2, "apac")
385            | (Model::Claude3_5Sonnet, _)
386            | (Model::Claude3Haiku, _)
387            | (Model::Claude3Sonnet, _)
388            | (Model::AmazonNovaLite, _)
389            | (Model::AmazonNovaMicro, _)
390            | (Model::AmazonNovaPro, _) => Ok(format!("{}.{}", region_group, model_id)),
391
392            // Models with limited EU availability
393            (Model::MetaLlama321BInstructV1, "us")
394            | (Model::MetaLlama321BInstructV1, "eu")
395            | (Model::MetaLlama323BInstructV1, "us")
396            | (Model::MetaLlama323BInstructV1, "eu") => {
397                Ok(format!("{}.{}", region_group, model_id))
398            }
399
400            // US-only models (all remaining Meta models)
401            (Model::MetaLlama38BInstructV1, "us")
402            | (Model::MetaLlama370BInstructV1, "us")
403            | (Model::MetaLlama318BInstructV1, "us")
404            | (Model::MetaLlama318BInstructV1_128k, "us")
405            | (Model::MetaLlama3170BInstructV1, "us")
406            | (Model::MetaLlama3170BInstructV1_128k, "us")
407            | (Model::MetaLlama3211BInstructV1, "us")
408            | (Model::MetaLlama3290BInstructV1, "us") => {
409                Ok(format!("{}.{}", region_group, model_id))
410            }
411
412            // Writer models only available in the US
413            (Model::PalmyraWriterX4, "us") | (Model::PalmyraWriterX5, "us") => {
414                // They have some goofiness
415                Ok(format!("{}.{}", region_group, model_id))
416            }
417
418            // Any other combination is not supported
419            _ => Ok(self.id().into()),
420        }
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    #[test]
429    fn test_us_region_inference_ids() -> anyhow::Result<()> {
430        // Test US regions
431        assert_eq!(
432            Model::Claude3_5SonnetV2.cross_region_inference_id("us-east-1")?,
433            "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
434        );
435        assert_eq!(
436            Model::Claude3_5SonnetV2.cross_region_inference_id("us-west-2")?,
437            "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
438        );
439        assert_eq!(
440            Model::AmazonNovaPro.cross_region_inference_id("us-east-2")?,
441            "us.amazon.nova-pro-v1:0"
442        );
443        Ok(())
444    }
445
446    #[test]
447    fn test_eu_region_inference_ids() -> anyhow::Result<()> {
448        // Test European regions
449        assert_eq!(
450            Model::Claude3Sonnet.cross_region_inference_id("eu-west-1")?,
451            "eu.anthropic.claude-3-sonnet-20240229-v1:0"
452        );
453        assert_eq!(
454            Model::AmazonNovaMicro.cross_region_inference_id("eu-north-1")?,
455            "eu.amazon.nova-micro-v1:0"
456        );
457        Ok(())
458    }
459
460    #[test]
461    fn test_apac_region_inference_ids() -> anyhow::Result<()> {
462        // Test Asia-Pacific regions
463        assert_eq!(
464            Model::Claude3_5SonnetV2.cross_region_inference_id("ap-northeast-1")?,
465            "apac.anthropic.claude-3-5-sonnet-20241022-v2:0"
466        );
467        assert_eq!(
468            Model::AmazonNovaLite.cross_region_inference_id("ap-south-1")?,
469            "apac.amazon.nova-lite-v1:0"
470        );
471        Ok(())
472    }
473
474    #[test]
475    fn test_gov_region_inference_ids() -> anyhow::Result<()> {
476        // Test Government regions
477        assert_eq!(
478            Model::Claude3_5Sonnet.cross_region_inference_id("us-gov-east-1")?,
479            "us-gov.anthropic.claude-3-5-sonnet-20240620-v1:0"
480        );
481        assert_eq!(
482            Model::Claude3Haiku.cross_region_inference_id("us-gov-west-1")?,
483            "us-gov.anthropic.claude-3-haiku-20240307-v1:0"
484        );
485        Ok(())
486    }
487
488    #[test]
489    fn test_meta_models_inference_ids() -> anyhow::Result<()> {
490        // Test Meta models
491        assert_eq!(
492            Model::MetaLlama370BInstructV1.cross_region_inference_id("us-east-1")?,
493            "us.meta.llama3-70b-instruct-v1:0"
494        );
495        assert_eq!(
496            Model::MetaLlama321BInstructV1.cross_region_inference_id("eu-west-1")?,
497            "eu.meta.llama3-2-1b-instruct-v1:0"
498        );
499        Ok(())
500    }
501
502    #[test]
503    fn test_mistral_models_inference_ids() -> anyhow::Result<()> {
504        // Mistral models don't follow the regional prefix pattern,
505        // so they should return their original IDs
506        assert_eq!(
507            Model::MistralMistralLarge2402V1.cross_region_inference_id("us-east-1")?,
508            "mistral.mistral-large-2402-v1:0"
509        );
510        assert_eq!(
511            Model::MistralMixtral8x7BInstructV0.cross_region_inference_id("eu-west-1")?,
512            "mistral.mixtral-8x7b-instruct-v0:1"
513        );
514        Ok(())
515    }
516
517    #[test]
518    fn test_ai21_models_inference_ids() -> anyhow::Result<()> {
519        // AI21 models don't follow the regional prefix pattern,
520        // so they should return their original IDs
521        assert_eq!(
522            Model::AI21J2UltraV1.cross_region_inference_id("us-east-1")?,
523            "ai21.j2-ultra-v1"
524        );
525        assert_eq!(
526            Model::AI21JambaInstructV1.cross_region_inference_id("eu-west-1")?,
527            "ai21.jamba-instruct-v1:0"
528        );
529        Ok(())
530    }
531
532    #[test]
533    fn test_cohere_models_inference_ids() -> anyhow::Result<()> {
534        // Cohere models don't follow the regional prefix pattern,
535        // so they should return their original IDs
536        assert_eq!(
537            Model::CohereCommandRV1.cross_region_inference_id("us-east-1")?,
538            "cohere.command-r-v1:0"
539        );
540        assert_eq!(
541            Model::CohereCommandTextV14_4k.cross_region_inference_id("ap-southeast-1")?,
542            "cohere.command-text-v14:7:4k"
543        );
544        Ok(())
545    }
546
547    #[test]
548    fn test_custom_model_inference_ids() -> anyhow::Result<()> {
549        // Test custom models
550        let custom_model = Model::Custom {
551            name: "custom.my-model-v1:0".to_string(),
552            max_tokens: 100000,
553            display_name: Some("My Custom Model".to_string()),
554            max_output_tokens: Some(8192),
555            default_temperature: Some(0.7),
556        };
557
558        // Custom model should return its name unchanged
559        assert_eq!(
560            custom_model.cross_region_inference_id("us-east-1")?,
561            "custom.my-model-v1:0"
562        );
563
564        Ok(())
565    }
566}