Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions open-sse/translator/helpers/toolCallHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

const ALPHANUM9 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

// Fallback streaming tool_call id when a provider response omits one (index optional).
// `call_<ts>` when no index is given; `call_<index>_<ts>` 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)}`;
Expand Down
3 changes: 2 additions & 1 deletion open-sse/translator/response/kiro-to-openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 || {};

Expand Down
5 changes: 3 additions & 2 deletions open-sse/translator/response/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() : "";
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/translator-helper-branches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_<ts>` (matches kiro/openai-responses fallback shape)"
);

const withIndex = toolCallHelper.fallbackToolCallId(2);
assert.match(
withIndex,
/^call_2_\d+$/,
"index form must be `call_<i>_<ts>` (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();
Expand Down
Loading