fix(providers): pin gpt-5.5 context to 272k for codex/proxy

- Add gpt-5.5 (272k) to codex + proxy contextWindows and fallbackModels
- Add contextWindowOverrides to force-correct stale upstream metadata (e.g. 1M)
- getModelContextInfoSync: apply overrides across all providers when model ID has no provider prefix; tag result source: "override"
- useChat: treat overrides/API as authoritative, only pin "fallback" values; gate auto-compaction on pinned window
- Normalize fetched + proxy grouped models through applyProviderContextOverride
- Tests: bare/prefixed match, no gpt-5.5-pro false match, codex/proxy normalization

Co-authored-by: SoulForge <[email protected]>
This commit is contained in:
KarolisandSoulForge authored and GitHub committed 2026-05-30 21:49:49 +02:00
1 parent c48c19e58c
commit cbd130382d
6 files changed
+144 -23

No files matched your search

+53 -14
View File
@@ -58,13 +58,46 @@ onProvidersChanged(() => {
const DEFAULT_CONTEXT_TOKENS = 128_000;
const METADATA_FETCH_TIMEOUT = 5_000;
type ContextWindowSource = "api" | "openrouter" | "fallback";
type ContextWindowSource = "api" | "openrouter" | "fallback" | "override";
interface ContextWindowResult {
tokens: number;
source: ContextWindowSource;
}
export function matchesContextOverride(model: string, pattern: string): boolean {
const lowerModel = model.toLowerCase();
const lowerPattern = pattern.toLowerCase();
const bareModel = lowerModel.split("/").pop() ?? lowerModel;
const barePattern = lowerPattern.split("/").pop() ?? lowerPattern;
return (
bareModel === barePattern ||
lowerModel === lowerPattern ||
lowerModel.endsWith(`/${lowerPattern}`)
);
}
function findContextOverride(
provider: ReturnType<typeof getProvider>,
model: string,
): number | undefined {
if (!provider?.contextWindowOverrides) return undefined;
for (const [pattern, tokens] of provider.contextWindowOverrides) {
if (matchesContextOverride(model, pattern)) return tokens;
}
return undefined;
}
export function applyProviderContextOverride(
providerId: string,
model: ProviderModelInfo,
): ProviderModelInfo {
const tokens = findContextOverride(getProvider(providerId), model.id);
return tokens ? { ...model, contextWindow: tokens } : model;
}
/**
* Synchronous cache-only lookup — returns best-known value from populated caches.
* Used internally and as fallback when async fetch times out.
@@ -88,11 +121,12 @@ export function getModelContextInfoSync(modelId: string): ContextWindowResult {
// 0. Provider-specific overrides for known-incorrect upstream API values
// (e.g. OpenRouter lists GLM-5 as 80k, actual ~200k)
const ownProvider = providerId ? getProvider(providerId) : null;
if (ownProvider?.contextWindowOverrides) {
for (const [pattern, tokens] of ownProvider.contextWindowOverrides) {
if (model.includes(pattern)) return { tokens, source: "fallback" };
}
}
const overrideTokens = providerId
? findContextOverride(ownProvider ?? undefined, model)
: getAllProviders()
.map((provider) => findContextOverride(provider, model))
.find((tokens) => tokens !== undefined);
if (overrideTokens) return { tokens: overrideTokens, source: "override" };
// 1. Provider's own API data (most accurate)
if (providerId && !ownProvider?.grouped) {
@@ -407,10 +441,13 @@ export async function fetchProviderModels(
try {
const models = await provider.fetchModels();
if (models) {
modelCache.set(providerId, { models, ts: Date.now() });
return { models };
const normalized = models.map((m) => applyProviderContextOverride(providerId, m));
modelCache.set(providerId, { models: normalized, ts: Date.now() });
return { models: normalized };
}
return { models: provider.fallbackModels };
return {
models: provider.fallbackModels.map((m) => applyProviderContextOverride(providerId, m)),
};
} catch (err) {
const msg = toErrorMessage(err);
return { models: [], error: `API error: ${msg}` };
@@ -690,11 +727,13 @@ async function fetchProxyGrouped(): Promise<GroupedModelsResult> {
const ctxWindow =
m.context_length ?? anthropicCtx.get(m.id) ?? findOpenRouterModel(m.id)?.context_length;
grouped[group].push({
id: m.id,
name: m.id,
contextWindow: ctxWindow,
});
grouped[group].push(
applyProviderContextOverride("proxy", {
id: m.id,
name: m.id,
contextWindow: ctxWindow,
}),
);
}
const subProviders: SubProvider[] = Object.keys(grouped)
+9
View File
@@ -14,16 +14,24 @@ import {
serializeCodexPrompt,
} from "./runner.js";
const GPT_55_INPUT_CONTEXT = 272_000;
export const CODEX_FALLBACK_MODELS: ProviderModelInfo[] = [
{ id: "gpt-5.5", name: "GPT-5.5", contextWindow: GPT_55_INPUT_CONTEXT },
{ id: "gpt-5.4", name: "GPT-5.4", contextWindow: 1_050_000 },
{ id: "gpt-5.2-codex", name: "GPT-5.2-Codex", contextWindow: 400_000 },
];
export const CODEX_CONTEXT_WINDOWS: [pattern: string, tokens: number][] = [
["gpt-5.5", GPT_55_INPUT_CONTEXT],
["gpt-5.4", 1_050_000],
["gpt-5.2-codex", 400_000],
];
const CODEX_CONTEXT_WINDOW_OVERRIDES: [pattern: string, tokens: number][] = [
["gpt-5.5", GPT_55_INPUT_CONTEXT],
];
export const codex: ProviderDefinition = {
id: "codex",
name: "Codex",
@@ -51,6 +59,7 @@ export const codex: ProviderDefinition = {
},
fallbackModels: CODEX_FALLBACK_MODELS,
contextWindows: CODEX_CONTEXT_WINDOWS,
contextWindowOverrides: CODEX_CONTEXT_WINDOW_OVERRIDES,
checkAvailability: async () => {
const status = getCodexLoginStatus();
return status.installed && status.loggedIn;
+5
View File
@@ -9,6 +9,7 @@ import { createReasoningFetchWrapper } from "./reasoning-fetch.js";
import type { ProviderDefinition, ProviderModelInfo } from "./types.js";
const baseURL = process.env.PROXY_API_URL || "http://127.0.0.1:8317/v1";
const GPT_55_INPUT_CONTEXT = 272_000;
function isAnthropicModel(modelId: string): boolean {
return modelId.toLowerCase().startsWith("claude");
@@ -75,8 +76,11 @@ export const proxy: ProviderDefinition = {
{ id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
{ id: "claude-opus-4-20250514", name: "Claude Opus 4" },
{ id: "claude-haiku-3-5-20241022", name: "Claude Haiku 3.5" },
{ id: "gpt-5.5", name: "GPT-5.5", contextWindow: GPT_55_INPUT_CONTEXT },
],
contextWindowOverrides: [["gpt-5.5", GPT_55_INPUT_CONTEXT]],
// Specific overrides first → shared patterns → generic catch-alls last.
contextWindows: [
// Claude (both dot/hyphen styles)
@@ -102,6 +106,7 @@ export const proxy: ProviderDefinition = {
["claude-3.5-haiku", 200_000],
["claude-3-5-haiku", 200_000],
// GPT
["gpt-5.5", GPT_55_INPUT_CONTEXT],
["gpt-5-chat", 128_000],
["gpt-4.1", 1_048_576],
// Grok
+10 -9
View File
@@ -510,22 +510,23 @@ export function useChat({
}, [coAuthorCommits]);
// Sync context window size to contextManager + status bar store.
// Pin per model — never downgrade if API cache expires (prevents 1M→200k drop).
// Pin per model, but allow provider overrides/API metadata to replace stale values.
const contextManagerRef = useRef(contextManager);
contextManagerRef.current = contextManager;
const pinnedContextWindow = useRef(new Map<string, number>());
// Context window: show sync fallback immediately, then correct from async API data.
// The sync fallback comes from hardcoded patterns (per-provider, never cross-provider).
// The async fetch gets the real value from the provider API or OpenRouter metadata.
// The async fetch gets the real value from provider overrides, provider API, or OpenRouter metadata.
const prevSyncedModel = useRef("");
if (activeModel !== prevSyncedModel.current && activeModel !== "none") {
prevSyncedModel.current = activeModel;
const { tokens: sync, source } = getModelContextInfoSync(activeModel);
const cached = pinnedContextWindow.current.get(activeModel);
const sync = cached || getModelContextInfoSync(activeModel).tokens;
pinnedContextWindow.current.set(activeModel, sync);
contextManagerRef.current.setContextWindow(sync);
if (visible) useStatusBarStore.getState().setContextWindow(sync);
const next = source === "fallback" ? (cached ?? sync) : sync;
pinnedContextWindow.current.set(activeModel, next);
contextManagerRef.current.setContextWindow(next);
if (visible) useStatusBarStore.getState().setContextWindow(next);
}
// Async fetch — resolves the authoritative context window from provider API.
@@ -537,7 +538,7 @@ export function useChat({
getModelContextInfo(activeModelForEffect).then(({ tokens: accurate, source }) => {
if (cancelled) return;
const prev = pinnedContextWindow.current.get(activeModelForEffect) ?? 0;
// API/OpenRouter data is authoritative — replace even if lower than fallback estimate.
// Provider overrides/API/OpenRouter data are authoritative — replace even if lower than fallback estimate.
// Fallback data only upgrades (never downgrades) since it's a guess.
const best = source !== "fallback" ? accurate : Math.max(prev, accurate);
if (best !== prev) {
@@ -1346,8 +1347,8 @@ export function useChat({
if (effectiveConfig.compaction?.strategy === "disabled") return;
if (activeModelRef.current === "none") return;
if (contextTokens <= 0) return;
// Only use pinned value (set by async fetch). If async hasn't resolved yet,
// skip this check — better to delay compaction than trigger on a wrong fallback.
// Only use pinned value (set by sync override/fallback or async metadata).
// If no pinned value exists yet, delay compaction rather than guess.
const ctxWindow = pinnedContextWindow.current.get(activeModelRef.current);
if (!ctxWindow) return;
const pct = contextTokens / ctxWindow;
+2
View File
@@ -37,10 +37,12 @@ describe("codex provider", () => {
expect(codex.badge).toBe("non-streaming");
expect(codex.onRequestAuth).toBeDefined();
expect(codex.fallbackModels).toEqual([
{ id: "gpt-5.5", name: "GPT-5.5", contextWindow: 272_000 },
{ id: "gpt-5.4", name: "GPT-5.4", contextWindow: 1_050_000 },
{ id: "gpt-5.2-codex", name: "GPT-5.2-Codex", contextWindow: 400_000 },
]);
expect(codex.contextWindows).toEqual([
["gpt-5.5", 272_000],
["gpt-5.4", 1_050_000],
["gpt-5.2-codex", 400_000],
]);
+65
View File
@@ -0,0 +1,65 @@
import { describe, expect, test } from "bun:test";
import {
applyProviderContextOverride,
getModelContextInfoSync,
matchesContextOverride,
} from "../src/core/llm/models.js";
import { codex } from "../src/core/llm/providers/codex/index.js";
const GPT_55_INPUT_CONTEXT = 272_000;
describe("provider model context normalization", () => {
test("matches exact and provider-prefixed override IDs", () => {
expect(matchesContextOverride("gpt-5.5", "gpt-5.5")).toBe(true);
expect(matchesContextOverride("openai/gpt-5.5", "gpt-5.5")).toBe(true);
expect(matchesContextOverride("gpt-5.5-pro", "gpt-5.5")).toBe(false);
});
test("resolves bare gpt-5.5 sync context as an override", () => {
expect(getModelContextInfoSync("gpt-5.5")).toEqual({
tokens: GPT_55_INPUT_CONTEXT,
source: "override",
});
});
test("normalizes proxy gpt-5.5 context from upstream metadata", () => {
expect(
applyProviderContextOverride("proxy", {
id: "gpt-5.5",
name: "gpt-5.5",
contextWindow: 1_000_000,
}).contextWindow,
).toBe(GPT_55_INPUT_CONTEXT);
expect(
applyProviderContextOverride("proxy", {
id: "openai/gpt-5.5",
name: "openai/gpt-5.5",
contextWindow: 1_000_000,
}).contextWindow,
).toBe(GPT_55_INPUT_CONTEXT);
});
test("does not apply gpt-5.5 override to gpt-5.5-pro", () => {
expect(
applyProviderContextOverride("proxy", {
id: "gpt-5.5-pro",
name: "gpt-5.5-pro",
contextWindow: 1_000_000,
}).contextWindow,
).toBe(1_000_000);
});
test("normalizes codex gpt-5.5 when fetched model omits context", () => {
expect(
applyProviderContextOverride("codex", {
id: "gpt-5.5",
name: "gpt-5.5",
}).contextWindow,
).toBe(GPT_55_INPUT_CONTEXT);
expect(codex.fallbackModels.find((m) => m.id === "gpt-5.5")?.contextWindow).toBe(
GPT_55_INPUT_CONTEXT,
);
});
});