Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,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. Applies only to direct AI Studio requests. Omitted or `true` keeps the `-tiered` wire rename for Gemini Flash ids (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`); `false` sends the requested bare ids to the wire unchanged. Vertex preserves the requested model ID, and Cloud Code Assist routing is unchanged. 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<string, CursorMcpServerConfig>` | Cursor only: stdio or Streamable HTTP MCP servers. |
Expand Down
24 changes: 13 additions & 11 deletions src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,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<string, string> = {
"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;
Expand Down Expand Up @@ -147,7 +145,7 @@ function geminiToolResultText(content: string | OcxContentPart[]): string {

function messagesToGeminiFormat(
parsed: OcxParsedRequest,
routedModelId = parsed.modelId,
identityModelId: string,
): { systemInstruction?: unknown; contents: unknown[] } {
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
Expand All @@ -156,7 +154,7 @@ function messagesToGeminiFormat(
...(parsed.context.systemPrompt ?? []),
...(toolCatalogNudge ? [toolCatalogNudge] : []),
GOOGLE_BREVITY_INSTRUCTION,
].join("\n\n"), routedModelId);
].join("\n\n"), identityModelId);
const systemInstruction = { parts: [{ text: systemText }] };

const contents: unknown[] = [];
Expand Down Expand Up @@ -396,8 +394,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
parsed.modelId,
mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
).wireModelId
: resolveDirectGeminiWireModelId(parsed.modelId);
const { systemInstruction, contents } = messagesToGeminiFormat(parsed, routedModelId);
: provider.googleMode === "vertex"
? parsed.modelId
: resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
Comment thread
chilung-cgu marked this conversation as resolved.
// AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
const { systemInstruction, contents } = messagesToGeminiFormat(parsed, identityModelId);
const tools = toolsToGeminiFormat(parsed);

const body: Record<string, unknown> = { contents };
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ const providerConfigSchema = z.object({
upstreamHttpVersion: z.enum(UPSTREAM_HTTP_VERSION_VALUES)
.nullish()
.transform(value => value ?? undefined),
directGeminiWireRenames: z.boolean().optional(),
noStructuredOutputModels: z.array(z.string().min(1))
.transform(normalizeNonBlankStringArray)
.optional(),
Expand Down
9 changes: 8 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1392,8 +1392,15 @@ export interface OcxProviderConfig {
* HTTP/2 streaming responses (issue #1668). "http1.1" / "h1" forces HTTP/1.1,
* "http2" / "h2" forces HTTP/2. Absent or "auto" keeps Bun's default negotiation
* (current behavior unchanged). Only meaningful for https: base URLs.
*/
*/
upstreamHttpVersion?: UpstreamHttpVersion;
/**
* 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;
/**
Expand Down
32 changes: 32 additions & 0 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,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");

Expand Down
105 changes: 105 additions & 0 deletions tests/google-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,108 @@ 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;
}

function identityParsed(modelId: string): OcxParsedRequest {
return {
modelId,
stream: false,
options: {},
context: {
systemPrompt: ["You are Codex, a coding agent based on GPT-5."],
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`);
}
});

test("directGeminiWireRenames: true still maps to the -tiered wire id", async () => {
const adapter = createGoogleAdapter({ ...provider, directGeminiWireRenames: true });
for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) {
const request = await adapter.buildRequest(identityParsed(modelId));
const { url } = request;
expect(url).toContain(`/v1beta/models/${modelId}-tiered:generateContent`);
const body = JSON.parse(request.body) as {
systemInstruction?: { parts?: Array<{ text?: string }> };
};
const systemText = body.systemInstruction?.parts?.[0]?.text ?? "";
expect(systemText).toContain(`powered by the ${modelId}`);
expect(systemText).not.toContain(`${modelId}-tiered`);
}
});
Comment thread
chilung-cgu marked this conversation as resolved.

test("directGeminiWireRenames does not affect Cloud Code Assist requests", async () => {
const ccaProvider = {
...provider,
googleMode: "cloud-code-assist",
baseUrl: "https://daily-cloudcode-pa.googleapis.com",
project: "proj-123",
} as const;
for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) {
const parsed = renamedParsed(modelId);
const defaultRequest = await createGoogleAdapter(ccaProvider).buildRequest(parsed);
const optOutRequest = await createGoogleAdapter({ ...ccaProvider, directGeminiWireRenames: false })
.buildRequest(parsed);
expect(optOutRequest.url).toBe(defaultRequest.url);
// The envelope's requestId/sessionId are minted per request; compare the wire model only.
const defaultModel = JSON.parse(defaultRequest.body).model as string;
const optOutModel = JSON.parse(optOutRequest.body).model as string;
expect(optOutModel).toBe(defaultModel);
}
});

test("Cloud Code Assist identity follows a migrated wire model", async () => {
const ccaProvider = {
...provider,
googleMode: "cloud-code-assist",
baseUrl: "https://daily-cloudcode-pa.googleapis.com",
project: "proj-123",
} as const;
const request = await createGoogleAdapter(ccaProvider).buildRequest(identityParsed("gemini-3.6-flash"));
const envelope = JSON.parse(request.body) as {
model: string;
request: { systemInstruction?: { parts?: Array<{ text?: string }> } };
};
const systemText = envelope.request.systemInstruction?.parts?.[0]?.text ?? "";

expect(envelope.model).toBe("gemini-3.7-flash-tiered");
expect(systemText).toContain("powered by the gemini-3.7-flash-tiered");
expect(systemText).not.toContain("powered by the gemini-3.6-flash.");
});

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);
}
});
});
Loading