Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ All notable changes to this project will be documented in this file.

#### Bug Fixes

- **GPT-5.6 ChatGPT-subscription sessions use the same 272k input budget as GPT-5.5** —
matching the current Codex catalog, so history is compacted before the
unofficial backend rejects a turn.
- **A missing helper model falls back to DeepSeek V4 Flash** — disabling or
removing the helper no longer silently switches auxiliary tasks to the
first picker model.
Expand Down
43 changes: 16 additions & 27 deletions src/model/providerCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ export interface ProviderCapabilityKey {
readonly useOpenRouter: boolean;
}

/** Default Codex budget (GPT-5.5 and earlier). */
export const CODEX_DEFAULT_SUBSCRIPTION_CONTEXT_WINDOW = 400_000;
/**
* ChatGPT-subscription Codex input budget. Matches Codex CLI 0.145.0's
* `context_window` / `max_context_window` for GPT-5.5 and GPT-5.6 Sol /
* Terra / Luna. Displayed context is this plus the registry `maxOutputTokens`
* (128k → 400k), same split OpenCode uses.
*/
export const CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT = 272_000;

/** GPT-5.6 Codex budget: 372k input plus 128k output. */
export const CODEX_GPT56_SUBSCRIPTION_CONTEXT_WINDOW = 500_000;
export const CODEX_GPT56_SUBSCRIPTION_INPUT_LIMIT = 372_000;

/** Trailing llm-zoo date pin (`-2026-04-23`) on a model `fullName`. */
const CODEX_MODEL_DATE_PIN = /-\d{4}-\d{2}-\d{2}$/;

