From d14c2f0febdb3bb76d0482a3c1ea2165133574c2 Mon Sep 17 00:00:00 2001 From: laijxa Date: Tue, 25 Aug 2026 19:42:10 +0800 Subject: [PATCH] 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 --- src/models.ts | 1 + tests/test-models.ts | 4 ++- tests/test-stream.ts | 79 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/models.ts b/src/models.ts index e102c00..706689d 100644 --- a/src/models.ts +++ b/src/models.ts @@ -43,6 +43,7 @@ export const MODEL_INPUT_MODALITIES: Readonly { it("matches command-code@1.32.1 image input capabilities", () => { assert.deepEqual(inputModalitiesForModel("gpt-5.6-luna"), ["text", "image"]) assert.deepEqual(inputModalitiesForModel("meta/muse-spark-1.2"), ["text", "image"]) + assert.deepEqual(inputModalitiesForModel("deepseek/deepseek-v4-flash-vision-exp"), ["text", "image"]) assert.deepEqual(inputModalitiesForModel("deepseek/deepseek-v4-pro"), ["text"]) assert.deepEqual(inputModalitiesForModel("unknown-new-model"), ["text"]) assert.equal(modelSupportsImageInput("gpt-5.6-luna"), true) + assert.equal(modelSupportsImageInput("deepseek/deepseek-v4-flash-vision-exp"), true) assert.equal(modelSupportsImageInput("deepseek/deepseek-v4-pro"), false) - assert.equal(Object.keys(MODEL_INPUT_MODALITIES).length, 37) + assert.equal(Object.keys(MODEL_INPUT_MODALITIES).length, 38) }) it("marks only known reasoning models as reasoning-capable", () => { diff --git a/tests/test-stream.ts b/tests/test-stream.ts index a429798..fa7626c 100644 --- a/tests/test-stream.ts +++ b/tests/test-stream.ts @@ -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() })