fix(anthropic-adapter): properly translate Anthropic image format to OpenAI (#16202)

* fix(anthropic-adapter): properly translate Anthropic image format to OpenAI

Fixed bug where images were stripped during Anthropic Messages API to Azure
OpenAI translation. Image source data was being stringified instead of having
fields properly extracted.

- Added _translate_anthropic_image_to_openai() helper method
- Support both base64 and URL image formats per Anthropic API spec
- Refactored user message and tool result image handling

* test(anthropic-adapter): add comprehensive image translation tests

Add 5 unit tests covering image translation from Anthropic to OpenAI format:
- User messages with base64 images
- User messages with URL images
- Tool results with base64 images
- Tool results with URL images
- Mixed content with multiple images
This commit is contained in:
Niv Goldenberg 2025-11-04 02:53:44 +00:00 committed by GitHub
parent bb86c94df4
commit 232d1558dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 412 additions and 76 deletions

View file

@ -167,14 +167,20 @@ class LiteLLMAnthropicMessagesAdapter:
)
new_user_content_list.append(text_obj)
elif content.get("type") == "image":
image_url = ChatCompletionImageUrlObject(
url=f"data:{content.get('type', '')};base64,{content.get('source', '')}"
)
image_obj = ChatCompletionImageObject(
type="image_url", image_url=image_url
# Convert Anthropic image format to OpenAI format
source = content.get("source", {})
openai_image_url = (
self._translate_anthropic_image_to_openai(source)
)
new_user_content_list.append(image_obj)
if openai_image_url:
image_url_obj = ChatCompletionImageUrlObject(
url=openai_image_url
)
image_obj = ChatCompletionImageObject(
type="image_url", image_url=image_url_obj
)
new_user_content_list.append(image_obj)
elif content.get("type") == "tool_result":
if "content" not in content:
tool_result = ChatCompletionToolMessage(
@ -210,13 +216,21 @@ class LiteLLMAnthropicMessagesAdapter:
)
tool_message_list.append(tool_result)
elif c.get("type") == "image":
image_str = f"data:{c.get('type', '')};base64,{c.get('source', '')}"
# Convert Anthropic image format to OpenAI format for tool results
source = c.get("source", {})
openai_image_url = (
self._translate_anthropic_image_to_openai(
source
)
or ""
)
tool_result = ChatCompletionToolMessage(
role="tool",
tool_call_id=content.get(
"tool_use_id", ""
),
content=image_str,
content=openai_image_url,
)
tool_message_list.append(tool_result)
@ -232,7 +246,9 @@ class LiteLLMAnthropicMessagesAdapter:
## ASSISTANT MESSAGE ##
assistant_message_str: Optional[str] = None
tool_calls: List[ChatCompletionAssistantToolCall] = []
thinking_blocks: List[Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock]] = []
thinking_blocks: List[
Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock]
] = []
if m["role"] == "assistant":
if isinstance(m.get("content"), str):
assistant_message_str = str(m.get("content", ""))
@ -264,23 +280,30 @@ class LiteLLMAnthropicMessagesAdapter:
type="thinking",
thinking=content.get("thinking") or "",
signature=content.get("signature") or "",
cache_control=content.get("cache_control", {})
cache_control=content.get("cache_control", {}),
)
thinking_blocks.append(thinking_block)
elif content.get("type") == "redacted_thinking":
redacted_thinking_block = ChatCompletionRedactedThinkingBlock(
type="redacted_thinking",
data=content.get("data") or "",
cache_control=content.get("cache_control", {})
redacted_thinking_block = (
ChatCompletionRedactedThinkingBlock(
type="redacted_thinking",
data=content.get("data") or "",
cache_control=content.get("cache_control", {}),
)
)
thinking_blocks.append(redacted_thinking_block)
if assistant_message_str is not None or len(tool_calls) > 0 or len(thinking_blocks) > 0:
if (
assistant_message_str is not None
or len(tool_calls) > 0
or len(thinking_blocks) > 0
):
assistant_message = ChatCompletionAssistantMessage(
role="assistant",
content=assistant_message_str,
thinking_blocks=thinking_blocks if len(thinking_blocks) > 0 else None,
thinking_blocks=(
thinking_blocks if len(thinking_blocks) > 0 else None
),
)
if len(tool_calls) > 0:
assistant_message["tool_calls"] = tool_calls
@ -406,19 +429,55 @@ class LiteLLMAnthropicMessagesAdapter:
return new_kwargs
def _translate_openai_content_to_anthropic(
self, choices: List[Choices]
) -> List[
Union[AnthropicResponseContentBlockText, AnthropicResponseContentBlockToolUse, AnthropicResponseContentBlockThinking, AnthropicResponseContentBlockRedactedThinking]
def _translate_anthropic_image_to_openai(self, image_source: dict) -> Optional[str]:
"""
Translate Anthropic image source format to OpenAI-compatible image URL.
Anthropic supports two image source formats:
1. Base64: {"type": "base64", "media_type": "image/jpeg", "data": "..."}
2. URL: {"type": "url", "url": "https://..."}
Returns the properly formatted image URL string, or None if invalid format.
"""
if not isinstance(image_source, dict):
return None
source_type = image_source.get("type")
if source_type == "base64":
# Base64 image format
media_type = image_source.get("media_type", "image/jpeg")
image_data = image_source.get("data", "")
if image_data:
return f"data:{media_type};base64,{image_data}"
elif source_type == "url":
# URL-referenced image format
return image_source.get("url", "")
return None
def _translate_openai_content_to_anthropic(self, choices: List[Choices]) -> List[
Union[
AnthropicResponseContentBlockText,
AnthropicResponseContentBlockToolUse,
AnthropicResponseContentBlockThinking,
AnthropicResponseContentBlockRedactedThinking,
]
]:
new_content: List[
Union[
AnthropicResponseContentBlockText, AnthropicResponseContentBlockToolUse, AnthropicResponseContentBlockThinking, AnthropicResponseContentBlockRedactedThinking
AnthropicResponseContentBlockText,
AnthropicResponseContentBlockToolUse,
AnthropicResponseContentBlockThinking,
AnthropicResponseContentBlockRedactedThinking,
]
] = []
for choice in choices:
# Handle thinking blocks first
if hasattr(choice.message, 'thinking_blocks') and choice.message.thinking_blocks:
if (
hasattr(choice.message, "thinking_blocks")
and choice.message.thinking_blocks
):
for thinking_block in choice.message.thinking_blocks:
if thinking_block.get("type") == "thinking":
thinking_value = thinking_block.get("thinking", "")
@ -426,8 +485,16 @@ class LiteLLMAnthropicMessagesAdapter:
new_content.append(
AnthropicResponseContentBlockThinking(
type="thinking",
thinking=str(thinking_value) if thinking_value is not None else "",
signature=str(signature_value) if signature_value is not None else None,
thinking=(
str(thinking_value)
if thinking_value is not None
else ""
),
signature=(
str(signature_value)
if signature_value is not None
else None
),
)
)
elif thinking_block.get("type") == "redacted_thinking":
@ -438,7 +505,7 @@ class LiteLLMAnthropicMessagesAdapter:
data=str(data_value) if data_value is not None else "",
)
)
# Handle tool calls
if (
choice.message.tool_calls is not None
@ -450,7 +517,11 @@ class LiteLLMAnthropicMessagesAdapter:
type="tool_use",
id=tool_call.id,
name=tool_call.function.name or "",
input=json.loads(tool_call.function.arguments) if tool_call.function.arguments else {},
input=(
json.loads(tool_call.function.arguments)
if tool_call.function.arguments
else {}
),
)
)
# Handle text content
@ -525,8 +596,8 @@ class LiteLLMAnthropicMessagesAdapter:
name=choice.delta.tool_calls[0].function.name or "",
input={},
)
elif (
isinstance(choice, StreamingChoices) and hasattr(choice.delta, "thinking_blocks")
elif isinstance(choice, StreamingChoices) and hasattr(
choice.delta, "thinking_blocks"
):
thinking_blocks = choice.delta.thinking_blocks or []
if len(thinking_blocks) > 0:
@ -539,22 +610,26 @@ class LiteLLMAnthropicMessagesAdapter:
assert isinstance(signature, str)
if thinking and signature:
raise ValueError("Both `thinking` and `signature` in a single streaming chunk isn't supported.")
raise ValueError(
"Both `thinking` and `signature` in a single streaming chunk isn't supported."
)
return "thinking", ChatCompletionThinkingBlock(
type="thinking",
thinking=thinking,
signature=signature
type="thinking", thinking=thinking, signature=signature
)
return "text", TextBlock(type="text", text="")
def _translate_streaming_openai_chunk_to_anthropic(
self, choices: List[Union[OpenAIStreamingChoice, StreamingChoices]]
) -> Tuple[
Literal["text_delta", "input_json_delta", "thinking_delta", "signature_delta"],
Union[ContentTextBlockDelta, ContentJsonBlockDelta, ContentThinkingBlockDelta, ContentThinkingSignatureBlockDelta],
Union[
ContentTextBlockDelta,
ContentJsonBlockDelta,
ContentThinkingBlockDelta,
ContentThinkingSignatureBlockDelta,
],
]:
text: str = ""
@ -572,7 +647,9 @@ class LiteLLMAnthropicMessagesAdapter:
and tool.function.arguments is not None
):
partial_json = (partial_json or "") + tool.function.arguments
elif isinstance(choice, StreamingChoices) and hasattr(choice.delta, "thinking_blocks"):
elif isinstance(choice, StreamingChoices) and hasattr(
choice.delta, "thinking_blocks"
):
thinking_blocks = choice.delta.thinking_blocks or []
if len(thinking_blocks) > 0:
for thinking_block in thinking_blocks:
@ -585,19 +662,24 @@ class LiteLLMAnthropicMessagesAdapter:
reasoning_content += thinking
reasoning_signature += signature
if reasoning_content and reasoning_signature:
raise ValueError("Both `reasoning` and `signature` in a single streaming chunk isn't supported.")
if reasoning_content and reasoning_signature:
raise ValueError(
"Both `reasoning` and `signature` in a single streaming chunk isn't supported."
)
if partial_json is not None:
return "input_json_delta", ContentJsonBlockDelta(
type="input_json_delta", partial_json=partial_json
)
elif reasoning_content:
return "thinking_delta", ContentThinkingBlockDelta(type="thinking_delta", thinking=reasoning_content)
return "thinking_delta", ContentThinkingBlockDelta(
type="thinking_delta", thinking=reasoning_content
)
elif reasoning_signature:
return "signature_delta", ContentThinkingSignatureBlockDelta(type="signature_delta", signature=reasoning_signature)
return "signature_delta", ContentThinkingSignatureBlockDelta(
type="signature_delta", signature=reasoning_signature
)
else:
return "text_delta", ContentTextBlockDelta(type="text_delta", text=text)

