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 @@ -81,7 +81,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids.
| `codexAccountMode?` | `"pool" \| "direct"` | Canonical `openai` only; defaults to Pool. Direct bypasses pool state. |
| `refreshPolicy?` | `"proactive" \| "lazy-only" \| "disabled"` | Override this OAuth provider's Token Guardian policy. |
| `reasoningEfforts?` | `string[]` | Provider-wide Codex reasoning labels to advertise and send. |
| `modelReasoningEfforts?` | `Record<string, string[]>` | Per-model labels. An empty list hides effort control. |
| `modelReasoningEfforts?` | `Record<string, string[]>` | Per-model labels. An empty list hides effort control. For `google`-adapter providers a configured ladder is also a capability assertion: the selected effort is sent on the wire as `generationConfig.thinkingConfig.thinkingLevel` (except Cloud Code Assist and image-capable models), so only configure a ladder for models that accept that field. |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| `modelSupportsReasoningSummaries?` | `Record<string, boolean>` | Set a model to `false` to stop advertising summaries and strip summary-delivery fields. |
| `modelReasoningSummaryDelivery?` | `Record<string, "sequential" \| "sequential_cutoff" \| "concurrent" \| "concurrent_cutoff">` | Per-model Responses delivery enum; rewrites an existing delivery field. |
| `modelAdapters?` | `Record<string, string>` | Per-model `openai-chat` or `openai-responses` wire override for mixed-wire gateways. Explicit entries beat registry defaults; DeepSeek's preset can select native Responses for `deepseek-v4-flash`, and GitHub Copilot declares Responses-only defaults for its GPT-5 family (`gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`) because those models reject `/chat/completions` for agent traffic. Models without a built-in default (for example `gpt-5.4-nano`) can be opted in here. Single-wire upstream pins and canonical ChatGPT forward reject overrides. |
Expand Down
20 changes: 15 additions & 5 deletions src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
type TranslatorBudget,
} from "../lib/translator-budget";
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
import { mapReasoningEffort } from "../reasoning-effort";
import { configuredReasoningEfforts, mapReasoningEffort } from "../reasoning-effort";

// Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
// tool calls. This steers them to keep the BETWEEN-STEP text to one line and reason internally
Expand Down Expand Up @@ -340,12 +340,22 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
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;
const directFlashThinking = provider.googleMode !== "vertex"
&& provider.googleMode !== "cloud-code-assist"
&& (parsed.modelId === "gemini-3.5-flash" || parsed.modelId === "gemini-3.6-flash")
// Effort → thinkingLevel follows the configured ladder: any model advertising reasoning
// efforts (registry preset or user config) sends the mapped level, so a picker-selected
// effort actually reaches the wire (gemini-3.1-pro-preview ships a ladder). The original
// gemini-3.5/3.6-flash direct-mode slice stays hardcoded so unladdered configs keep their
// current behavior; Vertex participates only through an explicitly configured ladder (the
// seed google-vertex entry ships none). Image models are excluded — thinkingConfig would
// suppress the responseModalities fallback below. CCA maps effort on its envelope path.
const thinkingEligible = provider.googleMode !== "cloud-code-assist"
&& !isImageCapableModel(parsed.modelId)
&& (configuredReasoningEfforts(provider, parsed.modelId) !== undefined
|| (provider.googleMode !== "vertex"
&& (parsed.modelId === "gemini-3.5-flash" || parsed.modelId === "gemini-3.6-flash")));
const thinkingLevel = thinkingEligible
? mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning)
: undefined;
if (directFlashThinking) generationConfig.thinkingConfig = { thinkingLevel: directFlashThinking };
if (thinkingLevel) generationConfig.thinkingConfig = { thinkingLevel };
if (!generationConfig.thinkingConfig && isImageCapableModel(parsed.modelId)) {
generationConfig.responseModalities = ["TEXT", "IMAGE"];
}
Expand Down
76 changes: 76 additions & 0 deletions tests/google-hardening.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,82 @@ describe("google provider hardening", () => {
expect(JSON.parse(antigravity.body).request.generationConfig).toBeUndefined();
});

test("effort ladder drives thinkingLevel beyond the flash slice", async () => {
const direct = createGoogleAdapter(provider({
modelReasoningEfforts: { "gemini-3.1-pro-preview": ["low", "medium", "high"] },
}));
const proHigh = await direct.buildRequest({
...parsed(),
modelId: "gemini-3.1-pro-preview",
options: { reasoning: "high" },
});
const proMinimal = await direct.buildRequest({
...parsed(),
modelId: "gemini-3.1-pro-preview",
options: { reasoning: "minimal" },
});
const proUnset = await direct.buildRequest({
...parsed(),
modelId: "gemini-3.1-pro-preview",
});
const unladdered = await direct.buildRequest({
...parsed(),
modelId: "gemini-3.5-flash-lite",
options: { reasoning: "high" },
});

expect(JSON.parse(proHigh.body).generationConfig.thinkingConfig).toEqual({ thinkingLevel: "high" });
// minimal is not on the pro-preview ladder; the clamp lands on the nearest supported tier.
expect(JSON.parse(proMinimal.body).generationConfig.thinkingConfig).toEqual({ thinkingLevel: "low" });
expect(JSON.parse(proUnset.body).generationConfig).toBeUndefined();
expect(JSON.parse(unladdered.body).generationConfig).toBeUndefined();
});

test("Vertex sends thinkingLevel only when a ladder is explicitly configured", async () => {
const frozen = createGoogleAdapter(provider({ googleMode: "vertex" }));
const opted = createGoogleAdapter(provider({
googleMode: "vertex",
modelReasoningEfforts: { "gemini-3-pro": ["low", "medium", "high"] },
}));
const withoutLadder = await frozen.buildRequest({
...parsed(),
modelId: "gemini-3.5-flash",
options: { reasoning: "high" },
});
const withLadder = await opted.buildRequest({
...parsed(),
modelId: "gemini-3-pro",
options: { reasoning: "high" },
});

expect(JSON.parse(withoutLadder.body).generationConfig).toBeUndefined();
expect(JSON.parse(withLadder.body).generationConfig.thinkingConfig).toEqual({ thinkingLevel: "high" });
});

test("unladdered direct flash keeps its hardcoded thinking slice", async () => {
const bare = createGoogleAdapter(provider());
const flash = await bare.buildRequest({
...parsed(),
modelId: "gemini-3.6-flash",
options: { reasoning: "medium" },
});

expect(JSON.parse(flash.body).generationConfig.thinkingConfig).toEqual({ thinkingLevel: "medium" });
});

test("image models keep responseModalities even with a provider-wide effort ladder", async () => {
const direct = createGoogleAdapter(provider({ reasoningEfforts: ["low", "high"] }));
const image = await direct.buildRequest({
...parsed(),
modelId: "gemini-3.1-flash-image",
options: { reasoning: "high" },
});

const generationConfig = JSON.parse(image.body).generationConfig;
expect(generationConfig.thinkingConfig).toBeUndefined();
expect(generationConfig.responseModalities).toEqual(["TEXT", "IMAGE"]);
});

Comment thread
coderabbitai[bot] marked this conversation as resolved.
test("publishes audited AI Studio metadata while Vertex stays frozen", () => {
const google = PROVIDER_REGISTRY.find(entry => entry.id === "google");
const vertex = PROVIDER_REGISTRY.find(entry => entry.id === "google-vertex");
Expand Down
Loading