mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
#2504: thread _signatureNamespace through the FORMATS.GEMINI and FORMATS.GEMINI_CLI request translators so a cached Gemini thoughtSignature is re-attached to the functionCall on the follow-up turn (was 400 'missing thought_signature'). #2515: accept input_file (Responses API) on the Gemini path and document (Gemini-style) on the Responses/Codex path so PDFs reach the model regardless of content-part name.
This commit is contained in:
@@ -128,16 +128,14 @@ export function convertOpenAIContentToParts(content: unknown): JsonRecord[] {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Handle raw data strings (e.g. {"type": "file", "data": "JVBER...", "mime_type": "..."})
|
||||
// 3. Handle raw data strings (e.g. {"type": "file", "data": "JVBER...", "mime_type": "..."}).
|
||||
// Also accept the Responses-API shape {"type":"input_file","file_data":"JVBER...","filename":...}
|
||||
// so PDFs sent as `input_file` reach Gemini instead of being silently dropped (#2515).
|
||||
const file = toRecord(rec.file);
|
||||
const doc = toRecord(rec.document);
|
||||
const rawDataStr = rec.data || file?.data || doc?.data;
|
||||
const rawDataStr = rec.data || rec.file_data || file?.data || doc?.data;
|
||||
const mimeTypeFallback =
|
||||
rec.mime_type ||
|
||||
rec.media_type ||
|
||||
file?.mime_type ||
|
||||
doc?.mime_type ||
|
||||
"application/octet-stream";
|
||||
rec.mime_type || rec.media_type || file?.mime_type || doc?.mime_type || "application/pdf";
|
||||
if (typeof rawDataStr === "string" && !rawDataStr.startsWith("http")) {
|
||||
const rawData = rawDataStr.replace(/^data:[a-zA-Z0-9/+-]+;base64,/, "");
|
||||
parts.push({
|
||||
@@ -154,7 +152,13 @@ export function convertOpenAIContentToParts(content: unknown): JsonRecord[] {
|
||||
const fileUrl = toRecord(rec.file_url);
|
||||
const fileObj = toRecord(rec.file);
|
||||
const docObj = toRecord(rec.document);
|
||||
const fileData = imageUrl?.url || fileUrl?.url || fileObj?.url || docObj?.url;
|
||||
// `file_url` is a top-level string on the Responses-API input_file shape (#2515).
|
||||
const fileData =
|
||||
(typeof rec.file_url === "string" ? rec.file_url : undefined) ||
|
||||
imageUrl?.url ||
|
||||
fileUrl?.url ||
|
||||
fileObj?.url ||
|
||||
docObj?.url;
|
||||
if (typeof fileData === "string" && fileData.startsWith("data:")) {
|
||||
const commaIndex = fileData.indexOf(",");
|
||||
if (commaIndex !== -1) {
|
||||
|
||||
@@ -377,13 +377,21 @@ export function openaiToOpenAIResponsesRequest(
|
||||
}
|
||||
return imgResult;
|
||||
}
|
||||
if (contentItem.type === "file") {
|
||||
const file = toRecord(contentItem.file);
|
||||
if (contentItem.type === "file" || contentItem.type === "document") {
|
||||
// Accept both the OpenAI `file` shape and the Gemini-style `document` shape,
|
||||
// and map the bare `data`/`url` fields too, so a PDF reaches Codex/Responses
|
||||
// regardless of which content-part name the client used (#2515).
|
||||
const file = toRecord(
|
||||
contentItem.type === "document" ? contentItem.document : contentItem.file
|
||||
);
|
||||
const fileResult: JsonRecord = { type: "input_file" };
|
||||
if (file.file_data !== undefined) fileResult.file_data = file.file_data;
|
||||
else if (file.data !== undefined) fileResult.file_data = file.data;
|
||||
if (file.file_id !== undefined) fileResult.file_id = file.file_id;
|
||||
if (file.file_url !== undefined) fileResult.file_url = file.file_url;
|
||||
else if (file.url !== undefined) fileResult.file_url = file.url;
|
||||
if (file.filename !== undefined) fileResult.filename = file.filename;
|
||||
else if (file.name !== undefined) fileResult.filename = file.name;
|
||||
return fileResult;
|
||||
}
|
||||
return contentValue;
|
||||
|
||||
@@ -456,8 +456,18 @@ function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolName
|
||||
}
|
||||
|
||||
// OpenAI -> Gemini (standard API)
|
||||
export function openaiToGeminiRequest(model, body, stream) {
|
||||
return openaiToGeminiBase(model, body, stream);
|
||||
export function openaiToGeminiRequest(model, body, stream, credentials = null) {
|
||||
// Thread the signature namespace so a thinking model's thoughtSignature (cached on the
|
||||
// response turn under `<connectionId>:<toolCallId>`) is found and re-attached to the
|
||||
// functionCall on the follow-up request. Without this the streaming lookup key didn't
|
||||
// match and Gemini rejected tool calls with 400 "missing thought_signature" (#2504).
|
||||
const signatureNamespace =
|
||||
credentials &&
|
||||
typeof credentials === "object" &&
|
||||
typeof (credentials as Record<string, unknown>)._signatureNamespace === "string"
|
||||
? ((credentials as Record<string, unknown>)._signatureNamespace as string)
|
||||
: null;
|
||||
return openaiToGeminiBase(model, body, stream, { signatureNamespace });
|
||||
}
|
||||
|
||||
// OpenAI -> Gemini CLI (Cloud Code Assist)
|
||||
@@ -642,7 +652,16 @@ register(
|
||||
(model, body, stream, credentials) =>
|
||||
wrapInCloudCodeEnvelope(
|
||||
model,
|
||||
openaiToGeminiCLIRequest(model, body, stream, { functionResponseShape: "output" }),
|
||||
openaiToGeminiCLIRequest(model, body, stream, {
|
||||
functionResponseShape: "output",
|
||||
// Forward the signature namespace so streaming thoughtSignatures round-trip (#2504).
|
||||
signatureNamespace:
|
||||
credentials &&
|
||||
typeof credentials === "object" &&
|
||||
typeof (credentials as Record<string, unknown>)._signatureNamespace === "string"
|
||||
? ((credentials as Record<string, unknown>)._signatureNamespace as string)
|
||||
: null,
|
||||
}),
|
||||
credentials
|
||||
),
|
||||
null
|
||||
|
||||
@@ -891,3 +891,58 @@ test("OpenAI -> Antigravity Gemini path preserves thinkingConfig (only Claude is
|
||||
assert.equal((result as any).request?.generationConfig.thinkingConfig.thinkingBudget > 0, true);
|
||||
assert.equal((result as any).request?.generationConfig.thinkingConfig.includeThoughts, true);
|
||||
});
|
||||
|
||||
// Regression for #2515: a PDF sent in the Responses-API `input_file` shape must reach
|
||||
// Gemini as inlineData instead of being silently dropped.
|
||||
test("convertOpenAIContentToParts handles input_file file_data (#2515)", () => {
|
||||
const parts = convertOpenAIContentToParts([
|
||||
{ type: "input_file", file_data: "JVBERi0xLjcKJ", filename: "doc.pdf" },
|
||||
]);
|
||||
const inline = parts.find((p) => (p as any).inlineData);
|
||||
assert.ok(inline, "input_file with file_data must produce an inlineData part");
|
||||
assert.equal((inline as any).inlineData.data, "JVBERi0xLjcKJ");
|
||||
});
|
||||
|
||||
test("convertOpenAIContentToParts handles input_file file_url data URI (#2515)", () => {
|
||||
const parts = convertOpenAIContentToParts([
|
||||
{ type: "input_file", file_url: "data:application/pdf;base64,QUJD", filename: "d.pdf" },
|
||||
]);
|
||||
const inline = parts.find((p) => (p as any).inlineData);
|
||||
assert.ok(inline, "input_file with file_url data URI must produce an inlineData part");
|
||||
assert.equal((inline as any).inlineData.data, "QUJD");
|
||||
assert.equal((inline as any).inlineData.mimeType, "application/pdf");
|
||||
});
|
||||
|
||||
// Regression for #2504: with credentials._signatureNamespace set, a previously-cached
|
||||
// Gemini thoughtSignature must be re-attached to the functionCall on the follow-up turn.
|
||||
test("openaiToGeminiRequest re-attaches cached thoughtSignature for FORMATS.GEMINI (#2504)", async () => {
|
||||
const { buildGeminiThoughtSignatureKey, storeGeminiThoughtSignature } =
|
||||
await import("../../open-sse/services/geminiThoughtSignatureStore.ts");
|
||||
const ns = "conn-2504";
|
||||
const toolId = "call_2504_abc";
|
||||
storeGeminiThoughtSignature(buildGeminiThoughtSignatureKey(ns, toolId), "SIG_2504_XYZ");
|
||||
|
||||
const result: any = openaiToGeminiRequest(
|
||||
"gemini-2.5-pro-preview",
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: "run a tool" },
|
||||
{
|
||||
role: "assistant",
|
||||
tool_calls: [
|
||||
{ id: toolId, type: "function", function: { name: "Bash", arguments: '{"cmd":"ls"}' } },
|
||||
],
|
||||
},
|
||||
{ role: "tool", tool_call_id: toolId, content: "ok" },
|
||||
],
|
||||
},
|
||||
false,
|
||||
{ _signatureNamespace: ns }
|
||||
);
|
||||
|
||||
const json = JSON.stringify(result);
|
||||
assert.ok(
|
||||
json.includes("SIG_2504_XYZ"),
|
||||
"cached thoughtSignature must be re-attached to the functionCall"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user