Skip to content

Commit 97ef2d1

Browse files
committed
feat(anthropic): add 5 translation improvements from reference
1. Session ID for Prompt Caching (High Priority) - Derive stable session ID from first user message hash - Enables prompt caching continuity across conversation turns - Falls back to random ID if no user message found 2. Content Reordering (Medium Priority) - Reorder assistant content blocks: thinking → text → tool_use - Matches Anthropic's expected ordering - Sanitizes thinking blocks by removing cache_control 3. Document/PDF Handling (Low Priority) - Support for 'document' type content blocks - Converts base64/URL documents to OpenAI image_url format - Default media type: application/pdf 4. Gemini Output Token Cap (Low Priority) - Add GEMINI_MAX_OUTPUT_TOKENS constant (16384) - Cap maxOutputTokens for non-Claude models - Prevents errors from exceeding Gemini limits 5. Schema Sanitization Improvements (Low Priority) - Add _score_schema_option() for smarter anyOf/oneOf selection - Add _merge_all_of() to properly merge allOf schemas - Add description hints when flattening union types - Select best option (objects > arrays > primitives > null)
1 parent b81ca57 commit 97ef2d1

File tree

2 files changed

+387
-13
lines changed

2 files changed

+387
-13
lines changed

src/rotator_library/anthropic_compat/translator.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,57 @@
1515
MIN_THINKING_SIGNATURE_LENGTH = 100
1616

1717

18+
def _reorder_assistant_content(content: List[dict]) -> List[dict]:
19+
"""
20+
Reorder assistant message content blocks to ensure correct order:
21+
1. Thinking blocks come first (required when thinking is enabled)
22+
2. Text blocks come in the middle (filtering out empty ones)
23+
3. Tool_use blocks come at the end (required before tool_result)
24+
25+
This matches Anthropic's expected ordering and prevents API errors.
26+
"""
27+
if not isinstance(content, list) or len(content) <= 1:
28+
return content
29+
30+
thinking_blocks = []
31+
text_blocks = []
32+
tool_use_blocks = []
33+
other_blocks = []
34+
35+
for block in content:
36+
if not isinstance(block, dict):
37+
other_blocks.append(block)
38+
continue
39+
40+
block_type = block.get("type", "")
41+
42+
if block_type in ("thinking", "redacted_thinking"):
43+
# Sanitize thinking blocks - remove cache_control and other extra fields
44+
sanitized = {
45+
"type": block_type,
46+
"thinking": block.get("thinking", ""),
47+
}
48+
if block.get("signature"):
49+
sanitized["signature"] = block["signature"]
50+
thinking_blocks.append(sanitized)
51+
52+
elif block_type == "tool_use":
53+
tool_use_blocks.append(block)
54+
55+
elif block_type == "text":
56+
# Only keep text blocks with meaningful content
57+
text = block.get("text", "")
58+
if text and text.strip():
59+
text_blocks.append(block)
60+
61+
else:
62+
# Other block types (images, documents, etc.) go in the text position
63+
other_blocks.append(block)
64+
65+
# Reorder: thinking → other → text → tool_use
66+
return thinking_blocks + other_blocks + text_blocks + tool_use_blocks
67+
68+
1869
def anthropic_to_openai_messages(
1970
anthropic_messages: List[dict], system: Optional[Union[str, List[dict]]] = None
2071
) -> List[dict]:
@@ -55,6 +106,11 @@ def anthropic_to_openai_messages(
55106
if isinstance(content, str):
56107
openai_messages.append({"role": role, "content": content})
57108
elif isinstance(content, list):
109+
# Reorder assistant content blocks to ensure correct order:
110+
# thinking → text → tool_use
111+
if role == "assistant":
112+
content = _reorder_assistant_content(content)
113+
58114
# Handle content blocks
59115
openai_content = []
60116
tool_calls = []
@@ -88,6 +144,26 @@ def anthropic_to_openai_messages(
88144
"image_url": {"url": source.get("url", "")},
89145
}
90146
)
147+
elif block_type == "document":
148+
# Convert Anthropic document format (e.g. PDF) to OpenAI
149+
# Documents are treated similarly to images with appropriate mime type
150+
source = block.get("source", {})
151+
if source.get("type") == "base64":
152+
openai_content.append(
153+
{
154+
"type": "image_url",
155+
"image_url": {
156+
"url": f"data:{source.get('media_type', 'application/pdf')};base64,{source.get('data', '')}"
157+
},
158+
}
159+
)
160+
elif source.get("type") == "url":
161+
openai_content.append(
162+
{
163+
"type": "image_url",
164+
"image_url": {"url": source.get("url", "")},
165+
}
166+
)
91167
elif block_type == "thinking":
92168
signature = block.get("signature", "")
93169
if signature and len(signature) >= MIN_THINKING_SIGNATURE_LENGTH:

0 commit comments

Comments
 (0)