From d14c2f0febdb3bb76d0482a3c1ea2165133574c2 Mon Sep 17 00:00:00 2001 From: laijxa Date: Tue, 25 Aug 2026 19:42:10 +0800 Subject: [PATCH 1/2] 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() }) From f5e2fece8ca624c952ac2511666d3ea7af7fc6ef Mon Sep 17 00:00:00 2001 From: Patrick Wozniak Date: Tue, 25 Aug 2026 13:54:33 +0200 Subject: [PATCH 2/2] test(models): fix DeepSeek vision regressions --- tests/test-models.ts | 5 ++++- tests/test-stream.ts | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/test-models.ts b/tests/test-models.ts index 583ed5e..0bf34a3 100644 --- a/tests/test-models.ts +++ b/tests/test-models.ts @@ -103,7 +103,10 @@ describe("commandCodeModelsFromApiResponse()", () => { 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-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) diff --git a/tests/test-stream.ts b/tests/test-stream.ts index fa7626c..f831dc2 100644 --- a/tests/test-stream.ts +++ b/tests/test-stream.ts @@ -247,8 +247,10 @@ describe("streamCommandCode — successful streams", () => { ), ) - assert.equal(events.at(-1)?.type, "error") - assert.match(events.at(-1)?.error.errorMessage ?? "", /does not support image content/i) + const lastEvent = events.at(-1) + assert.equal(lastEvent?.type, "error") + if (lastEvent?.type !== "error") throw new Error("expected error event") + assert.match(lastEvent.error.errorMessage ?? "", /does not support image content/i) assert.equal(server.requestCount(), 0) })