Skip to main content

mixtape_core/models/
catalog.rs

1//! Bedrock model descriptors for configuration-driven applications.
2//!
3//! Reviewed against AWS model cards on 2026-09-22. A catalog entry is not proof
4//! of account access or a tested multi-turn tool loop. Card limit labels are
5//! retained verbatim; they are not silently converted into exact token counts.
6
7use crate::model::RuntimeBedrockModel;
8use crate::provider::ProviderError;
9use serde::Serialize;
10
11/// The API required for the intended multi-turn conversation behavior.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
13#[serde(rename_all = "snake_case")]
14pub enum BedrockConversationApi {
15    Converse,
16    /// Requires a separate Bedrock Chat Completions adapter. Do not silently
17    /// select Converse and discard reasoning to work around its limitations.
18    ChatCompletions,
19}
20
21/// Documented model identity and limits, separate from executable configuration.
22#[derive(Debug, Clone, Copy, Serialize)]
23pub struct BedrockModelDescriptor {
24    pub model_id: &'static str,
25    pub display_name: &'static str,
26    pub context_window_label: &'static str,
27    pub max_output_label: Option<&'static str>,
28    pub conversation_api: BedrockConversationApi,
29    pub requires_inference_profile: bool,
30    pub model_card: &'static str,
31}
32
33impl BedrockModelDescriptor {
34    /// Select explicit application token ceilings. They must be verified against
35    /// the chosen endpoint before use; a missing card limit is not unlimited.
36    /// Use `BedrockProvider::with_inference_profile` or an exact runtime target
37    /// for models requiring a profile. No credentials or requests are used here.
38    pub fn with_token_limits(
39        &self,
40        context_tokens: usize,
41        output_tokens: usize,
42    ) -> Result<RuntimeBedrockModel, ProviderError> {
43        RuntimeBedrockModel::new(
44            self.display_name,
45            self.model_id,
46            context_tokens,
47            output_tokens,
48        )
49    }
50}
51
52macro_rules! descriptor {
53    ($id:literal, $name:literal, $context:literal, $output:expr, $api:ident, $profile:literal, $card:literal) => {
54        BedrockModelDescriptor {
55            model_id: $id,
56            display_name: $name,
57            context_window_label: $context,
58            max_output_label: $output,
59            conversation_api: BedrockConversationApi::$api,
60            requires_inference_profile: $profile,
61            model_card: concat!(
62                "https://docs.aws.amazon.com/bedrock/latest/userguide/",
63                $card,
64                ".html"
65            ),
66        }
67    };
68}
69
70/// The 22 mapped model candidates. Historical typed models remain available;
71/// routers and private aliases without a verified Bedrock identity are omitted.
72/// Library metadata does not establish permission to send any particular data.
73pub const BEDROCK_MODEL_CATALOG: &[BedrockModelDescriptor] = &[
74    descriptor!(
75        "anthropic.claude-opus-5",
76        "Claude Opus 5.0",
77        "1M",
78        Some("128K"),
79        Converse,
80        true,
81        "model-card-anthropic-claude-opus-5"
82    ),
83    descriptor!(
84        "anthropic.claude-sonnet-5",
85        "Claude Sonnet 5",
86        "1M",
87        Some("128K"),
88        Converse,
89        true,
90        "model-card-anthropic-claude-sonnet-5"
91    ),
92    descriptor!(
93        "anthropic.claude-opus-4-8",
94        "Claude Opus 4.8",
95        "1M",
96        Some("128K"),
97        Converse,
98        true,
99        "model-card-anthropic-claude-opus-4-8"
100    ),
101    descriptor!(
102        "anthropic.claude-opus-4-7",
103        "Claude Opus 4.7",
104        "1M",
105        Some("128K"),
106        Converse,
107        true,
108        "model-card-anthropic-claude-opus-4-7"
109    ),
110    descriptor!(
111        "anthropic.claude-sonnet-4-6",
112        "Claude Sonnet 4.6",
113        "1M",
114        Some("64K"),
115        Converse,
116        true,
117        "model-card-anthropic-claude-sonnet-4-6"
118    ),
119    descriptor!(
120        "anthropic.claude-opus-4-5-20251101-v1:0",
121        "Claude Opus 4.5",
122        "200K",
123        Some("64K"),
124        Converse,
125        true,
126        "model-card-anthropic-claude-opus-4-5"
127    ),
128    descriptor!(
129        "anthropic.claude-sonnet-4-5-20250929-v1:0",
130        "Claude Sonnet 4.5",
131        "200K",
132        Some("64K"),
133        Converse,
134        true,
135        "model-card-anthropic-claude-sonnet-4-5"
136    ),
137    descriptor!(
138        "anthropic.claude-sonnet-4-20250514-v1:0",
139        "Claude Sonnet 4",
140        "200K",
141        Some("64K"),
142        Converse,
143        true,
144        "model-card-anthropic-claude-sonnet-4"
145    ),
146    descriptor!(
147        "anthropic.claude-haiku-4-5-20251001-v1:0",
148        "Claude Haiku 4.5",
149        "200K",
150        Some("64K"),
151        Converse,
152        true,
153        "model-card-anthropic-claude-haiku-4-5"
154    ),
155    // Restricted previews: callers must establish data/use-case eligibility.
156    descriptor!(
157        "anthropic.claude-fable-5",
158        "Claude Fable 5",
159        "1M",
160        Some("128K"),
161        Converse,
162        true,
163        "model-card-anthropic-claude-fable-5"
164    ),
165    descriptor!(
166        "anthropic.claude-fable-5-1",
167        "Claude Fable 5.1",
168        "1M",
169        Some("128K"),
170        Converse,
171        true,
172        "model-card-anthropic-claude-fable-5-1"
173    ),
174    descriptor!(
175        "openai.gpt-6-astra",
176        "GPT-6 Astra",
177        "1,050,000",
178        Some("128,000"),
179        Converse,
180        true,
181        "model-card-openai-gpt-6-astra"
182    ),
183    descriptor!(
184        "openai.gpt-5.6-sol",
185        "GPT-5.6 Sol",
186        "1M",
187        None,
188        Converse,
189        true,
190        "model-card-openai-gpt-56-sol"
191    ),
192    descriptor!(
193        "openai.gpt-5.6-terra",
194        "GPT-5.6 Terra",
195        "1M",
196        None,
197        Converse,
198        true,
199        "model-card-openai-gpt-56-terra"
200    ),
201    descriptor!(
202        "openai.gpt-5.6-luna",
203        "GPT-5.6 Luna",
204        "1M",
205        None,
206        Converse,
207        true,
208        "model-card-openai-gpt-56-luna"
209    ),
210    descriptor!(
211        "minimax.minimax-m2.5",
212        "MiniMax M2.5",
213        "196K",
214        Some("8K"),
215        Converse,
216        false,
217        "model-card-minimax-minimax-m2-5"
218    ),
219    descriptor!(
220        "minimax.minimax-m2.1",
221        "MiniMax M2.1",
222        "196K",
223        Some("8K"),
224        Converse,
225        false,
226        "model-card-minimax-minimax-m2-1"
227    ),
228    descriptor!(
229        "zai.glm-5",
230        "GLM 5",
231        "200K",
232        Some("128K"),
233        Converse,
234        false,
235        "model-card-zai-glm-5"
236    ),
237    descriptor!(
238        "deepseek.v3.2",
239        "DeepSeek V3.2",
240        "164K",
241        Some("8K"),
242        Converse,
243        false,
244        "model-card-deepseek-deepseek-v3-2"
245    ),
246    descriptor!(
247        "qwen.qwen3-coder-next",
248        "Qwen3 Coder Next",
249        "256K",
250        Some("16K"),
251        Converse,
252        false,
253        "model-card-qwen-qwen3-coder-next"
254    ),
255    descriptor!(
256        "xai.grok-4.6",
257        "Grok 4.6",
258        "500K",
259        None,
260        Converse,
261        true,
262        "model-card-xai-grok-4-6"
263    ),
264    descriptor!(
265        "moonshotai.kimi-k3",
266        "Kimi K3",
267        "1M",
268        None,
269        ChatCompletions,
270        true,
271        "model-card-moonshot-ai-kimi-k3"
272    ),
273];
274
275/// Find a descriptor by base model ID or geographic inference-profile ID.
276/// Exact application-profile ARNs need an account-side mapping; never infer one.
277pub fn bedrock_model_descriptor(model_id: &str) -> Option<&'static BedrockModelDescriptor> {
278    let base = ["us.", "eu.", "apac.", "au.", "in.", "jp.", "global."]
279        .iter()
280        .find_map(|prefix| model_id.strip_prefix(prefix))
281        .unwrap_or(model_id);
282    BEDROCK_MODEL_CATALOG
283        .iter()
284        .find(|model| model.model_id == base)
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use crate::model::{BedrockModel, InferenceProfile, Model};
291    use std::collections::HashSet;
292
293    #[test]
294    fn catalog_ids_are_unique_and_constructible_without_implicit_routing() {
295        assert_eq!(BEDROCK_MODEL_CATALOG.len(), 22);
296        let mut seen = HashSet::new();
297        for descriptor in BEDROCK_MODEL_CATALOG {
298            assert!(seen.insert(descriptor.model_id));
299            let configured = descriptor.with_token_limits(128_000, 4096).unwrap();
300            assert_eq!(configured.bedrock_id(), descriptor.model_id);
301            assert_eq!(configured.name(), descriptor.display_name);
302            assert_eq!(
303                configured.default_inference_profile(),
304                InferenceProfile::None
305            );
306            assert!(descriptor
307                .model_card
308                .starts_with("https://docs.aws.amazon.com/bedrock/"));
309        }
310    }
311
312    #[test]
313    fn unknown_limits_and_unmapped_aliases_stay_unknown() {
314        assert!(bedrock_model_descriptor("openai.gpt-5.6-sol")
315            .unwrap()
316            .max_output_label
317            .is_none());
318        for alias in [
319            "auto",
320            "agi-nova-beta-1m",
321            "kirin-inference-glm",
322            "unknown.model",
323        ] {
324            assert!(bedrock_model_descriptor(alias).is_none());
325        }
326    }
327
328    #[test]
329    fn profile_lookup_does_not_substitute_models_or_infer_arns() {
330        assert_eq!(
331            bedrock_model_descriptor("us.anthropic.claude-opus-5")
332                .unwrap()
333                .display_name,
334            "Claude Opus 5.0"
335        );
336        assert!(bedrock_model_descriptor(
337            "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/example"
338        )
339        .is_none());
340        assert_eq!(
341            bedrock_model_descriptor("moonshotai.kimi-k3")
342                .unwrap()
343                .conversation_api,
344            BedrockConversationApi::ChatCompletions
345        );
346    }
347}