models.rs

   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    Max,
  12}
  13
  14impl BedrockAdaptiveThinkingEffort {
  15    pub fn as_str(&self) -> &'static str {
  16        match self {
  17            Self::Low => "low",
  18            Self::Medium => "medium",
  19            Self::High => "high",
  20            Self::Max => "max",
  21        }
  22    }
  23}
  24
  25#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
  26#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
  27pub enum BedrockModelMode {
  28    #[default]
  29    Default,
  30    Thinking {
  31        budget_tokens: Option<u64>,
  32    },
  33    AdaptiveThinking {
  34        effort: BedrockAdaptiveThinkingEffort,
  35    },
  36}
  37
  38#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
  39#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
  40pub struct BedrockModelCacheConfiguration {
  41    pub max_cache_anchors: usize,
  42    pub min_total_token: u64,
  43}
  44
  45#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
  46#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
  47pub enum Model {
  48    // Anthropic models (already included)
  49    #[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
  50    ClaudeSonnet4,
  51    #[serde(
  52        rename = "claude-sonnet-4-thinking",
  53        alias = "claude-sonnet-4-thinking-latest"
  54    )]
  55    ClaudeSonnet4Thinking,
  56    #[default]
  57    #[serde(rename = "claude-sonnet-4-5", alias = "claude-sonnet-4-5-latest")]
  58    ClaudeSonnet4_5,
  59    #[serde(
  60        rename = "claude-sonnet-4-5-thinking",
  61        alias = "claude-sonnet-4-5-thinking-latest"
  62    )]
  63    ClaudeSonnet4_5Thinking,
  64    #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
  65    ClaudeOpus4,
  66    #[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")]
  67    ClaudeOpus4_1,
  68    #[serde(
  69        rename = "claude-opus-4-thinking",
  70        alias = "claude-opus-4-thinking-latest"
  71    )]
  72    ClaudeOpus4Thinking,
  73    #[serde(
  74        rename = "claude-opus-4-1-thinking",
  75        alias = "claude-opus-4-1-thinking-latest"
  76    )]
  77    ClaudeOpus4_1Thinking,
  78    #[serde(rename = "claude-opus-4-5", alias = "claude-opus-4-5-latest")]
  79    ClaudeOpus4_5,
  80    #[serde(
  81        rename = "claude-opus-4-5-thinking",
  82        alias = "claude-opus-4-5-thinking-latest"
  83    )]
  84    ClaudeOpus4_5Thinking,
  85    #[serde(rename = "claude-opus-4-6", alias = "claude-opus-4-6-latest")]
  86    ClaudeOpus4_6,
  87    #[serde(
  88        rename = "claude-opus-4-6-thinking",
  89        alias = "claude-opus-4-6-thinking-latest"
  90    )]
  91    ClaudeOpus4_6Thinking,
  92    #[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
  93    Claude3_5SonnetV2,
  94    #[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
  95    Claude3_7Sonnet,
  96    #[serde(
  97        rename = "claude-3-7-sonnet-thinking",
  98        alias = "claude-3-7-sonnet-thinking-latest"
  99    )]
 100    Claude3_7SonnetThinking,
 101    #[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
 102    Claude3Opus,
 103    #[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-latest")]
 104    Claude3Sonnet,
 105    #[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
 106    Claude3_5Haiku,
 107    #[serde(rename = "claude-haiku-4-5", alias = "claude-haiku-4-5-latest")]
 108    ClaudeHaiku4_5,
 109    Claude3_5Sonnet,
 110    Claude3Haiku,
 111    // Amazon Nova Models
 112    AmazonNovaLite,
 113    AmazonNovaMicro,
 114    AmazonNovaPro,
 115    AmazonNovaPremier,
 116    // AI21 models
 117    AI21J2GrandeInstruct,
 118    AI21J2JumboInstruct,
 119    AI21J2Mid,
 120    AI21J2MidV1,
 121    AI21J2Ultra,
 122    AI21J2UltraV1_8k,
 123    AI21J2UltraV1,
 124    AI21JambaInstructV1,
 125    AI21Jamba15LargeV1,
 126    AI21Jamba15MiniV1,
 127    // Cohere models
 128    CohereCommandTextV14_4k,
 129    CohereCommandRV1,
 130    CohereCommandRPlusV1,
 131    CohereCommandLightTextV14_4k,
 132    // DeepSeek
 133    DeepSeekR1,
 134    // Meta models
 135    MetaLlama38BInstructV1,
 136    MetaLlama370BInstructV1,
 137    MetaLlama318BInstructV1_128k,
 138    MetaLlama318BInstructV1,
 139    MetaLlama3170BInstructV1_128k,
 140    MetaLlama3170BInstructV1,
 141    MetaLlama31405BInstructV1,
 142    MetaLlama321BInstructV1,
 143    MetaLlama323BInstructV1,
 144    MetaLlama3211BInstructV1,
 145    MetaLlama3290BInstructV1,
 146    MetaLlama3370BInstructV1,
 147    #[allow(non_camel_case_types)]
 148    MetaLlama4Scout17BInstructV1,
 149    #[allow(non_camel_case_types)]
 150    MetaLlama4Maverick17BInstructV1,
 151    // Mistral models
 152    MistralMistral7BInstructV0,
 153    MistralMixtral8x7BInstructV0,
 154    MistralMistralLarge2402V1,
 155    MistralMistralSmall2402V1,
 156    MistralPixtralLarge2502V1,
 157    // Writer models
 158    PalmyraWriterX5,
 159    PalmyraWriterX4,
 160    #[serde(rename = "custom")]
 161    Custom {
 162        name: String,
 163        max_tokens: u64,
 164        /// The name displayed in the UI, such as in the assistant panel model dropdown menu.
 165        display_name: Option<String>,
 166        max_output_tokens: Option<u64>,
 167        default_temperature: Option<f32>,
 168        cache_configuration: Option<BedrockModelCacheConfiguration>,
 169    },
 170}
 171
 172impl Model {
 173    pub fn default_fast(region: &str) -> Self {
 174        if region.starts_with("us-") {
 175            Self::Claude3_5Haiku
 176        } else {
 177            Self::Claude3Haiku
 178        }
 179    }
 180
 181    pub fn from_id(id: &str) -> anyhow::Result<Self> {
 182        if id.starts_with("claude-opus-4-6-thinking") {
 183            Ok(Self::ClaudeOpus4_6Thinking)
 184        } else if id.starts_with("claude-opus-4-6") {
 185            Ok(Self::ClaudeOpus4_6)
 186        } else if id.starts_with("claude-opus-4-5-thinking") {
 187            Ok(Self::ClaudeOpus4_5Thinking)
 188        } else if id.starts_with("claude-opus-4-5") {
 189            Ok(Self::ClaudeOpus4_5)
 190        } else if id.starts_with("claude-opus-4-1-thinking") {
 191            Ok(Self::ClaudeOpus4_1Thinking)
 192        } else if id.starts_with("claude-opus-4-1") {
 193            Ok(Self::ClaudeOpus4_1)
 194        } else if id.starts_with("claude-opus-4-thinking") {
 195            Ok(Self::ClaudeOpus4Thinking)
 196        } else if id.starts_with("claude-opus-4") {
 197            Ok(Self::ClaudeOpus4)
 198        } else if id.starts_with("claude-3-5-sonnet-v2") {
 199            Ok(Self::Claude3_5SonnetV2)
 200        } else if id.starts_with("claude-3-opus") {
 201            Ok(Self::Claude3Opus)
 202        } else if id.starts_with("claude-3-sonnet") {
 203            Ok(Self::Claude3Sonnet)
 204        } else if id.starts_with("claude-3-5-haiku") {
 205            Ok(Self::Claude3_5Haiku)
 206        } else if id.starts_with("claude-haiku-4-5") {
 207            Ok(Self::ClaudeHaiku4_5)
 208        } else if id.starts_with("claude-3-7-sonnet") {
 209            Ok(Self::Claude3_7Sonnet)
 210        } else if id.starts_with("claude-3-7-sonnet-thinking") {
 211            Ok(Self::Claude3_7SonnetThinking)
 212        } else if id.starts_with("claude-sonnet-4-5-thinking") {
 213            Ok(Self::ClaudeSonnet4_5Thinking)
 214        } else if id.starts_with("claude-sonnet-4-5") {
 215            Ok(Self::ClaudeSonnet4_5)
 216        } else if id.starts_with("claude-sonnet-4-thinking") {
 217            Ok(Self::ClaudeSonnet4Thinking)
 218        } else if id.starts_with("claude-sonnet-4") {
 219            Ok(Self::ClaudeSonnet4)
 220        } else {
 221            anyhow::bail!("invalid model id {id}");
 222        }
 223    }
 224
 225    pub fn id(&self) -> &str {
 226        match self {
 227            Model::ClaudeSonnet4 => "claude-sonnet-4",
 228            Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking",
 229            Model::ClaudeSonnet4_5 => "claude-sonnet-4-5",
 230            Model::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-thinking",
 231            Model::ClaudeOpus4 => "claude-opus-4",
 232            Model::ClaudeOpus4_1 => "claude-opus-4-1",
 233            Model::ClaudeOpus4Thinking => "claude-opus-4-thinking",
 234            Model::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking",
 235            Model::ClaudeOpus4_5 => "claude-opus-4-5",
 236            Model::ClaudeOpus4_5Thinking => "claude-opus-4-5-thinking",
 237            Model::ClaudeOpus4_6 => "claude-opus-4-6",
 238            Model::ClaudeOpus4_6Thinking => "claude-opus-4-6-thinking",
 239            Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
 240            Model::Claude3_5Sonnet => "claude-3-5-sonnet",
 241            Model::Claude3Opus => "claude-3-opus",
 242            Model::Claude3Sonnet => "claude-3-sonnet",
 243            Model::Claude3Haiku => "claude-3-haiku",
 244            Model::Claude3_5Haiku => "claude-3-5-haiku",
 245            Model::ClaudeHaiku4_5 => "claude-haiku-4-5",
 246            Model::Claude3_7Sonnet => "claude-3-7-sonnet",
 247            Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking",
 248            Model::AmazonNovaLite => "amazon-nova-lite",
 249            Model::AmazonNovaMicro => "amazon-nova-micro",
 250            Model::AmazonNovaPro => "amazon-nova-pro",
 251            Model::AmazonNovaPremier => "amazon-nova-premier",
 252            Model::DeepSeekR1 => "deepseek-r1",
 253            Model::AI21J2GrandeInstruct => "ai21-j2-grande-instruct",
 254            Model::AI21J2JumboInstruct => "ai21-j2-jumbo-instruct",
 255            Model::AI21J2Mid => "ai21-j2-mid",
 256            Model::AI21J2MidV1 => "ai21-j2-mid-v1",
 257            Model::AI21J2Ultra => "ai21-j2-ultra",
 258            Model::AI21J2UltraV1_8k => "ai21-j2-ultra-v1-8k",
 259            Model::AI21J2UltraV1 => "ai21-j2-ultra-v1",
 260            Model::AI21JambaInstructV1 => "ai21-jamba-instruct-v1",
 261            Model::AI21Jamba15LargeV1 => "ai21-jamba-1-5-large-v1",
 262            Model::AI21Jamba15MiniV1 => "ai21-jamba-1-5-mini-v1",
 263            Model::CohereCommandTextV14_4k => "cohere-command-text-v14-4k",
 264            Model::CohereCommandRV1 => "cohere-command-r-v1",
 265            Model::CohereCommandRPlusV1 => "cohere-command-r-plus-v1",
 266            Model::CohereCommandLightTextV14_4k => "cohere-command-light-text-v14-4k",
 267            Model::MetaLlama38BInstructV1 => "meta-llama3-8b-instruct-v1",
 268            Model::MetaLlama370BInstructV1 => "meta-llama3-70b-instruct-v1",
 269            Model::MetaLlama318BInstructV1_128k => "meta-llama3-1-8b-instruct-v1-128k",
 270            Model::MetaLlama318BInstructV1 => "meta-llama3-1-8b-instruct-v1",
 271            Model::MetaLlama3170BInstructV1_128k => "meta-llama3-1-70b-instruct-v1-128k",
 272            Model::MetaLlama3170BInstructV1 => "meta-llama3-1-70b-instruct-v1",
 273            Model::MetaLlama31405BInstructV1 => "meta-llama3-1-405b-instruct-v1",
 274            Model::MetaLlama321BInstructV1 => "meta-llama3-2-1b-instruct-v1",
 275            Model::MetaLlama323BInstructV1 => "meta-llama3-2-3b-instruct-v1",
 276            Model::MetaLlama3211BInstructV1 => "meta-llama3-2-11b-instruct-v1",
 277            Model::MetaLlama3290BInstructV1 => "meta-llama3-2-90b-instruct-v1",
 278            Model::MetaLlama3370BInstructV1 => "meta-llama3-3-70b-instruct-v1",
 279            Model::MetaLlama4Scout17BInstructV1 => "meta-llama4-scout-17b-instruct-v1",
 280            Model::MetaLlama4Maverick17BInstructV1 => "meta-llama4-maverick-17b-instruct-v1",
 281            Model::MistralMistral7BInstructV0 => "mistral-7b-instruct-v0",
 282            Model::MistralMixtral8x7BInstructV0 => "mistral-mixtral-8x7b-instruct-v0",
 283            Model::MistralMistralLarge2402V1 => "mistral-large-2402-v1",
 284            Model::MistralMistralSmall2402V1 => "mistral-small-2402-v1",
 285            Model::MistralPixtralLarge2502V1 => "mistral-pixtral-large-2502-v1",
 286            Model::PalmyraWriterX4 => "palmyra-writer-x4",
 287            Model::PalmyraWriterX5 => "palmyra-writer-x5",
 288            Self::Custom { name, .. } => name,
 289        }
 290    }
 291
 292    pub fn request_id(&self) -> &str {
 293        match self {
 294            Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => {
 295                "anthropic.claude-sonnet-4-20250514-v1:0"
 296            }
 297            Model::ClaudeSonnet4_5 | Model::ClaudeSonnet4_5Thinking => {
 298                "anthropic.claude-sonnet-4-5-20250929-v1:0"
 299            }
 300            Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => {
 301                "anthropic.claude-opus-4-20250514-v1:0"
 302            }
 303            Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => {
 304                "anthropic.claude-opus-4-1-20250805-v1:0"
 305            }
 306            Model::ClaudeOpus4_5 | Model::ClaudeOpus4_5Thinking => {
 307                "anthropic.claude-opus-4-5-20251101-v1:0"
 308            }
 309            Model::ClaudeOpus4_6 | Model::ClaudeOpus4_6Thinking => "anthropic.claude-opus-4-6-v1",
 310            Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
 311            Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
 312            Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
 313            Model::Claude3Sonnet => "anthropic.claude-3-sonnet-20240229-v1:0",
 314            Model::Claude3Haiku => "anthropic.claude-3-haiku-20240307-v1:0",
 315            Model::Claude3_5Haiku => "anthropic.claude-3-5-haiku-20241022-v1:0",
 316            Model::ClaudeHaiku4_5 => "anthropic.claude-haiku-4-5-20251001-v1:0",
 317            Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => {
 318                "anthropic.claude-3-7-sonnet-20250219-v1:0"
 319            }
 320            Model::AmazonNovaLite => "amazon.nova-lite-v1:0",
 321            Model::AmazonNovaMicro => "amazon.nova-micro-v1:0",
 322            Model::AmazonNovaPro => "amazon.nova-pro-v1:0",
 323            Model::AmazonNovaPremier => "amazon.nova-premier-v1:0",
 324            Model::DeepSeekR1 => "deepseek.r1-v1:0",
 325            Model::AI21J2GrandeInstruct => "ai21.j2-grande-instruct",
 326            Model::AI21J2JumboInstruct => "ai21.j2-jumbo-instruct",
 327            Model::AI21J2Mid => "ai21.j2-mid",
 328            Model::AI21J2MidV1 => "ai21.j2-mid-v1",
 329            Model::AI21J2Ultra => "ai21.j2-ultra",
 330            Model::AI21J2UltraV1_8k => "ai21.j2-ultra-v1:0:8k",
 331            Model::AI21J2UltraV1 => "ai21.j2-ultra-v1",
 332            Model::AI21JambaInstructV1 => "ai21.jamba-instruct-v1:0",
 333            Model::AI21Jamba15LargeV1 => "ai21.jamba-1-5-large-v1:0",
 334            Model::AI21Jamba15MiniV1 => "ai21.jamba-1-5-mini-v1:0",
 335            Model::CohereCommandTextV14_4k => "cohere.command-text-v14:7:4k",
 336            Model::CohereCommandRV1 => "cohere.command-r-v1:0",
 337            Model::CohereCommandRPlusV1 => "cohere.command-r-plus-v1:0",
 338            Model::CohereCommandLightTextV14_4k => "cohere.command-light-text-v14:7:4k",
 339            Model::MetaLlama38BInstructV1 => "meta.llama3-8b-instruct-v1:0",
 340            Model::MetaLlama370BInstructV1 => "meta.llama3-70b-instruct-v1:0",
 341            Model::MetaLlama318BInstructV1_128k => "meta.llama3-1-8b-instruct-v1:0",
 342            Model::MetaLlama318BInstructV1 => "meta.llama3-1-8b-instruct-v1:0",
 343            Model::MetaLlama3170BInstructV1_128k => "meta.llama3-1-70b-instruct-v1:0",
 344            Model::MetaLlama3170BInstructV1 => "meta.llama3-1-70b-instruct-v1:0",
 345            Model::MetaLlama31405BInstructV1 => "meta.llama3-1-405b-instruct-v1:0",
 346            Model::MetaLlama3211BInstructV1 => "meta.llama3-2-11b-instruct-v1:0",
 347            Model::MetaLlama3290BInstructV1 => "meta.llama3-2-90b-instruct-v1:0",
 348            Model::MetaLlama321BInstructV1 => "meta.llama3-2-1b-instruct-v1:0",
 349            Model::MetaLlama323BInstructV1 => "meta.llama3-2-3b-instruct-v1:0",
 350            Model::MetaLlama3370BInstructV1 => "meta.llama3-3-70b-instruct-v1:0",
 351            Model::MetaLlama4Scout17BInstructV1 => "meta.llama4-scout-17b-instruct-v1:0",
 352            Model::MetaLlama4Maverick17BInstructV1 => "meta.llama4-maverick-17b-instruct-v1:0",
 353            Model::MistralMistral7BInstructV0 => "mistral.mistral-7b-instruct-v0:2",
 354            Model::MistralMixtral8x7BInstructV0 => "mistral.mixtral-8x7b-instruct-v0:1",
 355            Model::MistralMistralLarge2402V1 => "mistral.mistral-large-2402-v1:0",
 356            Model::MistralMistralSmall2402V1 => "mistral.mistral-small-2402-v1:0",
 357            Model::MistralPixtralLarge2502V1 => "mistral.pixtral-large-2502-v1:0",
 358            Model::PalmyraWriterX4 => "writer.palmyra-x4-v1:0",
 359            Model::PalmyraWriterX5 => "writer.palmyra-x5-v1:0",
 360            Self::Custom { name, .. } => name,
 361        }
 362    }
 363
 364    pub fn display_name(&self) -> &str {
 365        match self {
 366            Self::ClaudeSonnet4 => "Claude Sonnet 4",
 367            Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
 368            Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
 369            Self::ClaudeSonnet4_5Thinking => "Claude Sonnet 4.5 Thinking",
 370            Self::ClaudeOpus4 => "Claude Opus 4",
 371            Self::ClaudeOpus4_1 => "Claude Opus 4.1",
 372            Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
 373            Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
 374            Self::ClaudeOpus4_5 => "Claude Opus 4.5",
 375            Self::ClaudeOpus4_5Thinking => "Claude Opus 4.5 Thinking",
 376            Self::ClaudeOpus4_6 => "Claude Opus 4.6",
 377            Self::ClaudeOpus4_6Thinking => "Claude Opus 4.6 Thinking",
 378            Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
 379            Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
 380            Self::Claude3Opus => "Claude 3 Opus",
 381            Self::Claude3Sonnet => "Claude 3 Sonnet",
 382            Self::Claude3Haiku => "Claude 3 Haiku",
 383            Self::Claude3_5Haiku => "Claude 3.5 Haiku",
 384            Self::ClaudeHaiku4_5 => "Claude Haiku 4.5",
 385            Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
 386            Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
 387            Self::AmazonNovaLite => "Amazon Nova Lite",
 388            Self::AmazonNovaMicro => "Amazon Nova Micro",
 389            Self::AmazonNovaPro => "Amazon Nova Pro",
 390            Self::AmazonNovaPremier => "Amazon Nova Premier",
 391            Self::DeepSeekR1 => "DeepSeek R1",
 392            Self::AI21J2GrandeInstruct => "AI21 Jurassic2 Grande Instruct",
 393            Self::AI21J2JumboInstruct => "AI21 Jurassic2 Jumbo Instruct",
 394            Self::AI21J2Mid => "AI21 Jurassic2 Mid",
 395            Self::AI21J2MidV1 => "AI21 Jurassic2 Mid V1",
 396            Self::AI21J2Ultra => "AI21 Jurassic2 Ultra",
 397            Self::AI21J2UltraV1_8k => "AI21 Jurassic2 Ultra V1 8K",
 398            Self::AI21J2UltraV1 => "AI21 Jurassic2 Ultra V1",
 399            Self::AI21JambaInstructV1 => "AI21 Jamba Instruct",
 400            Self::AI21Jamba15LargeV1 => "AI21 Jamba 1.5 Large",
 401            Self::AI21Jamba15MiniV1 => "AI21 Jamba 1.5 Mini",
 402            Self::CohereCommandTextV14_4k => "Cohere Command Text V14 4K",
 403            Self::CohereCommandRV1 => "Cohere Command R V1",
 404            Self::CohereCommandRPlusV1 => "Cohere Command R Plus V1",
 405            Self::CohereCommandLightTextV14_4k => "Cohere Command Light Text V14 4K",
 406            Self::MetaLlama38BInstructV1 => "Meta Llama 3 8B Instruct",
 407            Self::MetaLlama370BInstructV1 => "Meta Llama 3 70B Instruct",
 408            Self::MetaLlama318BInstructV1_128k => "Meta Llama 3.1 8B Instruct 128K",
 409            Self::MetaLlama318BInstructV1 => "Meta Llama 3.1 8B Instruct",
 410            Self::MetaLlama3170BInstructV1_128k => "Meta Llama 3.1 70B Instruct 128K",
 411            Self::MetaLlama3170BInstructV1 => "Meta Llama 3.1 70B Instruct",
 412            Self::MetaLlama31405BInstructV1 => "Meta Llama 3.1 405B Instruct",
 413            Self::MetaLlama3211BInstructV1 => "Meta Llama 3.2 11B Instruct",
 414            Self::MetaLlama3290BInstructV1 => "Meta Llama 3.2 90B Instruct",
 415            Self::MetaLlama321BInstructV1 => "Meta Llama 3.2 1B Instruct",
 416            Self::MetaLlama323BInstructV1 => "Meta Llama 3.2 3B Instruct",
 417            Self::MetaLlama3370BInstructV1 => "Meta Llama 3.3 70B Instruct",
 418            Self::MetaLlama4Scout17BInstructV1 => "Meta Llama 4 Scout 17B Instruct",
 419            Self::MetaLlama4Maverick17BInstructV1 => "Meta Llama 4 Maverick 17B Instruct",
 420            Self::MistralMistral7BInstructV0 => "Mistral 7B Instruct V0",
 421            Self::MistralMixtral8x7BInstructV0 => "Mistral Mixtral 8x7B Instruct V0",
 422            Self::MistralMistralLarge2402V1 => "Mistral Large 2402 V1",
 423            Self::MistralMistralSmall2402V1 => "Mistral Small 2402 V1",
 424            Self::MistralPixtralLarge2502V1 => "Pixtral Large 25.02 V1",
 425            Self::PalmyraWriterX5 => "Writer Palmyra X5",
 426            Self::PalmyraWriterX4 => "Writer Palmyra X4",
 427            Self::Custom {
 428                display_name, name, ..
 429            } => display_name.as_deref().unwrap_or(name),
 430        }
 431    }
 432
 433    pub fn max_token_count(&self) -> u64 {
 434        match self {
 435            Self::Claude3_5SonnetV2
 436            | Self::Claude3Opus
 437            | Self::Claude3Sonnet
 438            | Self::Claude3_5Haiku
 439            | Self::ClaudeHaiku4_5
 440            | Self::Claude3_7Sonnet
 441            | Self::ClaudeSonnet4
 442            | Self::ClaudeOpus4
 443            | Self::ClaudeOpus4_1
 444            | Self::ClaudeSonnet4Thinking
 445            | Self::ClaudeSonnet4_5
 446            | Self::ClaudeSonnet4_5Thinking
 447            | Self::ClaudeOpus4Thinking
 448            | Self::ClaudeOpus4_1Thinking
 449            | Self::ClaudeOpus4_5
 450            | Self::ClaudeOpus4_5Thinking
 451            | Self::ClaudeOpus4_6
 452            | Self::ClaudeOpus4_6Thinking => 200_000,
 453            Self::AmazonNovaPremier => 1_000_000,
 454            Self::PalmyraWriterX5 => 1_000_000,
 455            Self::PalmyraWriterX4 => 128_000,
 456            Self::Custom { max_tokens, .. } => *max_tokens,
 457            _ => 128_000,
 458        }
 459    }
 460
 461    pub fn max_output_tokens(&self) -> u64 {
 462        match self {
 463            Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
 464            Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
 465            Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
 466            Self::ClaudeSonnet4_5
 467            | Self::ClaudeSonnet4_5Thinking
 468            | Self::ClaudeHaiku4_5
 469            | Self::ClaudeOpus4_5
 470            | Self::ClaudeOpus4_5Thinking => 64_000,
 471            Self::ClaudeOpus4_6 | Self::ClaudeOpus4_6Thinking => 128_000,
 472            Self::ClaudeOpus4
 473            | Self::ClaudeOpus4Thinking
 474            | Self::ClaudeOpus4_1
 475            | Self::ClaudeOpus4_1Thinking => 32_000,
 476            Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
 477            Self::Custom {
 478                max_output_tokens, ..
 479            } => max_output_tokens.unwrap_or(4_096),
 480            _ => 4_096,
 481        }
 482    }
 483
 484    pub fn default_temperature(&self) -> f32 {
 485        match self {
 486            Self::Claude3_5SonnetV2
 487            | Self::Claude3Opus
 488            | Self::Claude3Sonnet
 489            | Self::Claude3_5Haiku
 490            | Self::ClaudeHaiku4_5
 491            | Self::Claude3_7Sonnet
 492            | Self::ClaudeOpus4
 493            | Self::ClaudeOpus4Thinking
 494            | Self::ClaudeOpus4_1
 495            | Self::ClaudeOpus4_1Thinking
 496            | Self::ClaudeOpus4_5
 497            | Self::ClaudeOpus4_5Thinking
 498            | Self::ClaudeOpus4_6
 499            | Self::ClaudeOpus4_6Thinking
 500            | Self::ClaudeSonnet4
 501            | Self::ClaudeSonnet4Thinking
 502            | Self::ClaudeSonnet4_5
 503            | Self::ClaudeSonnet4_5Thinking => 1.0,
 504            Self::Custom {
 505                default_temperature,
 506                ..
 507            } => default_temperature.unwrap_or(1.0),
 508            _ => 1.0,
 509        }
 510    }
 511
 512    pub fn supports_tool_use(&self) -> bool {
 513        match self {
 514            // Anthropic Claude 3 models (all support tool use)
 515            Self::Claude3Opus
 516            | Self::Claude3Sonnet
 517            | Self::Claude3_5Sonnet
 518            | Self::Claude3_5SonnetV2
 519            | Self::Claude3_7Sonnet
 520            | Self::Claude3_7SonnetThinking
 521            | Self::ClaudeOpus4
 522            | Self::ClaudeOpus4Thinking
 523            | Self::ClaudeOpus4_1
 524            | Self::ClaudeOpus4_1Thinking
 525            | Self::ClaudeOpus4_5
 526            | Self::ClaudeOpus4_5Thinking
 527            | Self::ClaudeOpus4_6
 528            | Self::ClaudeOpus4_6Thinking
 529            | Self::ClaudeSonnet4
 530            | Self::ClaudeSonnet4Thinking
 531            | Self::ClaudeSonnet4_5
 532            | Self::ClaudeSonnet4_5Thinking
 533            | Self::Claude3_5Haiku
 534            | Self::ClaudeHaiku4_5 => true,
 535
 536            // Amazon Nova models (all support tool use)
 537            Self::AmazonNovaPremier
 538            | Self::AmazonNovaPro
 539            | Self::AmazonNovaLite
 540            | Self::AmazonNovaMicro => true,
 541
 542            // AI21 Jamba 1.5 models support tool use
 543            Self::AI21Jamba15LargeV1 | Self::AI21Jamba15MiniV1 => true,
 544
 545            // Cohere Command R models support tool use
 546            Self::CohereCommandRV1 | Self::CohereCommandRPlusV1 => true,
 547
 548            // All other models don't support tool use
 549            // Including Meta Llama 3.2, AI21 Jurassic, and others
 550            _ => false,
 551        }
 552    }
 553
 554    pub fn supports_images(&self) -> bool {
 555        match self {
 556            // Anthropic Claude 3+ models (all support vision)
 557            Self::Claude3Opus
 558            | Self::Claude3Sonnet
 559            | Self::Claude3_5Sonnet
 560            | Self::Claude3_5SonnetV2
 561            | Self::Claude3_7Sonnet
 562            | Self::Claude3_7SonnetThinking
 563            | Self::ClaudeOpus4
 564            | Self::ClaudeOpus4Thinking
 565            | Self::ClaudeOpus4_1
 566            | Self::ClaudeOpus4_1Thinking
 567            | Self::ClaudeOpus4_5
 568            | Self::ClaudeOpus4_5Thinking
 569            | Self::ClaudeSonnet4
 570            | Self::ClaudeSonnet4Thinking
 571            | Self::ClaudeSonnet4_5
 572            | Self::ClaudeSonnet4_5Thinking
 573            | Self::Claude3_5Haiku
 574            | Self::ClaudeHaiku4_5
 575            | Self::Claude3Haiku => true,
 576
 577            // Amazon Nova visual models
 578            Self::AmazonNovaPro | Self::AmazonNovaLite => true,
 579
 580            // Meta Llama 3.2 Vision models
 581            Self::MetaLlama3211BInstructV1 | Self::MetaLlama3290BInstructV1 => true,
 582
 583            // Mistral Pixtral (visual model)
 584            Self::MistralPixtralLarge2502V1 => true,
 585
 586            // Custom models default to no image support
 587            Self::Custom { .. } => false,
 588
 589            // All other models don't support images
 590            _ => false,
 591        }
 592    }
 593
 594    pub fn supports_caching(&self) -> bool {
 595        match self {
 596            // Only Claude models on Bedrock support caching
 597            // Nova models support only text caching
 598            // https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html#prompt-caching-models
 599            Self::Claude3_5Haiku
 600            | Self::ClaudeHaiku4_5
 601            | Self::Claude3_7Sonnet
 602            | Self::Claude3_7SonnetThinking
 603            | Self::ClaudeSonnet4
 604            | Self::ClaudeSonnet4Thinking
 605            | Self::ClaudeSonnet4_5
 606            | Self::ClaudeSonnet4_5Thinking
 607            | Self::ClaudeOpus4
 608            | Self::ClaudeOpus4Thinking
 609            | Self::ClaudeOpus4_1
 610            | Self::ClaudeOpus4_1Thinking
 611            | Self::ClaudeOpus4_5
 612            | Self::ClaudeOpus4_5Thinking
 613            | Self::ClaudeOpus4_6
 614            | Self::ClaudeOpus4_6Thinking => true,
 615
 616            // Custom models - check if they have cache configuration
 617            Self::Custom {
 618                cache_configuration,
 619                ..
 620            } => cache_configuration.is_some(),
 621
 622            // All other models don't support caching
 623            _ => false,
 624        }
 625    }
 626
 627    pub fn cache_configuration(&self) -> Option<BedrockModelCacheConfiguration> {
 628        match self {
 629            Self::Claude3_7Sonnet
 630            | Self::Claude3_7SonnetThinking
 631            | Self::ClaudeSonnet4
 632            | Self::ClaudeSonnet4Thinking
 633            | Self::ClaudeOpus4
 634            | Self::ClaudeOpus4Thinking
 635            | Self::ClaudeOpus4_1
 636            | Self::ClaudeOpus4_1Thinking
 637            | Self::ClaudeOpus4_5
 638            | Self::ClaudeOpus4_5Thinking
 639            | Self::ClaudeOpus4_6
 640            | Self::ClaudeOpus4_6Thinking => Some(BedrockModelCacheConfiguration {
 641                max_cache_anchors: 4,
 642                min_total_token: 1024,
 643            }),
 644
 645            Self::Claude3_5Haiku | Self::ClaudeHaiku4_5 => Some(BedrockModelCacheConfiguration {
 646                max_cache_anchors: 4,
 647                min_total_token: 2048,
 648            }),
 649
 650            Self::Custom {
 651                cache_configuration,
 652                ..
 653            } => cache_configuration.clone(),
 654
 655            _ => None,
 656        }
 657    }
 658
 659    pub fn mode(&self) -> BedrockModelMode {
 660        match self {
 661            Model::Claude3_7SonnetThinking => BedrockModelMode::Thinking {
 662                budget_tokens: Some(4096),
 663            },
 664            Model::ClaudeSonnet4Thinking | Model::ClaudeSonnet4_5Thinking => {
 665                BedrockModelMode::Thinking {
 666                    budget_tokens: Some(4096),
 667                }
 668            }
 669            Model::ClaudeOpus4Thinking
 670            | Model::ClaudeOpus4_1Thinking
 671            | Model::ClaudeOpus4_5Thinking => BedrockModelMode::Thinking {
 672                budget_tokens: Some(4096),
 673            },
 674            Model::ClaudeOpus4_6Thinking => BedrockModelMode::AdaptiveThinking {
 675                effort: BedrockAdaptiveThinkingEffort::default(),
 676            },
 677            _ => BedrockModelMode::Default,
 678        }
 679    }
 680
 681    pub fn supports_extended_context(&self) -> bool {
 682        matches!(
 683            self,
 684            Model::ClaudeSonnet4
 685                | Model::ClaudeSonnet4Thinking
 686                | Model::ClaudeSonnet4_5
 687                | Model::ClaudeSonnet4_5Thinking
 688                | Model::ClaudeOpus4_5
 689                | Model::ClaudeOpus4_5Thinking
 690                | Model::ClaudeOpus4_6
 691                | Model::ClaudeOpus4_6Thinking
 692        )
 693    }
 694
 695    pub fn cross_region_inference_id(
 696        &self,
 697        region: &str,
 698        allow_global: bool,
 699    ) -> anyhow::Result<String> {
 700        // List derived from here:
 701        // https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html#inference-profiles-support-system
 702        let model_id = self.request_id();
 703
 704        let supports_global = matches!(
 705            self,
 706            Model::ClaudeOpus4_5
 707                | Model::ClaudeOpus4_5Thinking
 708                | Model::ClaudeOpus4_6
 709                | Model::ClaudeOpus4_6Thinking
 710                | Model::ClaudeHaiku4_5
 711                | Model::ClaudeSonnet4
 712                | Model::ClaudeSonnet4Thinking
 713                | Model::ClaudeSonnet4_5
 714                | Model::ClaudeSonnet4_5Thinking
 715        );
 716
 717        let region_group = if region.starts_with("us-gov-") {
 718            "us-gov"
 719        } else if region.starts_with("us-")
 720            || region.starts_with("ca-")
 721            || region.starts_with("sa-")
 722        {
 723            if allow_global && supports_global {
 724                "global"
 725            } else {
 726                "us"
 727            }
 728        } else if region.starts_with("eu-") {
 729            if allow_global && supports_global {
 730                "global"
 731            } else {
 732                "eu"
 733            }
 734        } else if region.starts_with("ap-") || region == "me-central-1" || region == "me-south-1" {
 735            if allow_global && supports_global {
 736                "global"
 737            } else {
 738                "apac"
 739            }
 740        } else {
 741            anyhow::bail!("Unsupported Region {region}");
 742        };
 743
 744        match (self, region_group, region) {
 745            (Model::Custom { .. }, _, _) => Ok(self.request_id().into()),
 746
 747            (
 748                Model::ClaudeOpus4_5
 749                | Model::ClaudeOpus4_5Thinking
 750                | Model::ClaudeOpus4_6
 751                | Model::ClaudeOpus4_6Thinking
 752                | Model::ClaudeHaiku4_5
 753                | Model::ClaudeSonnet4
 754                | Model::ClaudeSonnet4Thinking
 755                | Model::ClaudeSonnet4_5
 756                | Model::ClaudeSonnet4_5Thinking,
 757                "global",
 758                _,
 759            ) => Ok(format!("{}.{}", region_group, model_id)),
 760
 761            (
 762                Model::Claude3Haiku
 763                | Model::Claude3_5Sonnet
 764                | Model::Claude3_7Sonnet
 765                | Model::Claude3_7SonnetThinking
 766                | Model::ClaudeSonnet4_5
 767                | Model::ClaudeSonnet4_5Thinking,
 768                "us-gov",
 769                _,
 770            ) => Ok(format!("{}.{}", region_group, model_id)),
 771
 772            (
 773                Model::ClaudeHaiku4_5 | Model::ClaudeSonnet4_5 | Model::ClaudeSonnet4_5Thinking,
 774                "apac",
 775                "ap-southeast-2" | "ap-southeast-4",
 776            ) => Ok(format!("au.{}", model_id)),
 777
 778            (
 779                Model::ClaudeHaiku4_5 | Model::ClaudeSonnet4_5 | Model::ClaudeSonnet4_5Thinking,
 780                "apac",
 781                "ap-northeast-1" | "ap-northeast-3",
 782            ) => Ok(format!("jp.{}", model_id)),
 783
 784            (Model::AmazonNovaLite, "us", r) if r.starts_with("ca-") => {
 785                Ok(format!("ca.{}", model_id))
 786            }
 787
 788            (
 789                Model::AmazonNovaPremier
 790                | Model::AmazonNovaLite
 791                | Model::AmazonNovaMicro
 792                | Model::AmazonNovaPro
 793                | Model::Claude3_5Haiku
 794                | Model::ClaudeHaiku4_5
 795                | Model::Claude3_5Sonnet
 796                | Model::Claude3_5SonnetV2
 797                | Model::Claude3_7Sonnet
 798                | Model::Claude3_7SonnetThinking
 799                | Model::ClaudeSonnet4
 800                | Model::ClaudeSonnet4Thinking
 801                | Model::ClaudeSonnet4_5
 802                | Model::ClaudeSonnet4_5Thinking
 803                | Model::ClaudeOpus4
 804                | Model::ClaudeOpus4Thinking
 805                | Model::ClaudeOpus4_1
 806                | Model::ClaudeOpus4_1Thinking
 807                | Model::ClaudeOpus4_5
 808                | Model::ClaudeOpus4_5Thinking
 809                | Model::ClaudeOpus4_6
 810                | Model::ClaudeOpus4_6Thinking
 811                | Model::Claude3Haiku
 812                | Model::Claude3Opus
 813                | Model::Claude3Sonnet
 814                | Model::DeepSeekR1
 815                | Model::MetaLlama31405BInstructV1
 816                | Model::MetaLlama3170BInstructV1_128k
 817                | Model::MetaLlama3170BInstructV1
 818                | Model::MetaLlama318BInstructV1_128k
 819                | Model::MetaLlama318BInstructV1
 820                | Model::MetaLlama3211BInstructV1
 821                | Model::MetaLlama321BInstructV1
 822                | Model::MetaLlama323BInstructV1
 823                | Model::MetaLlama3290BInstructV1
 824                | Model::MetaLlama3370BInstructV1
 825                | Model::MetaLlama4Maverick17BInstructV1
 826                | Model::MetaLlama4Scout17BInstructV1
 827                | Model::MistralPixtralLarge2502V1
 828                | Model::PalmyraWriterX4
 829                | Model::PalmyraWriterX5,
 830                "us",
 831                _,
 832            ) => Ok(format!("{}.{}", region_group, model_id)),
 833
 834            (
 835                Model::AmazonNovaLite
 836                | Model::AmazonNovaMicro
 837                | Model::AmazonNovaPro
 838                | Model::Claude3_5Sonnet
 839                | Model::ClaudeHaiku4_5
 840                | Model::Claude3_7Sonnet
 841                | Model::Claude3_7SonnetThinking
 842                | Model::ClaudeSonnet4
 843                | Model::ClaudeSonnet4_5
 844                | Model::ClaudeSonnet4_5Thinking
 845                | Model::ClaudeOpus4_6
 846                | Model::ClaudeOpus4_6Thinking
 847                | Model::Claude3Haiku
 848                | Model::Claude3Sonnet
 849                | Model::MetaLlama321BInstructV1
 850                | Model::MetaLlama323BInstructV1
 851                | Model::MistralPixtralLarge2502V1,
 852                "eu",
 853                _,
 854            ) => Ok(format!("{}.{}", region_group, model_id)),
 855
 856            (
 857                Model::AmazonNovaLite
 858                | Model::AmazonNovaMicro
 859                | Model::AmazonNovaPro
 860                | Model::Claude3_5Sonnet
 861                | Model::Claude3_5SonnetV2
 862                | Model::ClaudeHaiku4_5
 863                | Model::Claude3_7Sonnet
 864                | Model::Claude3_7SonnetThinking
 865                | Model::ClaudeSonnet4
 866                | Model::Claude3Haiku
 867                | Model::Claude3Sonnet,
 868                "apac",
 869                _,
 870            ) => Ok(format!("{}.{}", region_group, model_id)),
 871
 872            _ => Ok(model_id.into()),
 873        }
 874    }
 875}
 876
 877#[cfg(test)]
 878mod tests {
 879    use super::*;
 880
 881    #[test]
 882    fn test_us_region_inference_ids() -> anyhow::Result<()> {
 883        // Test US regions
 884        assert_eq!(
 885            Model::Claude3_5SonnetV2.cross_region_inference_id("us-east-1", false)?,
 886            "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
 887        );
 888        assert_eq!(
 889            Model::Claude3_5SonnetV2.cross_region_inference_id("us-west-2", false)?,
 890            "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
 891        );
 892        assert_eq!(
 893            Model::AmazonNovaPro.cross_region_inference_id("us-east-2", false)?,
 894            "us.amazon.nova-pro-v1:0"
 895        );
 896        Ok(())
 897    }
 898
 899    #[test]
 900    fn test_eu_region_inference_ids() -> anyhow::Result<()> {
 901        // Test European regions
 902        assert_eq!(
 903            Model::ClaudeSonnet4.cross_region_inference_id("eu-west-1", false)?,
 904            "eu.anthropic.claude-sonnet-4-20250514-v1:0"
 905        );
 906        assert_eq!(
 907            Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", false)?,
 908            "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
 909        );
 910        assert_eq!(
 911            Model::Claude3Sonnet.cross_region_inference_id("eu-west-1", false)?,
 912            "eu.anthropic.claude-3-sonnet-20240229-v1:0"
 913        );
 914        assert_eq!(
 915            Model::AmazonNovaMicro.cross_region_inference_id("eu-north-1", false)?,
 916            "eu.amazon.nova-micro-v1:0"
 917        );
 918        Ok(())
 919    }
 920
 921    #[test]
 922    fn test_apac_region_inference_ids() -> anyhow::Result<()> {
 923        // Test Asia-Pacific regions
 924        assert_eq!(
 925            Model::Claude3_5SonnetV2.cross_region_inference_id("ap-northeast-1", false)?,
 926            "apac.anthropic.claude-3-5-sonnet-20241022-v2:0"
 927        );
 928        assert_eq!(
 929            Model::Claude3_5SonnetV2.cross_region_inference_id("ap-southeast-2", false)?,
 930            "apac.anthropic.claude-3-5-sonnet-20241022-v2:0"
 931        );
 932        assert_eq!(
 933            Model::AmazonNovaLite.cross_region_inference_id("ap-south-1", false)?,
 934            "apac.amazon.nova-lite-v1:0"
 935        );
 936        Ok(())
 937    }
 938
 939    #[test]
 940    fn test_gov_region_inference_ids() -> anyhow::Result<()> {
 941        // Test Government regions
 942        assert_eq!(
 943            Model::Claude3_5Sonnet.cross_region_inference_id("us-gov-east-1", false)?,
 944            "us-gov.anthropic.claude-3-5-sonnet-20240620-v1:0"
 945        );
 946        assert_eq!(
 947            Model::Claude3Haiku.cross_region_inference_id("us-gov-west-1", false)?,
 948            "us-gov.anthropic.claude-3-haiku-20240307-v1:0"
 949        );
 950        Ok(())
 951    }
 952
 953    #[test]
 954    fn test_meta_models_inference_ids() -> anyhow::Result<()> {
 955        // Test Meta models
 956        assert_eq!(
 957            Model::MetaLlama370BInstructV1.cross_region_inference_id("us-east-1", false)?,
 958            "meta.llama3-70b-instruct-v1:0"
 959        );
 960        assert_eq!(
 961            Model::MetaLlama3170BInstructV1.cross_region_inference_id("us-east-1", false)?,
 962            "us.meta.llama3-1-70b-instruct-v1:0"
 963        );
 964        assert_eq!(
 965            Model::MetaLlama321BInstructV1.cross_region_inference_id("eu-west-1", false)?,
 966            "eu.meta.llama3-2-1b-instruct-v1:0"
 967        );
 968        Ok(())
 969    }
 970
 971    #[test]
 972    fn test_mistral_models_inference_ids() -> anyhow::Result<()> {
 973        // Mistral models don't follow the regional prefix pattern,
 974        // so they should return their original IDs
 975        assert_eq!(
 976            Model::MistralMistralLarge2402V1.cross_region_inference_id("us-east-1", false)?,
 977            "mistral.mistral-large-2402-v1:0"
 978        );
 979        assert_eq!(
 980            Model::MistralMixtral8x7BInstructV0.cross_region_inference_id("eu-west-1", false)?,
 981            "mistral.mixtral-8x7b-instruct-v0:1"
 982        );
 983        Ok(())
 984    }
 985
 986    #[test]
 987    fn test_ai21_models_inference_ids() -> anyhow::Result<()> {
 988        // AI21 models don't follow the regional prefix pattern,
 989        // so they should return their original IDs
 990        assert_eq!(
 991            Model::AI21J2UltraV1.cross_region_inference_id("us-east-1", false)?,
 992            "ai21.j2-ultra-v1"
 993        );
 994        assert_eq!(
 995            Model::AI21JambaInstructV1.cross_region_inference_id("eu-west-1", false)?,
 996            "ai21.jamba-instruct-v1:0"
 997        );
 998        Ok(())
 999    }
1000
1001    #[test]
1002    fn test_cohere_models_inference_ids() -> anyhow::Result<()> {
1003        // Cohere models don't follow the regional prefix pattern,
1004        // so they should return their original IDs
1005        assert_eq!(
1006            Model::CohereCommandRV1.cross_region_inference_id("us-east-1", false)?,
1007            "cohere.command-r-v1:0"
1008        );
1009        assert_eq!(
1010            Model::CohereCommandTextV14_4k.cross_region_inference_id("ap-southeast-1", false)?,
1011            "cohere.command-text-v14:7:4k"
1012        );
1013        Ok(())
1014    }
1015
1016    #[test]
1017    fn test_custom_model_inference_ids() -> anyhow::Result<()> {
1018        // Test custom models
1019        let custom_model = Model::Custom {
1020            name: "custom.my-model-v1:0".to_string(),
1021            max_tokens: 100000,
1022            display_name: Some("My Custom Model".to_string()),
1023            max_output_tokens: Some(8192),
1024            default_temperature: Some(0.7),
1025            cache_configuration: None,
1026        };
1027
1028        // Custom model should return its name unchanged
1029        assert_eq!(
1030            custom_model.cross_region_inference_id("us-east-1", false)?,
1031            "custom.my-model-v1:0"
1032        );
1033
1034        // Test that models without global support fall back to regional when allow_global is true
1035        assert_eq!(
1036            Model::AmazonNovaPro.cross_region_inference_id("us-east-1", true)?,
1037            "us.amazon.nova-pro-v1:0",
1038            "Nova Pro should fall back to regional profile even when allow_global is true"
1039        );
1040
1041        Ok(())
1042    }
1043
1044    #[test]
1045    fn test_friendly_id_vs_request_id() {
1046        // Test that id() returns friendly identifiers
1047        assert_eq!(Model::Claude3_5SonnetV2.id(), "claude-3-5-sonnet-v2");
1048        assert_eq!(Model::AmazonNovaLite.id(), "amazon-nova-lite");
1049        assert_eq!(Model::DeepSeekR1.id(), "deepseek-r1");
1050        assert_eq!(
1051            Model::MetaLlama38BInstructV1.id(),
1052            "meta-llama3-8b-instruct-v1"
1053        );
1054
1055        // Test that request_id() returns actual backend model IDs
1056        assert_eq!(
1057            Model::Claude3_5SonnetV2.request_id(),
1058            "anthropic.claude-3-5-sonnet-20241022-v2:0"
1059        );
1060        assert_eq!(Model::AmazonNovaLite.request_id(), "amazon.nova-lite-v1:0");
1061        assert_eq!(Model::DeepSeekR1.request_id(), "deepseek.r1-v1:0");
1062        assert_eq!(
1063            Model::MetaLlama38BInstructV1.request_id(),
1064            "meta.llama3-8b-instruct-v1:0"
1065        );
1066
1067        // Test thinking models have different friendly IDs but same request IDs
1068        assert_eq!(Model::ClaudeSonnet4.id(), "claude-sonnet-4");
1069        assert_eq!(
1070            Model::ClaudeSonnet4Thinking.id(),
1071            "claude-sonnet-4-thinking"
1072        );
1073        assert_eq!(
1074            Model::ClaudeSonnet4.request_id(),
1075            Model::ClaudeSonnet4Thinking.request_id()
1076        );
1077    }
1078}
1079
1080#[test]
1081fn test_global_inference_ids() -> anyhow::Result<()> {
1082    // Test global inference for models that support it when allow_global is true
1083    assert_eq!(
1084        Model::ClaudeSonnet4.cross_region_inference_id("us-east-1", true)?,
1085        "global.anthropic.claude-sonnet-4-20250514-v1:0"
1086    );
1087    assert_eq!(
1088        Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", true)?,
1089        "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
1090    );
1091    assert_eq!(
1092        Model::ClaudeHaiku4_5.cross_region_inference_id("ap-south-1", true)?,
1093        "global.anthropic.claude-haiku-4-5-20251001-v1:0"
1094    );
1095
1096    // Test that regional prefix is used when allow_global is false
1097    assert_eq!(
1098        Model::ClaudeSonnet4.cross_region_inference_id("us-east-1", false)?,
1099        "us.anthropic.claude-sonnet-4-20250514-v1:0"
1100    );
1101
1102    Ok(())
1103}