From 6e3860aee938f41adb49c670e8969b914e9f60b7 Mon Sep 17 00:00:00 2001 From: chilung Date: Sat, 15 Aug 2026 13:41:21 +0800 Subject: [PATCH 1/2] fix(google): allow direct Gemini wire rename opt-out Keep the existing -tiered mapping by default while allowing AI Studio providers that still serve bare Gemini Flash ids to opt out. Cover both Flash generations and document the provider setting. --- .../docs/reference/configuration/providers.md | 1 + src/adapters/google.ts | 14 +++++----- src/config.ts | 1 + src/types.ts | 7 +++++ tests/google-adapter.test.ts | 26 +++++++++++++++++++ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index aef4bbbe08..e552074391 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -121,6 +121,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids. | `escapeBuiltinToolNames?` | `boolean` | Escape built-in tool names for Anthropic-compatible gateways and restore them in returned calls. | | `anthropicEofTolerance?` | `boolean` | Let an Anthropic-compatible gateway complete a stream that ends before `message_stop`, only when visible text or a complete JSON-object tool input was received. Off by default. | | `googleMode?` | `"ai-studio" \| "vertex" \| "cloud-code-assist"` | Google transport/auth mode. Default `ai-studio`. | +| `directGeminiWireRenames?` | `boolean` | Google only. When `false`, the AI Studio (direct) path sends Gemini Flash ids to the wire unchanged instead of applying the `-tiered` suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Defaults to the rename; set `false` when the configured upstream still serves the bare ids. | | `project?` | `string` | Vertex or Antigravity Cloud Code Assist project id. | | `location?` | `string` | Vertex location; environment fallback is `GOOGLE_CLOUD_LOCATION`. | | `mcpServers?` | `Record` | Cursor only: stdio or Streamable HTTP MCP servers. | diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 9a074c0506..728556ee98 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -46,19 +46,17 @@ const GOOGLE_BREVITY_INSTRUCTION = [ ].join("\n"); /** - * Google renamed the current Gemini Flash generations on the Generative Language API, - * appending a `-tiered` suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). The - * old `gemini-3.7-flash` path 404s, so a saved config or registry entry naming the base - * id must be resolved here before it reaches the URL. The user-facing id is deliberately - * left alone: the picker, the catalog, the usage log and the price overlays all stay - * keyed on the base id, and only the wire path learns the new spelling. + * Some Google direct deployments expose current Gemini Flash generations with a `-tiered` + * wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id + * stable and make the mapping configurable for deployments that still serve the bare id. */ const GEMINI_DIRECT_WIRE_RENAMES: Record = { "gemini-3.7-flash": "gemini-3.7-flash-tiered", "gemini-3.6-flash": "gemini-3.6-flash-tiered", }; -function resolveDirectGeminiWireModelId(modelId: string): string { +function resolveDirectGeminiWireModelId(modelId: string, applyRenames: boolean): string { + if (!applyRenames) return modelId; return Object.hasOwn(GEMINI_DIRECT_WIRE_RENAMES, modelId) ? GEMINI_DIRECT_WIRE_RENAMES[modelId]! : modelId; @@ -372,7 +370,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte parsed.modelId, mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning), ).wireModelId - : resolveDirectGeminiWireModelId(parsed.modelId); + : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false); const { systemInstruction, contents } = messagesToGeminiFormat(parsed, routedModelId); const tools = toolsToGeminiFormat(parsed); diff --git a/src/config.ts b/src/config.ts index 4b83c82954..5822e6e9f8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -734,6 +734,7 @@ const providerConfigSchema = z.object({ modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(), preserveResponsesReasoningContent: z.boolean().optional(), allowPrivateNetwork: z.boolean().optional(), + directGeminiWireRenames: z.boolean().optional(), noStructuredOutputModels: z.array(z.string().min(1)) .transform(normalizeNonBlankStringArray) .optional(), diff --git a/src/types.ts b/src/types.ts index aaf4fe18d2..9887947036 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1353,6 +1353,13 @@ export interface OcxProviderConfig { * link-local, or unique-local upstreams. Metadata endpoints remain blocked. */ allowPrivateNetwork?: boolean; + /** + * Google only. When `false`, the AI Studio (direct) path sends Gemini Flash ids + * unchanged to the wire instead of applying the `-tiered` suffix (`gemini-3.7-flash` + * -> `gemini-3.7-flash-tiered`). Set this to `false` when the configured upstream still + * serves the bare ids. Absent (default) keeps the rename. + */ + directGeminiWireRenames?: boolean; /** Keep provider settings on disk but exclude it from routing and model/catalog listings. */ disabled?: boolean; /** diff --git a/tests/google-adapter.test.ts b/tests/google-adapter.test.ts index 467cf19c37..076f746b57 100644 --- a/tests/google-adapter.test.ts +++ b/tests/google-adapter.test.ts @@ -278,3 +278,29 @@ describe("google adapter — tool_choice on the wire", () => { }); }); }); + +describe("google adapter — direct -tiered wire renames", () => { + function renamedParsed(modelId: string): OcxParsedRequest { + return { + modelId, + stream: false, + options: {}, + context: { messages: [{ role: "user", content: "hi" }], tools: [] }, + } as unknown as OcxParsedRequest; + } + + test("default maps the picker id to the -tiered wire id", async () => { + for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { + const { url } = await createGoogleAdapter(provider).buildRequest(renamedParsed(modelId)); + expect(url).toContain(`/v1beta/models/${modelId}-tiered:generateContent`); + } + }); + + test("directGeminiWireRenames: false keeps the bare wire id", async () => { + const adapter = createGoogleAdapter({ ...provider, directGeminiWireRenames: false }); + for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { + const { url } = await adapter.buildRequest(renamedParsed(modelId)); + expect(url).toContain(`/v1beta/models/${modelId}:generateContent`); + } + }); +}); From d4fad12b55c5e27eae1e3db744bcf024c3fc35e6 Mon Sep 17 00:00:00 2001 From: chilung Date: Sat, 15 Aug 2026 14:33:22 +0800 Subject: [PATCH 2/2] fix(google): scope direct Gemini rename opt-out --- src/adapters/google.ts | 4 +++- tests/config.test.ts | 32 ++++++++++++++++++++++++++++++++ tests/google-adapter.test.ts | 12 ++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 728556ee98..24a155c9e0 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -370,7 +370,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte parsed.modelId, mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning), ).wireModelId - : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false); + : provider.googleMode === "vertex" + ? parsed.modelId + : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false); const { systemInstruction, contents } = messagesToGeminiFormat(parsed, routedModelId); const tools = toolsToGeminiFormat(parsed); diff --git a/tests/config.test.ts b/tests/config.test.ts index 5ec0d918a7..b710eb7eb1 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -789,6 +789,38 @@ describe("opencodex config defaults", () => { expect(readConfigDiagnostics().error).toContain("responsesSnapshotRepair"); }); + test("direct Gemini wire rename opt-out is a boolean and round-trips", () => { + const base = { + port: 12345, + providers: { + google: { + adapter: "google", + baseUrl: "https://generativelanguage.googleapis.com", + }, + }, + defaultProvider: "google", + }; + writeConfig({ + ...base, + providers: { + google: { ...base.providers.google, directGeminiWireRenames: false }, + }, + }); + const config = loadConfig(); + expect(config.providers.google.directGeminiWireRenames).toBe(false); + saveConfig(config); + expect(loadConfig().providers.google.directGeminiWireRenames).toBe(false); + + writeConfig({ + ...base, + providers: { + google: { ...base.providers.google, directGeminiWireRenames: "false" }, + }, + }); + expect(readConfigDiagnostics().source).toBe("fallback"); + expect(readConfigDiagnostics().error).toContain("directGeminiWireRenames"); + }); + test("accepts a relative responsesPath", () => { writeResponsesPathConfig("/responses"); diff --git a/tests/google-adapter.test.ts b/tests/google-adapter.test.ts index 076f746b57..e24b113ec6 100644 --- a/tests/google-adapter.test.ts +++ b/tests/google-adapter.test.ts @@ -303,4 +303,16 @@ describe("google adapter — direct -tiered wire renames", () => { expect(url).toContain(`/v1beta/models/${modelId}:generateContent`); } }); + + test("directGeminiWireRenames does not affect Vertex requests", async () => { + const vertexProvider = { ...provider, googleMode: "vertex" as const }; + for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { + const parsed = renamedParsed(modelId); + const defaultRequest = await createGoogleAdapter(vertexProvider).buildRequest(parsed); + const optOutRequest = await createGoogleAdapter({ ...vertexProvider, directGeminiWireRenames: false }) + .buildRequest(parsed); + expect(optOutRequest.url).toBe(defaultRequest.url); + expect(optOutRequest.body).toBe(defaultRequest.body); + } + }); });