diff --git a/Dockerfile b/Dockerfile index 1a3e996b99b3..f1efc2fc0354 100644 --- a/Dockerfile +++ b/Dockerfile @@ -230,6 +230,6 @@ RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \ # Install CLI tools globally. Separate layer from apt for better cache reuse. RUN --mount=type=cache,id=npm-cache,target=/root/.npm \ - npm install -g --no-audit --no-fund @openai/codex@0.156.1 @anthropic-ai/claude-code droid openclaw@latest + npm install -g --no-audit --no-fund @openai/codex@0.159.2 @anthropic-ai/claude-code droid openclaw@latest USER node diff --git a/changelog.d/features/15171-stable-gpt61-sol.md b/changelog.d/features/15171-stable-gpt61-sol.md new file mode 100644 index 000000000000..d54cef599d21 --- /dev/null +++ b/changelog.d/features/15171-stable-gpt61-sol.md @@ -0,0 +1 @@ +- **feat(codex):** Add GPT-6.1 Sol to the stable fork's OpenAI and Codex catalogs, preserve Max/Ultra routing and tool calls, accept Responses model imports, and align the Codex client profile and optional Docker CLI at 0.159.2. Include Standard short-context price estimates and the purchased-credit Fast multiplier; adapt upstream [#15171](https://github.com/diegosouzapw/OmniRoute/pull/15171) and [#15158](https://github.com/diegosouzapw/OmniRoute/pull/15158) — thanks @xiaoyaner0201 and @HouMinXi. diff --git a/open-sse/config/codexClient.ts b/open-sse/config/codexClient.ts index 176278679616..7398df724aec 100644 --- a/open-sse/config/codexClient.ts +++ b/open-sse/config/codexClient.ts @@ -1,8 +1,8 @@ // Codex's OAuth backend gates newer models by client version: GPT-6 Astra rejects // older clients with "requires a newer version of Codex" (upstream issue #12761). // Keep this in lockstep with CODEX_CLI_PROFILE in src/shared/constants/clientIdentityProfiles.ts. -// https://github.com/openai/codex/releases/tag/rust-v0.156.1 -const DEFAULT_CODEX_CLIENT_VERSION = "0.156.1"; +// https://github.com/openai/codex/releases/tag/rust-v0.159.2 +const DEFAULT_CODEX_CLIENT_VERSION = "0.159.2"; const DEFAULT_CODEX_USER_AGENT_PLATFORM = "Windows 10.0.26200"; const DEFAULT_CODEX_USER_AGENT_ARCH = "x64"; const CODEX_VERSION_OVERRIDE_ENV = "CODEX_CLIENT_VERSION"; diff --git a/open-sse/config/codexModels.ts b/open-sse/config/codexModels.ts index 05e3cb4d952d..a042c39d03e2 100644 --- a/open-sse/config/codexModels.ts +++ b/open-sse/config/codexModels.ts @@ -9,7 +9,12 @@ export const GPT_5_6_ULTRA_ALIAS_MODELS = new Set(["gpt-5.6-sol", "gpt-5.6-terra // STANDARD_EFFORT_SUFFIXES, so without this set neither would ever split off the // model id. `ultra` is an OmniRoute-side tier that goes out as wire effort `max` // while keeping parallel tool calls for sub-agent delegation. -export const GPT_6_ALIAS_MODELS = new Set(["gpt-6-astra", "gpt-6-sol", "gpt-6-luna"]); +export const GPT_6_ALIAS_MODELS = new Set([ + "gpt-6.1-sol", + "gpt-6-astra", + "gpt-6-sol", + "gpt-6-luna", +]); export function splitCodexReasoningSuffix(model: unknown): { baseModel: string; @@ -26,7 +31,7 @@ export function splitCodexReasoningSuffix(model: unknown): { } } - const gpt6AliasMatch = /^(gpt-6-(?:astra|sol|luna))-(max|ultra)$/.exec(modelId); + const gpt6AliasMatch = /^(gpt-6-(?:astra|sol|luna)|gpt-6\.1-sol)-(max|ultra)$/.exec(modelId); if (gpt6AliasMatch) { const [, baseModel, alias] = gpt6AliasMatch; if (GPT_6_ALIAS_MODELS.has(baseModel) && !(baseModel === "gpt-6-luna" && alias === "ultra")) { diff --git a/open-sse/config/providers/registry/codex/index.ts b/open-sse/config/providers/registry/codex/index.ts index d992be928485..537cf5b2e617 100644 --- a/open-sse/config/providers/registry/codex/index.ts +++ b/open-sse/config/providers/registry/codex/index.ts @@ -24,6 +24,14 @@ export const codexProvider: RegistryEntry = { tokenUrl: "https://auth.openai.com/oauth/token", }, models: [ + // Codex OAuth uses the 872K window; the public API has a separate 1.05M limit. + { id: "gpt-6.1-sol", name: "GPT 6.1 Sol", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6.1-sol-low", name: "GPT 6.1 Sol (low)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6.1-sol-medium", name: "GPT 6.1 Sol (medium)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6.1-sol-high", name: "GPT 6.1 Sol (high)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6.1-sol-xhigh", name: "GPT 6.1 Sol (xhigh)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6.1-sol-max", name: "GPT 6.1 Sol (max)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6.1-sol-ultra", name: "GPT 6.1 Sol (ultra)", ...GPT_5_6_CODEX_CAPABILITIES }, { id: "gpt-6-sol", name: "GPT 6 sol", ...GPT_5_6_CODEX_CAPABILITIES }, { id: "gpt-6-sol-low", name: "GPT 6 sol-low", ...GPT_5_6_CODEX_CAPABILITIES }, { id: "gpt-6-sol-medium", name: "GPT 6 sol-medium", ...GPT_5_6_CODEX_CAPABILITIES }, diff --git a/open-sse/config/providers/registry/openai/index.ts b/open-sse/config/providers/registry/openai/index.ts index d354735bf0f1..700c518cdbb5 100644 --- a/open-sse/config/providers/registry/openai/index.ts +++ b/open-sse/config/providers/registry/openai/index.ts @@ -11,6 +11,14 @@ export const openaiProvider: RegistryEntry = { authHeader: "bearer", defaultContextLength: 128000, models: [ + // Sol tool calling requires Responses (OpenAI's GPT-6.1 Sol model reference). + { + id: "gpt-6.1-sol", + name: "GPT-6.1 Sol", + ...GPT_5_6_API_CAPABILITIES, + targetFormat: "openai-responses", + unsupportedParams: ["temperature", "top_p", "top_logprobs", "logprobs"], + }, { id: "gpt-6-sol", name: "GPT 6 sol", ...GPT_5_6_API_CAPABILITIES }, { id: "gpt-6-luna", name: "GPT 6 luna", ...GPT_5_6_API_CAPABILITIES }, { id: "gpt-5.6", name: "GPT-5.6", ...GPT_5_6_API_CAPABILITIES }, diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 5cc95bac9d59..84d46c96857f 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -332,6 +332,7 @@ function normalizeServiceTierValue(value: unknown): string | undefined { * Update this table when Codex releases new models with different caps. */ const MAX_EFFORT_BY_MODEL: Record = { + "gpt-6.1-sol": "ultra", "gpt-6-astra": "ultra", "gpt-6-sol": "ultra", "gpt-6-luna": "max", @@ -1331,7 +1332,7 @@ export class CodexExecutor extends BaseExecutor { const explicitReasoning = normalizeEffortValue(reasoningRecord?.effort); const requestReasoningEffort = normalizeEffortValue(body.reasoning_effort); const fallbackReasoningEffort = allowConnectionReasoningDefaults - ? requestDefaults.reasoningEffort || "medium" + ? requestDefaults.reasoningEffort || (cleanModel === "gpt-6.1-sol" ? "low" : "medium") : undefined; // Issue #2331: model suffix aliases (for example gpt-5.5-xhigh) represent an // explicit model selection, so they must override client-injected defaults such diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index ac4acdc2162c..f5b4eb367af2 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -139,6 +139,13 @@ const CODEX_PREFERRED_UNPREFIXED_MODELS = new Set([ // `gpt-5.5 → gpt-5.5-medium` entry is removed to preserve #2877's bare-id contract. const CODEX_PREFERRED_UNPREFIXED_MODEL_ALIASES = new Map([]); export const CODEX_NATIVE_UNPREFIXED_MODELS = new Set([ + "gpt-6.1-sol", + "gpt-6.1-sol-low", + "gpt-6.1-sol-medium", + "gpt-6.1-sol-high", + "gpt-6.1-sol-xhigh", + "gpt-6.1-sol-max", + "gpt-6.1-sol-ultra", "codex-auto-review", "gpt-6-sol", "gpt-6-sol-low", diff --git a/open-sse/translator/request/openai-responses/helpers.ts b/open-sse/translator/request/openai-responses/helpers.ts index 3d4aad6bbfd4..49dceb33cc20 100644 --- a/open-sse/translator/request/openai-responses/helpers.ts +++ b/open-sse/translator/request/openai-responses/helpers.ts @@ -53,7 +53,7 @@ export function normalizeResponsesReasoningEffort(value: unknown, model?: unknow const effort = toString(value).toLowerCase(); const codexModel = typeof model === "string" && - /^(?:(?:codex|cx)\/)?gpt-6-(?:astra|sol|luna)(?:-(?:low|medium|high|xhigh|max|ultra))?$/.test( + /^(?:(?:codex|cx)\/)?(?:gpt-6-(?:astra|sol|luna)|gpt-6\.1-sol)(?:-(?:low|medium|high|xhigh|max|ultra))?$/.test( model ); return effort === "max" && !codexModel ? "xhigh" : effort; diff --git a/src/lib/providers/codexFastTier.ts b/src/lib/providers/codexFastTier.ts index 1ff6154a6d2b..76f43d9d701c 100644 --- a/src/lib/providers/codexFastTier.ts +++ b/src/lib/providers/codexFastTier.ts @@ -14,6 +14,7 @@ export type CodexFastTierValue = CodexServiceTier; export type CodexGlobalServiceMode = "none" | CodexServiceTier; export const CODEX_FAST_TIER_DEFAULT_SUPPORTED_MODELS: readonly string[] = [ + "gpt-6.1-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", diff --git a/src/lib/usage/costCalculator.ts b/src/lib/usage/costCalculator.ts index bedb8528f9c5..600df63ec067 100644 --- a/src/lib/usage/costCalculator.ts +++ b/src/lib/usage/costCalculator.ts @@ -101,6 +101,8 @@ export function getCodexFastCostMultiplier( const modelKey = stripCodexEffortSuffix(normalizeModelName(String(model || "")).toLowerCase()); const compactModelKey = modelKey.replace(/-/g, ""); + // Purchased credits use 2x; included subscription allowance uses 2.5x. + if (compactModelKey === "gpt6.1sol") return 2; // Codex Astra Fast is 2.5x Standard (https://developers.openai.com/codex/pricing). if ( /^gpt-6-(?:astra|sol|luna)$/.test(modelKey) || @@ -168,7 +170,15 @@ export function computeCostFromPricing( cost += outputTokens * (outputPrice / 1_000_000); const reasoningTokens = tokens.reasoning ?? tokens.reasoning_tokens ?? 0; - if (reasoningTokens > 0) cost += reasoningTokens * (reasoningPrice / 1_000_000); + if (reasoningTokens > 0) { + const model = stripCodexEffortSuffix(normalizeModelName(options.model || "")); + const solOutputIncludesReasoning = + model === "gpt-6.1-sol" && ["openai", "codex", "cx"].includes(options.provider || ""); + const reasoningRate = solOutputIncludesReasoning + ? Math.max(0, reasoningPrice - outputPrice) + : reasoningPrice; + cost += reasoningTokens * (reasoningRate / 1_000_000); + } if (cacheCreationTokens > 0) cost += cacheCreationTokens * (cacheCreationPrice / 1_000_000); diff --git a/src/lib/vscode/reasoningMetadata.ts b/src/lib/vscode/reasoningMetadata.ts index 528d1093c180..52808d00658e 100644 --- a/src/lib/vscode/reasoningMetadata.ts +++ b/src/lib/vscode/reasoningMetadata.ts @@ -21,7 +21,7 @@ const STANDARD_EFFORT_SUFFIX_PATTERN = /-(xhigh|high|medium|low|none)$/i; // effort suffixes. Without a match here the base id never splits off, and VS Code // discovery expands the alias again into invalid ids like `gpt-6-astra-max-high`. const EXTENDED_EFFORT_SUFFIX_PATTERN = - /^(.*(?:gpt-5\.6-(?:sol|terra|luna)|gpt-6-(?:astra|sol|luna)))-(max|ultra)$/i; + /^(.*(?:gpt-5\.6-(?:sol|terra|luna)|gpt-6-(?:astra|sol|luna)|gpt-6\.1-sol))-(max|ultra)$/i; const DEFAULT_REASONING_EFFORT = "none"; const KNOWN_REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh", "max", "ultra"]); @@ -182,7 +182,14 @@ export function getReasoningVariantBaseModelId(modelId: string) { } export function getDefaultReasoningEffort(model: VscodeCatalogModel, supportedValues?: string[]) { - return inferSelectedReasoningEffort(model, supportedValues) || DEFAULT_REASONING_EFFORT; + const selected = inferSelectedReasoningEffort(model, supportedValues); + if (selected) return selected; + const parsed = parseModel(getCatalogModelName(model)); + const provider = parsed.provider || model.owned_by; + if ((provider === "codex" || provider === "cx") && parsed.model === "gpt-6.1-sol") { + return "low"; + } + return DEFAULT_REASONING_EFFORT; } export function buildReasoningConfigSchema( diff --git a/src/shared/constants/clientIdentityProfiles.ts b/src/shared/constants/clientIdentityProfiles.ts index f5438fb5f627..5cf299867b48 100644 --- a/src/shared/constants/clientIdentityProfiles.ts +++ b/src/shared/constants/clientIdentityProfiles.ts @@ -39,7 +39,7 @@ const CODEX_CLI_PROFILE: ClientIdentityProfile = Object.freeze({ id: "codex-cli", label: "Codex CLI", headers: Object.freeze({ - "User-Agent": "codex_cli_rs/0.156.1", + "User-Agent": "codex_cli_rs/0.159.2", originator: "codex_cli_rs", }), }); diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index b5f785557c65..afa1b8e1f21f 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -92,6 +92,7 @@ export const MODEL_SPECS: Record = { }, // Public model limits; the Codex registry supplies its smaller OAuth window. + "gpt-6.1-sol": { ...GPT_5_6_MODEL_SPEC, aliases: ["openai/gpt-6.1-sol"] }, "gpt-6-sol": { ...GPT_5_6_MODEL_SPEC, aliases: ["openai/gpt-6-sol"] }, "gpt-6-luna": { ...GPT_5_6_MODEL_SPEC, aliases: ["openai/gpt-6-luna"] }, "gpt-6-astra": { diff --git a/src/shared/constants/pricing/frontier-labs.ts b/src/shared/constants/pricing/frontier-labs.ts index 55f673502ba4..802b08dd278e 100644 --- a/src/shared/constants/pricing/frontier-labs.ts +++ b/src/shared/constants/pricing/frontier-labs.ts @@ -20,6 +20,8 @@ import { export const DEFAULT_PRICING_FRONTIER = { openai: { + // Standard short-context USD/MTok; API cache writes have a separate rate. + "gpt-6.1-sol": { input: 2, output: 10, cached: 0.1, reasoning: 10, cache_creation: 2.5 }, "gpt-5.6": GPT_5_6_SOL_PRICING, "gpt-5.6-sol": GPT_5_6_SOL_PRICING, "gpt-5.6-terra": GPT_5_6_TERRA_PRICING, diff --git a/src/shared/constants/pricing/oauth-subscriptions.ts b/src/shared/constants/pricing/oauth-subscriptions.ts index 3fae46211cbd..cf26c59caa7d 100644 --- a/src/shared/constants/pricing/oauth-subscriptions.ts +++ b/src/shared/constants/pricing/oauth-subscriptions.ts @@ -13,6 +13,10 @@ import { GPT_5_6_TERRA_PRICING, } from "./shared-tiers"; +// Codex: 50 / 2.5 / 250 credits per MTok at 25 credits/USD; no cache-write charge. +// https://learn.chatgpt.com/docs/pricing +const GPT_6_1_SOL_CODEX_PRICING = { input: 2, output: 10, cached: 0.1, reasoning: 10 }; + export const DEFAULT_PRICING_OAUTH = { cc: { "claude-fable-5": { @@ -80,6 +84,14 @@ export const DEFAULT_PRICING_OAUTH = { }, }, cx: { + "gpt-6.1-sol": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-low": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-medium": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-high": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-xhigh": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-max": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-ultra": GPT_6_1_SOL_CODEX_PRICING, + "codex-auto-review": GPT_5_5_PRICING, // Codex uses credits per 1M tokens. OmniRoute stores the dollar-equivalent // values below at the documented conversion of 25 credits per USD. diff --git a/src/shared/reasoning/effortStandardization.ts b/src/shared/reasoning/effortStandardization.ts index 611433b8ff49..232790ab25b5 100644 --- a/src/shared/reasoning/effortStandardization.ts +++ b/src/shared/reasoning/effortStandardization.ts @@ -34,6 +34,10 @@ export function extendCodexGpt56EffortValues( return values; } + if (/^gpt-6\.1-sol(?:-(?:low|medium|high|xhigh|max|ultra))?$/.test(normalizedModel)) { + return ["low", "medium", "high", "xhigh", "max", "ultra"]; + } + const match = normalizedModel.match( /^gpt-5\.6-(sol|terra|luna)(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/ ); diff --git a/src/shared/validation/schemas/provider.ts b/src/shared/validation/schemas/provider.ts index 72f3af64fe04..a51eb4939937 100644 --- a/src/shared/validation/schemas/provider.ts +++ b/src/shared/validation/schemas/provider.ts @@ -188,6 +188,7 @@ export const providerModelMutationSchema = z.object({ .array( z.enum([ "chat", + "responses", "embeddings", "rerank", "images", diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index f4204bd5ed8b..7f5429ba7aca 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -966,16 +966,16 @@ "Authorization": "Bearer ", "Content-Type": "application/json", "Openai-Beta": "responses=experimental", - "User-Agent": "codex-cli/0.156.1 (; )", - "Version": "0.156.1", + "User-Agent": "codex-cli/0.159.2 (; )", + "Version": "0.159.2", "X-Codex-Beta-Features": "responses_websockets" }, "nonStream": { "Authorization": "Bearer ", "Content-Type": "application/json", "Openai-Beta": "responses=experimental", - "User-Agent": "codex-cli/0.156.1 (; )", - "Version": "0.156.1", + "User-Agent": "codex-cli/0.159.2 (; )", + "Version": "0.159.2", "X-Codex-Beta-Features": "responses_websockets" }, "oauth": { @@ -983,8 +983,8 @@ "Authorization": "Bearer ", "Content-Type": "application/json", "Openai-Beta": "responses=experimental", - "User-Agent": "codex-cli/0.156.1 (; )", - "Version": "0.156.1", + "User-Agent": "codex-cli/0.159.2 (; )", + "Version": "0.159.2", "X-Codex-Beta-Features": "responses_websockets" } }, diff --git a/tests/unit/claude-codex-identity-version-sync.test.ts b/tests/unit/claude-codex-identity-version-sync.test.ts index 18a7c28d1cad..bc822a58e91c 100644 --- a/tests/unit/claude-codex-identity-version-sync.test.ts +++ b/tests/unit/claude-codex-identity-version-sync.test.ts @@ -42,10 +42,10 @@ test("Claude CLI is pinned to the captured 2.1.207 release", () => { assert.equal(id.CLAUDE_CODE_VERSION, "2.1.207"); }); -test("Codex client is pinned to the captured 0.156.1 release", () => { - assert.equal(codexCfg.getCodexClientVersion(), "0.156.1"); - assert.equal(codexCfg.getCodexUserAgent(), "codex-cli/0.156.1 (Windows 10.0.26200; x64)"); - assert.equal(codexCfg.getCodexDefaultHeaders().Version, "0.156.1"); +test("Codex client is pinned to the captured 0.159.2 release", () => { + assert.equal(codexCfg.getCodexClientVersion(), "0.159.2"); + assert.equal(codexCfg.getCodexUserAgent(), "codex-cli/0.159.2 (Windows 10.0.26200; x64)"); + assert.equal(codexCfg.getCodexDefaultHeaders().Version, "0.159.2"); }); test("Codex CLI preset and optional Docker CLI stay aligned with the wire version", () => { diff --git a/tests/unit/client-identity-profiles.test.ts b/tests/unit/client-identity-profiles.test.ts index 3837961f951b..c6327ed6602c 100644 --- a/tests/unit/client-identity-profiles.test.ts +++ b/tests/unit/client-identity-profiles.test.ts @@ -43,7 +43,7 @@ test("getClientIdentityProfileHeaders: known CLI profiles expose their preset he assert.equal(claudeCli["X-App"], "cli"); const codexCli = getClientIdentityProfileHeaders("codex-cli"); - assert.equal(codexCli["User-Agent"], "codex_cli_rs/0.156.1"); + assert.equal(codexCli["User-Agent"], "codex_cli_rs/0.159.2"); assert.equal(codexCli.originator, "codex_cli_rs"); const geminiCli = getClientIdentityProfileHeaders("gemini-cli"); @@ -80,7 +80,7 @@ test("a selected profile's headers land in providerSpecificData.customHeaders", customHeaders: { ...profileHeaders, "X-Operator-Set": "keep-me" }, }; - assert.equal(providerSpecificData.customHeaders["User-Agent"], "codex_cli_rs/0.156.1"); + assert.equal(providerSpecificData.customHeaders["User-Agent"], "codex_cli_rs/0.159.2"); assert.equal(providerSpecificData.customHeaders.originator, "codex_cli_rs"); assert.equal(providerSpecificData.customHeaders["X-Operator-Set"], "keep-me"); }); diff --git a/tests/unit/codex-fast-tier.test.ts b/tests/unit/codex-fast-tier.test.ts index 82c2aada21eb..1f5d24d0bbdc 100644 --- a/tests/unit/codex-fast-tier.test.ts +++ b/tests/unit/codex-fast-tier.test.ts @@ -42,6 +42,7 @@ test("Codex global service mode distinguishes no setting from explicit tiers", ( enabled: true, tier: "default", supportedModels: [ + "gpt-6.1-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", diff --git a/tests/unit/codex-gpt6-astra.test.ts b/tests/unit/codex-gpt6-astra.test.ts index 27e3cc936640..688620b6bdf2 100644 --- a/tests/unit/codex-gpt6-astra.test.ts +++ b/tests/unit/codex-gpt6-astra.test.ts @@ -138,8 +138,8 @@ test("VS Code discovery splits Astra's extended effort aliases off the base id", test("Codex identifies as a client version Astra accepts", () => { // Astra rejects older clients with HTTP 400 "requires a newer version of Codex" // (upstream issue #12761), so the advertised identity gates the whole model. - assert.equal(getCodexClientVersion(), "0.156.1"); - assert.equal(getCodexDefaultHeaders().Version, "0.156.1"); + assert.equal(getCodexClientVersion(), "0.159.2"); + assert.equal(getCodexDefaultHeaders().Version, "0.159.2"); }); test("Codex Fast bills GPT-6 Astra at the 2.5x multiplier", () => { diff --git a/tests/unit/codex-gpt61-sol.test.ts b/tests/unit/codex-gpt61-sol.test.ts new file mode 100644 index 000000000000..dea8b3b3adc2 --- /dev/null +++ b/tests/unit/codex-gpt61-sol.test.ts @@ -0,0 +1,218 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + getModelsByProviderId, + getModelTargetFormat, +} from "../../open-sse/config/providerModels.ts"; +import { CodexExecutor } from "../../open-sse/executors/codex.ts"; +import { DefaultExecutor } from "../../open-sse/executors/default.ts"; +import { getCodexUpstreamModel } from "../../open-sse/config/codexModels.ts"; +import { getModelInfoCore, parseModel } from "../../open-sse/services/model.ts"; +import { openaiToOpenAIResponsesRequest } from "../../open-sse/translator/request/openai-responses/toResponses.ts"; +import { getModelSpec } from "../../src/shared/constants/modelSpecs.ts"; +import { getPricingForModel } from "../../src/shared/constants/pricing.ts"; +import { + computeCostFromPricing, + getCodexFastCostMultiplier, +} from "../../src/lib/usage/costCalculator.ts"; +import { resolveCodexGlobalFastServiceTier } from "../../src/lib/providers/codexFastTier.ts"; +import { + getDefaultReasoningEffort, + getReasoningEffortValues, + getReasoningVariantBaseModelId, +} from "../../src/lib/vscode/reasoningMetadata.ts"; +import { providerModelMutationSchema } from "../../src/shared/validation/schemas/provider.ts"; +import { normalizeCodexModelsResponse } from "../../src/app/api/providers/[id]/models/discovery/codex.ts"; + +const MODEL = "gpt-6.1-sol"; +const EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"]; +const IDS = [MODEL, ...EFFORTS.map((effort) => `${MODEL}-${effort}`)]; + +test.after(async () => { + const { resetDbInstance } = await import("../../src/lib/db/core.ts"); + resetDbInstance(); +}); + +type ResponsesBody = Record & { + reasoning: { effort: string; summary?: string }; + tools: { name: string }[]; +}; + +function transform(model: string, body: Record = {}, providerSpecificData = {}) { + return new CodexExecutor().transformRequest(model, { model, input: [], ...body }, true, { + requestEndpointPath: "/responses", + providerSpecificData, + }) as ResponsesBody; +} + +test("Sol has provider-specific context limits and routes tool requests through Responses", async () => { + const catalog = getModelsByProviderId("codex"); + assert.deepEqual( + catalog.filter((m) => m.id.startsWith(MODEL)).map((m) => m.id), + IDS + ); + for (const id of IDS) { + const entry = catalog.find((m) => m.id === id); + assert.equal(entry?.contextLength, 872000); + assert.equal(entry?.maxOutputTokens, 128000); + assert.equal(entry?.toolCalling, true); + assert.equal(entry?.supportsVision, true); + assert.equal(getModelTargetFormat("cx", id), "openai-responses"); + assert.equal((await getModelInfoCore(id, {})).provider, "codex"); + assert.equal(getCodexUpstreamModel(id), MODEL, "account eligibility uses the wire model"); + } + const api = getModelsByProviderId("openai").find((m) => m.id === MODEL); + assert.equal(api?.contextLength, 1050000); + assert.equal(api?.maxOutputTokens, 128000); + assert.ok(api?.unsupportedParams?.includes("temperature")); + assert.equal(getModelSpec(`openai/${MODEL}`)?.contextWindow, 1050000); + assert.equal(getModelTargetFormat("openai", MODEL), "openai-responses"); + assert.equal( + new DefaultExecutor("openai").buildUrl(MODEL, true), + "https://api.openai.com/v1/responses" + ); + assert.equal(parseModel(`openai/${MODEL}`).provider, "openai"); + assert.equal((await getModelInfoCore(MODEL, { [MODEL]: `openai/${MODEL}` })).provider, "openai"); +}); + +test("Sol aliases override injected effort and keep max on native and translated requests", () => { + for (const effort of EFFORTS) { + const result = transform(`${MODEL}-${effort}`, { + reasoning: { effort: "medium", summary: "detailed" }, + }); + assert.equal(result.model, MODEL); + assert.equal(result.reasoning.effort, effort === "ultra" ? "max" : effort); + assert.equal(result.reasoning.summary, "detailed"); + } + assert.equal(transform(MODEL, { reasoning: { effort: "max" } }).reasoning.effort, "max"); + const translated = openaiToOpenAIResponsesRequest( + MODEL, + { + model: MODEL, + messages: [{ role: "user", content: "test" }], + reasoning_effort: "max", + tools: [{ type: "function", function: { name: "lookup", parameters: { type: "object" } } }], + }, + true, + {} + ) as ResponsesBody; + assert.equal(translated.reasoning.effort, "max"); + assert.equal(translated.tools[0].name, "lookup"); +}); + +test("Sol defaults to low while preserving explicit request and connection preferences", () => { + assert.equal(transform(MODEL).reasoning.effort, "low"); + assert.equal(transform(MODEL, { reasoning_effort: "high" }).reasoning.effort, "high"); + assert.equal( + transform(MODEL, {}, { requestDefaults: { reasoningEffort: "xhigh" } }).reasoning.effort, + "xhigh" + ); + assert.equal(transform("gpt-6-sol").reasoning.effort, "medium"); +}); + +test("Sol VS Code metadata exposes supported efforts without generating nested suffixes", () => { + const model = { id: `cx/${MODEL}`, owned_by: "codex", capabilities: { reasoning: true } }; + assert.deepEqual(getReasoningEffortValues(model), EFFORTS); + assert.equal(getDefaultReasoningEffort(model), "low"); + for (const effort of ["max", "ultra"]) { + assert.equal(getReasoningVariantBaseModelId(`cx/${MODEL}-${effort}`), `cx/${MODEL}`); + assert.equal(getDefaultReasoningEffort({ ...model, id: `cx/${MODEL}-${effort}` }), effort); + } +}); + +test("Sol prices cached input independently from GPT-6 Sol and makes Fast eligible", () => { + for (const id of IDS) { + const pricing = getPricingForModel("cx", id); + assert.equal(pricing?.input, 2); + assert.equal(pricing?.cached, 0.1); + assert.equal(pricing?.output, 10); + assert.equal(pricing?.cache_creation, undefined); + assert.equal(getCodexFastCostMultiplier("cx", id, "priority"), 2); + assert.equal(getCodexFastCostMultiplier("codex", id, "fast"), 2); + assert.equal(getCodexFastCostMultiplier("codex", id, "default"), 1); + } + assert.equal(getPricingForModel("openai", MODEL)?.cache_creation, 2.5); + assert.equal(getPricingForModel("cx", "gpt-6-sol")?.cached, 0.2); + assert.ok( + resolveCodexGlobalFastServiceTier({ + codexServiceTier: { enabled: true }, + }).supportedModels.includes(MODEL) + ); +}); + +test("Sol cost counts cached input and reasoning-inclusive output once", () => { + const tokens = { input: 100000, cacheRead: 50000, output: 2000, reasoning: 1000 }; + const pricing = getPricingForModel("cx", MODEL); + for (const provider of ["cx", "codex", "openai"]) { + const cost = computeCostFromPricing(pricing, tokens, { provider, model: MODEL }); + assert.ok(Math.abs(cost - 0.125) < 1e-10, `${provider}: ${cost}`); + } + const fast = computeCostFromPricing(pricing, tokens, { + provider: "cx", + model: `${MODEL}-max`, + serviceTier: "priority", + }); + assert.ok(Math.abs(fast - 0.25) < 1e-10); + // Other providers retain the fork's existing separate-reasoning accounting. + const existing = computeCostFromPricing(pricing, tokens, { provider: "custom", model: MODEL }); + assert.ok(Math.abs(existing - 0.135) < 1e-10); +}); + +test("discovered Sol Responses endpoints pass import validation and unknown endpoints fail", () => { + const discovered = normalizeCodexModelsResponse({ models: [{ slug: MODEL }] })[0]; + const parsed = providerModelMutationSchema.parse({ + provider: "codex", + modelId: MODEL, + apiFormat: "responses", + supportedEndpoints: discovered.supportedEndpoints, + }); + assert.deepEqual(parsed.supportedEndpoints, ["responses"]); + assert.equal( + providerModelMutationSchema.safeParse({ + provider: "codex", + modelId: MODEL, + supportedEndpoints: ["arbitrary-endpoint"], + }).success, + false + ); +}); + +test("Sol streams tool events and retains Ultra parallel calls with the updated client identity", async (t) => { + const executor = new CodexExecutor(); + const captured: { headers: Headers; body: Record }[] = []; + const sse = [ + 'event: response.created\ndata: {"type":"response.created","response":{"id":"resp_sol","object":"response","status":"in_progress"}}\n\n', + 'event: response.output_item.added\ndata: {"type":"response.output_item.added","output_index":0,"item":{"id":"fc_sol","type":"function_call","call_id":"call_sol","name":"lookup","arguments":"{}"}}\n\n', + 'event: response.completed\ndata: {"type":"response.completed","response":{"id":"resp_sol","status":"completed","output":[{"type":"function_call","call_id":"call_sol","name":"lookup","arguments":"{}"}]}}\n\n', + ].join(""); + t.mock.method(globalThis, "fetch", async (_url: unknown, init: RequestInit) => { + captured.push({ headers: new Headers(init.headers), body: JSON.parse(String(init.body)) }); + return new Response(sse, { headers: { "content-type": "text/event-stream" } }); + }); + for (const effort of ["ultra", "high"]) { + const model = `${MODEL}-${effort}`; + const result = await executor.execute({ + model, + stream: true, + credentials: { accessToken: "test-token" }, + clientHeaders: { "X-OpenAI-Internal-Codex-Responses-Lite": "true" }, + body: { + _nativeCodexPassthrough: true, + model, + input: [{ role: "user", content: "test" }], + parallel_tool_calls: true, + tools: [{ type: "function", name: "lookup", parameters: { type: "object" } }], + }, + }); + assert.equal(result.response.status, 200); + const output = await result.response.text(); + assert.match(output, /response.completed/); + assert.match(output, /call_sol/); + const sent = captured.at(-1)!; + assert.equal(sent.headers.get("version"), "0.159.2"); + assert.match(sent.headers.get("user-agent") || "", /0\.159\.2/); + assert.equal(sent.body.model, MODEL); + assert.equal(sent.body.parallel_tool_calls, effort === "ultra"); + assert.equal((sent.body.tools as { name: string }[])[0].name, "lookup"); + } +}); diff --git a/tests/unit/codex-sol-luna.test.ts b/tests/unit/codex-sol-luna.test.ts index d43fa723b982..9ba8491c0ed0 100644 --- a/tests/unit/codex-sol-luna.test.ts +++ b/tests/unit/codex-sol-luna.test.ts @@ -116,5 +116,5 @@ test("caller version passes through with safe pinned fallback", () => { executor.buildHeaders({ accessToken: "opaque" }, true, { version: "0.157.0" }).Version, "0.157.0" ); - assert.equal(executor.buildHeaders({ accessToken: "opaque" }, true).Version, "0.156.1"); + assert.equal(executor.buildHeaders({ accessToken: "opaque" }, true).Version, "0.159.2"); }); diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index 8e993fcd24a7..24c9b649bb59 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -185,10 +185,10 @@ test("CodexExecutor.buildHeaders binds workspace ids and disables SSE accept for assert.equal(standardHeaders.Authorization, "Bearer codex-token"); assert.equal(standardHeaders.Accept, "text/event-stream"); assert.equal(standardHeaders["chatgpt-account-id"], "workspace-1"); - assert.equal(standardHeaders.Version, "0.156.1"); + assert.equal(standardHeaders.Version, "0.159.2"); assert.equal(standardHeaders["Openai-Beta"], "responses=experimental"); assert.equal(standardHeaders["X-Codex-Beta-Features"], "responses_websockets"); - assert.equal(standardHeaders["User-Agent"], "codex-cli/0.156.1 (Windows 10.0.26200; x64)"); + assert.equal(standardHeaders["User-Agent"], "codex-cli/0.159.2 (Windows 10.0.26200; x64)"); assert.equal(compactHeaders.Accept, "application/json"); }); @@ -302,7 +302,7 @@ test("CodexExecutor.buildHeaders honors safe env overrides for Version and User- }, () => { const headers = executor.buildHeaders({ accessToken: "codex-token" }, true); - assert.equal(headers.Version, "0.156.1"); + assert.equal(headers.Version, "0.159.2"); assert.equal(headers["User-Agent"], "custom-codex/9.9.9"); } ); @@ -1065,8 +1065,8 @@ test("CodexExecutor.execute captures the exact websocket request body before sen assert.equal(sentBody.model, "gpt-5.5"); assert.equal(websocketHeaders?.["OpenAI-Beta"], "responses_websockets=2026-02-06"); assert.equal(websocketHeaders?.Origin, "https://chatgpt.com"); - assert.equal(websocketHeaders?.Version, "0.156.1"); - assert.equal(websocketHeaders?.["User-Agent"], "codex-cli/0.156.1 (Windows 10.0.26200; x64)"); + assert.equal(websocketHeaders?.Version, "0.159.2"); + assert.equal(websocketHeaders?.["User-Agent"], "codex-cli/0.159.2 (Windows 10.0.26200; x64)"); }); test("CodexExecutor.execute adds CLI-like session identity headers without changing response flow", async () => { diff --git a/tests/unit/openai-gpt56-catalog.test.ts b/tests/unit/openai-gpt56-catalog.test.ts index f1eb8cd57137..9560f6ce94ea 100644 --- a/tests/unit/openai-gpt56-catalog.test.ts +++ b/tests/unit/openai-gpt56-catalog.test.ts @@ -9,7 +9,7 @@ const EXPECTED_MODELS = ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-lun test("OpenAI API catalog exposes the public GPT-5.6 family and keeps GPT-5.4", () => { const models = getModelsByProviderId("openai"); - const expectedLeadingModels = ["gpt-6-sol", "gpt-6-luna", ...EXPECTED_MODELS]; + const expectedLeadingModels = ["gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna", ...EXPECTED_MODELS]; assert.deepEqual( models.slice(0, expectedLeadingModels.length).map((model) => model.id), diff --git a/tests/unit/provider-models-route-codex.test.ts b/tests/unit/provider-models-route-codex.test.ts index 16a5ee077e4e..b36348541f8a 100644 --- a/tests/unit/provider-models-route-codex.test.ts +++ b/tests/unit/provider-models-route-codex.test.ts @@ -159,11 +159,11 @@ test("provider models route merges live Codex models with the local catalog then assert.equal(body.discoveredCandidateCount, undefined); assert.deepEqual(seenRequests, [ { - url: "https://chatgpt.com/backend-api/codex/models?client_version=0.156.1", + url: "https://chatgpt.com/backend-api/codex/models?client_version=0.159.2", authorization: "Bearer codex-access-token", workspaceId: "account-123", originator: "codex_cli_rs", - userAgent: "codex-cli/0.156.1 (Windows 10.0.26200; x64)", + userAgent: "codex-cli/0.159.2 (Windows 10.0.26200; x64)", }, { url: "https://raw.githubusercontent.com/openai/codex/refs/heads/main/codex-rs/models-manager/models.json",