From 0176f84d0888e93b5e9aa36a480a860879177bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E4=B9=98=E5=A6=8D=20=28Xiaoyaner=29?= Date: Wed, 30 Sep 2026 13:14:24 +0800 Subject: [PATCH 1/5] feat(providers): add GPT-6.1 Sol catalog, pricing and reasoning support Register public API and Codex limits separately, retain max/ultra alias behavior, and use the live Codex low default. Add the current Standard cached-input rate and the purchased-credit Fast multiplier without changing older model prices. Keep the bundled Codex CLI and request identity at the verified 0.159.2 release. Add focused catalog, translation, delegation and cost regressions; refresh only the Codex golden header values. Refs #15166. Related #15158 remains an independent model-import validation fix. --- Dockerfile | 2 +- .../config/providers/registry/codex/index.ts | 18 ++ .../config/providers/registry/openai/index.ts | 8 + open-sse/executors/codex.ts | 2 +- open-sse/executors/codex/reasoningSuffix.ts | 2 + .../request/openai-responses/helpers.ts | 2 +- src/lib/providers/codexFastTier.ts | 1 + src/lib/usage/costCalculator.ts | 8 +- src/lib/vscode/reasoningMetadata.ts | 3 +- src/shared/constants/codexClient.ts | 2 +- src/shared/constants/modelSpecs.ts | 5 + src/shared/constants/pricing/frontier-labs.ts | 3 + .../constants/pricing/oauth-subscriptions.ts | 10 + src/shared/reasoning/effortStandardization.ts | 2 +- tests/snapshots/provider/translate-path.json | 12 +- tests/unit/executor-codex.test.ts | 6 +- tests/unit/gpt61-sol-catalog.test.ts | 174 ++++++++++++++++++ tests/unit/openai-gpt56-catalog.test.ts | 126 +++++++------ 18 files changed, 308 insertions(+), 78 deletions(-) create mode 100644 tests/unit/gpt61-sol-catalog.test.ts diff --git a/Dockerfile b/Dockerfile index 897c8dbe516..878d82a410b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -363,7 +363,7 @@ RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-apt-cache,targe # build, not the floating `@latest`. RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-npm-cache,target=/root/.npm \ npm install -g --no-audit --no-fund \ - @openai/codex@0.156.1 \ + @openai/codex@0.159.2 \ @anthropic-ai/claude-code@2.1.260 \ droid@0.212.0 \ openclaw@2026.9.1 diff --git a/open-sse/config/providers/registry/codex/index.ts b/open-sse/config/providers/registry/codex/index.ts index c135d8b99c8..b80e314a2ff 100644 --- a/open-sse/config/providers/registry/codex/index.ts +++ b/open-sse/config/providers/registry/codex/index.ts @@ -28,6 +28,24 @@ export const codexProvider: RegistryEntry = { tokenUrl: "https://auth.openai.com/oauth/token", }, models: [ + // Live Codex catalog: 872K maximum window, low default, low..ultra. + { id: "gpt-6.1-sol", name: "GPT 6.1 Sol", ...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.1-sol-max", name: "GPT 6.1 Sol (Max)", ...GPT_5_6_CODEX_CAPABILITIES }, + { + id: "gpt-6.1-sol-xhigh", + name: "GPT 6.1 Sol (xHigh)", + ...GPT_5_6_CODEX_CAPABILITIES, + timeoutMs: 1200000, + }, + { + id: "gpt-6.1-sol-high", + name: "GPT 6.1 Sol (High)", + ...GPT_5_6_CODEX_CAPABILITIES, + timeoutMs: 1200000, + }, + { id: "gpt-6.1-sol-medium", name: "GPT 6.1 Sol (Medium)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6.1-sol-low", name: "GPT 6.1 Sol (Low)", ...GPT_5_6_CODEX_CAPABILITIES }, // Astra shares GPT-5.6's Codex limits: the live OAuth catalog reports // max_context_window=872000 (context_window=272000 is the pricing tier). { id: "gpt-6-astra", name: "GPT 6 Astra", ...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 673fc6bf0ac..a63b3fbbec6 100644 --- a/open-sse/config/providers/registry/openai/index.ts +++ b/open-sse/config/providers/registry/openai/index.ts @@ -12,6 +12,14 @@ export const openaiProvider: RegistryEntry = { authHeader: "bearer", defaultContextLength: 128000, models: [ + // https://developers.openai.com/api/docs/models/gpt-6.1-sol + // API: 1.05M context, low..max (no none/minimal); tools require Responses. + { + id: "gpt-6.1-sol", + name: "GPT-6.1 Sol", + ...GPT_5_6_API_CAPABILITIES, + unsupportedParams: ["temperature", "top_p", "top_logprobs", "logprobs"], + }, // Astra shares the public GPT-5.6 limits; tool calling requires Responses. // https://developers.openai.com/api/docs/guides/latest-model { diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index c1279c49bd0..df525a0526e 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -1414,7 +1414,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/executors/codex/reasoningSuffix.ts b/open-sse/executors/codex/reasoningSuffix.ts index 627f2a339fb..be7aa588308 100644 --- a/open-sse/executors/codex/reasoningSuffix.ts +++ b/open-sse/executors/codex/reasoningSuffix.ts @@ -9,6 +9,7 @@ export const CODEX_EFFORT_ORDER = [ ] as const; export type CodexEffortLevel = (typeof CODEX_EFFORT_ORDER)[number]; export const CODEX_MAX_ALIAS_MODELS = new Set([ + "gpt-6.1-sol", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", @@ -17,6 +18,7 @@ export const CODEX_MAX_ALIAS_MODELS = new Set([ "gpt-6-luna", ]); export const CODEX_ULTRA_ALIAS_MODELS = new Set([ + "gpt-6.1-sol", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-6-astra", diff --git a/open-sse/translator/request/openai-responses/helpers.ts b/open-sse/translator/request/openai-responses/helpers.ts index 8065bddb385..47096562d1d 100644 --- a/open-sse/translator/request/openai-responses/helpers.ts +++ b/open-sse/translator/request/openai-responses/helpers.ts @@ -51,7 +51,7 @@ export function imageUrlToText(value: unknown): string { } const CODEX_MAX_EFFORT_MODEL_PATTERN = - /^(?:gpt-5\.6-(?:sol|terra|luna)|gpt-6-(?:astra|sol|luna))(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/; + /^(?:gpt-5\.6-(?:sol|terra|luna)|gpt-6-(?:astra|sol|luna)|gpt-6\.1-sol)(?:-(?: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))?$/; diff --git a/src/lib/providers/codexFastTier.ts b/src/lib/providers/codexFastTier.ts index 3715b81ffab..45d9447bf1d 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 5774fc4b56c..ecc2aa84fcc 100644 --- a/src/lib/usage/costCalculator.ts +++ b/src/lib/usage/costCalculator.ts @@ -101,11 +101,11 @@ export function getCodexFastCostMultiplier( const modelKey = stripCodexEffortSuffix(normalizeModelName(String(model || "")).toLowerCase()); const compactModelKey = modelKey.replace(/-/g, ""); + // GPT-6.1 Sol purchased-credit/USD Fast rate is 2x, not the 2.5x + // included-subscription consumption multiplier (codex/pricing and codex/speed). + if (compactModelKey === "gpt6.1sol") return 2; // Codex GPT-6 Fast is 2.5x Standard (https://developers.openai.com/codex/pricing). - if ( - /^gpt-6-(?:astra|sol|luna)$/.test(modelKey) || - /^gpt6(?:astra|sol|luna)$/.test(compactModelKey) - ) { + if (/^gpt6(?:astra|sol|luna)$/.test(compactModelKey)) { return 2.5; } if ( diff --git a/src/lib/vscode/reasoningMetadata.ts b/src/lib/vscode/reasoningMetadata.ts index 9c954332cbf..7f3cb91aabd 100644 --- a/src/lib/vscode/reasoningMetadata.ts +++ b/src/lib/vscode/reasoningMetadata.ts @@ -18,7 +18,7 @@ export type VscodeCatalogModel = { const STANDARD_EFFORT_SUFFIX_PATTERN = /-(xhigh|high|medium|low|none)$/i; const CODEX_EXTENDED_EFFORT_SUFFIX_PATTERN = - /^(.*gpt-(?:5\.6-(?:sol|terra|luna)|6-(?:astra|sol|luna)))-(max|ultra)$/i; + /^(.*gpt-(?:5\.6-(?:sol|terra|luna)|6-(?:astra|sol|luna)|6\.1-sol))-(max|ultra)$/i; const DEFAULT_REASONING_EFFORT = "none"; const KNOWN_REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh", "max", "ultra"]); @@ -190,6 +190,7 @@ function getCodexGpt56DefaultReasoningEffort(model: VscodeCatalogModel) { const providerModelId = (parsed.model || model.root || modelId.split("/").pop() || modelId) .trim() .toLowerCase(); + if (/^gpt-6\.1-sol(?:-(?:low|medium|high|xhigh|max|ultra))?$/.test(providerModelId)) return "low"; const match = providerModelId.match( /^gpt-(?:5\.6-(sol|terra|luna)|6-(?:astra|sol|luna))(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/ ); diff --git a/src/shared/constants/codexClient.ts b/src/shared/constants/codexClient.ts index 108f32a7036..e8d2b854d22 100644 --- a/src/shared/constants/codexClient.ts +++ b/src/shared/constants/codexClient.ts @@ -3,7 +3,7 @@ // refresh this so the fingerprint OpenAI sees from the OAuth/Responses face // matches the real client version. Overridable per-deployment via // CODEX_CLIENT_VERSION. -export const DEFAULT_CODEX_CLIENT_VERSION = "0.156.1"; +export const DEFAULT_CODEX_CLIENT_VERSION = "0.159.2"; export const CODEX_CLI_RS_ORIGINATOR = "codex_cli_rs"; export function getCodexCliRsHeaders( diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index 0a140e855df..d98c375b2ae 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -128,6 +128,11 @@ const GEMINI_36_FLASH_MODEL_SPEC = { } satisfies ModelSpec; export const MODEL_SPECS: Record = { + // Public API limits; Codex's smaller window lives in its provider registry. + "gpt-6.1-sol": { + ...GPT_5_6_MODEL_SPEC, + aliases: ["openai/gpt-6.1-sol"], + }, // Public model limits; the Codex registry supplies its smaller OAuth window. // https://developers.openai.com/api/docs/models/gpt-6-astra "gpt-6-astra": { diff --git a/src/shared/constants/pricing/frontier-labs.ts b/src/shared/constants/pricing/frontier-labs.ts index 0d6e0ce8878..bfdceb824ec 100644 --- a/src/shared/constants/pricing/frontier-labs.ts +++ b/src/shared/constants/pricing/frontier-labs.ts @@ -21,6 +21,9 @@ import { export const DEFAULT_PRICING_FRONTIER = { openai: { + // Standard short-context USD/MTok. Long-context and other processing tiers + // are not represented by this static row. See the GPT-6.1 Sol API model page. + "gpt-6.1-sol": { input: 2, output: 10, cached: 0.1, reasoning: 10, cache_creation: 2.5 }, "gpt-6-astra": GPT_6_ASTRA_PRICING, "gpt-5.6": GPT_5_6_SOL_PRICING, "gpt-5.6-sol": GPT_5_6_SOL_PRICING, diff --git a/src/shared/constants/pricing/oauth-subscriptions.ts b/src/shared/constants/pricing/oauth-subscriptions.ts index 84df7cf369e..46880017d36 100644 --- a/src/shared/constants/pricing/oauth-subscriptions.ts +++ b/src/shared/constants/pricing/oauth-subscriptions.ts @@ -23,6 +23,9 @@ const ANTIGRAVITY_GEMINI_3_7_PRICING = { // Codex Standard: 250 / 25 / 1250 credits per MTok, at 25 credits per USD. // https://developers.openai.com/codex/pricing const GPT_6_ASTRA_CODEX_PRICING = GPT_6_ASTRA_PRICING; +// GPT-6.1 Sol: 50 / 2.5 / 250 Standard credits per MTok, at 25 credits/USD. +// Codex's credit card does not specify a separate cache-write rate. +const GPT_6_1_SOL_CODEX_PRICING = { input: 2, output: 10, cached: 0.1, reasoning: 10 }; // Codex Standard: Sol 50 / 5 / 250 and Luna 2.5 / 0.25 / 12.5 credits per MTok. const GPT_6_SOL_CODEX_PRICING = { input: 2.0, @@ -108,6 +111,13 @@ export const DEFAULT_PRICING_OAUTH = { }, }, cx: { + "gpt-6.1-sol": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-ultra": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-max": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-xhigh": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-high": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-medium": GPT_6_1_SOL_CODEX_PRICING, + "gpt-6.1-sol-low": GPT_6_1_SOL_CODEX_PRICING, "gpt-6-astra": GPT_6_ASTRA_CODEX_PRICING, "gpt-6-astra-ultra": GPT_6_ASTRA_CODEX_PRICING, "gpt-6-astra-max": GPT_6_ASTRA_CODEX_PRICING, diff --git a/src/shared/reasoning/effortStandardization.ts b/src/shared/reasoning/effortStandardization.ts index 7f34ddc3f98..b5e5c1dbd98 100644 --- a/src/shared/reasoning/effortStandardization.ts +++ b/src/shared/reasoning/effortStandardization.ts @@ -41,7 +41,7 @@ export function extendCodexGpt56EffortValues( } const match = normalizedModel.match( - /^gpt-(?:5\.6-(sol|terra|luna)|6-(astra|sol|luna))(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/ + /^gpt-(?:5\.6-(sol|terra|luna)|6-(astra|sol|luna)|6\.1-sol)(?:-(?:none|low|medium|high|xhigh|max|ultra))?$/ ); if (!match) return values; diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index b16d46e1350..7b21664b4e9 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -1334,23 +1334,23 @@ "Authorization": "Bearer ", "Content-Type": "application/json", "Openai-Beta": "responses_websockets=2026-02-06", - "User-Agent": "codex-cli/0.156.1 (; )", - "Version": "0.156.1" + "User-Agent": "codex-cli/0.159.2 (; )", + "Version": "0.159.2" }, "nonStream": { "Authorization": "Bearer ", "Content-Type": "application/json", "Openai-Beta": "responses_websockets=2026-02-06", - "User-Agent": "codex-cli/0.156.1 (; )", - "Version": "0.156.1" + "User-Agent": "codex-cli/0.159.2 (; )", + "Version": "0.159.2" }, "oauth": { "Accept": "text/event-stream", "Authorization": "Bearer ", "Content-Type": "application/json", "Openai-Beta": "responses_websockets=2026-02-06", - "User-Agent": "codex-cli/0.156.1 (; )", - "Version": "0.156.1" + "User-Agent": "codex-cli/0.159.2 (; )", + "Version": "0.159.2" } }, "url": { diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index bc061ae6e3f..25df2cf5376 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -184,10 +184,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_websockets=2026-02-06"); assert.equal(standardHeaders["X-Codex-Beta-Features"], undefined); - 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"); }); @@ -213,7 +213,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"); } ); diff --git a/tests/unit/gpt61-sol-catalog.test.ts b/tests/unit/gpt61-sol-catalog.test.ts new file mode 100644 index 00000000000..2a450082310 --- /dev/null +++ b/tests/unit/gpt61-sol-catalog.test.ts @@ -0,0 +1,174 @@ +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 { getModelSpec, capMaxOutputTokens } from "../../src/shared/constants/modelSpecs.ts"; +import { + computeCostFromPricing, + getCodexFastCostMultiplier, +} from "../../src/lib/usage/costCalculator.ts"; +import { extendCodexGpt56EffortValues } from "../../src/shared/reasoning/effortStandardization.ts"; +import { isCodexExtendedEffortBaseModel } from "../../src/shared/reasoning/codexExtendedEffort.ts"; +import * as reasoningMetadata from "../../src/lib/vscode/reasoningMetadata.ts"; +import { supportsVscodeServiceTierVariants } from "../../src/lib/vscode/serviceTierVariants.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 }; +}; + +function transform(model: string, body: Record = {}): ResponsesBody { + return new CodexExecutor().transformRequest(model, { model, input: [], ...body }, false, { + requestEndpointPath: "/responses", + }) as ResponsesBody; +} + +test("GPT-6.1 Sol has separate API and Codex catalog limits", () => { + for (const provider of ["codex", "codex-app-server"]) { + const entries = getModelsByProviderId(provider).filter((entry) => entry.id.startsWith(MODEL)); + assert.deepEqual(entries.map((entry) => entry.id).sort(), [...IDS].sort()); + for (const entry of entries) { + assert.equal(entry.contextLength, 872000); + assert.equal(entry.maxInputTokens, 872000); + assert.equal(entry.maxOutputTokens, 128000); + assert.equal(entry.targetFormat, "openai-responses"); + assert.equal(entry.supportsVision, true); + assert.equal(entry.supportsReasoning, true); + assert.equal(entry.toolCalling, true); + } + } + const api = getModelsByProviderId("openai").find((entry) => entry.id === MODEL); + assert.ok(api); + assert.equal(api.contextLength, 1050000); + assert.equal(api.maxOutputTokens, 128000); + assert.equal(api.targetFormat, "openai-responses"); + assert.deepEqual(api.supportedThinkingEfforts, EFFORTS.slice(0, -1)); + assert.equal(getModelSpec(`openai/${MODEL}`)?.contextWindow, 1050000); + assert.equal(capMaxOutputTokens(MODEL, 200000), 128000); +}); + +test("GPT-6.1 Sol aliases and chat translation preserve max on the Codex wire", () => { + for (const effort of EFFORTS) { + const result = transform(`${MODEL}-${effort}`); + assert.equal(result.model, MODEL); + assert.equal(result.reasoning.effort, effort === "ultra" ? "max" : effort); + } + for (const effort of ["max", "ultra"]) { + assert.equal(isCodexExtendedEffortBaseModel(`cx/${MODEL}`, effort as "max" | "ultra"), true); + const result = transform(`${MODEL}(${effort})`, { reasoning: { summary: "detailed" } }); + assert.equal(result.model, MODEL); + assert.equal(result.reasoning.effort, "max"); + assert.equal(result.reasoning.summary, "detailed"); + } + const translated = openaiToOpenAIResponsesRequest( + MODEL, + { + model: MODEL, + messages: [{ role: "user", content: "test" }], + reasoning_effort: "max", + }, + true, + {} + ) as ResponsesBody; + assert.equal(translated.reasoning.effort, "max"); + assert.equal(transform(MODEL, translated).reasoning.effort, "max"); +}); + +test("GPT-6.1 Sol uses low Codex defaults without changing explicit effort or older models", () => { + assert.equal(transform(MODEL).reasoning.effort, "low"); + assert.equal(transform(MODEL, { reasoning: { effort: "high" } }).reasoning.effort, "high"); + assert.equal(transform("gpt-6-sol").reasoning.effort, "medium"); + for (const provider of ["codex", "cx"]) { + assert.deepEqual(extendCodexGpt56EffortValues(provider, MODEL, ["none", "high"]), EFFORTS); + const model = { + id: `${provider}/${MODEL}`, + owned_by: provider, + capabilities: { reasoning: true }, + }; + assert.deepEqual(reasoningMetadata.getReasoningEffortValues(model), EFFORTS); + assert.equal(reasoningMetadata.getDefaultReasoningEffort(model), "low"); + assert.equal(reasoningMetadata.getReasoningVariantBaseModelId(`${model.id}-ultra`), model.id); + assert.equal(supportsVscodeServiceTierVariants(model), true); + } + assert.deepEqual(extendCodexGpt56EffortValues("kiro", MODEL, ["high"]), ["high"]); + assert.deepEqual( + extendCodexGpt56EffortValues("openai", MODEL, ["low", "medium", "high", "xhigh", "max"]), + EFFORTS.slice(0, -1) + ); +}); + +test("GPT-6.1 Sol USD pricing uses the current Standard and purchased-credit Fast rates", () => { + for (const [provider, ids] of [ + ["openai", [MODEL]], + ["cx", IDS], + ] as const) { + for (const id of ids) { + const price = getPricingForModel(provider, id); + assert.ok(price, `${provider}/${id}`); + assert.equal(price.input, 2); + assert.equal(price.cached, 0.1); + assert.equal(price.output, 10); + assert.equal(price.reasoning, 10); + if (provider === "openai") assert.equal(price.cache_creation, 2.5); + assert.equal(getCodexFastCostMultiplier(provider, id, "priority"), provider === "cx" ? 2 : 1); + } + } + assert.equal(getCodexFastCostMultiplier("codex", `${MODEL}-ultra`, "fast"), 2); + assert.equal(getCodexFastCostMultiplier("codex", MODEL, "default"), 1); + const price = getPricingForModel("cx", MODEL); + assert.ok(price); + // 500 uncached input + 500 cached input + 100 output = $0.00205, Fast = $0.0041. + const tokens = { prompt_tokens: 1000, cached_tokens: 500, completion_tokens: 100 }; + assert.ok( + Math.abs( + computeCostFromPricing(price, tokens, { + provider: "codex", + model: MODEL, + serviceTier: "priority", + }) - 0.0041 + ) < 1e-12 + ); +}); + +test("Responses Lite keeps delegation for GPT-6.1 Sol ultra only", async () => { + for (const effort of ["ultra", "max"]) { + const captured: Record[] = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = async (_url, init) => { + captured.push(JSON.parse(String(init?.body || "{}"))); + return new Response(JSON.stringify({ id: "resp_test", object: "response" }), { + headers: { "Content-Type": "application/json" }, + }); + }; + try { + await new CodexExecutor().execute({ + model: `${MODEL}-${effort}`, + body: { + model: `${MODEL}-${effort}`, + input: [], + _nativeCodexPassthrough: true, + parallel_tool_calls: true, + }, + stream: true, + credentials: { accessToken: "test-token" }, + clientHeaders: { "X-OpenAI-Internal-Codex-Responses-Lite": "true" }, + }); + } finally { + globalThis.fetch = originalFetch; + } + assert.equal(captured.length, 1); + assert.equal(captured[0].model, MODEL); + assert.equal(captured[0].parallel_tool_calls, effort === "ultra"); + } +}); diff --git a/tests/unit/openai-gpt56-catalog.test.ts b/tests/unit/openai-gpt56-catalog.test.ts index c675af48fe7..6dd65285d7b 100644 --- a/tests/unit/openai-gpt56-catalog.test.ts +++ b/tests/unit/openai-gpt56-catalog.test.ts @@ -9,9 +9,16 @@ import { DefaultExecutor } from "../../open-sse/executors/default.ts"; import { resolveChatCoreTargetFormat } from "../../open-sse/handlers/chatCore/targetFormat.ts"; import { openaiToOpenAIResponsesRequest } from "../../open-sse/translator/request/openai-responses/toResponses.ts"; -const EXPECTED_MODELS = ["gpt-6-astra", "gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]; +const EXPECTED_MODELS = [ + "gpt-6.1-sol", + "gpt-6-astra", + "gpt-5.6", + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", +]; -test("OpenAI API catalog puts Astra before GPT-5.6 and keeps GPT-5.4", () => { +test("OpenAI API catalog puts GPT-6.1 Sol and Astra before GPT-5.6 and keeps GPT-5.4", () => { const models = getModelsByProviderId("openai"); assert.deepEqual( @@ -70,62 +77,63 @@ test("OpenAI Astra declares unsupported sampling parameters for the chat pipelin ]); }); -test("OpenAI Astra tool requests use Responses and retain each supported reasoning effort", async () => { - const model = "gpt-6-astra"; - assert.equal( - resolveChatCoreTargetFormat({ - provider: "openai", - resolvedModel: model, - apiFormat: undefined, - customModelTargetFormat: undefined, - providerSpecificData: null, - }).targetFormat, - "openai-responses" - ); - const originalFetch = globalThis.fetch; - const captured: Array<{ url: string; body: Record }> = []; - globalThis.fetch = async (url, init) => { - captured.push({ url: String(url), body: JSON.parse(String(init?.body || "{}")) }); - return new Response(JSON.stringify({ id: "resp_astra", object: "response", output: [] }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - }; - try { - for (const effort of ["low", "medium", "high", "xhigh", "max", "none", "minimal"]) { - const body = openaiToOpenAIResponsesRequest( - model, - { - model, - messages: [{ role: "user", content: "Call the test tool." }], - reasoning_effort: effort, - tools: [ - { - type: "function", - function: { name: "test_tool", parameters: { type: "object", properties: {} } }, - }, - ], - }, - false, - {} - ); - await new DefaultExecutor("openai").execute({ - model, - body, - stream: false, - credentials: { apiKey: "test-openai-key" }, +for (const model of ["gpt-6-astra", "gpt-6.1-sol"]) { + test(`OpenAI ${model} tool requests use Responses and retain each supported reasoning effort`, async () => { + assert.equal( + resolveChatCoreTargetFormat({ + provider: "openai", + resolvedModel: model, + apiFormat: undefined, + customModelTargetFormat: undefined, + providerSpecificData: null, + }).targetFormat, + "openai-responses" + ); + const originalFetch = globalThis.fetch; + const captured: Array<{ url: string; body: Record }> = []; + globalThis.fetch = async (url, init) => { + captured.push({ url: String(url), body: JSON.parse(String(init?.body || "{}")) }); + return new Response(JSON.stringify({ id: "resp_astra", object: "response", output: [] }), { + status: 200, + headers: { "Content-Type": "application/json" }, }); - const request = captured.at(-1)!; - assert.equal(request.url, "https://api.openai.com/v1/responses"); - assert.equal(request.body.model, model); - assert.equal( - (request.body.reasoning as { effort: string }).effort, - effort === "none" || effort === "minimal" ? "low" : effort - ); - assert.ok(Array.isArray(request.body.input)); - assert.equal((request.body.tools as Array<{ name: string }>)[0].name, "test_tool"); + }; + try { + for (const effort of ["low", "medium", "high", "xhigh", "max", "none", "minimal"]) { + const body = openaiToOpenAIResponsesRequest( + model, + { + model, + messages: [{ role: "user", content: "Call the test tool." }], + reasoning_effort: effort, + tools: [ + { + type: "function", + function: { name: "test_tool", parameters: { type: "object", properties: {} } }, + }, + ], + }, + false, + {} + ); + await new DefaultExecutor("openai").execute({ + model, + body, + stream: false, + credentials: { apiKey: "test-openai-key" }, + }); + const request = captured.at(-1)!; + assert.equal(request.url, "https://api.openai.com/v1/responses"); + assert.equal(request.body.model, model); + assert.equal( + (request.body.reasoning as { effort: string }).effort, + effort === "none" || effort === "minimal" ? "low" : effort + ); + assert.ok(Array.isArray(request.body.input)); + assert.equal((request.body.tools as Array<{ name: string }>)[0].name, "test_tool"); + } + } finally { + globalThis.fetch = originalFetch; } - } finally { - globalThis.fetch = originalFetch; - } -}); + }); +} From bcd186a0a3ad5f60ae25a7ba26b931c91b6cb325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E4=B9=98=E5=A6=8D=20=28Xiaoyaner=29?= Date: Wed, 30 Sep 2026 13:32:48 +0800 Subject: [PATCH 2/5] test(providers): keep the catalog ordering guard aligned with GPT-6.1 Sol Assert both the newly prepended Sol family and the retained Astra family in Codex and app-server catalogs. Keep the existing ordering guarantee rather than removing it. --- tests/unit/codex-astra.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/codex-astra.test.ts b/tests/unit/codex-astra.test.ts index e57169d2acf..989a4340c91 100644 --- a/tests/unit/codex-astra.test.ts +++ b/tests/unit/codex-astra.test.ts @@ -36,9 +36,10 @@ test("Codex exposes Astra and its effort variants with live OAuth limits", () => assert.equal(model.supportsVision, true); assert.equal(model.supportsXHighEffort, true); } + const solIds = ["gpt-6.1-sol", ...EFFORTS.map((effort) => `gpt-6.1-sol-${effort}`)]; assert.deepEqual( - models.slice(0, ids.length).map((model) => model.id), - ids + models.slice(0, solIds.length + ids.length).map((model) => model.id), + [...solIds, ...ids] ); } }); From 11143fed867b4104c6964851de9b7d0b99d7f6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E4=B9=98=E5=A6=8D=20=28Xiaoyaner=29?= Date: Wed, 30 Sep 2026 13:34:43 +0800 Subject: [PATCH 3/5] docs(changelog): record GPT-6.1 Sol catalog support --- changelog.d/features/15171-gpt-61-sol-catalog-pricing.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/features/15171-gpt-61-sol-catalog-pricing.md diff --git a/changelog.d/features/15171-gpt-61-sol-catalog-pricing.md b/changelog.d/features/15171-gpt-61-sol-catalog-pricing.md new file mode 100644 index 00000000000..1998156a2cb --- /dev/null +++ b/changelog.d/features/15171-gpt-61-sol-catalog-pricing.md @@ -0,0 +1 @@ +- **feat(providers):** Add GPT-6.1 Sol to the OpenAI and Codex catalogs with provider-specific limits, Standard pricing, Codex reasoning/Fast wiring, and a synchronized Codex 0.159.2 client identity ([#15171](https://github.com/diegosouzapw/OmniRoute/pull/15171)) — thanks @xiaoyaner0201 From db63e67860c5a2cdc7e0ec2c4b3ab0c62ee1a39b Mon Sep 17 00:00:00 2001 From: Qiancheng Yan Date: Wed, 30 Sep 2026 21:31:26 +0800 Subject: [PATCH 4/5] test(codex): align default tier catalog with GPT-6.1 Sol --- tests/unit/codex-fast-tier.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/codex-fast-tier.test.ts b/tests/unit/codex-fast-tier.test.ts index f686eeca057..bf65b821765 100644 --- a/tests/unit/codex-fast-tier.test.ts +++ b/tests/unit/codex-fast-tier.test.ts @@ -46,6 +46,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", From bc23cfd3f0d47e606e726d42ddf49f2fe802f6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E4=B9=98=E5=A6=8D=20=28Xiaoyaner=29?= Date: Fri, 2 Oct 2026 02:02:41 -0300 Subject: [PATCH 5/5] fix(codex): pin bare gpt-6.1-sol ids to the Codex provider Add the seven gpt-6.1-sol ids to CODEX_NATIVE_UNPREFIXED_MODELS (parity with gpt-6-astra and gpt-5.6-sol) and cover bare-id routing, catalog listing and pricing wiring with a test. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- open-sse/services/model.ts | 7 ++ tests/unit/codex-gpt61-sol-bare-id.test.ts | 111 +++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 tests/unit/codex-gpt61-sol-bare-id.test.ts diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index 3584b842ecb..59e4cbd461d 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -133,6 +133,13 @@ export const CODEX_NATIVE_UNPREFIXED_MODELS = new Set([ "gpt-6-astra-high", "gpt-6-astra-medium", "gpt-6-astra-low", + "gpt-6.1-sol", + "gpt-6.1-sol-ultra", + "gpt-6.1-sol-max", + "gpt-6.1-sol-xhigh", + "gpt-6.1-sol-high", + "gpt-6.1-sol-medium", + "gpt-6.1-sol-low", "gpt-5.6-sol", "gpt-5.6-sol-ultra", "gpt-5.6-sol-max", diff --git a/tests/unit/codex-gpt61-sol-bare-id.test.ts b/tests/unit/codex-gpt61-sol-bare-id.test.ts new file mode 100644 index 00000000000..6d86e0d145a --- /dev/null +++ b/tests/unit/codex-gpt61-sol-bare-id.test.ts @@ -0,0 +1,111 @@ +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"; + +// GPT-6.1 Sol is a Codex-native model like gpt-6-astra and the gpt-5.6 tiers. +// Its ids must be in CODEX_NATIVE_UNPREFIXED_MODELS, otherwise with both a Codex +// and an OpenAI connection active the bare `gpt-6.1-sol` id is ambiguous (or goes +// to OpenAI) and /v1/models never lists the bare Codex rows. + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-gpt61-bare-")); +process.env.DATA_DIR = TEST_DATA_DIR; +process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "codex-gpt61-bare-test-secret"; + +const core = await import("../../src/lib/db/core.ts"); +const apiKeysDb = await import("../../src/lib/db/apiKeys.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const { CODEX_NATIVE_UNPREFIXED_MODELS, getModelInfoCore } = + await import("../../open-sse/services/model.ts"); +const { getProviderModels } = await import("../../open-sse/config/providerModels.ts"); +const { getPricingForModel } = await import("../../src/shared/constants/pricing.ts"); +const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts"); + +const MODEL = "gpt-6.1-sol"; +const EFFORTS = ["ultra", "max", "xhigh", "high", "medium", "low"]; +const EXPECTED_IDS = [MODEL, ...EFFORTS.map((effort) => `${MODEL}-${effort}`)]; + +// The base id and its effort tiers, as registered for the codex provider. +const SOL_IDS = getProviderModels("codex") + .map((model) => model.id) + .filter((id) => id === MODEL || id.startsWith(`${MODEL}-`)); + +async function seedConnection(provider: "codex" | "openai") { + await providersDb.createProviderConnection({ + provider, + authType: provider === "codex" ? "oauth" : "apikey", + name: `${provider}-gpt61-bare`, + email: provider === "codex" ? "codex@example.com" : undefined, + apiKey: provider === "openai" ? "sk-openai-gpt61-bare" : undefined, + accessToken: provider === "codex" ? "codex-gpt61-bare-access" : undefined, + isActive: true, + testStatus: "active", + providerSpecificData: provider === "codex" ? { workspaceId: "ws-gpt61-bare" } : {}, + }); +} + +test.beforeEach(() => { + core.resetDbInstance(); + apiKeysDb.resetApiKeyState(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +}); + +test.after(() => { + core.resetDbInstance(); + apiKeysDb.resetApiKeyState(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); +}); + +test("the Codex catalog registers exactly the base GPT-6.1 Sol id and its six effort tiers", () => { + assert.deepEqual([...SOL_IDS].sort(), [...EXPECTED_IDS].sort()); +}); + +test("every GPT-6.1 Sol id in the Codex catalog is Codex-native when unprefixed", () => { + for (const id of SOL_IDS) { + assert.equal(CODEX_NATIVE_UNPREFIXED_MODELS.has(id), true, id); + } +}); + +test("every GPT-6.1 Sol Codex id resolves a non-zero pricing row", () => { + for (const id of SOL_IDS) { + const price = getPricingForModel("cx", id); + assert.ok(price, `cx/${id}`); + assert.ok(price.input > 0 && price.output > 0, `cx/${id} must not resolve to $0`); + } +}); + +test("bare gpt-6.1-sol routes to Codex when Codex and OpenAI are both active", async () => { + await seedConnection("codex"); + await seedConnection("openai"); + + for (const id of [MODEL, `${MODEL}-ultra`, `${MODEL}-low`]) { + const info = await getModelInfoCore(id, null); + assert.equal(info.provider, "codex", id); + assert.equal(info.model, id); + } +}); + +test("bare gpt-6.1-sol stays on OpenAI when no Codex connection is active", async () => { + await seedConnection("openai"); + + const info = await getModelInfoCore(MODEL, null); + assert.equal(info.provider, "openai"); + assert.equal(info.model, MODEL); +}); + +test("/v1/models lists the bare GPT-6.1 Sol ids under their codex/ rows", async () => { + await seedConnection("codex"); + + v1ModelsCatalog.__resetCatalogBuilderRunsForTest(); + const response = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models") + ); + const body = (await response.json()) as { data: Array<{ id: string; parent?: string | null }> }; + const parentById = new Map(body.data.map((row) => [row.id, row.parent ?? null])); + + for (const id of SOL_IDS) { + assert.equal(parentById.get(id), `codex/${id}`, id); + } +});