fix: Align OpenAI Codex context windows

ChatGPT Official runs through Codex OAuth, so context accounting should follow
the Codex model catalog and its effective context window instead of public API
limits. This keeps GPT-5.5 at the same usable window shown by Codex while
preserving GPT-5.4's larger Codex window.

Constraint: Codex OAuth model metadata applies effective_context_window_percent to raw catalog windows
Rejected: Use public API GPT-5.5 1.05M context | Codex app and OAuth runtime clamp this model to the smaller effective window
Confidence: high
Scope-risk: narrow
Directive: Keep ChatGPT Official context values aligned to Codex model catalog behavior, not generic OpenAI API docs
Tested: bun test src/services/openaiAuth/models.test.ts src/services/compact/autoCompact.test.ts src/server/__tests__/providers.test.ts
Tested: bun run check:server
Tested: git diff --check
Not-tested: Manual desktop screenshot after rebuilding the app
This commit is contained in:
程序员阿江(Relakkes) 2026-05-21 14:15:09 +08:00
parent e437199b81
commit 0c43b270b9
6 changed files with 72 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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