104 lines
4.1 KiB
TypeScript
104 lines
4.1 KiB
TypeScript
// Regression: open-sse/translator/request/gemini-to-openai.ts — a Gemini `content`
|
|
// whose `parts` array holds a `functionResponse` co-located with other parts
|
|
// (another functionCall, or trailing text) used to be dropped past the first
|
|
// functionResponse, because convertGeminiContent() early-returns the tool message
|
|
// on the first `functionResponse` part it finds. Gemini clients legitimately send
|
|
// functionResponse alongside functionCall (multi-tool turns) or text (a user
|
|
// follow-up next to a tool result). The fix pre-splits such contents so every
|
|
// co-located part is preserved (tool results emitted first to keep ordering).
|
|
//
|
|
// Tested against the exported geminiToOpenAIRequest — the function registered as
|
|
// the GEMINI→OPENAI request translator (translateRequest routes through it after
|
|
// its normalize pipeline, which strips *orphaned* tool results; a real multi-turn
|
|
// conversation carries the matching functionCall so the co-located parts reach this
|
|
// function intact). This mirrors OmniRoute's translator-gemini-to-openai.test.ts.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { geminiToOpenAIRequest } = await import(
|
|
"../../open-sse/translator/request/gemini-to-openai.ts"
|
|
);
|
|
|
|
test("preserves a functionCall co-located with a functionResponse in the same content", () => {
|
|
const body = {
|
|
contents: [
|
|
{
|
|
role: "model",
|
|
parts: [
|
|
{ functionCall: { id: "call_a", name: "tool_a", args: {} } },
|
|
{ functionResponse: { id: "call_b", name: "tool_b", response: { result: "b done" } } },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
const result = geminiToOpenAIRequest("gemini-pro", body, false);
|
|
const toolMsg = result.messages.find((m) => m.role === "tool");
|
|
const assistantMsg = result.messages.find((m) => m.role === "assistant" && m.tool_calls);
|
|
assert.ok(toolMsg, "tool result must be preserved");
|
|
assert.ok(assistantMsg, "co-located functionCall must be preserved");
|
|
assert.equal(assistantMsg.tool_calls[0].function.name, "tool_a");
|
|
});
|
|
|
|
test("preserves multiple functionResponses in the same content", () => {
|
|
const body = {
|
|
contents: [
|
|
{
|
|
role: "user",
|
|
parts: [
|
|
{ functionResponse: { id: "call_a", name: "tool_a", response: { result: "a done" } } },
|
|
{ functionResponse: { id: "call_b", name: "tool_b", response: { result: "b done" } } },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
const result = geminiToOpenAIRequest("gemini-pro", body, false);
|
|
const toolMsgs = result.messages.filter((m) => m.role === "tool");
|
|
assert.equal(toolMsgs.length, 2);
|
|
assert.deepEqual(
|
|
toolMsgs.map((m) => m.tool_call_id).sort(),
|
|
["call_a", "call_b"]
|
|
);
|
|
});
|
|
|
|
test("preserves text co-located with a functionResponse, keeping the original turn role", () => {
|
|
const body = {
|
|
contents: [
|
|
{
|
|
role: "user",
|
|
parts: [
|
|
{ functionResponse: { id: "call_a", name: "tool_a", response: { result: "a done" } } },
|
|
{ text: "also please summarize" },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
const result = geminiToOpenAIRequest("gemini-pro", body, false);
|
|
const toolMsg = result.messages.find((m) => m.role === "tool");
|
|
const userMsg = result.messages.find(
|
|
(m) => m.role === "user" && m.content === "also please summarize"
|
|
);
|
|
const asstWithText = result.messages.find(
|
|
(m) => m.role === "assistant" && m.content === "also please summarize"
|
|
);
|
|
assert.ok(toolMsg, "tool result must be preserved");
|
|
assert.ok(userMsg, "co-located text must be preserved with role:user");
|
|
assert.ok(!asstWithText, "co-located user text must NOT be attributed to assistant");
|
|
});
|
|
|
|
test("still works for a functionResponse alone (no regression)", () => {
|
|
const body = {
|
|
contents: [
|
|
{
|
|
role: "user",
|
|
parts: [
|
|
{ functionResponse: { id: "call_a", name: "tool_a", response: { result: "a done" } } },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
const result = geminiToOpenAIRequest("gemini-pro", body, false);
|
|
const toolMsgs = result.messages.filter((m) => m.role === "tool");
|
|
assert.equal(toolMsgs.length, 1);
|
|
assert.equal(toolMsgs[0].tool_call_id, "call_a");
|
|
});
|