Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Per-model ChatGPT auto-compaction budgets

## Problem

The catalog currently derives `auto_compact_token_limit` from one fixed rule: 90% of the
advertised context window, clamped by any measured max-input ceiling. That is a sound fallback,
but it cannot express that Sol, Terra, Luna, and Daybreak may need different working reserves.
Changing the hard context window is the wrong control: it also changes admission and model
capability reporting.

OpenCodex also cannot honestly choose a new threshold after inspecting each request. Codex reads
the catalog before the request, decides when to issue `/responses/compact`, and owns the history
that replaces the pre-compaction conversation. The proxy sees the ordinary request only after that
decision. Rewriting a shared catalog per request would race unrelated tasks, while silently issuing
a second model call would not replace the client's history and could duplicate or lose context.

## Contract

Add a provider setting:

```http
PATCH /api/providers?name=openai
Content-Type: application/json

{
"modelAutoCompactTokenLimits": {
"gpt-5.6-sol": 800000,
"gpt-5.6-terra": 810000,
"gpt-5.6-luna": 760000,
"gpt-daybreak-blue-latest": 700000
}
}
```

The same map may be stored alongside the canonical provider fields in
`providers.openai.modelAutoCompactTokenLimits`.

The map is model-specific and soft-policy only:

- each value may lower, never raise, the model's derived/default budget;
- hard `context_window`, `max_context_window`, and measured max-input admission stay authoritative;
- the provider-wide `providerContextCaps.openai` remains the final ceiling;
- bare, account-qualified, pinned/fallback, custom native-alias, and combo rows use the same value;
- a combo chooses the earliest effective budget among all possible targets;
- malformed, unsafe, unknown, or selector-qualified canonical OpenAI keys are rejected atomically.

The native family defaults to a 272,000-token advertised window, so its default effective budget is
244,800 tokens (90%). When the existing native-window control opts a model into the measured
922,000-token ceiling, its derived budget becomes 829,800 tokens. All four models can then be tuned
independently below their own effective window; a soft budget never opts a model into a wider hard
window and we do not invent different product defaults without measurements.

## Future adaptive profiles

True task-aware compaction belongs behind an explicit Codex-client capability. A future client can
select a deterministic profile (for example balanced, tool/output-heavy, or multi-agent), calculate
a request/session budget, and clamp it below this model-specific ceiling. Structural signals such as
tool-schema size, images, output reservation, and collaboration mode are reproducible; asking the
model to judge its own threshold is not suitable for enforcement because it adds a prompt-injectable,
nondeterministic call exactly when context is scarce.