Expand All @@ -67,22 +67,6 @@ export function codexBackendModelId(config: {
return config.shortName || config.fullName.replace(CODEX_MODEL_DATE_PIN, '');
}

function codexSubscriptionTokenLimits(model: ModelConfig): {
contextWindow: number;
inputTokenLimit: number;
} {
if (codexBackendModelId(model).startsWith('gpt-5.6')) {
return {
contextWindow: CODEX_GPT56_SUBSCRIPTION_CONTEXT_WINDOW,
inputTokenLimit: CODEX_GPT56_SUBSCRIPTION_INPUT_LIMIT,
};
}
return {
contextWindow: CODEX_DEFAULT_SUBSCRIPTION_CONTEXT_WINDOW,
inputTokenLimit: CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT,
};
}

/**
* Whether `model` is eligible to route through the ChatGPT-subscription
* (Codex) backend.
Expand Down Expand Up @@ -122,14 +106,19 @@ export function resolveCodexSubscriptionProfile({
if (model.provider !== ModelProvider.OPENAI) return null;
if (model.openRouterOnly) return null;
if (!isCodexSubscriptionEligible(model)) return null;
const tokenLimits = codexSubscriptionTokenLimits(model);
const inputTokenLimit = Math.min(
CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT,
model.contextWindow,
);
const contextWindow = Math.min(
inputTokenLimit + model.maxOutputTokens,
Comment thread
LionSR marked this conversation as resolved.
model.contextWindow,
);

return {
authMode: 'chatgpt-subscription',
...zeroCostAccessOverrides(
Math.min(tokenLimits.contextWindow, model.contextWindow),
),
inputTokenLimit: Math.min(tokenLimits.inputTokenLimit, model.contextWindow),
...zeroCostAccessOverrides(contextWindow),
inputTokenLimit,
usageRoute: 'chatgpt-subscription',
openAIResponses: {
backgroundMode: 'disabled',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import type { ModelCredentialRoute } from '@agent/types/ModelHandlerContracts';
import { CODEX_BACKEND_BASE_URL, resetCodexCoordinator } from '@auth/codex';
import { setServerSideKeyService } from '@auth/serverKeys';
import { apiKeySecretName, invalidateApiKeyCache } from '@model/apiProviders';
import {
CODEX_DEFAULT_SUBSCRIPTION_CONTEXT_WINDOW,
CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT,
} from '@model/providerCapabilities';
import { CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT } from '@model/providerCapabilities';
import {
setPreferCodexSubscription,
isPreferCodexSubscription,
Expand Down Expand Up @@ -53,11 +50,16 @@ const config: ModelConfig = buildTestModelConfig({
// gpt-5.5 declares a 1,050,000-token window over the OpenAI API, but the Codex
// subscription backend enforces a far smaller ceiling — the override must clamp
// the effective window to that ceiling while the subscription drives requests.
// Use the real Codex output budget so the displayed window is 272k + 128k = 400k.
const largeWindowConfig: ModelConfig = {
...config,
contextWindow: 1_050_000,
maxOutputTokens: 128_000,
};

const LARGE_WINDOW_SUBSCRIPTION_CONTEXT =
CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT + largeWindowConfig.maxOutputTokens;

const ONE_MILLION_INPUT_TOKENS = {
input_tokens: 1_000_000,
output_tokens: 0,
Expand Down Expand Up @@ -144,14 +146,14 @@ describe('ModelHandlerCodex subscription fallback', () => {
// The model's own 1.05M API window must not leak through on the
// subscription path, where the backend rejects requests past the ceiling.
expect(handler.getEffectiveContextWindow()).toBe(
CODEX_DEFAULT_SUBSCRIPTION_CONTEXT_WINDOW,
LARGE_WINDOW_SUBSCRIPTION_CONTEXT,
);
});

it('restores the full model window after falling back to the API key', async () => {
const handler = await newSubscriptionHandler(largeWindowConfig);
expect(handler.getEffectiveContextWindow()).toBe(
CODEX_DEFAULT_SUBSCRIPTION_CONTEXT_WINDOW,
LARGE_WINDOW_SUBSCRIPTION_CONTEXT,
);

// "Use your own API key" routes to the real OpenAI API, which honors the
Expand Down Expand Up @@ -252,7 +254,7 @@ describe('ModelHandlerCodex subscription fallback', () => {

await setPreferCodexSubscription(false);
expect(handler.getEffectiveContextWindow()).toBe(
CODEX_DEFAULT_SUBSCRIPTION_CONTEXT_WINDOW,
LARGE_WINDOW_SUBSCRIPTION_CONTEXT,
);
expect(handler.computePrice(RAW_USAGE)).toBe(0);
expect(handler.getLastCredentialUsageRoute()).toBe('chatgpt-subscription');
Expand Down
6 changes: 3 additions & 3 deletions src/test-kernel/cli/StatusBar.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,9 +733,9 @@ describe('CLI StatusBar display model', () => {
}),
);

// gpt-5.6's Codex subscription budget caps to 500k
// (CODEX_GPT56_SUBSCRIPTION_CONTEXT_WINDOW), not the raw 1.05M API window.
expect(leftTexts(display)).toContain('187k/500k (37%)');
// gpt-5.6's Codex subscription budget caps to 400k (272k input plus its
// 128k output allowance), not the raw 1.05M API window.
expect(leftTexts(display)).toContain('187k/400k (47%)');
});

it('uses the default 400k subscription budget for earlier Codex models', () => {
Expand Down
8 changes: 6 additions & 2 deletions src/test-kernel/model/ComputeModelOptions.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
shouldRouteModelThroughOpenRouter,
} from '@model/openRouterRouting';
import {
CODEX_DEFAULT_SUBSCRIPTION_CONTEXT_WINDOW,
CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT,
isCodexSubscriptionEligible,
} from '@model/providerCapabilities';
import { apiKeySecretName, invalidateApiKeyCache } from '@model/apiProviders';
Expand Down Expand Up @@ -399,7 +399,11 @@ describe('computeModelOptionsData relay quota state', () => {

expect(model.availability).toBe('subscription-access');
expect(model.context).toBe(
`${Math.round(CODEX_DEFAULT_SUBSCRIPTION_CONTEXT_WINDOW / 1000)}K`,
`${Math.round(
(CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT +
MODEL_CONFIGS.gpt55.maxOutputTokens) /
1000,
)}K`,
);
expect(model.cost).toBe('$0.000/$0.000');
expect(model.hint).not.toContain(FAST_FIRST_RESPONSE_HINT);
Expand Down
42 changes: 19 additions & 23 deletions src/test-kernel/model/ProviderCapabilities.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ import {
import { installTexraModelAccess } from '@controllers/modelAccess/installTexraModelAccess';
import {
CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT,
CODEX_DEFAULT_SUBSCRIPTION_CONTEXT_WINDOW,
CODEX_GPT56_SUBSCRIPTION_INPUT_LIMIT,
CODEX_GPT56_SUBSCRIPTION_CONTEXT_WINDOW,
isCodexSubscriptionActive,
resolveCodexSubscriptionCapabilities,
resolveCodexSubscriptionProfile,
Expand All @@ -44,14 +41,6 @@ const gpt55Config: ModelConfig = {
codexSubscription: true,
};

const gpt56Config: ModelConfig = {
...gpt55Config,
name: 'gpt56--',
label: 'GPT-5.6 Luna',
fullName: 'gpt-5.6-luna',
shortName: 'gpt-5.6-luna',
};

const signedInSession: CodexSession = {
accessToken: 'access-token',
refreshToken: 'refresh-token',
Expand Down Expand Up @@ -88,7 +77,8 @@ describe('provider capabilities', () => {

expect(capabilities).toMatchObject({
authMode: 'chatgpt-subscription',
contextWindow: CODEX_DEFAULT_SUBSCRIPTION_CONTEXT_WINDOW,
contextWindow:
CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT + gpt55Config.maxOutputTokens,
inputTokenLimit: CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT,
inputPrice: 0,
outputPrice: 0,
Expand All @@ -112,17 +102,23 @@ describe('provider capabilities', () => {
});
});

it('uses the larger Codex input budget for GPT-5.6', () => {
const capabilities = resolveCodexSubscriptionProfile({
model: gpt56Config,
useOpenRouter: false,
});

expect(capabilities).toMatchObject({
contextWindow: CODEX_GPT56_SUBSCRIPTION_CONTEXT_WINDOW,
inputTokenLimit: CODEX_GPT56_SUBSCRIPTION_INPUT_LIMIT,
});
});
it.each(['gpt56', 'gpt56-', 'gpt56--'] as const)(
'caps ChatGPT-subscription %s to the Codex 272k input / 400k context budget',
(id) => {
const model = MODEL_CONFIGS[id];
const capabilities = resolveCodexSubscriptionProfile({
model,
useOpenRouter: false,
});

expect(model.codexSubscription).toBe(true);
expect(capabilities).toMatchObject({
contextWindow:
CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT + model.maxOutputTokens,
inputTokenLimit: CODEX_DEFAULT_SUBSCRIPTION_INPUT_LIMIT,
});
},
);
});

describe('ChatGPT subscription model routing', () => {
Expand Down
Loading