From 6cff67e6acc2cf17fb5dfc5978c6fc2cd001e924 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:10:14 -0300 Subject: [PATCH] refactor(sse): extract openai-to-gemini pure helpers into a leaf (#5824) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split open-sse/translator/request/openai-to-gemini.ts (873 -> 756 LOC, back under the 800-line cap) by moving the module-private pure helpers — the historical-tool- context string builders (stringifyHistoricalToolArguments, buildInertHistorical*, escapeHistoricalContext*, buildHistoricalToolResultContext), deepCleanUndefined, extractClientThoughtSignature, buildChangedToolNameMap, isVertexGeminiProvider, and applyAntigravityGenerationDefaults (with its GeminiGenerationConfig shape) — into openai-to-gemini/helpers.ts. These were module-private, so the translator's public API is unchanged; the host imports them back internally. Bodies are verbatim: the code-line multiset of host + leaf equals the original. Adds tests/unit/openai-to-gemini-helpers-split.test.ts pinning the leaf's pure behaviour (escaping, undefined-pruning, signature extraction, antigravity generation-config defaults) and the host wiring. --- .../translator/request/openai-to-gemini.ts | 148 ++---------------- .../request/openai-to-gemini/helpers.ts | 142 +++++++++++++++++ .../openai-to-gemini-helpers-split.test.ts | 88 +++++++++++ 3 files changed, 244 insertions(+), 134 deletions(-) create mode 100644 open-sse/translator/request/openai-to-gemini/helpers.ts create mode 100644 tests/unit/openai-to-gemini-helpers-split.test.ts diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index b2c03bffaa..703f2cbe10 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -24,6 +24,20 @@ import { cleanJSONSchemaForAntigravity, } from "../helpers/geminiHelper.ts"; import { buildGeminiTools, sanitizeGeminiToolName } from "../helpers/geminiToolsSanitizer.ts"; +import { + type GeminiGenerationConfig, + isVertexGeminiProvider, + buildChangedToolNameMap, + extractClientThoughtSignature, + deepCleanUndefined, + applyAntigravityGenerationDefaults, + stringifyHistoricalToolArguments, + buildInertHistoricalToolCallText, + buildInertHistoricalToolResponseText, + escapeHistoricalContextAttribute, + escapeHistoricalContextContent, + buildHistoricalToolResultContext, +} from "./openai-to-gemini/helpers.ts"; // Observed Antigravity wrapper output cap, not an underlying model capability. // Keep this bridge-local: Antigravity currently caps visible output around 16K. @@ -43,20 +57,6 @@ const GEMINI_BUILTIN_TOOL_NAMES = new Set([ type GeminiPart = Record; type GeminiContent = { role: string; parts: GeminiPart[] }; -type GeminiGenerationConfig = { - temperature?: unknown; - topP?: unknown; - topK?: unknown; - maxOutputTokens?: unknown; - thinkingConfig?: { - thinkingBudget: number; - includeThoughts: boolean; - }; - responseMimeType?: string; - responseSchema?: unknown; - stopSequences?: string[] | unknown[]; -}; - type GeminiFunctionDeclaration = { name: string; description: string; @@ -118,126 +118,6 @@ type GeminiToolNameOptions = { supportsSignatureBypass?: boolean; }; -// Vertex AI (and Vertex Partner models) reject the OpenAI-style `id` field inside -// function_call / function_response parts. Detect these by the routed provider id. -function isVertexGeminiProvider(provider: unknown): boolean { - return provider === "vertex" || provider === "vertex-partner"; -} - -type OpenAIToolCallLike = { - thoughtSignature?: unknown; - thought_signature?: unknown; - function?: { - thoughtSignature?: unknown; - thought_signature?: unknown; - }; -}; - -function buildChangedToolNameMap(toolNameMap: Map): Map | null { - const changedEntries = [...toolNameMap.entries()].filter( - ([sanitizedName, originalName]) => sanitizedName !== originalName - ); - return changedEntries.length > 0 ? new Map(changedEntries) : null; -} - -function extractClientThoughtSignature(toolCall: unknown): string | null { - if (!toolCall || typeof toolCall !== "object") return null; - const candidate = toolCall as OpenAIToolCallLike; - - const signature = - candidate.thoughtSignature || - candidate.thought_signature || - candidate.function?.thoughtSignature || - candidate.function?.thought_signature || - null; - return typeof signature === "string" && signature.length > 0 ? signature : null; -} - -function deepCleanUndefined(value: unknown, depth = 0): void { - if (depth > 10 || !value || typeof value !== "object") { - return; - } - if (Array.isArray(value)) { - for (const item of value) { - deepCleanUndefined(item, depth + 1); - } - } else { - const obj = value as Record; - for (const key of Object.keys(obj)) { - const val = obj[key]; - if (typeof val === "string" && val === "[undefined]") { - delete obj[key]; - } else { - deepCleanUndefined(val, depth + 1); - } - } - } -} - -function applyAntigravityGenerationDefaults(generationConfig: GeminiGenerationConfig) { - const config = { ...generationConfig }; - if (config.topK === undefined) { - config.topK = 40; - } - if (config.topP === undefined) { - config.topP = 1; - } - - const thinkingBudget = Number(config.thinkingConfig?.thinkingBudget); - const maxOutputTokens = Number(config.maxOutputTokens); - if ( - Number.isFinite(thinkingBudget) && - thinkingBudget > 0 && - (!Number.isFinite(maxOutputTokens) || maxOutputTokens <= thinkingBudget) - ) { - config.maxOutputTokens = Math.floor(thinkingBudget) + 1; - } - - return config; -} - -function stringifyHistoricalToolArguments(value: unknown): string { - if (typeof value === "string") return value; - try { - return JSON.stringify(value ?? {}); - } catch { - return String(value ?? "{}"); - } -} - -function buildInertHistoricalToolCallText(name: string | undefined, args: unknown): string { - const toolName = name || "unknown"; - return `[tool_history_call: ${toolName}] ${stringifyHistoricalToolArguments(args || "{}")}`; -} - -function buildInertHistoricalToolResponseText(name: string, response: unknown): string { - return `[tool_history_result: ${name || "unknown"}] ${typeof response === "string" ? response : stringifyHistoricalToolArguments(response)}`; -} - -function escapeHistoricalContextAttribute(value: string): string { - return value - .replaceAll("&", "&") - .replaceAll('"', """) - .replaceAll("<", "<") - .replaceAll(">", ">"); -} - -function escapeHistoricalContextContent(value: string): string { - return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); -} - -function buildHistoricalToolResultContext(name: string, response: unknown): string { - const source = escapeHistoricalContextAttribute(name || "unknown"); - const rawResult = - typeof response === "string" ? response : stringifyHistoricalToolArguments(response); - const result = escapeHistoricalContextContent(rawResult); - return [ - ``, - result, - "", - ].join("\n"); -} - // Gemini-family APIs (incl. Antigravity / Vertex) reject a `contents[]` array that // has two adjacent entries with the same role: // 400 INVALID_ARGUMENT "Request contains consecutive messages with the same role". diff --git a/open-sse/translator/request/openai-to-gemini/helpers.ts b/open-sse/translator/request/openai-to-gemini/helpers.ts new file mode 100644 index 0000000000..3dfd35cef1 --- /dev/null +++ b/open-sse/translator/request/openai-to-gemini/helpers.ts @@ -0,0 +1,142 @@ +// Pure, self-contained helpers extracted verbatim from ../openai-to-gemini.ts +// (god-file decomposition): historical-tool-context string builders, undefined- +// pruning, thought-signature extraction, tool-name remapping, and the Vertex +// provider check + Antigravity generation-config defaults. No I/O or module state; +// the host imports them back internally (these were module-private — no public API +// change). The GeminiGenerationConfig shape lives here with its only mutator. + +export type GeminiGenerationConfig = { + temperature?: unknown; + topP?: unknown; + topK?: unknown; + maxOutputTokens?: unknown; + thinkingConfig?: { + thinkingBudget: number; + includeThoughts: boolean; + }; + responseMimeType?: string; + responseSchema?: unknown; + stopSequences?: string[] | unknown[]; +}; + +// Vertex AI (and Vertex Partner models) reject the OpenAI-style `id` field inside +// function_call / function_response parts. Detect these by the routed provider id. +export function isVertexGeminiProvider(provider: unknown): boolean { + return provider === "vertex" || provider === "vertex-partner"; +} + +type OpenAIToolCallLike = { + thoughtSignature?: unknown; + thought_signature?: unknown; + function?: { + thoughtSignature?: unknown; + thought_signature?: unknown; + }; +}; + +export function buildChangedToolNameMap( + toolNameMap: Map +): Map | null { + const changedEntries = [...toolNameMap.entries()].filter( + ([sanitizedName, originalName]) => sanitizedName !== originalName + ); + return changedEntries.length > 0 ? new Map(changedEntries) : null; +} + +export function extractClientThoughtSignature(toolCall: unknown): string | null { + if (!toolCall || typeof toolCall !== "object") return null; + const candidate = toolCall as OpenAIToolCallLike; + + const signature = + candidate.thoughtSignature || + candidate.thought_signature || + candidate.function?.thoughtSignature || + candidate.function?.thought_signature || + null; + return typeof signature === "string" && signature.length > 0 ? signature : null; +} + +export function deepCleanUndefined(value: unknown, depth = 0): void { + if (depth > 10 || !value || typeof value !== "object") { + return; + } + if (Array.isArray(value)) { + for (const item of value) { + deepCleanUndefined(item, depth + 1); + } + } else { + const obj = value as Record; + for (const key of Object.keys(obj)) { + const val = obj[key]; + if (typeof val === "string" && val === "[undefined]") { + delete obj[key]; + } else { + deepCleanUndefined(val, depth + 1); + } + } + } +} + +export function applyAntigravityGenerationDefaults(generationConfig: GeminiGenerationConfig) { + const config = { ...generationConfig }; + if (config.topK === undefined) { + config.topK = 40; + } + if (config.topP === undefined) { + config.topP = 1; + } + + const thinkingBudget = Number(config.thinkingConfig?.thinkingBudget); + const maxOutputTokens = Number(config.maxOutputTokens); + if ( + Number.isFinite(thinkingBudget) && + thinkingBudget > 0 && + (!Number.isFinite(maxOutputTokens) || maxOutputTokens <= thinkingBudget) + ) { + config.maxOutputTokens = Math.floor(thinkingBudget) + 1; + } + + return config; +} + +export function stringifyHistoricalToolArguments(value: unknown): string { + if (typeof value === "string") return value; + try { + return JSON.stringify(value ?? {}); + } catch { + return String(value ?? "{}"); + } +} + +export function buildInertHistoricalToolCallText(name: string | undefined, args: unknown): string { + const toolName = name || "unknown"; + return `[tool_history_call: ${toolName}] ${stringifyHistoricalToolArguments(args || "{}")}`; +} + +export function buildInertHistoricalToolResponseText(name: string, response: unknown): string { + return `[tool_history_result: ${name || "unknown"}] ${typeof response === "string" ? response : stringifyHistoricalToolArguments(response)}`; +} + +export function escapeHistoricalContextAttribute(value: string): string { + return value + .replaceAll("&", "&") + .replaceAll('"', """) + .replaceAll("<", "<") + .replaceAll(">", ">"); +} + +export function escapeHistoricalContextContent(value: string): string { + return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); +} + +export function buildHistoricalToolResultContext(name: string, response: unknown): string { + const source = escapeHistoricalContextAttribute(name || "unknown"); + const rawResult = + typeof response === "string" ? response : stringifyHistoricalToolArguments(response); + const result = escapeHistoricalContextContent(rawResult); + return [ + ``, + result, + "", + ].join("\n"); +} diff --git a/tests/unit/openai-to-gemini-helpers-split.test.ts b/tests/unit/openai-to-gemini-helpers-split.test.ts new file mode 100644 index 0000000000..7ad469b849 --- /dev/null +++ b/tests/unit/openai-to-gemini-helpers-split.test.ts @@ -0,0 +1,88 @@ +// Split-guard for the openai-to-gemini helpers extraction (god-file decomposition): +// the pure historical-tool-context builders, undefined-pruning, thought-signature +// extraction, tool-name remapping, the Vertex provider check, and the Antigravity +// generation-config defaults moved verbatim from openai-to-gemini.ts into +// openai-to-gemini/helpers.ts. These were module-private, so the translator's public +// API is unchanged; the host imports them back internally. The locks pin the leaf's +// pure behaviour and that the host now imports the leaf. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +import * as h from "../../open-sse/translator/request/openai-to-gemini/helpers.ts"; + +test("isVertexGeminiProvider matches only the vertex provider ids", () => { + assert.equal(h.isVertexGeminiProvider("vertex"), true); + assert.equal(h.isVertexGeminiProvider("vertex-partner"), true); + assert.equal(h.isVertexGeminiProvider("openai"), false); + assert.equal(h.isVertexGeminiProvider(undefined), false); +}); + +test("buildChangedToolNameMap keeps only renamed entries, else null", () => { + const changed = h.buildChangedToolNameMap( + new Map([ + ["a", "a"], + ["b_sanitized", "b"], + ]) + ); + assert.deepEqual([...(changed ?? new Map()).entries()], [["b_sanitized", "b"]]); + assert.equal(h.buildChangedToolNameMap(new Map([["a", "a"]])), null); +}); + +test("extractClientThoughtSignature reads the first non-empty signature field", () => { + assert.equal(h.extractClientThoughtSignature({ thoughtSignature: "sig" }), "sig"); + assert.equal(h.extractClientThoughtSignature({ function: { thought_signature: "s2" } }), "s2"); + assert.equal(h.extractClientThoughtSignature({ thoughtSignature: "" }), null); + assert.equal(h.extractClientThoughtSignature({}), null); + assert.equal(h.extractClientThoughtSignature(null), null); +}); + +test("deepCleanUndefined deletes '[undefined]' string values in place, recursively", () => { + const obj = { a: 1, b: "[undefined]", c: { d: "[undefined]", e: 2 }, f: ["[undefined]"] }; + h.deepCleanUndefined(obj); + assert.deepEqual(obj, { a: 1, c: { e: 2 }, f: ["[undefined]"] }); +}); + +test("applyAntigravityGenerationDefaults fills topK/topP and bumps maxOutputTokens past the budget", () => { + assert.deepEqual(h.applyAntigravityGenerationDefaults({}), { topK: 40, topP: 1 }); + assert.deepEqual(h.applyAntigravityGenerationDefaults({ topK: 5 }), { topK: 5, topP: 1 }); + const withBudget = h.applyAntigravityGenerationDefaults({ + thinkingConfig: { thinkingBudget: 100, includeThoughts: true }, + }); + assert.equal(withBudget.maxOutputTokens, 101); +}); + +test("historical-tool-context builders stringify and escape as expected", () => { + assert.equal(h.stringifyHistoricalToolArguments("raw"), "raw"); + assert.equal(h.stringifyHistoricalToolArguments({ a: 1 }), '{"a":1}'); + assert.equal(h.stringifyHistoricalToolArguments(undefined), "{}"); + assert.equal( + h.buildInertHistoricalToolCallText("foo", { a: 1 }), + '[tool_history_call: foo] {"a":1}' + ); + assert.equal( + h.buildInertHistoricalToolResponseText("bar", "ok"), + "[tool_history_result: bar] ok" + ); + // Attribute escaping includes quotes; content escaping does not. + assert.equal(h.escapeHistoricalContextAttribute('"&'), "<t>"&"); + assert.equal(h.escapeHistoricalContextContent('"&'), '<t>"&'); +}); + +test("buildHistoricalToolResultContext wraps escaped source + result in the context tag", () => { + assert.equal( + h.buildHistoricalToolResultContext("myTool", { r: 1 }), + '\n{"r":1}\n' + ); +}); + +test("host imports the helpers leaf and no longer defines them inline", () => { + const host = fs.readFileSync( + path.join("open-sse", "translator", "request", "openai-to-gemini.ts"), + "utf-8" + ); + assert.match(host, /from "\.\/openai-to-gemini\/helpers\.ts"/); + assert.doesNotMatch(host, /^function deepCleanUndefined\(/m); + assert.doesNotMatch(host, /^type GeminiGenerationConfig =/m); +});