diff --git a/src/shared/compatible-cli-contracts.ts b/src/shared/compatible-cli-contracts.ts index 0c0f1b42..7433d322 100644 --- a/src/shared/compatible-cli-contracts.ts +++ b/src/shared/compatible-cli-contracts.ts @@ -91,6 +91,8 @@ export interface CodexSupportMatrixEntry { export interface CodexUserConfigDiagnostics { model: string | null; modelReasoningEffort: string | null; + modelContextWindow: number | null; + modelAutoCompactTokenLimit: number | null; modelProvider: string | null; activeProfile: string | null; approvalPolicy: string | null; @@ -137,6 +139,8 @@ export interface CodexRawConfigResponse { export interface CodexTopLevelSettingsPatch { model?: string | null; modelReasoningEffort?: string | null; + modelContextWindow?: number | null; + modelAutoCompactTokenLimit?: number | null; modelProvider?: string | null; approvalPolicy?: string | null; sandboxMode?: string | null; diff --git a/src/web-server/services/codex-dashboard-service.ts b/src/web-server/services/codex-dashboard-service.ts index 725c0bc1..8bba991f 100644 --- a/src/web-server/services/codex-dashboard-service.ts +++ b/src/web-server/services/codex-dashboard-service.ts @@ -243,6 +243,18 @@ function applyTopLevelSettingsPatch( 'model_reasoning_effort' ); } + if (hasOwn(values, 'modelContextWindow')) { + setNumberField(target, 'model_context_window', values.modelContextWindow, { + integer: true, + min: 1, + }); + } + if (hasOwn(values, 'modelAutoCompactTokenLimit')) { + setNumberField(target, 'model_auto_compact_token_limit', values.modelAutoCompactTokenLimit, { + integer: true, + min: 1, + }); + } if (hasOwn(values, 'modelProvider')) { setStringField(target, 'model_provider', values.modelProvider); } @@ -778,6 +790,8 @@ export async function getCodexDashboardDiagnostics(): Promise { path.join(codexHome, 'config.toml'), `model = "gpt-5.4" profile = "work" +model_context_window = 800000 +model_auto_compact_token_limit = 700000 model_provider = "cliproxy" approval_policy = "never" sandbox_mode = "danger-full-access" @@ -217,6 +219,8 @@ model = "gpt-5.4" expect(diagnostics.binary.installed).toBe(true); expect(diagnostics.binary.supportsConfigOverrides).toBe(true); expect(diagnostics.config.model).toBe('gpt-5.4'); + expect(diagnostics.config.modelContextWindow).toBe(800000); + expect(diagnostics.config.modelAutoCompactTokenLimit).toBe(700000); expect(diagnostics.config.activeProfile).toBe('work'); expect(diagnostics.config.modelProvider).toBe('cliproxy'); expect(diagnostics.config.profileCount).toBe(1); @@ -376,6 +380,8 @@ bearer_token = "secret" values: { model: 'gpt-5.4', modelReasoningEffort: 'high', + modelContextWindow: 800000, + modelAutoCompactTokenLimit: 700000, approvalPolicy: 'never', sandboxMode: 'workspace-write', webSearch: 'cached', @@ -394,10 +400,14 @@ bearer_token = "secret" const diagnostics = await getCodexDashboardDiagnostics(); expect(diagnostics.config.model).toBe('gpt-5.4'); expect(diagnostics.config.modelReasoningEffort).toBe('high'); + expect(diagnostics.config.modelContextWindow).toBe(800000); + expect(diagnostics.config.modelAutoCompactTokenLimit).toBe(700000); expect(diagnostics.config.toolOutputTokenLimit).toBe(12000); expect(diagnostics.config.personality).toBe('friendly'); expect(diagnostics.config.projectTrust[0]?.path).toBe('/tmp/workspace-a'); expect(result.rawText).toContain('model = "gpt-5.4"'); + expect(result.rawText).toContain('model_context_window = 800000'); + expect(result.rawText).toContain('model_auto_compact_token_limit = 700000'); expect(result.config?.model).toBe('gpt-5.4'); }); @@ -665,4 +675,24 @@ bearer_token = "secret" }) ).rejects.toThrow(CodexRawConfigValidationError); }); + + it('rejects invalid long-context values in structured top-level patches', async () => { + await expect( + patchCodexConfig({ + kind: 'top-level', + values: { + modelContextWindow: 0, + }, + }) + ).rejects.toThrow(CodexRawConfigValidationError); + + await expect( + patchCodexConfig({ + kind: 'top-level', + values: { + modelAutoCompactTokenLimit: 1.5, + }, + }) + ).rejects.toThrow(CodexRawConfigValidationError); + }); }); diff --git a/ui/src/components/compatible-cli/codex-top-level-controls-card.tsx b/ui/src/components/compatible-cli/codex-top-level-controls-card.tsx index 15dc5927..2194b5b9 100644 --- a/ui/src/components/compatible-cli/codex-top-level-controls-card.tsx +++ b/ui/src/components/compatible-cli/codex-top-level-controls-card.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; -import { Loader2, SlidersHorizontal } from 'lucide-react'; +import { CircleAlert, Loader2, SlidersHorizontal } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { @@ -14,6 +15,11 @@ import type { CodexTopLevelSettingsView } from '@/lib/codex-config'; import { CodexConfigCardShell } from './codex-config-card-shell'; const UNSET = '__unset__'; +const GPT_54_MAX_CONTEXT_WINDOW = 1_050_000; +const GPT_54_STANDARD_CONTEXT_WINDOW = 272_000; +const CCS_GPT_54_STARTER_CONTEXT_WINDOW = 800_000; +const CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT = 700_000; +const INTEGER_FORMATTER = new Intl.NumberFormat('en-US'); interface CodexTopLevelControlsCardProps { values: CodexTopLevelSettingsView; @@ -32,6 +38,14 @@ function withCurrentValue(options: string[], current: string | null | undefined) return current && !options.includes(current) ? [current, ...options] : options; } +function formatInteger(value: number) { + return INTEGER_FORMATTER.format(value); +} + +function isGpt54ModelId(value: string | null | undefined) { + return value?.trim().toLowerCase().startsWith('gpt-5.4') ?? false; +} + function buildTopLevelPatch( initialValues: CodexTopLevelSettingsView, draft: CodexTopLevelSettingsView @@ -42,6 +56,12 @@ function buildTopLevelPatch( if (draft.modelReasoningEffort !== initialValues.modelReasoningEffort) { patch.modelReasoningEffort = draft.modelReasoningEffort; } + if (draft.modelContextWindow !== initialValues.modelContextWindow) { + patch.modelContextWindow = draft.modelContextWindow; + } + if (draft.modelAutoCompactTokenLimit !== initialValues.modelAutoCompactTokenLimit) { + patch.modelAutoCompactTokenLimit = draft.modelAutoCompactTokenLimit; + } if (draft.modelProvider !== initialValues.modelProvider) { patch.modelProvider = draft.modelProvider; } @@ -91,6 +111,11 @@ function TopLevelControlsForm({ const personalityOptions = withCurrentValue(['none', 'friendly', 'pragmatic'], draft.personality); const patch = buildTopLevelPatch(initialValues, draft); const hasChanges = Object.keys(patch).length > 0; + const isGpt54Selected = isGpt54ModelId(draft.model); + const parseOptionalInteger = (value: string) => { + const trimmed = value.trim(); + return trimmed.length > 0 ? Number(trimmed) : null; + }; return ( <> @@ -266,6 +291,214 @@ function TopLevelControlsForm({ +
+
+
+
+ +

Long context override

+ + Manual opt-in only + + + {isGpt54Selected ? 'GPT-5.4 selected' : 'GPT-5.4 reference'} + +
+

Draft values only. Nothing applies until Save.

+
+ +
+ + + +
+
+ +
+
+

+ Official max +

+

1.05M / 1M

+

GPT-5.4 context cap

+
+
+

+ Standard window +

+

+ {formatInteger(GPT_54_STANDARD_CONTEXT_WINDOW)} +

+

Normal usage window

+
+
+

+ Above 272K +

+

Counts 2x

+

Usage-limit cost above 272K

+
+
+ +
+
+
+

+ One cautious pair +

+
+ Context {formatInteger(CCS_GPT_54_STARTER_CONTEXT_WINDOW)} +
+
+ Auto-compact {formatInteger(CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT)} +
+
+
+ + Not official + + + Draft only + +
+
+
+ Quick-fill only. Review before saving. + {!isGpt54Selected && draft.model ? ( + + {draft.model} should be checked separately. + + ) : null} +
+
+ +
+
+

Model context window

+ + setDraft((current) => ({ + ...current, + modelContextWindow: parseOptionalInteger(event.target.value), + })) + } + placeholder="Unset" + disabled={disabled} + /> +

+ Writes model_context_window. Leave unset to keep Codex defaults. +

+
+ +
+

Auto-compact token limit

+ + setDraft((current) => ({ + ...current, + modelAutoCompactTokenLimit: parseOptionalInteger(event.target.value), + })) + } + placeholder="Unset" + disabled={disabled} + /> +

+ Writes model_auto_compact_token_limit. Leave unset to keep model + defaults. +

+
+
+ +
+ Docs + + GPT-5.4 model page + + + Release notes + + + Config reference + +
+
+