diff --git a/src/server/__tests__/providers.test.ts b/src/server/__tests__/providers.test.ts index 8bd7fbed..8d215bbd 100644 --- a/src/server/__tests__/providers.test.ts +++ b/src/server/__tests__/providers.test.ts @@ -355,6 +355,12 @@ describe('ProviderService', () => { expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('gpt-5.4') expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('gpt-5.3-codex') expect(typeof env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS).toBe('string') + expect(JSON.parse(env.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS)).toEqual({ + 'gpt-5.3-codex': 258_400, + 'gpt-5.4': 950_000, + 'gpt-5.5': 258_400, + 'gpt-5.4-mini': 258_400, + }) expect(env.ANTHROPIC_BASE_URL).toBeUndefined() expect(env.ANTHROPIC_API_KEY).toBeUndefined() expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined() diff --git a/src/server/services/openaiOfficialProvider.ts b/src/server/services/openaiOfficialProvider.ts index 5d39cbc4..bfc05a06 100644 --- a/src/server/services/openaiOfficialProvider.ts +++ b/src/server/services/openaiOfficialProvider.ts @@ -1,9 +1,10 @@ import { OPENAI_CODEX_API_ENDPOINT } from '../../services/openaiAuth/client.js' import { + OPENAI_CODEX_MODEL_CATALOG, OPENAI_DEFAULT_HAIKU_MODEL, OPENAI_DEFAULT_MAIN_MODEL, OPENAI_DEFAULT_SONNET_MODEL, - getOpenAIContextWindowForModel, + getOpenAICodexContextWindowForModel, } from '../../services/openaiAuth/models.js' import { MODEL_CONTEXT_WINDOWS_ENV_KEY } from '../../utils/model/modelContextWindows.js' import { getHahaOpenAIOAuthFilePath } from './hahaOpenAIOAuthService.js' @@ -28,8 +29,10 @@ const openAIModels: SavedProvider['models'] = { } const modelContextWindows = Object.fromEntries( - Object.values(openAIModels) - .map((model) => [model, getOpenAIContextWindowForModel(model)] as const) + OPENAI_CODEX_MODEL_CATALOG.map( + ({ value }) => + [value, getOpenAICodexContextWindowForModel(value)] as const, + ) .filter((entry): entry is readonly [string, number] => entry[1] !== null), ) diff --git a/src/services/compact/autoCompact.test.ts b/src/services/compact/autoCompact.test.ts index e4c5853e..913f4ade 100644 --- a/src/services/compact/autoCompact.test.ts +++ b/src/services/compact/autoCompact.test.ts @@ -36,6 +36,13 @@ describe('model context window resolution', () => { expect(getContextWindowForModel('glm-4.5-air')).toBe(128_000) }) + test('uses Codex OAuth effective context windows for OpenAI GPT models', () => { + expect(getContextWindowForModel('gpt-5.5')).toBe(258_400) + expect(getContextWindowForModel('gpt-5.4')).toBe(950_000) + expect(getContextWindowForModel('gpt-5.4-mini')).toBe(258_400) + expect(getContextWindowForModel('gpt-5.3-codex-spark')).toBe(121_600) + }) + test('uses per-model provider overrides before built-in defaults', () => { process.env[MODEL_CONTEXT_WINDOWS_ENV_KEY] = JSON.stringify({ 'deepseek-v4-pro': 500_000, diff --git a/src/services/openaiAuth/models.test.ts b/src/services/openaiAuth/models.test.ts index 284bf158..b8bcf9b6 100644 --- a/src/services/openaiAuth/models.test.ts +++ b/src/services/openaiAuth/models.test.ts @@ -1,9 +1,10 @@ import { describe, expect, test } from 'bun:test' import { + OPENAI_CODEX_LARGE_EFFECTIVE_CONTEXT_WINDOW, + OPENAI_CODEX_SPARK_EFFECTIVE_CONTEXT_WINDOW, + OPENAI_CODEX_STANDARD_EFFECTIVE_CONTEXT_WINDOW, OPENAI_DEFAULT_MAIN_MODEL, - OPENAI_GPT5_LARGE_CONTEXT_WINDOW, - OPENAI_GPT5_STANDARD_CONTEXT_WINDOW, - getOpenAIContextWindowForModel, + getOpenAICodexContextWindowForModel, isOpenAIResponsesModel, resolveOpenAICodexModel, } from './models.js' @@ -22,21 +23,21 @@ describe('openai auth model resolution', () => { expect(resolveOpenAICodexModel('opus')).toBe(OPENAI_DEFAULT_MAIN_MODEL) }) - test('maps large-context GPT-5 models to a 1.05M context window', () => { - expect(getOpenAIContextWindowForModel('gpt-5.5')).toBe( - OPENAI_GPT5_LARGE_CONTEXT_WINDOW, + test('maps Codex OAuth GPT models to effective Codex context windows', () => { + expect(getOpenAICodexContextWindowForModel('gpt-5.5')).toBe( + OPENAI_CODEX_STANDARD_EFFECTIVE_CONTEXT_WINDOW, ) - expect(getOpenAIContextWindowForModel('gpt-5.4')).toBe( - OPENAI_GPT5_LARGE_CONTEXT_WINDOW, + expect(getOpenAICodexContextWindowForModel('gpt-5.4')).toBe( + OPENAI_CODEX_LARGE_EFFECTIVE_CONTEXT_WINDOW, ) - }) - - test('maps standard GPT-5 models to a 400k context window', () => { - expect(getOpenAIContextWindowForModel('gpt-5.3-codex')).toBe( - OPENAI_GPT5_STANDARD_CONTEXT_WINDOW, + expect(getOpenAICodexContextWindowForModel('gpt-5.3-codex')).toBe( + OPENAI_CODEX_STANDARD_EFFECTIVE_CONTEXT_WINDOW, ) - expect(getOpenAIContextWindowForModel('gpt-5.4-mini')).toBe( - OPENAI_GPT5_STANDARD_CONTEXT_WINDOW, + expect(getOpenAICodexContextWindowForModel('gpt-5.4-mini')).toBe( + OPENAI_CODEX_STANDARD_EFFECTIVE_CONTEXT_WINDOW, + ) + expect(getOpenAICodexContextWindowForModel('gpt-5.3-codex-spark')).toBe( + OPENAI_CODEX_SPARK_EFFECTIVE_CONTEXT_WINDOW, ) }) }) diff --git a/src/services/openaiAuth/models.ts b/src/services/openaiAuth/models.ts index b5ed2505..1222d6dd 100644 --- a/src/services/openaiAuth/models.ts +++ b/src/services/openaiAuth/models.ts @@ -1,8 +1,22 @@ export const OPENAI_DEFAULT_MAIN_MODEL = 'gpt-5.3-codex' export const OPENAI_DEFAULT_SONNET_MODEL = 'gpt-5.4' export const OPENAI_DEFAULT_HAIKU_MODEL = 'gpt-5.4-mini' -export const OPENAI_GPT5_STANDARD_CONTEXT_WINDOW = 400_000 -export const OPENAI_GPT5_LARGE_CONTEXT_WINDOW = 1_050_000 +export const OPENAI_CODEX_EFFECTIVE_CONTEXT_PERCENT = 95 +export const OPENAI_CODEX_STANDARD_CONTEXT_WINDOW = 272_000 +export const OPENAI_CODEX_LARGE_CONTEXT_WINDOW = 1_000_000 +export const OPENAI_CODEX_SPARK_CONTEXT_WINDOW = 128_000 +export const OPENAI_CODEX_STANDARD_EFFECTIVE_CONTEXT_WINDOW = Math.floor( + (OPENAI_CODEX_STANDARD_CONTEXT_WINDOW * OPENAI_CODEX_EFFECTIVE_CONTEXT_PERCENT) / + 100, +) +export const OPENAI_CODEX_LARGE_EFFECTIVE_CONTEXT_WINDOW = Math.floor( + (OPENAI_CODEX_LARGE_CONTEXT_WINDOW * OPENAI_CODEX_EFFECTIVE_CONTEXT_PERCENT) / + 100, +) +export const OPENAI_CODEX_SPARK_EFFECTIVE_CONTEXT_WINDOW = Math.floor( + (OPENAI_CODEX_SPARK_CONTEXT_WINDOW * OPENAI_CODEX_EFFECTIVE_CONTEXT_PERCENT) / + 100, +) export type OpenAIModelCatalogEntry = { value: string @@ -80,6 +94,8 @@ export function getOpenAIModelDisplayName(model: string): string | null { switch (model.trim().toLowerCase()) { case 'gpt-5.3-codex': return 'GPT-5.3 Codex' + case 'gpt-5.3-codex-spark': + return 'GPT-5.3 Codex Spark' case 'gpt-5.5': return 'GPT-5.5' case 'gpt-5.4': @@ -101,21 +117,28 @@ export function getOpenAIModelDisplayName(model: string): string | null { } } -export function getOpenAIContextWindowForModel(model: string): number | null { +export function getOpenAICodexContextWindowForModel( + model: string, +): number | null { const normalized = model.trim().toLowerCase() - // Current OpenAI model docs split GPT-5-family context windows into - // standard 400k models and larger 1.05M models. + // Codex OAuth follows the Codex app model catalog, not the public API model + // context limits. The catalog applies effective_context_window_percent=95, + // and the runtime /context display reports this effective window. if ( - normalized === 'gpt-5.5' || - normalized === 'gpt-5.5-pro' || normalized === 'gpt-5.4' || normalized === 'gpt-5.4-pro' ) { - return OPENAI_GPT5_LARGE_CONTEXT_WINDOW + return OPENAI_CODEX_LARGE_EFFECTIVE_CONTEXT_WINDOW + } + + if (normalized === 'gpt-5.3-codex-spark') { + return OPENAI_CODEX_SPARK_EFFECTIVE_CONTEXT_WINDOW } if ( + normalized === 'gpt-5.5' || + normalized === 'gpt-5.5-pro' || normalized === 'gpt-5.4-mini' || normalized === 'gpt-5.4-nano' || normalized === 'gpt-5.3-codex' || @@ -130,8 +153,11 @@ export function getOpenAIContextWindowForModel(model: string): number | null { normalized === 'gpt-5-mini' || normalized === 'gpt-5-nano' ) { - return OPENAI_GPT5_STANDARD_CONTEXT_WINDOW + return OPENAI_CODEX_STANDARD_EFFECTIVE_CONTEXT_WINDOW } return null } + +export const getOpenAIContextWindowForModel = + getOpenAICodexContextWindowForModel diff --git a/src/utils/context.ts b/src/utils/context.ts index 8da26ad3..37045ed2 100644 --- a/src/utils/context.ts +++ b/src/utils/context.ts @@ -1,6 +1,6 @@ // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered import { CONTEXT_1M_BETA_HEADER } from '../constants/betas.js' -import { getOpenAIContextWindowForModel } from '../services/openaiAuth/models.js' +import { getOpenAICodexContextWindowForModel } from '../services/openaiAuth/models.js' import { getGlobalConfig } from './config.js' import { isEnvTruthy } from './envUtils.js' import { getCanonicalName } from './model/model.js' @@ -84,7 +84,7 @@ export function getContextWindowForModel( return configuredWindow } - const openAIContextWindow = getOpenAIContextWindowForModel(model) + const openAIContextWindow = getOpenAICodexContextWindowForModel(model) if (openAIContextWindow) { return openAIContextWindow }