View file

@ -2,11 +2,9 @@ import os
import sys
import pytest
from fastapi.testclient import TestClient
sys.path.insert(0, os.path.abspath("../../../../.."))
from unittest.mock import patch
from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import (
LiteLLMAnthropicMessagesAdapter,
@ -211,7 +209,7 @@ def test_translate_anthropic_messages_to_openai_thinking_blocks():
anthropic_messages = [
AnthropicMessagesUserMessageParam(
role="user",
content=[{"type": "text", "text": "What's the weather in Boston?"}]
content=[{"type": "text", "text": "What's the weather in Boston?"}],
),
AnthopicMessagesAssistantMessageParam(
role="assistant",
@ -219,7 +217,7 @@ def test_translate_anthropic_messages_to_openai_thinking_blocks():
{
"type": "thinking",
"thinking": "I will call the get_weather tool.",
"signature": "sigsig"
"signature": "sigsig",
},
{
"type": "redacted_thinking",
@ -229,9 +227,9 @@ def test_translate_anthropic_messages_to_openai_thinking_blocks():
"type": "tool_use",
"id": "toolu_01234",
"name": "get_weather",
"input": {"location": "Boston"}
}
]
"input": {"location": "Boston"},
},
],
),
]
@ -243,7 +241,10 @@ def test_translate_anthropic_messages_to_openai_thinking_blocks():
assert "thinking_blocks" in result[1]
assert len(result[1]["thinking_blocks"]) == 2
assert result[1]["thinking_blocks"][0]["type"] == "thinking"
assert result[1]["thinking_blocks"][0]["thinking"] == "I will call the get_weather tool."
assert (
result[1]["thinking_blocks"][0]["thinking"]
== "I will call the get_weather tool."
)
assert result[1]["thinking_blocks"][0]["signature"] == "sigsig"
assert result[1]["thinking_blocks"][1]["type"] == "redacted_thinking"
assert result[1]["thinking_blocks"][1]["data"] == "REDACTED"
@ -258,7 +259,7 @@ def test_translate_anthropic_messages_to_openai_tool_message_placement():
anthropic_messages = [
AnthropicMessagesUserMessageParam(
role="user",
content=[{"type": "text", "text": "What's the weather in Boston?"}]
content=[{"type": "text", "text": "What's the weather in Boston?"}],
),
AnthopicMessagesAssistantMessageParam(
role="assistant",
@ -267,9 +268,9 @@ def test_translate_anthropic_messages_to_openai_tool_message_placement():
"type": "tool_use",
"id": "toolu_01234",
"name": "get_weather",
"input": {"location": "Boston"}
"input": {"location": "Boston"},
}
]
],
),
AnthropicMessagesUserMessageParam(
role="user",
@ -277,11 +278,11 @@ def test_translate_anthropic_messages_to_openai_tool_message_placement():
{
"type": "tool_result",
"tool_use_id": "toolu_01234",
"content": "Sunny, 75°F"
"content": "Sunny, 75°F",
},
{"type": "text", "text": "What about tomorrow?"}
]
)
{"type": "text", "text": "What about tomorrow?"},
],
),
]
adapter = LiteLLMAnthropicMessagesAdapter()
@ -294,13 +295,19 @@ def test_translate_anthropic_messages_to_openai_tool_message_placement():
for i, msg in enumerate(result):
if isinstance(msg, dict) and msg.get("role") == "tool":
tool_message_idx = i
elif isinstance(msg, dict) and msg.get("role") == "user" and "What about tomorrow?" in str(msg.get("content", "")):
elif (
isinstance(msg, dict)
and msg.get("role") == "user"
and "What about tomorrow?" in str(msg.get("content", ""))
):
user_message_idx = i
break
assert tool_message_idx is not None, "Tool message not found"
assert user_message_idx is not None, "User message not found"
assert tool_message_idx < user_message_idx, "Tool message should be placed before user message"
assert (
tool_message_idx < user_message_idx
), "Tool message should be placed before user message"
def test_translate_openai_content_to_anthropic_empty_function_arguments():
@ -316,11 +323,10 @@ def test_translate_openai_content_to_anthropic_empty_function_arguments():
id="call_empty_args",
type="function",
function=Function(
name="test_function",
arguments="" # empty arguments string
)
name="test_function", arguments="" # empty arguments string
),
)
]
],
)
)
]
@ -335,7 +341,6 @@ def test_translate_openai_content_to_anthropic_empty_function_arguments():
assert result[0].input == {}, "Empty function arguments should result in empty dict"
def test_translate_streaming_openai_chunk_to_anthropic_with_partial_json():
"""Test that partial tool arguments are correctly handled as input_json_delta."""
choices = [
@ -344,15 +349,15 @@ def test_translate_streaming_openai_chunk_to_anthropic_with_partial_json():
index=1,
delta=Delta(
provider_specific_fields=None,
content='',
role='assistant',
content="",
role="assistant",
function_call=None,
tool_calls=[
ChatCompletionDeltaToolCall(
id=None,
function=Function(arguments=': "San ', name=None),
type='function',
index=0
type="function",
index=0,
)
],
audio=None,
@ -372,11 +377,10 @@ def test_translate_streaming_openai_chunk_to_anthropic_with_partial_json():
print("Content block delta:", content_block_delta)
assert type_of_content == "input_json_delta"
assert content_block_delta["type"] == "input_json_delta"
assert content_block_delta["type"] == "input_json_delta"
assert content_block_delta["partial_json"] == ': "San '
def test_translate_openai_content_to_anthropic_thinking_and_redacted_thinking():
openai_choices = [
Choices(
@ -389,11 +393,8 @@ def test_translate_openai_content_to_anthropic_thinking_and_redacted_thinking():
"thinking": "I need to summar",
"signature": "sigsig",
},
{
"type": "redacted_thinking",
"data": "REDACTED"
}
]
{"type": "redacted_thinking", "data": "REDACTED"},
],
)
)
]
@ -450,7 +451,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_with_thinking():
)
assert type_of_content == "thinking_delta"
assert content_block_delta["type"] == "thinking_delta"
assert content_block_delta["type"] == "thinking_delta"
assert content_block_delta["thinking"] == "I need to summar"
@ -495,7 +496,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_with_thinking():
)
assert type_of_content == "signature_delta"
assert content_block_delta["type"] == "signature_delta"
assert content_block_delta["type"] == "signature_delta"
assert content_block_delta["signature"] == "sigsig"
@ -536,3 +537,256 @@ def test_translate_streaming_openai_chunk_to_anthropic_raises_when_thinking_and_
LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic(
choices=choices
)
def test_translate_anthropic_messages_to_openai_user_message_with_base64_image():
"""Test that base64 images in user messages are correctly translated to OpenAI format."""
anthropic_messages = [
AnthropicMessagesUserMessageParam(
role="user",
content=[
{"type": "text", "text": "What's in this image?"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
},
},
],
)
]
adapter = LiteLLMAnthropicMessagesAdapter()
result = adapter.translate_anthropic_messages_to_openai(messages=anthropic_messages)
assert len(result) == 1
assert result[0]["role"] == "user"
assert isinstance(result[0]["content"], list)
assert len(result[0]["content"]) == 2
# Check text content
assert result[0]["content"][0]["type"] == "text"
assert result[0]["content"][0]["text"] == "What's in this image?"
# Check image content
assert result[0]["content"][1]["type"] == "image_url"
assert "image_url" in result[0]["content"][1]
assert result[0]["content"][1]["image_url"]["url"].startswith(
"data:image/png;base64,"
)
assert (
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
in result[0]["content"][1]["image_url"]["url"]
)
def test_translate_anthropic_messages_to_openai_user_message_with_url_image():
"""Test that URL-based images in user messages are correctly translated to OpenAI format."""
anthropic_messages = [
AnthropicMessagesUserMessageParam(
role="user",
content=[
{"type": "text", "text": "Describe this forest path"},
{
"type": "image",
"source": {"type": "url", "url": "https://example.com/forest.jpg"},
},
],
)
]
adapter = LiteLLMAnthropicMessagesAdapter()
result = adapter.translate_anthropic_messages_to_openai(messages=anthropic_messages)
assert len(result) == 1
assert result[0]["role"] == "user"
assert isinstance(result[0]["content"], list)
assert len(result[0]["content"]) == 2
# Check text content
assert result[0]["content"][0]["type"] == "text"
assert result[0]["content"][0]["text"] == "Describe this forest path"
# Check image content
assert result[0]["content"][1]["type"] == "image_url"
assert "image_url" in result[0]["content"][1]
assert (
result[0]["content"][1]["image_url"]["url"] == "https://example.com/forest.jpg"
)
def test_translate_anthropic_messages_to_openai_tool_result_with_base64_image():
"""Test that base64 images in tool results are correctly translated to OpenAI format."""
anthropic_messages = [
AnthropicMessagesUserMessageParam(
role="user", content=[{"type": "text", "text": "Take a screenshot"}]
),
AnthopicMessagesAssistantMessageParam(
role="assistant",
content=[
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_screenshot",
"input": {"area": "desktop"},
}
],
),
AnthropicMessagesUserMessageParam(
role="user",
content=[
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/wA/==",
},
}
],
}
],
),
]
adapter = LiteLLMAnthropicMessagesAdapter()
result = adapter.translate_anthropic_messages_to_openai(messages=anthropic_messages)
# Find the tool message in the result
tool_message = None
for msg in result:
if isinstance(msg, dict) and msg.get("role") == "tool":
tool_message = msg
break
assert tool_message is not None, "Tool message not found in result"
# Tool messages in OpenAI format have string content (data URL), not list
assert isinstance(tool_message["content"], str)
assert tool_message["content"].startswith("data:image/jpeg;base64,")
assert "/9j/4AAQSkZJRgABAQAAAQABAAD" in tool_message["content"]
def test_translate_anthropic_messages_to_openai_tool_result_with_url_image():
"""Test that URL-based images in tool results are correctly translated to OpenAI format."""
anthropic_messages = [
AnthropicMessagesUserMessageParam(
role="user",
content=[{"type": "text", "text": "Take a screenshot of the forest"}],
),
AnthopicMessagesAssistantMessageParam(
role="assistant",
content=[
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_screenshot",
"input": {"area": "forest_path"},
}
],
),
AnthropicMessagesUserMessageParam(
role="user",
content=[
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": [
{
"type": "image",
"source": {
"type": "url",
"url": "https://i0.wp.com/picjumbo.com/wp-content/uploads/amazing-stone-path-in-forest-free-image.jpg",
},
}
],
}
],
),
]
adapter = LiteLLMAnthropicMessagesAdapter()
result = adapter.translate_anthropic_messages_to_openai(messages=anthropic_messages)
# Find the tool message in the result
tool_message = None
for msg in result:
if isinstance(msg, dict) and msg.get("role") == "tool":
tool_message = msg
break
assert tool_message is not None, "Tool message not found in result"
# Tool messages in OpenAI format have string content (URL), not list
assert isinstance(tool_message["content"], str)
assert (
tool_message["content"]
== "https://i0.wp.com/picjumbo.com/wp-content/uploads/amazing-stone-path-in-forest-free-image.jpg"
)
def test_translate_anthropic_messages_to_openai_mixed_content_with_image():
"""Test that messages with mixed text and image content are correctly translated."""
anthropic_messages = [
AnthropicMessagesUserMessageParam(
role="user",
content=[
{"type": "text", "text": "Here are two images:"},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
},
},
{"type": "text", "text": "and this one:"},
{
"type": "image",
"source": {"type": "url", "url": "https://example.com/image2.jpg"},
},
{"type": "text", "text": "What's the difference?"},
],
)
]
adapter = LiteLLMAnthropicMessagesAdapter()
result = adapter.translate_anthropic_messages_to_openai(messages=anthropic_messages)
assert len(result) == 1
assert result[0]["role"] == "user"
assert isinstance(result[0]["content"], list)
assert len(result[0]["content"]) == 5
# Check text content
assert result[0]["content"][0]["type"] == "text"
assert result[0]["content"][0]["text"] == "Here are two images:"
# Check first image (base64)
assert result[0]["content"][1]["type"] == "image_url"
assert result[0]["content"][1]["image_url"]["url"].startswith(
"data:image/png;base64,"
)
# Check middle text
assert result[0]["content"][2]["type"] == "text"
assert result[0]["content"][2]["text"] == "and this one:"
# Check second image (URL)
assert result[0]["content"][3]["type"] == "image_url"
assert (
result[0]["content"][3]["image_url"]["url"] == "https://example.com/image2.jpg"
)
# Check final text
assert result[0]["content"][4]["type"] == "text"
assert result[0]["content"][4]["text"] == "What's the difference?"