feat(codex-dashboard): add manual long-context controls

This commit is contained in:
Tam Nhu Tran
2026-04-01 14:57:56 -04:00
parent b3ec9ff78d
commit 3246c40319
9 changed files with 359 additions and 1 deletions
+4
View File
@@ -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;
@@ -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<CodexDashboardDiag
config: {
model: asString(config?.model),
modelReasoningEffort: asString(config?.model_reasoning_effort),
modelContextWindow: asNumber(config?.model_context_window),
modelAutoCompactTokenLimit: asNumber(config?.model_auto_compact_token_limit),
modelProvider: asString(config?.model_provider),
activeProfile,
approvalPolicy: summarizeApprovalPolicy(config?.approval_policy),
@@ -179,6 +179,8 @@ describe('codex-dashboard-service', () => {
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);
});
});
@@ -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({
</div>
</div>
<div className="space-y-4 rounded-xl border border-amber-500/30 bg-amber-500/5 p-4 shadow-sm dark:bg-amber-400/5">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<CircleAlert className="h-4 w-4 text-amber-600 dark:text-amber-300" />
<p className="text-sm font-semibold">Long context override</p>
<Badge
variant="outline"
className="border-amber-500/40 bg-background/80 text-[10px] uppercase tracking-[0.16em] text-amber-700 dark:text-amber-300"
>
Manual opt-in only
</Badge>
<Badge
variant="secondary"
className="text-[10px] uppercase tracking-[0.16em] text-muted-foreground"
>
{isGpt54Selected ? 'GPT-5.4 selected' : 'GPT-5.4 reference'}
</Badge>
</div>
<p className="text-xs text-muted-foreground">Draft values only. Nothing applies until Save.</p>
</div>
<div className="flex flex-wrap gap-2">
<Button
type="button"
variant="outline"
size="sm"
disabled={disabled}
onClick={() =>
setDraft((current) => ({
...current,
modelContextWindow: CCS_GPT_54_STARTER_CONTEXT_WINDOW,
modelAutoCompactTokenLimit: CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT,
}))
}
>
Fill cautious pair
</Button>
<Button
type="button"
variant="outline"
size="sm"
disabled={disabled}
onClick={() =>
setDraft((current) => ({
...current,
modelContextWindow: GPT_54_MAX_CONTEXT_WINDOW,
}))
}
>
Set official max window
</Button>
<Button
type="button"
variant="ghost"
size="sm"
disabled={disabled}
onClick={() =>
setDraft((current) => ({
...current,
modelContextWindow: null,
modelAutoCompactTokenLimit: null,
}))
}
>
Clear
</Button>
</div>
</div>
<div className="grid gap-2 sm:grid-cols-3">
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
Official max
</p>
<p className="mt-1 font-mono text-base font-semibold text-foreground">1.05M / 1M</p>
<p className="mt-1 text-[11px] text-muted-foreground">GPT-5.4 context cap</p>
</div>
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
Standard window
</p>
<p className="mt-1 font-mono text-base font-semibold text-foreground">
{formatInteger(GPT_54_STANDARD_CONTEXT_WINDOW)}
</p>
<p className="mt-1 text-[11px] text-muted-foreground">Normal usage window</p>
</div>
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
Above 272K
</p>
<p className="mt-1 font-mono text-base font-semibold text-foreground">Counts 2x</p>
<p className="mt-1 text-[11px] text-muted-foreground">Usage-limit cost above 272K</p>
</div>
</div>
<div className="space-y-3 rounded-lg border bg-background/75 px-3 py-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
One cautious pair
</p>
<div className="rounded-full border bg-background px-2.5 py-1 font-mono text-[11px] font-medium">
Context {formatInteger(CCS_GPT_54_STARTER_CONTEXT_WINDOW)}
</div>
<div className="rounded-full border bg-background px-2.5 py-1 font-mono text-[11px] font-medium">
Auto-compact {formatInteger(CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT)}
</div>
</div>
<div className="flex flex-wrap items-center gap-1.5">
<Badge
variant="outline"
className="border-border/70 bg-background/80 text-[10px] uppercase tracking-[0.14em] text-muted-foreground"
>
Not official
</Badge>
<Badge
variant="outline"
className="border-border/70 bg-background/80 text-[10px] uppercase tracking-[0.14em] text-muted-foreground"
>
Draft only
</Badge>
</div>
</div>
<div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
<span>Quick-fill only. Review before saving.</span>
{!isGpt54Selected && draft.model ? (
<span>
<code>{draft.model}</code> should be checked separately.
</span>
) : null}
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1">
<p className="text-xs font-medium">Model context window</p>
<Input
aria-label="Model context window"
type="number"
min={1}
value={draft.modelContextWindow ?? ''}
onChange={(event) =>
setDraft((current) => ({
...current,
modelContextWindow: parseOptionalInteger(event.target.value),
}))
}
placeholder="Unset"
disabled={disabled}
/>
<p className="text-xs text-muted-foreground">
Writes <code>model_context_window</code>. Leave unset to keep Codex defaults.
</p>
</div>
<div className="space-y-1">
<p className="text-xs font-medium">Auto-compact token limit</p>
<Input
aria-label="Auto-compact token limit"
type="number"
min={1}
value={draft.modelAutoCompactTokenLimit ?? ''}
onChange={(event) =>
setDraft((current) => ({
...current,
modelAutoCompactTokenLimit: parseOptionalInteger(event.target.value),
}))
}
placeholder="Unset"
disabled={disabled}
/>
<p className="text-xs text-muted-foreground">
Writes <code>model_auto_compact_token_limit</code>. Leave unset to keep model
defaults.
</p>
</div>
</div>
<div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
<span className="text-[10px] uppercase tracking-[0.14em]">Docs</span>
<a
href="https://developers.openai.com/api/docs/models/gpt-5.4"
target="_blank"
rel="noreferrer"
className="underline underline-offset-2 hover:text-foreground"
>
GPT-5.4 model page
</a>
<a
href="https://openai.com/index/introducing-gpt-5-4/"
target="_blank"
rel="noreferrer"
className="underline underline-offset-2 hover:text-foreground"
>
Release notes
</a>
<a
href="https://developers.openai.com/codex/config-reference"
target="_blank"
rel="noreferrer"
className="underline underline-offset-2 hover:text-foreground"
>
Config reference
</a>
</div>
</div>
<div className="flex justify-end">
<Button onClick={() => onSave(patch)} disabled={disabled || saving || !hasChanges}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
+4
View File
@@ -1,6 +1,8 @@
export interface CodexTopLevelSettingsView {
model: string | null;
modelReasoningEffort: string | null;
modelContextWindow: number | null;
modelAutoCompactTokenLimit: number | null;
modelProvider: string | null;
approvalPolicy: string | null;
sandboxMode: string | null;
@@ -125,6 +127,8 @@ export function readCodexTopLevelSettings(
return {
model: asString(config?.model),
modelReasoningEffort: asString(config?.model_reasoning_effort),
modelContextWindow: asNumber(config?.model_context_window),
modelAutoCompactTokenLimit: asNumber(config?.model_auto_compact_token_limit),
modelProvider: asString(config?.model_provider),
approvalPolicy: asString(config?.approval_policy),
sandboxMode: asString(config?.sandbox_mode),
@@ -30,6 +30,8 @@ function buildDiagnostics(activeProfile: string | null): CodexDashboardDiagnosti
config: {
model: 'gpt-5.4',
modelReasoningEffort: null,
modelContextWindow: null,
modelAutoCompactTokenLimit: null,
modelProvider: 'openai',
activeProfile,
approvalPolicy: null,
@@ -11,6 +11,8 @@ describe('CodexTopLevelControlsCard', () => {
values={{
model: null,
modelReasoningEffort: null,
modelContextWindow: null,
modelAutoCompactTokenLimit: null,
modelProvider: null,
approvalPolicy: null,
sandboxMode: null,
@@ -34,4 +36,69 @@ describe('CodexTopLevelControlsCard', () => {
expect(onSave).toHaveBeenCalledTimes(1);
expect(onSave).toHaveBeenCalledWith({ model: 'gpt-5.4-mini' });
});
it('submits manual long-context overrides without auto-filling defaults', async () => {
const onSave = vi.fn();
render(
<CodexTopLevelControlsCard
values={{
model: 'gpt-5.4',
modelReasoningEffort: null,
modelContextWindow: null,
modelAutoCompactTokenLimit: null,
modelProvider: null,
approvalPolicy: null,
sandboxMode: null,
webSearch: null,
toolOutputTokenLimit: null,
personality: null,
}}
providerNames={[]}
onSave={onSave}
/>
);
await userEvent.type(screen.getByLabelText('Model context window'), '800000');
await userEvent.type(screen.getByLabelText('Auto-compact token limit'), '700000');
await userEvent.click(screen.getByRole('button', { name: 'Save top-level settings' }));
expect(onSave).toHaveBeenCalledWith({
modelContextWindow: 800000,
modelAutoCompactTokenLimit: 700000,
});
});
it('fills draft starter values without saving automatically', async () => {
const onSave = vi.fn();
render(
<CodexTopLevelControlsCard
values={{
model: 'gpt-5.4',
modelReasoningEffort: null,
modelContextWindow: null,
modelAutoCompactTokenLimit: null,
modelProvider: null,
approvalPolicy: null,
sandboxMode: null,
webSearch: null,
toolOutputTokenLimit: null,
personality: null,
}}
providerNames={[]}
onSave={onSave}
/>
);
expect(screen.getByText('Manual opt-in only')).toBeInTheDocument();
expect(screen.getByText('1.05M / 1M')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Fill cautious pair' }));
expect(onSave).not.toHaveBeenCalled();
expect(screen.getByLabelText('Model context window')).toHaveValue(800000);
expect(screen.getByLabelText('Auto-compact token limit')).toHaveValue(700000);
expect(screen.getByRole('button', { name: 'Save top-level settings' })).toBeEnabled();
});
});
+2
View File
@@ -37,6 +37,8 @@ const diagnosticsResponse = {
config: {
model: 'gpt-5.3-codex',
modelReasoningEffort: null,
modelContextWindow: null,
modelAutoCompactTokenLimit: null,
modelProvider: null,
activeProfile: null,
approvalPolicy: null,
@@ -79,6 +79,8 @@ const diagnostics = {
config: {
model: 'gpt-5.4',
modelReasoningEffort: null,
modelContextWindow: null,
modelAutoCompactTokenLimit: null,
modelProvider: null,
activeProfile: null,
approvalPolicy: null,