Skip to content
Merged
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
40 changes: 39 additions & 1 deletion src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,43 @@ const GOOGLE_BREVITY_INSTRUCTION = [
"- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
].join("\n");

/**
* Documented output ceiling for a Google-surface model, or `undefined` when the id is not
* recognized.
*
* Unknown ids return `undefined` deliberately. An earlier revision returned a 16,384 floor for
* anything unmatched, which silently truncated aliases, gateway ids, and any model added after
* this table was written — the operator asked for N tokens and got 16,384 with no signal. A cap
* we cannot justify is worse than no cap: `structure/02_config-and-codex-home.md` is explicit
* that an explicit request value wins, so an unrecognized model passes through untouched and the
* upstream remains the authority on its own limit.
*
* Matching is prefix/family based rather than substring based for the same reason: `includes("pro")`
* matched any id containing "pro" (`my-prototype-model`), and `includes("oss")` matched any id
* containing "oss" (`crossover-v2`).
*/
export function maxOutputTokensForGoogleModel(modelId: string): number | undefined {
const lower = modelId.toLowerCase().trim();
if (lower.startsWith("gemini")) {
// Pro tops out one token below the flash/other Gemini ceiling; both are documented values.
return /(^|[-.])pro([-.]|$)/.test(lower) ? 65535 : 65536;
}
if (lower.startsWith("claude")) return 64000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve per-model Claude output ceilings

When Google Antigravity live discovery selects claude-sonnet-4-6-thinking, the repository metadata at scripts/model-metadata.source.json:12511-12529 records a 128,000-token output limit, but this family-wide branch makes clampGoogleMaxOutputTokens(..., 100000) return 64,000. Before this change the requested 100,000 tokens reached upstream; now it is silently truncated, so read exact ceilings from the canonical provider metadata and pass through unmatched Claude IDs rather than treating every Claude model as 64,000.

AGENTS.md reference: src/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

if (lower.startsWith("gpt-oss")) return 32768;
return undefined;
}

export function clampGoogleMaxOutputTokens(
modelId: string,
requestedTokens?: number,
): number | undefined {
if (requestedTokens === undefined || requestedTokens <= 0) return undefined;
const modelMax = maxOutputTokensForGoogleModel(modelId);
// Unknown model: honour the request as-is rather than inventing a ceiling for it.
if (modelMax === undefined) return requestedTokens;
return Math.min(requestedTokens, modelMax);
}

/**
* 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
Expand Down Expand Up @@ -650,7 +687,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
if (toolConfig) body.toolConfig = toolConfig;

const generationConfig: Record<string, unknown> = {};
if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
const clampedMaxOutputTokens = clampGoogleMaxOutputTokens(identityModelId, parsed.options.maxOutputTokens);
if (clampedMaxOutputTokens !== undefined) generationConfig.maxOutputTokens = clampedMaxOutputTokens;
if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
if (parsed.options.stopSequences) generationConfig.stopSequences = parsed.options.stopSequences;
Expand Down
48 changes: 48 additions & 0 deletions tests/google-output-clamp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test";
import { clampGoogleMaxOutputTokens, maxOutputTokensForGoogleModel } from "../src/adapters/google";

describe("google maxOutputTokens clamp", () => {
test("returns model-specific max output tokens limit", () => {
expect(maxOutputTokensForGoogleModel("gemini-3.7-flash")).toBe(65536);
expect(maxOutputTokensForGoogleModel("gemini-3.7-flash-tiered")).toBe(65536);
expect(maxOutputTokensForGoogleModel("gemini-3-pro")).toBe(65535);
expect(maxOutputTokensForGoogleModel("claude-3-7-sonnet")).toBe(64000);
expect(maxOutputTokensForGoogleModel("claude-3-5-sonnet@20241022")).toBe(64000);
expect(maxOutputTokensForGoogleModel("gpt-oss-120b")).toBe(32768);
});

test("an unrecognized model has no invented ceiling", () => {
// A cap we cannot justify silently truncates the operator's explicit request. Aliases,
// gateway ids, and models newer than this table must pass through untouched.
expect(maxOutputTokensForGoogleModel("custom-unknown-model")).toBeUndefined();
expect(clampGoogleMaxOutputTokens("custom-unknown-model", 128000)).toBe(128000);
expect(maxOutputTokensForGoogleModel("some-gateway/gemini-3-pro")).toBeUndefined();
});

test("family matching does not fire on incidental substrings", () => {
// "includes(pro)" matched my-prototype-model; "includes(oss)" matched crossover-v2.
expect(maxOutputTokensForGoogleModel("my-prototype-model")).toBeUndefined();
expect(maxOutputTokensForGoogleModel("crossover-v2")).toBeUndefined();
expect(maxOutputTokensForGoogleModel("gemini-3-pro-preview")).toBe(65535);
expect(maxOutputTokensForGoogleModel("gemini-3.5-flash")).toBe(65536);
});

test("downward clamps excessive requested tokens to model max", () => {
expect(clampGoogleMaxOutputTokens("gemini-3.7-flash", 128000)).toBe(65536);
expect(clampGoogleMaxOutputTokens("gemini-3-pro", 100000)).toBe(65535);
expect(clampGoogleMaxOutputTokens("claude-3-7-sonnet", 100000)).toBe(64000);
expect(clampGoogleMaxOutputTokens("gpt-oss-120b", 64000)).toBe(32768);
});

test("preserves requested tokens when within model max", () => {
expect(clampGoogleMaxOutputTokens("gemini-3.7-flash", 4096)).toBe(4096);
expect(clampGoogleMaxOutputTokens("gemini-3-pro", 8192)).toBe(8192);
expect(clampGoogleMaxOutputTokens("claude-3-7-sonnet", 32000)).toBe(32000);
});

test("returns undefined when requested tokens is undefined or non-positive", () => {
expect(clampGoogleMaxOutputTokens("gemini-3.7-flash", undefined)).toBeUndefined();
expect(clampGoogleMaxOutputTokens("gemini-3.7-flash", 0)).toBeUndefined();
expect(clampGoogleMaxOutputTokens("gemini-3.7-flash", -10)).toBeUndefined();
});
});
Loading