From 0621dc849b80eeacea44c653726e66f1852e7388 Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 9 Jul 2026 13:17:15 -0500 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20GPT-5.6=20Sol/?= =?UTF-8?q?Terra/Luna=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds OpenAI's GPT-5.6 preview family (previewed June 26, 2026): - Known-model registry entries with sol/terra/luna aliases (not warmed; bare gpt alias stays on gpt-5.5 until broad availability) - Token metadata with published preview pricing (Sol $5/$30, Terra $2.50/$15, Luna $1/$6 per 1M) incl. 90%-off cache reads and 1.25x cache writes; context window mirrors GPT-5.5 pending official specs - Thinking policy: 5 levels including xhigh (same as gpt-5.5) - Codex OAuth allowlist entries (preview ships via API and Codex) - Regenerated docs model table + built-in skill snapshot --- _Generated with `mux` • Model: `anthropic:claude-fable-5` • Thinking: `high` • Cost: `$`_ --- docs/config/models.mdx | 3 ++ src/common/constants/codexOAuth.ts | 4 ++ src/common/constants/knownModels.ts | 28 ++++++++++ src/common/utils/thinking/policy.test.ts | 23 ++++++++ src/common/utils/thinking/policy.ts | 12 +++-- src/common/utils/tokens/models-extra.ts | 54 +++++++++++++++++++ .../builtInSkillContent.generated.ts | 3 ++ 7 files changed, 124 insertions(+), 3 deletions(-) diff --git a/docs/config/models.mdx b/docs/config/models.mdx index 98f9c99733..d5491b9b2b 100644 --- a/docs/config/models.mdx +++ b/docs/config/models.mdx @@ -20,6 +20,9 @@ Mux ships with curated models kept up to date with the frontier. Use any custom | Haiku 4.5 | anthropic:claude-haiku-4-5 | `haiku` | | | GPT-5.5 | openai:gpt-5.5 | `gpt`, `gpt-5.5` | | | GPT-5.5 Pro | openai:gpt-5.5-pro | `gpt-pro`, `gpt-5.5-pro` | | +| GPT-5.6 Sol | openai:gpt-5.6-sol | `sol`, `gpt-5.6-sol` | | +| GPT-5.6 Terra | openai:gpt-5.6-terra | `terra`, `gpt-5.6-terra` | | +| GPT-5.6 Luna | openai:gpt-5.6-luna | `luna`, `gpt-5.6-luna` | | | GPT-5.4 Mini | openai:gpt-5.4-mini | `gpt-mini` | | | GPT-5.4 Nano | openai:gpt-5.4-nano | `gpt-nano` | | | Codex 5.3 | openai:gpt-5.3-codex | `codex`, `codex-5.3` | | diff --git a/src/common/constants/codexOAuth.ts b/src/common/constants/codexOAuth.ts index 25f8b71763..fbda3ae218 100644 --- a/src/common/constants/codexOAuth.ts +++ b/src/common/constants/codexOAuth.ts @@ -94,6 +94,10 @@ export const CODEX_OAUTH_ALLOWED_MODELS = new Set([ "gpt-5.2", "gpt-5.4-mini", "gpt-5.5", + // GPT-5.6 preview tiers are available "through the API and Codex" for approved orgs. + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark", diff --git a/src/common/constants/knownModels.ts b/src/common/constants/knownModels.ts index 8789161631..d1f94219f0 100644 --- a/src/common/constants/knownModels.ts +++ b/src/common/constants/knownModels.ts @@ -101,6 +101,34 @@ const MODEL_DEFINITIONS = { warm: true, tokenizerOverride: "openai/gpt-5", }, + // GPT-5.6 family - previewed June 26, 2026. New naming system: the number is the + // generation, while Sol/Terra/Luna are durable capability tiers. Limited preview via + // the API and Codex for approved orgs only (not in ChatGPT); GA planned "in coming + // weeks". Cache writes bill at 1.25x input; cache reads keep the 90% discount. + // Not warmed and bare `gpt` alias stays on gpt-5.5 until broad availability + // (mirrors the MYTHOS precedent for restricted-access models). + // GPT-5.6 Sol - flagship tier. $5/M input, $30/M output. + GPT_56_SOL: { + provider: "openai", + providerModelId: "gpt-5.6-sol", + aliases: ["sol", "gpt-5.6-sol"], + tokenizerOverride: "openai/gpt-5", + }, + // GPT-5.6 Terra - balanced tier; GPT-5.5-competitive at half the cost. + // $2.50/M input, $15/M output. + GPT_56_TERRA: { + provider: "openai", + providerModelId: "gpt-5.6-terra", + aliases: ["terra", "gpt-5.6-terra"], + tokenizerOverride: "openai/gpt-5", + }, + // GPT-5.6 Luna - fastest/cheapest tier. $1/M input, $6/M output. + GPT_56_LUNA: { + provider: "openai", + providerModelId: "gpt-5.6-luna", + aliases: ["luna", "gpt-5.6-luna"], + tokenizerOverride: "openai/gpt-5", + }, // GPT Mini alias tracks the latest stable GPT-5 mini tier. GPT_54_MINI: { provider: "openai", diff --git a/src/common/utils/thinking/policy.test.ts b/src/common/utils/thinking/policy.test.ts index 4a6fee5eb7..eec25b58a2 100644 --- a/src/common/utils/thinking/policy.test.ts +++ b/src/common/utils/thinking/policy.test.ts @@ -186,6 +186,29 @@ describe("getThinkingPolicyForModel", () => { ]); }); + test.each(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"])( + "returns 5 levels including xhigh for %s", + (modelId) => { + expect(getThinkingPolicyForModel(`openai:${modelId}`)).toEqual([ + "off", + "low", + "medium", + "high", + "xhigh", + ]); + } + ); + + test("returns 5 levels including xhigh for gpt-5.6-sol behind mux-gateway with version suffix", () => { + expect(getThinkingPolicyForModel("mux-gateway:openai/gpt-5.6-sol-2026-06-26")).toEqual([ + "off", + "low", + "medium", + "high", + "xhigh", + ]); + }); + test("returns 5 levels including xhigh for gpt-5.5", () => { expect(getThinkingPolicyForModel("openai:gpt-5.5")).toEqual([ "off", diff --git a/src/common/utils/thinking/policy.ts b/src/common/utils/thinking/policy.ts index 7d47db9ea5..46b46efb4e 100644 --- a/src/common/utils/thinking/policy.ts +++ b/src/common/utils/thinking/policy.ts @@ -53,7 +53,8 @@ export function isGeminiFlashThinkingLevelModelName(modelName: string): boolean * - openai:gpt-5.2-codex → ["off", "low", "medium", "high", "xhigh"] (5 levels including xhigh) * - openai:gpt-5.3-codex / Spark variants → * ["off", "low", "medium", "high", "xhigh"] (5 levels including xhigh) - * - openai:gpt-5.2 / openai:gpt-5.5 → ["off", "low", "medium", "high", "xhigh"] + * - openai:gpt-5.2 / openai:gpt-5.5 / gpt-5.6 tiers (sol/terra/luna) → + * ["off", "low", "medium", "high", "xhigh"] * - openai:gpt-5.2-pro / openai:gpt-5.5-pro → ["medium", "high", "xhigh"] (3 levels) * - openai:gpt-5-pro → ["high"] (only supported level, legacy) * - Gemini Flash chat variants → ["off", "low", "medium", "high"] @@ -125,10 +126,15 @@ function getExplicitThinkingPolicy(modelString: string): ThinkingPolicy | null { return ["medium", "high", "xhigh"]; } - // gpt-5.2, gpt-5.5 and the gpt-5.4-mini / gpt-5.4-nano variants support 5 reasoning levels including xhigh. + // gpt-5.2, gpt-5.5, the gpt-5.4-mini / gpt-5.4-nano variants, and the gpt-5.6 tiers + // (sol/terra/luna) support 5 reasoning levels including xhigh. + // Note: preview reports say gpt-5.6-sol adds a native "max" effort; we keep it at + // xhigh until OpenAI publishes official reasoning-level docs (OPENAI_REASONING_EFFORT + // maps our "max" to "xhigh" anyway, so nothing would be sent differently today). if ( /^gpt-5\.2(?!-[a-z])/.test(withoutProviderNamespace) || - /^gpt-5\.(?:4|5)(?:-(?:mini|nano))?(?!-[a-z])/.test(withoutProviderNamespace) + /^gpt-5\.(?:4|5)(?:-(?:mini|nano))?(?!-[a-z])/.test(withoutProviderNamespace) || + /^gpt-5\.6-(?:sol|terra|luna)(?!-[a-z])/.test(withoutProviderNamespace) ) { return ["off", "low", "medium", "high", "xhigh"]; } diff --git a/src/common/utils/tokens/models-extra.ts b/src/common/utils/tokens/models-extra.ts index 09867e3842..10989074ef 100644 --- a/src/common/utils/tokens/models-extra.ts +++ b/src/common/utils/tokens/models-extra.ts @@ -261,6 +261,60 @@ export const modelsExtra: Record = { supported_endpoints: ["/v1/responses"], }, + // GPT-5.6 family (Sol / Terra / Luna) - Previewed June 26, 2026 + // Limited preview via the API and Codex; GA planned "in coming weeks". + // Published preview pricing per 1M tokens: Sol $5/$30, Terra $2.50/$15, Luna $1/$6. + // Prompt caching: cache reads keep the 90% discount; cache writes for GPT-5.6+ bill + // at 1.25x the uncached input rate (with explicit breakpoints and a 30-min minimum + // cache life). No long-context tiered pricing has been published for this family. + // OpenAI has not published official context specs yet; we mirror GPT-5.5's window + // (1.05M in / 128K out) as a provisional value until the model card lands. + // (Early preview reports suggest Sol may be larger, ~1.5M, but that's unconfirmed.) + "gpt-5.6-sol": { + max_input_tokens: 1050000, + max_output_tokens: 128000, + input_cost_per_token: 0.000005, // $5 per million input tokens + output_cost_per_token: 0.00003, // $30 per million output tokens + cache_read_input_token_cost: 0.0000005, // $0.50 per million cached input tokens (90% off) + cache_creation_input_token_cost: 0.00000625, // $6.25 per million tokens (1.25x input) + litellm_provider: "openai", + mode: "chat", + supports_function_calling: true, + supports_vision: true, + supports_reasoning: true, + supports_response_schema: true, + }, + + "gpt-5.6-terra": { + max_input_tokens: 1050000, + max_output_tokens: 128000, + input_cost_per_token: 0.0000025, // $2.50 per million input tokens + output_cost_per_token: 0.000015, // $15 per million output tokens + cache_read_input_token_cost: 0.00000025, // $0.25 per million cached input tokens (90% off) + cache_creation_input_token_cost: 0.000003125, // $3.125 per million tokens (1.25x input) + litellm_provider: "openai", + mode: "chat", + supports_function_calling: true, + supports_vision: true, + supports_reasoning: true, + supports_response_schema: true, + }, + + "gpt-5.6-luna": { + max_input_tokens: 1050000, + max_output_tokens: 128000, + input_cost_per_token: 0.000001, // $1 per million input tokens + output_cost_per_token: 0.000006, // $6 per million output tokens + cache_read_input_token_cost: 0.0000001, // $0.10 per million cached input tokens (90% off) + cache_creation_input_token_cost: 0.00000125, // $1.25 per million tokens (1.25x input) + litellm_provider: "openai", + mode: "chat", + supports_function_calling: true, + supports_vision: true, + supports_reasoning: true, + supports_response_schema: true, + }, + // GPT-5.4 mini - Released March 11, 2026 // Smaller/faster gpt-5.4-mini tier with 400K context, 128K max output, and published // pricing of $0.75/M input, $4.50/M output, and $0.075/M cached input. diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index 412815d093..ac94d4998d 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -3099,6 +3099,9 @@ export const BUILTIN_SKILL_FILES: Record> = { "| Haiku 4.5 | anthropic:claude-haiku-4-5 | `haiku` | |", "| GPT-5.5 | openai:gpt-5.5 | `gpt`, `gpt-5.5` | |", "| GPT-5.5 Pro | openai:gpt-5.5-pro | `gpt-pro`, `gpt-5.5-pro` | |", + "| GPT-5.6 Sol | openai:gpt-5.6-sol | `sol`, `gpt-5.6-sol` | |", + "| GPT-5.6 Terra | openai:gpt-5.6-terra | `terra`, `gpt-5.6-terra` | |", + "| GPT-5.6 Luna | openai:gpt-5.6-luna | `luna`, `gpt-5.6-luna` | |", "| GPT-5.4 Mini | openai:gpt-5.4-mini | `gpt-mini` | |", "| GPT-5.4 Nano | openai:gpt-5.4-nano | `gpt-nano` | |", "| Codex 5.3 | openai:gpt-5.3-codex | `codex`, `codex-5.3` | |", From 52537dae866401155737f106ad9779c6b2d4f34b Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 9 Jul 2026 13:34:06 -0500 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=A4=96=20feat:=20point=20bare=20gpt?= =?UTF-8?q?=20alias=20at=20gpt-5.6-sol=20(GA=20verified)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GPT-5.6 went GA July 9, 2026. Verified via live API: all three tiers appear in /v1/models and complete Responses API calls with a plain API key. Sol succeeds gpt-5.5 as the flagship at the same list price, so the bare `gpt` alias moves there; gpt-5.5 stays selectable via its explicit id alias. Sol is now warmed. --- _Generated with `mux` • Model: `anthropic:claude-fable-5` • Thinking: `high` • Cost: `$10.12`_ --- docs/config/models.mdx | 4 ++-- src/common/constants/codexOAuth.ts | 2 +- src/common/constants/knownModels.ts | 21 ++++++++++--------- src/common/utils/tokens/models-extra.ts | 11 +++++----- .../builtInSkillContent.generated.ts | 4 ++-- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/config/models.mdx b/docs/config/models.mdx index d5491b9b2b..18767e6176 100644 --- a/docs/config/models.mdx +++ b/docs/config/models.mdx @@ -18,9 +18,9 @@ Mux ships with curated models kept up to date with the frontier. Use any custom | Opus 4.8 | anthropic:claude-opus-4-8 | `opus` | ✓ | | Sonnet 5 | anthropic:claude-sonnet-5 | `sonnet` | | | Haiku 4.5 | anthropic:claude-haiku-4-5 | `haiku` | | -| GPT-5.5 | openai:gpt-5.5 | `gpt`, `gpt-5.5` | | +| GPT-5.5 | openai:gpt-5.5 | `gpt-5.5` | | | GPT-5.5 Pro | openai:gpt-5.5-pro | `gpt-pro`, `gpt-5.5-pro` | | -| GPT-5.6 Sol | openai:gpt-5.6-sol | `sol`, `gpt-5.6-sol` | | +| GPT-5.6 Sol | openai:gpt-5.6-sol | `gpt`, `sol`, `gpt-5.6-sol` | | | GPT-5.6 Terra | openai:gpt-5.6-terra | `terra`, `gpt-5.6-terra` | | | GPT-5.6 Luna | openai:gpt-5.6-luna | `luna`, `gpt-5.6-luna` | | | GPT-5.4 Mini | openai:gpt-5.4-mini | `gpt-mini` | | diff --git a/src/common/constants/codexOAuth.ts b/src/common/constants/codexOAuth.ts index fbda3ae218..0583cf16a9 100644 --- a/src/common/constants/codexOAuth.ts +++ b/src/common/constants/codexOAuth.ts @@ -94,7 +94,7 @@ export const CODEX_OAUTH_ALLOWED_MODELS = new Set([ "gpt-5.2", "gpt-5.4-mini", "gpt-5.5", - // GPT-5.6 preview tiers are available "through the API and Codex" for approved orgs. + // GPT-5.6 tiers ship in both Codex and the public API (GA July 9, 2026). "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", diff --git a/src/common/constants/knownModels.ts b/src/common/constants/knownModels.ts index d1f94219f0..186031cf60 100644 --- a/src/common/constants/knownModels.ts +++ b/src/common/constants/knownModels.ts @@ -85,11 +85,12 @@ const MODEL_DEFINITIONS = { aliases: ["haiku"], tokenizerOverride: "anthropic/claude-3.5-haiku", }, - // GPT alias tracks the latest stable GPT-5 tier. + // Previous flagship tier; kept selectable via its explicit id alias. + // The bare `gpt` alias moved to GPT_56_SOL when GPT-5.6 went GA (July 9, 2026). GPT: { provider: "openai", providerModelId: "gpt-5.5", - aliases: ["gpt", "gpt-5.5"], + aliases: ["gpt-5.5"], warm: true, tokenizerOverride: "openai/gpt-5", }, @@ -101,17 +102,17 @@ const MODEL_DEFINITIONS = { warm: true, tokenizerOverride: "openai/gpt-5", }, - // GPT-5.6 family - previewed June 26, 2026. New naming system: the number is the - // generation, while Sol/Terra/Luna are durable capability tiers. Limited preview via - // the API and Codex for approved orgs only (not in ChatGPT); GA planned "in coming - // weeks". Cache writes bill at 1.25x input; cache reads keep the 90% discount. - // Not warmed and bare `gpt` alias stays on gpt-5.5 until broad availability - // (mirrors the MYTHOS precedent for restricted-access models). - // GPT-5.6 Sol - flagship tier. $5/M input, $30/M output. + // GPT-5.6 family - previewed June 26, 2026; generally available July 9, 2026 across + // ChatGPT, Codex, and the API (verified callable via plain API keys). New naming + // system: the number is the generation, while Sol/Terra/Luna are durable capability + // tiers. Cache writes bill at 1.25x input; cache reads keep the 90% discount. + // GPT-5.6 Sol - flagship tier. $5/M input, $30/M output. Bare `gpt` alias lives here: + // Sol succeeds gpt-5.5 as the flagship at the same list price. GPT_56_SOL: { provider: "openai", providerModelId: "gpt-5.6-sol", - aliases: ["sol", "gpt-5.6-sol"], + aliases: ["gpt", "sol", "gpt-5.6-sol"], + warm: true, tokenizerOverride: "openai/gpt-5", }, // GPT-5.6 Terra - balanced tier; GPT-5.5-competitive at half the cost. diff --git a/src/common/utils/tokens/models-extra.ts b/src/common/utils/tokens/models-extra.ts index 10989074ef..2b38363eeb 100644 --- a/src/common/utils/tokens/models-extra.ts +++ b/src/common/utils/tokens/models-extra.ts @@ -261,15 +261,14 @@ export const modelsExtra: Record = { supported_endpoints: ["/v1/responses"], }, - // GPT-5.6 family (Sol / Terra / Luna) - Previewed June 26, 2026 - // Limited preview via the API and Codex; GA planned "in coming weeks". - // Published preview pricing per 1M tokens: Sol $5/$30, Terra $2.50/$15, Luna $1/$6. + // GPT-5.6 family (Sol / Terra / Luna) - Previewed June 26, 2026; GA July 9, 2026. + // Published pricing per 1M tokens: Sol $5/$30, Terra $2.50/$15, Luna $1/$6. // Prompt caching: cache reads keep the 90% discount; cache writes for GPT-5.6+ bill // at 1.25x the uncached input rate (with explicit breakpoints and a 30-min minimum // cache life). No long-context tiered pricing has been published for this family. - // OpenAI has not published official context specs yet; we mirror GPT-5.5's window - // (1.05M in / 128K out) as a provisional value until the model card lands. - // (Early preview reports suggest Sol may be larger, ~1.5M, but that's unconfirmed.) + // OpenAI has not published official context specs in the GA announcement; we mirror + // GPT-5.5's window (1.05M in / 128K out) as a provisional value until the model card + // lands. (Preview reports suggest Sol may be larger, ~1.5M, but that's unconfirmed.) "gpt-5.6-sol": { max_input_tokens: 1050000, max_output_tokens: 128000, diff --git a/src/node/services/agentSkills/builtInSkillContent.generated.ts b/src/node/services/agentSkills/builtInSkillContent.generated.ts index ac94d4998d..65383840cb 100644 --- a/src/node/services/agentSkills/builtInSkillContent.generated.ts +++ b/src/node/services/agentSkills/builtInSkillContent.generated.ts @@ -3097,9 +3097,9 @@ export const BUILTIN_SKILL_FILES: Record> = { "| Opus 4.8 | anthropic:claude-opus-4-8 | `opus` | ✓ |", "| Sonnet 5 | anthropic:claude-sonnet-5 | `sonnet` | |", "| Haiku 4.5 | anthropic:claude-haiku-4-5 | `haiku` | |", - "| GPT-5.5 | openai:gpt-5.5 | `gpt`, `gpt-5.5` | |", + "| GPT-5.5 | openai:gpt-5.5 | `gpt-5.5` | |", "| GPT-5.5 Pro | openai:gpt-5.5-pro | `gpt-pro`, `gpt-5.5-pro` | |", - "| GPT-5.6 Sol | openai:gpt-5.6-sol | `sol`, `gpt-5.6-sol` | |", + "| GPT-5.6 Sol | openai:gpt-5.6-sol | `gpt`, `sol`, `gpt-5.6-sol` | |", "| GPT-5.6 Terra | openai:gpt-5.6-terra | `terra`, `gpt-5.6-terra` | |", "| GPT-5.6 Luna | openai:gpt-5.6-luna | `luna`, `gpt-5.6-luna` | |", "| GPT-5.4 Mini | openai:gpt-5.4-mini | `gpt-mini` | |", From 991009ec037226883e4fe5fa46de1a877e7f5e02 Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 9 Jul 2026 14:07:37 -0500 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=A4=96=20feat:=20support=20GPT-5.6=20?= =?UTF-8?q?native=20max=20effort=20and=20pro=20reasoning=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per OpenAI's GA docs, GPT-5.6 adds two capabilities: 1. Native "max" reasoning effort for demanding tasks that need more exploration and verification. The thinking policy for GPT-5.6 tiers now exposes all 6 levels; on the Responses API we send "max" through (the SDK schema accepts arbitrary effort strings), while the Chat Completions path keeps the shared max→xhigh clamp (its enum rejects "max"). 2. reasoning.mode: "pro" — more model work for reliability on difficult tasks, returning a single final answer, for when quality matters more than latency and token usage. The SDK cannot send reasoning.mode, so it rides a Mux-internal header (mirroring the Anthropic xhigh effort rewrite) that the OpenAI fetch wrapper consumes: strips the header and injects reasoning.mode into the Responses body on the wire. Enable via muxProviderOptions.openai.reasoningMode or providers.jsonc openai reasoningMode: "pro"; gated per-model to the GPT-5.6 family, direct OpenAI routes, and the Responses wire format. Live-verified against the API: effort "max" and reasoning.mode "pro" complete on gpt-5.6-sol; gpt-5.5 rejects "max" (confirms gating). --- _Generated with `mux` • Model: `anthropic:claude-fable-5` • Thinking: `high` • Cost: `$14.25`_ --- src/common/config/schemas/providersConfig.ts | 3 + src/common/constants/knownModels.ts | 2 + src/common/schemas/providerOptions.ts | 8 ++ src/common/types/thinking.ts | 33 ++++++ src/common/utils/ai/providerOptions.test.ts | 106 ++++++++++++++++++ src/common/utils/ai/providerOptions.ts | 44 +++++++- src/common/utils/thinking/policy.test.ts | 6 +- src/common/utils/thinking/policy.ts | 23 ++-- .../services/providerModelFactory.test.ts | 87 +++++++++++++- src/node/services/providerModelFactory.ts | 55 ++++++++- 10 files changed, 352 insertions(+), 15 deletions(-) diff --git a/src/common/config/schemas/providersConfig.ts b/src/common/config/schemas/providersConfig.ts index beed8dfb43..5834f0001c 100644 --- a/src/common/config/schemas/providersConfig.ts +++ b/src/common/config/schemas/providersConfig.ts @@ -30,6 +30,9 @@ export const AnthropicProviderConfigSchema = BaseProviderConfigSchema.extend({ export const OpenAIProviderConfigSchema = BaseProviderConfigSchema.extend({ serviceTier: ServiceTierSchema.optional(), + // GPT-5.6 pro mode (reasoning.mode: "pro"). Provider-level intent; requests are + // still gated per-model to the GPT-5.6 family in buildRequestHeaders. + reasoningMode: z.enum(["default", "pro"]).optional(), organization: z.string().optional(), codexOauthDefaultAuth: CodexOauthDefaultAuthSchema.optional(), codexOauth: z.record(z.string(), z.unknown()).optional(), diff --git a/src/common/constants/knownModels.ts b/src/common/constants/knownModels.ts index 186031cf60..75e6283edc 100644 --- a/src/common/constants/knownModels.ts +++ b/src/common/constants/knownModels.ts @@ -106,6 +106,8 @@ const MODEL_DEFINITIONS = { // ChatGPT, Codex, and the API (verified callable via plain API keys). New naming // system: the number is the generation, while Sol/Terra/Luna are durable capability // tiers. Cache writes bill at 1.25x input; cache reads keep the 90% discount. + // All tiers support a native "max" reasoning effort (see the thinking policy) and + // Responses API `reasoning.mode: "pro"` (see MUX_OPENAI_REASONING_MODE_HEADER). // GPT-5.6 Sol - flagship tier. $5/M input, $30/M output. Bare `gpt` alias lives here: // Sol succeeds gpt-5.5 as the flagship at the same list price. GPT_56_SOL: { diff --git a/src/common/schemas/providerOptions.ts b/src/common/schemas/providerOptions.ts index 9dbe888bc9..67f5e4b12d 100644 --- a/src/common/schemas/providerOptions.ts +++ b/src/common/schemas/providerOptions.ts @@ -42,6 +42,14 @@ export const MuxProviderOptionsSchema = z.object({ store: z.boolean().optional().meta({ description: "Whether OpenAI stores responses. Set false for zero data retention (ZDR).", }), + reasoningMode: z + .enum(["default", "pro"]) + .optional() + .meta({ + description: + 'OpenAI reasoning mode: "pro" performs more model work for reliability on difficult ' + + "tasks (GPT-5.6 family, Responses API only); default/unset uses standard reasoning", + }), forceContextLimitError: z.boolean().optional().meta({ description: "Force context limit error (used in integration tests to simulate overflow)", }), diff --git a/src/common/types/thinking.ts b/src/common/types/thinking.ts index 0ceb2b1d86..ed94f80fa2 100644 --- a/src/common/types/thinking.ts +++ b/src/common/types/thinking.ts @@ -237,6 +237,36 @@ export function anthropicSupportsNativeXhigh(modelString: string): boolean { ); } +/** + * GPT-5.6 family (Sol / Terra / Luna), including version-suffixed ids like + * `gpt-5.6-sol-2026-07-09`. Kept as one predicate so the two GPT-5.6-only + * capabilities below stay in sync. + */ +const GPT_56_FAMILY_REGEX = /^gpt-5\.6-(?:sol|terra|luna)(?!-[a-z])/; + +/** + * Whether the given OpenAI model supports the native "max" reasoning effort + * (GPT-5.6 family, GA July 9, 2026). OpenAI recommends max effort "for demanding + * tasks that need more exploration and verification". + * + * Only valid on the Responses API path: the @ai-sdk/openai Responses schema + * accepts arbitrary effort strings, but the Chat Completions schema rejects + * anything outside its enum — callers must clamp to "xhigh" there. + */ +export function openaiSupportsNativeMaxEffort(modelString: string): boolean { + return GPT_56_FAMILY_REGEX.test(stripModelProviderPrefixes(modelString)); +} + +/** + * Whether the given OpenAI model supports `reasoning.mode: "pro"` on the + * Responses API (GPT-5.6 family). Pro mode performs more model work to improve + * reliability on difficult tasks and returns a single final answer — intended + * for when quality matters more than latency and token usage. + */ +export function openaiSupportsProReasoningMode(modelString: string): boolean { + return GPT_56_FAMILY_REGEX.test(stripModelProviderPrefixes(modelString)); +} + /** * Whether the given Anthropic model rejects `thinking: { type: "disabled" }`. * @@ -276,6 +306,9 @@ export const OPENAI_REASONING_EFFORT: Record medium: "medium", high: "high", xhigh: "xhigh", // Maps 1:1 to OpenAI's reasoning effort value + // Shared clamp for models without a native max effort. GPT-5.6 (see + // openaiSupportsNativeMaxEffort) overrides this to "max" in buildProviderOptions + // on the Responses API path. max: "xhigh", }; diff --git a/src/common/utils/ai/providerOptions.test.ts b/src/common/utils/ai/providerOptions.test.ts index fc7d08385d..651113ea7d 100644 --- a/src/common/utils/ai/providerOptions.test.ts +++ b/src/common/utils/ai/providerOptions.test.ts @@ -14,6 +14,7 @@ import { resolveProviderOptionsNamespaceKey, ANTHROPIC_1M_CONTEXT_HEADER, MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER, + MUX_OPENAI_REASONING_MODE_HEADER, MUX_WORKSPACE_ID_HEADER, } from "./providerOptions"; @@ -415,6 +416,45 @@ describe("buildProviderOptions - OpenAI", () => { expect(openai!.parallelToolCalls).toBe(true); }); + describe("GPT-5.6 native max reasoning effort", () => { + test.each(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"])( + "sends native max effort for %s on the Responses API", + (modelId) => { + const result = buildProviderOptions(`openai:${modelId}`, "max"); + const openai = getOpenAIOptions(result); + + expect(openai).toBeDefined(); + expect(openai!.reasoningEffort).toBe("max"); + } + ); + + test("clamps max to xhigh on the Chat Completions wire format", () => { + const result = buildProviderOptions("openai:gpt-5.6-sol", "max", undefined, undefined, { + openai: { wireFormat: "chatCompletions" }, + }); + const openai = getOpenAIOptions(result); + + expect(openai).toBeDefined(); + expect(openai!.reasoningEffort).toBe("xhigh"); + }); + + test("keeps the max→xhigh clamp for non-GPT-5.6 models", () => { + const result = buildProviderOptions("openai:gpt-5.5", "max"); + const openai = getOpenAIOptions(result); + + expect(openai).toBeDefined(); + expect(openai!.reasoningEffort).toBe("xhigh"); + }); + + test("non-max levels stay on the shared mapping for GPT-5.6", () => { + const result = buildProviderOptions("openai:gpt-5.6-sol", "high"); + const openai = getOpenAIOptions(result); + + expect(openai).toBeDefined(); + expect(openai!.reasoningEffort).toBe("high"); + }); + }); + describe("store option", () => { test("should include store: false when muxProviderOptions sets store to false", () => { const result = buildProviderOptions("openai:gpt-5", "medium", undefined, undefined, { @@ -1044,6 +1084,72 @@ describe("buildRequestHeaders", () => { } }); + describe("GPT-5.6 pro reasoning mode header", () => { + for (const { name, model, options, routeProvider, expected } of [ + { + name: "emits pro mode header for gpt-5.6-sol with reasoningMode pro", + model: "openai:gpt-5.6-sol", + options: { openai: { reasoningMode: "pro" } }, + routeProvider: undefined, + expected: { [MUX_OPENAI_REASONING_MODE_HEADER]: "pro" }, + }, + { + name: "emits pro mode header for version-suffixed gpt-5.6-terra", + model: "openai:gpt-5.6-terra-2026-07-09", + options: { openai: { reasoningMode: "pro" } }, + routeProvider: "openai", + expected: { [MUX_OPENAI_REASONING_MODE_HEADER]: "pro" }, + }, + { + name: "does not emit header for non-GPT-5.6 model even with reasoningMode pro", + model: "openai:gpt-5.5", + options: { openai: { reasoningMode: "pro" } }, + routeProvider: undefined, + expected: undefined, + }, + { + name: "does not emit header when reasoningMode is default", + model: "openai:gpt-5.6-sol", + options: { openai: { reasoningMode: "default" } }, + routeProvider: undefined, + expected: undefined, + }, + { + name: "does not emit header when reasoningMode is unset", + model: "openai:gpt-5.6-sol", + options: { openai: {} }, + routeProvider: undefined, + expected: undefined, + }, + { + name: "does not emit header on the Chat Completions wire format", + model: "openai:gpt-5.6-sol", + options: { openai: { reasoningMode: "pro", wireFormat: "chatCompletions" } }, + routeProvider: undefined, + expected: undefined, + }, + { + name: "does not emit header for gateway-routed GPT-5.6 (wrapper not applied there)", + model: "mux-gateway:openai/gpt-5.6-sol", + options: { openai: { reasoningMode: "pro" } }, + routeProvider: "mux-gateway", + expected: undefined, + }, + ] as const) { + test(name, () => { + expect( + buildRequestHeaders( + model, + options as Parameters[1], + undefined, + undefined, + routeProvider + ) + ).toEqual(expected); + }); + } + }); + for (const { name, model, options, workspaceId, expected } of [ { name: "should include X-Mux-Workspace-Id for non-Anthropic provider when workspaceId provided", diff --git a/src/common/utils/ai/providerOptions.ts b/src/common/utils/ai/providerOptions.ts index 35b38dbeb5..164010f697 100644 --- a/src/common/utils/ai/providerOptions.ts +++ b/src/common/utils/ai/providerOptions.ts @@ -23,6 +23,8 @@ import { GEMINI_THINKING_BUDGETS, OPENAI_REASONING_EFFORT, OPENROUTER_REASONING_EFFORT, + openaiSupportsNativeMaxEffort, + openaiSupportsProReasoningMode, } from "@/common/types/thinking"; import { isGeminiFlashThinkingLevelModelName } from "@/common/utils/thinking/policy"; import { resolveModelForMetadata } from "@/common/utils/providers/modelEntries"; @@ -39,6 +41,16 @@ import { normalizeToCanonical, supports1MContext } from "./models"; */ export const MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER = "x-mux-anthropic-effort"; +/** + * Mux-internal request header that asks the OpenAI fetch wrapper to inject + * `reasoning.mode` into the Responses API body on the wire. The @ai-sdk/openai + * Responses schema has no mode field (it only builds reasoning.effort/summary), + * so — like the Anthropic effort override above — the option rides a header to + * the fetch wrapper, which rewrites the body and strips the header before the + * request leaves Mux. Value is the reasoning mode to inject (currently "pro"). + */ +export const MUX_OPENAI_REASONING_MODE_HEADER = "x-mux-openai-reasoning-mode"; + /** * OpenRouter reasoning options * @see https://openrouter.ai/docs/use-cases/reasoning-tokens @@ -353,8 +365,6 @@ export function buildProviderOptions( // Build OpenAI-specific options if (formatProvider === "openai") { - const reasoningEffort = OPENAI_REASONING_EFFORT[effectiveThinking]; - // Mux always sends the latest conversation history explicitly. OpenAI's // previous_response_id is an alternative state-management path, not an additive one. // Chaining it on top of explicit history double-counts prior turns and caused GPT-5.4 @@ -374,6 +384,15 @@ export function buildProviderOptions( const truncationMode = openaiTruncationMode ?? "disabled"; const shouldSendReasoningSummary = supportsOpenAIReasoningSummary(capModelName); + // GPT-5.6 supports a native "max" reasoning effort for demanding tasks that need + // more exploration and verification. The Responses schema in @ai-sdk/openai accepts + // arbitrary effort strings, so "max" passes through unchanged; the Chat Completions + // schema enum rejects it, so that path keeps the shared max→"xhigh" clamp. + const reasoningEffort = + effectiveThinking === "max" && isResponses && openaiSupportsNativeMaxEffort(capModelName) + ? "max" + : OPENAI_REASONING_EFFORT[effectiveThinking]; + log.debug("buildProviderOptions: OpenAI config", { reasoningEffort, shouldSendReasoningSummary, @@ -620,5 +639,26 @@ export function buildRequestHeaders( headers[MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER] = "xhigh"; } + // GPT-5.6 pro mode (`reasoning.mode: "pro"`): more model work for reliability on + // difficult tasks, returning a single final answer — for when quality matters more + // than latency and token usage. The SDK cannot send reasoning.mode, so emit a + // Mux-internal header for the OpenAI fetch wrapper to rewrite the body on the wire. + // + // Gates: + // - direct OpenAI route only: unlike the Anthropic wrapper, the OpenAI fetch + // wrapper is not applied on passthrough gateways, so a header emitted there + // would leak to the gateway verbatim instead of being consumed. + // - Responses API only (reasoning.mode does not exist on Chat Completions). + // - GPT-5.6 family only; other models would reject the parameter. + if ( + origin === "openai" && + (routeProvider == null || routeProvider === "openai") && + muxProviderOptions?.openai?.reasoningMode === "pro" && + (muxProviderOptions.openai.wireFormat ?? "responses") === "responses" && + openaiSupportsProReasoningMode(modelString) + ) { + headers[MUX_OPENAI_REASONING_MODE_HEADER] = "pro"; + } + return Object.keys(headers).length > 0 ? headers : undefined; } diff --git a/src/common/utils/thinking/policy.test.ts b/src/common/utils/thinking/policy.test.ts index eec25b58a2..01d6c60ad4 100644 --- a/src/common/utils/thinking/policy.test.ts +++ b/src/common/utils/thinking/policy.test.ts @@ -187,7 +187,7 @@ describe("getThinkingPolicyForModel", () => { }); test.each(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"])( - "returns 5 levels including xhigh for %s", + "returns all 6 levels including native max for %s", (modelId) => { expect(getThinkingPolicyForModel(`openai:${modelId}`)).toEqual([ "off", @@ -195,17 +195,19 @@ describe("getThinkingPolicyForModel", () => { "medium", "high", "xhigh", + "max", ]); } ); - test("returns 5 levels including xhigh for gpt-5.6-sol behind mux-gateway with version suffix", () => { + test("returns all 6 levels for gpt-5.6-sol behind mux-gateway with version suffix", () => { expect(getThinkingPolicyForModel("mux-gateway:openai/gpt-5.6-sol-2026-06-26")).toEqual([ "off", "low", "medium", "high", "xhigh", + "max", ]); }); diff --git a/src/common/utils/thinking/policy.ts b/src/common/utils/thinking/policy.ts index 46b46efb4e..f826f8ef8f 100644 --- a/src/common/utils/thinking/policy.ts +++ b/src/common/utils/thinking/policy.ts @@ -19,6 +19,7 @@ import { THINKING_LEVEL_OFF, anthropicRejectsDisabledThinking, anthropicSupportsNativeXhigh, + openaiSupportsNativeMaxEffort, stripModelProviderPrefixes, type ThinkingLevel, type ParsedThinkingInput, @@ -53,8 +54,8 @@ export function isGeminiFlashThinkingLevelModelName(modelName: string): boolean * - openai:gpt-5.2-codex → ["off", "low", "medium", "high", "xhigh"] (5 levels including xhigh) * - openai:gpt-5.3-codex / Spark variants → * ["off", "low", "medium", "high", "xhigh"] (5 levels including xhigh) - * - openai:gpt-5.2 / openai:gpt-5.5 / gpt-5.6 tiers (sol/terra/luna) → - * ["off", "low", "medium", "high", "xhigh"] + * - openai:gpt-5.2 / openai:gpt-5.5 → ["off", "low", "medium", "high", "xhigh"] + * - gpt-5.6 tiers (sol/terra/luna) → all 6 levels including native "max" * - openai:gpt-5.2-pro / openai:gpt-5.5-pro → ["medium", "high", "xhigh"] (3 levels) * - openai:gpt-5-pro → ["high"] (only supported level, legacy) * - Gemini Flash chat variants → ["off", "low", "medium", "high"] @@ -126,15 +127,19 @@ function getExplicitThinkingPolicy(modelString: string): ThinkingPolicy | null { return ["medium", "high", "xhigh"]; } - // gpt-5.2, gpt-5.5, the gpt-5.4-mini / gpt-5.4-nano variants, and the gpt-5.6 tiers - // (sol/terra/luna) support 5 reasoning levels including xhigh. - // Note: preview reports say gpt-5.6-sol adds a native "max" effort; we keep it at - // xhigh until OpenAI publishes official reasoning-level docs (OPENAI_REASONING_EFFORT - // maps our "max" to "xhigh" anyway, so nothing would be sent differently today). + // GPT-5.6 tiers (sol/terra/luna) support all 6 levels: OpenAI documents a native + // "max" reasoning effort for demanding tasks that need more exploration and + // verification. On the Responses API we send "max" through as-is; the Chat + // Completions path clamps to "xhigh" (see buildProviderOptions). + if (openaiSupportsNativeMaxEffort(withoutProviderNamespace)) { + return ["off", "low", "medium", "high", "xhigh", "max"]; + } + + // gpt-5.2, gpt-5.5 and the gpt-5.4-mini / gpt-5.4-nano variants support 5 reasoning + // levels including xhigh. if ( /^gpt-5\.2(?!-[a-z])/.test(withoutProviderNamespace) || - /^gpt-5\.(?:4|5)(?:-(?:mini|nano))?(?!-[a-z])/.test(withoutProviderNamespace) || - /^gpt-5\.6-(?:sol|terra|luna)(?!-[a-z])/.test(withoutProviderNamespace) + /^gpt-5\.(?:4|5)(?:-(?:mini|nano))?(?!-[a-z])/.test(withoutProviderNamespace) ) { return ["off", "low", "medium", "high", "xhigh"]; } diff --git a/src/node/services/providerModelFactory.test.ts b/src/node/services/providerModelFactory.test.ts index 2302ff5a85..817fc3404e 100644 --- a/src/node/services/providerModelFactory.test.ts +++ b/src/node/services/providerModelFactory.test.ts @@ -11,6 +11,7 @@ import { resolveProviderOptionsNamespaceKey } from "@/common/utils/ai/providerOp import { Ok } from "@/common/types/result"; import { ProviderModelFactory, + applyOpenAIReasoningModeOverride, buildAIProviderRequestHeaders, classifyCopilotInitiator, countAnthropicCacheBreakpoints, @@ -21,7 +22,10 @@ import { resolveOpenAIWebSocketResponsesUrl, wrapFetchWithAnthropicCacheControl, } from "./providerModelFactory"; -import { MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER } from "@/common/utils/ai/providerOptions"; +import { + MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER, + MUX_OPENAI_REASONING_MODE_HEADER, +} from "@/common/utils/ai/providerOptions"; import { hasLanguageModelCleanup } from "./languageModelCleanup"; import type { DevToolsService } from "./devToolsService"; import { CodexOauthService } from "./codexOauthService"; @@ -1706,3 +1710,84 @@ describe("wrapFetchWithAnthropicCacheControl — gateway (AI SDK) body shape", ( expect(sent.providerOptions.anthropic.effort).toBe("xhigh"); }); }); + +describe("applyOpenAIReasoningModeOverride", () => { + const HEADER = MUX_OPENAI_REASONING_MODE_HEADER; + const responsesRequest = { isOpenAIResponses: true, method: "POST" } as const; + + it("returns init unchanged when the header is absent", () => { + const init: Parameters[1] = { + method: "POST", + body: JSON.stringify({ model: "gpt-5.6-sol" }), + }; + expect(applyOpenAIReasoningModeOverride(init, responsesRequest)).toBe(init); + }); + + it("injects reasoning.mode and strips the header on Responses POSTs", () => { + const result = applyOpenAIReasoningModeOverride( + { + method: "POST", + headers: { [HEADER]: "pro", "content-type": "application/json" }, + body: JSON.stringify({ model: "gpt-5.6-sol", reasoning: { effort: "high" } }), + }, + responsesRequest + ); + + const headers = new Headers(result?.headers); + expect(headers.get(HEADER)).toBeNull(); + expect(headers.get("content-type")).toBe("application/json"); + const body = JSON.parse(result?.body as string) as { + reasoning: { effort: string; mode: string }; + }; + // SDK-built reasoning fields must survive alongside the injected mode. + expect(body.reasoning).toEqual({ effort: "high", mode: "pro" }); + }); + + it("creates the reasoning object when the body has none", () => { + const result = applyOpenAIReasoningModeOverride( + { + method: "POST", + headers: { [HEADER]: "pro" }, + body: JSON.stringify({ model: "gpt-5.6-sol" }), + }, + responsesRequest + ); + + const body = JSON.parse(result?.body as string) as { reasoning: { mode: string } }; + expect(body.reasoning).toEqual({ mode: "pro" }); + }); + + it("strips stale content-length after rewriting the body", () => { + const result = applyOpenAIReasoningModeOverride( + { + method: "POST", + headers: { [HEADER]: "pro", "content-length": "42" }, + body: JSON.stringify({ model: "gpt-5.6-sol" }), + }, + responsesRequest + ); + + expect(new Headers(result?.headers).get("content-length")).toBeNull(); + }); + + it("strips the header without touching the body on non-Responses endpoints", () => { + const body = JSON.stringify({ model: "gpt-5.6-sol" }); + const result = applyOpenAIReasoningModeOverride( + { method: "POST", headers: { [HEADER]: "pro" }, body }, + { isOpenAIResponses: false, method: "POST" } + ); + + expect(new Headers(result?.headers).get(HEADER)).toBeNull(); + expect(result?.body).toBe(body); + }); + + it("strips the header and forwards non-JSON bodies unmodified", () => { + const result = applyOpenAIReasoningModeOverride( + { method: "POST", headers: { [HEADER]: "pro" }, body: "not-json" }, + responsesRequest + ); + + expect(new Headers(result?.headers).get(HEADER)).toBeNull(); + expect(result?.body).toBe("not-json"); + }); +}); diff --git a/src/node/services/providerModelFactory.ts b/src/node/services/providerModelFactory.ts index 185a84e945..05b37b574e 100644 --- a/src/node/services/providerModelFactory.ts +++ b/src/node/services/providerModelFactory.ts @@ -51,6 +51,7 @@ import { createOpenAIWebSocketTransportFetch } from "@/node/services/openAIWebSo import { log } from "@/node/services/log"; import { MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER, + MUX_OPENAI_REASONING_MODE_HEADER, resolveProviderOptionsNamespaceKey, } from "@/common/utils/ai/providerOptions"; import { resolveRoute, type RouteContext } from "@/common/routing"; @@ -460,6 +461,43 @@ export function wrapFetchWithAnthropicCacheControl( return Object.assign(cachingFetch, baseFetch) as typeof fetch; } +/** + * Consume the Mux-internal reasoning-mode header (see MUX_OPENAI_REASONING_MODE_HEADER): + * strip it from the outgoing request and, for OpenAI Responses API POSTs, inject + * `reasoning.mode` into the JSON body. The @ai-sdk/openai Responses schema only builds + * reasoning.effort/summary, so GPT-5.6 pro mode must be added on the wire — the same + * pattern as the Anthropic xhigh effort rewrite in wrapFetchWithAnthropicCacheControl. + * + * Exported for tests. Returns the (possibly replaced) RequestInit; never throws. + */ +export function applyOpenAIReasoningModeOverride( + init: Parameters[1], + request: { isOpenAIResponses: boolean; method: string } +): Parameters[1] { + if (!init) return init; + const headers = new Headers(init.headers); + const mode = headers.get(MUX_OPENAI_REASONING_MODE_HEADER); + if (mode == null) return init; + // Always strip the Mux-internal header so it never leaves the process, even + // when the body cannot be modified (non-Responses endpoint, non-JSON body). + headers.delete(MUX_OPENAI_REASONING_MODE_HEADER); + + if (!(request.isOpenAIResponses && request.method === "POST" && typeof init.body === "string")) { + return { ...init, headers }; + } + + try { + const json = JSON.parse(init.body) as Record; + // Preserve SDK-built reasoning fields (effort/summary) and add mode alongside them. + json.reasoning = { ...(isRecord(json.reasoning) ? json.reasoning : {}), mode }; + headers.delete("content-length"); + return { ...init, headers, body: JSON.stringify(json) }; + } catch { + // Body isn't JSON — forward unmodified (minus the internal header). + return { ...init, headers }; + } +} + /** * Wrap fetch so any mux-gateway 401 response clears local credentials (best-effort). * @@ -1305,7 +1343,8 @@ export class ProviderModelFactory { // when call sites omit options (e.g. TaskService, WorkspaceTitleGenerator). const configServiceTier = providerConfig.serviceTier as string | undefined; const configWireFormat = providerConfig.wireFormat as string | undefined; - if (configServiceTier || configWireFormat) { + const configReasoningMode = providerConfig.reasoningMode as string | undefined; + if (configServiceTier || configWireFormat || configReasoningMode) { muxProviderOptions ??= {}; if (configServiceTier && muxProviderOptions.openai?.serviceTier == null) { muxProviderOptions.openai = { @@ -1319,6 +1358,14 @@ export class ProviderModelFactory { wireFormat: configWireFormat, }; } + // GPT-5.6 pro mode from providers.jsonc. Request-level options win; per-model + // gating (GPT-5.6 family only) happens in buildRequestHeaders. + if (configReasoningMode === "pro" && muxProviderOptions.openai?.reasoningMode == null) { + muxProviderOptions.openai = { + ...muxProviderOptions.openai, + reasoningMode: configReasoningMode, + }; + } } // Resolve effective wireFormat once — used by both fetch wrapper and model selection. @@ -1345,6 +1392,12 @@ export class ProviderModelFactory { const isOpenAIResponses = /\/v1\/responses(\?|$)/.test(urlString); const isOpenAIChatCompletions = /\/chat\/completions(\?|$)/.test(urlString); + // GPT-5.6 pro mode: consume the Mux-internal reasoning-mode header and + // inject `reasoning.mode` into the Responses body before any Codex + // normalization reads it. Reassigning `init` keeps every downstream + // consumer (Codex body handling, error fallback) on the rewritten request. + init = applyOpenAIReasoningModeOverride(init, { isOpenAIResponses, method }); + let nextInput: Parameters[0] = input; let nextInit: Parameters[1] | undefined = init; From 75119b702a908292f023fbc167d9b396a3108aad Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 9 Jul 2026 14:23:43 -0500 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20address=20Codex=20rev?= =?UTF-8?q?iew=20=E2=80=94=20drop=20pro=20mode,=20fix=20GPT-5.6=20max=20la?= =?UTF-8?q?bel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review decision, GPT-5.6 pro mode (reasoning.mode: "pro") is removed from this PR. It belongs as a toggle inside the thinking slider rather than backend-only config plumbing; tracked in #3704. This also moots the Codex finding about provider-level pro mode being ignored by headless createModel() callers. Addresses the other Codex finding: getThinkingDisplayLabel now returns MAX (and getThinkingOptionLabel "max") for the max level on GPT-5.6 models, keeping the native max effort distinguishable from xhigh in the thinking slider and settings dropdowns. Other OpenAI models keep XHIGH for both levels since they clamp max→xhigh on the wire. --- _Generated with `mux` • Model: `anthropic:claude-fable-5` • Thinking: `high` • Cost: `$34.69`_ --- src/common/config/schemas/providersConfig.ts | 3 - src/common/constants/knownModels.ts | 5 +- src/common/schemas/providerOptions.ts | 8 -- src/common/types/thinking.test.ts | 10 +++ src/common/types/thinking.ts | 35 +++----- src/common/utils/ai/providerOptions.test.ts | 67 -------------- src/common/utils/ai/providerOptions.ts | 32 ------- .../services/providerModelFactory.test.ts | 87 +------------------ src/node/services/providerModelFactory.ts | 55 +----------- 9 files changed, 29 insertions(+), 273 deletions(-) diff --git a/src/common/config/schemas/providersConfig.ts b/src/common/config/schemas/providersConfig.ts index 5834f0001c..beed8dfb43 100644 --- a/src/common/config/schemas/providersConfig.ts +++ b/src/common/config/schemas/providersConfig.ts @@ -30,9 +30,6 @@ export const AnthropicProviderConfigSchema = BaseProviderConfigSchema.extend({ export const OpenAIProviderConfigSchema = BaseProviderConfigSchema.extend({ serviceTier: ServiceTierSchema.optional(), - // GPT-5.6 pro mode (reasoning.mode: "pro"). Provider-level intent; requests are - // still gated per-model to the GPT-5.6 family in buildRequestHeaders. - reasoningMode: z.enum(["default", "pro"]).optional(), organization: z.string().optional(), codexOauthDefaultAuth: CodexOauthDefaultAuthSchema.optional(), codexOauth: z.record(z.string(), z.unknown()).optional(), diff --git a/src/common/constants/knownModels.ts b/src/common/constants/knownModels.ts index 75e6283edc..dc4818b00f 100644 --- a/src/common/constants/knownModels.ts +++ b/src/common/constants/knownModels.ts @@ -106,8 +106,9 @@ const MODEL_DEFINITIONS = { // ChatGPT, Codex, and the API (verified callable via plain API keys). New naming // system: the number is the generation, while Sol/Terra/Luna are durable capability // tiers. Cache writes bill at 1.25x input; cache reads keep the 90% discount. - // All tiers support a native "max" reasoning effort (see the thinking policy) and - // Responses API `reasoning.mode: "pro"` (see MUX_OPENAI_REASONING_MODE_HEADER). + // All tiers support a native "max" reasoning effort (see the thinking policy). + // The API also offers `reasoning.mode: "pro"`, not yet implemented in Mux (planned + // as a thinking-slider toggle; tracked in issue #3704). // GPT-5.6 Sol - flagship tier. $5/M input, $30/M output. Bare `gpt` alias lives here: // Sol succeeds gpt-5.5 as the flagship at the same list price. GPT_56_SOL: { diff --git a/src/common/schemas/providerOptions.ts b/src/common/schemas/providerOptions.ts index 67f5e4b12d..9dbe888bc9 100644 --- a/src/common/schemas/providerOptions.ts +++ b/src/common/schemas/providerOptions.ts @@ -42,14 +42,6 @@ export const MuxProviderOptionsSchema = z.object({ store: z.boolean().optional().meta({ description: "Whether OpenAI stores responses. Set false for zero data retention (ZDR).", }), - reasoningMode: z - .enum(["default", "pro"]) - .optional() - .meta({ - description: - 'OpenAI reasoning mode: "pro" performs more model work for reliability on difficult ' + - "tasks (GPT-5.6 family, Responses API only); default/unset uses standard reasoning", - }), forceContextLimitError: z.boolean().optional().meta({ description: "Force context limit error (used in integration tests to simulate overflow)", }), diff --git a/src/common/types/thinking.test.ts b/src/common/types/thinking.test.ts index a80b9a32b4..1c310c327e 100644 --- a/src/common/types/thinking.test.ts +++ b/src/common/types/thinking.test.ts @@ -22,6 +22,16 @@ describe("getThinkingDisplayLabel", () => { expect(getThinkingDisplayLabel("max", "mux-gateway:openai/gpt-5.2")).toBe("XHIGH"); }); + test("keeps xhigh and max distinguishable on GPT-5.6 (native max effort)", () => { + expect(getThinkingDisplayLabel("xhigh", "openai:gpt-5.6-sol")).toBe("XHIGH"); + expect(getThinkingDisplayLabel("max", "openai:gpt-5.6-sol")).toBe("MAX"); + expect(getThinkingDisplayLabel("max", "openai:gpt-5.6-terra")).toBe("MAX"); + expect(getThinkingDisplayLabel("max", "mux-gateway:openai/gpt-5.6-luna")).toBe("MAX"); + // Option labels (settings dropdowns) must diverge too + expect(getThinkingOptionLabel("xhigh", "openai:gpt-5.6-sol")).toBe("xhigh"); + expect(getThinkingOptionLabel("max", "openai:gpt-5.6-sol")).toBe("max"); + }); + test("returns MAX for xhigh/max when no model specified (default)", () => { expect(getThinkingDisplayLabel("xhigh")).toBe("MAX"); expect(getThinkingDisplayLabel("max")).toBe("MAX"); diff --git a/src/common/types/thinking.ts b/src/common/types/thinking.ts index ed94f80fa2..333a680ea8 100644 --- a/src/common/types/thinking.ts +++ b/src/common/types/thinking.ts @@ -38,8 +38,13 @@ export function getThinkingDisplayLabel(level: ThinkingLevel, modelString?: stri const normalized = modelString.trim().toLowerCase(); const withoutPrefix = normalized.replace(/^[a-z0-9_-]+:\s*/, ""); - // OpenAI: both xhigh and max resolve to "xhigh" reasoning effort - if (normalized.startsWith("openai:") || withoutPrefix.startsWith("openai/")) return "XHIGH"; + if (normalized.startsWith("openai:") || withoutPrefix.startsWith("openai/")) { + // GPT-5.6 has a native "max" reasoning effort distinct from xhigh, so both + // labels must stay distinguishable in settings dropdowns and the slider. + if (level === "max" && openaiSupportsNativeMaxEffort(modelString)) return "MAX"; + // Other OpenAI models: both xhigh and max resolve to "xhigh" reasoning effort + return "XHIGH"; + } // Anthropic Opus 4.7+: xhigh is a distinct effort level from max if (level === "xhigh" && anthropicSupportsNativeXhigh(modelString)) return "XHIGH"; @@ -237,34 +242,22 @@ export function anthropicSupportsNativeXhigh(modelString: string): boolean { ); } -/** - * GPT-5.6 family (Sol / Terra / Luna), including version-suffixed ids like - * `gpt-5.6-sol-2026-07-09`. Kept as one predicate so the two GPT-5.6-only - * capabilities below stay in sync. - */ -const GPT_56_FAMILY_REGEX = /^gpt-5\.6-(?:sol|terra|luna)(?!-[a-z])/; - /** * Whether the given OpenAI model supports the native "max" reasoning effort - * (GPT-5.6 family, GA July 9, 2026). OpenAI recommends max effort "for demanding + * (GPT-5.6 family — Sol/Terra/Luna, GA July 9, 2026 — including version-suffixed + * ids like `gpt-5.6-sol-2026-07-09`). OpenAI recommends max effort "for demanding * tasks that need more exploration and verification". * * Only valid on the Responses API path: the @ai-sdk/openai Responses schema * accepts arbitrary effort strings, but the Chat Completions schema rejects * anything outside its enum — callers must clamp to "xhigh" there. + * + * Note: GPT-5.6 also supports `reasoning.mode: "pro"` (more model work for + * reliability, single final answer). Mux does not implement it yet — planned as + * a toggle inside the thinking slider; tracked in issue #3704. */ export function openaiSupportsNativeMaxEffort(modelString: string): boolean { - return GPT_56_FAMILY_REGEX.test(stripModelProviderPrefixes(modelString)); -} - -/** - * Whether the given OpenAI model supports `reasoning.mode: "pro"` on the - * Responses API (GPT-5.6 family). Pro mode performs more model work to improve - * reliability on difficult tasks and returns a single final answer — intended - * for when quality matters more than latency and token usage. - */ -export function openaiSupportsProReasoningMode(modelString: string): boolean { - return GPT_56_FAMILY_REGEX.test(stripModelProviderPrefixes(modelString)); + return /^gpt-5\.6-(?:sol|terra|luna)(?!-[a-z])/.test(stripModelProviderPrefixes(modelString)); } /** diff --git a/src/common/utils/ai/providerOptions.test.ts b/src/common/utils/ai/providerOptions.test.ts index 651113ea7d..8c6bfe7450 100644 --- a/src/common/utils/ai/providerOptions.test.ts +++ b/src/common/utils/ai/providerOptions.test.ts @@ -14,7 +14,6 @@ import { resolveProviderOptionsNamespaceKey, ANTHROPIC_1M_CONTEXT_HEADER, MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER, - MUX_OPENAI_REASONING_MODE_HEADER, MUX_WORKSPACE_ID_HEADER, } from "./providerOptions"; @@ -1084,72 +1083,6 @@ describe("buildRequestHeaders", () => { } }); - describe("GPT-5.6 pro reasoning mode header", () => { - for (const { name, model, options, routeProvider, expected } of [ - { - name: "emits pro mode header for gpt-5.6-sol with reasoningMode pro", - model: "openai:gpt-5.6-sol", - options: { openai: { reasoningMode: "pro" } }, - routeProvider: undefined, - expected: { [MUX_OPENAI_REASONING_MODE_HEADER]: "pro" }, - }, - { - name: "emits pro mode header for version-suffixed gpt-5.6-terra", - model: "openai:gpt-5.6-terra-2026-07-09", - options: { openai: { reasoningMode: "pro" } }, - routeProvider: "openai", - expected: { [MUX_OPENAI_REASONING_MODE_HEADER]: "pro" }, - }, - { - name: "does not emit header for non-GPT-5.6 model even with reasoningMode pro", - model: "openai:gpt-5.5", - options: { openai: { reasoningMode: "pro" } }, - routeProvider: undefined, - expected: undefined, - }, - { - name: "does not emit header when reasoningMode is default", - model: "openai:gpt-5.6-sol", - options: { openai: { reasoningMode: "default" } }, - routeProvider: undefined, - expected: undefined, - }, - { - name: "does not emit header when reasoningMode is unset", - model: "openai:gpt-5.6-sol", - options: { openai: {} }, - routeProvider: undefined, - expected: undefined, - }, - { - name: "does not emit header on the Chat Completions wire format", - model: "openai:gpt-5.6-sol", - options: { openai: { reasoningMode: "pro", wireFormat: "chatCompletions" } }, - routeProvider: undefined, - expected: undefined, - }, - { - name: "does not emit header for gateway-routed GPT-5.6 (wrapper not applied there)", - model: "mux-gateway:openai/gpt-5.6-sol", - options: { openai: { reasoningMode: "pro" } }, - routeProvider: "mux-gateway", - expected: undefined, - }, - ] as const) { - test(name, () => { - expect( - buildRequestHeaders( - model, - options as Parameters[1], - undefined, - undefined, - routeProvider - ) - ).toEqual(expected); - }); - } - }); - for (const { name, model, options, workspaceId, expected } of [ { name: "should include X-Mux-Workspace-Id for non-Anthropic provider when workspaceId provided", diff --git a/src/common/utils/ai/providerOptions.ts b/src/common/utils/ai/providerOptions.ts index 164010f697..a477545fcc 100644 --- a/src/common/utils/ai/providerOptions.ts +++ b/src/common/utils/ai/providerOptions.ts @@ -24,7 +24,6 @@ import { OPENAI_REASONING_EFFORT, OPENROUTER_REASONING_EFFORT, openaiSupportsNativeMaxEffort, - openaiSupportsProReasoningMode, } from "@/common/types/thinking"; import { isGeminiFlashThinkingLevelModelName } from "@/common/utils/thinking/policy"; import { resolveModelForMetadata } from "@/common/utils/providers/modelEntries"; @@ -41,16 +40,6 @@ import { normalizeToCanonical, supports1MContext } from "./models"; */ export const MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER = "x-mux-anthropic-effort"; -/** - * Mux-internal request header that asks the OpenAI fetch wrapper to inject - * `reasoning.mode` into the Responses API body on the wire. The @ai-sdk/openai - * Responses schema has no mode field (it only builds reasoning.effort/summary), - * so — like the Anthropic effort override above — the option rides a header to - * the fetch wrapper, which rewrites the body and strips the header before the - * request leaves Mux. Value is the reasoning mode to inject (currently "pro"). - */ -export const MUX_OPENAI_REASONING_MODE_HEADER = "x-mux-openai-reasoning-mode"; - /** * OpenRouter reasoning options * @see https://openrouter.ai/docs/use-cases/reasoning-tokens @@ -639,26 +628,5 @@ export function buildRequestHeaders( headers[MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER] = "xhigh"; } - // GPT-5.6 pro mode (`reasoning.mode: "pro"`): more model work for reliability on - // difficult tasks, returning a single final answer — for when quality matters more - // than latency and token usage. The SDK cannot send reasoning.mode, so emit a - // Mux-internal header for the OpenAI fetch wrapper to rewrite the body on the wire. - // - // Gates: - // - direct OpenAI route only: unlike the Anthropic wrapper, the OpenAI fetch - // wrapper is not applied on passthrough gateways, so a header emitted there - // would leak to the gateway verbatim instead of being consumed. - // - Responses API only (reasoning.mode does not exist on Chat Completions). - // - GPT-5.6 family only; other models would reject the parameter. - if ( - origin === "openai" && - (routeProvider == null || routeProvider === "openai") && - muxProviderOptions?.openai?.reasoningMode === "pro" && - (muxProviderOptions.openai.wireFormat ?? "responses") === "responses" && - openaiSupportsProReasoningMode(modelString) - ) { - headers[MUX_OPENAI_REASONING_MODE_HEADER] = "pro"; - } - return Object.keys(headers).length > 0 ? headers : undefined; } diff --git a/src/node/services/providerModelFactory.test.ts b/src/node/services/providerModelFactory.test.ts index 817fc3404e..2302ff5a85 100644 --- a/src/node/services/providerModelFactory.test.ts +++ b/src/node/services/providerModelFactory.test.ts @@ -11,7 +11,6 @@ import { resolveProviderOptionsNamespaceKey } from "@/common/utils/ai/providerOp import { Ok } from "@/common/types/result"; import { ProviderModelFactory, - applyOpenAIReasoningModeOverride, buildAIProviderRequestHeaders, classifyCopilotInitiator, countAnthropicCacheBreakpoints, @@ -22,10 +21,7 @@ import { resolveOpenAIWebSocketResponsesUrl, wrapFetchWithAnthropicCacheControl, } from "./providerModelFactory"; -import { - MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER, - MUX_OPENAI_REASONING_MODE_HEADER, -} from "@/common/utils/ai/providerOptions"; +import { MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER } from "@/common/utils/ai/providerOptions"; import { hasLanguageModelCleanup } from "./languageModelCleanup"; import type { DevToolsService } from "./devToolsService"; import { CodexOauthService } from "./codexOauthService"; @@ -1710,84 +1706,3 @@ describe("wrapFetchWithAnthropicCacheControl — gateway (AI SDK) body shape", ( expect(sent.providerOptions.anthropic.effort).toBe("xhigh"); }); }); - -describe("applyOpenAIReasoningModeOverride", () => { - const HEADER = MUX_OPENAI_REASONING_MODE_HEADER; - const responsesRequest = { isOpenAIResponses: true, method: "POST" } as const; - - it("returns init unchanged when the header is absent", () => { - const init: Parameters[1] = { - method: "POST", - body: JSON.stringify({ model: "gpt-5.6-sol" }), - }; - expect(applyOpenAIReasoningModeOverride(init, responsesRequest)).toBe(init); - }); - - it("injects reasoning.mode and strips the header on Responses POSTs", () => { - const result = applyOpenAIReasoningModeOverride( - { - method: "POST", - headers: { [HEADER]: "pro", "content-type": "application/json" }, - body: JSON.stringify({ model: "gpt-5.6-sol", reasoning: { effort: "high" } }), - }, - responsesRequest - ); - - const headers = new Headers(result?.headers); - expect(headers.get(HEADER)).toBeNull(); - expect(headers.get("content-type")).toBe("application/json"); - const body = JSON.parse(result?.body as string) as { - reasoning: { effort: string; mode: string }; - }; - // SDK-built reasoning fields must survive alongside the injected mode. - expect(body.reasoning).toEqual({ effort: "high", mode: "pro" }); - }); - - it("creates the reasoning object when the body has none", () => { - const result = applyOpenAIReasoningModeOverride( - { - method: "POST", - headers: { [HEADER]: "pro" }, - body: JSON.stringify({ model: "gpt-5.6-sol" }), - }, - responsesRequest - ); - - const body = JSON.parse(result?.body as string) as { reasoning: { mode: string } }; - expect(body.reasoning).toEqual({ mode: "pro" }); - }); - - it("strips stale content-length after rewriting the body", () => { - const result = applyOpenAIReasoningModeOverride( - { - method: "POST", - headers: { [HEADER]: "pro", "content-length": "42" }, - body: JSON.stringify({ model: "gpt-5.6-sol" }), - }, - responsesRequest - ); - - expect(new Headers(result?.headers).get("content-length")).toBeNull(); - }); - - it("strips the header without touching the body on non-Responses endpoints", () => { - const body = JSON.stringify({ model: "gpt-5.6-sol" }); - const result = applyOpenAIReasoningModeOverride( - { method: "POST", headers: { [HEADER]: "pro" }, body }, - { isOpenAIResponses: false, method: "POST" } - ); - - expect(new Headers(result?.headers).get(HEADER)).toBeNull(); - expect(result?.body).toBe(body); - }); - - it("strips the header and forwards non-JSON bodies unmodified", () => { - const result = applyOpenAIReasoningModeOverride( - { method: "POST", headers: { [HEADER]: "pro" }, body: "not-json" }, - responsesRequest - ); - - expect(new Headers(result?.headers).get(HEADER)).toBeNull(); - expect(result?.body).toBe("not-json"); - }); -}); diff --git a/src/node/services/providerModelFactory.ts b/src/node/services/providerModelFactory.ts index 05b37b574e..185a84e945 100644 --- a/src/node/services/providerModelFactory.ts +++ b/src/node/services/providerModelFactory.ts @@ -51,7 +51,6 @@ import { createOpenAIWebSocketTransportFetch } from "@/node/services/openAIWebSo import { log } from "@/node/services/log"; import { MUX_ANTHROPIC_EFFORT_OVERRIDE_HEADER, - MUX_OPENAI_REASONING_MODE_HEADER, resolveProviderOptionsNamespaceKey, } from "@/common/utils/ai/providerOptions"; import { resolveRoute, type RouteContext } from "@/common/routing"; @@ -461,43 +460,6 @@ export function wrapFetchWithAnthropicCacheControl( return Object.assign(cachingFetch, baseFetch) as typeof fetch; } -/** - * Consume the Mux-internal reasoning-mode header (see MUX_OPENAI_REASONING_MODE_HEADER): - * strip it from the outgoing request and, for OpenAI Responses API POSTs, inject - * `reasoning.mode` into the JSON body. The @ai-sdk/openai Responses schema only builds - * reasoning.effort/summary, so GPT-5.6 pro mode must be added on the wire — the same - * pattern as the Anthropic xhigh effort rewrite in wrapFetchWithAnthropicCacheControl. - * - * Exported for tests. Returns the (possibly replaced) RequestInit; never throws. - */ -export function applyOpenAIReasoningModeOverride( - init: Parameters[1], - request: { isOpenAIResponses: boolean; method: string } -): Parameters[1] { - if (!init) return init; - const headers = new Headers(init.headers); - const mode = headers.get(MUX_OPENAI_REASONING_MODE_HEADER); - if (mode == null) return init; - // Always strip the Mux-internal header so it never leaves the process, even - // when the body cannot be modified (non-Responses endpoint, non-JSON body). - headers.delete(MUX_OPENAI_REASONING_MODE_HEADER); - - if (!(request.isOpenAIResponses && request.method === "POST" && typeof init.body === "string")) { - return { ...init, headers }; - } - - try { - const json = JSON.parse(init.body) as Record; - // Preserve SDK-built reasoning fields (effort/summary) and add mode alongside them. - json.reasoning = { ...(isRecord(json.reasoning) ? json.reasoning : {}), mode }; - headers.delete("content-length"); - return { ...init, headers, body: JSON.stringify(json) }; - } catch { - // Body isn't JSON — forward unmodified (minus the internal header). - return { ...init, headers }; - } -} - /** * Wrap fetch so any mux-gateway 401 response clears local credentials (best-effort). * @@ -1343,8 +1305,7 @@ export class ProviderModelFactory { // when call sites omit options (e.g. TaskService, WorkspaceTitleGenerator). const configServiceTier = providerConfig.serviceTier as string | undefined; const configWireFormat = providerConfig.wireFormat as string | undefined; - const configReasoningMode = providerConfig.reasoningMode as string | undefined; - if (configServiceTier || configWireFormat || configReasoningMode) { + if (configServiceTier || configWireFormat) { muxProviderOptions ??= {}; if (configServiceTier && muxProviderOptions.openai?.serviceTier == null) { muxProviderOptions.openai = { @@ -1358,14 +1319,6 @@ export class ProviderModelFactory { wireFormat: configWireFormat, }; } - // GPT-5.6 pro mode from providers.jsonc. Request-level options win; per-model - // gating (GPT-5.6 family only) happens in buildRequestHeaders. - if (configReasoningMode === "pro" && muxProviderOptions.openai?.reasoningMode == null) { - muxProviderOptions.openai = { - ...muxProviderOptions.openai, - reasoningMode: configReasoningMode, - }; - } } // Resolve effective wireFormat once — used by both fetch wrapper and model selection. @@ -1392,12 +1345,6 @@ export class ProviderModelFactory { const isOpenAIResponses = /\/v1\/responses(\?|$)/.test(urlString); const isOpenAIChatCompletions = /\/chat\/completions(\?|$)/.test(urlString); - // GPT-5.6 pro mode: consume the Mux-internal reasoning-mode header and - // inject `reasoning.mode` into the Responses body before any Codex - // normalization reads it. Reassigning `init` keeps every downstream - // consumer (Codex body handling, error fallback) on the rewritten request. - init = applyOpenAIReasoningModeOverride(init, { isOpenAIResponses, method }); - let nextInput: Parameters[0] = input; let nextInit: Parameters[1] | undefined = init; From 30baf4bd6d083531f4bd3ac1956b5e3900c0736d Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 9 Jul 2026 14:35:01 -0500 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=A4=96=20docs:=20document=20GPT-5.6?= =?UTF-8?q?=20cache-write=20cost=20attribution=20gap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex flagged that the 1.25x cache-write rate on GPT-5.6 entries is never applied: createDisplayUsage() only sources cacheCreateTokens from Anthropic provider metadata. Verified live that the Responses API does report input_tokens_details.cache_write_tokens, but @ai-sdk/openai (through at least 3.0.83) hardcodes cacheWrite: undefined and strips the field from its usage schema, so Mux cannot attribute the premium without SDK support. Bounded impact: 0.25x input rate on first-write tokens only; cache reads are costed correctly. Metadata is kept so pricing lights up automatically once the SDK surfaces cache-write usage. Tracked in #3705. --- _Generated with `mux` • Model: `anthropic:claude-fable-5` • Thinking: `high` • Cost: `$40.68`_ --- src/common/utils/tokens/models-extra.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/common/utils/tokens/models-extra.ts b/src/common/utils/tokens/models-extra.ts index 2b38363eeb..9c25c29211 100644 --- a/src/common/utils/tokens/models-extra.ts +++ b/src/common/utils/tokens/models-extra.ts @@ -266,6 +266,13 @@ export const modelsExtra: Record = { // Prompt caching: cache reads keep the 90% discount; cache writes for GPT-5.6+ bill // at 1.25x the uncached input rate (with explicit breakpoints and a 30-min minimum // cache life). No long-context tiered pricing has been published for this family. + // KNOWN COST GAP: the API reports cache writes via input_tokens_details.cache_write_tokens + // (verified live), but @ai-sdk/openai (through at least 3.0.83) hardcodes cacheWrite to + // undefined and strips the field from its usage schema, so Mux cannot attribute the 1.25x + // write premium yet — cache-written tokens are costed at the base input rate (bounded + // underreport: 0.25x input rate on first-write tokens only). The + // cache_creation_input_token_cost values below are kept so pricing lights up automatically + // once the SDK surfaces cache-write usage. Tracked in issue #3705. // OpenAI has not published official context specs in the GA announcement; we mirror // GPT-5.5's window (1.05M in / 128K out) as a provisional value until the model card // lands. (Preview reports suggest Sol may be larger, ~1.5M, but that's unconfirmed.) From 6d233d779c547858bec8ce778ae17fd43b68e5e0 Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 9 Jul 2026 14:45:03 -0500 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20gate=20GPT-5.6=20nati?= =?UTF-8?q?ve=20max=20effort=20to=20passthrough=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex flagged that non-passthrough gateways rebuild reasoning params with their own effort maps (OpenRouter clamps max→high, Copilot→xhigh), so offering MAX in the thinking slider for e.g. openrouter:openai/gpt-5.6-sol would silently send a lower effort than promised. The GPT-5.6 policy now exposes all 6 levels only when the route delivers Mux-built openai providerOptions verbatim (direct openai or a PROVIDER_DEFINITIONS passthrough gateway); other routes keep the 5-level xhigh-capped policy. --- _Generated with `mux` • Model: `anthropic:claude-fable-5` • Thinking: `high` • Cost: `$46.88`_ --- src/common/utils/thinking/policy.test.ts | 17 +++++++++++- src/common/utils/thinking/policy.ts | 33 ++++++++++++++++++++---- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/common/utils/thinking/policy.test.ts b/src/common/utils/thinking/policy.test.ts index 01d6c60ad4..276ea7a77b 100644 --- a/src/common/utils/thinking/policy.test.ts +++ b/src/common/utils/thinking/policy.test.ts @@ -200,7 +200,7 @@ describe("getThinkingPolicyForModel", () => { } ); - test("returns all 6 levels for gpt-5.6-sol behind mux-gateway with version suffix", () => { + test("returns all 6 levels for gpt-5.6-sol behind mux-gateway with version suffix (passthrough)", () => { expect(getThinkingPolicyForModel("mux-gateway:openai/gpt-5.6-sol-2026-06-26")).toEqual([ "off", "low", @@ -211,6 +211,21 @@ describe("getThinkingPolicyForModel", () => { ]); }); + // Non-passthrough routes rebuild reasoning params with their own effort maps + // (OpenRouter clamps max→high, Copilot →xhigh), so native max must not be offered. + test.each(["openrouter:openai/gpt-5.6-sol", "github-copilot:openai/gpt-5.6-terra"])( + "excludes native max for GPT-5.6 on non-passthrough route %s", + (modelString) => { + expect(getThinkingPolicyForModel(modelString)).toEqual([ + "off", + "low", + "medium", + "high", + "xhigh", + ]); + } + ); + test("returns 5 levels including xhigh for gpt-5.5", () => { expect(getThinkingPolicyForModel("openai:gpt-5.5")).toEqual([ "off", diff --git a/src/common/utils/thinking/policy.ts b/src/common/utils/thinking/policy.ts index f826f8ef8f..2bbfd32a92 100644 --- a/src/common/utils/thinking/policy.ts +++ b/src/common/utils/thinking/policy.ts @@ -12,6 +12,7 @@ * - Multiple elements = User can select from options */ +import { PROVIDER_DEFINITIONS } from "@/common/constants/providers"; import type { ProvidersConfigMap } from "@/common/orpc/types"; import { THINKING_LEVELS, @@ -76,6 +77,22 @@ export function getThinkingPolicyForModel(modelString: string): ThinkingPolicy { */ const DEFAULT_THINKING_POLICY: ThinkingPolicy = ["off", "low", "medium", "high"]; +/** + * Whether the model string's route delivers Mux-built openai providerOptions to + * OpenAI unchanged: direct `openai:` (or a bare/namespaced id with no route prefix), + * or a gateway marked passthrough in PROVIDER_DEFINITIONS (e.g. mux-gateway). + * Non-passthrough routes (openrouter, github-copilot, ...) rebuild reasoning params + * with their own effort maps and cannot express OpenAI's native "max". + */ +function routeCanPassThroughOpenAIOptions(modelString: string): boolean { + const colonIndex = modelString.indexOf(":"); + if (colonIndex === -1) return true; // bare model id — no route info, assume direct + const route = modelString.slice(0, colonIndex).trim().toLowerCase(); + if (route === "openai") return true; + const definition = PROVIDER_DEFINITIONS[route as keyof typeof PROVIDER_DEFINITIONS]; + return definition != null && "passthrough" in definition && definition.passthrough === true; +} + /** * Returns the policy for a model that matches an explicit reasoning rule, or `null` * when the model falls through to {@link DEFAULT_THINKING_POLICY}. @@ -127,12 +144,18 @@ function getExplicitThinkingPolicy(modelString: string): ThinkingPolicy | null { return ["medium", "high", "xhigh"]; } - // GPT-5.6 tiers (sol/terra/luna) support all 6 levels: OpenAI documents a native - // "max" reasoning effort for demanding tasks that need more exploration and - // verification. On the Responses API we send "max" through as-is; the Chat - // Completions path clamps to "xhigh" (see buildProviderOptions). + // GPT-5.6 tiers (sol/terra/luna) support a native "max" reasoning effort for + // demanding tasks that need more exploration and verification. Only expose "max" + // on routes that can actually deliver it — direct OpenAI or passthrough gateways + // (which forward the openai providerOptions namespace verbatim). Non-passthrough + // routes build their own reasoning params (OpenRouter clamps max→"high", Copilot + // →"xhigh"), so offering MAX there would silently send a lower effort than the + // slider promises; those routes keep the 5-level policy below. if (openaiSupportsNativeMaxEffort(withoutProviderNamespace)) { - return ["off", "low", "medium", "high", "xhigh", "max"]; + if (routeCanPassThroughOpenAIOptions(modelString)) { + return ["off", "low", "medium", "high", "xhigh", "max"]; + } + return ["off", "low", "medium", "high", "xhigh"]; } // gpt-5.2, gpt-5.5 and the gpt-5.4-mini / gpt-5.4-nano variants support 5 reasoning From fc03cbe059a37e8b13657fe67329157e24405360 Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 9 Jul 2026 14:55:01 -0500 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20clamp=20GPT-5.6=20nat?= =?UTF-8?q?ive=20max=20after=20request-time=20route=20resolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex flagged that the policy-level route gate only inspects the model string, but routing can be decided at request time via routePriority/ overrides — openai:gpt-5.6-sol can resolve onto OpenRouter or Copilot, whose effort maps silently send high/xhigh for a selected MAX. New clampThinkingLevelForRoute() downgrades max→xhigh for GPT-5.6 when the resolved route is non-passthrough, applied in AIService right after route resolution (primary and fallback-model paths) so provider options, headers, replay transforms, and metadata all agree with what the wire can deliver. Mirrors the existing Mythos-class clamp precedent. xhigh is those routes' policy ceiling; their effort maps handle any further provider-side clamping. No-op for every other model since max and xhigh already share a wire value outside GPT-5.6. --- _Generated with `mux` • Model: `anthropic:claude-fable-5` • Thinking: `high` • Cost: `$54.12`_ --- src/common/utils/thinking/policy.test.ts | 32 ++++++++++++++ src/common/utils/thinking/policy.ts | 55 ++++++++++++++++++++---- src/node/services/aiService.ts | 31 ++++++++++--- 3 files changed, 103 insertions(+), 15 deletions(-) diff --git a/src/common/utils/thinking/policy.test.ts b/src/common/utils/thinking/policy.test.ts index 276ea7a77b..bc4542ba1e 100644 --- a/src/common/utils/thinking/policy.test.ts +++ b/src/common/utils/thinking/policy.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import type { ProvidersConfigMap } from "@/common/orpc/types"; import { + clampThinkingLevelForRoute, getThinkingPolicyForModel, enforceThinkingPolicy, resolveThinkingInput, @@ -226,6 +227,37 @@ describe("getThinkingPolicyForModel", () => { } ); + // Route may be decided at request time (routePriority), not by the model string: + // an `openai:` GPT-5.6 model can resolve onto OpenRouter/Copilot. The post-route + // clamp must downgrade native max there while leaving passthrough routes alone. + describe("clampThinkingLevelForRoute", () => { + test.each(["openrouter", "github-copilot"])( + "clamps max to xhigh for openai:gpt-5.6-sol resolved onto %s", + (route) => { + expect(clampThinkingLevelForRoute("openai:gpt-5.6-sol", route, "max")).toBe("xhigh"); + } + ); + + test("keeps max on direct OpenAI and passthrough gateway routes", () => { + expect(clampThinkingLevelForRoute("openai:gpt-5.6-sol", "openai", "max")).toBe("max"); + expect(clampThinkingLevelForRoute("openai:gpt-5.6-sol", "mux-gateway", "max")).toBe("max"); + // No resolved route provided — falls back to model-string inference. + expect(clampThinkingLevelForRoute("openai:gpt-5.6-sol", undefined, "max")).toBe("max"); + expect(clampThinkingLevelForRoute("openrouter:openai/gpt-5.6-sol", undefined, "max")).toBe( + "xhigh" + ); + }); + + test("leaves non-max levels and non-GPT-5.6 models untouched", () => { + expect(clampThinkingLevelForRoute("openai:gpt-5.6-sol", "openrouter", "xhigh")).toBe("xhigh"); + // Non-GPT-5.6: max and xhigh already share a wire value; no clamp needed. + expect(clampThinkingLevelForRoute("openai:gpt-5.5", "openrouter", "max")).toBe("max"); + expect(clampThinkingLevelForRoute("anthropic:claude-opus-4-8", "openrouter", "max")).toBe( + "max" + ); + }); + }); + test("returns 5 levels including xhigh for gpt-5.5", () => { expect(getThinkingPolicyForModel("openai:gpt-5.5")).toEqual([ "off", diff --git a/src/common/utils/thinking/policy.ts b/src/common/utils/thinking/policy.ts index 2bbfd32a92..6018431532 100644 --- a/src/common/utils/thinking/policy.ts +++ b/src/common/utils/thinking/policy.ts @@ -78,21 +78,58 @@ export function getThinkingPolicyForModel(modelString: string): ThinkingPolicy { const DEFAULT_THINKING_POLICY: ThinkingPolicy = ["off", "low", "medium", "high"]; /** - * Whether the model string's route delivers Mux-built openai providerOptions to - * OpenAI unchanged: direct `openai:` (or a bare/namespaced id with no route prefix), - * or a gateway marked passthrough in PROVIDER_DEFINITIONS (e.g. mux-gateway). - * Non-passthrough routes (openrouter, github-copilot, ...) rebuild reasoning params - * with their own effort maps and cannot express OpenAI's native "max". + * Whether a route provider delivers Mux-built openai providerOptions to OpenAI + * unchanged: OpenAI itself or a gateway marked passthrough in PROVIDER_DEFINITIONS + * (e.g. mux-gateway). Non-passthrough routes (openrouter, github-copilot, ...) + * rebuild reasoning params with their own effort maps and cannot express OpenAI's + * native "max". */ -function routeCanPassThroughOpenAIOptions(modelString: string): boolean { - const colonIndex = modelString.indexOf(":"); - if (colonIndex === -1) return true; // bare model id — no route info, assume direct - const route = modelString.slice(0, colonIndex).trim().toLowerCase(); +function isOpenAIPassthroughRoute(route: string): boolean { if (route === "openai") return true; const definition = PROVIDER_DEFINITIONS[route as keyof typeof PROVIDER_DEFINITIONS]; return definition != null && "passthrough" in definition && definition.passthrough === true; } +/** + * Route inference from the model string alone (UI path, where the resolved route + * is not yet known). A bare/namespaced id without a route prefix is assumed direct; + * the request path re-clamps with the actually-resolved route via + * clampThinkingLevelForRoute, covering routePriority-based rerouting. + */ +function routeCanPassThroughOpenAIOptions(modelString: string): boolean { + const colonIndex = modelString.indexOf(":"); + if (colonIndex === -1) return true; // bare model id — no route info, assume direct + return isOpenAIPassthroughRoute(modelString.slice(0, colonIndex).trim().toLowerCase()); +} + +/** + * Clamp a thinking level after route resolution. + * + * The UI policy can only infer the route from the model string, but routing may be + * decided at request time (routePriority/overrides) — e.g. `openai:gpt-5.6-sol` + * resolved through OpenRouter. Non-passthrough routes map "max" via their own effort + * tables (OpenRouter → "high", Copilot → "xhigh"), silently sending less effort than + * the user selected. Clamping to "xhigh" here keeps the request, metadata, and wire + * consistent with the best effort the route can express (xhigh is those routes' policy + * ceiling; their effort maps handle any further provider-side clamping). + * + * Only GPT-5.6's native max needs this: for every other model "max" and "xhigh" + * already share a wire value, so clamping would be a no-op. + */ +export function clampThinkingLevelForRoute( + modelString: string, + routeProvider: string | undefined, + level: ThinkingLevel +): ThinkingLevel { + if (level !== "max") return level; + if (!openaiSupportsNativeMaxEffort(stripModelProviderPrefixes(modelString))) return level; + const passthrough = + routeProvider != null + ? isOpenAIPassthroughRoute(routeProvider) + : routeCanPassThroughOpenAIOptions(modelString); + return passthrough ? level : "xhigh"; +} + /** * Returns the policy for a model that matches an explicit reasoning rule, or `null` * when the model falls through to {@link DEFAULT_THINKING_POLICY}. diff --git a/src/node/services/aiService.ts b/src/node/services/aiService.ts index 5318a2102d..0b090e1e2e 100644 --- a/src/node/services/aiService.ts +++ b/src/node/services/aiService.ts @@ -111,6 +111,7 @@ import { mergeGoalDefaults } from "@/common/utils/goals/resolveGoalSetIntent"; import { MULTI_PROJECT_CONFIG_KEY } from "@/common/constants/multiProject"; import { THINKING_LEVEL_OFF, type ThinkingLevel } from "@/common/types/thinking"; import { + clampThinkingLevelForRoute, enforceThinkingPolicy, resolveEffectiveThinkingLevel, resolveMinimumThinkingLevel, @@ -1087,7 +1088,7 @@ export class AIService extends EventEmitter { // disable thinking) so provider options, replay transforms, and metadata // all agree with the provider's actual thinking behavior. Providers config // is passed so aliases mapped to Mythos models get the same treatment. - const effectiveThinkingLevel: ThinkingLevel = resolveEffectiveThinkingLevel( + let effectiveThinkingLevel: ThinkingLevel = resolveEffectiveThinkingLevel( modelString, thinkingLevel, this.providerService.getConfig() @@ -1113,6 +1114,17 @@ export class AIService extends EventEmitter { routeProvider, } = modelResult.data; + // Re-clamp with the resolved route: routing may be decided at request time + // (routePriority/overrides), so an `openai:` GPT-5.6 model can end up on a + // non-passthrough route (OpenRouter/Copilot) that cannot express native "max". + // Must happen before provider options, headers, and metadata are built so + // they all agree with what the wire can deliver. + effectiveThinkingLevel = clampThinkingLevelForRoute( + canonicalModelString, + routeProvider, + effectiveThinkingLevel + ); + // Dump original messages for debugging log.debug_obj(`${workspaceId}/1_original_messages.json`, messages); @@ -2584,6 +2596,13 @@ export class AIService extends EventEmitter { return Err(formatSendMessageError(nextModelResult.error).message); } const next = nextModelResult.data; + // Same post-route clamp as the primary path: the fallback model's + // resolved route may not support GPT-5.6 native "max". + const routedNextThinkingLevel = clampThinkingLevelForRoute( + next.canonicalModelString, + next.routeProvider, + nextThinkingLevel + ); try { // Rebuild the toolset for the fallback model: provider-native @@ -2672,7 +2691,7 @@ export class AIService extends EventEmitter { prepareProviderRequestMessages( fallbackSourceMessages, next.canonicalProviderName, - nextThinkingLevel + routedNextThinkingLevel ); const nextFinalMessages = await prepareMessagesForProvider({ messagesWithSentinel: addInterruptedSentinel(nextProviderRequestMessages), @@ -2686,7 +2705,7 @@ export class AIService extends EventEmitter { workspacePath, abortSignal: combinedAbortSignal, providerForMessages: next.canonicalProviderName, - effectiveThinkingLevel: nextThinkingLevel, + effectiveThinkingLevel: routedNextThinkingLevel, modelString: next.canonicalModelString, anthropicCacheTtl: effectiveMuxProviderOptions.anthropic?.cacheTtl, workspaceId, @@ -2694,7 +2713,7 @@ export class AIService extends EventEmitter { const nextProviderOptions = buildProviderOptions( next.canonicalModelString, - nextThinkingLevel, + routedNextThinkingLevel, nextProviderRequestMessages, (id) => this.streamManager.isResponseIdLost(id), effectiveMuxProviderOptions, @@ -2711,7 +2730,7 @@ export class AIService extends EventEmitter { workspaceId, this.providerService.getConfig(), next.routeProvider, - nextThinkingLevel + routedNextThinkingLevel ); if (pendingRunMetadataId != null) { // Keep DevTools run correlation on fallback requests too. @@ -2756,7 +2775,7 @@ export class AIService extends EventEmitter { headers: nextHeaders, callSettingsOverrides: nextOverrides.standard, anthropicCacheTtl: effectiveMuxProviderOptions.anthropic?.cacheTtl ?? undefined, - thinkingLevel: nextThinkingLevel, + thinkingLevel: routedNextThinkingLevel, initialMetadataPatch: { routedThroughGateway: next.routedThroughGateway, ...(next.routeProvider != null ? { routeProvider: next.routeProvider } : {}),