fix(models): add image support for deepseek-v4-flash-vision-exp

deepseek/deepseek-v4-flash-vision-exp is served by the Provider API but was
missing from the hardcoded MODEL_INPUT_MODALITIES allowlist, so pi rejected
any conversation containing an image block. Notably this also rejected images
returned by the read tool via toolResult:

  Error: Selected Command Code model does not support image content in tool results

Adding the allowlist entry lets modelSupportsImageInput() return true and the
converters forward images using the current Command Code wire format.

Coverage:
- regression test for modelSupportsImageInput("deepseek/deepseek-v4-flash-vision-exp")
- end-to-end stream test for a tool-result image forwarded as a following user
  image (the concrete read reproduction), not only a user-attached image
- end-to-end stream test asserting a text-only model still rejects tool-result
  images before any network access

Refs: https://github.com/patlux/pi-commandcode-provider/issues/54
This commit is contained in:
laijxa
2026-08-25 19:42:10 +08:00
parent dc515e85b6
commit d14c2f0feb
3 changed files with 83 additions and 1 deletions
+79
View File
@@ -173,6 +173,85 @@ describe("streamCommandCode — successful streams", () => {
)
})
it("forwards a tool-result image as a following user image for vision-capable models", async () => {
server.mockResponse({
type: "success",
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
})
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
const events = await collectEvents(
streamCommandCode(
makeModel({ id: "deepseek/deepseek-v4-flash-vision-exp" }),
makeContext({
messages: [
{ role: "user", content: "read the image" },
{
role: "assistant",
content: [{ type: "toolCall", id: "c1", name: "read", arguments: {} }],
},
{
role: "toolResult",
toolCallId: "c1",
toolName: "read",
content: [
{ type: "text", text: "image attached" },
{ type: "image", data: "aGVsbG8=", mimeType: "image/png" },
],
},
],
}),
{ apiKey: "mock-key" },
),
)
// No error: the tool-result image must not be rejected for this model.
assert.equal(events.at(-1)?.type, "done")
const body = server.lastRequestBody()
// The tool-result text is forwarded on the tool message at index 2.
assert.equal(
objectAt(body, ["params", "messages", "2", "content", "0", "output", "value"]),
"image attached",
)
// The tool-result image is forwarded as a following user image message at index 3.
assert.equal(objectAt(body, ["params", "messages", "3", "role"]), "user")
assert.equal(
objectAt(body, ["params", "messages", "3", "content", "0", "image"]),
"data:image/png;base64,aGVsbG8=",
)
})
it("rejects a tool-result image before network access for text-only models", async () => {
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
const events = await collectEvents(
streamCommandCode(
makeModel({ id: "deepseek/deepseek-v4-pro" }),
makeContext({
messages: [
{ role: "user", content: "read the image" },
{
role: "assistant",
content: [{ type: "toolCall", id: "c1", name: "read", arguments: {} }],
},
{
role: "toolResult",
toolCallId: "c1",
toolName: "read",
content: [{ type: "image", data: "aGVsbG8=", mimeType: "image/png" }],
},
],
}),
{ apiKey: "mock-key" },
),
)
assert.equal(events.at(-1)?.type, "error")
assert.match(events.at(-1)?.error.errorMessage ?? "", /does not support image content/i)
assert.equal(server.requestCount(), 0)
})
it("rejects images before network access for text-only models", async () => {
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })