Skip to main content

nemo_relay/codec/
anthropic.rs

1// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Built-in codec for the Anthropic Messages API.
5//!
6//! Implements [`LlmCodec`] (request decode/encode) and [`LlmResponseCodec`]
7//! (response decode) for the Anthropic Messages API format.
8//!
9//! # Anthropic-specific patterns handled
10//!
11//! - **Content blocks**: Heterogeneous array of `text`, `tool_use`, `thinking`,
12//!   `redacted_thinking`, `mcp_tool_use`, `server_tool_use` blocks
13//! - **Top-level system**: System prompt is a top-level field, not inside messages
14//! - **stop_reason**: Maps to [`FinishReason`] (not `finish_reason`)
15//! - **Tool definitions**: Uses `input_schema` instead of `parameters`
16//! - **Tool choice**: `{"type":"auto"}` / `{"type":"any"}` / `{"type":"tool","name":"..."}`
17//! - **Cache tokens**: `cache_read_input_tokens` / `cache_creation_input_tokens`
18
19use serde::Deserialize;
20
21use crate::api::llm::LlmRequest;
22use crate::api::runtime::{BuiltinLlmCodec, LlmCodecIdentity};
23use crate::error::{FlowError, Result};
24use crate::json::Json;
25
26use super::request::{
27    AnnotatedLlmRequest, ApiSpecificRequest, ContentPart, FunctionDefinition, GenerationParams,
28    Message, MessageContent, ProviderNativeComponent, ToolChoice, ToolChoiceFunction,
29    ToolChoiceFunctionName, ToolDefinition,
30};
31use super::resolve::{ProviderSurface, ProviderSurfaceDescriptor};
32use super::response::{
33    AnnotatedLlmResponse, ApiSpecificResponse, FinishReason, RawUsageCost, ResponseToolCall, Usage,
34    estimate_cost_for_provider, infer_model_provider, provider_reported_cost,
35};
36use super::traits::{LlmCodec, LlmResponseCodec};
37
38// ---------------------------------------------------------------------------
39// Public codec struct
40// ---------------------------------------------------------------------------
41
42/// Built-in codec for the Anthropic Messages API.
43pub struct AnthropicMessagesCodec;
44
45pub(crate) const PROVIDER_SURFACE: ProviderSurfaceDescriptor = ProviderSurfaceDescriptor {
46    surface: ProviderSurface::AnthropicMessages,
47    detect_request: |obj, hint| {
48        // A system-less Anthropic request is shape-identical to OpenAI Chat;
49        // a recognized Anthropic provider hint disambiguates it.
50        let hinted_anthropic = hint.is_some_and(|hint_value| {
51            hint_value == "anthropic" || hint_value == "anthropic.messages"
52        });
53        obj.contains_key("system") || (hinted_anthropic && obj.contains_key("messages"))
54    },
55    detect_response: |obj| {
56        obj.get("type").and_then(Json::as_str) == Some("message")
57            && obj.get("content").is_some_and(Json::is_array)
58    },
59    decode_request: |request| AnthropicMessagesCodec.decode(request),
60    decode_response: |raw| AnthropicMessagesCodec.decode_response(raw),
61    codec_name: "anthropic_messages",
62    request_codec: || std::sync::Arc::new(AnthropicMessagesCodec),
63    response_codec: || std::sync::Arc::new(AnthropicMessagesCodec),
64    streaming_codec: || Box::new(AnthropicMessagesStreamingCodec::new()),
65};
66
67// ---------------------------------------------------------------------------
68// Private intermediate serde structs for response decode
69// ---------------------------------------------------------------------------
70
71#[derive(Deserialize)]
72struct RawAnthropicResponse {
73    id: Option<String>,
74    #[serde(rename = "type")]
75    object_type: Option<String>,
76    role: Option<String>,
77    model: Option<String>,
78    content: Option<Vec<Json>>,
79    stop_reason: Option<String>,
80    stop_sequence: Option<String>,
81    service_tier: Option<String>,
82    container: Option<Json>,
83    usage: Option<RawAnthropicUsage>,
84    #[serde(flatten)]
85    extra: serde_json::Map<String, Json>,
86}
87
88#[derive(Deserialize)]
89struct RawAnthropicUsage {
90    input_tokens: Option<u64>,
91    output_tokens: Option<u64>,
92    cache_read_input_tokens: Option<u64>,
93    cache_creation_input_tokens: Option<u64>,
94    #[serde(rename = "cost_usd")]
95    provider_cost: Option<f64>,
96    cost: Option<RawUsageCost>,
97}
98
99// ---------------------------------------------------------------------------
100// Helper functions
101// ---------------------------------------------------------------------------
102
103/// Map Anthropic `stop_reason` string to normalized [`FinishReason`].
104fn map_anthropic_stop_reason(reason: &str) -> FinishReason {
105    match reason {
106        "end_turn" => FinishReason::Complete,
107        "max_tokens" => FinishReason::Length,
108        "tool_use" => FinishReason::ToolUse,
109        other => FinishReason::Unknown(other.to_string()),
110    }
111}
112
113/// Helper to construct a [`Json`] number from an `f64`.
114fn json_f64(v: f64) -> Json {
115    serde_json::Number::from_f64(v)
116        .map(Json::Number)
117        .unwrap_or(Json::Null)
118}
119
120/// Keys that are modeled in [`AnnotatedLlmRequest`] and should NOT go into `extra`.
121const MODELED_REQUEST_KEYS: &[&str] = &[
122    "system",
123    "messages",
124    "model",
125    "max_tokens",
126    "temperature",
127    "top_p",
128    "stop_sequences",
129    "tools",
130    "tool_choice",
131    "metadata",
132    "service_tier",
133    "stream",
134    "cache_control",
135    "container",
136    "inference_geo",
137    "output_config",
138    "thinking",
139    "top_k",
140    "anthropic-user-profile-id",
141];
142
143/// Decode the Anthropic `tool_choice` JSON value into a normalized [`ToolChoice`].
144///
145/// Anthropic format:
146/// - `{"type": "auto"}` -> `ToolChoice::Auto`
147/// - `{"type": "any"}` -> `ToolChoice::Required`
148/// - `{"type": "none"}` -> `ToolChoice::None`
149/// - `{"type": "tool", "name": "X"}` -> `ToolChoice::Specific`
150fn decode_anthropic_tool_choice(val: &Json) -> Option<ToolChoice> {
151    let obj = val.as_object()?;
152    let tc_type = obj.get("type")?.as_str()?;
153    match tc_type {
154        "auto" => Some(ToolChoice::Auto),
155        "any" => Some(ToolChoice::Required),
156        "none" => Some(ToolChoice::None),
157        "tool" => {
158            let name = obj.get("name")?.as_str()?.to_string();
159            Some(ToolChoice::Specific(ToolChoiceFunction {
160                choice_type: "function".into(),
161                function: ToolChoiceFunctionName { name },
162            }))
163        }
164        _ => None,
165    }
166}
167
168/// Extract Anthropic `disable_parallel_tool_use` from tool_choice and map
169/// to normalized `parallel_tool_calls` semantics.
170fn decode_parallel_tool_calls(val: &Json) -> Result<Option<bool>> {
171    let Some(obj) = val.as_object() else {
172        return Ok(None);
173    };
174    Ok(super::optional_bool(
175        obj,
176        "disable_parallel_tool_use",
177        "Anthropic Messages tool_choice",
178    )?
179    .map(|disabled| !disabled))
180}
181
182/// Encode a normalized [`ToolChoice`] back into Anthropic JSON format.
183fn encode_anthropic_tool_choice(tc: &ToolChoice) -> Result<Json> {
184    match tc {
185        ToolChoice::Auto => Ok(serde_json::json!({"type": "auto"})),
186        ToolChoice::Required => Ok(serde_json::json!({"type": "any"})),
187        ToolChoice::None => Ok(serde_json::json!({"type": "none"})),
188        ToolChoice::Specific(func) => {
189            Ok(serde_json::json!({"type": "tool", "name": func.function.name}))
190        }
191        ToolChoice::ProviderNative(native) if native.provider == "anthropic_messages" => {
192            Ok(native.value.clone())
193        }
194        ToolChoice::ProviderNative(native) => Err(FlowError::InvalidArgument(format!(
195            "tool choice for {} cannot be encoded for Anthropic Messages",
196            native.provider
197        ))),
198    }
199}
200
201fn encode_tool_choice_with_parallel_hint(
202    tc: &ToolChoice,
203    parallel_tool_calls: Option<bool>,
204) -> Result<Json> {
205    let mut value = encode_anthropic_tool_choice(tc)?;
206    if let (Some(parallel), Some(obj)) = (parallel_tool_calls, value.as_object_mut()) {
207        obj.insert("disable_parallel_tool_use".into(), Json::Bool(!parallel));
208    }
209    Ok(value)
210}
211
212fn native_component(provider: &str, value: &Json) -> ProviderNativeComponent {
213    ProviderNativeComponent {
214        provider: provider.to_string(),
215        kind: value
216            .get("type")
217            .and_then(Json::as_str)
218            .unwrap_or("unknown")
219            .to_string(),
220        value: value.clone(),
221    }
222}
223
224fn decode_anthropic_content(value: &Json) -> Result<MessageContent> {
225    if let Some(text) = value.as_str() {
226        return Ok(MessageContent::Text(text.to_string()));
227    }
228    let blocks = value.as_array().ok_or_else(|| {
229        FlowError::InvalidArgument("Anthropic Messages content must be a string or an array".into())
230    })?;
231    let parts = blocks
232        .iter()
233        .map(decode_anthropic_content_part)
234        .collect::<Result<Vec<_>>>()?;
235    Ok(MessageContent::Parts(parts))
236}
237
238fn decode_anthropic_content_part(value: &Json) -> Result<ContentPart> {
239    let obj = value.as_object().ok_or_else(|| {
240        FlowError::InvalidArgument("Anthropic Messages content block must be an object".into())
241    })?;
242    let kind = obj.get("type").and_then(Json::as_str).unwrap_or("unknown");
243    match kind {
244        "text" => {
245            let text = obj.get("text").and_then(Json::as_str).ok_or_else(|| {
246                FlowError::InvalidArgument("Anthropic text block is missing text".into())
247            })?;
248            Ok(ContentPart::Text {
249                text: text.to_string(),
250                extra: obj
251                    .iter()
252                    .filter(|(key, _)| !matches!(key.as_str(), "type" | "text"))
253                    .map(|(key, value)| (key.clone(), value.clone()))
254                    .collect(),
255            })
256        }
257        "image" => Ok(ContentPart::Image {
258            image: Json::Object(
259                obj.iter()
260                    .filter(|(key, _)| key.as_str() != "type")
261                    .map(|(key, value)| (key.clone(), value.clone()))
262                    .collect(),
263            ),
264            extra: serde_json::Map::new(),
265        }),
266        "document" => Ok(ContentPart::File {
267            file: Json::Object(
268                obj.iter()
269                    .filter(|(key, _)| key.as_str() != "type")
270                    .map(|(key, value)| (key.clone(), value.clone()))
271                    .collect(),
272            ),
273            extra: serde_json::Map::new(),
274        }),
275        "tool_use" => {
276            let id = obj.get("id").and_then(Json::as_str).ok_or_else(|| {
277                FlowError::InvalidArgument("Anthropic tool_use block is missing id".into())
278            })?;
279            let name = obj.get("name").and_then(Json::as_str).ok_or_else(|| {
280                FlowError::InvalidArgument("Anthropic tool_use block is missing name".into())
281            })?;
282            let input = obj.get("input").ok_or_else(|| {
283                FlowError::InvalidArgument("Anthropic tool_use block is missing input".into())
284            })?;
285            let extra = obj
286                .iter()
287                .filter(|(key, _)| !matches!(key.as_str(), "type" | "id" | "name" | "input"))
288                .map(|(key, value)| (key.clone(), value.clone()))
289                .collect();
290            Ok(ContentPart::ToolUse {
291                id: id.to_string(),
292                name: name.to_string(),
293                input: input.clone(),
294                extra,
295            })
296        }
297        "tool_result" => {
298            let tool_use_id = obj
299                .get("tool_use_id")
300                .and_then(Json::as_str)
301                .ok_or_else(|| {
302                    FlowError::InvalidArgument(
303                        "Anthropic tool_result block is missing tool_use_id".into(),
304                    )
305                })?;
306            let content = obj.get("content").ok_or_else(|| {
307                FlowError::InvalidArgument("Anthropic tool_result block is missing content".into())
308            })?;
309            let is_error = match obj.get("is_error") {
310                Some(Json::Null) | None => None,
311                Some(value) => Some(value.as_bool().ok_or_else(|| {
312                    FlowError::InvalidArgument(
313                        "Anthropic tool_result is_error must be a boolean".into(),
314                    )
315                })?),
316            };
317            let extra = obj
318                .iter()
319                .filter(|(key, _)| {
320                    !matches!(
321                        key.as_str(),
322                        "type" | "tool_use_id" | "content" | "is_error"
323                    )
324                })
325                .map(|(key, value)| (key.clone(), value.clone()))
326                .collect();
327            Ok(ContentPart::ToolResult {
328                tool_use_id: tool_use_id.to_string(),
329                content: content.clone(),
330                is_error,
331                extra,
332            })
333        }
334        _ => {
335            let native = native_component("anthropic_messages", value);
336            Ok(ContentPart::ProviderNative {
337                provider: native.provider,
338                kind: native.kind,
339                value: native.value,
340            })
341        }
342    }
343}
344
345fn decode_anthropic_message(value: &Json) -> Result<Message> {
346    let obj = value.as_object().ok_or_else(|| {
347        FlowError::InvalidArgument("Anthropic Messages message must be an object".into())
348    })?;
349    let role = obj.get("role").and_then(Json::as_str).ok_or_else(|| {
350        FlowError::InvalidArgument("Anthropic Messages message is missing role".into())
351    })?;
352    let content = obj.get("content").ok_or_else(|| {
353        FlowError::InvalidArgument("Anthropic Messages message is missing content".into())
354    })?;
355    if obj
356        .keys()
357        .any(|key| !matches!(key.as_str(), "role" | "content"))
358    {
359        return Ok(Message::ProviderNative {
360            provider: "anthropic_messages".into(),
361            kind: role.to_string(),
362            value: value.clone(),
363        });
364    }
365    let content = decode_anthropic_content(content)?;
366    match role {
367        "user" => Ok(Message::User {
368            content,
369            name: None,
370        }),
371        "assistant" => Ok(Message::Assistant {
372            content: Some(content),
373            tool_calls: None,
374            name: None,
375        }),
376        "system" => Ok(Message::System {
377            content,
378            name: None,
379        }),
380        _ => Ok(Message::ProviderNative {
381            provider: "anthropic_messages".into(),
382            kind: role.to_string(),
383            value: value.clone(),
384        }),
385    }
386}
387
388fn encode_anthropic_content(content: &MessageContent) -> Result<Json> {
389    match content {
390        MessageContent::Text(text) => Ok(Json::String(text.clone())),
391        MessageContent::Parts(parts) => Ok(Json::Array(
392            parts
393                .iter()
394                .map(encode_anthropic_content_part)
395                .collect::<Result<Vec<_>>>()?,
396        )),
397    }
398}
399
400fn encode_anthropic_content_part(part: &ContentPart) -> Result<Json> {
401    match part {
402        ContentPart::Text { text, extra } => {
403            let mut obj = extra.clone();
404            obj.insert("type".into(), Json::String("text".into()));
405            obj.insert("text".into(), Json::String(text.clone()));
406            Ok(Json::Object(obj))
407        }
408        ContentPart::Image { image, extra } => {
409            let mut obj = image.as_object().cloned().ok_or_else(|| {
410                FlowError::InvalidArgument("Anthropic image payload must be an object".into())
411            })?;
412            obj.extend(extra.clone());
413            obj.insert("type".into(), Json::String("image".into()));
414            Ok(Json::Object(obj))
415        }
416        ContentPart::File { file, extra } => {
417            let mut obj = file.as_object().cloned().ok_or_else(|| {
418                FlowError::InvalidArgument("Anthropic document payload must be an object".into())
419            })?;
420            obj.extend(extra.clone());
421            obj.insert("type".into(), Json::String("document".into()));
422            Ok(Json::Object(obj))
423        }
424        ContentPart::ToolUse {
425            id,
426            name,
427            input,
428            extra,
429        } => {
430            let mut obj = extra.clone();
431            obj.insert("type".into(), Json::String("tool_use".into()));
432            obj.insert("id".into(), Json::String(id.clone()));
433            obj.insert("name".into(), Json::String(name.clone()));
434            obj.insert("input".into(), input.clone());
435            Ok(Json::Object(obj))
436        }
437        ContentPart::ToolResult {
438            tool_use_id,
439            content,
440            is_error,
441            extra,
442        } => {
443            let mut obj = extra.clone();
444            obj.insert("type".into(), Json::String("tool_result".into()));
445            obj.insert("tool_use_id".into(), Json::String(tool_use_id.clone()));
446            obj.insert("content".into(), content.clone());
447            if let Some(is_error) = is_error {
448                obj.insert("is_error".into(), Json::Bool(*is_error));
449            }
450            Ok(Json::Object(obj))
451        }
452        ContentPart::ProviderNative {
453            provider, value, ..
454        } if provider == "anthropic_messages" => Ok(value.clone()),
455        other => Err(FlowError::InvalidArgument(format!(
456            "content part {other:?} cannot be encoded for Anthropic Messages"
457        ))),
458    }
459}
460
461fn encode_anthropic_message(message: &Message) -> Result<Json> {
462    match message {
463        Message::User { content, .. } | Message::System { content, .. } => {
464            let role = if matches!(message, Message::User { .. }) {
465                "user"
466            } else {
467                "system"
468            };
469            let mut obj = serde_json::Map::new();
470            obj.insert("role".into(), Json::String(role.into()));
471            obj.insert("content".into(), encode_anthropic_content(content)?);
472            Ok(Json::Object(obj))
473        }
474        Message::Assistant { content, .. } => {
475            let mut obj = serde_json::Map::new();
476            obj.insert("role".into(), Json::String("assistant".into()));
477            obj.insert(
478                "content".into(),
479                match content {
480                    Some(content) => encode_anthropic_content(content)?,
481                    None => Json::Array(Vec::new()),
482                },
483            );
484            Ok(Json::Object(obj))
485        }
486        Message::ProviderNative {
487            provider, value, ..
488        } if provider == "anthropic_messages" => Ok(value.clone()),
489        other => Err(FlowError::InvalidArgument(format!(
490            "message {other:?} cannot be encoded for Anthropic Messages"
491        ))),
492    }
493}
494
495fn encode_anthropic_tool(tool: &ToolDefinition) -> Result<Json> {
496    match tool {
497        ToolDefinition::Function { function, extra } => {
498            let mut obj = extra.clone();
499            obj.insert("name".into(), Json::String(function.name.clone()));
500            if let Some(description) = &function.description {
501                obj.insert("description".into(), Json::String(description.clone()));
502            }
503            if let Some(parameters) = &function.parameters {
504                obj.insert("input_schema".into(), parameters.clone());
505            }
506            if let Some(strict) = function.strict {
507                obj.insert("strict".into(), Json::Bool(strict));
508            }
509            obj.extend(function.extra.clone());
510            Ok(Json::Object(obj))
511        }
512        ToolDefinition::ProviderNative {
513            provider, value, ..
514        } if provider == "anthropic_messages" => Ok(value.clone()),
515        other => Err(FlowError::InvalidArgument(format!(
516            "tool {other:?} cannot be encoded for Anthropic Messages"
517        ))),
518    }
519}
520
521fn decode_anthropic_tool(value: &Json) -> Result<ToolDefinition> {
522    let obj = value.as_object().ok_or_else(|| {
523        FlowError::InvalidArgument("Anthropic Messages tool must be an object".into())
524    })?;
525    let is_client_tool =
526        obj.get("type").is_none() && obj.contains_key("name") && obj.contains_key("input_schema");
527    if !is_client_tool {
528        let native = native_component("anthropic_messages", value);
529        return Ok(ToolDefinition::ProviderNative {
530            provider: native.provider,
531            kind: native.kind,
532            value: native.value,
533        });
534    }
535
536    let function_extra = serde_json::Map::new();
537    let wrapper_extra = obj
538        .iter()
539        .filter(|(key, _)| {
540            !matches!(
541                key.as_str(),
542                "name" | "description" | "input_schema" | "strict"
543            )
544        })
545        .map(|(key, value)| (key.clone(), value.clone()))
546        .collect();
547    let name =
548        super::optional_string(obj, "name", "Anthropic Messages tool")?.ok_or_else(|| {
549            FlowError::InvalidArgument("Anthropic Messages tool is missing name".into())
550        })?;
551    let description = super::optional_string(obj, "description", "Anthropic Messages tool")?;
552    let strict = super::optional_bool(obj, "strict", "Anthropic Messages tool")?;
553    Ok(ToolDefinition::Function {
554        function: FunctionDefinition {
555            name,
556            description,
557            parameters: obj.get("input_schema").cloned(),
558            strict,
559            extra: function_extra,
560        },
561        extra: wrapper_extra,
562    })
563}
564
565fn patch_extra_fields(
566    obj: &mut serde_json::Map<String, Json>,
567    baseline: &serde_json::Map<String, Json>,
568    edited: &serde_json::Map<String, Json>,
569) {
570    for key in baseline.keys().filter(|key| !edited.contains_key(*key)) {
571        obj.remove(key);
572    }
573    for (key, value) in edited {
574        if baseline.get(key) != Some(value) {
575            obj.insert(key.clone(), value.clone());
576        }
577    }
578}
579
580fn set_or_remove_json(obj: &mut serde_json::Map<String, Json>, key: &str, value: Option<Json>) {
581    if let Some(value) = value {
582        obj.insert(key.into(), value);
583    } else {
584        obj.remove(key);
585    }
586}
587
588fn patch_anthropic_messages_and_model(
589    obj: &mut serde_json::Map<String, Json>,
590    annotated: &AnnotatedLlmRequest,
591    baseline: &AnnotatedLlmRequest,
592) -> Result<()> {
593    if annotated.messages != baseline.messages {
594        let original_messages = obj.get("messages").and_then(Json::as_array);
595        let messages = super::encode_changed_items(
596            &annotated.messages,
597            &baseline.messages,
598            original_messages.map(Vec::as_slice),
599            encode_anthropic_message,
600        )?;
601        obj.insert("messages".into(), Json::Array(messages));
602    }
603    if annotated.instructions != baseline.instructions {
604        set_or_remove_json(
605            obj,
606            "system",
607            annotated
608                .instructions
609                .as_ref()
610                .map(encode_anthropic_content)
611                .transpose()?,
612        );
613    }
614    if annotated.model != baseline.model {
615        set_or_remove_json(obj, "model", annotated.model.clone().map(Json::String));
616    }
617    Ok(())
618}
619
620fn patch_anthropic_params(
621    obj: &mut serde_json::Map<String, Json>,
622    annotated: &AnnotatedLlmRequest,
623    baseline: &AnnotatedLlmRequest,
624) {
625    if annotated.params == baseline.params {
626        return;
627    }
628    let edited = annotated.params.as_ref();
629    let before = baseline.params.as_ref();
630    for (key, value, old_value) in [
631        (
632            "temperature",
633            edited.and_then(|params| params.temperature),
634            before.and_then(|params| params.temperature),
635        ),
636        (
637            "top_p",
638            edited.and_then(|params| params.top_p),
639            before.and_then(|params| params.top_p),
640        ),
641    ] {
642        if value != old_value {
643            set_or_remove_json(obj, key, value.map(json_f64));
644        }
645    }
646    let max_tokens = edited.and_then(|params| params.max_tokens);
647    if max_tokens != before.and_then(|params| params.max_tokens) {
648        set_or_remove_json(obj, "max_tokens", max_tokens.map(Json::from));
649    }
650    let stop = edited.and_then(|params| params.stop.as_ref());
651    if stop != before.and_then(|params| params.stop.as_ref()) {
652        set_or_remove_json(
653            obj,
654            "stop_sequences",
655            stop.map(|values| serde_json::json!(values)),
656        );
657    }
658}
659
660fn patch_anthropic_tools(
661    obj: &mut serde_json::Map<String, Json>,
662    annotated: &AnnotatedLlmRequest,
663    baseline: &AnnotatedLlmRequest,
664) -> Result<()> {
665    if annotated.tools != baseline.tools {
666        let tools = annotated
667            .tools
668            .as_deref()
669            .map(|tools| {
670                super::encode_changed_items(
671                    tools,
672                    baseline.tools.as_deref().unwrap_or(&[]),
673                    obj.get("tools").and_then(Json::as_array).map(Vec::as_slice),
674                    encode_anthropic_tool,
675                )
676            })
677            .transpose()?
678            .map(Json::Array);
679        set_or_remove_json(obj, "tools", tools);
680    }
681    if annotated.tool_choice != baseline.tool_choice
682        || annotated.parallel_tool_calls != baseline.parallel_tool_calls
683    {
684        let tool_choice = match (&annotated.tool_choice, &baseline.tool_choice) {
685            (Some(edited), Some(before)) => {
686                let edited =
687                    encode_tool_choice_with_parallel_hint(edited, annotated.parallel_tool_calls)?;
688                let before =
689                    encode_tool_choice_with_parallel_hint(before, baseline.parallel_tool_calls)?;
690                Some(match obj.get("tool_choice") {
691                    Some(original) => super::patch_changed_json(original, &before, &edited)?,
692                    None => edited,
693                })
694            }
695            (Some(edited), None) => Some(encode_tool_choice_with_parallel_hint(
696                edited,
697                annotated.parallel_tool_calls,
698            )?),
699            (None, _) => annotated
700                .parallel_tool_calls
701                .map(|parallel| {
702                    encode_tool_choice_with_parallel_hint(&ToolChoice::Auto, Some(parallel))
703                })
704                .transpose()?,
705        };
706        set_or_remove_json(obj, "tool_choice", tool_choice);
707    }
708    Ok(())
709}
710
711fn patch_anthropic_common_fields(
712    obj: &mut serde_json::Map<String, Json>,
713    annotated: &AnnotatedLlmRequest,
714    baseline: &AnnotatedLlmRequest,
715) {
716    if annotated.metadata != baseline.metadata {
717        set_or_remove_json(obj, "metadata", annotated.metadata.clone());
718    }
719    if annotated.service_tier != baseline.service_tier {
720        set_or_remove_json(
721            obj,
722            "service_tier",
723            annotated.service_tier.clone().map(Json::String),
724        );
725    }
726    if annotated.stream != baseline.stream {
727        set_or_remove_json(obj, "stream", annotated.stream.map(Json::Bool));
728    }
729}
730
731fn validate_anthropic_supported_fields(
732    annotated: &AnnotatedLlmRequest,
733    baseline: &AnnotatedLlmRequest,
734) -> Result<()> {
735    let unsupported = [
736        annotated.store != baseline.store,
737        annotated.previous_response_id != baseline.previous_response_id,
738        annotated.truncation != baseline.truncation,
739        annotated.reasoning != baseline.reasoning,
740        annotated.include != baseline.include,
741        annotated.user != baseline.user,
742        annotated.max_output_tokens != baseline.max_output_tokens,
743        annotated.max_tool_calls != baseline.max_tool_calls,
744        annotated.top_logprobs != baseline.top_logprobs,
745    ]
746    .into_iter()
747    .any(|changed| changed);
748    if unsupported {
749        return Err(FlowError::InvalidArgument(
750            "request contains fields that cannot be encoded for Anthropic Messages".into(),
751        ));
752    }
753    Ok(())
754}
755
756fn patch_anthropic_api_specific(
757    obj: &mut serde_json::Map<String, Json>,
758    edited: &Option<ApiSpecificRequest>,
759    baseline: &Option<ApiSpecificRequest>,
760) -> Result<()> {
761    match (edited, baseline) {
762        (
763            Some(ApiSpecificRequest::AnthropicMessages {
764                cache_control,
765                container,
766                inference_geo,
767                output_config,
768                thinking,
769                top_k,
770                user_profile_id,
771            }),
772            Some(ApiSpecificRequest::AnthropicMessages {
773                cache_control: old_cache_control,
774                container: old_container,
775                inference_geo: old_inference_geo,
776                output_config: old_output_config,
777                thinking: old_thinking,
778                top_k: old_top_k,
779                user_profile_id: old_user_profile_id,
780            }),
781        ) => {
782            if cache_control != old_cache_control {
783                set_or_remove_json(obj, "cache_control", cache_control.clone());
784            }
785            patch_anthropic_api_strings(
786                obj,
787                &[
788                    ("container", container, old_container),
789                    ("inference_geo", inference_geo, old_inference_geo),
790                    (
791                        "anthropic-user-profile-id",
792                        user_profile_id,
793                        old_user_profile_id,
794                    ),
795                ],
796            );
797            patch_anthropic_api_json(
798                obj,
799                &[
800                    ("output_config", output_config, old_output_config),
801                    ("thinking", thinking, old_thinking),
802                ],
803            );
804            if top_k != old_top_k {
805                set_or_remove_json(obj, "top_k", top_k.map(Json::from));
806            }
807            Ok(())
808        }
809        (None, Some(ApiSpecificRequest::AnthropicMessages { .. })) => {
810            for key in [
811                "cache_control",
812                "container",
813                "inference_geo",
814                "output_config",
815                "thinking",
816                "top_k",
817                "anthropic-user-profile-id",
818            ] {
819                obj.remove(key);
820            }
821            Ok(())
822        }
823        (Some(_), _) | (None, Some(_)) => Err(FlowError::InvalidArgument(
824            "api_specific provider does not match Anthropic Messages".into(),
825        )),
826        (None, None) => Ok(()),
827    }
828}
829
830fn patch_anthropic_api_strings(
831    obj: &mut serde_json::Map<String, Json>,
832    fields: &[(&str, &Option<String>, &Option<String>)],
833) {
834    for (key, edited, before) in fields {
835        if edited != before {
836            set_or_remove_json(obj, key, (*edited).clone().map(Json::String));
837        }
838    }
839}
840
841fn patch_anthropic_api_json(
842    obj: &mut serde_json::Map<String, Json>,
843    fields: &[(&str, &Option<Json>, &Option<Json>)],
844) {
845    for (key, edited, before) in fields {
846        if edited != before {
847            set_or_remove_json(obj, key, (*edited).clone());
848        }
849    }
850}
851
852fn anthropic_text_message(content_blocks: Option<&[Json]>) -> Option<MessageContent> {
853    let text_parts: Vec<&str> = content_blocks
854        .map(|blocks| blocks.iter().filter_map(anthropic_text_block).collect())
855        .unwrap_or_default();
856
857    (!text_parts.is_empty()).then(|| MessageContent::Text(text_parts.join("\n")))
858}
859
860fn anthropic_text_block(block: &Json) -> Option<&str> {
861    if block.get("type")?.as_str()? != "text" {
862        return None;
863    }
864    block.get("text")?.as_str()
865}
866
867fn anthropic_tool_calls(content_blocks: Option<&[Json]>) -> Option<Vec<ResponseToolCall>> {
868    let tool_calls: Vec<ResponseToolCall> = content_blocks
869        .map(|blocks| {
870            blocks
871                .iter()
872                .filter_map(anthropic_tool_call_block)
873                .collect()
874        })
875        .unwrap_or_default();
876
877    (!tool_calls.is_empty()).then_some(tool_calls)
878}
879
880fn anthropic_tool_call_block(block: &Json) -> Option<ResponseToolCall> {
881    if block.get("type")?.as_str()? != "tool_use" {
882        return None;
883    }
884    Some(ResponseToolCall {
885        id: block.get("id")?.as_str()?.to_string(),
886        name: block.get("name")?.as_str()?.to_string(),
887        // CRITICAL: input is already parsed JSON -- clone directly.
888        arguments: block.get("input")?.clone(),
889    })
890}
891
892fn anthropic_usage(
893    raw_usage: Option<RawAnthropicUsage>,
894    model_for_pricing: Option<&str>,
895) -> Option<Usage> {
896    let model_provider = infer_model_provider("anthropic", model_for_pricing);
897    raw_usage.map(|u| {
898        let prompt = u.input_tokens;
899        let completion = u.output_tokens;
900        let mut usage = Usage {
901            prompt_tokens: prompt,
902            completion_tokens: completion,
903            // Anthropic does not supply total_tokens; compute it.
904            total_tokens: match (prompt, completion) {
905                (Some(p), Some(c)) => Some(p + c),
906                _ => None,
907            },
908            cache_read_tokens: u.cache_read_input_tokens,
909            cache_write_tokens: u.cache_creation_input_tokens,
910            cost: provider_reported_cost(u.provider_cost, u.cost),
911        };
912        if usage.cost.is_none() {
913            usage.cost = model_for_pricing.and_then(|model| {
914                estimate_cost_for_provider(model_provider.as_deref(), model, &usage)
915            });
916        }
917        usage
918    })
919}
920
921// ---------------------------------------------------------------------------
922// LlmResponseCodec implementation
923// ---------------------------------------------------------------------------
924
925impl LlmResponseCodec for AnthropicMessagesCodec {
926    fn codec_identity(&self) -> LlmCodecIdentity {
927        LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::AnthropicMessages)
928    }
929
930    fn decode_response(&self, response: &Json) -> Result<AnnotatedLlmResponse> {
931        let raw: RawAnthropicResponse = serde_json::from_value(response.clone())
932            .map_err(|e| FlowError::Internal(format!("Anthropic Messages response decode: {e}")))?;
933
934        let content_blocks = raw.content.as_deref();
935        let message = anthropic_text_message(content_blocks);
936        // Extract tool_use blocks (only "tool_use" type, NOT mcp_tool_use or server_tool_use).
937        let tool_calls = anthropic_tool_calls(content_blocks);
938
939        // Map stop_reason to FinishReason.
940        let finish_reason = raw.stop_reason.as_deref().map(map_anthropic_stop_reason);
941
942        // Map usage.
943        let usage = anthropic_usage(raw.usage, raw.model.as_deref());
944
945        // Build API-specific fields: all content blocks + stop_sequence.
946        let api_specific_content_blocks = raw.content.clone();
947        let api_specific = Some(ApiSpecificResponse::AnthropicMessages {
948            object_type: raw.object_type,
949            role: raw.role,
950            stop_reason: raw.stop_reason,
951            stop_sequence: raw.stop_sequence,
952            service_tier: raw.service_tier,
953            container: raw.container,
954            content_blocks: api_specific_content_blocks,
955        });
956
957        Ok(AnnotatedLlmResponse {
958            id: raw.id,
959            model: raw.model,
960            message,
961            tool_calls,
962            finish_reason,
963            usage,
964            optimization_summary: None,
965            api_specific,
966            extra: raw.extra,
967        })
968    }
969}
970
971// ---------------------------------------------------------------------------
972// LlmCodec implementation
973// ---------------------------------------------------------------------------
974
975impl LlmCodec for AnthropicMessagesCodec {
976    fn codec_identity(&self) -> LlmCodecIdentity {
977        LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::AnthropicMessages)
978    }
979
980    fn decode(&self, request: &LlmRequest) -> Result<AnnotatedLlmRequest> {
981        let obj = request
982            .content
983            .as_object()
984            .ok_or_else(|| FlowError::Internal("request content is not an object".into()))?;
985        let raw_messages = obj.get("messages").ok_or_else(|| {
986            FlowError::InvalidArgument("Anthropic Messages request is missing messages".into())
987        })?;
988        let messages = raw_messages
989            .as_array()
990            .ok_or_else(|| {
991                FlowError::InvalidArgument("Anthropic Messages messages must be an array".into())
992            })?
993            .iter()
994            .map(decode_anthropic_message)
995            .collect::<Result<Vec<_>>>()?;
996        let instructions = obj
997            .get("system")
998            .map(decode_anthropic_content)
999            .transpose()?;
1000        let model = super::optional_string(obj, "model", "Anthropic Messages")?;
1001        let temperature = super::optional_f64(obj, "temperature", "Anthropic Messages")?;
1002        let top_p = super::optional_f64(obj, "top_p", "Anthropic Messages")?;
1003        let max_tokens = super::optional_u64(obj, "max_tokens", "Anthropic Messages")?;
1004        let stop = obj
1005            .get("stop_sequences")
1006            .filter(|value| !value.is_null())
1007            .map(|value| {
1008                serde_json::from_value::<Vec<String>>(value.clone()).map_err(|error| {
1009                    FlowError::InvalidArgument(format!(
1010                        "invalid Anthropic stop_sequences value: {error}"
1011                    ))
1012                })
1013            })
1014            .transpose()?;
1015        let params =
1016            if temperature.is_some() || max_tokens.is_some() || top_p.is_some() || stop.is_some() {
1017                Some(GenerationParams {
1018                    temperature,
1019                    max_tokens,
1020                    top_p,
1021                    stop,
1022                })
1023            } else {
1024                None
1025            };
1026        let tools = obj
1027            .get("tools")
1028            .map(|value| {
1029                value
1030                    .as_array()
1031                    .ok_or_else(|| {
1032                        FlowError::InvalidArgument(
1033                            "Anthropic Messages tools must be an array".into(),
1034                        )
1035                    })?
1036                    .iter()
1037                    .map(decode_anthropic_tool)
1038                    .collect::<Result<Vec<_>>>()
1039            })
1040            .transpose()?;
1041        let tool_choice = obj.get("tool_choice").map(|value| {
1042            decode_anthropic_tool_choice(value).unwrap_or_else(|| {
1043                ToolChoice::ProviderNative(native_component("anthropic_messages", value))
1044            })
1045        });
1046        let parallel_tool_calls = obj
1047            .get("tool_choice")
1048            .map(decode_parallel_tool_calls)
1049            .transpose()?
1050            .flatten();
1051        let service_tier = super::optional_string(obj, "service_tier", "Anthropic Messages")?;
1052        let stream = super::optional_bool(obj, "stream", "Anthropic Messages")?;
1053        let container = super::optional_string(obj, "container", "Anthropic Messages")?;
1054        let inference_geo = super::optional_string(obj, "inference_geo", "Anthropic Messages")?;
1055        let top_k = super::optional_u64(obj, "top_k", "Anthropic Messages")?;
1056        let user_profile_id =
1057            super::optional_string(obj, "anthropic-user-profile-id", "Anthropic Messages")?;
1058        let metadata = super::optional_object(obj, "metadata", "Anthropic Messages")?;
1059        let cache_control = super::optional_object(obj, "cache_control", "Anthropic Messages")?;
1060        let output_config = super::optional_object(obj, "output_config", "Anthropic Messages")?;
1061        let thinking = super::optional_object(obj, "thinking", "Anthropic Messages")?;
1062        let extra: serde_json::Map<String, Json> = obj
1063            .iter()
1064            .filter(|(k, _)| !MODELED_REQUEST_KEYS.contains(&k.as_str()))
1065            .map(|(k, v)| (k.clone(), v.clone()))
1066            .collect();
1067        Ok(AnnotatedLlmRequest {
1068            messages,
1069            instructions,
1070            model,
1071            params,
1072            tools,
1073            tool_choice,
1074            store: None,
1075            previous_response_id: None,
1076            truncation: None,
1077            reasoning: None,
1078            include: None,
1079            user: None,
1080            metadata,
1081            service_tier,
1082            parallel_tool_calls,
1083            max_output_tokens: None,
1084            max_tool_calls: None,
1085            top_logprobs: None,
1086            stream,
1087            api_specific: Some(ApiSpecificRequest::AnthropicMessages {
1088                cache_control,
1089                container,
1090                inference_geo,
1091                output_config,
1092                thinking,
1093                top_k,
1094                user_profile_id,
1095            }),
1096            extra,
1097        })
1098    }
1099
1100    fn encode(&self, annotated: &AnnotatedLlmRequest, original: &LlmRequest) -> Result<LlmRequest> {
1101        let baseline = self.decode(original)?;
1102        let mut content = original.content.clone();
1103        let obj = content
1104            .as_object_mut()
1105            .ok_or_else(|| FlowError::Internal("original content is not an object".into()))?;
1106        patch_anthropic_messages_and_model(obj, annotated, &baseline)?;
1107        patch_anthropic_params(obj, annotated, &baseline);
1108        patch_anthropic_tools(obj, annotated, &baseline)?;
1109        patch_anthropic_common_fields(obj, annotated, &baseline);
1110        validate_anthropic_supported_fields(annotated, &baseline)?;
1111        patch_anthropic_api_specific(obj, &annotated.api_specific, &baseline.api_specific)?;
1112        patch_extra_fields(obj, &baseline.extra, &annotated.extra);
1113
1114        Ok(LlmRequest {
1115            headers: original.headers.clone(),
1116            content,
1117        })
1118    }
1119}
1120
1121// ---------------------------------------------------------------------------
1122// Streaming codec
1123// ---------------------------------------------------------------------------
1124
1125/// Streaming counterpart to [`AnthropicMessagesCodec`].
1126///
1127/// Replays the Anthropic Messages SSE event sequence into the same JSON shape Anthropic returns
1128/// for a non-streaming request (`{id, type, role, model, content, stop_reason, stop_sequence,
1129/// usage}`). Once finalized, the assembled JSON can be fed back through
1130/// [`AnthropicMessagesCodec::decode_response`] to produce an
1131/// [`AnnotatedLlmResponse`] — meaning streaming and
1132/// non-streaming Anthropic requests converge on the same observability output.
1133///
1134/// Internal state lives behind `Arc<Mutex<...>>` so the `&self`-produced collector and finalizer
1135/// closures share access. Each instance is single-use because [`LlmFinalizerFn`] consumes the
1136/// finalize step.
1137///
1138/// [`LlmFinalizerFn`]: crate::api::runtime::LlmFinalizerFn
1139pub struct AnthropicMessagesStreamingCodec {
1140    state: std::sync::Arc<std::sync::Mutex<AnthropicMessagesStreamingState>>,
1141}
1142
1143impl AnthropicMessagesStreamingCodec {
1144    /// Creates a fresh streaming codec with empty accumulator state.
1145    pub fn new() -> Self {
1146        Self {
1147            state: std::sync::Arc::new(std::sync::Mutex::new(
1148                AnthropicMessagesStreamingState::default(),
1149            )),
1150        }
1151    }
1152}
1153
1154impl Default for AnthropicMessagesStreamingCodec {
1155    fn default() -> Self {
1156        Self::new()
1157    }
1158}
1159
1160impl super::streaming::StreamingCodec for AnthropicMessagesStreamingCodec {
1161    fn collector(&self) -> crate::api::runtime::LlmCollectorFn {
1162        let state = std::sync::Arc::clone(&self.state);
1163        Box::new(move |event: Json| -> Result<()> {
1164            let mut guard = state
1165                .lock()
1166                .unwrap_or_else(|poisoned| poisoned.into_inner());
1167            guard.observe(&event);
1168            Ok(())
1169        })
1170    }
1171
1172    fn finalizer(&self) -> crate::api::runtime::LlmFinalizerFn {
1173        let state = std::sync::Arc::clone(&self.state);
1174        Box::new(move || -> Json {
1175            let mut guard = state
1176                .lock()
1177                .unwrap_or_else(|poisoned| poisoned.into_inner());
1178            // Move state out so finalize can consume it; the codec is single-use, so leaving a
1179            // default behind is intentional and never observed by another caller.
1180            std::mem::take(&mut *guard).finalize()
1181        })
1182    }
1183}
1184
1185#[derive(Debug, Default)]
1186struct AnthropicMessagesStreamingState {
1187    id: Option<String>,
1188    type_: Option<String>,
1189    role: Option<String>,
1190    model: Option<String>,
1191    /// Latest usage snapshot. `message_start` carries an initial value (input tokens, zero output
1192    /// so far); `message_delta` updates it cumulatively. Last write wins.
1193    usage: Option<Json>,
1194    stop_reason: Option<String>,
1195    /// Stored as raw `Json` to preserve `null` (Anthropic's wire shape) versus omitted.
1196    stop_sequence: Option<Json>,
1197    /// Indexed by the SSE event's `index` field. `None` slots accommodate sparse indices though
1198    /// Anthropic emits them in order today.
1199    blocks: Vec<Option<StreamingBlock>>,
1200}
1201
1202#[derive(Debug, Default, Clone)]
1203struct StreamingBlock {
1204    /// The `content_block` JSON captured at `content_block_start`. Deltas mutate fields directly
1205    /// for blocks Anthropic delivers incrementally (text, tool_use input, citations); other block
1206    /// types (server_tool_use results) ship complete at start and pass through unchanged.
1207    skeleton: serde_json::Map<String, Json>,
1208    text: String,
1209    has_text: bool,
1210    partial_json: String,
1211    has_partial_json: bool,
1212    citations: Vec<Json>,
1213    has_citations: bool,
1214}
1215
1216impl AnthropicMessagesStreamingState {
1217    fn observe(&mut self, event: &Json) {
1218        let event_type = event.get("type").and_then(Json::as_str).unwrap_or("");
1219        match event_type {
1220            "message_start" => self.observe_message_start(event),
1221            "content_block_start" => self.observe_content_block_start(event),
1222            "content_block_delta" => self.observe_content_block_delta(event),
1223            "message_delta" => self.observe_message_delta(event),
1224            // content_block_stop, message_stop, ping, and any unknown event type carry no
1225            // accumulator-relevant payload. Unknown types are ignored rather than erroring so a
1226            // future Anthropic event addition does not break observability.
1227            _ => {}
1228        }
1229    }
1230
1231    fn observe_message_start(&mut self, event: &Json) {
1232        let Some(message) = event.get("message") else {
1233            return;
1234        };
1235        if let Some(id) = message.get("id").and_then(Json::as_str) {
1236            self.id = Some(id.to_string());
1237        }
1238        if let Some(model) = message.get("model").and_then(Json::as_str) {
1239            self.model = Some(model.to_string());
1240        }
1241        if let Some(role) = message.get("role").and_then(Json::as_str) {
1242            self.role = Some(role.to_string());
1243        }
1244        if let Some(t) = message.get("type").and_then(Json::as_str) {
1245            self.type_ = Some(t.to_string());
1246        }
1247        if let Some(usage) = message.get("usage") {
1248            self.usage = Some(usage.clone());
1249        }
1250    }
1251
1252    fn observe_content_block_start(&mut self, event: &Json) {
1253        let Some(index) = event.get("index").and_then(Json::as_u64) else {
1254            return;
1255        };
1256        let Some(content_block) = event.get("content_block") else {
1257            return;
1258        };
1259        let skeleton = match content_block {
1260            Json::Object(map) => map.clone(),
1261            _ => return,
1262        };
1263        let index = index as usize;
1264        while self.blocks.len() <= index {
1265            self.blocks.push(None);
1266        }
1267        self.blocks[index] = Some(StreamingBlock {
1268            skeleton,
1269            ..StreamingBlock::default()
1270        });
1271    }
1272
1273    fn observe_content_block_delta(&mut self, event: &Json) {
1274        let Some(index) = event.get("index").and_then(Json::as_u64) else {
1275            return;
1276        };
1277        let index = index as usize;
1278        let Some(delta) = event.get("delta") else {
1279            return;
1280        };
1281        let delta_type = delta.get("type").and_then(Json::as_str).unwrap_or("");
1282        let Some(slot) = self.blocks.get_mut(index) else {
1283            return;
1284        };
1285        let Some(block) = slot.as_mut() else { return };
1286        match delta_type {
1287            "text_delta" => {
1288                if let Some(text) = delta.get("text").and_then(Json::as_str) {
1289                    block.text.push_str(text);
1290                    block.has_text = true;
1291                }
1292            }
1293            "input_json_delta" => {
1294                if let Some(partial) = delta.get("partial_json").and_then(Json::as_str) {
1295                    block.partial_json.push_str(partial);
1296                    block.has_partial_json = true;
1297                }
1298            }
1299            "citations_delta" => {
1300                if let Some(citation) = delta.get("citation") {
1301                    block.citations.push(citation.clone());
1302                    block.has_citations = true;
1303                }
1304            }
1305            // thinking_delta, signature_delta, and any future delta types fall through; the block
1306            // skeleton retains whatever shape was set at content_block_start.
1307            _ => {}
1308        }
1309    }
1310
1311    fn observe_message_delta(&mut self, event: &Json) {
1312        if let Some(delta) = event.get("delta") {
1313            if let Some(reason) = delta.get("stop_reason").and_then(Json::as_str) {
1314                self.stop_reason = Some(reason.to_string());
1315            }
1316            if let Some(seq) = delta.get("stop_sequence") {
1317                self.stop_sequence = Some(seq.clone());
1318            }
1319        }
1320        if let Some(usage) = event.get("usage") {
1321            self.usage = Some(usage.clone());
1322        }
1323    }
1324
1325    fn finalize(self) -> Json {
1326        let mut output = serde_json::Map::new();
1327        if let Some(id) = self.id {
1328            output.insert("id".to_string(), Json::String(id));
1329        }
1330        if let Some(t) = self.type_ {
1331            output.insert("type".to_string(), Json::String(t));
1332        }
1333        if let Some(role) = self.role {
1334            output.insert("role".to_string(), Json::String(role));
1335        }
1336        if let Some(model) = self.model {
1337            output.insert("model".to_string(), Json::String(model));
1338        }
1339        let content: Vec<Json> = self
1340            .blocks
1341            .into_iter()
1342            .filter_map(|block| block.map(StreamingBlock::finalize))
1343            .collect();
1344        output.insert("content".to_string(), Json::Array(content));
1345        if let Some(reason) = self.stop_reason {
1346            output.insert("stop_reason".to_string(), Json::String(reason));
1347        }
1348        if let Some(seq) = self.stop_sequence {
1349            output.insert("stop_sequence".to_string(), seq);
1350        }
1351        if let Some(usage) = self.usage {
1352            output.insert("usage".to_string(), usage);
1353        }
1354        Json::Object(output)
1355    }
1356}
1357
1358impl StreamingBlock {
1359    fn finalize(mut self) -> Json {
1360        if self.has_text {
1361            self.skeleton
1362                .insert("text".to_string(), Json::String(self.text));
1363        }
1364        if self.has_partial_json {
1365            // Concatenated `partial_json` fragments are expected to parse as a JSON object — that's
1366            // the assembled tool input. If parsing fails (Anthropic emits malformed deltas, stream
1367            // truncated mid-block), surface the raw concatenation so observability still captures
1368            // something rather than dropping the call.
1369            let parsed = match serde_json::from_str::<Json>(&self.partial_json) {
1370                Ok(value) => value,
1371                Err(_) => Json::String(self.partial_json),
1372            };
1373            self.skeleton.insert("input".to_string(), parsed);
1374        }
1375        if self.has_citations {
1376            self.skeleton
1377                .insert("citations".to_string(), Json::Array(self.citations));
1378        }
1379        Json::Object(self.skeleton)
1380    }
1381}
1382
1383// ---------------------------------------------------------------------------
1384// Tests
1385// ---------------------------------------------------------------------------
1386
1387#[cfg(test)]
1388#[path = "../../tests/unit/codec/anthropic_tests.rs"]
1389mod tests;