Until such a capability exists, this per-model setting is the safe static authority and fallback.
14 changes: 13 additions & 1 deletion src/codex/catalog/aggregation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, mo
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
import { getProviderRegistryEntry } from "../../providers/registry";
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
import { applyProviderContextCap, clampAutoCompactTokenLimit, providerContextCap } from "../../providers/context-cap";
import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
Expand Down Expand Up @@ -156,6 +156,15 @@ export function deriveComboCatalogModel(
const maxInputTokens = Math.min(
...members.map(member => member.maxInputTokens ?? member.contextWindow!),
);
// A failover combo must compact early enough for every possible target. Missing
// per-model metadata means "use that member's normal derived budget", not "ignore it".
const autoCompactTokenLimit = Math.min(
...members.map(member => clampAutoCompactTokenLimit(
member.contextWindow!,
member.maxInputTokens,
member.autoCompactTokenLimit,
)),
);
const defaultReasoningEffort = effectiveComboDefault(
combo.defaultEffort,
reasoningEfforts,
Expand All @@ -167,6 +176,7 @@ export function deriveComboCatalogModel(
owned_by: COMBO_NAMESPACE,
contextWindow,
maxInputTokens,
autoCompactTokenLimit,
...(hasLimitingContextCapMetadata ? { contextCapped } : {}),
inputModalities,
reasoningEfforts,
Expand Down Expand Up @@ -207,6 +217,7 @@ export function comboCatalogWarningSignature(
key,
contextWindow: member?.contextWindow ?? null,
maxInputTokens: member?.maxInputTokens ?? null,
autoCompactTokenLimit: member?.autoCompactTokenLimit ?? null,
inputModalities: [...new Set(member?.inputModalities ?? [])].sort(),
reasoningEfforts: [...new Set(member?.reasoningEfforts ?? [])].sort(),
parallelToolCalls: member?.parallelToolCalls === true,
Expand Down Expand Up @@ -296,6 +307,7 @@ export function normalizedOpenAiApiSignature(model: CatalogModel): string {
id: model.id,
contextWindow: model.contextWindow ?? null,
maxInputTokens: model.maxInputTokens ?? null,
autoCompactTokenLimit: model.autoCompactTokenLimit ?? null,
inputModalities: [...new Set(model.inputModalities ?? [])].sort(),
reasoningEfforts: [...new Set(model.reasoningEfforts ?? [])].sort(),
ownedBy: model.owned_by ?? null,
Expand Down
20 changes: 16 additions & 4 deletions src/codex/catalog/effort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, mo
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
import { getProviderRegistryEntry } from "../../providers/registry";
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
import { applyProviderContextCap, clampAutoCompactTokenLimit, providerContextCap } from "../../providers/context-cap";
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
Expand Down Expand Up @@ -125,9 +125,21 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
if (typeof model.contextWindow === "number" && model.contextWindow > 0) {
entry.context_window = model.contextWindow;
entry.max_context_window = model.contextWindow;
entry.auto_compact_token_limit = Math.min(
Math.floor(model.contextWindow * 0.9),
model.maxInputTokens ?? Number.POSITIVE_INFINITY,
}
const effectiveContextWindow = typeof model.contextWindow === "number" && model.contextWindow > 0
? model.contextWindow
: typeof entry.context_window === "number" && entry.context_window > 0
? entry.context_window
: 128_000;
if (
(typeof model.contextWindow === "number" && model.contextWindow > 0)
|| (typeof model.autoCompactTokenLimit === "number" && model.autoCompactTokenLimit > 0)
|| (typeof model.maxInputTokens === "number" && model.maxInputTokens > 0)
) {
entry.auto_compact_token_limit = clampAutoCompactTokenLimit(
effectiveContextWindow,
model.maxInputTokens,
model.autoCompactTokenLimit,
);
}
if (Array.isArray(model.inputModalities) && model.inputModalities.length > 0) {
Expand Down
50 changes: 47 additions & 3 deletions src/codex/catalog/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, mo
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
import { getProviderRegistryEntry } from "../../providers/registry";
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
import { applyProviderContextCap, clampAutoCompactTokenLimit, providerContextCap } from "../../providers/context-cap";
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
import { identifyRoutedModel } from "../../adapters/identity";
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
Expand Down Expand Up @@ -144,10 +144,22 @@ const NATIVE_GPT56_FAMILY = new Set<string>([
NATIVE_DAYBREAK_BLUE_MODEL,
]);

export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: number; maxContextWindow?: number; maxInputTokens?: number }> = {
export interface NativeOpenAiContextOverride {
contextWindow?: number;
maxContextWindow?: number;
/** Measured hard input ceiling. This is admission authority, not compaction policy. */
maxInputTokens?: number;
/** Model-specific soft budget exposed to Codex as auto_compact_token_limit. */
autoCompactTokenLimit?: number;
}

export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, NativeOpenAiContextOverride> = {
"gpt-5.5": { contextWindow: 272_000, maxContextWindow: 272_000 },
"gpt-5.4": { contextWindow: 1_000_000, maxContextWindow: 1_000_000 },
"gpt-5.3-codex-spark": { contextWindow: 100_000, maxContextWindow: 100_000 },
// The shared clamp derives 90% of the effective 272k default (or 922k opt-in) for every
// GPT-5.6 member. Per-model soft budgets may lower that value; no ineffective Sol-only
// upper bound is stored here.
"gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_MAX_INPUT_TOKENS, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
"gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_MAX_INPUT_TOKENS, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
"gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_MAX_INPUT_TOKENS, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
Expand Down Expand Up @@ -274,6 +286,30 @@ export function nativeOpenAiMaxInputTokens(slug: string, limits?: NativeContextL
return window === undefined ? narrowed : Math.min(narrowed, window);
}

/** Effective native soft budget. User configuration may lower, but never raise, the model default. */
export function nativeOpenAiAutoCompactTokenLimit(
slug: string,
limits?: NativeContextLimitsInput,
configuredLimit?: number,
): number | undefined {
const contextWindow = nativeOpenAiContextWindow(slug, limits);
if (contextWindow === undefined) return undefined;
const nativeDefault = NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.autoCompactTokenLimit;
const positiveConfigured = typeof configuredLimit === "number" && configuredLimit > 0
? configuredLimit
: undefined;
const softLimit = nativeDefault === undefined
? positiveConfigured
: positiveConfigured === undefined
? nativeDefault
: Math.min(nativeDefault, positiveConfigured);
return clampAutoCompactTokenLimit(
contextWindow,
nativeOpenAiMaxInputTokens(slug, limits),
softLimit,
);
}

export function nativeInputModalities(slug: string): string[] {
const upstream = PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug);
if (Array.isArray(upstream?.input_modalities) && upstream!.input_modalities!.length > 0) {
Expand Down Expand Up @@ -384,7 +420,9 @@ export function desktopVisibleNativeSlugs(
]);
}

export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps" | "providers">): Array<{ slug: string; disabled: boolean; contextWindow?: number; maxInputTokens?: number }> {
export function nativeModelRows(
config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps" | "providers">,
): Array<{ slug: string; disabled: boolean; contextWindow?: number; maxInputTokens?: number; autoCompactTokenLimit?: number }> {
const disabled = disabledNativeSlugs(config);
const shadowed = configuredNativeAliasSlugs(config);
// Both user levers, not just the cap: a per-model window set from the dashboard has to show
Expand All @@ -393,11 +431,17 @@ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "comb
return NATIVE_OPENAI_MODELS.filter(slug => !shadowed.has(slug)).map(slug => {
const contextWindow = nativeOpenAiContextWindow(slug, limits);
const maxInputTokens = nativeOpenAiMaxInputTokens(slug, limits);
const autoCompactTokenLimit = nativeOpenAiAutoCompactTokenLimit(
slug,
limits,
config.providers?.[OPENAI_CODEX_PROVIDER_ID]?.modelAutoCompactTokenLimits?.[slug],
);
return {
slug,
disabled: disabled.has(slug),
...(contextWindow !== undefined ? { contextWindow } : {}),
...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
};
});
}
Expand Down
46 changes: 37 additions & 9 deletions src/codex/catalog/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, mo
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
import { getProviderRegistryEntry } from "../../providers/registry";
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
import { applyProviderContextCap, clampAutoCompactTokenLimit, providerContextCap } from "../../providers/context-cap";
import { encodeRoutedModelId, routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
Expand Down Expand Up @@ -111,6 +111,8 @@ export interface CatalogModel {
defaultReasoningEffort?: string;
contextWindow?: number;
maxInputTokens?: number;
/** Soft compaction budget; always clamped below the model's hard context and input ceilings. */
autoCompactTokenLimit?: number;
contextCap?: number;
contextCapped?: boolean;
inputModalities?: string[];
Expand Down Expand Up @@ -275,11 +277,21 @@ export function isNativeOpenAiEntry(entry: RawEntry): boolean {
* longer trips this (922,000 window, 829,800 at 90%), but the routed and API-key rows carry
* the same family at a 1,050,000 window where 90% would be 945,000 — past the ceiling.
*/
function nativeAutoCompactLimit(contextWindow: number, maxInputTokens: number | undefined, contextCap?: number): number {
const ninety = Math.floor(contextWindow * 0.9);
if (typeof maxInputTokens !== "number" || maxInputTokens <= 0) return ninety;
const cappedMaxInput = applyProviderContextCap(maxInputTokens, contextCap) ?? maxInputTokens;
return Math.min(ninety, cappedMaxInput, contextWindow);
function nativeAutoCompactLimit(
contextWindow: number,
maxInputTokens: number | undefined,
nativeDefault: number | undefined,
configuredLimit: number | undefined,
): number {
const positiveConfigured = typeof configuredLimit === "number" && configuredLimit > 0
? configuredLimit
: undefined;
const softLimit = nativeDefault === undefined
? positiveConfigured
: positiveConfigured === undefined
? nativeDefault
: Math.min(nativeDefault, positiveConfigured);
return clampAutoCompactTokenLimit(contextWindow, maxInputTokens, softLimit);
}

/**
Expand All @@ -302,7 +314,11 @@ function narrowNativeMaxContextWindow(
return Math.min(value, Math.max(resolved, 1));
}

export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: NativeContextLimitsInput): void {
export function applyNativeOpenAiContextOverride(
entry: RawEntry,
limits?: NativeContextLimitsInput,
modelAutoCompactTokenLimits?: Readonly<Record<string, number>>,
): void {
const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry)
?? (isNativeOpenAiEntry(entry) ? entry.slug as string : undefined);
if (!nativeSlug) return;
Expand All @@ -317,7 +333,8 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ
entry.auto_compact_token_limit = nativeAutoCompactLimit(
contextWindow,
nativeOpenAiMaxInputTokens(nativeSlug, limits) ?? override.maxInputTokens,
undefined,
override.autoCompactTokenLimit,
modelAutoCompactTokenLimits?.[nativeSlug],
);
}
if (typeof override.maxContextWindow === "number") {
Expand All @@ -336,7 +353,18 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ
entry.auto_compact_token_limit = nativeAutoCompactLimit(
cappedContext,
nativeOpenAiMaxInputTokens(nativeSlug, limits) ?? override?.maxInputTokens,
undefined,
override?.autoCompactTokenLimit,
modelAutoCompactTokenLimits?.[nativeSlug],
);
}
const configuredAutoCompact = modelAutoCompactTokenLimits?.[nativeSlug];
const effectiveContext = typeof entry.context_window === "number" ? entry.context_window : undefined;
if (typeof configuredAutoCompact === "number" && configuredAutoCompact > 0 && effectiveContext !== undefined) {
entry.auto_compact_token_limit = nativeAutoCompactLimit(
effectiveContext,
nativeOpenAiMaxInputTokens(nativeSlug, limits) ?? override?.maxInputTokens,
override?.autoCompactTokenLimit,
configuredAutoCompact,
);
}
const currentMax = typeof entry.max_context_window === "number" ? entry.max_context_window : undefined;
Expand Down
Loading
Loading