diff --git a/open-sse/config/providers/registry/github/index.ts b/open-sse/config/providers/registry/github/index.ts
index e72c98dd4484..96d42ee19652 100644
--- a/open-sse/config/providers/registry/github/index.ts
+++ b/open-sse/config/providers/registry/github/index.ts
@@ -163,18 +163,24 @@ export const githubProvider: RegistryEntry = {
{
id: "gpt-5.6-sol",
name: "GPT-5.6 Sol",
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
targetFormat: "openai-responses",
maxOutputTokens: 128000,
},
{
id: "gpt-5.6-terra",
name: "GPT-5.6 Terra",
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
targetFormat: "openai-responses",
maxOutputTokens: 128000,
},
{
id: "gpt-5.6-luna",
name: "GPT-5.6 Luna",
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
targetFormat: "openai-responses",
maxOutputTokens: 128000,
},
diff --git a/open-sse/config/providers/registry/opencode/go/index.ts b/open-sse/config/providers/registry/opencode/go/index.ts
index 1826cdd9e209..d8536678176f 100644
--- a/open-sse/config/providers/registry/opencode/go/index.ts
+++ b/open-sse/config/providers/registry/opencode/go/index.ts
@@ -357,6 +357,7 @@ export const opencode_goProvider: RegistryEntry = {
id: "gpt-5.6-luna",
name: "GPT-5.6 Luna",
supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
targetFormat: "openai-responses",
maxOutputTokens: 128000,
},
diff --git a/open-sse/config/providers/registry/opencode/zen/index.ts b/open-sse/config/providers/registry/opencode/zen/index.ts
index a7ffec42add2..11d87134a70a 100644
--- a/open-sse/config/providers/registry/opencode/zen/index.ts
+++ b/open-sse/config/providers/registry/opencode/zen/index.ts
@@ -36,9 +36,30 @@ export const opencode_zenProvider: RegistryEntry = {
// opencode.ai/zen/v1 with the same key on both endpoints). #12196 made the
// same declaration for gpt-5.6-luna on opencode-go; the zen entries here
// never got it.
- { id: "gpt-5.6-sol", name: "GPT 5.6 Sol", targetFormat: "openai-responses" },
- { id: "gpt-5.6-terra", name: "GPT 5.6 Terra", targetFormat: "openai-responses" },
- { id: "gpt-5.6-luna", name: "GPT 5.6 Luna", targetFormat: "openai-responses" },
+ {
+ id: "gpt-5.6-sol",
+ name: "GPT 5.6 Sol",
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
+ targetFormat: "openai-responses",
+ maxOutputTokens: 128000,
+ },
+ {
+ id: "gpt-5.6-terra",
+ name: "GPT 5.6 Terra",
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
+ targetFormat: "openai-responses",
+ maxOutputTokens: 128000,
+ },
+ {
+ id: "gpt-5.6-luna",
+ name: "GPT 5.6 Luna",
+ supportsReasoning: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
+ targetFormat: "openai-responses",
+ maxOutputTokens: 128000,
+ },
{ id: "gpt-5.4", name: "GPT 5.4" },
{ id: "gpt-5.4-mini", name: "GPT 5.4 Mini" },
{ id: "gpt-5.4-nano", name: "GPT 5.4 Nano" },
diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts
index 3da468589790..9f52553471df 100644
--- a/open-sse/config/providers/shared.ts
+++ b/open-sse/config/providers/shared.ts
@@ -323,6 +323,7 @@ export const GPT_5_6_API_CAPABILITIES = {
supportsReasoning: true,
supportsVision: true,
supportsXHighEffort: true,
+ supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"],
contextLength: 1050000,
maxInputTokens: 922000,
maxOutputTokens: 128000,
diff --git a/src/lib/reasoningRouting/policy.ts b/src/lib/reasoningRouting/policy.ts
index d73d93c183ef..26be6a46702a 100644
--- a/src/lib/reasoningRouting/policy.ts
+++ b/src/lib/reasoningRouting/policy.ts
@@ -318,7 +318,11 @@ function capabilityFor(
if (capabilities.reasoningEffortsOverride && Array.isArray(declaredEfforts)) {
return "unsupported" as const;
}
- if (codexModelFamilySupportsExtendedEffort(model, targetEffort)) return "supported" as const;
+ // Strip the provider namespace (`openai/`, `github/`, `opencode-zen/`,
+ // `codex/`…) so every provider serving the family resolves the same way.
+ if (codexModelFamilySupportsExtendedEffort(modelIdForRegistry, targetEffort)) {
+ return "supported" as const;
+ }
if (capabilities.supportsThinking === null) return "unknown" as const;
return "unsupported" as const;
}
diff --git a/src/shared/components/ReasoningRoutingRules.tsx b/src/shared/components/ReasoningRoutingRules.tsx
index d9e5e109185b..e2c41d77733f 100644
--- a/src/shared/components/ReasoningRoutingRules.tsx
+++ b/src/shared/components/ReasoningRoutingRules.tsx
@@ -205,26 +205,39 @@ export default function ReasoningRoutingRules({
const targetModelForCapability =
form.targetKind === "model" ? form.targetModel : form.modelPattern;
+ const isLunaTarget = useMemo(() => {
+ const normalized = targetModelForCapability
+ .trim()
+ .toLowerCase()
+ .replace(/^[^/]+\//, "");
+ return /^gpt-5\.6-luna(?:-|$)/.test(normalized);
+ }, [targetModelForCapability]);
+
+ // gpt-5.6-luna accepts `max` but not `ultra`: a saved `ultra` is coerced to
+ // `max` so the editor never re-offers (or re-saves) a tier the upstream 400s.
+ const currentTargetEffort =
+ isLunaTarget && form.targetEffort === "ultra" ? "max" : form.targetEffort;
+
const effortOptions = useMemo(
() =>
- getReasoningRoutingTargetEffortOptions(targetModelForCapability, form.targetEffort).map(
+ getReasoningRoutingTargetEffortOptions(targetModelForCapability, currentTargetEffort).map(
(value) => ({ value, label: value })
),
- [form.targetEffort, targetModelForCapability]
+ [currentTargetEffort, targetModelForCapability]
);
const capabilityWarning = useMemo(() => {
if (form.effortMode === "inherit") return "";
- if (!(EXTENDED_REASONING_EFFORTS as readonly string[]).includes(form.targetEffort)) return "";
+ if (!(EXTENDED_REASONING_EFFORTS as readonly string[]).includes(currentTargetEffort)) return "";
if (form.targetKind === "combo") return t("extendedComboWarning");
if (!targetModelForCapability.trim()) return t("extendedUnknownWarning");
return supportsExtendedCodexEffort(
targetModelForCapability,
- form.targetEffort as "max" | "ultra"
+ currentTargetEffort as "max" | "ultra"
)
? ""
: t("extendedUnsupportedWarning");
- }, [form.effortMode, form.targetEffort, form.targetKind, t, targetModelForCapability]);
+ }, [currentTargetEffort, form.effortMode, form.targetKind, t, targetModelForCapability]);
const dirty = editorOpen && JSON.stringify(form) !== baseline;
const confirmDiscard = (action: () => void) => {
@@ -310,7 +323,7 @@ export default function ReasoningRoutingRules({
.split(",")
.map((tag) => tag.trim())
.filter(Boolean),
- targetEffort: form.effortMode === "inherit" ? null : form.targetEffort,
+ targetEffort: form.effortMode === "inherit" ? null : currentTargetEffort,
targetKind,
targetModel: targetKind === "model" ? form.targetModel || null : null,
targetComboId: targetKind === "combo" ? form.targetComboId || null : null,
@@ -756,13 +769,12 @@ export default function ReasoningRoutingRules({
options={[
{ value: "any", label: t("any") },
{ value: "missing", label: t("missing") },
- ...[
- ...STANDARD_REASONING_EFFORTS,
- ...EXTENDED_REASONING_EFFORTS,
- ].map((value) => ({
- value,
- label: value,
- })),
+ ...[...STANDARD_REASONING_EFFORTS, ...EXTENDED_REASONING_EFFORTS].map(
+ (value) => ({
+ value,
+ label: value,
+ })
+ ),
]}
/>
@@ -826,7 +838,7 @@ export default function ReasoningRoutingRules({
@@ -954,7 +966,7 @@ export default function ReasoningRoutingRules({
options={[
{ value: "missing", label: t("missing") },
{ value: "signal", label: t("signalOnly") },
- ...[...STANDARD_EFFORTS, ...EXTENDED_EFFORTS].map((value) => ({
+ ...[...STANDARD_REASONING_EFFORTS, ...EXTENDED_REASONING_EFFORTS].map((value) => ({
value,
label: value,
})),
diff --git a/src/shared/reasoning/reasoningRoutingEfforts.ts b/src/shared/reasoning/reasoningRoutingEfforts.ts
index bb2abff5b05b..6d1baea702ab 100644
--- a/src/shared/reasoning/reasoningRoutingEfforts.ts
+++ b/src/shared/reasoning/reasoningRoutingEfforts.ts
@@ -6,14 +6,16 @@ export const EXTENDED_REASONING_EFFORTS = ["max", "ultra"] as const;
type ExtendedReasoningEffort = (typeof EXTENDED_REASONING_EFFORTS)[number];
/**
- * The rules editor accepts `codex/`, `cx/` and `openai/` prefixed ids (#14961); the alias-set
- * lookup shared with the Codex executor (#14720) knows the first two, so strip `openai/` here.
+ * The rules editor accepts provider-prefixed ids (`codex/`, `cx/`, `openai/` #14961, and
+ * `github/`, `opencode-zen/`… #14059); the alias-set lookup shared with the Codex executor
+ * (#14720) matches bare ids, so strip any single provider namespace here — the same way the
+ * routing policy resolves `modelIdForRegistry`.
*/
export function supportsExtendedCodexEffort(
model: string,
effort: ExtendedReasoningEffort
): boolean {
- return codexModelFamilySupportsExtendedEffort(model.trim().replace(/^openai\//i, ""), effort);
+ return codexModelFamilySupportsExtendedEffort(model.trim().replace(/^[^/]+\//, ""), effort);
}
export function getReasoningRoutingTargetEffortOptions(
diff --git a/tests/unit/dashboard/reasoning-routing-efforts.test.ts b/tests/unit/dashboard/reasoning-routing-efforts.test.ts
index 7b023808bbbf..09ac481605d9 100644
--- a/tests/unit/dashboard/reasoning-routing-efforts.test.ts
+++ b/tests/unit/dashboard/reasoning-routing-efforts.test.ts
@@ -48,3 +48,16 @@ test("openai/ prefixed ids read the same Codex alias sets as the executor (#1472
"ultra",
]);
});
+
+test("github/ and opencode-zen/ prefixed luna ids offer max but not ultra (#14059)", () => {
+ for (const model of ["github/gpt-5.6-luna", "opencode-zen/gpt-5.6-luna"]) {
+ assert.deepEqual(getReasoningRoutingTargetEffortOptions(model, "medium"), [
+ "none",
+ "low",
+ "medium",
+ "high",
+ "xhigh",
+ "max",
+ ]);
+ }
+});
diff --git a/tests/unit/luna-reasoning-effort-400.test.ts b/tests/unit/luna-reasoning-effort-400.test.ts
new file mode 100644
index 000000000000..acc4ee9cf1cc
--- /dev/null
+++ b/tests/unit/luna-reasoning-effort-400.test.ts
@@ -0,0 +1,190 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { cleanupTempDataDir } from "../_setup/tempDataDir.ts";
+
+const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-luna-routing-"));
+process.env.DATA_DIR = TEST_DATA_DIR;
+process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "test-luna-routing-secret";
+
+const core = await import("../../src/lib/db/core.ts");
+const rulesDb = await import("../../src/lib/db/reasoningRoutingRules.ts");
+const policy = await import("../../src/lib/reasoningRouting/policy.ts");
+const { getModelTargetFormat } = await import("../../open-sse/config/providerModels.ts");
+const { resolveOpencodeTargetFormat } = await import("../../open-sse/executors/opencode.ts");
+
+test.after(() => {
+ core.resetDbInstance();
+ cleanupTempDataDir(TEST_DATA_DIR);
+});
+
+test("opencode-zen gpt-5.6 family routes to openai-responses", () => {
+ for (const modelId of ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]) {
+ assert.equal(
+ resolveOpencodeTargetFormat("opencode-zen", modelId),
+ "openai-responses",
+ `opencode-zen/${modelId} must route to openai-responses`
+ );
+ }
+});
+
+test("github and openai gpt-5.6-luna route to openai-responses", () => {
+ assert.equal(
+ getModelTargetFormat("openai", "gpt-5.6-luna"),
+ "openai-responses",
+ "openai/gpt-5.6-luna must route to openai-responses"
+ );
+ assert.equal(
+ getModelTargetFormat("github", "gpt-5.6-luna"),
+ "openai-responses",
+ "github/gpt-5.6-luna must route to openai-responses"
+ );
+});
+
+test("reasoning routing policy treats forced max on openai/gpt-5.6-luna as supported", async () => {
+ await rulesDb.createReasoningRoutingRule({
+ name: "force max on openai luna",
+ description: "",
+ scope: "model",
+ apiKeyId: null,
+ comboId: null,
+ connectionId: null,
+ modelPattern: "openai/gpt-5.6-luna",
+ sourceEffort: "any",
+ requestTags: [],
+ tagMatchMode: "any",
+ effortMode: "force",
+ targetEffort: "max",
+ targetKind: "keep",
+ targetModel: null,
+ targetComboId: null,
+ budgetAction: "preserve",
+ budgetTokens: null,
+ priority: 10,
+ enabled: true,
+ });
+
+ const decision = await policy.resolveReasoningRoutingRule({
+ sourceModel: "openai/gpt-5.6-luna",
+ sourceEffort: "missing",
+ hasReasoningSignal: false,
+ });
+
+ assert.ok(decision, "rule should match openai/gpt-5.6-luna");
+ assert.equal(decision.targetEffort, "max");
+ assert.equal(
+ decision.capability,
+ "supported",
+ "openai/gpt-5.6-luna must be supported for max effort (not 400 unsupported)"
+ );
+});
+
+test("reasoning routing policy treats forced ultra on gpt-5.6-luna as unsupported", async () => {
+ await rulesDb.createReasoningRoutingRule({
+ name: "force ultra on luna",
+ description: "",
+ scope: "model",
+ apiKeyId: null,
+ comboId: null,
+ connectionId: null,
+ modelPattern: "gpt-5.6-luna",
+ sourceEffort: "any",
+ requestTags: [],
+ tagMatchMode: "any",
+ effortMode: "force",
+ targetEffort: "ultra",
+ targetKind: "keep",
+ targetModel: null,
+ targetComboId: null,
+ budgetAction: "preserve",
+ budgetTokens: null,
+ priority: 20,
+ enabled: true,
+ });
+
+ const decision = await policy.resolveReasoningRoutingRule({
+ sourceModel: "gpt-5.6-luna",
+ sourceEffort: "missing",
+ hasReasoningSignal: false,
+ });
+
+ assert.ok(decision, "rule should match gpt-5.6-luna");
+ assert.equal(decision.targetEffort, "ultra");
+ assert.equal(decision.capability, "unsupported", "gpt-5.6-luna does not support ultra effort");
+});
+
+test("reasoning routing policy handles github and codex prefixed luna models", async () => {
+ await rulesDb.createReasoningRoutingRule({
+ name: "force max on github luna",
+ description: "",
+ scope: "model",
+ apiKeyId: null,
+ comboId: null,
+ connectionId: null,
+ modelPattern: "github/gpt-5.6-luna",
+ sourceEffort: "any",
+ requestTags: [],
+ tagMatchMode: "any",
+ effortMode: "force",
+ targetEffort: "max",
+ targetKind: "keep",
+ targetModel: null,
+ targetComboId: null,
+ budgetAction: "preserve",
+ budgetTokens: null,
+ priority: 30,
+ enabled: true,
+ });
+
+ const githubDecision = await policy.resolveReasoningRoutingRule({
+ sourceModel: "github/gpt-5.6-luna",
+ sourceEffort: "missing",
+ hasReasoningSignal: false,
+ });
+
+ assert.ok(githubDecision, "rule should match github/gpt-5.6-luna");
+ assert.equal(githubDecision.targetEffort, "max");
+ assert.equal(
+ githubDecision.capability,
+ "supported",
+ "github/gpt-5.6-luna must be supported for max effort"
+ );
+
+ await rulesDb.createReasoningRoutingRule({
+ name: "force ultra on codex luna",
+ description: "",
+ scope: "model",
+ apiKeyId: null,
+ comboId: null,
+ connectionId: null,
+ modelPattern: "codex/gpt-5.6-luna",
+ sourceEffort: "any",
+ requestTags: [],
+ tagMatchMode: "any",
+ effortMode: "force",
+ targetEffort: "ultra",
+ targetKind: "keep",
+ targetModel: null,
+ targetComboId: null,
+ budgetAction: "preserve",
+ budgetTokens: null,
+ priority: 40,
+ enabled: true,
+ });
+
+ const codexDecision = await policy.resolveReasoningRoutingRule({
+ sourceModel: "codex/gpt-5.6-luna",
+ sourceEffort: "missing",
+ hasReasoningSignal: false,
+ });
+
+ assert.ok(codexDecision, "rule should match codex/gpt-5.6-luna");
+ assert.equal(codexDecision.targetEffort, "ultra");
+ assert.equal(
+ codexDecision.capability,
+ "unsupported",
+ "codex/gpt-5.6-luna must not support ultra effort"
+ );
+});