From 9f41ee754bbb367058d15a27669dab279f485ddc Mon Sep 17 00:00:00 2001 From: Tasa <93864300+tasarren@users.noreply.github.com> Date: Mon, 28 Sep 2026 19:09:12 +0700 Subject: [PATCH] feat(codex): expose 1M context variants for GPT-6 and GPT-5.6 Add [1m] extended-context variants for gpt-6-astra/sol/luna and gpt-5.6-sol/terra/luna mapping to their base upstream model IDs with an 872k-token context window, share the codex capability table with the cx alias, align model discovery with the inference client version, respect per-connection enabledModels during account selection, and fall back to another account when Codex reports a model unsupported for a ChatGPT account. --- open-sse/config/errorConfig.js | 1 + open-sse/providers/capabilities.js | 8 ++++++++ open-sse/providers/registry/codex.js | 6 ++++++ open-sse/services/accountFallback.js | 3 ++- src/app/api/providers/[id]/models/route.js | 9 +++------ src/sse/handlers/chat.js | 6 +++--- src/sse/services/auth.js | 5 ++++- tests/unit/codex-extended-context.mjs | 19 +++++++++++++++++++ 8 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 tests/unit/codex-extended-context.mjs diff --git a/open-sse/config/errorConfig.js b/open-sse/config/errorConfig.js index 71491a4d..0197da75 100644 --- a/open-sse/config/errorConfig.js +++ b/open-sse/config/errorConfig.js @@ -58,6 +58,7 @@ const COOLDOWN = { */ export const ERROR_RULES = [ // --- Text-based rules (checked first, order = priority) --- + { provider: "codex", text: "model is not supported when using codex with a chatgpt account", cooldownMs: MAX_RATE_LIMIT_COOLDOWN_MS }, { text: "no credentials", cooldownMs: COOLDOWN.long }, { text: "request not allowed", cooldownMs: COOLDOWN.short }, { text: "improperly formed request", cooldownMs: COOLDOWN.long }, diff --git a/open-sse/providers/capabilities.js b/open-sse/providers/capabilities.js index deda5cfb..1e4240c8 100644 --- a/open-sse/providers/capabilities.js +++ b/open-sse/providers/capabilities.js @@ -161,6 +161,7 @@ const KIRO_GPT_5_6_CAPABILITIES = { vision: true, reasoning: true, search: true, // (lower than OpenAI API's 1.05M). Sol differs from Terra/Luna. #2720 const CODEX_GPT_56_SOL_CAPS = { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 372000, maxOutput: 128000 }; const CODEX_GPT_56_DEFAULT_CAPS = { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 272000, maxOutput: 128000 }; +const CODEX_EXTENDED_CAPS = { ...CODEX_GPT_56_DEFAULT_CAPS, contextWindow: 872000 }; /** * Provider-specific capability overrides. Keyed by provider alias/id. @@ -185,6 +186,12 @@ export const PROVIDER_CAPABILITIES = { "gpt-6-astra": { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 272000, maxOutput: 128000 }, "gpt-6-sol": { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 272000, maxOutput: 128000 }, "gpt-6-luna": { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 272000, maxOutput: 128000 }, + "gpt-6-astra[1m]": CODEX_EXTENDED_CAPS, + "gpt-6-sol[1m]": CODEX_EXTENDED_CAPS, + "gpt-6-luna[1m]": CODEX_EXTENDED_CAPS, + "gpt-5.6-sol[1m]": CODEX_EXTENDED_CAPS, + "gpt-5.6-terra[1m]": CODEX_EXTENDED_CAPS, + "gpt-5.6-luna[1m]": CODEX_EXTENDED_CAPS, "gpt-5.6-sol": CODEX_GPT_56_SOL_CAPS, "gpt-5.6-sol-review": CODEX_GPT_56_SOL_CAPS, "gpt-5.6-terra": CODEX_GPT_56_DEFAULT_CAPS, @@ -268,6 +275,7 @@ export const PROVIDER_CAPABILITIES = { // Qoder CN serves the identical model catalog from the CN gateway, so it shares // the intl Qoder capability table verbatim (vision/reasoning/contextWindow). PROVIDER_CAPABILITIES["qoder-cn"] = PROVIDER_CAPABILITIES["qoder"]; +PROVIDER_CAPABILITIES.cx = PROVIDER_CAPABILITIES.codex; /** * Pattern fallback — glob (* = wildcard), matched case-insensitively and diff --git a/open-sse/providers/registry/codex.js b/open-sse/providers/registry/codex.js index bfa30e3e..c468aefc 100644 --- a/open-sse/providers/registry/codex.js +++ b/open-sse/providers/registry/codex.js @@ -53,13 +53,19 @@ export default { }, models: [ { id: "gpt-6-astra", name: "GPT 6.0 Astra" }, + { id: "gpt-6-astra[1m]", name: "GPT 6.0 Astra (extended context)", upstreamModelId: "gpt-6-astra" }, { id: "gpt-6-sol", name: "GPT 6.0 Sol", responsesLite: true, thinkingLevels: GPT_6_LITE_THINKING_LEVELS }, + { id: "gpt-6-sol[1m]", name: "GPT 6.0 Sol (extended context)", upstreamModelId: "gpt-6-sol", responsesLite: true, thinkingLevels: GPT_6_LITE_THINKING_LEVELS }, { id: "gpt-6-luna", name: "GPT 6.0 Luna", responsesLite: true, thinkingLevels: GPT_6_LITE_THINKING_LEVELS }, + { id: "gpt-6-luna[1m]", name: "GPT 6.0 Luna (extended context)", upstreamModelId: "gpt-6-luna", responsesLite: true, thinkingLevels: GPT_6_LITE_THINKING_LEVELS }, { id: "gpt-5.6-sol", name: "GPT 5.6 Sol" }, + { id: "gpt-5.6-sol[1m]", name: "GPT 5.6 Sol (extended context)", upstreamModelId: "gpt-5.6-sol" }, { id: "gpt-5.6-sol-review", name: "GPT 5.6 Sol Review", upstreamModelId: "gpt-5.6-sol", quotaFamily: "review" }, { id: "gpt-5.6-terra", name: "GPT 5.6 Terra" }, + { id: "gpt-5.6-terra[1m]", name: "GPT 5.6 Terra (extended context)", upstreamModelId: "gpt-5.6-terra" }, { id: "gpt-5.6-terra-review", name: "GPT 5.6 Terra Review", upstreamModelId: "gpt-5.6-terra", quotaFamily: "review" }, { id: "gpt-5.6-luna", name: "GPT 5.6 Luna" }, + { id: "gpt-5.6-luna[1m]", name: "GPT 5.6 Luna (extended context)", upstreamModelId: "gpt-5.6-luna" }, { id: "gpt-5.6-luna-review", name: "GPT 5.6 Luna Review", upstreamModelId: "gpt-5.6-luna", quotaFamily: "review" }, { id: "gpt-5.5", name: "GPT 5.5" }, { id: "gpt-5.5-review", name: "GPT 5.5 Review", upstreamModelId: "gpt-5.5", quotaFamily: "review" }, diff --git a/open-sse/services/accountFallback.js b/open-sse/services/accountFallback.js index 766b9981..bc329e55 100644 --- a/open-sse/services/accountFallback.js +++ b/open-sse/services/accountFallback.js @@ -20,12 +20,13 @@ export function getQuotaCooldown(backoffLevel = 0) { * @param {number} backoffLevel - Current backoff level for exponential backoff * @returns {{ shouldFallback: boolean, cooldownMs: number, newBackoffLevel?: number }} */ -export function checkFallbackError(status, errorText, backoffLevel = 0) { +export function checkFallbackError(status, errorText, backoffLevel = 0, provider = null) { const lowerError = errorText ? (typeof errorText === "string" ? errorText : JSON.stringify(errorText)).toLowerCase() : ""; for (const rule of ERROR_RULES) { + if (rule.provider && rule.provider !== provider) continue; // Text-based rule: match substring in error message if (rule.text && lowerError && lowerError.includes(rule.text)) { if (rule.backoff) { diff --git a/src/app/api/providers/[id]/models/route.js b/src/app/api/providers/[id]/models/route.js index a2e09501..4e990bca 100644 --- a/src/app/api/providers/[id]/models/route.js +++ b/src/app/api/providers/[id]/models/route.js @@ -13,15 +13,12 @@ import { resolveConnectionProxyConfig } from "@/lib/network/connectionProxy"; import { resolveCursorModels } from "open-sse/services/cursorModels.js"; import { resolveZedModels } from "open-sse/shared/zedAuth.js"; import { resolveClineModels, resolveClinepassModels } from "open-sse/services/clinepassModels.js"; +import codexProvider from "open-sse/providers/registry/codex.js"; const GEMINI_CLI_MODELS_URL = "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels"; -// The /codex/models endpoint gates each entry by minimal_client_version against this -// value, and codex CLI's own manifest (openai/codex codex-rs/models-manager/models.json) -// already requires 0.144.0 for its newest models, so a stale client_version here comes -// back 200 with those entries quietly missing instead of erroring. -const CODEX_CLIENT_VERSION = "0.144.6"; -const CODEX_MODELS_URL = `https://chatgpt.com/backend-api/codex/models?client_version=${CODEX_CLIENT_VERSION}`; +// Model discovery must identify as the same Codex CLI version as inference. +const CODEX_MODELS_URL = `https://chatgpt.com/backend-api/codex/models?client_version=${codexProvider.transport.cliVersion}`; const parseOpenAIStyleModels = (data) => { if (Array.isArray(data)) return data; diff --git a/src/sse/handlers/chat.js b/src/sse/handlers/chat.js index 172549ea..f7070f9a 100644 --- a/src/sse/handlers/chat.js +++ b/src/sse/handlers/chat.js @@ -158,13 +158,13 @@ export async function handleChat(request, clientRawRequest = null) { }); } - return handleSingleModelChat(body, modelStr, clientRawRequest, request, apiKey); + return handleSingleModelChat(body, modelStr, clientRawRequest, request, apiKey, contextMarker ? `${modelStr.slice(modelStr.indexOf("/") + 1)}[${contextMarker}]` : null); } /** * Handle single model chat request */ -async function handleSingleModelChat(body, modelStr, clientRawRequest = null, request = null, apiKey = null) { +async function handleSingleModelChat(body, modelStr, clientRawRequest = null, request = null, apiKey = null, requestedModel = null) { const modelInfo = await getModelInfo(modelStr); // If provider is null, this might be a combo name - check and handle @@ -233,7 +233,7 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re let lastHeaders = null; while (true) { - const credentials = await getProviderCredentials(provider, excludeConnectionIds, model); + const credentials = await getProviderCredentials(provider, excludeConnectionIds, model, { requestedModel: requestedModel || model }); // All accounts unavailable if (!credentials || credentials.allRateLimited) { diff --git a/src/sse/services/auth.js b/src/sse/services/auth.js index eff83e7c..cbe0a737 100644 --- a/src/sse/services/auth.js +++ b/src/sse/services/auth.js @@ -31,6 +31,7 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu ? excludeConnectionIds : (excludeConnectionIds ? new Set([excludeConnectionIds]) : new Set()); const preferredConnectionId = options?.preferredConnectionId || null; + const requestedModel = options?.requestedModel || model; // Acquire mutex to prevent race conditions const currentMutex = selectionMutex; let resolveMutex; @@ -85,6 +86,8 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu const availableConnections = connections.filter(c => { if (excludeSet.has(c.id)) return false; if (isModelLockActive(c, model)) return false; + const enabled = c.providerSpecificData?.enabledModels; + if (providerId === "codex" && Array.isArray(enabled) && enabled.length && requestedModel && !enabled.includes(requestedModel)) return false; // Antigravity: skip if live quota exhausted for this model if (isAntigravity && model && antigravityQuotaCache) { const quota = antigravityQuotaCache.get(c.id)?.[model]; @@ -259,7 +262,7 @@ export async function markAccountUnavailable(connectionId, status, errorText, pr : Math.min(resetsAtMs - Date.now(), MAX_RATE_LIMIT_COOLDOWN_MS); newBackoffLevel = 0; } else { - ({ shouldFallback, cooldownMs, newBackoffLevel } = checkFallbackError(status, errorText, backoffLevel)); + ({ shouldFallback, cooldownMs, newBackoffLevel } = checkFallbackError(status, errorText, backoffLevel, resolveProviderId(provider))); } if (!shouldFallback) return { shouldFallback: false, cooldownMs: 0 }; diff --git a/tests/unit/codex-extended-context.mjs b/tests/unit/codex-extended-context.mjs new file mode 100644 index 00000000..9af970db --- /dev/null +++ b/tests/unit/codex-extended-context.mjs @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import codex from "../../open-sse/providers/registry/codex.js"; +import { getModelUpstreamId } from "../../open-sse/config/providerModels.js"; +import { getCapabilitiesForModel } from "../../open-sse/providers/capabilities.js"; +import { stripModelContextMarker } from "../../open-sse/utils/modelMarkers.js"; +import { checkFallbackError } from "../../open-sse/services/accountFallback.js"; + +for (const id of ["gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]) { + const extended = `${id}[1m]`; + assert.equal(codex.models.find((model) => model.id === extended)?.upstreamModelId, id); + assert.equal(getModelUpstreamId("cx", extended), id); + assert.equal(getCapabilitiesForModel("codex", extended).contextWindow, 872000); + assert.equal(getCapabilitiesForModel("cx", extended).contextWindow, 872000); + assert.deepEqual(stripModelContextMarker(`cx/${extended}`), { model: `cx/${id}`, contextMarker: "1m" }); +} +const unsupported = "The 'gpt-6-sol' model is not supported when using Codex with a ChatGPT account."; +assert.equal(checkFallbackError(400, unsupported, 0, "codex").shouldFallback, true); +assert.equal(checkFallbackError(400, "Invalid JSON body", 0, "codex").shouldFallback, false); +console.log("Codex extended models and account fallback OK");