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.
This commit is contained in:
diegosouzapw
2026-04-13 14:58:37 -03:00
parent cd56ea7040
commit a2436bc50b
3 changed files with 18 additions and 5 deletions

View File

@@ -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

View File

@@ -28,7 +28,7 @@ export function geminiToOpenAIResponse(chunk, state) {
choices: [
{
index: 0,
delta: { role: "assistant" },
delta: { role: "assistant", content: "" },
finish_reason: null,
},
],

View File

@@ -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<Response>)(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}`);