Skip to content
5 changes: 4 additions & 1 deletion docs/config/models.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ 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 | `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` | |
| GPT-5.4 Nano | openai:gpt-5.4-nano | `gpt-nano` | |
| Codex 5.3 | openai:gpt-5.3-codex | `codex`, `codex-5.3` | |
Expand Down
4 changes: 4 additions & 0 deletions src/common/constants/codexOAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ export const CODEX_OAUTH_ALLOWED_MODELS = new Set<string>([
"gpt-5.2",
"gpt-5.4-mini",
"gpt-5.5",
// 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",
"gpt-5.2-codex",
"gpt-5.3-codex",
"gpt-5.3-codex-spark",
Expand Down
36 changes: 34 additions & 2 deletions src/common/constants/knownModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand All @@ -101,6 +102,37 @@ const MODEL_DEFINITIONS = {
warm: true,
tokenizerOverride: "openai/gpt-5",
},
// 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.
// 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: {
provider: "openai",
providerModelId: "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.
// $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",
Expand Down
10 changes: 10 additions & 0 deletions src/common/types/thinking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
30 changes: 28 additions & 2 deletions src/common/types/thinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -237,6 +242,24 @@ export function anthropicSupportsNativeXhigh(modelString: string): boolean {
);
}

/**
* Whether the given OpenAI model supports the native "max" reasoning effort
* (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-5\.6-(?:sol|terra|luna)(?!-[a-z])/.test(stripModelProviderPrefixes(modelString));
}

/**
* Whether the given Anthropic model rejects `thinking: { type: "disabled" }`.
*
Expand Down Expand Up @@ -276,6 +299,9 @@ export const OPENAI_REASONING_EFFORT: Record<ThinkingLevel, string | undefined>
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",
};

Expand Down
39 changes: 39 additions & 0 deletions src/common/utils/ai/providerOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,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, {
Expand Down
12 changes: 10 additions & 2 deletions src/common/utils/ai/providerOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
GEMINI_THINKING_BUDGETS,
OPENAI_REASONING_EFFORT,
OPENROUTER_REASONING_EFFORT,
openaiSupportsNativeMaxEffort,
} from "@/common/types/thinking";
import { isGeminiFlashThinkingLevelModelName } from "@/common/utils/thinking/policy";
import { resolveModelForMetadata } from "@/common/utils/providers/modelEntries";
Expand Down Expand Up @@ -353,8 +354,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
Expand All @@ -374,6 +373,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,
Expand Down
72 changes: 72 additions & 0 deletions src/common/utils/thinking/policy.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test";
import type { ProvidersConfigMap } from "@/common/orpc/types";
import {
clampThinkingLevelForRoute,
getThinkingPolicyForModel,
enforceThinkingPolicy,
resolveThinkingInput,
Expand Down Expand Up @@ -186,6 +187,77 @@ describe("getThinkingPolicyForModel", () => {
]);
});

test.each(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"])(
"returns all 6 levels including native max for %s",
(modelId) => {
expect(getThinkingPolicyForModel(`openai:${modelId}`)).toEqual([
"off",
"low",
"medium",
"high",
"xhigh",
"max",
]);
}
);

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",
"medium",
"high",
"xhigh",
"max",
]);
});

// 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",
]);
}
);

// 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",
Expand Down
Loading
Loading