Add Codex GPT-5.5 support (#1617)

Integrated into release/v3.7.1
This commit is contained in:
Aleksandr
2026-04-26 20:56:25 +03:00
committed by GitHub
parent dd67b25df1
commit 0c90c9768b
9 changed files with 111 additions and 22 deletions

View File

@@ -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<string, RegistryModel[]> = {
deepinfra: buildModels([
"Qwen/Qwen3-Coder-480B-A35B-Instruct",
@@ -373,10 +383,10 @@ export const REGISTRY: Record<string, RegistryEntry> = {
},
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" },

View File

@@ -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<string, unknown>): 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<string, unknown>;
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);

View File

@@ -18,6 +18,23 @@ export interface ModelSpec {
}
export const MODEL_SPECS: Record<string, ModelSpec> = {
"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,

View File

@@ -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,

View File

@@ -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);
});

View File

@@ -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,
{}
);

View File

@@ -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", () => {

View File

@@ -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", () => {

View File

@@ -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);