From e227fc0dd32b8a9d077eb9e22a4bcd9e11a2da6f Mon Sep 17 00:00:00 2001 From: ksmaze Date: Sat, 14 Mar 2026 21:39:36 -0700 Subject: [PATCH 1/3] fix: remove gemini unrecognized fields before sending request --- .../routes/proxy/upstreamEndpoint.test.ts | 65 +++++++++++++++++++ src/server/routes/proxy/upstreamEndpoint.ts | 24 +++++-- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/server/routes/proxy/upstreamEndpoint.test.ts b/src/server/routes/proxy/upstreamEndpoint.test.ts index 01eb370..60cffc6 100644 --- a/src/server/routes/proxy/upstreamEndpoint.test.ts +++ b/src/server/routes/proxy/upstreamEndpoint.test.ts @@ -614,6 +614,71 @@ describe('buildUpstreamEndpointRequest', () => { expect(request.headers['Content-Type']).toBe('application/json'); }); + it('strips unsupported openai parameters like frequency_penalty for Gemini models on chat endpoints', () => { + const request = buildUpstreamEndpointRequest({ + endpoint: 'chat', + modelName: 'gemini-1.5-pro', + stream: false, + tokenValue: 'sk-test', + sitePlatform: 'gemini', + openaiBody: { + model: 'gemini-1.5-pro', + messages: [{ role: 'user', content: 'hello' }], + frequency_penalty: 0.5, + presence_penalty: 0.2, + logit_bias: { '100': 1 }, + logprobs: true, + top_logprobs: 2, + store: true, + temperature: 0.8, + top_p: 1.0, + }, + downstreamFormat: 'openai', + }); + + expect(request.path).toBe('/v1beta/openai/chat/completions'); + expect(request.body.frequency_penalty).toBeUndefined(); + expect(request.body.presence_penalty).toBeUndefined(); + expect(request.body.logit_bias).toBeUndefined(); + expect(request.body.logprobs).toBeUndefined(); + expect(request.body.top_logprobs).toBeUndefined(); + expect(request.body.store).toBeUndefined(); + expect(request.body.temperature).toBe(0.8); + expect(request.body.top_p).toBe(1.0); + }); + + it('strips unsupported openai parameters like frequency_penalty for Gemini models on responses endpoints', () => { + const request = buildUpstreamEndpointRequest({ + endpoint: 'responses', + modelName: 'gemini-1.5-pro', + stream: false, + tokenValue: 'sk-test', + sitePlatform: 'gemini', + openaiBody: { + model: 'gemini-1.5-pro', + messages: [{ role: 'user', content: 'hello' }], + frequency_penalty: 0.5, + presence_penalty: 0.2, + logit_bias: { '100': 1 }, + logprobs: true, + top_logprobs: 2, + store: true, + temperature: 0.8, + top_p: 1.0, + }, + downstreamFormat: 'openai', + }); + + expect(request.path).toBe('/v1beta/openai/responses'); + expect(request.body.frequency_penalty).toBeUndefined(); + expect(request.body.presence_penalty).toBeUndefined(); + expect(request.body.logit_bias).toBeUndefined(); + expect(request.body.logprobs).toBeUndefined(); + expect(request.body.top_logprobs).toBeUndefined(); + expect(request.body.store).toBeUndefined(); + expect(request.body.temperature).toBe(0.8); + }); + it('preserves structured responses content blocks instead of flattening them', () => { const request = buildUpstreamEndpointRequest({ endpoint: 'responses', diff --git a/src/server/routes/proxy/upstreamEndpoint.ts b/src/server/routes/proxy/upstreamEndpoint.ts index 5698bd9..29f0298 100644 --- a/src/server/routes/proxy/upstreamEndpoint.ts +++ b/src/server/routes/proxy/upstreamEndpoint.ts @@ -437,6 +437,7 @@ export function buildUpstreamEndpointRequest(input: { } { const sitePlatform = normalizePlatformName(input.sitePlatform); const isClaudeUpstream = sitePlatform === 'claude'; + const isGeminiUpstream = sitePlatform === 'gemini'; const resolveGeminiEndpointPath = (endpoint: UpstreamEndpoint): string => { const normalizedSiteUrl = asTrimmedString(input.siteUrl).toLowerCase(); @@ -452,7 +453,7 @@ export function buildUpstreamEndpointRequest(input: { }; const resolveEndpointPath = (endpoint: UpstreamEndpoint): string => { - if (sitePlatform === 'gemini') { + if (isGeminiUpstream) { return resolveGeminiEndpointPath(endpoint); } @@ -480,6 +481,21 @@ export function buildUpstreamEndpointRequest(input: { commonHeaders.Authorization = `Bearer ${input.tokenValue}`; } + const openaiBody = { ...input.openaiBody }; + if (isGeminiUpstream) { + const blacklist = [ + 'frequency_penalty', + 'presence_penalty', + 'logit_bias', + 'logprobs', + 'top_logprobs', + 'store', + ]; + for (const key of blacklist) { + delete openaiBody[key]; + } + } + if (input.endpoint === 'messages') { const claudeHeaders = input.downstreamFormat === 'claude' ? extractClaudePassthroughHeaders(input.downstreamHeaders) @@ -514,7 +530,7 @@ export function buildUpstreamEndpointRequest(input: { const sanitizedBody = nativeClaudeBody ?? normalizedClaudeBody ?? sanitizeAnthropicMessagesBody( - convertOpenAiBodyToAnthropicMessagesBody(input.openaiBody, input.modelName, input.stream), + convertOpenAiBodyToAnthropicMessagesBody(openaiBody, input.modelName, input.stream), ); const headers = ensureStreamAcceptHeader({ @@ -542,7 +558,7 @@ export function buildUpstreamEndpointRequest(input: { model: input.modelName, stream: input.stream, } - : convertOpenAiBodyToResponsesBodyViaTransformer(input.openaiBody, input.modelName, input.stream) + : convertOpenAiBodyToResponsesBodyViaTransformer(openaiBody, input.modelName, input.stream) ); const body = sanitizeResponsesBodyForProxyViaTransformer(rawBody, input.modelName, input.stream); @@ -563,7 +579,7 @@ export function buildUpstreamEndpointRequest(input: { path: resolveEndpointPath('chat'), headers, body: { - ...input.openaiBody, + ...openaiBody, model: input.modelName, stream: input.stream, }, From 9e4ffbd6c83839cad46dd0ecea4845b720f84181 Mon Sep 17 00:00:00 2001 From: ksmaze Date: Sat, 14 Mar 2026 22:05:12 -0700 Subject: [PATCH 2/3] address comment --- .../routes/proxy/upstreamEndpoint.test.ts | 34 +++++++++++++++++++ src/server/routes/proxy/upstreamEndpoint.ts | 32 +++++++++-------- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/server/routes/proxy/upstreamEndpoint.test.ts b/src/server/routes/proxy/upstreamEndpoint.test.ts index 60cffc6..00646c6 100644 --- a/src/server/routes/proxy/upstreamEndpoint.test.ts +++ b/src/server/routes/proxy/upstreamEndpoint.test.ts @@ -679,6 +679,40 @@ describe('buildUpstreamEndpointRequest', () => { expect(request.body.temperature).toBe(0.8); }); + it('strips unsupported openai parameters like frequency_penalty for Gemini models from downstream responses bodies', () => { + const request = buildUpstreamEndpointRequest({ + endpoint: 'responses', + modelName: 'gemini-1.5-pro', + stream: false, + tokenValue: 'sk-test', + sitePlatform: 'gemini', + openaiBody: {}, + downstreamFormat: 'responses', + responsesOriginalBody: { + model: 'gemini-1.5-pro', + input: 'hello', + frequency_penalty: 0.5, + presence_penalty: 0.2, + logit_bias: { '100': 1 }, + logprobs: true, + top_logprobs: 2, + store: true, + temperature: 0.8, + top_p: 1.0, + }, + }); + + expect(request.path).toBe('/v1beta/openai/responses'); + expect(request.body.frequency_penalty).toBeUndefined(); + expect(request.body.presence_penalty).toBeUndefined(); + expect(request.body.logit_bias).toBeUndefined(); + expect(request.body.logprobs).toBeUndefined(); + expect(request.body.top_logprobs).toBeUndefined(); + expect(request.body.store).toBeUndefined(); + expect(request.body.temperature).toBe(0.8); + expect(request.body.top_p).toBe(1.0); + }); + it('preserves structured responses content blocks instead of flattening them', () => { const request = buildUpstreamEndpointRequest({ endpoint: 'responses', diff --git a/src/server/routes/proxy/upstreamEndpoint.ts b/src/server/routes/proxy/upstreamEndpoint.ts index 29f0298..78baaa1 100644 --- a/src/server/routes/proxy/upstreamEndpoint.ts +++ b/src/server/routes/proxy/upstreamEndpoint.ts @@ -481,20 +481,24 @@ export function buildUpstreamEndpointRequest(input: { commonHeaders.Authorization = `Bearer ${input.tokenValue}`; } - const openaiBody = { ...input.openaiBody }; - if (isGeminiUpstream) { - const blacklist = [ - 'frequency_penalty', - 'presence_penalty', - 'logit_bias', - 'logprobs', - 'top_logprobs', - 'store', - ]; - for (const key of blacklist) { - delete openaiBody[key]; + const stripGeminiUnsupportedFields = (body: Record) => { + const next = { ...body }; + if (isGeminiUpstream) { + for (const key of [ + 'frequency_penalty', + 'presence_penalty', + 'logit_bias', + 'logprobs', + 'top_logprobs', + 'store', + ]) { + delete next[key]; + } } - } + return next; + }; + + const openaiBody = stripGeminiUnsupportedFields(input.openaiBody); if (input.endpoint === 'messages') { const claudeHeaders = input.downstreamFormat === 'claude' @@ -554,7 +558,7 @@ export function buildUpstreamEndpointRequest(input: { const rawBody = ( input.downstreamFormat === 'responses' && input.responsesOriginalBody ? { - ...input.responsesOriginalBody, + ...stripGeminiUnsupportedFields(input.responsesOriginalBody), model: input.modelName, stream: input.stream, } From c9d4a10f15b1c0a4b57c3a1c597041a108956d05 Mon Sep 17 00:00:00 2001 From: ciat-777 Date: Sun, 15 Mar 2026 13:16:50 +0800 Subject: [PATCH 3/3] fix(db): make external bootstrap idempotent --- src/server/db/runtimeSchemaBootstrap.test.ts | 75 ++++++++++++++++++++ src/server/db/runtimeSchemaBootstrap.ts | 34 ++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/server/db/runtimeSchemaBootstrap.test.ts b/src/server/db/runtimeSchemaBootstrap.test.ts index fc378b6..9d4f460 100644 --- a/src/server/db/runtimeSchemaBootstrap.test.ts +++ b/src/server/db/runtimeSchemaBootstrap.test.ts @@ -38,4 +38,79 @@ describe('runtime schema bootstrap', () => { expect(executedSql.slice(0, expectedBootstrapSql.length)).toEqual(expectedBootstrapSql); }); + + it('ignores duplicate mysql index errors when replaying bootstrap statements', async () => { + const executedSql: string[] = []; + const targetSql = 'CREATE UNIQUE INDEX `model_availability_account_model_unique` ON `model_availability` (`account_id`, `model_name`(191))'; + + await ensureRuntimeDatabaseSchema({ + ...createStubClient('mysql', executedSql), + execute: async (sqlText: string) => { + executedSql.push(sqlText); + if (sqlText === targetSql) { + const error = new Error("Duplicate key name 'model_availability_account_model_unique'") as Error & { code?: string }; + error.code = 'ER_DUP_KEYNAME'; + throw error; + } + return []; + }, + }); + + expect(executedSql).toContain(targetSql); + }); + + it('replays the mysql bootstrap against an already-initialized schema', async () => { + const executedSql: string[] = []; + const createdStatements = new Set(); + + const mysqlClient = createStubClient('mysql', executedSql); + mysqlClient.execute = async (sqlText: string) => { + executedSql.push(sqlText); + const normalized = sqlText.trim().toLowerCase(); + const createsSchemaObject = normalized.startsWith('create table if not exists') + || normalized.startsWith('create index') + || normalized.startsWith('create unique index'); + + if (createsSchemaObject) { + if (createdStatements.has(sqlText)) { + const error = new Error( + normalized.startsWith('create table') + ? 'Table already exists' + : 'Duplicate key name during bootstrap replay', + ) as Error & { code?: string }; + error.code = normalized.startsWith('create table') ? 'ER_TABLE_EXISTS_ERROR' : 'ER_DUP_KEYNAME'; + throw error; + } + createdStatements.add(sqlText); + } + + return []; + }; + + await ensureRuntimeDatabaseSchema(mysqlClient); + await ensureRuntimeDatabaseSchema(mysqlClient); + + expect(executedSql.length).toBeGreaterThan(createdStatements.size); + expect(createdStatements.size).toBeGreaterThan(0); + }); + + it('ignores postgres relation-already-exists errors when replaying bootstrap statements', async () => { + const executedSql: string[] = []; + const targetSql = 'CREATE UNIQUE INDEX "model_availability_account_model_unique" ON "model_availability" ("account_id", "model_name")'; + + await ensureRuntimeDatabaseSchema({ + ...createStubClient('postgres', executedSql), + execute: async (sqlText: string) => { + executedSql.push(sqlText); + if (sqlText === targetSql) { + const error = new Error('relation "model_availability_account_model_unique" already exists') as Error & { code?: string }; + error.code = '42P07'; + throw error; + } + return []; + }, + }); + + expect(executedSql).toContain(targetSql); + }); }); diff --git a/src/server/db/runtimeSchemaBootstrap.ts b/src/server/db/runtimeSchemaBootstrap.ts index 1ee2ef1..3121489 100644 --- a/src/server/db/runtimeSchemaBootstrap.ts +++ b/src/server/db/runtimeSchemaBootstrap.ts @@ -28,6 +28,38 @@ export interface RuntimeSchemaConnectionInput { ssl?: boolean; } +function normalizeSchemaErrorMessage(error: unknown): string { + if (typeof error === 'object' && error && 'message' in error) { + return String((error as { message?: unknown }).message || ''); + } + return String(error || ''); +} + +function isExistingSchemaObjectError(error: unknown): boolean { + const lowered = normalizeSchemaErrorMessage(error).toLowerCase(); + const code = typeof error === 'object' && error && 'code' in error + ? String((error as { code?: unknown }).code || '') + : ''; + + return code === 'ER_DUP_KEYNAME' + || code === 'ER_TABLE_EXISTS_ERROR' + || code === '42P07' + || code === '42710' + || lowered.includes('already exists') + || lowered.includes('duplicate key name') + || lowered.includes('relation') && lowered.includes('already exists'); +} + +async function executeBootstrapStatement(client: RuntimeSchemaClient, sqlText: string): Promise { + try { + await client.execute(sqlText); + } catch (error) { + if (!isExistingSchemaObjectError(error)) { + throw error; + } + } +} + function validateIdentifier(identifier: string): string { if (!/^[a-z_][a-z0-9_]*$/i.test(identifier)) { throw new Error(`Invalid SQL identifier: ${identifier}`); @@ -212,7 +244,7 @@ export async function ensureRuntimeDatabaseSchema(client: RuntimeSchemaClient): : readGeneratedBootstrapStatements(client.dialect); for (const sqlText of statements) { - await client.execute(sqlText); + await executeBootstrapStatement(client, sqlText); } await ensureLegacySchemaCompatibility(createLegacySchemaInspector(client));