fix: stringify Anthropic typeless blocks as JSON
This commit is contained in:
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
@@ -280,7 +281,10 @@ class AnthropicProvider(LLMProvider):
|
|||||||
# Anthropic requires every content block to declare a "type".
|
# Anthropic requires every content block to declare a "type".
|
||||||
# A tool that returned a bare dict lands here; coerce it to
|
# A tool that returned a bare dict lands here; coerce it to
|
||||||
# a text block instead of emitting one that the API rejects.
|
# a text block instead of emitting one that the API rejects.
|
||||||
blocks.append({"type": "text", "text": str(item)})
|
blocks.append({
|
||||||
|
"type": "text",
|
||||||
|
"text": AnthropicProvider._stringify_typeless_block(item),
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
blocks.append(item)
|
blocks.append(item)
|
||||||
else:
|
else:
|
||||||
@@ -324,11 +328,18 @@ class AnthropicProvider(LLMProvider):
|
|||||||
# A tool that returned a bare dict (or a list of dicts) lands
|
# A tool that returned a bare dict (or a list of dicts) lands
|
||||||
# here; coerce it to a text block instead of emitting a block
|
# here; coerce it to a text block instead of emitting a block
|
||||||
# the API rejects with "content.0.type: Field required".
|
# the API rejects with "content.0.type: Field required".
|
||||||
result.append({"type": "text", "text": str(item)})
|
result.append({
|
||||||
|
"type": "text",
|
||||||
|
"text": AnthropicProvider._stringify_typeless_block(item),
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
result.append(item)
|
result.append(item)
|
||||||
return result or "(empty)"
|
return result or "(empty)"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _stringify_typeless_block(block: dict[str, Any]) -> str:
|
||||||
|
return json.dumps(block, ensure_ascii=False, sort_keys=True, default=str)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convert_image_block(block: dict[str, Any]) -> dict[str, Any] | None:
|
def _convert_image_block(block: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
"""Convert OpenAI image_url block to Anthropic image block."""
|
"""Convert OpenAI image_url block to Anthropic image block."""
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ def test_convert_user_content_coerces_typeless_dict():
|
|||||||
{"foo": "bar"},
|
{"foo": "bar"},
|
||||||
{"type": "text", "text": "ok"},
|
{"type": "text", "text": "ok"},
|
||||||
])
|
])
|
||||||
assert result[0] == {"type": "text", "text": str({"foo": "bar"})}
|
assert result[0] == {"type": "text", "text": '{"foo": "bar"}'}
|
||||||
assert result[1] == {"type": "text", "text": "ok"}
|
assert result[1] == {"type": "text", "text": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@@ -81,7 +81,16 @@ def test_convert_user_content_coerces_mixed_typeless():
|
|||||||
{"key": "val"},
|
{"key": "val"},
|
||||||
])
|
])
|
||||||
assert result[0] == {"type": "text", "text": "42"}
|
assert result[0] == {"type": "text", "text": "42"}
|
||||||
assert result[1] == {"type": "text", "text": str({"key": "val"})}
|
assert result[1] == {"type": "text", "text": '{"key": "val"}'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_assistant_blocks_coerce_typeless_dict_to_json_text():
|
||||||
|
blocks = AnthropicProvider._assistant_blocks({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": [{"answer": "ok", "count": 2}],
|
||||||
|
})
|
||||||
|
|
||||||
|
assert blocks == [{"type": "text", "text": '{"answer": "ok", "count": 2}'}]
|
||||||
|
|
||||||
|
|
||||||
def test_convert_assistant_message_repairs_history_tool_arguments():
|
def test_convert_assistant_message_repairs_history_tool_arguments():
|
||||||
|
|||||||
Reference in New Issue
Block a user