From 9401bc9709d441a51ecd8eeadc0deea90bfb041e Mon Sep 17 00:00:00 2001 From: Sicheng Yu Date: Wed, 24 Jun 2026 21:41:15 +0800 Subject: [PATCH] fix(gateway): tolerate malformed SSE chunks and fix Anthropic tool block indices - gateway.ts: wrap SSE JSON.parse in try/catch so a single malformed chunk from the upstream no longer aborts the entire stream (matches mimoAdapter and anthropicAdapter behavior). - anthropicAdapter.ts: assign sequential content_block indices when emitting multiple tool_use blocks in the streaming response. Previously every tool call reused the same index because nextBlockIndex was never incremented, which caused the Claude SDK to drop all but the first tool call. --- apps/server/src/anthropicAdapter.ts | 10 ++++++---- apps/server/src/gateway.ts | 7 ++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/server/src/anthropicAdapter.ts b/apps/server/src/anthropicAdapter.ts index 3d2592d..92cb39c 100644 --- a/apps/server/src/anthropicAdapter.ts +++ b/apps/server/src/anthropicAdapter.ts @@ -381,7 +381,6 @@ export function openAIChatStreamToAnthropicStream( const reader = upstream.getReader(); let buffer = ""; - let nextBlockIndex = 0; let textBlockOpen = false; let textBlockStarted = false; let finalStopReason = "end_turn"; @@ -480,12 +479,16 @@ export function openAIChatStreamToAnthropicStream( } } - // Flush: close text block, then emit tool_use blocks. + // Flush: close text block, then emit tool_use blocks. Tool blocks + // follow the text block sequentially so their indices are contiguous + // (text=0, tool1=1, tool2=2, …) — Claude Code uses `index` to match + // content_block_start/delta/stop events, so collisions break tool calls. closeTextBlock(); + let toolBlockIndex = textBlockStarted ? 1 : 0; for (const [idx, tc] of [...toolCallsByIndex.entries()].sort((a, b) => a[0] - b[0])) { if (emittedToolStarts.has(idx)) continue; emittedToolStarts.add(idx); - const blockIndex = textBlockStarted ? nextBlockIndex + 1 : nextBlockIndex; + const blockIndex = toolBlockIndex++; // Anthropic streaming tool_use protocol: // 1. content_block_start with EMPTY input ({}) — Claude Code expects // input to arrive via input_json_delta, not in the start event. @@ -505,7 +508,6 @@ export function openAIChatStreamToAnthropicStream( type: "content_block_stop", index: blockIndex, }); - nextBlockIndex = blockIndex; } enqueue("message_delta", { diff --git a/apps/server/src/gateway.ts b/apps/server/src/gateway.ts index b66adbb..72814ce 100644 --- a/apps/server/src/gateway.ts +++ b/apps/server/src/gateway.ts @@ -425,7 +425,12 @@ export function chatStreamToResponsesStream( .map((line) => line.slice(5).trim()) .join("\n"); if (!data || data === "[DONE]") continue; - const chunk = JSON.parse(data) as unknown; + let chunk: unknown; + try { + chunk = JSON.parse(data); + } catch { + continue; + } if (!isRecord(chunk) || !Array.isArray(chunk.choices)) continue; for (const choice of chunk.choices) { if (!isRecord(choice) || !isRecord(choice.delta)) continue;