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_caching(&self) -> bool {
 555        match self {
 556            // Only Claude models on Bedrock support caching
 557            // Nova models support only text caching
 558            // https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html#prompt-caching-models
 559            Self::Claude3_5Haiku
 560            | Self::ClaudeHaiku4_5
 561            | Self::Claude3_7Sonnet
 562            | Self::Claude3_7SonnetThinking
 563            | Self::ClaudeSonnet4
 564            | Self::ClaudeSonnet4Thinking
 565            | Self::ClaudeSonnet4_5
 566            | Self::ClaudeSonnet4_5Thinking
 567            | Self::ClaudeOpus4
 568            | Self::ClaudeOpus4Thinking
 569            | Self::ClaudeOpus4_1
 570            | Self::ClaudeOpus4_1Thinking
 571            | Self::ClaudeOpus4_5
 572            | Self::ClaudeOpus4_5Thinking
 573            | Self::ClaudeOpus4_6
 574            | Self::ClaudeOpus4_6Thinking => true,
 575
 576            // Custom models - check if they have cache configuration
 577            Self::Custom {
 578                cache_configuration,
 579                ..
 580            } => cache_configuration.is_some(),
 581
 582            // All other models don't support caching
 583            _ => false,
 584        }
 585    }
 586
 587    pub fn cache_configuration(&self) -> Option<BedrockModelCacheConfiguration> {
 588        match self {
 589            Self::Claude3_7Sonnet
 590            | Self::Claude3_7SonnetThinking
 591            | Self::ClaudeSonnet4
 592            | Self::ClaudeSonnet4Thinking
 593            | Self::ClaudeOpus4
 594            | Self::ClaudeOpus4Thinking
 595            | Self::ClaudeOpus4_1
 596            | Self::ClaudeOpus4_1Thinking
 597            | Self::ClaudeOpus4_5
 598            | Self::ClaudeOpus4_5Thinking
 599            | Self::ClaudeOpus4_6
 600            | Self::ClaudeOpus4_6Thinking => Some(BedrockModelCacheConfiguration {
 601                max_cache_anchors: 4,
 602                min_total_token: 1024,
 603            }),
 604
 605            Self::Claude3_5Haiku | Self::ClaudeHaiku4_5 => Some(BedrockModelCacheConfiguration {
 606                max_cache_anchors: 4,
 607                min_total_token: 2048,
 608            }),
 609
 610            Self::Custom {
 611                cache_configuration,
 612                ..
 613            } => cache_configuration.clone(),
 614
 615            _ => None,
 616        }
 617    }
 618
 619    pub fn mode(&self) -> BedrockModelMode {
 620        match self {
 621            Model::Claude3_7SonnetThinking => BedrockModelMode::Thinking {
 622                budget_tokens: Some(4096),
 623            },
 624            Model::ClaudeSonnet4Thinking | Model::ClaudeSonnet4_5Thinking => {
 625                BedrockModelMode::Thinking {
 626                    budget_tokens: Some(4096),
 627                }
 628            }
 629            Model::ClaudeOpus4Thinking
 630            | Model::ClaudeOpus4_1Thinking
 631            | Model::ClaudeOpus4_5Thinking => BedrockModelMode::Thinking {
 632                budget_tokens: Some(4096),
 633            },
 634            Model::ClaudeOpus4_6Thinking => BedrockModelMode::AdaptiveThinking {
 635                effort: BedrockAdaptiveThinkingEffort::default(),
 636            },
 637            _ => BedrockModelMode::Default,
 638        }
 639    }
 640
 641    pub fn supports_extended_context(&self) -> bool {
 642        matches!(
 643            self,
 644            Model::ClaudeSonnet4
 645                | Model::ClaudeSonnet4Thinking
 646                | Model::ClaudeSonnet4_5
 647                | Model::ClaudeSonnet4_5Thinking
 648                | Model::ClaudeOpus4_5
 649                | Model::ClaudeOpus4_5Thinking
 650                | Model::ClaudeOpus4_6
 651                | Model::ClaudeOpus4_6Thinking
 652        )
 653    }
 654
 655    pub fn cross_region_inference_id(
 656        &self,
 657        region: &str,
 658        allow_global: bool,
 659    ) -> anyhow::Result<String> {
 660        // List derived from here:
 661        // https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html#inference-profiles-support-system
 662        let model_id = self.request_id();
 663
 664        let supports_global = matches!(
 665            self,
 666            Model::ClaudeOpus4_5
 667                | Model::ClaudeOpus4_5Thinking
 668                | Model::ClaudeOpus4_6
 669                | Model::ClaudeOpus4_6Thinking
 670                | Model::ClaudeHaiku4_5
 671                | Model::ClaudeSonnet4
 672                | Model::ClaudeSonnet4Thinking
 673                | Model::ClaudeSonnet4_5
 674                | Model::ClaudeSonnet4_5Thinking
 675        );
 676
 677        let region_group = if region.starts_with("us-gov-") {
 678            "us-gov"
 679        } else if region.starts_with("us-")
 680            || region.starts_with("ca-")
 681            || region.starts_with("sa-")
 682        {
 683            if allow_global && supports_global {
 684                "global"
 685            } else {
 686                "us"
 687            }
 688        } else if region.starts_with("eu-") {
 689            if allow_global && supports_global {
 690                "global"
 691            } else {
 692                "eu"
 693            }
 694        } else if region.starts_with("ap-") || region == "me-central-1" || region == "me-south-1" {
 695            if allow_global && supports_global {
 696                "global"
 697            } else {
 698                "apac"
 699            }
 700        } else {
 701            anyhow::bail!("Unsupported Region {region}");
 702        };
 703
 704        match (self, region_group, region) {
 705            (Model::Custom { .. }, _, _) => Ok(self.request_id().into()),
 706
 707            (
 708                Model::ClaudeOpus4_5
 709                | Model::ClaudeOpus4_5Thinking
 710                | Model::ClaudeOpus4_6
 711                | Model::ClaudeOpus4_6Thinking
 712                | Model::ClaudeHaiku4_5
 713                | Model::ClaudeSonnet4
 714                | Model::ClaudeSonnet4Thinking
 715                | Model::ClaudeSonnet4_5
 716                | Model::ClaudeSonnet4_5Thinking,
 717                "global",
 718                _,
 719            ) => Ok(format!("{}.{}", region_group, model_id)),
 720
 721            (
 722                Model::Claude3Haiku
 723                | Model::Claude3_5Sonnet
 724                | Model::Claude3_7Sonnet
 725                | Model::Claude3_7SonnetThinking
 726                | Model::ClaudeSonnet4_5
 727                | Model::ClaudeSonnet4_5Thinking,
 728                "us-gov",
 729                _,
 730            ) => Ok(format!("{}.{}", region_group, model_id)),
 731
 732            (
 733                Model::ClaudeHaiku4_5 | Model::ClaudeSonnet4_5 | Model::ClaudeSonnet4_5Thinking,
 734                "apac",
 735                "ap-southeast-2" | "ap-southeast-4",
 736            ) => Ok(format!("au.{}", model_id)),
 737
 738            (
 739                Model::ClaudeHaiku4_5 | Model::ClaudeSonnet4_5 | Model::ClaudeSonnet4_5Thinking,
 740                "apac",
 741                "ap-northeast-1" | "ap-northeast-3",
 742            ) => Ok(format!("jp.{}", model_id)),
 743
 744            (Model::AmazonNovaLite, "us", r) if r.starts_with("ca-") => {
 745                Ok(format!("ca.{}", model_id))
 746            }
 747
 748            (
 749                Model::AmazonNovaPremier
 750                | Model::AmazonNovaLite
 751                | Model::AmazonNovaMicro
 752                | Model::AmazonNovaPro
 753                | Model::Claude3_5Haiku
 754                | Model::ClaudeHaiku4_5
 755                | Model::Claude3_5Sonnet
 756                | Model::Claude3_5SonnetV2
 757                | Model::Claude3_7Sonnet
 758                | Model::Claude3_7SonnetThinking
 759                | Model::ClaudeSonnet4
 760                | Model::ClaudeSonnet4Thinking
 761                | Model::ClaudeSonnet4_5
 762                | Model::ClaudeSonnet4_5Thinking
 763                | Model::ClaudeOpus4
 764                | Model::ClaudeOpus4Thinking
 765                | Model::ClaudeOpus4_1
 766                | Model::ClaudeOpus4_1Thinking
 767                | Model::ClaudeOpus4_5
 768                | Model::ClaudeOpus4_5Thinking
 769                | Model::ClaudeOpus4_6
 770                | Model::ClaudeOpus4_6Thinking
 771                | Model::Claude3Haiku
 772                | Model::Claude3Opus
 773                | Model::Claude3Sonnet
 774                | Model::DeepSeekR1
 775                | Model::MetaLlama31405BInstructV1
 776                | Model::MetaLlama3170BInstructV1_128k
 777                | Model::MetaLlama3170BInstructV1
 778                | Model::MetaLlama318BInstructV1_128k
 779                | Model::MetaLlama318BInstructV1
 780                | Model::MetaLlama3211BInstructV1
 781                | Model::MetaLlama321BInstructV1
 782                | Model::MetaLlama323BInstructV1
 783                | Model::MetaLlama3290BInstructV1
 784                | Model::MetaLlama3370BInstructV1
 785                | Model::MetaLlama4Maverick17BInstructV1
 786                | Model::MetaLlama4Scout17BInstructV1
 787                | Model::MistralPixtralLarge2502V1
 788                | Model::PalmyraWriterX4
 789                | Model::PalmyraWriterX5,
 790                "us",
 791                _,
 792            ) => Ok(format!("{}.{}", region_group, model_id)),
 793
 794            (
 795                Model::AmazonNovaLite
 796                | Model::AmazonNovaMicro
 797                | Model::AmazonNovaPro
 798                | Model::Claude3_5Sonnet
 799                | Model::ClaudeHaiku4_5
 800                | Model::Claude3_7Sonnet
 801                | Model::Claude3_7SonnetThinking
 802                | Model::ClaudeSonnet4
 803                | Model::ClaudeSonnet4_5
 804                | Model::ClaudeSonnet4_5Thinking
 805                | Model::ClaudeOpus4_6
 806                | Model::ClaudeOpus4_6Thinking
 807                | Model::Claude3Haiku
 808                | Model::Claude3Sonnet
 809                | Model::MetaLlama321BInstructV1
 810                | Model::MetaLlama323BInstructV1
 811                | Model::MistralPixtralLarge2502V1,
 812                "eu",
 813                _,
 814            ) => Ok(format!("{}.{}", region_group, model_id)),
 815
 816            (
 817                Model::AmazonNovaLite
 818                | Model::AmazonNovaMicro
 819                | Model::AmazonNovaPro
 820                | Model::Claude3_5Sonnet
 821                | Model::Claude3_5SonnetV2
 822                | Model::ClaudeHaiku4_5
 823                | Model::Claude3_7Sonnet
 824                | Model::Claude3_7SonnetThinking
 825                | Model::ClaudeSonnet4
 826                | Model::Claude3Haiku
 827                | Model::Claude3Sonnet,
 828                "apac",
 829                _,
 830            ) => Ok(format!("{}.{}", region_group, model_id)),
 831
 832            _ => Ok(model_id.into()),
 833        }
 834    }
 835}
 836
 837#[cfg(test)]
 838mod tests {
 839    use super::*;
 840
 841    #[test]
 842    fn test_us_region_inference_ids() -> anyhow::Result<()> {
 843        // Test US regions
 844        assert_eq!(
 845            Model::Claude3_5SonnetV2.cross_region_inference_id("us-east-1", false)?,
 846            "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
 847        );
 848        assert_eq!(
 849            Model::Claude3_5SonnetV2.cross_region_inference_id("us-west-2", false)?,
 850            "us.anthropic.claude-3-5-sonnet-20241022-v2:0"
 851        );
 852        assert_eq!(
 853            Model::AmazonNovaPro.cross_region_inference_id("us-east-2", false)?,
 854            "us.amazon.nova-pro-v1:0"
 855        );
 856        Ok(())
 857    }
 858
 859    #[test]
 860    fn test_eu_region_inference_ids() -> anyhow::Result<()> {
 861        // Test European regions
 862        assert_eq!(
 863            Model::ClaudeSonnet4.cross_region_inference_id("eu-west-1", false)?,
 864            "eu.anthropic.claude-sonnet-4-20250514-v1:0"
 865        );
 866        assert_eq!(
 867            Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", false)?,
 868            "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
 869        );
 870        assert_eq!(
 871            Model::Claude3Sonnet.cross_region_inference_id("eu-west-1", false)?,
 872            "eu.anthropic.claude-3-sonnet-20240229-v1:0"
 873        );
 874        assert_eq!(
 875            Model::AmazonNovaMicro.cross_region_inference_id("eu-north-1", false)?,
 876            "eu.amazon.nova-micro-v1:0"
 877        );
 878        Ok(())
 879    }
 880
 881    #[test]
 882    fn test_apac_region_inference_ids() -> anyhow::Result<()> {
 883        // Test Asia-Pacific regions
 884        assert_eq!(
 885            Model::Claude3_5SonnetV2.cross_region_inference_id("ap-northeast-1", false)?,
 886            "apac.anthropic.claude-3-5-sonnet-20241022-v2:0"
 887        );
 888        assert_eq!(
 889            Model::Claude3_5SonnetV2.cross_region_inference_id("ap-southeast-2", false)?,
 890            "apac.anthropic.claude-3-5-sonnet-20241022-v2:0"
 891        );
 892        assert_eq!(
 893            Model::AmazonNovaLite.cross_region_inference_id("ap-south-1", false)?,
 894            "apac.amazon.nova-lite-v1:0"
 895        );
 896        Ok(())
 897    }
 898
 899    #[test]
 900    fn test_gov_region_inference_ids() -> anyhow::Result<()> {
 901        // Test Government regions
 902        assert_eq!(
 903            Model::Claude3_5Sonnet.cross_region_inference_id("us-gov-east-1", false)?,
 904            "us-gov.anthropic.claude-3-5-sonnet-20240620-v1:0"
 905        );
 906        assert_eq!(
 907            Model::Claude3Haiku.cross_region_inference_id("us-gov-west-1", false)?,
 908            "us-gov.anthropic.claude-3-haiku-20240307-v1:0"
 909        );
 910        Ok(())
 911    }
 912
 913    #[test]
 914    fn test_meta_models_inference_ids() -> anyhow::Result<()> {
 915        // Test Meta models
 916        assert_eq!(
 917            Model::MetaLlama370BInstructV1.cross_region_inference_id("us-east-1", false)?,
 918            "meta.llama3-70b-instruct-v1:0"
 919        );
 920        assert_eq!(
 921            Model::MetaLlama3170BInstructV1.cross_region_inference_id("us-east-1", false)?,
 922            "us.meta.llama3-1-70b-instruct-v1:0"
 923        );
 924        assert_eq!(
 925            Model::MetaLlama321BInstructV1.cross_region_inference_id("eu-west-1", false)?,
 926            "eu.meta.llama3-2-1b-instruct-v1:0"
 927        );
 928        Ok(())
 929    }
 930
 931    #[test]
 932    fn test_mistral_models_inference_ids() -> anyhow::Result<()> {
 933        // Mistral models don't follow the regional prefix pattern,
 934        // so they should return their original IDs
 935        assert_eq!(
 936            Model::MistralMistralLarge2402V1.cross_region_inference_id("us-east-1", false)?,
 937            "mistral.mistral-large-2402-v1:0"
 938        );
 939        assert_eq!(
 940            Model::MistralMixtral8x7BInstructV0.cross_region_inference_id("eu-west-1", false)?,
 941            "mistral.mixtral-8x7b-instruct-v0:1"
 942        );
 943        Ok(())
 944    }
 945
 946    #[test]
 947    fn test_ai21_models_inference_ids() -> anyhow::Result<()> {
 948        // AI21 models don't follow the regional prefix pattern,
 949        // so they should return their original IDs
 950        assert_eq!(
 951            Model::AI21J2UltraV1.cross_region_inference_id("us-east-1", false)?,
 952            "ai21.j2-ultra-v1"
 953        );
 954        assert_eq!(
 955            Model::AI21JambaInstructV1.cross_region_inference_id("eu-west-1", false)?,
 956            "ai21.jamba-instruct-v1:0"
 957        );
 958        Ok(())
 959    }
 960
 961    #[test]
 962    fn test_cohere_models_inference_ids() -> anyhow::Result<()> {
 963        // Cohere models don't follow the regional prefix pattern,
 964        // so they should return their original IDs
 965        assert_eq!(
 966            Model::CohereCommandRV1.cross_region_inference_id("us-east-1", false)?,
 967            "cohere.command-r-v1:0"
 968        );
 969        assert_eq!(
 970            Model::CohereCommandTextV14_4k.cross_region_inference_id("ap-southeast-1", false)?,
 971            "cohere.command-text-v14:7:4k"
 972        );
 973        Ok(())
 974    }
 975
 976    #[test]
 977    fn test_custom_model_inference_ids() -> anyhow::Result<()> {
 978        // Test custom models
 979        let custom_model = Model::Custom {
 980            name: "custom.my-model-v1:0".to_string(),
 981            max_tokens: 100000,
 982            display_name: Some("My Custom Model".to_string()),
 983            max_output_tokens: Some(8192),
 984            default_temperature: Some(0.7),
 985            cache_configuration: None,
 986        };
 987
 988        // Custom model should return its name unchanged
 989        assert_eq!(
 990            custom_model.cross_region_inference_id("us-east-1", false)?,
 991            "custom.my-model-v1:0"
 992        );
 993
 994        // Test that models without global support fall back to regional when allow_global is true
 995        assert_eq!(
 996            Model::AmazonNovaPro.cross_region_inference_id("us-east-1", true)?,
 997            "us.amazon.nova-pro-v1:0",
 998            "Nova Pro should fall back to regional profile even when allow_global is true"
 999        );
1000
1001        Ok(())
1002    }
1003
1004    #[test]
1005    fn test_friendly_id_vs_request_id() {
1006        // Test that id() returns friendly identifiers
1007        assert_eq!(Model::Claude3_5SonnetV2.id(), "claude-3-5-sonnet-v2");
1008        assert_eq!(Model::AmazonNovaLite.id(), "amazon-nova-lite");
1009        assert_eq!(Model::DeepSeekR1.id(), "deepseek-r1");
1010        assert_eq!(
1011            Model::MetaLlama38BInstructV1.id(),
1012            "meta-llama3-8b-instruct-v1"
1013        );
1014
1015        // Test that request_id() returns actual backend model IDs
1016        assert_eq!(
1017            Model::Claude3_5SonnetV2.request_id(),
1018            "anthropic.claude-3-5-sonnet-20241022-v2:0"
1019        );
1020        assert_eq!(Model::AmazonNovaLite.request_id(), "amazon.nova-lite-v1:0");
1021        assert_eq!(Model::DeepSeekR1.request_id(), "deepseek.r1-v1:0");
1022        assert_eq!(
1023            Model::MetaLlama38BInstructV1.request_id(),
1024            "meta.llama3-8b-instruct-v1:0"
1025        );
1026
1027        // Test thinking models have different friendly IDs but same request IDs
1028        assert_eq!(Model::ClaudeSonnet4.id(), "claude-sonnet-4");
1029        assert_eq!(
1030            Model::ClaudeSonnet4Thinking.id(),
1031            "claude-sonnet-4-thinking"
1032        );
1033        assert_eq!(
1034            Model::ClaudeSonnet4.request_id(),
1035            Model::ClaudeSonnet4Thinking.request_id()
1036        );
1037    }
1038}
1039
1040#[test]
1041fn test_global_inference_ids() -> anyhow::Result<()> {
1042    // Test global inference for models that support it when allow_global is true
1043    assert_eq!(
1044        Model::ClaudeSonnet4.cross_region_inference_id("us-east-1", true)?,
1045        "global.anthropic.claude-sonnet-4-20250514-v1:0"
1046    );
1047    assert_eq!(
1048        Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", true)?,
1049        "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
1050    );
1051    assert_eq!(
1052        Model::ClaudeHaiku4_5.cross_region_inference_id("ap-south-1", true)?,
1053        "global.anthropic.claude-haiku-4-5-20251001-v1:0"
1054    );
1055
1056    // Test that regional prefix is used when allow_global is false
1057    assert_eq!(
1058        Model::ClaudeSonnet4.cross_region_inference_id("us-east-1", false)?,
1059        "us.anthropic.claude-sonnet-4-20250514-v1:0"
1060    );
1061
1062    Ok(())
1063}