-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: sending OpenAI-style image_url causes Anthropic 400 invalid tag error #4444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Soulter
merged 1 commit into
AstrBotDevs:master
from
KBVsent:fix/anthropic-image-payload-format
Jan 14, 2026
Merged
fix: sending OpenAI-style image_url causes Anthropic 400 invalid tag error #4444
Soulter
merged 1 commit into
AstrBotDevs:master
from
KBVsent:fix/anthropic-image-payload-format
Jan 14, 2026
+44
−0
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Contributor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey - 我发现了两个问题,并给出了一些整体性的反馈:
- 当一个
data:URL 解析失败或处于不支持的格式时,对应的 image part 会在仅记录 warning 的情况下被静默丢弃;建议要么保留原始的image_urlpart,要么增加一个清晰的回退逻辑,以避免消息语义被无意间改变。 - 针对
user消息的处理现在有了自己的循环和内容转换逻辑;如果在其他地方也有对消息 part 做归一化处理的逻辑,建议抽取一个共享的 helper,避免对类似内容结构产生行为差异。
给 AI Agents 的提示词
Please address the comments from this code review:
## Overall Comments
- When a `data:` URL fails to parse or is in an unsupported format, the corresponding image part is silently dropped while only logging a warning; consider either preserving the original `image_url` part or adding a clear fallback so message semantics are not unintentionally changed.
- The `user`-message handling now has its own loop and content transformation logic; if there are other places that normalize message parts, consider extracting a shared helper to avoid diverging behavior for similar content structures.
## Individual Comments
### Comment 1
<location> `astrbot/core/provider/sources/anthropic_source.py:139-143` </location>
<code_context>
+ image_url_data = part.get("image_url", {})
+ url = image_url_data.get("url", "")
+ if url.startswith("data:"):
+ try:
+ _, base64_data = url.split(",", 1)
+ # Detect actual image format from binary data
+ image_bytes = base64.b64decode(base64_data)
+ media_type = self._detect_image_mime_type(
+ image_bytes
+ )
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Exception handling around base64 decoding is too narrow and may miss decoding errors.
The current `try`/`except ValueError` also wraps `base64.b64decode`, which raises `binascii.Error` for invalid input, so malformed data will currently escape this handler. Please either catch `binascii.Error` explicitly (in addition to `ValueError`) or broaden the except clause (e.g., to `Exception`) and log the error so bad image data doesn’t cause `_prepare_payload` to fail.
Suggested implementation:
```python
import base64
import binascii
```
```python
except (ValueError, binascii.Error):
```
</issue_to_address>
### Comment 2
<location> `astrbot/core/provider/sources/anthropic_source.py:130` </location>
<code_context>
],
},
)
+ elif message["role"] == "user":
+ if isinstance(message.get("content"), list):
+ converted_content = []
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the user message image-conversion logic into dedicated helper methods so `_prepare_payload` stays focused on simple message shaping and control flow.
You can reduce the added complexity by extracting the user-content/image conversion into small helpers and keeping `_prepare_payload` focused on message shaping.
For example, instead of inlining all of this logic:
```python
elif message["role"] == "user":
if isinstance(message.get("content"), list):
converted_content = []
for part in message["content"]:
if part.get("type") == "image_url":
# Convert OpenAI image_url format to Anthropic image format
image_url_data = part.get("image_url", {})
url = image_url_data.get("url", "")
if url.startswith("data:"):
try:
_, base64_data = url.split(",", 1)
# Detect actual image format from binary data
image_bytes = base64.b64decode(base64_data)
media_type = self._detect_image_mime_type(image_bytes)
converted_content.append(
{
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": base64_data,
},
}
)
except ValueError:
logger.warning(
f"Failed to parse image data URI: {url[:50]}..."
)
else:
logger.warning(
f"Unsupported image URL format for Anthropic: {url[:50]}..."
)
else:
converted_content.append(part)
new_messages.append(
{
"role": "user",
"content": converted_content,
}
)
else:
new_messages.append(message)
```
you can move the conversion into helpers and keep the control flow flatter:
```python
def _convert_user_content(self, content):
if not isinstance(content, list):
return content
converted = []
for part in content:
if part.get("type") == "image_url":
converted_part = self._convert_image_part(part)
if converted_part is not None:
converted.append(converted_part)
# if conversion failed, we just drop the part or could append original,
# depending on current behavior you want to preserve
else:
converted.append(part)
return converted
def _convert_image_part(self, part):
image_url_data = part.get("image_url", {})
url = image_url_data.get("url", "")
if not url.startswith("data:"):
logger.warning(
f"Unsupported image URL format for Anthropic: {url[:50]}..."
)
return None
try:
_, base64_data = url.split(",", 1)
image_bytes = base64.b64decode(base64_data)
media_type = self._detect_image_mime_type(image_bytes)
return {
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": base64_data,
},
}
except ValueError:
logger.warning(
f"Failed to parse image data URI: {url[:50]}..."
)
return None
```
Then `_prepare_payload` only needs:
```python
elif message["role"] == "user":
content = self._convert_user_content(message.get("content"))
new_messages.append(
{
"role": "user",
"content": content,
}
if isinstance(content, list)
else {**message, "content": content}
)
```
Benefits:
- Flattens nesting in `_prepare_payload` to a single `elif role == 'user'` branch.
- Localizes base64/MIME/logging details in `_convert_image_part`, making it easier to test and reason about.
- Separates “how to convert content” from “how to append the message,” which makes control flow clearer and avoids duplicated `new_messages.append` shapes.
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进之后的评审。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- When a
data:URL fails to parse or is in an unsupported format, the corresponding image part is silently dropped while only logging a warning; consider either preserving the originalimage_urlpart or adding a clear fallback so message semantics are not unintentionally changed. - The
user-message handling now has its own loop and content transformation logic; if there are other places that normalize message parts, consider extracting a shared helper to avoid diverging behavior for similar content structures.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- When a `data:` URL fails to parse or is in an unsupported format, the corresponding image part is silently dropped while only logging a warning; consider either preserving the original `image_url` part or adding a clear fallback so message semantics are not unintentionally changed.
- The `user`-message handling now has its own loop and content transformation logic; if there are other places that normalize message parts, consider extracting a shared helper to avoid diverging behavior for similar content structures.
## Individual Comments
### Comment 1
<location> `astrbot/core/provider/sources/anthropic_source.py:139-143` </location>
<code_context>
+ image_url_data = part.get("image_url", {})
+ url = image_url_data.get("url", "")
+ if url.startswith("data:"):
+ try:
+ _, base64_data = url.split(",", 1)
+ # Detect actual image format from binary data
+ image_bytes = base64.b64decode(base64_data)
+ media_type = self._detect_image_mime_type(
+ image_bytes
+ )
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Exception handling around base64 decoding is too narrow and may miss decoding errors.
The current `try`/`except ValueError` also wraps `base64.b64decode`, which raises `binascii.Error` for invalid input, so malformed data will currently escape this handler. Please either catch `binascii.Error` explicitly (in addition to `ValueError`) or broaden the except clause (e.g., to `Exception`) and log the error so bad image data doesn’t cause `_prepare_payload` to fail.
Suggested implementation:
```python
import base64
import binascii
```
```python
except (ValueError, binascii.Error):
```
</issue_to_address>
### Comment 2
<location> `astrbot/core/provider/sources/anthropic_source.py:130` </location>
<code_context>
],
},
)
+ elif message["role"] == "user":
+ if isinstance(message.get("content"), list):
+ converted_content = []
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the user message image-conversion logic into dedicated helper methods so `_prepare_payload` stays focused on simple message shaping and control flow.
You can reduce the added complexity by extracting the user-content/image conversion into small helpers and keeping `_prepare_payload` focused on message shaping.
For example, instead of inlining all of this logic:
```python
elif message["role"] == "user":
if isinstance(message.get("content"), list):
converted_content = []
for part in message["content"]:
if part.get("type") == "image_url":
# Convert OpenAI image_url format to Anthropic image format
image_url_data = part.get("image_url", {})
url = image_url_data.get("url", "")
if url.startswith("data:"):
try:
_, base64_data = url.split(",", 1)
# Detect actual image format from binary data
image_bytes = base64.b64decode(base64_data)
media_type = self._detect_image_mime_type(image_bytes)
converted_content.append(
{
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": base64_data,
},
}
)
except ValueError:
logger.warning(
f"Failed to parse image data URI: {url[:50]}..."
)
else:
logger.warning(
f"Unsupported image URL format for Anthropic: {url[:50]}..."
)
else:
converted_content.append(part)
new_messages.append(
{
"role": "user",
"content": converted_content,
}
)
else:
new_messages.append(message)
```
you can move the conversion into helpers and keep the control flow flatter:
```python
def _convert_user_content(self, content):
if not isinstance(content, list):
return content
converted = []
for part in content:
if part.get("type") == "image_url":
converted_part = self._convert_image_part(part)
if converted_part is not None:
converted.append(converted_part)
# if conversion failed, we just drop the part or could append original,
# depending on current behavior you want to preserve
else:
converted.append(part)
return converted
def _convert_image_part(self, part):
image_url_data = part.get("image_url", {})
url = image_url_data.get("url", "")
if not url.startswith("data:"):
logger.warning(
f"Unsupported image URL format for Anthropic: {url[:50]}..."
)
return None
try:
_, base64_data = url.split(",", 1)
image_bytes = base64.b64decode(base64_data)
media_type = self._detect_image_mime_type(image_bytes)
return {
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": base64_data,
},
}
except ValueError:
logger.warning(
f"Failed to parse image data URI: {url[:50]}..."
)
return None
```
Then `_prepare_payload` only needs:
```python
elif message["role"] == "user":
content = self._convert_user_content(message.get("content"))
new_messages.append(
{
"role": "user",
"content": content,
}
if isinstance(content, list)
else {**message, "content": content}
)
```
Benefits:
- Flattens nesting in `_prepare_payload` to a single `elif role == 'user'` branch.
- Localizes base64/MIME/logging details in `_convert_image_part`, making it easier to test and reason about.
- Separates “how to convert content” from “how to append the message,” which makes control flow clearer and avoids duplicated `new_messages.append` shapes.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Soulter
approved these changes
Jan 14, 2026
Soulter
pushed a commit
that referenced
this pull request
Jan 14, 2026
Soulter
added a commit
that referenced
this pull request
Jan 15, 2026
* stage * fix: update tool call logging to include tool call IDs and enhance sandbox ship creation parameters * feat: file upload * fix * update * fix: remove 'boxlite' option from booter and handle error in PythonTool execution * feat: implement singleton pattern for ShipyardSandboxClient and add FileUploadTool for file uploads * feat: sandbox * fix * beta * uv lock * remove * chore: makes world better * feat: implement localStorage persistence for showReservedPlugins state * docs: refine EULA * fix * feat: add availability check for sandbox in Shipyard and base booters * feat: add shipyard session configuration options and update related tools * feat: add file download functionality and update shipyard SDK version * fix: sending OpenAI-style image_url causes Anthropic 400 invalid tag error (#4444) * feat: chatui project (#4477) * feat: chatui-project * fix: remove console log from getProjects function * fix: title saving logic and update project sessions on changes * docs: standardize Context class documentation formatting (#4436) * docs: standardize Context class documentation formatting - Unified all method docstrings to standard format - Fixed mixed language and formatting issues - Added complete parameter and return descriptions - Enhanced developer experience for plugin creators - Fixes #4429 * docs: fix Context class documentation issues per review - Restored Sphinx directives for versionadded notes - Fixed MessageSesion typo to MessageSession throughout file - Added clarification for kwargs propagation in tool_loop_agent - Unified deprecation marker format - Fixes #4429 * Convert developer API comments to English * chore: revise comments --------- Co-authored-by: Soulter <[email protected]> * fix: handle empty output case in PythonTool execution * fix: update description for command parameter in ExecuteShellTool * refactor: remove unused file tools and update PythonTool output handling * project list * fix: ensure message stream order (#4487) * feat: enhance iPython tool rendering with Shiki syntax highlighting * bugfixes * feat: add sandbox mode prompt for enhanced user guidance in executing commands * chore: remove skills prompt --------- Co-authored-by: 時壹 <[email protected]> Co-authored-by: Li-shi-ling <[email protected]>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
area:provider
The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner.
lgtm
This PR has been approved by a maintainer
size:M
This PR changes 30-99 lines, ignoring generated files.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
When using Anthropic as the LLM provider, processing quoted images in messages fails with error:
Historical context messages from entities.py use OpenAI format (
type: "image_url") which Anthropic API doesn't recognizeModifications / 改动点
Modified: anthropic_source.py
_prepare_payload()method to handle user messages containingimage_urltype contentimage_urlformat to Anthropicimageformat with proper structure_detect_image_mime_type()method to detect actual MIME type from magic bytesScreenshots or Test Results / 运行截图或测试结果
Checklist / 检查清单
requirements.txt和pyproject.toml文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations inrequirements.txtandpyproject.toml.Summary by Sourcery
在为 Anthropic 准备请求负载时,处理包含 OpenAI 风格
image_url内容的用户消息,以避免出现无效标签错误。Bug Fixes:
image_url内容转换为与 Anthropic 兼容的图像内容,防止在引用图像时出现400 invalid_request_error响应。Enhancements:
Original summary in English
Summary by Sourcery
Handle user messages with OpenAI-style image_url content when preparing Anthropic payloads to avoid invalid tag errors.
Bug Fixes:
Enhancements: