While testing image input support in an Anna App, I found that sending an image using the MCP image block structure described in the llm.complete documentation resulted in a successful response, but the model said it could not see the attachment. In the same test application, image inputs to Agent Session using uploaded image URLs have been verified to work correctly.
I would appreciate the team’s help in confirming the image input protocol for llm.complete and whether the current implementation matches the documentation.
1. Official Documentation and Areas That Need Clarification
Relevant documentation:
llm.complete
The LLM Host API documentation describes the request as MCP-shaped and states that messages[].content can be a string or an MCP-shaped content block:
contentis a string OR an MCP-shaped block ({type:'text', text}/{type:'image', ...}); blocks are flattened into a LangChainHumanMessage/AIMessage/SystemMessageby_assemble_lc_messages.
The documentation also states:
Image content blocks require the selected model to be multimodal; otherwise the provider rejects upstream and the call surfaces as
APP_PROVIDER_ERROR.
Based on this, I understand that llm.complete supports image inputs. However, the documentation uses {type:'image', ...} without specifying the image block’s fields and does not provide a complete image request example. This leaves the following details unclear:
- Should images use the MCP
dataandmimeTypefields? - Must
datacontain raw base64, or is a full data URI also accepted? - Are image URLs supported? If so, what structure should be used?
- Can text and images be placed in separate, consecutive
usermessages?
Based on the documentation’s description of MCP, the current implementation uses { type: "image", data: "<base64>", mimeType: "image/png" }. This follows the MCP image block specification, but the Anna documentation itself does not show this request in full.
Agent Session
In comparison, the “Native image inputs (attachments)” section of the Agent Session documentation provides an explicit example:
const stream = sess.run({
content: "Check these slides for text overflow or layout issues.",
attachments: [
{ type: "image/png", url: "https://example.com/slide-1.png" },
{
type: "image/png",
url: "https://example.com/slide-2.png",
detail: "high"
},
{
type: "image/jpeg",
data: "<base64 or data: URI>",
filename: "photo.jpg"
}
]
});
This documentation explicitly describes both URL and base64 inputs and lists attachment limits and usage considerations, giving developers enough information to implement and verify the feature directly.
2. The Actual llm.complete Request
The test application calls llm.complete through Anna Runtime. The image is read in the browser using FileReader.readAsDataURL(). After removing the data URI prefix, the raw base64 is placed in an MCP image block.
The request structure is shown below, with the image data omitted. The Chinese prompt is translated into English here:
await runtime.call("llm", "complete", {
messages: [
{
role: "user",
content: {
type: "text",
text: "Can you see the contents of the image I attached?"
}
},
{
role: "user",
content: {
type: "image",
data: "<full image base64 omitted here>",
mimeType: "image/png"
}
}
]
});
This request does not use the Session attachments field or upload the image beforehand to obtain a URL. Instead, it sends the image as an MCP image block, based on the LLM documentation.
The RPC logs confirm that the request contained the full image data. Validation of the image data in the logs produced the following results:
| Item | Result |
|---|---|
| MIME type | image/png |
| Image dimensions | 1280 × 720 |
| Decoded size | 1,867,166 bytes |
| Base64 character count | 2,489,556 |
| Strict base64 decoding | Passed |
| PNG chunk checksum and compressed data checks | Passed |
Therefore, at least at the point where this RPC request was logged, the image data was complete and could be decoded correctly.
3. Actual Response: The Request Succeeded, but the Model Said It Could Not See the Image
The RPC returned res ok llm.complete, with no APP_PROVIDER_ERROR or parameter error.
The key response fields are shown below. Only the opening of content.text is included, translated from Chinese into English:
{
"role": "assistant",
"content": {
"type": "text",
"text": "At the moment, I **cannot directly see** the image attachment you uploaded."
},
"model": "google/gemini-3-flash-preview",
"stopReason": "endTurn",
"usage": {
"inputTokens": 9,
"outputTokens": 151,
"totalTokens": 160
},
"_meta": {
"provider": "openrouter",
"latencyMs": 2984,
"quotaConsumed": 0.3948275862068965,
"appSessionUuid": "aps_xjusbrpjg5biqhggfprkdm"
}
}
The rest of the response suggested resending the image using the upload button in the chat interface and checking for delays or format issues. It did not describe the image’s contents.
Two aspects of this response stand out:
- The model explicitly stated that it could not directly see the uploaded attachment.
usage.inputTokenswas only 9, which appears inconsistent with a request containing the full image and suggests that only the text portion may have been processed.
The model’s natural-language response and token counts alone cannot establish where the image was lost. However, together with the complete outbound RPC request, the observed behavior suggests that the image input was accepted but did not actually participate in model inference.
4. Comparison Test: URL-Based Image Input Works in Agent Session
In the Session feature of the same test application, I used a different image input path:
- Upload the image and obtain its URL.
- Call
sess.run(), passing the URL throughattachments. - Check whether the model can recognize the image’s contents.
The corresponding request structure is shown below, with the Chinese prompt translated into English:
const stream = sess.run({
content: "Please describe the contents of this image.",
attachments: [
{
type: "image/png",
url: "<image URL obtained after upload>"
}
]
});
This Session path has been verified to work: the model can read and recognize the image’s contents correctly.
The test results for the two paths are compared below:
| Item | llm.complete | Agent Session |
|---|---|---|
| Image input location | messages[].content |
attachments[] |
| Data format used in this test | Raw base64 in an MCP image block | Uploaded image URL |
| Complete image example in the documentation | No; only {type:'image', ...} is shown |
Yes; examples for both URL and base64 inputs |
| Observed result | Request succeeded, but the model said it could not see the image | Image was recognized correctly |
This comparison shows that the Session URL-based image input path works. It does not establish that the two paths use the same internal message conversion and model routing, so the llm.complete implementation still needs to be investigated separately.
5. Expected Behavior
If the MCP image block structure above is supported:
- The image should be converted into multimodal input that the upstream model can process.
- The model should be able to answer based on the image.
If this structure is not yet supported, or if there are restrictions on input formats, model capabilities, or other factors:
- A clear parameter or capability error should be returned.
- The call should not return successfully as an ordinary text completion when the image has not participated in inference.
6. Questions for the Team
-
Does
llm.completecurrently support the following complete image block structure?{ type: "image", data: "<raw base64>", mimeType: "image/png" } -
Is placing text and images in separate, consecutive
usermessages a supported usage pattern? -
If the usage above is correct, could you inspect the server-side message conversion (
_assemble_lc_messages, as mentioned in the documentation) and subsequent provider adaptation to confirm that the image content is passed to the upstream model in full? -
Could you add a directly runnable image input example for
llm.complete, similar to the Agent Session documentation, and clarify support and limitations for base64, data URIs, and URLs?
At this point, I suspect either an implementation issue in the llm.complete image input path or a discrepancy between the documented and actual support. I would like to confirm the correct protocol first, then determine whether the application-side implementation needs to be adjusted.