diff --git a/.env.example b/.env.example index f259d4d854..415c157a8d 100644 --- a/.env.example +++ b/.env.example @@ -1311,7 +1311,8 @@ CLAUDE_USER_AGENT="claude-cli/2.1.258 (external, cli)" # stream with a misleading 400 out-of-extra-usage placeholder. Set to true to # forward the original names verbatim (debugging only). # CLAUDE_DISABLE_TOOL_NAME_CLOAK=false -CODEX_USER_AGENT="codex-cli/0.144.1 (Windows 10.0.26200; x64)" +# Optional override; leave unset to follow the shared Codex client version. +# CODEX_USER_AGENT="codex-cli/0.153.4 (Windows 10.0.26200; x64)" GITHUB_USER_AGENT="GitHubCopilotChat/0.54.0" ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0" KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0" @@ -1331,7 +1332,7 @@ CURSOR_USER_AGENT="Cursor/3.4" # Override Codex client version sent in headers independently of the # CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts. -# CODEX_CLIENT_VERSION=0.144.1 +# CODEX_CLIENT_VERSION=0.153.4 # # Override the advertised Claude Code client version independently of # CLAUDE_USER_AGENT. Anthropic gates some models (Fable 5.1) on this diff --git a/Dockerfile b/Dockerfile index 235745535d..e5979b4d21 100644 --- a/Dockerfile +++ b/Dockerfile @@ -340,7 +340,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.153.2 \ + @openai/codex@0.153.4 \ @anthropic-ai/claude-code@2.1.260 \ droid@0.212.0 \ openclaw@2026.9.1 diff --git a/changelog.d/features/codex-gpt-6-astra.md b/changelog.d/features/codex-gpt-6-astra.md new file mode 100644 index 0000000000..faec8f2369 --- /dev/null +++ b/changelog.d/features/codex-gpt-6-astra.md @@ -0,0 +1 @@ +- **feat(sse):** Codex and OpenAI catalogs list GPT-6 Astra with effort aliases (`-low` through `-ultra`); Codex CLI identity pins `@openai/codex@0.153.4` in lockstep with the image ([#13026](https://github.com/diegosouzapw/OmniRoute/pull/13026)) diff --git a/changelog.d/maintenance/layout-no-google-fonts.md b/changelog.d/maintenance/layout-no-google-fonts.md new file mode 100644 index 0000000000..cff6b2b9ef --- /dev/null +++ b/changelog.d/maintenance/layout-no-google-fonts.md @@ -0,0 +1 @@ +- **build:** root layout no longer loads Inter from `next/font/google`, so a production image build does not need fonts.googleapis.com ([#13026](https://github.com/diegosouzapw/OmniRoute/pull/13026)) diff --git a/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md index 54bfc297cd..036adde5a3 100644 --- a/docs/reference/PROVIDER_REFERENCE.md +++ b/docs/reference/PROVIDER_REFERENCE.md @@ -1,14 +1,14 @@ --- title: "Provider Reference" version: 3.8.51 -lastUpdated: 2026-09-03 +lastUpdated: 2026-09-05 --- # Provider Reference > **Auto-generated** from `src/shared/constants/providers.ts` — do not edit by hand. > Regenerate with: `npm run gen:provider-reference` -> **Last generated:** 2026-09-03 +> **Last generated:** 2026-09-05 Total providers: **356**. See category breakdown below. diff --git a/open-sse/config/providers/registry/codex/index.ts b/open-sse/config/providers/registry/codex/index.ts index a70549f7c1..9c4811aa54 100644 --- a/open-sse/config/providers/registry/codex/index.ts +++ b/open-sse/config/providers/registry/codex/index.ts @@ -28,6 +28,25 @@ export const codexProvider: RegistryEntry = { tokenUrl: "https://auth.openai.com/oauth/token", }, models: [ + // 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 }, + { id: "gpt-6-astra-ultra", name: "GPT 6 Astra (Ultra)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6-astra-max", name: "GPT 6 Astra (Max)", ...GPT_5_6_CODEX_CAPABILITIES }, + { + id: "gpt-6-astra-xhigh", + name: "GPT 6 Astra (xHigh)", + ...GPT_5_6_CODEX_CAPABILITIES, + timeoutMs: 1200000, + }, + { + id: "gpt-6-astra-high", + name: "GPT 6 Astra (High)", + ...GPT_5_6_CODEX_CAPABILITIES, + timeoutMs: 1200000, + }, + { id: "gpt-6-astra-medium", name: "GPT 6 Astra (Medium)", ...GPT_5_6_CODEX_CAPABILITIES }, + { id: "gpt-6-astra-low", name: "GPT 6 Astra (Low)", ...GPT_5_6_CODEX_CAPABILITIES }, { id: "gpt-5.6-sol", name: "GPT 5.6 Sol", diff --git a/open-sse/config/providers/registry/openai/index.ts b/open-sse/config/providers/registry/openai/index.ts index f2cd5dc2d5..673fc6bf0a 100644 --- a/open-sse/config/providers/registry/openai/index.ts +++ b/open-sse/config/providers/registry/openai/index.ts @@ -12,6 +12,15 @@ export const openaiProvider: RegistryEntry = { authHeader: "bearer", defaultContextLength: 128000, models: [ + // Astra shares the public GPT-5.6 limits; tool calling requires Responses. + // https://developers.openai.com/api/docs/guides/latest-model + { + id: "gpt-6-astra", + name: "GPT-6 Astra", + ...GPT_5_6_API_CAPABILITIES, + supportedThinkingEfforts: ["low", "medium", "high", "xhigh", "max"], + unsupportedParams: ["temperature", "top_p", "top_logprobs", "logprobs"], + }, // #11489: per OpenAI's model reference `gpt-5.6` is an ALIAS of `gpt-5.6-sol`, // not a distinct model — quality scores point forward, which no suffix // stripper can express. Siblings `-terra`/`-luna` are their own models. diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 1194fe4dee..25b316df7e 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -54,7 +54,7 @@ export { import { isCodexFreePlan, normalizeCodexTools } from "./codex/tools.ts"; import { CODEX_EFFORT_ORDER as EFFORT_ORDER, - GPT_5_6_ULTRA_ALIAS_MODELS, + CODEX_ULTRA_ALIAS_MODELS, splitCodexReasoningSuffix, type CodexEffortLevel as EffortLevel, } from "./codex/reasoningSuffix.ts"; @@ -167,13 +167,13 @@ function isCodexResponsesLiteRequest( ); } -// GPT-5.6 ultra-tier (sol/terra at "ultra") and luna at "max" coordinate delegation to +// Astra/Sol/Terra at "ultra" and Luna at "max" coordinate delegation to // sub-agents via parallel tool calls (see the effort-clamp comment near clampEffort()). // Responses Lite must not strip parallel_tool_calls for those model/effort combos, or // 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" && CODEX_ULTRA_ALIAS_MODELS.has(baseModel)) return true; if (effort === "max" && baseModel === "gpt-5.6-luna") return true; return false; } @@ -324,12 +324,9 @@ function normalizeServiceTierValue(value: unknown): string | undefined { return normalized; } -/** - * 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; unlisted models keep 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", diff --git a/open-sse/executors/codex/reasoningSuffix.ts b/open-sse/executors/codex/reasoningSuffix.ts index 37cf237f6d..28b472a8f7 100644 --- a/open-sse/executors/codex/reasoningSuffix.ts +++ b/open-sse/executors/codex/reasoningSuffix.ts @@ -8,25 +8,28 @@ 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 CODEX_MAX_ALIAS_MODELS = new Set([ + "gpt-5.6-sol", + "gpt-5.6-terra", + "gpt-5.6-luna", + "gpt-6-astra", +]); +export const CODEX_ULTRA_ALIAS_MODELS = new Set(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-6-astra"]); 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 }; } diff --git a/open-sse/translator/request/openai-responses/helpers.ts b/open-sse/translator/request/openai-responses/helpers.ts index 7f31eb99ea..1450693f8f 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)(?:-(?: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/app/layout.tsx b/src/app/layout.tsx index a1373e31ab..a07aff90ee 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,3 @@ -import { Inter } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/shared/components/ThemeProvider"; import { NextIntlClientProvider } from "next-intl"; @@ -11,11 +10,6 @@ import { PwaRegister } from "@/shared/components/PwaRegister"; import { LocaleAutoDetect } from "@/shared/components/LocaleAutoDetect"; import { BasePathNetworkProvider } from "@/shared/components/BasePathNetworkProvider"; -const inter = Inter({ - subsets: ["latin"], - variable: "--font-inter", -}); - export const viewport: Viewport = { themeColor: "#0b0f1a", viewportFit: "cover", @@ -135,7 +129,7 @@ export default async function RootLayout({ children }) { }} /> - + = { + // Public model limits; the Codex registry supplies its smaller OAuth window. + // https://developers.openai.com/api/docs/models/gpt-6-astra + "gpt-6-astra": { + ...GPT_5_6_MODEL_SPEC, + aliases: ["openai/gpt-6-astra"], + }, "gpt-5.6": { ...GPT_5_6_MODEL_SPEC, aliases: ["openai/gpt-5.6"], diff --git a/src/shared/constants/pricing/frontier-labs.ts b/src/shared/constants/pricing/frontier-labs.ts index b8a80d3bf1..0d6e0ce887 100644 --- a/src/shared/constants/pricing/frontier-labs.ts +++ b/src/shared/constants/pricing/frontier-labs.ts @@ -3,6 +3,7 @@ * Pure data; merged by default-pricing.ts via spread (god-file decomposition; semantic split). */ import { + GPT_6_ASTRA_PRICING, GEMINI_3_7_FLASH_PROMO_PRICING, GPT_5_5_PRICING, GPT_5_6_LUNA_PRICING, @@ -20,6 +21,7 @@ import { export const DEFAULT_PRICING_FRONTIER = { openai: { + "gpt-6-astra": GPT_6_ASTRA_PRICING, "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 9d255e0e24..a6e68896ba 100644 --- a/src/shared/constants/pricing/oauth-subscriptions.ts +++ b/src/shared/constants/pricing/oauth-subscriptions.ts @@ -3,6 +3,7 @@ * Pure data; merged by default-pricing.ts via spread (god-file decomposition; semantic split). */ import { + GPT_6_ASTRA_PRICING, CLAUDE_FABLE_5_1_PRICING, CLAUDE_OPUS_5_PRICING, GEMINI_3_7_FLASH_PROMO_PRICING, @@ -19,6 +20,10 @@ const ANTIGRAVITY_GEMINI_3_7_PRICING = { "gemini-3.7-flash-high": GEMINI_3_7_FLASH_PROMO_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; + export const DEFAULT_PRICING_OAUTH = { cc: { "claude-fable-5-1": CLAUDE_FABLE_5_1_PRICING, @@ -88,6 +93,13 @@ export const DEFAULT_PRICING_OAUTH = { }, }, cx: { + "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, + "gpt-6-astra-xhigh": GPT_6_ASTRA_CODEX_PRICING, + "gpt-6-astra-high": GPT_6_ASTRA_CODEX_PRICING, + "gpt-6-astra-medium": GPT_6_ASTRA_CODEX_PRICING, + "gpt-6-astra-low": GPT_6_ASTRA_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/constants/pricing/shared-tiers.ts b/src/shared/constants/pricing/shared-tiers.ts index 7cb674df09..f6c0382ee4 100644 --- a/src/shared/constants/pricing/shared-tiers.ts +++ b/src/shared/constants/pricing/shared-tiers.ts @@ -1,6 +1,16 @@ /** * Pricing data — shared per-MTok tier constants (god-file decomposition). Pure data; merged by the barrel. */ +// OpenAI API Standard; Codex Standard has the same dollar-equivalent rates. +// https://openai.com/index/gpt-6-astra/ +export const GPT_6_ASTRA_PRICING = { + input: 10.0, + output: 50.0, + cached: 1.0, + reasoning: 50.0, + cache_creation: 12.5, +}; + export const GPT_5_3_CODEX_PRICING = { input: 5.0, output: 20.0, diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index fb0b76f75a..2b1b0fd49c 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -1311,16 +1311,16 @@ "Authorization": "Bearer ", "Content-Type": "application/json", "Openai-Beta": "responses=experimental", - "User-Agent": "codex-cli/0.153.2 (; )", - "Version": "0.153.2", + "User-Agent": "codex-cli/0.153.4 (; )", + "Version": "0.153.4", "X-Codex-Beta-Features": "responses_websockets" }, "nonStream": { "Authorization": "Bearer ", "Content-Type": "application/json", "Openai-Beta": "responses=experimental", - "User-Agent": "codex-cli/0.153.2 (; )", - "Version": "0.153.2", + "User-Agent": "codex-cli/0.153.4 (; )", + "Version": "0.153.4", "X-Codex-Beta-Features": "responses_websockets" }, "oauth": { @@ -1328,8 +1328,8 @@ "Authorization": "Bearer ", "Content-Type": "application/json", "Openai-Beta": "responses=experimental", - "User-Agent": "codex-cli/0.153.2 (; )", - "Version": "0.153.2", + "User-Agent": "codex-cli/0.153.4 (; )", + "Version": "0.153.4", "X-Codex-Beta-Features": "responses_websockets" } }, diff --git a/tests/unit/agentrouter-chatcore-protocols.test.ts b/tests/unit/agentrouter-chatcore-protocols.test.ts index 27c40bb8c5..26e068ffe6 100644 --- a/tests/unit/agentrouter-chatcore-protocols.test.ts +++ b/tests/unit/agentrouter-chatcore-protocols.test.ts @@ -112,7 +112,7 @@ test("AgentRouter Responses requests automatically use the native Responses prot body: structuredClone(body), headers: new Headers({ accept: "application/json", originator: "codex_cli_rs" }), }, - userAgent: "codex_cli_rs/0.149.0", + userAgent: "codex_cli_rs/0.153.4", }); assert.ok(captured); @@ -176,7 +176,7 @@ test("AgentRouter OpenAI Chat requests automatically use the native Chat protoco body: structuredClone(body), headers: new Headers({ accept: "application/json" }), }, - userAgent: "codex_cli_rs/0.149.0", + userAgent: "codex_cli_rs/0.153.4", }); assert.equal(result.success, true); @@ -304,7 +304,7 @@ test("AgentRouter Responses streaming stays native without a connection protocol body: structuredClone(body), headers: new Headers({ accept: "text/event-stream", originator: "codex_cli_rs" }), }, - userAgent: "codex_cli_rs/0.149.0", + userAgent: "codex_cli_rs/0.153.4", }); assert.equal(result.success, true); @@ -383,7 +383,7 @@ test("AgentRouter OpenAI Chat streaming stays native without a connection protoc body: structuredClone(body), headers: new Headers({ accept: "text/event-stream" }), }, - userAgent: "codex_cli_rs/0.149.0", + userAgent: "codex_cli_rs/0.153.4", }); assert.equal(result.success, true); diff --git a/tests/unit/claude-codex-identity-version-sync.test.ts b/tests/unit/claude-codex-identity-version-sync.test.ts index 0c2a60754a..b59f90a01c 100644 --- a/tests/unit/claude-codex-identity-version-sync.test.ts +++ b/tests/unit/claude-codex-identity-version-sync.test.ts @@ -127,3 +127,39 @@ test("test 7: live-empty GitHub catalog path does not call persist", () => { const liveWindow = src.slice(liveIdx, start); assert.match(liveWindow, /buildApiDiscoveryResponse\s*\(/); }); + +test("Codex client version locksteps Dockerfile @openai/codex and env override", () => { + const dockerfile = fs.readFileSync(path.join(process.cwd(), "Dockerfile"), "utf8"); + const match = dockerfile.match(/@openai\/codex@([0-9]+\.[0-9]+\.[0-9]+)/); + assert.ok(match, "Dockerfile must pin @openai/codex@x.y.z"); + const pinned = match[1]; + assert.notEqual(pinned, "0.149.0"); + assert.equal(codexCfg.DEFAULT_CODEX_CLIENT_VERSION, pinned); + assert.equal(codexCfg.getCodexClientVersion(), pinned); + assert.equal(codexCfg.getCodexDefaultHeaders().Version, pinned); + assert.equal( + codexCfg.getCodexCliRsHeaders()["User-Agent"], + `codex_cli_rs/${pinned}`, + ); +}); + +test("test 7: live-empty GitHub catalog path does not call persist", () => { + const src = fs.readFileSync( + path.join(process.cwd(), "src/app/api/providers/[id]/models/route.ts"), + "utf8", + ); + // The githubCatalogModels fallback must use buildResponse, not buildApiDiscoveryResponse. + const idx = src.indexOf("Codex live catalog unavailable — using GitHub model catalog"); + assert.ok(idx > 0); + const start = src.lastIndexOf("if (githubCatalogModels", idx); + const end = src.indexOf("if (cachedDiscoveryModels", idx); + assert.ok(start > 0 && end > start); + const window = src.slice(start, end); + assert.match(window, /buildResponse\s*\(/); + assert.doesNotMatch(window, /buildApiDiscoveryResponse\s*\(/); + + const liveIdx = src.lastIndexOf("if (liveModels && liveModels.length > 0)"); + assert.ok(liveIdx > 0 && liveIdx < start); + const liveWindow = src.slice(liveIdx, start); + assert.match(liveWindow, /buildApiDiscoveryResponse\s*\(/); +}); diff --git a/tests/unit/codex-astra.test.ts b/tests/unit/codex-astra.test.ts new file mode 100644 index 0000000000..e57169d2ac --- /dev/null +++ b/tests/unit/codex-astra.test.ts @@ -0,0 +1,109 @@ +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 { getModelSpec } from "../../src/shared/constants/modelSpecs.ts"; +import { getPricingForModel } from "../../src/shared/constants/pricing.ts"; +import { getCodexFastCostMultiplier } from "../../src/lib/usage/costCalculator.ts"; + +const MODEL = "gpt-6-astra"; +const EFFORTS = ["ultra", "max", "xhigh", "high", "medium", "low"] as const; + +test.after(async () => { + const { resetDbInstance } = await import("../../src/lib/db/core.ts"); + resetDbInstance(); +}); + +test("Codex exposes Astra and its effort variants with live OAuth limits", () => { + const ids = [MODEL, ...EFFORTS.map((effort) => `${MODEL}-${effort}`)]; + for (const provider of ["codex", "codex-app-server"]) { + const models = getModelsByProviderId(provider); + assert.deepEqual( + models.filter((model) => model.id.startsWith(MODEL)).map((model) => model.id), + ids + ); + for (const id of ids) { + const model = models.find((entry) => entry.id === id); + assert.ok(model, `${provider}/${id}`); + assert.equal(model.contextLength, 872000); + assert.equal(model.maxInputTokens, 872000); + assert.equal(model.maxOutputTokens, 128000); + assert.equal(model.targetFormat, "openai-responses"); + assert.equal(model.toolCalling, true); + assert.equal(model.supportsReasoning, true); + assert.equal(model.supportsVision, true); + assert.equal(model.supportsXHighEffort, true); + } + assert.deepEqual( + models.slice(0, ids.length).map((model) => model.id), + ids + ); + } +}); + +test("Astra specs retain the public context window separately from Codex limits", () => { + const spec = getModelSpec(MODEL); + assert.equal(spec?.contextWindow, 1050000); + assert.equal(spec?.maxOutputTokens, 128000); + assert.equal(spec?.supportsTools, true); + assert.equal(spec?.supportsVision, true); + assert.equal(spec?.supportsThinking, true); +}); + +test("Astra effort aliases reach Codex as the base model and supported wire effort", () => { + const executor = new CodexExecutor(); + for (const effort of EFFORTS) { + const model = `${MODEL}-${effort}`; + const result = executor.transformRequest(model, { model, input: [] }, false, { + requestEndpointPath: "/responses", + }); + assert.equal(result.model, MODEL, effort); + assert.equal(result.reasoning.effort, effort === "ultra" ? "max" : effort, effort); + } +}); + +test("Chat-to-Codex translation preserves Astra max reasoning", () => { + const translated = openaiToOpenAIResponsesRequest( + MODEL, + { 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"); +}); + +test("Astra parenthesized effort overrides preserve the reasoning summary", () => { + for (const effort of ["max", "ultra"]) { + const model = `${MODEL}(${effort})`; + const result = new CodexExecutor().transformRequest( + model, + { model, input: [], reasoning: { effort: "low", summary: "detailed" } }, + false, + { requestEndpointPath: "/responses" } + ); + assert.equal(result.model, MODEL); + assert.equal(result.reasoning.effort, "max"); + assert.equal(result.reasoning.summary, "detailed"); + } +}); + +test("Astra Codex pricing and Fast multiplier match the Codex credit rate card", () => { + for (const model of [MODEL, ...EFFORTS.map((effort) => `${MODEL}-${effort}`)]) { + const pricing = getPricingForModel("cx", model); + assert.ok(pricing, model); + assert.equal(pricing.input, 10); + assert.equal(pricing.cached, 1); + assert.equal(pricing.output, 50); + assert.equal(pricing.reasoning, 50); + assert.equal(getCodexFastCostMultiplier("codex", model, "priority"), 2.5); + assert.equal(getCodexFastCostMultiplier("cx", model, "fast"), 2.5); + assert.equal(getCodexFastCostMultiplier("codex", model, "default"), 1); + } + assert.equal(getCodexFastCostMultiplier("openai", MODEL, "priority"), 1); +}); diff --git a/tests/unit/codex-gpt56-catalog.test.ts b/tests/unit/codex-gpt56-catalog.test.ts index c7d8075ffb..b1fe8a0ed9 100644 --- a/tests/unit/codex-gpt56-catalog.test.ts +++ b/tests/unit/codex-gpt56-catalog.test.ts @@ -29,7 +29,7 @@ test("Codex catalog exposes the GPT-5.6 lineup in configured priority order", () ]; assert.deepEqual( - models.slice(0, expectedIds.length).map((model) => model.id), + models.filter((model) => model.id.startsWith("gpt-5.6-")).map((model) => model.id), expectedIds ); diff --git a/tests/unit/executor-codex-gpt56-lite-ultra.test.ts b/tests/unit/executor-codex-gpt56-lite-ultra.test.ts index ed0995e17b..f7b5e8a7b0 100644 --- a/tests/unit/executor-codex-gpt56-lite-ultra.test.ts +++ b/tests/unit/executor-codex-gpt56-lite-ultra.test.ts @@ -70,6 +70,12 @@ test("Responses Lite must not strip parallel_tool_calls for GPT-5.6 luna max-tie assert.equal(capturedBodies[0].parallel_tool_calls, true); }); +test("Responses Lite preserves parallel tool calls for Astra ultra delegation", async () => { + const capturedBodies = await runLiteRequest("gpt-6-astra-ultra"); + assert.equal(capturedBodies.length, 1); + assert.equal(capturedBodies[0].parallel_tool_calls, true); +}); + test("Responses Lite still forces parallel_tool_calls:false for non-delegation GPT-5.5", async () => { const capturedBodies = await runLiteRequest("gpt-5.5"); assert.equal( diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index a7814b88f5..b795e9932f 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.153.2"); + assert.equal(standardHeaders.Version, "0.153.4"); assert.equal(standardHeaders["Openai-Beta"], "responses=experimental"); assert.equal(standardHeaders["X-Codex-Beta-Features"], "responses_websockets"); - assert.equal(standardHeaders["User-Agent"], "codex-cli/0.153.2 (Windows 10.0.26200; x64)"); + assert.equal(standardHeaders["User-Agent"], "codex-cli/0.153.4 (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.153.2"); + assert.equal(headers.Version, "0.153.4"); assert.equal(headers["User-Agent"], "custom-codex/9.9.9"); } ); diff --git a/tests/unit/openai-gpt56-catalog.test.ts b/tests/unit/openai-gpt56-catalog.test.ts index b928d6f7c5..c675af48fe 100644 --- a/tests/unit/openai-gpt56-catalog.test.ts +++ b/tests/unit/openai-gpt56-catalog.test.ts @@ -4,10 +4,14 @@ import assert from "node:assert/strict"; import { getModelsByProviderId } from "../../open-sse/config/providerModels.ts"; import { getModelSpec } from "../../src/shared/constants/modelSpecs.ts"; import { getPricingForModel } from "../../src/shared/constants/pricing.ts"; +import { getUnsupportedParams } from "../../open-sse/config/providerRegistry.ts"; +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-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]; +const EXPECTED_MODELS = ["gpt-6-astra", "gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]; -test("OpenAI API catalog exposes the public GPT-5.6 family and keeps GPT-5.4", () => { +test("OpenAI API catalog puts Astra before GPT-5.6 and keeps GPT-5.4", () => { const models = getModelsByProviderId("openai"); assert.deepEqual( @@ -38,8 +42,9 @@ test("OpenAI API catalog exposes the public GPT-5.6 family and keeps GPT-5.4", ( } }); -test("OpenAI API GPT-5.6 pricing matches the published standard tier", () => { +test("OpenAI API Astra and GPT-5.6 pricing matches the published standard tier", () => { const expectedPricing = { + "gpt-6-astra": { input: 10, cached: 1, cache_creation: 12.5, output: 50 }, "gpt-5.6": { input: 5, cached: 0.5, cache_creation: 6.25, output: 30 }, "gpt-5.6-sol": { input: 5, cached: 0.5, cache_creation: 6.25, output: 30 }, "gpt-5.6-terra": { input: 2.5, cached: 0.25, cache_creation: 3.125, output: 15 }, @@ -55,3 +60,72 @@ test("OpenAI API GPT-5.6 pricing matches the published standard tier", () => { assert.equal(pricing.output, expected.output, `${modelId} output`); } }); + +test("OpenAI Astra declares unsupported sampling parameters for the chat pipeline", () => { + assert.deepEqual(getUnsupportedParams("openai", "gpt-6-astra"), [ + "temperature", + "top_p", + "top_logprobs", + "logprobs", + ]); +}); + +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" }, + }); + 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; + } +});