mirror of
https://github.com/proxysoul/Empryo.git
synced 2026-11-07 06:25:57 -10:00
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:
1 parent
c48c19e58c
commit
cbd130382d
6 files changed
+144
-23
No files matched your search
+53
-14
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
]);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user