diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index bc8044a3cf..e9ce05eb69 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -140,6 +140,16 @@ const KIMI_CODING_SHARED = { const buildModels = (ids: readonly string[]): RegistryModel[] => ids.map((id) => ({ id, name: id })); +const GPT_5_5_CONTEXT_LENGTH = 1050000; +const GPT_5_5_CODEX_CAPABILITIES = { + targetFormat: "openai-responses", + toolCalling: true, + supportsReasoning: true, + supportsVision: true, + supportsXHighEffort: true, + contextLength: GPT_5_5_CONTEXT_LENGTH, +} as const; + const CHAT_OPENAI_COMPAT_MODELS: Record = { deepinfra: buildModels([ "Qwen/Qwen3-Coder-480B-A35B-Instruct", @@ -373,10 +383,10 @@ export const REGISTRY: Record = { }, models: [ { id: "codex-auto-review", name: "Codex Auto Review", targetFormat: "openai-responses" }, - { id: "gpt-5.5-xhigh", name: "GPT 5.5 (xHigh)", targetFormat: "openai-responses" }, - { id: "gpt-5.5-high", name: "GPT 5.5 (High)", targetFormat: "openai-responses" }, - { id: "gpt-5.5", name: "GPT 5.5 (Medium)", targetFormat: "openai-responses" }, - { id: "gpt-5.5-low", name: "GPT 5.5 (Low)", targetFormat: "openai-responses" }, + { id: "gpt-5.5-xhigh", name: "GPT 5.5 (xHigh)", ...GPT_5_5_CODEX_CAPABILITIES }, + { id: "gpt-5.5-high", name: "GPT 5.5 (High)", ...GPT_5_5_CODEX_CAPABILITIES }, + { id: "gpt-5.5", name: "GPT 5.5 (Medium)", ...GPT_5_5_CODEX_CAPABILITIES }, + { id: "gpt-5.5-low", name: "GPT 5.5 (Low)", ...GPT_5_5_CODEX_CAPABILITIES }, { id: "gpt-5.5-mini", name: "GPT 5.5 Mini", targetFormat: "openai-responses" }, { id: "gpt-5.4", name: "GPT 5.4", targetFormat: "openai-responses" }, { id: "gpt-5.4-mini", name: "GPT 5.4 Mini", targetFormat: "openai-responses" }, diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index fd3254708c..022c4389c4 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -202,6 +202,26 @@ type EffortLevel = (typeof EFFORT_ORDER)[number]; const CODEX_FAST_WIRE_VALUE = "priority"; const CODEX_RESPONSES_WS_URL = "wss://chatgpt.com/backend-api/codex/responses"; +function splitCodexReasoningSuffix(model: unknown): { + baseModel: string; + effort: EffortLevel | null; +} { + const modelId = typeof model === "string" ? model : ""; + for (const level of EFFORT_ORDER) { + if (modelId.endsWith(`-${level}`)) { + return { + baseModel: modelId.slice(0, -`-${level}`.length), + effort: level, + }; + } + } + return { baseModel: modelId, effort: null }; +} + +export function getCodexUpstreamModel(model: unknown): string { + return splitCodexReasoningSuffix(model).baseModel; +} + function stringifyCodexInstructionContent(content: unknown): string { if (typeof content === "string") { return content.trim(); @@ -469,10 +489,8 @@ function consumeResponsesStoreMarker(body: Record): unknown { return marker; } -function isCodexResponsesWebSocketRequired(model: string, credentials: unknown): boolean { - const normalizedModel = String(model || "") - .trim() - .toLowerCase(); +export function isCodexResponsesWebSocketRequired(model: string, credentials: unknown): boolean { + const normalizedModel = getCodexUpstreamModel(model).trim().toLowerCase(); if (normalizedModel === "gpt-5.5") return true; const providerSpecificData = credentials && typeof credentials === "object" @@ -626,7 +644,7 @@ export class CodexExecutor extends BaseExecutor { true, input.credentials )) as Record; - transformedBody.model = input.model; + transformedBody.model = getCodexUpstreamModel(transformedBody.model || input.model); delete transformedBody.stream; delete transformedBody.stream_options; @@ -988,16 +1006,13 @@ export class CodexExecutor extends BaseExecutor { delete body.messages; delete body.prompt; - const effortLevels = ["none", "low", "medium", "high", "xhigh"]; let modelEffort: string | null = null; let cleanModel = typeof body.model === "string" ? body.model : model; - for (const level of effortLevels) { - if (typeof cleanModel === "string" && cleanModel.endsWith(`-${level}`)) { - modelEffort = level; - body.model = cleanModel.slice(0, -`-${level}`.length); - cleanModel = body.model; - break; - } + const splitModel = splitCodexReasoningSuffix(cleanModel); + if (splitModel.effort) { + modelEffort = splitModel.effort; + body.model = splitModel.baseModel; + cleanModel = body.model; } const explicitReasoning = normalizeEffortValue(body?.reasoning?.effort); diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index fc062a6a6d..0f36a8a11b 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -18,6 +18,23 @@ export interface ModelSpec { } export const MODEL_SPECS: Record = { + "gpt-5.5-mini": { + maxOutputTokens: 128000, + contextWindow: 400000, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + }, + + "gpt-5.5": { + maxOutputTokens: 128000, + contextWindow: 1050000, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + aliases: ["gpt-5.5-xhigh", "gpt-5.5-high", "gpt-5.5-medium", "gpt-5.5-low", "gpt-5.5-none"], + }, + // ── Gemini 3 Flash series ─────────────────────────────────────── "gemini-3-flash": { maxOutputTokens: 65536, diff --git a/src/shared/constants/pricing.ts b/src/shared/constants/pricing.ts index 0ac5157e07..37e34d950f 100644 --- a/src/shared/constants/pricing.ts +++ b/src/shared/constants/pricing.ts @@ -11,6 +11,14 @@ const GPT_5_3_CODEX_PRICING = { cache_creation: 5.0, }; +const GPT_5_5_PRICING = { + input: 5.0, + output: 30.0, + cached: 0.5, + reasoning: 30.0, + cache_creation: 5.0, +}; + const CLAUDE_OPUS_4_PRICING = { input: 15.0, output: 75.0, @@ -167,6 +175,14 @@ export const DEFAULT_PRICING = { // OpenAI Codex (cx) cx: { + // GPT 5.5 + "gpt-5.5": GPT_5_5_PRICING, + "gpt5.5": GPT_5_5_PRICING, + "gpt-5.5-xhigh": GPT_5_5_PRICING, + "gpt-5.5-high": GPT_5_5_PRICING, + "gpt-5.5-medium": GPT_5_5_PRICING, + "gpt-5.5-low": GPT_5_5_PRICING, + "gpt-5.5-none": GPT_5_5_PRICING, // GPT 5.4 "gpt-5.4": { input: 5.0, @@ -584,6 +600,7 @@ export const DEFAULT_PRICING = { // OpenAI openai: { + "gpt-5.5": GPT_5_5_PRICING, "gpt-4o": { input: 2.5, output: 10.0, diff --git a/tests/unit/context-manager.test.ts b/tests/unit/context-manager.test.ts index 7cfa47fab8..a9ad714794 100644 --- a/tests/unit/context-manager.test.ts +++ b/tests/unit/context-manager.test.ts @@ -26,6 +26,10 @@ test("getTokenLimit: detects gemini", () => { assert.equal(getTokenLimit("gemini", "gemini-2.5-pro"), 1048576); }); +test("getTokenLimit: uses GPT-5.5 Codex model context", () => { + assert.equal(getTokenLimit("codex", "gpt-5.5"), 1050000); +}); + test("getTokenLimit: default fallback", () => { assert.equal(getTokenLimit("unknown"), 128000); }); diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index 16d7b177f3..cc3b43272e 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -7,6 +7,8 @@ import { getCodexModelScope, getCodexRateLimitKey, getCodexResetTime, + getCodexUpstreamModel, + isCodexResponsesWebSocketRequired, parseCodexQuotaHeaders, } from "../../open-sse/executors/codex.ts"; import { @@ -58,6 +60,10 @@ test("Codex helper functions isolate rate-limit scopes and parse quota headers", assert.equal(getCodexModelScope("codex-spark-mini"), "spark"); assert.equal(getCodexModelScope("gpt-5.3-codex"), "codex"); + assert.equal(getCodexModelScope("gpt-5.5-xhigh"), "codex"); + assert.equal(getCodexUpstreamModel("gpt-5.5-xhigh"), "gpt-5.5"); + assert.equal(isCodexResponsesWebSocketRequired("gpt-5.5-xhigh", {}), true); + assert.equal(isCodexResponsesWebSocketRequired("gpt-5.5-mini", {}), false); assert.equal(getCodexRateLimitKey("acct-1", "codex-spark-mini"), "acct-1:spark"); assert.equal(quota.usage5h, 100); assert.equal(quota.limit7d, 5000); @@ -276,8 +282,8 @@ test("CodexExecutor.transformRequest lets model suffix beat connection reasoning test("CodexExecutor.transformRequest keeps gpt-5.5 as the model and applies xhigh reasoning", () => { const executor = new CodexExecutor(); const result = executor.transformRequest( - "gpt-5.5", - { model: "gpt-5.5", input: [], reasoning_effort: "xhigh" }, + "gpt-5.5-xhigh", + { model: "gpt-5.5-xhigh", input: [] }, false, {} ); diff --git a/tests/unit/model-capabilities-registry.test.ts b/tests/unit/model-capabilities-registry.test.ts index 2273da658d..b6d00dab6f 100644 --- a/tests/unit/model-capabilities-registry.test.ts +++ b/tests/unit/model-capabilities-registry.test.ts @@ -102,6 +102,12 @@ test("canonical model capability resolver merges models.dev data and keeps stati modelCapabilities.capThinkingBudget("antigravity/gemini-3.1-pro-high", 40000), 32768 ); + + const codexGpt55 = modelCapabilities.getResolvedModelCapabilities("codex/gpt-5.5"); + assert.equal(codexGpt55.contextWindow, 1050000); + assert.equal(codexGpt55.maxOutputTokens, 128000); + assert.equal(codexGpt55.supportsThinking, true); + assert.equal(codexGpt55.supportsVision, true); }); test("GPT OSS and DeepSeek Reasoner models support tool calling", () => { diff --git a/tests/unit/t12-pricing-updates.test.ts b/tests/unit/t12-pricing-updates.test.ts index 3023f6faee..b47a399bc3 100644 --- a/tests/unit/t12-pricing-updates.test.ts +++ b/tests/unit/t12-pricing-updates.test.ts @@ -7,6 +7,12 @@ import { REGISTRY } from "../../open-sse/config/providerRegistry.ts"; test("T12: pricing table includes MiniMax, GLM, Kimi and gpt-5.4 mini entries", () => { const pricing = getDefaultPricing(); + assert.ok(pricing.cx["gpt-5.5"], "missing cx/gpt-5.5"); + assert.ok(pricing.cx["gpt-5.5-xhigh"], "missing cx/gpt-5.5-xhigh"); + assert.equal(pricing.cx["gpt-5.5"].input, 5.0); + assert.equal(pricing.cx["gpt-5.5"].cached, 0.5); + assert.equal(pricing.cx["gpt-5.5"].output, 30.0); + assert.ok(pricing.cx["gpt-5.4"], "missing cx/gpt-5.4"); assert.ok(pricing.cx["gpt-5.4-mini"], "missing cx/gpt-5.4-mini"); @@ -30,9 +36,12 @@ test("T12: pricing table includes MiniMax, GLM, Kimi and gpt-5.4 mini entries", }); test("T12: codex catalog includes GPT 5.5 entries", () => { - const codexModels = REGISTRY.codex.models.map((m) => m.id); - assert.ok(codexModels.includes("gpt-5.5"), "missing codex/gpt-5.5"); - assert.ok(codexModels.includes("gpt-5.5-mini"), "missing codex/gpt-5.5-mini"); + const codexModels = new Map(REGISTRY.codex.models.map((m) => [m.id, m])); + assert.ok(codexModels.has("gpt-5.5"), "missing codex/gpt-5.5"); + assert.ok(codexModels.has("gpt-5.5-mini"), "missing codex/gpt-5.5-mini"); + assert.equal(codexModels.get("gpt-5.5")?.contextLength, 1050000); + assert.equal(codexModels.get("gpt-5.5")?.supportsXHighEffort, true); + assert.equal(codexModels.get("gpt-5.5-xhigh")?.targetFormat, "openai-responses"); }); test("T12: minimax default model list starts with M2.7", () => { diff --git a/tests/unit/t31-t33-t34-t38-model-specs.test.ts b/tests/unit/t31-t33-t34-t38-model-specs.test.ts index 3ca83fe6e0..f1f60497e7 100644 --- a/tests/unit/t31-t33-t34-t38-model-specs.test.ts +++ b/tests/unit/t31-t33-t34-t38-model-specs.test.ts @@ -42,6 +42,8 @@ test("T33: thinkingLevel string is converted into numeric thinkingBudget", () => }); test("T34: max output tokens are capped by model spec", () => { + assert.equal(capMaxOutputTokens("gpt-5.5", 200000), 128000); + assert.equal(capMaxOutputTokens("gpt-5.5-xhigh", 200000), 128000); assert.equal(capMaxOutputTokens("gemini-3-flash", 131072), 65536); assert.equal(capMaxOutputTokens("gemini-3-flash"), 65536); assert.equal(capMaxOutputTokens("gemini-3.1-pro-high", 131072), 65535); @@ -49,6 +51,9 @@ test("T34: max output tokens are capped by model spec", () => { }); test("T38: modelSpecs exposes centralized helpers with alias and prefix lookup", () => { + assert.equal(getModelSpec("gpt-5.5").contextWindow, 1050000); + assert.equal(getModelSpec("gpt-5.5-high").maxOutputTokens, 128000); + assert.equal(getModelSpec("gpt-5.5-mini").contextWindow, 400000); assert.equal(typeof MODEL_SPECS["gemini-3.1-pro-high"], "object"); assert.equal(getModelSpec("gemini-3-pro-high").maxOutputTokens, 65535); assert.equal(getModelSpec("gemini-3-pro-preview").maxOutputTokens, 65535);