diff --git a/open-sse/translator/response/gemini-to-openai.js b/open-sse/translator/response/gemini-to-openai.js index 28e7d39a..8f04ffd3 100644 --- a/open-sse/translator/response/gemini-to-openai.js +++ b/open-sse/translator/response/gemini-to-openai.js @@ -25,7 +25,10 @@ function emitFunctionCall(functionCall, state) { type: OPENAI_BLOCK.FUNCTION, function: { name: fcName, arguments: JSON.stringify(fcArgs) }, }; - state.toolCalls.set(toolCallIndex, toolCall); + // Keep Gemini bookkeeping separate from the shared translator state.toolCalls map. + // The downstream OpenAI→Claude translator uses state.toolCalls for Claude block + // metadata; pre-populating it here makes Anthropic tool deltas lose index. + state.geminiToolCallCount = (state.geminiToolCallCount || 0) + 1; return buildChunk(chunkMeta(state), { tool_calls: [toolCall] }, null); } @@ -46,6 +49,7 @@ export function geminiToOpenAIResponse(chunk, state) { state.messageId = response.responseId || `msg_${Date.now()}`; state.model = response.modelVersion || "gemini"; state.functionIndex = 0; + state.geminiToolCallCount = 0; results.push(buildChunk(chunkMeta(state), { role: ROLE.ASSISTANT }, null)); } @@ -117,7 +121,7 @@ export function geminiToOpenAIResponse(chunk, state) { // Finish reason - include usage in final chunk if (candidate.finishReason) { let finishReason = toOpenAIFinish(candidate.finishReason, "gemini"); - if (finishReason === OPENAI_FINISH.STOP && state.toolCalls.size > 0) { + if (finishReason === OPENAI_FINISH.STOP && state.geminiToolCallCount > 0) { finishReason = OPENAI_FINISH.TOOL_CALLS; } diff --git a/tests/translator/bugs-antigravity.test.js b/tests/translator/bugs-antigravity.test.js index c20fcd2e..a594161e 100644 --- a/tests/translator/bugs-antigravity.test.js +++ b/tests/translator/bugs-antigravity.test.js @@ -1,7 +1,7 @@ // Real Antigravity-MITM requests (Gemini-internal: { request: { contents, ... } }) → OpenAI. import { describe, it, expect } from "vitest"; import "./registerAll.js"; -import { translateRequest } from "../../open-sse/translator/index.js"; +import { translateRequest, translateResponse, initState } from "../../open-sse/translator/index.js"; import { FORMATS } from "../../open-sse/translator/formats.js"; import { AntigravityExecutor } from "../../open-sse/executors/antigravity.js"; @@ -54,6 +54,32 @@ describe("Antigravity → OpenAI", () => { }); }); +describe("Antigravity → Claude", () => { + it("tool call input_json_delta includes Anthropic index", () => { + const state = initState(FORMATS.CLAUDE); + const events = translateResponse(FORMATS.ANTIGRAVITY, FORMATS.CLAUDE, { + response: { + responseId: "resp-1", + modelVersion: "gemini-pro-agent", + candidates: [{ + content: { + role: "model", + parts: [{ functionCall: { name: "bash", args: { command: "git status" } } }], + }, + finishReason: "STOP", + index: 0, + }], + }, + }, state); + + const jsonDelta = events.find( + (event) => event.type === "content_block_delta" && event.delta?.type === "input_json_delta" + ); + expect(jsonDelta).toMatchObject({ index: expect.any(Number) }); + expect(JSON.parse(jsonDelta.delta.partial_json)).toEqual({ command: "git status" }); + }); +}); + describe("Antigravity executor", () => { it("strips optional from nested tool schemas", () => { const out = new AntigravityExecutor().transformRequest("gemini-2.5-pro", { diff --git a/tests/translator/claude-kiro-direct.test.js b/tests/translator/claude-kiro-direct.test.js index 7e01951f..1b189ec6 100644 --- a/tests/translator/claude-kiro-direct.test.js +++ b/tests/translator/claude-kiro-direct.test.js @@ -171,6 +171,7 @@ describe("Kiro → Claude (direct route, OpenAI-shaped chunks from executor)", ( const jsonDelta = events.find( (e) => e.type === "content_block_delta" && e.delta.type === "input_json_delta" ); + expect(jsonDelta.index).toBeDefined(); expect(jsonDelta.delta.partial_json).toBe('{"q":"x"}'); const md = events.find((e) => e.type === "message_delta"); expect(md.delta.stop_reason).toBe("tool_use");