From 6e28889aaddda1192c7b193f867fa112f2f51ef2 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:38:25 -0300 Subject: [PATCH] refactor(sse): dedup fallback tool_call id helper (#4736) Integrated into release/v3.8.36 --- open-sse/translator/helpers/toolCallHelper.ts | 6 ++++++ .../translator/response/kiro-to-openai.ts | 3 ++- .../translator/response/openai-responses.ts | 5 +++-- tests/unit/translator-helper-branches.test.ts | 20 +++++++++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/open-sse/translator/helpers/toolCallHelper.ts b/open-sse/translator/helpers/toolCallHelper.ts index e9177fa374..01f866fac7 100644 --- a/open-sse/translator/helpers/toolCallHelper.ts +++ b/open-sse/translator/helpers/toolCallHelper.ts @@ -2,6 +2,12 @@ const ALPHANUM9 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +// Fallback streaming tool_call id when a provider response omits one (index optional). +// `call_` when no index is given; `call__` when an index is supplied. +export function fallbackToolCallId(index?: number): string { + return index === undefined ? `call_${Date.now()}` : `call_${index}_${Date.now()}`; +} + // Generate unique tool call ID (default long form) export function generateToolCallId() { return `call_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`; diff --git a/open-sse/translator/response/kiro-to-openai.ts b/open-sse/translator/response/kiro-to-openai.ts index 5baaf4dc3c..57b70e626b 100644 --- a/open-sse/translator/response/kiro-to-openai.ts +++ b/open-sse/translator/response/kiro-to-openai.ts @@ -4,6 +4,7 @@ */ import { register } from "../registry.ts"; import { FORMATS } from "../formats.ts"; +import { fallbackToolCallId } from "../helpers/toolCallHelper.ts"; /** * Parse Kiro SSE event and convert to OpenAI format @@ -116,7 +117,7 @@ export function convertKiroToOpenAI(chunk, state) { // Handle tool use events if (eventType === "toolUseEvent" || data.toolUseEvent) { const toolUse = data.toolUseEvent || data; - const toolCallId = toolUse.toolUseId || `call_${Date.now()}`; + const toolCallId = toolUse.toolUseId || fallbackToolCallId(); const toolName = toolUse.name || ""; const toolInput = toolUse.input || {}; diff --git a/open-sse/translator/response/openai-responses.ts b/open-sse/translator/response/openai-responses.ts index cd3deb5b64..adc4680bb8 100644 --- a/open-sse/translator/response/openai-responses.ts +++ b/open-sse/translator/response/openai-responses.ts @@ -5,6 +5,7 @@ import { register } from "../registry.ts"; import { FORMATS } from "../formats.ts"; import { appendToolCallArgumentDelta } from "../../utils/toolCallArguments.ts"; +import { fallbackToolCallId } from "../helpers/toolCallHelper.ts"; function normalizeToolName(value) { return typeof value === "string" ? value.trim() : ""; @@ -615,7 +616,7 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) { // Function call started if (eventType === "response.output_item.added" && data.item?.type === "function_call") { const item = data.item; - state.currentToolCallId = item.call_id || `call_${Date.now()}`; + state.currentToolCallId = item.call_id || fallbackToolCallId(); state.currentToolCallArgsBuffer = ""; // reset per-call arg buffer state.currentToolCallDeferred = false; @@ -694,7 +695,7 @@ function openaiResponsesToOpenAIResponseStream(chunk, state) { const item = data.item; const buffered = state.currentToolCallArgsBuffer || ""; const currentIndex = state.toolCallIndex; // capture before increment - const callId = item.call_id || state.currentToolCallId || `call_${Date.now()}`; + const callId = item.call_id || state.currentToolCallId || fallbackToolCallId(); const toolName = normalizeToolName(item.name); if (state.currentToolCallDeferred) { diff --git a/tests/unit/translator-helper-branches.test.ts b/tests/unit/translator-helper-branches.test.ts index 630d7d3d8f..ef4afd8d1f 100644 --- a/tests/unit/translator-helper-branches.test.ts +++ b/tests/unit/translator-helper-branches.test.ts @@ -599,6 +599,26 @@ test("fixMissingToolResponses keeps OpenAI role:tool when assistant uses OpenAI assert.equal(fixed.messages[2].tool_call_id, "call_b"); }); +test("fallbackToolCallId returns the right id shape with and without an index", () => { + const noIndex = toolCallHelper.fallbackToolCallId(); + assert.match( + noIndex, + /^call_\d+$/, + "no-index form must be `call_` (matches kiro/openai-responses fallback shape)" + ); + + const withIndex = toolCallHelper.fallbackToolCallId(2); + assert.match( + withIndex, + /^call_2_\d+$/, + "index form must be `call__` (matches indexed fallback shape)" + ); + + // index 0 is falsy but defined — must still produce the indexed form, not the no-index form. + const zeroIndex = toolCallHelper.fallbackToolCallId(0); + assert.match(zeroIndex, /^call_0_\d+$/, "index 0 must use the indexed form, not the bare form"); +}); + test("translateRequest replays cached reasoning-only messages when interleaved field is reasoning_content", () => { clearReasoningCacheAll(); clearModelsDevCapabilities();