fix(provider): serialize Claude tool results as JSON strings

This commit is contained in:
zhefox
2026-05-22 14:41:27 +08:00
parent 2b8ff8a743
commit 2a298de971
3 changed files with 58 additions and 3 deletions

View File

@@ -538,6 +538,15 @@ mod tests {
converted["messages"][2]["content"][0]["tool_use_id"],
"call_1"
);
assert_eq!(
serde_json::from_str::<Value>(
converted["messages"][2]["content"][0]["content"]
.as_str()
.expect("object tool result content should be serialized for Claude")
)
.expect("serialized tool result content should remain JSON"),
json!({"rows": 1})
);
assert_eq!(converted["tool_choice"]["type"], "auto");
assert_eq!(converted["stream"], true);
}

View File

@@ -835,6 +835,15 @@ mod tests {
provider_request_body["messages"][2]["content"][0]["type"],
"tool_result"
);
assert_eq!(
serde_json::from_str::<Value>(
provider_request_body["messages"][2]["content"][0]["content"]
.as_str()
.expect("object tool result content should be serialized for Claude")
)
.expect("serialized tool result content should remain JSON"),
json!({"rows": 1})
);
assert_eq!(provider_request_body["tool_choice"]["type"], "auto");
assert_eq!(provider_request_body["stream"], true);
}

View File

@@ -3820,9 +3820,11 @@ pub(crate) fn canonical_block_to_claude(
);
out.insert(
"content".to_string(),
output
.clone()
.unwrap_or_else(|| Value::String(content_text.clone().unwrap_or_default())),
canonical_tool_result_content_to_claude(
output.as_ref(),
content_text.as_deref(),
role,
),
);
out.insert("is_error".to_string(), Value::Bool(*is_error));
out.extend(namespace_extension_object(extensions, "claude", &out));
@@ -3832,6 +3834,41 @@ pub(crate) fn canonical_block_to_claude(
}
}
fn canonical_tool_result_content_to_claude(
output: Option<&Value>,
content_text: Option<&str>,
role: &CanonicalRole,
) -> Value {
if matches!(role, CanonicalRole::Assistant) {
return output
.cloned()
.unwrap_or_else(|| Value::String(content_text.unwrap_or_default().to_string()));
}
match output {
Some(Value::String(text)) => Value::String(text.clone()),
Some(Value::Array(parts)) if claude_tool_result_content_blocks_are_wire_safe(parts) => {
Value::Array(parts.clone())
}
Some(value) => serde_json::to_string(value)
.map(Value::String)
.unwrap_or_else(|_| Value::String(content_text.unwrap_or_default().to_string())),
None => Value::String(content_text.unwrap_or_default().to_string()),
}
}
fn claude_tool_result_content_blocks_are_wire_safe(parts: &[Value]) -> bool {
!parts.is_empty()
&& parts.iter().all(|part| {
part.as_object()
.and_then(|object| object.get("type"))
.and_then(Value::as_str)
.is_some_and(|block_type| {
matches!(block_type, "text" | "image" | "document" | "file")
})
})
}
pub(crate) fn claude_source_value(
media_type: Option<&str>,
data: Option<&str>,