diff --git a/src/mitm/handlers/claudeCode.ts b/src/mitm/handlers/claudeCode.ts index fe07dcb96c..5e693995de 100644 --- a/src/mitm/handlers/claudeCode.ts +++ b/src/mitm/handlers/claudeCode.ts @@ -17,7 +17,7 @@ export class ClaudeCodeHandler extends MitmHandlerBase { req: IncomingMessage, res: ServerResponse, body: Buffer, - mappedModel: string, + mappedModel: string ): Promise { const startedAt = this.now(); const intercepted = await this.hookBufferStart(req, body, mappedModel); @@ -26,6 +26,23 @@ export class ClaudeCodeHandler extends MitmHandlerBase { const payload = JSON.parse(body.toString()); payload.model = mappedModel; + // Strip trailing assistant prefill to prevent "This model does not support assistant + // message prefill" upstream error. Loop over ALL consecutive trailing assistant turns + // (not just one) — mirrors the pop-loop already used for Copilot + // (open-sse/executors/github.ts::dropTrailingAssistantPrefill) and Antigravity/Vertex + // Claude (open-sse/executors/antigravity.ts::stripTrailingAntigravityAssistantTurn). + // Guard: never strip messages down to empty — an empty array is itself an invalid + // request, so at least one entry (even a lone trailing assistant turn) is always + // preserved. + if (Array.isArray(payload.messages) && payload.messages.length > 0) { + while ( + payload.messages.length > 1 && + payload.messages[payload.messages.length - 1]?.role === "assistant" + ) { + payload.messages.pop(); + } + } + const upstreamStart = this.now(); const upstream = await this.fetchRouter(payload, "/v1/messages", req.headers); diff --git a/tests/unit/mitm-handler-claudeCode.test.ts b/tests/unit/mitm-handler-claudeCode.test.ts index e02fe8c99b..918e6726bb 100644 --- a/tests/unit/mitm-handler-claudeCode.test.ts +++ b/tests/unit/mitm-handler-claudeCode.test.ts @@ -15,3 +15,37 @@ test("claude-code handler — happy path forwards to /v1/messages", async () => const sent = JSON.parse(r.fetchBody); assert.equal(sent.model, "claude-opus-4.5"); }); + +test("claude-code handler — strips ALL consecutive trailing assistant turns, not just one", async () => { + const r = await runHandler( + new ClaudeCodeHandler(), + { + model: "claude-3.5-sonnet", + messages: [ + { role: "user", content: "hi" }, + { role: "assistant", content: "partial reply 1" }, + { role: "assistant", content: "partial reply 2" }, + ], + }, + "claude-opus-4.5" + ); + assert.ok(r.fetchCalled); + const sent = JSON.parse(r.fetchBody); + assert.equal(sent.messages.length, 1); + assert.equal(sent.messages[0].role, "user"); +}); + +test("claude-code handler — never collapses messages to empty when the ENTIRE history is trailing assistant turns", async () => { + const r = await runHandler( + new ClaudeCodeHandler(), + { + model: "claude-3.5-sonnet", + messages: [{ role: "assistant", content: "lone assistant turn" }], + }, + "claude-opus-4.5" + ); + assert.ok(r.fetchCalled); + const sent = JSON.parse(r.fetchBody); + assert.equal(sent.messages.length, 1); + assert.equal(sent.messages[0].content, "lone assistant turn"); +});