1use serde_json::{json, Map, Value};
7
8use crate::codecs::common::{is_known_role_name, provider_extensions, text_from_blocks};
9use crate::codecs::openai_chat::{decode_file_source, decode_image_source};
10use crate::codecs::{
11 DecodedRequest, DecodedResponse, EncodedRequest, EncodedResponse, FormatCodec,
12};
13use crate::diagnostic::TranslationDiagnostic;
14use crate::error::{Result, TranslationError};
15use crate::format::{FormatId, WireFormat};
16use crate::llm::{
17 ContentBlock, FileSource, ImageSource, InstructionBlock, LlmRequest, LlmResponse, MediaSource,
18 Message, OutputParams, ProviderExtensions, ReasoningParams, ResponseOutput, Role,
19 SamplingParams, StopReason, ToolCall, ToolChoice, ToolDefinition, ToolResult, Usage,
20};
21use crate::policy::{DeterministicIdPolicy, TranslationPolicy};
22use crate::util::sanitize_anthropic_tool_use_id;
23use crate::util::{
24 capture_request_preservation, capture_response_preservation, embed_preservation,
25 exact_preserved_request, exact_preserved_response,
26};
27use crate::util::{
28 json_string, push_lossy, stable_id, string_value, validate_request_capabilities,
29};
30
31pub struct AnthropicMessagesCodec;
33
34impl FormatCodec for AnthropicMessagesCodec {
35 fn format(&self) -> FormatId {
36 WireFormat::AnthropicMessages.into()
37 }
38
39 fn decode_request(&self, body: &Value, policy: &TranslationPolicy) -> Result<DecodedRequest> {
40 let body = crate::util::object(body, "$")?;
41 let mut diagnostics = Vec::new();
42 let mut request = LlmRequest {
43 model: body
44 .get("model")
45 .and_then(Value::as_str)
46 .filter(|model| !model.is_empty())
47 .map(ToOwned::to_owned),
48 output: OutputParams {
49 max_output_tokens: body.get("max_tokens").and_then(Value::as_u64),
50 response_format: None,
51 },
52 sampling: SamplingParams {
53 temperature: body.get("temperature").and_then(Value::as_f64),
54 top_p: body.get("top_p").and_then(Value::as_f64),
55 top_k: body.get("top_k").and_then(Value::as_i64),
56 },
57 reasoning: ReasoningParams {
58 effort: body
59 .get("output_config")
60 .and_then(Value::as_object)
61 .and_then(|object| object.get("effort"))
62 .and_then(Value::as_str)
63 .map(ToOwned::to_owned),
64 raw: body.get("thinking").cloned(),
65 },
66 stream: body.get("stream").and_then(Value::as_bool).unwrap_or(false),
67 preservation: capture_request_preservation(
68 WireFormat::AnthropicMessages,
69 &Value::Object(body.clone()),
70 policy,
71 ),
72 ..LlmRequest::default()
73 };
74 if let Some(system) = body.get("system") {
75 if let Some(content) = decode_anthropic_system(system, &mut diagnostics, policy)? {
76 request.instructions.push(InstructionBlock {
77 role: Role::System,
78 content,
79 });
80 }
81 }
82 if let Some(messages) = body.get("messages").and_then(Value::as_array) {
83 let mut generated_id = 0;
84 for (index, message) in messages.iter().enumerate() {
85 let Some(message) = message.as_object() else {
86 push_lossy(
87 &mut diagnostics,
88 policy,
89 format!("Anthropic message at index {index} is not an object"),
90 )?;
91 continue;
92 };
93 let role = match message.get("role").and_then(Value::as_str) {
99 Some("assistant") => Role::Assistant,
100 None => Role::User,
101 Some(other) if is_known_role_name(other) => Role::User,
102 Some(other) => {
103 return Err(TranslationError::unsupported_role(
104 format!("$.messages[{index}].role"),
105 other,
106 ));
107 }
108 };
109 generated_id += 1;
110 let content = decode_anthropic_content(
111 message
112 .get("content")
113 .unwrap_or(&Value::String(String::new())),
114 role,
115 generated_id,
116 &mut diagnostics,
117 policy,
118 )?;
119 request.messages.push(Message { role, content });
120 }
121 }
122 request.tools = decode_anthropic_tools(body.get("tools"));
123 request.tool_choice = body.get("tool_choice").map(decode_anthropic_tool_choice);
124 request.extensions.fields = provider_extensions(
125 body,
126 &[
127 "model",
128 "messages",
129 "system",
130 "tools",
131 "tool_choice",
132 "max_tokens",
133 "temperature",
134 "top_p",
135 "top_k",
136 "thinking",
137 "output_config",
138 "stream",
139 ],
140 );
141
142 Ok(DecodedRequest {
143 request,
144 diagnostics,
145 })
146 }
147
148 fn encode_request(
149 &self,
150 request: &LlmRequest,
151 policy: &TranslationPolicy,
152 ) -> Result<EncodedRequest> {
153 if let Some(body) =
154 exact_preserved_request(&request.preservation, WireFormat::AnthropicMessages, policy)
155 {
156 return Ok(EncodedRequest {
157 body,
158 diagnostics: Vec::new(),
159 });
160 }
161 let mut diagnostics = Vec::new();
162 validate_request_capabilities(request, &mut diagnostics, policy)?;
163 let mut body = Map::new();
164 if let Some(model) = &request.model {
165 body.insert("model".to_string(), Value::String(model.clone()));
166 }
167 let system_text = request
168 .instructions
169 .iter()
170 .flat_map(|instruction| instruction.content.iter())
171 .filter_map(|block| match block {
172 ContentBlock::Text { text } | ContentBlock::Refusal { text } => Some(text.as_str()),
173 _ => None,
174 })
175 .collect::<Vec<_>>()
176 .join("\n\n");
177 if !system_text.is_empty() {
178 body.insert("system".to_string(), Value::String(system_text));
179 }
180
181 body.insert(
182 "messages".to_string(),
183 Value::Array(encode_anthropic_messages(
184 &request.messages,
185 &mut diagnostics,
186 policy,
187 )?),
188 );
189
190 if !request.tools.is_empty() {
191 body.insert("tools".to_string(), encode_anthropic_tools(&request.tools));
192 }
193 if let Some(choice) = &request.tool_choice {
194 body.insert(
195 "tool_choice".to_string(),
196 encode_anthropic_tool_choice(choice),
197 );
198 }
199 if let Some(stop_sequences) =
200 anthropic_stop_sequences_from_extensions(&request.extensions.fields)
201 {
202 body.insert("stop_sequences".to_string(), stop_sequences);
203 }
204 if let Some(max_tokens) = request.output.max_output_tokens {
205 body.insert("max_tokens".to_string(), json!(max_tokens));
206 } else {
207 body.insert("max_tokens".to_string(), json!(128_000));
208 }
209 if let Some(value) = request.sampling.temperature {
210 body.insert("temperature".to_string(), json!(value));
211 }
212 if let Some(value) = request.sampling.top_p {
213 body.insert("top_p".to_string(), json!(value));
214 }
215 if let Some(value) = request.sampling.top_k {
216 body.insert("top_k".to_string(), json!(value));
217 }
218 if request.stream {
219 body.insert("stream".to_string(), Value::Bool(true));
220 }
221 if let Some(effort) = &request.reasoning.effort {
222 body.insert("thinking".to_string(), json!({"type": "adaptive"}));
223 body.insert("output_config".to_string(), json!({"effort": effort}));
224 }
225
226 let body = embed_preservation(Value::Object(body), &request.preservation, policy);
227 Ok(EncodedRequest { body, diagnostics })
228 }
229
230 fn decode_response(
231 &self,
232 body: &Value,
233 _policy: &TranslationPolicy,
234 ) -> Result<DecodedResponse> {
235 let body = crate::util::object(body, "$")?;
236 let mut content = Vec::new();
237 if let Some(blocks) = body.get("content").and_then(Value::as_array) {
238 for (index, block) in blocks.iter().enumerate() {
239 if let Some(block) = block.as_object() {
240 content.extend(decode_anthropic_content_block(
241 block,
242 Role::Assistant,
243 index + 1,
244 &mut Vec::new(),
245 &TranslationPolicy::default(),
246 )?);
247 }
248 }
249 }
250 if content.is_empty() {
251 content.push(ContentBlock::Text {
252 text: String::new(),
253 });
254 }
255 let response = LlmResponse {
256 id: body
257 .get("id")
258 .and_then(Value::as_str)
259 .map(ToOwned::to_owned),
260 model: body
261 .get("model")
262 .and_then(Value::as_str)
263 .map(ToOwned::to_owned),
264 outputs: vec![ResponseOutput {
265 role: Role::Assistant,
266 content,
267 stop_reason: Some(map_anthropic_stop_reason(
268 body.get("stop_reason").and_then(Value::as_str),
269 )),
270 }],
271 usage: decode_anthropic_usage(body.get("usage")),
272 extensions: ProviderExtensions {
273 fields: provider_extensions(
274 body,
275 &[
276 "id",
277 "type",
278 "role",
279 "model",
280 "content",
281 "stop_reason",
282 "usage",
283 ],
284 ),
285 },
286 preservation: capture_response_preservation(
287 WireFormat::AnthropicMessages,
288 &Value::Object(body.clone()),
289 _policy,
290 ),
291 };
292 Ok(DecodedResponse {
293 response,
294 diagnostics: Vec::new(),
295 })
296 }
297
298 fn encode_response(
299 &self,
300 response: &LlmResponse,
301 _policy: &TranslationPolicy,
302 ) -> Result<EncodedResponse> {
303 if let Some(body) = exact_preserved_response(
304 &response.preservation,
305 WireFormat::AnthropicMessages,
306 _policy,
307 ) {
308 return Ok(EncodedResponse {
309 body,
310 diagnostics: Vec::new(),
311 });
312 }
313 let output = response.first_output();
314 let content = output
315 .map(|output| encode_anthropic_content(&output.content))
316 .unwrap_or_else(|| vec![json!({"type": "text", "text": ""})]);
317 let body = json!({
318 "id": response.id.clone().unwrap_or_else(|| "msg_switchyard".to_string()),
319 "type": "message",
320 "role": "assistant",
321 "model": response.model.clone().unwrap_or_else(|| "unknown".to_string()),
322 "content": content,
323 "stop_reason": output
324 .and_then(|output| output.stop_reason)
325 .map(anthropic_stop_reason)
326 .unwrap_or("end_turn"),
327 "stop_sequence": Value::Null,
328 "usage": encode_anthropic_usage(&response.usage),
329 });
330 Ok(EncodedResponse {
331 body: embed_preservation(body, &response.preservation, _policy),
332 diagnostics: Vec::new(),
333 })
334 }
335}
336
337fn decode_anthropic_system(
339 value: &Value,
340 diagnostics: &mut Vec<TranslationDiagnostic>,
341 policy: &TranslationPolicy,
342) -> Result<Option<Vec<ContentBlock>>> {
343 match value {
344 Value::String(text) if !text.is_empty() => {
345 Ok(Some(vec![ContentBlock::Text { text: text.clone() }]))
346 }
347 Value::String(_) | Value::Null => Ok(None),
348 Value::Array(blocks) => {
349 let mut content = Vec::new();
350 for block in blocks {
351 if let Some(block) = block.as_object() {
352 if block.get("type").and_then(Value::as_str) == Some("text") {
353 let text = block
354 .get("text")
355 .and_then(Value::as_str)
356 .unwrap_or_default()
357 .to_string();
358 content.push(ContentBlock::Text { text });
359 }
360 }
361 }
362 Ok((!content.is_empty()).then_some(content))
363 }
364 other => {
365 push_lossy(diagnostics, policy, "Anthropic system field was not text")?;
366 Ok(Some(vec![ContentBlock::Text {
367 text: string_value(other).unwrap_or_default(),
368 }]))
369 }
370 }
371}
372
373fn decode_anthropic_content(
375 value: &Value,
376 role: Role,
377 generated_counter: usize,
378 diagnostics: &mut Vec<TranslationDiagnostic>,
379 policy: &TranslationPolicy,
380) -> Result<Vec<ContentBlock>> {
381 match value {
382 Value::String(text) => Ok(vec![ContentBlock::Text { text: text.clone() }]),
383 Value::Null => Ok(vec![ContentBlock::Text {
384 text: String::new(),
385 }]),
386 Value::Array(blocks) => {
387 let mut content = Vec::new();
388 for (index, block) in blocks.iter().enumerate() {
389 let Some(block) = block.as_object() else {
390 push_lossy(
391 diagnostics,
392 policy,
393 format!("Anthropic content block {index} is not an object"),
394 )?;
395 continue;
396 };
397 content.extend(decode_anthropic_content_block(
398 block,
399 role,
400 generated_counter + index,
401 diagnostics,
402 policy,
403 )?);
404 }
405 if content.is_empty() {
406 content.push(ContentBlock::Text {
407 text: String::new(),
408 });
409 }
410 Ok(content)
411 }
412 other => Ok(vec![ContentBlock::Text {
413 text: string_value(other).unwrap_or_default(),
414 }]),
415 }
416}
417
418fn decode_anthropic_content_block(
420 block: &Map<String, Value>,
421 _role: Role,
422 generated_counter: usize,
423 _diagnostics: &mut Vec<TranslationDiagnostic>,
424 policy: &TranslationPolicy,
425) -> Result<Vec<ContentBlock>> {
426 Ok(match block.get("type").and_then(Value::as_str) {
427 Some("text") => vec![ContentBlock::Text {
428 text: block
429 .get("text")
430 .and_then(Value::as_str)
431 .unwrap_or_default()
432 .to_string(),
433 }],
434 Some("thinking") => vec![ContentBlock::Reasoning {
435 text: block
436 .get("thinking")
437 .and_then(Value::as_str)
438 .unwrap_or_default()
439 .to_string(),
440 signature: block
441 .get("signature")
442 .and_then(Value::as_str)
443 .filter(|signature| !signature.is_empty())
444 .map(ToOwned::to_owned),
445 }],
446 Some("tool_use") => vec![ContentBlock::ToolCall(ToolCall {
447 id: block
448 .get("id")
449 .and_then(Value::as_str)
450 .filter(|id| !id.is_empty())
451 .map(ToOwned::to_owned)
452 .unwrap_or_else(|| match &policy.deterministic_ids {
453 DeterministicIdPolicy::GenerateStable { prefix } => {
454 stable_id(prefix, generated_counter)
455 }
456 DeterministicIdPolicy::Preserve => String::new(),
457 }),
458 name: block
459 .get("name")
460 .and_then(Value::as_str)
461 .unwrap_or_default()
462 .to_string(),
463 arguments: block.get("input").cloned().unwrap_or_else(|| json!({})),
464 })],
465 Some("tool_result") => vec![ContentBlock::ToolResult(ToolResult {
466 tool_call_id: block
467 .get("tool_use_id")
468 .and_then(Value::as_str)
469 .unwrap_or_default()
470 .to_string(),
471 content: decode_tool_result_content(block.get("content").unwrap_or(&Value::Null)),
472 is_error: block.get("is_error").and_then(Value::as_bool),
473 })],
474 Some("image") => {
475 let source = block
476 .get("source")
477 .cloned()
478 .map(ImageSource::Raw)
479 .unwrap_or_else(|| ImageSource::Raw(Value::Object(block.clone())));
480 vec![ContentBlock::Image { source }]
481 }
482 Some("input_image") | Some("image_url") => decode_image_source(block)
483 .map(|source| vec![ContentBlock::Image { source }])
484 .unwrap_or_default(),
485 Some("input_file") | Some("file") => vec![ContentBlock::File {
486 source: decode_file_source(block),
487 }],
488 _ => vec![ContentBlock::Unknown {
489 provider: WireFormat::AnthropicMessages.into(),
490 raw: Value::Object(block.clone()),
491 }],
492 })
493}
494
495fn decode_tool_result_content(value: &Value) -> Vec<ContentBlock> {
497 match value {
498 Value::String(text) => vec![ContentBlock::Text { text: text.clone() }],
499 Value::Array(blocks) => {
500 let mut text = Vec::new();
501 for block in blocks {
502 if let Some(block) = block.as_object() {
503 if block.get("type").and_then(Value::as_str) == Some("text") {
504 text.push(
505 block
506 .get("text")
507 .and_then(Value::as_str)
508 .unwrap_or_default()
509 .to_string(),
510 );
511 } else {
512 text.push(json_string(&Value::Object(block.clone())));
513 }
514 }
515 }
516 vec![ContentBlock::Text {
517 text: text.join(" "),
518 }]
519 }
520 Value::Null => vec![ContentBlock::Text {
521 text: String::new(),
522 }],
523 other => vec![ContentBlock::Text {
524 text: json_string(other),
525 }],
526 }
527}
528
529fn decode_anthropic_tools(value: Option<&Value>) -> Vec<ToolDefinition> {
531 value
532 .and_then(Value::as_array)
533 .into_iter()
534 .flatten()
535 .filter_map(Value::as_object)
536 .filter_map(|tool| {
537 let name = tool.get("name").and_then(Value::as_str)?.to_string();
538 (!name.is_empty()).then(|| ToolDefinition {
539 name,
540 description: tool
541 .get("description")
542 .and_then(Value::as_str)
543 .map(ToOwned::to_owned),
544 parameters: tool
545 .get("input_schema")
546 .cloned()
547 .unwrap_or_else(|| json!({})),
548 strict: None,
549 })
550 })
551 .collect()
552}
553
554fn decode_anthropic_tool_choice(value: &Value) -> ToolChoice {
556 match value {
557 Value::String(text) if text == "auto" => ToolChoice::Auto,
558 Value::String(text) if text == "any" => ToolChoice::Required,
559 Value::String(text) if text == "none" => ToolChoice::None,
560 Value::Object(object) => match object.get("type").and_then(Value::as_str) {
561 Some("auto") => ToolChoice::Auto,
562 Some("any") => ToolChoice::Required,
563 Some("none") => ToolChoice::None,
564 Some("tool") => object
565 .get("name")
566 .and_then(Value::as_str)
567 .map(|name| ToolChoice::Tool {
568 name: name.to_string(),
569 })
570 .unwrap_or_else(|| ToolChoice::Raw(value.clone())),
571 _ => ToolChoice::Raw(value.clone()),
572 },
573 _ => ToolChoice::Raw(value.clone()),
574 }
575}
576
577fn encode_anthropic_message(
579 message: &Message,
580 diagnostics: &mut Vec<TranslationDiagnostic>,
581 policy: &TranslationPolicy,
582) -> Result<Value> {
583 let role = match message.role {
584 Role::Assistant => "assistant",
585 Role::User | Role::Tool | Role::System | Role::Developer => "user",
586 };
587 let content = encode_anthropic_content_with_policy(&message.content, diagnostics, policy)?;
588 let simple_text = content.len() == 1
589 && content
590 .first()
591 .and_then(Value::as_object)
592 .and_then(|object| object.get("type"))
593 .and_then(Value::as_str)
594 == Some("text");
595 let content = if simple_text {
596 content
597 .first()
598 .and_then(Value::as_object)
599 .and_then(|object| object.get("text"))
600 .cloned()
601 .unwrap_or_else(|| Value::String(String::new()))
602 } else {
603 Value::Array(content)
604 };
605 Ok(json!({"role": role, "content": content}))
606}
607
608fn encode_anthropic_messages(
610 messages: &[Message],
611 diagnostics: &mut Vec<TranslationDiagnostic>,
612 policy: &TranslationPolicy,
613) -> Result<Vec<Value>> {
614 let mut encoded = Vec::new();
615 let mut index = 0;
616
617 while let Some(message) = messages.get(index) {
618 if !message_is_tool_result_only(message) {
619 encoded.push(encode_anthropic_message(message, diagnostics, policy)?);
620 index += 1;
621 continue;
622 }
623
624 let mut content = Vec::new();
625 while let Some(tool_message) = messages.get(index) {
626 if !message_is_tool_result_only(tool_message) {
627 break;
628 }
629 content.extend(encode_anthropic_content_with_policy(
630 &tool_message.content,
631 diagnostics,
632 policy,
633 )?);
634 index += 1;
635 }
636 encoded.push(json!({"role": "user", "content": content}));
637 }
638
639 Ok(encoded)
640}
641
642fn anthropic_stop_sequences_from_extensions(extensions: &Map<String, Value>) -> Option<Value> {
644 match extensions.get("stop") {
645 Some(Value::String(stop)) => Some(json!([stop])),
646 Some(Value::Array(stops)) => Some(Value::Array(stops.clone())),
647 _ => None,
648 }
649}
650
651fn message_is_tool_result_only(message: &Message) -> bool {
653 (message.role == Role::Tool || message.role == Role::User)
654 && !message.content.is_empty()
655 && message
656 .content
657 .iter()
658 .all(|block| matches!(block, ContentBlock::ToolResult(_)))
659}
660
661fn encode_anthropic_content_with_policy(
663 content: &[ContentBlock],
664 diagnostics: &mut Vec<TranslationDiagnostic>,
665 policy: &TranslationPolicy,
666) -> Result<Vec<Value>> {
667 let mut blocks = Vec::new();
668 for block in content {
669 match block {
670 ContentBlock::Unknown { raw, .. } => {
671 push_lossy(
672 diagnostics,
673 policy,
674 "unknown content block encoded as text for Anthropic",
675 )?;
676 blocks.push(json!({"type": "text", "text": json_string(raw)}));
677 }
678 other => blocks.extend(encode_one_anthropic_block(other)),
679 }
680 }
681 if blocks.is_empty() {
682 blocks.push(json!({"type": "text", "text": ""}));
683 }
684 Ok(blocks)
685}
686
687fn encode_anthropic_content(content: &[ContentBlock]) -> Vec<Value> {
689 let mut blocks = content
690 .iter()
691 .flat_map(encode_one_anthropic_response_block)
692 .collect::<Vec<_>>();
693 if blocks.is_empty() {
694 blocks.push(json!({"type": "text", "text": ""}));
695 }
696 blocks
697}
698
699fn encode_one_anthropic_response_block(block: &ContentBlock) -> Vec<Value> {
701 match block {
702 ContentBlock::Reasoning {
703 text,
704 signature: None,
705 } => vec![json!({
706 "type": "thinking",
707 "thinking": text,
708 "signature": "",
709 })],
710 other => encode_one_anthropic_block(other),
711 }
712}
713
714fn encode_one_anthropic_block(block: &ContentBlock) -> Vec<Value> {
716 match block {
717 ContentBlock::Text { text } | ContentBlock::Refusal { text } => {
718 vec![json!({"type": "text", "text": text})]
719 }
720 ContentBlock::Reasoning {
721 text,
722 signature: Some(signature),
723 } if !signature.is_empty() => vec![json!({
724 "type": "thinking",
725 "thinking": text,
726 "signature": signature,
727 })],
728 ContentBlock::Reasoning { .. } => Vec::new(),
729 ContentBlock::ToolCall(call) => vec![json!({
730 "type": "tool_use",
731 "id": sanitize_anthropic_tool_use_id(&call.id),
732 "name": call.name,
733 "input": anthropic_tool_input(&call.arguments),
734 })],
735 ContentBlock::ToolResult(result) => vec![json!({
736 "type": "tool_result",
737 "tool_use_id": sanitize_anthropic_tool_use_id(&result.tool_call_id),
738 "content": text_from_blocks(&result.content, " "),
739 })],
740 ContentBlock::Image { source } => vec![match source {
741 ImageSource::Url { url, .. } => {
742 json!({"type": "image", "source": {"type": "url", "url": url}})
743 }
744 ImageSource::Base64 { media_type, data } => json!({
745 "type": "image",
746 "source": {
747 "type": "base64",
748 "media_type": media_type.clone().unwrap_or_else(|| "image/png".to_string()),
749 "data": data,
750 },
751 }),
752 ImageSource::Raw(raw) => raw.clone(),
753 }],
754 ContentBlock::File { source } => vec![match source {
755 FileSource::FileId(file_id) => {
756 json!({"type": "document", "source": {"type": "file", "file_id": file_id}})
757 }
758 FileSource::FileData { data, filename } => json!({
759 "type": "document",
760 "source": {
761 "type": "base64",
762 "data": data,
763 "filename": filename,
764 },
765 }),
766 FileSource::Raw(raw) => raw.clone(),
767 }],
768 ContentBlock::Audio { source } => vec![match source {
769 MediaSource::Url { url, media_type } => {
770 json!({"type": "audio", "source": {"type": "url", "url": url, "media_type": media_type}})
771 }
772 MediaSource::Base64 { media_type, data } => json!({
773 "type": "audio",
774 "source": {
775 "type": "base64",
776 "media_type": media_type.clone().unwrap_or_else(|| "audio/mpeg".to_string()),
777 "data": data,
778 },
779 }),
780 MediaSource::Raw(raw) => raw.clone(),
781 }],
782 ContentBlock::Video { source } => vec![match source {
783 MediaSource::Url { url, media_type } => {
784 json!({"type": "video", "source": {"type": "url", "url": url, "media_type": media_type}})
785 }
786 MediaSource::Base64 { media_type, data } => json!({
787 "type": "video",
788 "source": {
789 "type": "base64",
790 "media_type": media_type.clone().unwrap_or_else(|| "video/mp4".to_string()),
791 "data": data,
792 },
793 }),
794 MediaSource::Raw(raw) => raw.clone(),
795 }],
796 ContentBlock::Unknown { raw, .. } => vec![raw.clone()],
797 }
798}
799
800fn anthropic_tool_input(arguments: &Value) -> Value {
803 match arguments {
804 Value::Object(object) => Value::Object(object.clone()),
805 Value::String(text) => serde_json::from_str::<Value>(text)
806 .map_or_else(|_| json!({"raw": text}), ensure_anthropic_tool_input_object),
807 Value::Null => json!({}),
808 other => json!({"value": other}),
809 }
810}
811
812fn ensure_anthropic_tool_input_object(arguments: Value) -> Value {
815 match arguments {
816 Value::Object(_) => arguments,
817 Value::Null => json!({}),
818 other => json!({"value": other}),
819 }
820}
821
822fn encode_anthropic_tools(tools: &[ToolDefinition]) -> Value {
824 Value::Array(
825 tools
826 .iter()
827 .map(|tool| {
828 json!({
829 "name": tool.name,
830 "description": tool.description.clone().unwrap_or_default(),
831 "input_schema": tool.parameters,
832 })
833 })
834 .collect(),
835 )
836}
837
838fn encode_anthropic_tool_choice(choice: &ToolChoice) -> Value {
840 match choice {
841 ToolChoice::Auto => json!({"type": "auto"}),
842 ToolChoice::Required => json!({"type": "any"}),
843 ToolChoice::None => json!({"type": "none"}),
844 ToolChoice::Tool { name } => json!({"type": "tool", "name": name}),
845 ToolChoice::Raw(value) => value.clone(),
846 }
847}
848
849fn decode_anthropic_usage(value: Option<&Value>) -> Usage {
851 let Some(value) = value.and_then(Value::as_object) else {
852 return Usage::default();
853 };
854 let input_tokens = value.get("input_tokens").and_then(Value::as_u64);
855 let output_tokens = value.get("output_tokens").and_then(Value::as_u64);
856 Usage {
857 input_tokens,
858 output_tokens,
859 total_tokens: input_tokens
860 .zip(output_tokens)
861 .map(|(input, output)| input + output),
862 reasoning_tokens: value
863 .get("output_tokens_details")
864 .and_then(|details| details.get("reasoning_tokens"))
865 .and_then(Value::as_u64),
866 }
867}
868
869fn encode_anthropic_usage(usage: &Usage) -> Value {
871 json!({
872 "input_tokens": usage.input_tokens.unwrap_or(0),
873 "output_tokens": usage.output_tokens.unwrap_or(0),
874 })
875}
876
877fn map_anthropic_stop_reason(reason: Option<&str>) -> StopReason {
879 match reason {
880 Some("max_tokens") => StopReason::MaxTokens,
881 Some("tool_use") => StopReason::ToolUse,
882 Some("end_turn") | None => StopReason::EndTurn,
883 _ => StopReason::Unknown,
884 }
885}
886
887fn anthropic_stop_reason(reason: StopReason) -> &'static str {
889 match reason {
890 StopReason::MaxTokens => "max_tokens",
891 StopReason::ToolUse => "tool_use",
892 StopReason::EndTurn
893 | StopReason::ContentFilter
894 | StopReason::Error
895 | StopReason::Unknown => "end_turn",
896 }
897}