From 27384973b3809448980f2a43d844bea6c510a340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B5nis=20Ormisson?= Date: Thu, 24 Sep 2026 09:47:47 +0300 Subject: [PATCH 1/2] feat(sse): support GPT-6 Sol and Luna in Codex Port upstream #14677 and the related reasoning-routing fix from #14720. Preserve the fork deployment workflow and existing GPT-5.6/Astra behavior. Co-authored-by: Quang Blue --- changelog.d/features/gpt6-sol-luna-fork.md | 1 + .../config/providers/registry/codex/index.ts | 34 +++ open-sse/executors/codex.ts | 18 +- open-sse/executors/codex/reasoningSuffix.ts | 43 ++-- .../request/openai-responses/helpers.ts | 6 +- src/lib/reasoningRouting/policy.ts | 16 +- src/lib/usage/costCalculator.ts | 3 +- src/lib/vscode/reasoningMetadata.ts | 8 +- .../components/ReasoningRoutingRules.tsx | 18 +- .../constants/pricing/oauth-subscriptions.ts | 30 +++ src/shared/reasoning/codexExtendedEffort.ts | 44 ++++ src/shared/reasoning/effortStandardization.ts | 5 +- tests/unit/codex-gpt6-sol-luna.test.ts | 198 ++++++++++++++++++ ...ning-routing-codex-extended-effort.test.ts | 176 ++++++++++++++++ 14 files changed, 543 insertions(+), 57 deletions(-) create mode 100644 changelog.d/features/gpt6-sol-luna-fork.md create mode 100644 src/shared/reasoning/codexExtendedEffort.ts create mode 100644 tests/unit/codex-gpt6-sol-luna.test.ts create mode 100644 tests/unit/reasoning-routing-codex-extended-effort.test.ts diff --git a/changelog.d/features/gpt6-sol-luna-fork.md b/changelog.d/features/gpt6-sol-luna-fork.md new file mode 100644 index 000000000000..ecb8967932fe --- /dev/null +++ b/changelog.d/features/gpt6-sol-luna-fork.md @@ -0,0 +1 @@ +- Add GPT-6 Sol and Luna to the Codex catalog with their supported reasoning variants, pricing, Fast-mode cost estimates and VS Code effort metadata. Align reasoning-routing rules with the executor's supported Max/Ultra models. Ported from upstream PRs #14677 and #14720 by Quang Blue. diff --git a/open-sse/config/providers/registry/codex/index.ts b/open-sse/config/providers/registry/codex/index.ts index 43087e33c488..e49d092bf986 100644 --- a/open-sse/config/providers/registry/codex/index.ts +++ b/open-sse/config/providers/registry/codex/index.ts @@ -175,6 +175,40 @@ export const codexProvider: RegistryEntry = { name: "GPT 6 Astra (Low)", ...GPT_5_6_CODEX_CAPABILITIES, }, + // Sol and Luna share Astra's Codex windows; Sol offers low..ultra, Luna low..max. + { id: "gpt-6-sol", name: "GPT 6 Sol", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6-sol-ultra", name: "GPT 6 Sol (Ultra)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6-sol-max", name: "GPT 6 Sol (Max)", ...GPT_5_6_CODEX_CAPABILITIES }, + { + id: "gpt-6-sol-xhigh", + name: "GPT 6 Sol (xHigh)", + ...GPT_5_6_CODEX_CAPABILITIES, + timeoutMs: 1200000, + }, + { + id: "gpt-6-sol-high", + name: "GPT 6 Sol (High)", + ...GPT_5_6_CODEX_CAPABILITIES, + timeoutMs: 1200000, + }, + { id: "gpt-6-sol-medium", name: "GPT 6 Sol (Medium)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6-sol-low", name: "GPT 6 Sol (Low)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6-luna", name: "GPT 6 Luna", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6-luna-max", name: "GPT 6 Luna (Max)", ...GPT_5_6_CODEX_CAPABILITIES }, + { + id: "gpt-6-luna-xhigh", + name: "GPT 6 Luna (xHigh)", + ...GPT_5_6_CODEX_CAPABILITIES, + timeoutMs: 1200000, + }, + { + id: "gpt-6-luna-high", + name: "GPT 6 Luna (High)", + ...GPT_5_6_CODEX_CAPABILITIES, + timeoutMs: 1200000, + }, + { id: "gpt-6-luna-medium", name: "GPT 6 Luna (Medium)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6-luna-low", name: "GPT 6 Luna (Low)", ...GPT_5_6_CODEX_CAPABILITIES }, // gpt-5.5 codex OAuth backend caps context at 400K (not the public-API // 1.05M). Public refs : openai/codex#19208, #19319, #19464 ; // opencode#24171. max_output_tokens is stripped server-side diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 8c16e29fbd4a..3712c8e165fd 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -53,8 +53,8 @@ export { import { isCodexFreePlan, normalizeCodexTools } from "./codex/tools.ts"; import { CODEX_EFFORT_ORDER as EFFORT_ORDER, - GPT_5_6_ULTRA_ALIAS_MODELS, - GPT_6_ALIAS_MODELS, + CODEX_ULTRA_ALIAS_MODELS, + getCodexAliasEffortCap, splitCodexReasoningSuffix, type CodexEffortLevel as EffortLevel, } from "./codex/reasoningSuffix.ts"; @@ -173,8 +173,7 @@ function isCodexResponsesLiteRequest( // delegation silently breaks while the request still returns HTTP 200 (issue #7821). function isCodexDelegationDependentModel(model: unknown): boolean { const { baseModel, effort } = splitCodexReasoningSuffix(model); - if (effort === "ultra" && GPT_5_6_ULTRA_ALIAS_MODELS.has(baseModel)) return true; - if (effort === "ultra" && GPT_6_ALIAS_MODELS.has(baseModel)) return true; + if (effort === "ultra" && CODEX_ULTRA_ALIAS_MODELS.has(baseModel)) return true; if (effort === "max" && baseModel === "gpt-5.6-luna") return true; return false; } @@ -326,15 +325,10 @@ function normalizeServiceTierValue(value: unknown): string | undefined { } /** - * Maximum reasoning effort allowed per Codex model. - * Models not listed here retain the legacy xhigh cap. - * Update this table when Codex releases new models with different caps. + * Maximum reasoning effort per Codex model. Max/ultra-tier models come from the alias + * sets in reasoningSuffix.ts; everything else unlisted keeps the xhigh cap. */ const MAX_EFFORT_BY_MODEL: Record = { - "gpt-6-astra": "ultra", - "gpt-5.6-sol": "ultra", - "gpt-5.6-terra": "ultra", - "gpt-5.6-luna": "max", "gpt-5.3-codex": "xhigh", "gpt-5.1-codex-max": "xhigh", "gpt-5-mini": "high", @@ -347,7 +341,7 @@ const MAX_EFFORT_BY_MODEL: Record = { * Returns the original value if within limits, or the cap if it exceeds it. */ function clampEffort(model: string, requested: string): string { - const max: EffortLevel = MAX_EFFORT_BY_MODEL[model] ?? "xhigh"; + const max: EffortLevel = MAX_EFFORT_BY_MODEL[model] ?? getCodexAliasEffortCap(model) ?? "xhigh"; const reqIdx = EFFORT_ORDER.indexOf(requested as EffortLevel); const maxIdx = EFFORT_ORDER.indexOf(max); if (reqIdx > maxIdx) { diff --git a/open-sse/executors/codex/reasoningSuffix.ts b/open-sse/executors/codex/reasoningSuffix.ts index 29fc1aa66860..627f2a339fb0 100644 --- a/open-sse/executors/codex/reasoningSuffix.ts +++ b/open-sse/executors/codex/reasoningSuffix.ts @@ -8,36 +8,47 @@ export const CODEX_EFFORT_ORDER = [ "ultra", ] as const; export type CodexEffortLevel = (typeof CODEX_EFFORT_ORDER)[number]; -export const GPT_5_6_MAX_ALIAS_MODELS = new Set(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]); -export const GPT_5_6_ULTRA_ALIAS_MODELS = new Set(["gpt-5.6-sol", "gpt-5.6-terra"]); -export const GPT_6_ALIAS_MODELS = new Set(["gpt-6-astra"]); +export const CODEX_MAX_ALIAS_MODELS = new Set([ + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + "gpt-6-astra", + "gpt-6-sol", + "gpt-6-luna", +]); +export const CODEX_ULTRA_ALIAS_MODELS = new Set([ + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-6-astra", + "gpt-6-sol", +]); + +/** Highest effort a max/ultra-tier base model accepts, or null for other models. */ +export function getCodexAliasEffortCap(model: string): CodexEffortLevel | null { + if (CODEX_ULTRA_ALIAS_MODELS.has(model)) return "ultra"; + if (CODEX_MAX_ALIAS_MODELS.has(model)) return "max"; + return null; +} export function splitCodexReasoningSuffix(model: unknown): { baseModel: string; effort: CodexEffortLevel | null; } { const modelId = typeof model === "string" ? model : ""; - const gpt56Match = /^(gpt-5\.6-(?:sol|terra|luna))(?:-(max|ultra)|\((max|ultra)\))$/.exec( - modelId - ); - if (gpt56Match) { - const [, baseModel, hyphenEffort, parenthesizedEffort] = gpt56Match; + const maxTierMatch = /^(.+?)(?:-(max|ultra)|\((max|ultra)\))$/.exec(modelId); + if (maxTierMatch) { + const [, baseModel, hyphenEffort, parenthesizedEffort] = maxTierMatch; const effort = hyphenEffort ?? parenthesizedEffort; const supportedModels = parenthesizedEffort - ? GPT_5_6_MAX_ALIAS_MODELS + ? CODEX_MAX_ALIAS_MODELS : effort === "ultra" - ? GPT_5_6_ULTRA_ALIAS_MODELS - : GPT_5_6_MAX_ALIAS_MODELS; + ? CODEX_ULTRA_ALIAS_MODELS + : CODEX_MAX_ALIAS_MODELS; if (supportedModels.has(baseModel)) { return { baseModel, effort: effort as CodexEffortLevel }; } } - const gpt6Match = /^(gpt-6-astra)-(max|ultra)$/.exec(modelId); - if (gpt6Match && GPT_6_ALIAS_MODELS.has(gpt6Match[1])) { - return { baseModel: gpt6Match[1], effort: gpt6Match[2] as CodexEffortLevel }; - } - for (const effort of ["none", "low", "medium", "high", "xhigh"] as const) { if (modelId.endsWith(`-${effort}`)) { return { baseModel: modelId.slice(0, -`-${effort}`.length), effort }; diff --git a/open-sse/translator/request/openai-responses/helpers.ts b/open-sse/translator/request/openai-responses/helpers.ts index 7f31eb99ea7c..8065bddb3853 100644 --- a/open-sse/translator/request/openai-responses/helpers.ts +++ b/open-sse/translator/request/openai-responses/helpers.ts @@ -50,8 +50,8 @@ export function imageUrlToText(value: unknown): string { return toString(record.url); } -const CODEX_GPT_5_6_MODEL_PATTERN = - /^gpt-5\.6-(?:sol|terra|luna)(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/; +const CODEX_MAX_EFFORT_MODEL_PATTERN = + /^(?:gpt-5\.6-(?:sol|terra|luna)|gpt-6-(?:astra|sol|luna))(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/; const KIRO_GPT_5_6_MODEL_PATTERN = /^(?:kiro|kr)\/gpt-5\.6-(?:sol|terra|luna)(?:-(?:none|low|medium|high|xhigh|max))?$/; @@ -61,7 +61,7 @@ function supportsNativeMaxReasoningEffort(model: unknown): boolean { .toLowerCase() .replace(/^(?:codex|cx)\//, ""); return ( - CODEX_GPT_5_6_MODEL_PATTERN.test(normalizedModel) || + CODEX_MAX_EFFORT_MODEL_PATTERN.test(normalizedModel) || KIRO_GPT_5_6_MODEL_PATTERN.test(toString(model).trim().toLowerCase()) ); } diff --git a/src/lib/reasoningRouting/policy.ts b/src/lib/reasoningRouting/policy.ts index 54647a9badb6..bd3c65074e63 100644 --- a/src/lib/reasoningRouting/policy.ts +++ b/src/lib/reasoningRouting/policy.ts @@ -9,6 +9,10 @@ import { import { getResolvedModelCapabilities } from "@/lib/modelCapabilities"; import { normalizeRoutingTags } from "@/domain/tagRouter"; import { splitClaudeEffortSuffix } from "@omniroute/open-sse/config/providerModels.ts"; +import { + codexModelFamilySupportsExtendedEffort, + isCodexExtendedEffortBaseModel, +} from "@/shared/reasoning/codexExtendedEffort"; type JsonRecord = Record; const EFFORTS = new Set([ @@ -76,8 +80,9 @@ function splitGenericEffortSuffix(model: string): { } function supportsCodexSuffix(candidate: string, normalizedBase: string): boolean { - if (candidate === "max") return /^gpt-5\.6-(?:sol|terra|luna)$/.test(normalizedBase); - if (candidate === "ultra") return /^gpt-5\.6-(?:sol|terra)$/.test(normalizedBase); + if (candidate === "max" || candidate === "ultra") { + return isCodexExtendedEffortBaseModel(normalizedBase, candidate); + } return true; } @@ -264,12 +269,7 @@ function capabilityFor( const capabilities = getResolvedModelCapabilities(model); if (capabilities.supportsThinking === false) return "unsupported" as const; if (targetEffort === "max" || targetEffort === "ultra") { - const normalized = model.toLowerCase().replace(/^(?:codex|cx)\//, ""); - const supported = - targetEffort === "ultra" - ? /^gpt-5\.6-(?:sol|terra)(?:-|$)/.test(normalized) - : /^gpt-5\.6-(?:sol|terra|luna)(?:-|$)/.test(normalized); - if (supported) return "supported" as const; + if (codexModelFamilySupportsExtendedEffort(model, targetEffort)) return "supported" as const; if (capabilities.supportsThinking === null) return "unknown" as const; return "unsupported" as const; } diff --git a/src/lib/usage/costCalculator.ts b/src/lib/usage/costCalculator.ts index ff405e54599f..176808660818 100644 --- a/src/lib/usage/costCalculator.ts +++ b/src/lib/usage/costCalculator.ts @@ -101,7 +101,8 @@ export function getCodexFastCostMultiplier( const modelKey = stripCodexEffortSuffix(normalizeModelName(String(model || "")).toLowerCase()); const compactModelKey = modelKey.replace(/-/g, ""); - if (modelKey === "gpt-6-astra" || compactModelKey === "gpt6astra") return 2.5; + // Codex GPT-6 Fast is 2.5x Standard (https://developers.openai.com/codex/pricing). + if (/^gpt6(?:astra|sol|luna)$/.test(compactModelKey)) return 2.5; if ( /^gpt-5\.6-(?:sol|terra|luna)$/.test(modelKey) || /^gpt5\.6(?:sol|terra|luna)$/.test(compactModelKey) diff --git a/src/lib/vscode/reasoningMetadata.ts b/src/lib/vscode/reasoningMetadata.ts index a11ff0c2574e..9c954332cbfa 100644 --- a/src/lib/vscode/reasoningMetadata.ts +++ b/src/lib/vscode/reasoningMetadata.ts @@ -17,7 +17,8 @@ export type VscodeCatalogModel = { }; const STANDARD_EFFORT_SUFFIX_PATTERN = /-(xhigh|high|medium|low|none)$/i; -const GPT_5_6_EXTENDED_EFFORT_SUFFIX_PATTERN = /^(.*gpt-5\.6-(?:sol|terra|luna))-(max|ultra)$/i; +const CODEX_EXTENDED_EFFORT_SUFFIX_PATTERN = + /^(.*gpt-(?:5\.6-(?:sol|terra|luna)|6-(?:astra|sol|luna)))-(max|ultra)$/i; const DEFAULT_REASONING_EFFORT = "none"; const KNOWN_REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh", "max", "ultra"]); @@ -41,7 +42,7 @@ export function getCatalogModelName(model: VscodeCatalogModel) { } function matchReasoningEffortSuffix(modelId: string) { - const extendedMatch = modelId.match(GPT_5_6_EXTENDED_EFFORT_SUFFIX_PATTERN); + const extendedMatch = modelId.match(CODEX_EXTENDED_EFFORT_SUFFIX_PATTERN); if (extendedMatch?.[1] && extendedMatch[2]) { return { baseModelId: extendedMatch[1], effort: extendedMatch[2].toLowerCase() }; } @@ -190,9 +191,10 @@ function getCodexGpt56DefaultReasoningEffort(model: VscodeCatalogModel) { .trim() .toLowerCase(); const match = providerModelId.match( - /^gpt-5\.6-(sol|terra|luna)(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/ + /^gpt-(?:5\.6-(sol|terra|luna)|6-(?:astra|sol|luna))(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/ ); if (!match) return undefined; + // The live catalog defaults GPT-5.6 Sol to low and every other model here to medium. return match[1] === "sol" ? "low" : "medium"; } diff --git a/src/shared/components/ReasoningRoutingRules.tsx b/src/shared/components/ReasoningRoutingRules.tsx index c087a83593f6..80cc63c7eb61 100644 --- a/src/shared/components/ReasoningRoutingRules.tsx +++ b/src/shared/components/ReasoningRoutingRules.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslations } from "next-intl"; import { Button, Card, Input, Select, Toggle } from "@/shared/components"; +import { codexModelFamilySupportsExtendedEffort } from "@/shared/reasoning/codexExtendedEffort"; type RuleScope = "global" | "apiKey" | "combo" | "model" | "connection"; type TargetKind = "keep" | "model" | "combo"; @@ -83,16 +84,6 @@ function emptyRule(apiKeyId?: string): FormState { }; } -function supportsExtendedCodexEffort(model: string, effort: "max" | "ultra"): boolean { - const normalized = model - .trim() - .toLowerCase() - .replace(/^(?:codex|cx)\//, ""); - return effort === "ultra" - ? /^gpt-5\.6-(?:sol|terra)(?:-|$)/.test(normalized) - : /^gpt-5\.6-(?:sol|terra|luna)(?:-|$)/.test(normalized); -} - export default function ReasoningRoutingRules({ apiKeyId }: { apiKeyId?: string }) { const t = useTranslations("reasoningRouting"); const [rules, setRules] = useState([]); @@ -155,7 +146,10 @@ export default function ReasoningRoutingRules({ apiKeyId }: { apiKeyId?: string const values = [...STANDARD_EFFORTS]; for (const effort of EXTENDED_EFFORTS) { if ( - supportsExtendedCodexEffort(targetModelForCapability, effort as "max" | "ultra") || + codexModelFamilySupportsExtendedEffort( + targetModelForCapability, + effort as "max" | "ultra" + ) || form.targetEffort === effort ) { values.push(effort); @@ -169,7 +163,7 @@ export default function ReasoningRoutingRules({ apiKeyId }: { apiKeyId?: string if (!EXTENDED_EFFORTS.includes(form.targetEffort)) return ""; if (form.targetKind === "combo") return t("extendedComboWarning"); if (!targetModelForCapability.trim()) return t("extendedUnknownWarning"); - return supportsExtendedCodexEffort( + return codexModelFamilySupportsExtendedEffort( targetModelForCapability, form.targetEffort as "max" | "ultra" ) diff --git a/src/shared/constants/pricing/oauth-subscriptions.ts b/src/shared/constants/pricing/oauth-subscriptions.ts index 04966178f8ed..84a5ccf12cd0 100644 --- a/src/shared/constants/pricing/oauth-subscriptions.ts +++ b/src/shared/constants/pricing/oauth-subscriptions.ts @@ -19,6 +19,23 @@ const ANTIGRAVITY_GEMINI_3_7_PRICING = { "gemini-3.7-flash-high": GEMINI_3_7_FLASH_PROMO_PRICING, }; +// Codex Standard: Sol 50 / 5 / 250 and Luna 2.5 / 0.25 / 12.5 credits per MTok, +// converted at 25 credits per USD. https://developers.openai.com/codex/pricing +const GPT_6_SOL_CODEX_PRICING = { + input: 2.0, + output: 10.0, + cached: 0.2, + reasoning: 10.0, + cache_creation: 2.5, +}; +const GPT_6_LUNA_CODEX_PRICING = { + input: 0.1, + output: 0.5, + cached: 0.01, + reasoning: 0.5, + cache_creation: 0.125, +}; + export const DEFAULT_PRICING_OAUTH = { cc: { "claude-fable-5": { @@ -117,6 +134,19 @@ export const DEFAULT_PRICING_OAUTH = { "gpt-6-astra-high": GPT_6_ASTRA_PRICING, "gpt-6-astra-medium": GPT_6_ASTRA_PRICING, "gpt-6-astra-low": GPT_6_ASTRA_PRICING, + "gpt-6-sol": GPT_6_SOL_CODEX_PRICING, + "gpt-6-sol-ultra": GPT_6_SOL_CODEX_PRICING, + "gpt-6-sol-max": GPT_6_SOL_CODEX_PRICING, + "gpt-6-sol-xhigh": GPT_6_SOL_CODEX_PRICING, + "gpt-6-sol-high": GPT_6_SOL_CODEX_PRICING, + "gpt-6-sol-medium": GPT_6_SOL_CODEX_PRICING, + "gpt-6-sol-low": GPT_6_SOL_CODEX_PRICING, + "gpt-6-luna": GPT_6_LUNA_CODEX_PRICING, + "gpt-6-luna-max": GPT_6_LUNA_CODEX_PRICING, + "gpt-6-luna-xhigh": GPT_6_LUNA_CODEX_PRICING, + "gpt-6-luna-high": GPT_6_LUNA_CODEX_PRICING, + "gpt-6-luna-medium": GPT_6_LUNA_CODEX_PRICING, + "gpt-6-luna-low": GPT_6_LUNA_CODEX_PRICING, // GPT 5.5 "gpt-5.5": GPT_5_5_PRICING, "gpt5.5": GPT_5_5_PRICING, diff --git a/src/shared/reasoning/codexExtendedEffort.ts b/src/shared/reasoning/codexExtendedEffort.ts new file mode 100644 index 000000000000..8eb79dce0755 --- /dev/null +++ b/src/shared/reasoning/codexExtendedEffort.ts @@ -0,0 +1,44 @@ +import { + CODEX_MAX_ALIAS_MODELS, + CODEX_ULTRA_ALIAS_MODELS, +} from "@omniroute/open-sse/executors/codex/reasoningSuffix.ts"; + +// Which Codex models accept the `max` / `ultra` reasoning tiers, read from the +// alias sets the Codex executor uses so routing rules and the rules editor +// cannot drift from what the executor serves. + +export type CodexExtendedEffort = "max" | "ultra"; + +function modelsFor(effort: CodexExtendedEffort): ReadonlySet { + return effort === "ultra" ? CODEX_ULTRA_ALIAS_MODELS : CODEX_MAX_ALIAS_MODELS; +} + +function normalizeCodexModelId(model: string): string { + return model + .trim() + .toLowerCase() + .replace(/^(?:codex|cx)\//, ""); +} + +/** True when `model` (optionally `codex/` or `cx/` prefixed) is exactly a base model that accepts `effort`. */ +export function isCodexExtendedEffortBaseModel( + model: string, + effort: CodexExtendedEffort +): boolean { + return modelsFor(effort).has(normalizeCodexModelId(model)); +} + +/** + * Like {@link isCodexExtendedEffortBaseModel}, but also accepts the base + * model's variants (`-`, e.g. `cx/gpt-6-astra-high`). + */ +export function codexModelFamilySupportsExtendedEffort( + model: string, + effort: CodexExtendedEffort +): boolean { + const normalized = normalizeCodexModelId(model); + for (const base of modelsFor(effort)) { + if (normalized === base || normalized.startsWith(`${base}-`)) return true; + } + return false; +} diff --git a/src/shared/reasoning/effortStandardization.ts b/src/shared/reasoning/effortStandardization.ts index 7f0edcd41c41..497596ace854 100644 --- a/src/shared/reasoning/effortStandardization.ts +++ b/src/shared/reasoning/effortStandardization.ts @@ -34,19 +34,20 @@ export function extendCodexGpt56EffortValues( if (!normalizedModel) return values; const match = normalizedModel.match( - /^gpt-5\.6-(sol|terra|luna)(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/ + /^gpt-(?:5\.6-(sol|terra|luna)|6-(astra|sol|luna))(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/ ); if (!match) return values; const isKiroProvider = normalizedProvider === "kiro" || normalizedProvider === "kr"; if (isKiroProvider) { + if (!match[1]) return values; return values.includes("max") ? values : [...values, "max"]; } if (normalizedProvider !== "codex" && normalizedProvider !== "cx") return values; const nativeValues = ["low", "medium", "high", "xhigh", "max"]; - return match[1] === "luna" ? nativeValues : [...nativeValues, "ultra"]; + return (match[1] || match[2]) === "luna" ? nativeValues : [...nativeValues, "ultra"]; } /** diff --git a/tests/unit/codex-gpt6-sol-luna.test.ts b/tests/unit/codex-gpt6-sol-luna.test.ts new file mode 100644 index 000000000000..1504ad504358 --- /dev/null +++ b/tests/unit/codex-gpt6-sol-luna.test.ts @@ -0,0 +1,198 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { getModelsByProviderId } from "../../open-sse/config/providerModels.ts"; +import { CodexExecutor } from "../../open-sse/executors/codex.ts"; +import { openaiToOpenAIResponsesRequest } from "../../open-sse/translator/request/openai-responses/toResponses.ts"; +import { getPricingForModel } from "../../src/shared/constants/pricing.ts"; +import { getCodexFastCostMultiplier } from "../../src/lib/usage/costCalculator.ts"; +import { extendCodexGpt56EffortValues } from "../../src/shared/reasoning/effortStandardization.ts"; +import * as reasoningMetadata from "../../src/lib/vscode/reasoningMetadata.ts"; + +// The live Codex catalog (client 0.155.1) advertises Sol at low..ultra and Luna at +// low..max, mirroring the GPT-5.6 Sol/Luna split. +const FAMILIES = [ + { model: "gpt-6-sol", efforts: ["ultra", "max", "xhigh", "high", "medium", "low"] }, + { model: "gpt-6-luna", efforts: ["max", "xhigh", "high", "medium", "low"] }, +] as const; + +test.after(async () => { + const { resetDbInstance } = await import("../../src/lib/db/core.ts"); + resetDbInstance(); +}); + +function transform(model: string, body: Record) { + return new CodexExecutor().transformRequest(model, { model, input: [], ...body }, false, { + requestEndpointPath: "/responses", + }); +} + +test("Codex exposes GPT-6 Sol and Luna with their effort variants", () => { + for (const provider of ["codex", "codex-app-server"]) { + const models = getModelsByProviderId(provider); + for (const { model, efforts } of FAMILIES) { + const ids = [model, ...efforts.map((effort) => `${model}-${effort}`)]; + assert.deepEqual( + models.filter((entry) => entry.id.startsWith(`${model}`)).map((entry) => entry.id), + ids, + `${provider}/${model}` + ); + for (const id of ids) { + const entry = models.find((candidate) => candidate.id === id); + assert.ok(entry, `${provider}/${id}`); + assert.equal(entry.contextLength, 872000); + assert.equal(entry.maxOutputTokens, 128000); + assert.equal(entry.targetFormat, "openai-responses"); + assert.equal(entry.supportsReasoning, true); + assert.equal(entry.supportsVision, true); + assert.equal(entry.supportsXHighEffort, true); + } + } + } +}); + +test("GPT-6 Sol and Luna effort aliases reach Codex as the base model", () => { + for (const { model, efforts } of FAMILIES) { + for (const effort of efforts) { + const result = transform(`${model}-${effort}`, {}); + assert.equal(result.model, model, `${model}-${effort}`); + assert.equal(result.reasoning.effort, effort === "ultra" ? "max" : effort, effort); + } + } +}); + +test("Explicit max reasoning is not clamped to xhigh for GPT-6 Sol and Luna", () => { + assert.equal(transform("gpt-6-sol", { reasoning: { effort: "max" } }).reasoning.effort, "max"); + assert.equal(transform("gpt-6-sol", { reasoning: { effort: "ultra" } }).reasoning.effort, "max"); + assert.equal(transform("gpt-6-luna", { reasoning: { effort: "max" } }).reasoning.effort, "max"); + // Luna tops out at max, like GPT-5.6 Luna. + assert.equal(transform("gpt-6-luna", { reasoning: { effort: "ultra" } }).reasoning.effort, "max"); +}); + +test("Luna has no ultra alias, so the suffix is not split off", () => { + assert.equal(transform("gpt-6-luna-ultra", {}).model, "gpt-6-luna-ultra"); +}); + +async function runResponsesLiteRequest(model: string): Promise> { + const originalFetch = globalThis.fetch; + const captured: Record[] = []; + globalThis.fetch = async (_url, init) => { + captured.push(JSON.parse(String(init?.body || "{}"))); + return new Response(JSON.stringify({ id: "resp_lite", object: "response" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + try { + await new CodexExecutor().execute({ + model, + body: { _nativeCodexPassthrough: true, model, input: [], parallel_tool_calls: true }, + stream: true, + credentials: { accessToken: "codex-token" }, + clientHeaders: { "X-OpenAI-Internal-Codex-Responses-Lite": "true" }, + }); + } finally { + globalThis.fetch = originalFetch; + } + assert.equal(captured.length, 1); + return captured[0]; +} + +test("Responses Lite keeps parallel tool calls only for Sol ultra delegation", async () => { + // The live catalog describes only `ultra` as automatic task delegation. + assert.equal((await runResponsesLiteRequest("gpt-6-sol-ultra")).parallel_tool_calls, true); + assert.equal((await runResponsesLiteRequest("gpt-6-sol-max")).parallel_tool_calls, false); + assert.equal((await runResponsesLiteRequest("gpt-6-luna-max")).parallel_tool_calls, false); +}); + +test("Catalog effort tiers follow the live Codex levels for GPT-6 models", () => { + const base = ["none", "low", "medium", "high", "xhigh"]; + const withUltra = ["low", "medium", "high", "xhigh", "max", "ultra"]; + const withMax = ["low", "medium", "high", "xhigh", "max"]; + for (const provider of ["codex", "cx"]) { + assert.deepEqual(extendCodexGpt56EffortValues(provider, "gpt-6-astra", base), withUltra); + assert.deepEqual(extendCodexGpt56EffortValues(provider, "gpt-6-sol", base), withUltra); + assert.deepEqual(extendCodexGpt56EffortValues(provider, "gpt-6-luna-max", base), withMax); + } + // Kiro's GPT-5.6 max extension does not reach models Kiro does not serve. + assert.deepEqual(extendCodexGpt56EffortValues("kiro", "gpt-6-sol", base), base); +}); + +test("VS Code reasoning metadata knows GPT-6 Sol and Luna efforts and defaults", () => { + const model = (id: string) => ({ id, owned_by: "codex", capabilities: { reasoning: true } }); + const sol = model("cx/gpt-6-sol"); + const luna = model("cx/gpt-6-luna"); + const solValues = reasoningMetadata.getReasoningEffortValues(sol); + assert.deepEqual(solValues, ["low", "medium", "high", "xhigh", "max", "ultra"]); + assert.deepEqual(reasoningMetadata.getReasoningEffortValues(luna), [ + "low", + "medium", + "high", + "xhigh", + "max", + ]); + // The live catalog's default_reasoning_level is medium for every GPT-6 model. + for (const id of ["cx/gpt-6-astra", "cx/gpt-6-sol", "cx/gpt-6-luna"]) { + assert.equal(reasoningMetadata.getDefaultReasoningEffort(model(id)), "medium", id); + } + assert.equal( + reasoningMetadata.inferSelectedReasoningEffort(model("cx/gpt-6-sol-ultra"), solValues), + "ultra" + ); + assert.equal( + reasoningMetadata.getReasoningVariantBaseModelId("cx/gpt-6-sol-max"), + "cx/gpt-6-sol" + ); + assert.equal( + reasoningMetadata.getReasoningVariantBaseModelId("cx/gpt-6-luna-max"), + "cx/gpt-6-luna" + ); +}); + +test("Parenthesized Sol and Luna effort overrides keep the reasoning summary", () => { + for (const model of ["gpt-6-sol(ultra)", "gpt-6-sol(max)", "gpt-6-luna(max)"]) { + const result = transform(model, { reasoning: { effort: "low", summary: "detailed" } }); + assert.equal(result.model, model.slice(0, model.indexOf("(")), model); + assert.equal(result.reasoning.effort, "max", model); + assert.equal(result.reasoning.summary, "detailed", model); + } +}); + +test("Chat-to-Codex translation preserves max reasoning for GPT-6 Sol and Luna", () => { + for (const { model } of FAMILIES) { + const translated = openaiToOpenAIResponsesRequest( + model, + { model, messages: [{ role: "user", content: "test" }], reasoning_effort: "max" }, + true, + {} + ); + const result = new CodexExecutor().transformRequest(model, translated, true, { + requestEndpointPath: "/chat/completions", + }); + assert.equal(result.model, model); + assert.equal(result.reasoning.effort, "max", model); + } +}); + +test("GPT-6 Sol and Luna Codex pricing and Fast multiplier match the credit rate card", () => { + // Codex Standard credits per MTok (input / cached / output), at 25 credits per USD: + // Sol 50 / 5 / 250, Luna 2.5 / 0.25 / 12.5. Fast is 2.5x Standard for both. + const expected = { + "gpt-6-sol": { input: 2, cached: 0.2, output: 10 }, + "gpt-6-luna": { input: 0.1, cached: 0.01, output: 0.5 }, + }; + for (const { model, efforts } of FAMILIES) { + for (const id of [model, ...efforts.map((effort) => `${model}-${effort}`)]) { + const pricing = getPricingForModel("cx", id); + assert.ok(pricing, id); + assert.equal(pricing.input, expected[model].input, id); + assert.equal(pricing.cached, expected[model].cached, id); + assert.equal(pricing.output, expected[model].output, id); + assert.equal(pricing.reasoning, expected[model].output, id); + assert.equal(pricing.cache_creation, expected[model].input * 1.25, id); + assert.equal(getCodexFastCostMultiplier("codex", id, "priority"), 2.5, id); + assert.equal(getCodexFastCostMultiplier("cx", id, "fast"), 2.5, id); + assert.equal(getCodexFastCostMultiplier("codex", id, "default"), 1, id); + } + } +}); diff --git a/tests/unit/reasoning-routing-codex-extended-effort.test.ts b/tests/unit/reasoning-routing-codex-extended-effort.test.ts new file mode 100644 index 000000000000..bbd681ebc19c --- /dev/null +++ b/tests/unit/reasoning-routing-codex-extended-effort.test.ts @@ -0,0 +1,176 @@ +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"; + +// Reasoning-routing rules decided which Codex models accept `max` / `ultra` with a +// hard-coded gpt-5.6 regex, so GPT-6 models the Codex executor already serves at +// those tiers were read as unsupported: a rule forcing `max` dropped cx/gpt-6-astra +// from its target combo (or rejected it as a single target), and cx/gpt-6-astra-max +// was not read as a max request. The gate now reads the executor's alias sets +// (open-sse/executors/codex/reasoningSuffix.ts). + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-reasoning-extended-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const combosDb = await import("../../src/lib/db/combos.ts"); +const rulesDb = await import("../../src/lib/db/reasoningRoutingRules.ts"); +const policy = await import("../../src/lib/reasoningRouting/policy.ts"); +const { CODEX_MAX_ALIAS_MODELS, CODEX_ULTRA_ALIAS_MODELS } = + await import("../../open-sse/executors/codex/reasoningSuffix.ts"); +const { codexModelFamilySupportsExtendedEffort, isCodexExtendedEffortBaseModel } = + await import("../../src/shared/reasoning/codexExtendedEffort.ts"); + +const ALIAS_MODELS = new Set([...CODEX_MAX_ALIAS_MODELS, ...CODEX_ULTRA_ALIAS_MODELS]); + +function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + rulesDb.invalidateReasoningRoutingRuleCache(); +} + +function forcedRule( + targetEffort: "max" | "ultra", + target: { targetKind: "model" | "combo"; targetModel?: string; targetComboId?: string } +): rulesDb.ReasoningRoutingRuleInput { + return { + name: `force ${targetEffort}`, + description: "", + scope: "global", + apiKeyId: null, + comboId: null, + connectionId: null, + modelPattern: "reasoning-source", + sourceEffort: "any", + requestTags: [], + tagMatchMode: "any", + effortMode: "force", + targetEffort, + targetKind: target.targetKind, + targetModel: target.targetModel ?? null, + targetComboId: target.targetComboId ?? null, + budgetAction: "preserve", + budgetTokens: null, + priority: 0, + enabled: true, + }; +} + +async function decide(rule: rulesDb.ReasoningRoutingRuleInput) { + await rulesDb.createReasoningRoutingRule(rule); + const decision = await policy.resolveReasoningRoutingRule({ + sourceModel: "reasoning-source", + sourceEffort: "missing", + hasReasoningSignal: false, + }); + assert.ok(decision, "the forced rule must match"); + return decision; +} + +async function removedFromCombo(models: string[], targetEffort: "max" | "ultra") { + const combo = await combosDb.createCombo({ + name: "extended-effort-target", + models, + strategy: "priority", + }); + const decision = await decide( + forcedRule(targetEffort, { targetKind: "combo", targetComboId: String(combo.id) }) + ); + return policy.filterComboForReasoningDecision(decision.targetCombo, decision).removed; +} + +test.beforeEach(resetStorage); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("forced max keeps GPT-6 Astra in a Codex combo", async () => { + assert.deepEqual( + await removedFromCombo(["cx/gpt-6-astra", "codex/gpt-6-astra-high", "cx/gpt-5.6-sol"], "max"), + [] + ); +}); + +test("forced ultra keeps GPT-6 Astra and still drops max-only models", async () => { + assert.deepEqual(await removedFromCombo(["cx/gpt-6-astra", "cx/gpt-5.6-luna"], "ultra"), [ + "cx/gpt-5.6-luna", + ]); +}); + +test("forced max/ultra on a single GPT-6 Astra target is supported", async () => { + for (const effort of ["max", "ultra"] as const) { + resetStorage(); + const decision = await decide( + forcedRule(effort, { targetKind: "model", targetModel: "cx/gpt-6-astra" }) + ); + assert.equal(decision.capability, "supported", effort); + } +}); + +test("every Codex max/ultra alias model passes the gate for its tiers", async () => { + const maxModels = [...CODEX_MAX_ALIAS_MODELS].map((model) => `cx/${model}`); + assert.deepEqual(await removedFromCombo(maxModels, "max"), []); + + resetStorage(); + const allModels = [...ALIAS_MODELS]; + const notUltra = allModels + .filter((model) => !CODEX_ULTRA_ALIAS_MODELS.has(model)) + .map((model) => `cx/${model}`); + assert.deepEqual( + await removedFromCombo( + allModels.map((model) => `cx/${model}`), + "ultra" + ), + notUltra + ); +}); + +test("max/ultra suffixes on GPT-6 Astra are read as the requested effort", () => { + for (const effort of ["max", "ultra"] as const) { + const intent = policy.extractReasoningIntent(`cx/gpt-6-astra-${effort}`, {}); + assert.equal(intent.model, "cx/gpt-6-astra"); + assert.equal(intent.effort, effort); + assert.equal(intent.sourceEffort, effort); + } +}); + +test("suffix parsing follows the alias sets for every model", () => { + for (const model of ALIAS_MODELS) { + for (const effort of ["max", "ultra"] as const) { + const supported = ( + effort === "ultra" ? CODEX_ULTRA_ALIAS_MODELS : CODEX_MAX_ALIAS_MODELS + ).has(model); + const intent = policy.extractReasoningIntent(`codex/${model}-${effort}`, {}); + assert.equal(intent.effort, supported ? effort : null, `${model}-${effort}`); + assert.equal( + intent.model, + supported ? `codex/${model}` : `codex/${model}-${effort}`, + `${model}-${effort}` + ); + } + } +}); + +test("suffix parsing needs the exact base model", () => { + for (const model of ["cx/gpt-6-astra-high-max", "cx/gpt-6-astral-max"]) { + const intent = policy.extractReasoningIntent(model, {}); + assert.equal(intent.model, model); + assert.equal(intent.effort, null); + } +}); + +test("model helpers match whole Codex model ids", () => { + assert.equal(isCodexExtendedEffortBaseModel("CX/GPT-6-Astra", "ultra"), true); + assert.equal(isCodexExtendedEffortBaseModel("cx/gpt-6-astra-high", "max"), false); + assert.equal(codexModelFamilySupportsExtendedEffort("gpt-6-astra", "max"), true); + assert.equal(codexModelFamilySupportsExtendedEffort(" CX/GPT-6-Astra-High ", "ultra"), true); + assert.equal(codexModelFamilySupportsExtendedEffort("gpt-6-astral", "max"), false); + assert.equal(codexModelFamilySupportsExtendedEffort("openai/gpt-6-astra", "max"), false); + assert.equal(codexModelFamilySupportsExtendedEffort("cx/gpt-5.6-luna", "ultra"), false); + assert.equal(codexModelFamilySupportsExtendedEffort("", "max"), false); +}); From 751afb6699a43d6e557b2099284bac2ebeb56274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B5nis=20Ormisson?= Date: Thu, 24 Sep 2026 09:52:15 +0300 Subject: [PATCH 2/2] fix(build): synchronize the transitive brace-expansion lock entry --- package-lock.json | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 10f668c47e31..afd61631c171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25588,17 +25588,6 @@ "node": ">= 14" } }, - "node_modules/libxmljs2/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/libxmljs2/node_modules/cacache": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", @@ -25722,6 +25711,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/libxmljs2/node_modules/minimatch/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/libxmljs2/node_modules/minipass-fetch": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz",