From a2436bc50bd8d2cc1288ddb3a6829b10e48d3fc7 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Mon, 13 Apr 2026 14:58:37 -0300 Subject: [PATCH] fix(gemini): harden schema sanitizing and streaming defaults Remove unsupported Gemini schema fields including vendor-prefixed `x-` properties so translated tool definitions avoid rejected keywords. Emit an empty `content` field with the initial assistant role delta to keep OpenAI-compatible streaming output consistent for clients that expect content on the first chunk. Also force undici's fetch whenever a dispatcher is provided and treat `onRequestStart` version mismatches as fatal instead of falling back to native fetch, preventing broken proxy requests under mixed undici versions. --- open-sse/translator/helpers/geminiHelper.ts | 7 ++++--- open-sse/translator/response/gemini-to-openai.ts | 2 +- open-sse/utils/proxyFetch.ts | 14 +++++++++++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/open-sse/translator/helpers/geminiHelper.ts b/open-sse/translator/helpers/geminiHelper.ts index cd6eb63538..a0c671917b 100644 --- a/open-sse/translator/helpers/geminiHelper.ts +++ b/open-sse/translator/helpers/geminiHelper.ts @@ -42,6 +42,7 @@ export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [ "contentMediaType", "contentEncoding", // Non-standard schema fields (not recognized by Gemini API) + "deprecated", "optional", // UI/Styling properties (from Cursor tools - NOT JSON Schema standard) "cornerRadius", @@ -185,9 +186,9 @@ function removeUnsupportedKeywords(obj, keywords) { } } else { // Delete unsupported keys at current level - for (const keyword of keywords) { - if (keyword in obj) { - delete obj[keyword]; + for (const key of Object.keys(obj)) { + if (keywords.includes(key) || key.startsWith("x-")) { + delete obj[key]; } } // Recurse into remaining values diff --git a/open-sse/translator/response/gemini-to-openai.ts b/open-sse/translator/response/gemini-to-openai.ts index bc6d6bb877..b13cf46695 100644 --- a/open-sse/translator/response/gemini-to-openai.ts +++ b/open-sse/translator/response/gemini-to-openai.ts @@ -28,7 +28,7 @@ export function geminiToOpenAIResponse(chunk, state) { choices: [ { index: 0, - delta: { role: "assistant" }, + delta: { role: "assistant", content: "" }, finish_reason: null, }, ], diff --git a/open-sse/utils/proxyFetch.ts b/open-sse/utils/proxyFetch.ts index ff0a2de58a..ccbb986a81 100644 --- a/open-sse/utils/proxyFetch.ts +++ b/open-sse/utils/proxyFetch.ts @@ -165,6 +165,9 @@ export async function runWithProxyContext(proxyConfig, fn) { async function patchedFetch(input: RequestInfo | URL, options: FetchWithDispatcherOptions = {}) { if (options?.dispatcher) { + // When a dispatcher is present, we MUST use the undici library fetch + // to ensure version compatibility. Node 22 built-in fetch (undici v6) + // is incompatible with undici v8 dispatchers (missing onRequestStart, etc.) return (undiciFetch as unknown as (...args: unknown[]) => Promise)(input, options); } @@ -206,7 +209,16 @@ async function patchedFetch(input: RequestInfo | URL, options: FetchWithDispatch dispatcher: getDefaultDispatcher(), }); } catch (dispatcherError) { - const msg = dispatcherError instanceof Error ? dispatcherError.message : String(dispatcherError); + const msg = + dispatcherError instanceof Error ? dispatcherError.message : String(dispatcherError); + // CAUTION: Do NOT fallback to native fetch if the error is a version mismatch (invalid onRequestStart) + // because the native fetch will definitely fail with the undici v8 dispatcher. + if (msg.includes("onRequestStart")) { + console.error( + `[ProxyFetch] Fatal version mismatch: Dispatcher (v8) vs Fetch (v6/native). Hardware upgrade or SOCKS5 config isolation required. Error: ${msg}` + ); + throw dispatcherError; + } // Only fallback for connection/dispatcher errors, not HTTP errors if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED") || msg.includes("UND_ERR")) { console.warn(`[ProxyFetch] Undici dispatcher failed, falling back to native fetch: ${msg}`);