mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
refactor(sse): extract openai-to-gemini pure helpers into a leaf (#5824)
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.
This commit is contained in:
committed by
GitHub
parent
ec457e264d
commit
6cff67e6ac
@@ -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<string>([
|
||||
type GeminiPart = Record<string, unknown>;
|
||||
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<string, string>): Map<string, string> | 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<string, unknown>;
|
||||
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 [
|
||||
`<previous_tool_result_context source="${source}">`,
|
||||
result,
|
||||
"</previous_tool_result_context>",
|
||||
].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".
|
||||
|
||||
142
open-sse/translator/request/openai-to-gemini/helpers.ts
Normal file
142
open-sse/translator/request/openai-to-gemini/helpers.ts
Normal file
@@ -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<string, string>
|
||||
): Map<string, string> | 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<string, unknown>;
|
||||
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 [
|
||||
`<previous_tool_result_context source="${source}">`,
|
||||
result,
|
||||
"</previous_tool_result_context>",
|
||||
].join("\n");
|
||||
}
|
||||
88
tests/unit/openai-to-gemini-helpers-split.test.ts
Normal file
88
tests/unit/openai-to-gemini-helpers-split.test.ts
Normal file
@@ -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>"&'), "<t>"&");
|
||||
assert.equal(h.escapeHistoricalContextContent('<t>"&'), '<t>"&');
|
||||
});
|
||||
|
||||
test("buildHistoricalToolResultContext wraps escaped source + result in the context tag", () => {
|
||||
assert.equal(
|
||||
h.buildHistoricalToolResultContext("myTool", { r: 1 }),
|
||||
'<previous_tool_result_context source="myTool">\n{"r":1}\n</previous_tool_result_context>'
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user