Skip to content
Closed
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 @@ -208,6 +208,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `preserveReasoningContentModels?` | `string[]` | Models requiring prior assistant `reasoning_content` in chat history. |
| `reasoningDetailsModels?` | `string[]` | Models whose endpoint returns thinking as a structured `reasoning_details` array (MiniMax M-series with `reasoning_split`); stream deltas are cumulative snapshots that are prefix-diffed, and preserved reasoning replays as a `reasoning_details` array instead of a `reasoning_content` string. |
| `requiresReasoningPlaceholderModels?` | `string[]` | Models whose upstream rejects a tool_call continuation missing `reasoning_content` (DeepSeek thinking mode); a minimal placeholder is injected when the replay cache misses. Defaults to `preserveReasoningContentModels`; set `[]` to opt out. |
| `showThinkingSummary?` | `boolean` | Opt-in: surface upstream thinking as visible reasoning even when the client omits `reasoning.summary` (the Codex default, which otherwise keeps thinking in hidden replay envelopes). An explicit client `reasoning.summary: "none"` still wins, and `false` opts a seeded preset back out. Which channel carries the visible text is the bridge's decision, not this flag's. `google-antigravity` is an OAuth-only `google` provider using the Cloud Code Assist wire and is seeded `true`. On that wire the opt-in also sets `generationConfig.thinkingConfig.includeThoughts` for Gemini models — Cloud Code Assist reports `thoughtsTokenCount` either way but sends no `thought` text without it (Gemini only; Claude is never asked and `gpt-oss` rejects the field with `INVALID_ARGUMENT`). |
| `thinkingToggleModels?` | `string[]` | Chat models using `thinking.enabled` rather than an effort ladder. |
| `thinkingBudgetModels?` | `string[]` | Chat models using integer `thinking_budget`; effort maps to a budget fraction. |
| `noVisionModels?` | `string[]` | Text-only models sent through the vision sidecar; matching tolerates an Ollama `:size` tag. |
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,7 @@
"responses-routed-web-search-fields.test.ts": "responses",
"responses-self-named-namespace-scrub.test.ts": "responses",
"responses-shadow-intercept.test.ts": "responses",
"responses-show-thinking-summary.test.ts": "responses",
"responses-snapshot-repair-server.test.ts": "responses",
"responses-snapshot-repair.test.ts": "responses",
"responses-state-write-amplification.test.ts": "responses",
Expand Down
20 changes: 14 additions & 6 deletions src/adapters/google-wire-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,20 @@ function compileGenerationConfig(value: unknown): JsonObject | undefined {
))].slice(0, 5);
if (stopSequences.length > 0) out.stopSequences = stopSequences;
}
if (isObject(value.thinkingConfig) && typeof value.thinkingConfig.thinkingLevel === "string") {
const raw = value.thinkingConfig.thinkingLevel.toLowerCase();
const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw)
? raw
: (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined);
if (thinkingLevel) out.thinkingConfig = { thinkingLevel };
if (isObject(value.thinkingConfig)) {
const thinking: JsonObject = {};
if (typeof value.thinkingConfig.thinkingLevel === "string") {
const raw = value.thinkingConfig.thinkingLevel.toLowerCase();
const thinkingLevel = GOOGLE_THINKING_LEVELS.has(raw)
? raw
: (["xhigh", "max", "ultra"].includes(raw) ? "high" : undefined);
if (thinkingLevel) thinking.thinkingLevel = thinkingLevel;
}
// The one key that makes Google return `thought: true` text. Cloud Code Assist serves
// thinking either way (thoughtsTokenCount stays non-zero) but withholds the text unless the
// request opts in, so dropping it here silently reinstates the missing-thinking behavior.
if (value.thinkingConfig.includeThoughts === true) thinking.includeThoughts = true;
if (Object.keys(thinking).length > 0) out.thinkingConfig = thinking;
}
if (Array.isArray(value.responseModalities)) {
const valid = value.responseModalities.filter((m): m is string => typeof m === "string" && ["TEXT", "IMAGE", "AUDIO"].includes(m));
Expand Down
20 changes: 18 additions & 2 deletions src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,11 +866,27 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
);
antigravityModel = wireModelId;
antigravitySession = sessionId;
// Gemini returns no chain-of-thought TEXT unless the request opts in. Probed against CCA
// 2026-09-12: `gemini-3.8-flash-high` answered with thoughtsTokenCount=321 and zero
// `thought` parts, then 358-652 chars of genuine reasoning once includeThoughts was set.
// Scoped to Gemini wire ids — Claude-on-CCA accepts the flag but never returns thought
// parts, and gpt-oss rejects it outright (400 INVALID_ARGUMENT, which would break every
// gpt-oss turn). Gated on the provider's visible-thinking opt-in so a user who wants
// thinking hidden does not pay conversation-history tokens for text nobody renders;
// `hideThinkingSummary !== true` is the same per-request gate the response path uses, so
// a client that explicitly asked for hidden thinking is not billed for the text either.
const includeThoughts = provider.showThinkingSummary === true
&& parsed.options.hideThinkingSummary !== true
&& /^gemini-/.test(wireModelId)
&& !isImageCapableModel(parsed.modelId);
// Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
// Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction.
if (thinkingLevel) {
if (thinkingLevel || includeThoughts) {
const gc = (body.generationConfig ?? {}) as Record<string, unknown>;
gc.thinkingConfig = { thinkingLevel };
gc.thinkingConfig = {
...(thinkingLevel ? { thinkingLevel } : {}),
...(includeThoughts ? { includeThoughts: true } : {}),
};
body.generationConfig = gc;
}
// Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity
Expand Down
4 changes: 4 additions & 0 deletions src/providers/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface DerivedKeyLoginProvider {
autoToolChoiceOnlyModels?: string[];
preserveReasoningContentModels?: string[];
requiresReasoningPlaceholderModels?: string[];
showThinkingSummary?: boolean;
reasoningSplitModels?: string[];
reasoningDetailsModels?: string[];
thinkingToggleModels?: string[];
Expand Down Expand Up @@ -271,6 +272,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}),
...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}),
...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}),
...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}),
...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
Expand Down Expand Up @@ -320,6 +322,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}),
...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}),
...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}),
...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}),
...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
Expand Down Expand Up @@ -574,6 +577,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels];
if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels];
if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames;
if (prov.showThinkingSummary === undefined && seed.showThinkingSummary !== undefined) prov.showThinkingSummary = seed.showThinkingSummary;
if (prov.keyOptional === undefined && seed.keyOptional !== undefined) prov.keyOptional = seed.keyOptional;
if (prov.freeTier === undefined && seed.freeTier !== undefined) prov.freeTier = seed.freeTier;
if (prov.modelSuffixBracketStrip === undefined && seed.modelSuffixBracketStrip !== undefined) prov.modelSuffixBracketStrip = seed.modelSuffixBracketStrip;
Expand Down
8 changes: 6 additions & 2 deletions src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,10 @@ export interface ProviderRegistryEntry {
autoToolChoiceOnlyModels?: string[];
preserveReasoningContentModels?: string[];
requiresReasoningPlaceholderModels?: string[];
/**
* Opt this provider into visible thinking (see OcxProviderConfig.showThinkingSummary).
*/
showThinkingSummary?: boolean;
reasoningSplitModels?: string[];
reasoningDetailsModels?: string[];
thinkingToggleModels?: string[];
Expand All @@ -380,7 +384,7 @@ export type ProviderConfigSeed = Pick<
| "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens"
| "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "reasoningWireFormat"
| "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance"
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance" | "showThinkingSummary"
| "googleMode" | "project" | "location" | "headers"
>;

Expand Down Expand Up @@ -2112,7 +2116,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
// path must stay RELATIVE: this row sets `allowBaseUrlOverride`, and an absolute `url` would
// retarget a user's custom base back to Google. A leading `./` is required because a bare
// `v1internal:` reads as a URL scheme and `providerModelDiscoverySpecError` rejects it.
{ id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"], modelDiscovery: { path: "./v1internal:fetchAvailableModels" } },
{ id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", showThinkingSummary: true, jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"], modelDiscovery: { path: "./v1internal:fetchAvailableModels" } },
{ id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
{ id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
{ id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
Expand Down
7 changes: 7 additions & 0 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,13 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined
? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent }
: {}),
// The request path resolves through routedProviderConfig() and never calls
// enrichProviderFromRegistry(), so a saved provider row written before the
// registry learned this flag must be backfilled here or route.provider never
// carries it and the showThinkingSummary opt-in stays dead.
...(provider.showThinkingSummary === undefined && registryEntry.showThinkingSummary !== undefined
? { showThinkingSummary: registryEntry.showThinkingSummary }
: {}),
// Registry-only client-facing repair policy (#938): fill only when the
// saved provider has no explicit policy; clone so runtime never aliases
// the registry constant.
Expand Down
1 change: 1 addition & 0 deletions src/server/auth-cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
autoToolChoiceOnlyModels: "editor",
preserveReasoningContentModels: "editor",
requiresReasoningPlaceholderModels: "editor",
showThinkingSummary: "editor",
retryOn429: "editor",
transientRetryOn5xx: "editor",
reasoningSplitModels: "editor",
Expand Down
24 changes: 24 additions & 0 deletions src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2473,6 +2473,20 @@ async function resolveSubagentFallbackModelEligibility(args: {
};
}

/**
* Whether the client explicitly asked for hidden thinking (`reasoning.summary: "none"`).
*
* Pinned: parseRequest collapses "omitted" and "none" into one hideThinkingSummary
* flag, so the raw request body is the ONLY place that still distinguishes them.
* Provider opt-ins like showThinkingSummary must consult this — never the flag
* alone — or a future caller that copies only the flag would silently unlock an
* explicit opt-out.
*/
function clientExplicitlyHidThinking(parsed: OcxParsedRequest): boolean {
const rawReasoning = (parsed._rawBody as { reasoning?: { summary?: unknown } } | undefined)?.reasoning;
return typeof rawReasoning === "object" && rawReasoning !== null
&& (rawReasoning as { summary?: unknown }).summary === "none";
}
/**
* Apply every route-dependent request mutation against the final selected route.
* Must run only after subagent fallback has settled the model/provider.
Expand Down Expand Up @@ -2514,6 +2528,16 @@ async function applyFinalRouteRequestNormalization(args: {
// this request will actually use (#404).
route.provider = resolveOpenCodeGoTransport(route.provider, getOrAllocateRequestSessionLane(req));
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
// Provider-opted visible thinking (e.g. google-antigravity): parseRequest hides thinking
// whenever the client omits reasoning.summary, which is the Codex default. A provider that
// serves genuine user-facing reasoning opts back into visible reasoning here, so thought
// parts (Gemini thought, content-channel reasoning_text) reach the client instead of only
// the hidden replay envelopes. Which channel carries them is the bridge's decision, not
// this flag's. An explicit client reasoning.summary "none" still wins.
if (route.provider.showThinkingSummary === true && parsed.options.hideThinkingSummary === true
&& !clientExplicitlyHidThinking(parsed)) {
parsed.options.hideThinkingSummary = false;
}
if (preserveAnthropicResponseModel) parsed._responseModelId = responseModelId;
logCtx.model = route.modelId;
logCtx.provider = route.providerName;
Expand Down
9 changes: 9 additions & 0 deletions src/types/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,15 @@ export interface OcxProviderConfig {
* out explicitly (e.g. MiniMax, where low effort disables thinking).
*/
requiresReasoningPlaceholderModels?: string[];
/**
* Opt-in: surface upstream thinking as visible reasoning even when the client did not
* send `reasoning.summary`. parseRequest hides thinking by default (Codex omits the
* field), which strands genuine reasoning — e.g. Gemini `thought` parts on the
* google-antigravity (Cloud Code Assist) wire — in hidden replay envelopes. An explicit
* client `reasoning.summary: "none"` still wins. Which channel carries the visible text
* is the bridge's decision, not this flag's. Set `false` to opt a seeded preset back out.
*/
showThinkingSummary?: boolean;
/**
* Opt-in same-target 429 retry policy. Codex itself never retries 429 (it retries 5xx only,
* openai/codex#30471), and single-key pools have no failover, so the proxy waits and replays
Expand Down
Loading
Loading