1mod runtime;
11pub use runtime::RuntimeBedrockModel;
12
13use crate::events::TokenUsage;
14use crate::types::{ContentBlock, Message, StopReason, ToolDefinition};
15
16#[derive(Debug, Clone)]
18pub struct ModelRequest {
19 pub messages: Vec<Message>,
20 pub system_prompt: Option<String>,
21 pub max_tokens: i32,
22 pub temperature: Option<f32>,
23 pub top_p: Option<f32>,
24 pub tools: Vec<ToolDefinition>,
25}
26
27#[derive(Debug, Clone)]
29pub struct ModelResponse {
30 pub message: Message,
32 pub stop_reason: StopReason,
34 pub usage: Option<TokenUsage>,
36}
37
38pub trait Model: Send + Sync {
44 fn name(&self) -> &str;
46
47 fn max_context_tokens(&self) -> usize;
49
50 fn max_output_tokens(&self) -> usize;
52
53 fn estimate_token_count(&self, text: &str) -> usize;
59
60 fn estimate_message_tokens(&self, messages: &[Message]) -> usize {
65 let mut total = 0;
66 for message in messages {
67 total += 4;
69 for block in &message.content {
71 total += self.estimate_content_block_tokens(block);
72 }
73 }
74 total
75 }
76
77 fn estimate_content_block_tokens(&self, block: &ContentBlock) -> usize {
79 match block {
80 ContentBlock::Text(text) => self.estimate_token_count(text),
81 ContentBlock::ToolUse(tool_use) => {
82 self.estimate_token_count(&tool_use.name)
84 + self.estimate_token_count(&tool_use.id)
85 + self.estimate_token_count(&tool_use.input.to_string())
86 + 10 }
88 ContentBlock::ToolResult(result) => {
89 self.estimate_token_count(&result.tool_use_id)
91 + match &result.content {
92 crate::tool::ToolResult::Text(t) => self.estimate_token_count(t.as_str()),
93 crate::tool::ToolResult::Json(v) => {
94 self.estimate_token_count(&v.to_string())
95 }
96 crate::tool::ToolResult::Image { data, .. } => {
97 data.len() / 750 + 85 }
100 crate::tool::ToolResult::Document { data, .. } => {
101 data.len() / 500 + 50 }
104 }
105 + 10 }
107 ContentBlock::ResponsesReplay(replay) => {
108 replay
109 .items
110 .iter()
111 .map(|item| self.estimate_token_count(&item.to_string()))
112 .sum::<usize>()
113 + 10
114 }
115 ContentBlock::RedactedThinking { data } => self.estimate_token_count(data) + 10,
116 ContentBlock::Thinking {
117 thinking,
118 signature,
119 } => {
120 self.estimate_token_count(thinking) + self.estimate_token_count(signature) + 10
122 }
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
138pub enum InferenceProfile {
139 #[default]
144 None,
145
146 US,
148
149 EU,
151
152 APAC,
154
155 Global,
159}
160
161impl InferenceProfile {
162 pub fn apply_to(&self, base_model_id: &str) -> String {
167 if base_model_id.starts_with("arn:")
168 || ["us.", "eu.", "apac.", "au.", "in.", "jp.", "global."]
169 .iter()
170 .any(|prefix| base_model_id.starts_with(prefix))
171 {
172 return base_model_id.to_owned();
173 }
174 match self.prefix() {
175 Some(prefix) => format!("{}.{}", prefix, base_model_id),
176 None => base_model_id.to_string(),
177 }
178 }
179
180 fn prefix(&self) -> Option<&'static str> {
182 match self {
183 InferenceProfile::None => None,
184 InferenceProfile::US => Some("us"),
185 InferenceProfile::EU => Some("eu"),
186 InferenceProfile::APAC => Some("apac"),
187 InferenceProfile::Global => Some("global"),
188 }
189 }
190}
191
192pub trait BedrockModel: Model {
196 fn bedrock_id(&self) -> &str;
201
202 fn default_inference_profile(&self) -> InferenceProfile {
208 InferenceProfile::None
209 }
210}
211
212pub trait AnthropicModel: Model {
216 fn anthropic_id(&self) -> &'static str;
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::tool::{DocumentFormat, ImageFormat, ToolResult};
226 use crate::types::{
227 ContentBlock, Message, Role, ToolResultBlock, ToolResultStatus, ToolUseBlock,
228 };
229
230 struct TestModel;
232
233 impl Model for TestModel {
234 fn name(&self) -> &'static str {
235 "TestModel"
236 }
237
238 fn max_context_tokens(&self) -> usize {
239 100_000
240 }
241
242 fn max_output_tokens(&self) -> usize {
243 4096
244 }
245
246 fn estimate_token_count(&self, text: &str) -> usize {
247 text.len().div_ceil(4)
249 }
250 }
251
252 #[test]
255 fn test_estimate_message_tokens_empty() {
256 let model = TestModel;
257 let messages: Vec<Message> = vec![];
258 assert_eq!(model.estimate_message_tokens(&messages), 0);
259 }
260
261 #[test]
262 fn test_estimate_message_tokens_simple_text() {
263 let model = TestModel;
264 let messages = vec![Message::user("Hello world")]; let tokens = model.estimate_message_tokens(&messages);
267 assert_eq!(tokens, 7);
268 }
269
270 #[test]
271 fn test_estimate_message_tokens_multiple_messages() {
272 let model = TestModel;
273 let messages = vec![
274 Message::user("Hello"), Message::assistant("Hi there"), ];
277
278 let tokens = model.estimate_message_tokens(&messages);
279 assert_eq!(tokens, 12);
280 }
281
282 #[test]
283 fn test_estimate_content_block_tokens_text() {
284 let model = TestModel;
285 let block = ContentBlock::Text("test".to_string()); assert_eq!(model.estimate_content_block_tokens(&block), 1);
287 }
288
289 #[test]
290 fn test_estimate_content_block_tokens_text_empty() {
291 let model = TestModel;
292 let block = ContentBlock::Text(String::new());
293 assert_eq!(model.estimate_content_block_tokens(&block), 0);
294 }
295
296 #[test]
297 fn test_estimate_content_block_tokens_tool_use() {
298 let model = TestModel;
299 let block = ContentBlock::ToolUse(ToolUseBlock {
300 id: "id12".to_string(), name: "search".to_string(), input: serde_json::json!({"q": "x"}), });
304
305 let tokens = model.estimate_content_block_tokens(&block);
307 assert!(tokens >= 10, "Should include overhead, got {}", tokens);
308 }
309
310 #[test]
311 fn test_estimate_content_block_tokens_tool_result_text() {
312 let model = TestModel;
313 let block = ContentBlock::ToolResult(ToolResultBlock {
314 tool_use_id: "id12".to_string(), content: ToolResult::Text("result text".to_string()), status: ToolResultStatus::Success,
317 });
318
319 let tokens = model.estimate_content_block_tokens(&block);
321 assert!(tokens >= 10, "Should include overhead, got {}", tokens);
322 }
323
324 #[test]
325 fn test_estimate_content_block_tokens_tool_result_json() {
326 let model = TestModel;
327 let block = ContentBlock::ToolResult(ToolResultBlock {
328 tool_use_id: "id".to_string(),
329 content: ToolResult::Json(serde_json::json!({"key": "value"})),
330 status: ToolResultStatus::Success,
331 });
332
333 let tokens = model.estimate_content_block_tokens(&block);
334 assert!(tokens >= 10, "Should include overhead, got {}", tokens);
335 }
336
337 #[test]
338 fn test_estimate_content_block_tokens_image() {
339 let model = TestModel;
340 let data = vec![0u8; 7500];
342 let block = ContentBlock::ToolResult(ToolResultBlock {
343 tool_use_id: "img".to_string(),
344 content: ToolResult::Image {
345 format: ImageFormat::Png,
346 data,
347 },
348 status: ToolResultStatus::Success,
349 });
350
351 let tokens = model.estimate_content_block_tokens(&block);
352 assert!(
354 tokens >= 95,
355 "Expected at least 95 tokens for image, got {}",
356 tokens
357 );
358 }
359
360 #[test]
361 fn test_estimate_content_block_tokens_document() {
362 let model = TestModel;
363 let data = vec![0u8; 5000];
365 let block = ContentBlock::ToolResult(ToolResultBlock {
366 tool_use_id: "doc".to_string(),
367 content: ToolResult::Document {
368 format: DocumentFormat::Pdf,
369 data,
370 name: Some("test.pdf".to_string()),
371 },
372 status: ToolResultStatus::Success,
373 });
374
375 let tokens = model.estimate_content_block_tokens(&block);
376 assert!(
378 tokens >= 60,
379 "Expected at least 60 tokens for document, got {}",
380 tokens
381 );
382 }
383
384 #[test]
385 fn test_estimate_content_block_tokens_thinking() {
386 let model = TestModel;
387 let block = ContentBlock::Thinking {
388 thinking: "complex reasoning here".to_string(), signature: "sig".to_string(), };
391
392 let tokens = model.estimate_content_block_tokens(&block);
394 assert!(tokens >= 10, "Should include overhead, got {}", tokens);
395 }
396
397 #[test]
398 fn test_estimate_message_with_multiple_content_blocks() {
399 let model = TestModel;
400 let messages = vec![Message {
401 role: Role::Assistant,
402 content: vec![
403 ContentBlock::Text("Let me search".to_string()),
404 ContentBlock::ToolUse(ToolUseBlock {
405 id: "1".to_string(),
406 name: "search".to_string(),
407 input: serde_json::json!({"q": "test"}),
408 }),
409 ],
410 }];
411
412 let tokens = model.estimate_message_tokens(&messages);
413 assert!(tokens > 4, "Should have content tokens plus overhead");
415 }
416
417 #[test]
420 fn test_inference_profile_apply_none() {
421 let profile = InferenceProfile::None;
422 assert_eq!(profile.apply_to("anthropic.claude-3"), "anthropic.claude-3");
423 }
424
425 #[test]
426 fn test_inference_profile_apply_us() {
427 let profile = InferenceProfile::US;
428 assert_eq!(
429 profile.apply_to("anthropic.claude-3"),
430 "us.anthropic.claude-3"
431 );
432 }
433
434 #[test]
435 fn test_inference_profile_apply_eu() {
436 let profile = InferenceProfile::EU;
437 assert_eq!(
438 profile.apply_to("anthropic.claude-3"),
439 "eu.anthropic.claude-3"
440 );
441 }
442
443 #[test]
444 fn test_inference_profile_apply_apac() {
445 let profile = InferenceProfile::APAC;
446 assert_eq!(profile.apply_to("model-id"), "apac.model-id");
447 }
448
449 #[test]
450 fn test_inference_profile_apply_global() {
451 let profile = InferenceProfile::Global;
452 assert_eq!(profile.apply_to("model-id"), "global.model-id");
453 }
454
455 #[test]
456 fn test_inference_profile_all_variants() {
457 let cases = [
458 (InferenceProfile::None, "model", "model"),
459 (InferenceProfile::US, "model", "us.model"),
460 (InferenceProfile::EU, "model", "eu.model"),
461 (InferenceProfile::APAC, "model", "apac.model"),
462 (InferenceProfile::Global, "model", "global.model"),
463 ];
464
465 for (profile, base, expected) in cases {
466 assert_eq!(profile.apply_to(base), expected, "Failed for {:?}", profile);
467 }
468 }
469
470 #[test]
471 fn test_inference_profile_default() {
472 let profile = InferenceProfile::default();
473 assert_eq!(profile, InferenceProfile::None);
474 }
475}