mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-30 17:11:12 +08:00
Cap imported Codex GPT-5 context windows
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
readCatalogCapability,
|
||||
type ModelCatalogEntry
|
||||
} from "@ccr/core/gateway/model-catalog";
|
||||
import { codexDefaultBaseUrl, readCodexLocalModelCatalog } from "@ccr/core/agents/local-providers/codex";
|
||||
import { codexDefaultBaseUrl, codexImportedModelContextWindow, readCodexLocalModelCatalog } from "@ccr/core/agents/local-providers/codex";
|
||||
import { localAgentProviderApiKey } from "@ccr/core/agents/local-providers/shared";
|
||||
import { normalizeProviderBaseUrl } from "@ccr/core/providers/url";
|
||||
import { resolveUsageModelAttribution } from "@ccr/core/usage/model-attribution";
|
||||
@@ -211,7 +211,7 @@ function codexModelCapabilityProfile(
|
||||
: findConfiguredProviderForModel(config, attribution.model ?? model);
|
||||
const providerModel = attribution.model ?? selector?.model ?? model;
|
||||
const providerModelMetadata = provider
|
||||
? providerModelMetadataFor(provider, providerModel) ?? localCodexModelMetadataFor(provider, providerModel)
|
||||
? codexProviderModelMetadataFor(provider, providerModel)
|
||||
: undefined;
|
||||
const physicalModelSelector = provider ? `${provider.name}/${providerModel}` : providerModel;
|
||||
const catalogEntry = findModelCatalogEntry(physicalModelSelector);
|
||||
@@ -281,6 +281,22 @@ function providerModelMetadataFor(provider: GatewayProviderConfig, model: string
|
||||
return match?.[1];
|
||||
}
|
||||
|
||||
function codexProviderModelMetadataFor(provider: GatewayProviderConfig, model: string): ProviderModelMetadata | undefined {
|
||||
const metadata = providerModelMetadataFor(provider, model) ?? localCodexModelMetadataFor(provider, model);
|
||||
if (!isLocalCodexProvider(provider)) {
|
||||
return metadata;
|
||||
}
|
||||
const contextWindow = codexImportedModelContextWindow(model);
|
||||
if (!contextWindow) {
|
||||
return metadata;
|
||||
}
|
||||
return {
|
||||
...(metadata ?? {}),
|
||||
contextWindow,
|
||||
maxContextWindow: contextWindow
|
||||
};
|
||||
}
|
||||
|
||||
function localCodexModelMetadataFor(provider: GatewayProviderConfig, model: string): ProviderModelMetadata | undefined {
|
||||
if (!isLocalCodexProvider(provider)) {
|
||||
return undefined;
|
||||
|
||||
@@ -37,6 +37,8 @@ export const codexDefaultBaseUrl = "https://chatgpt.com/backend-api/codex";
|
||||
|
||||
const codexAccountBaseUrl = "https://chatgpt.com/backend-api";
|
||||
const codexDefaultModels = ["gpt-5-codex"];
|
||||
const codexImportedGpt56ContextWindow = 368_000;
|
||||
const codexImportedGpt5ContextWindow = 256_000;
|
||||
const codexProbeTimeoutMs = 8_000;
|
||||
|
||||
export type LocalAgentModelCatalog = {
|
||||
@@ -382,19 +384,30 @@ export function codexRateLimitResetCreditDetails(payload: unknown): ProviderAcco
|
||||
}
|
||||
|
||||
export function normalizeCodexProviderAccountConfig(provider: GatewayProviderConfig): GatewayProviderConfig {
|
||||
if (!isLocalCodexProvider(provider) || !shouldUseCurrentCodexAccountConfig(provider.account)) {
|
||||
return provider;
|
||||
const normalizedProvider = normalizeCodexImportedProviderModelMetadata(provider);
|
||||
if (!isLocalCodexProvider(normalizedProvider) || !shouldUseCurrentCodexAccountConfig(normalizedProvider.account)) {
|
||||
return normalizedProvider;
|
||||
}
|
||||
const account = codexProviderAccountConfig();
|
||||
return {
|
||||
...provider,
|
||||
...normalizedProvider,
|
||||
account: {
|
||||
...account,
|
||||
refreshIntervalMs: provider.account?.refreshIntervalMs ?? account.refreshIntervalMs
|
||||
refreshIntervalMs: normalizedProvider.account?.refreshIntervalMs ?? account.refreshIntervalMs
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCodexImportedProviderModelMetadata(provider: GatewayProviderConfig): GatewayProviderConfig {
|
||||
if (!isLocalCodexProvider(provider)) {
|
||||
return provider;
|
||||
}
|
||||
const modelMetadata = codexImportedModelMetadataWithContextOverrides(provider.modelMetadata, provider.models ?? []);
|
||||
return modelMetadata === provider.modelMetadata
|
||||
? provider
|
||||
: { ...provider, modelMetadata };
|
||||
}
|
||||
|
||||
function isLocalCodexProvider(provider: GatewayProviderConfig): boolean {
|
||||
return (
|
||||
providerApiKey(provider) === localAgentProviderApiKey &&
|
||||
@@ -612,7 +625,7 @@ export function readCodexLocalModelCatalog(): LocalAgentModelCatalog {
|
||||
const uniqueModels = uniqueStrings([...catalog.models, ...codexDefaultModels]);
|
||||
return {
|
||||
modelDisplayNames: modelDisplayNamesForModels(catalog.modelDisplayNames, uniqueModels),
|
||||
modelMetadata: modelMetadataForModels(catalog.modelMetadata, uniqueModels),
|
||||
modelMetadata: codexImportedModelMetadataForModels(catalog.modelMetadata, uniqueModels),
|
||||
models: uniqueModels
|
||||
};
|
||||
}
|
||||
@@ -718,11 +731,50 @@ function codexModelCatalogFromPayload(payload: unknown): LocalAgentModelCatalog
|
||||
const uniqueModels = uniqueStrings(models);
|
||||
return {
|
||||
modelDisplayNames: modelDisplayNamesForModels(modelDisplayNames, uniqueModels),
|
||||
modelMetadata: modelMetadataForModels(modelMetadata, uniqueModels),
|
||||
modelMetadata: codexImportedModelMetadataForModels(modelMetadata, uniqueModels),
|
||||
models: uniqueModels
|
||||
};
|
||||
}
|
||||
|
||||
function codexImportedModelMetadataForModels(
|
||||
value: Record<string, ProviderModelMetadata> | undefined,
|
||||
models: string[]
|
||||
): Record<string, ProviderModelMetadata> | undefined {
|
||||
return modelMetadataForModels(codexImportedModelMetadataWithContextOverrides(value, models), models);
|
||||
}
|
||||
|
||||
function codexImportedModelMetadataWithContextOverrides(
|
||||
value: Record<string, ProviderModelMetadata> | undefined,
|
||||
models: string[]
|
||||
): Record<string, ProviderModelMetadata> | undefined {
|
||||
const metadata: Record<string, ProviderModelMetadata> = { ...(value ?? {}) };
|
||||
let changed = false;
|
||||
for (const model of models) {
|
||||
const contextWindow = codexImportedModelContextWindow(model);
|
||||
if (!contextWindow) {
|
||||
continue;
|
||||
}
|
||||
metadata[model] = {
|
||||
...(metadata[model] ?? {}),
|
||||
contextWindow,
|
||||
maxContextWindow: contextWindow
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
return changed ? metadata : value;
|
||||
}
|
||||
|
||||
export function codexImportedModelContextWindow(model: string): number | undefined {
|
||||
const name = model.trim().toLowerCase().split("/").at(-1) ?? "";
|
||||
if (/^gpt-5\.6(?:[.-]|$)/.test(name)) {
|
||||
return codexImportedGpt56ContextWindow;
|
||||
}
|
||||
if (/^gpt-5(?:[.-]|$)/.test(name)) {
|
||||
return codexImportedGpt5ContextWindow;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function codexModelMetadataFromItem(item: Record<string, unknown>): ProviderModelMetadata | undefined {
|
||||
const additionalSpeedTiers = readArray(item.additional_speed_tiers) ?? readArray(item.additionalSpeedTiers) ?? readArray(item.speed_tiers) ?? readArray(item.speedTiers);
|
||||
const contextWindow = readPositiveInteger(item.context_window ?? item.contextWindow);
|
||||
|
||||
@@ -314,6 +314,37 @@ test("codex catalog uses provider model metadata for reasoning effort and speed
|
||||
assert.equal(model.supports_reasoning_summaries, true);
|
||||
});
|
||||
|
||||
test("codex catalog caps persisted local Codex GPT-5 context metadata", () => {
|
||||
const cases = [
|
||||
["gpt-5-codex", 256000],
|
||||
["gpt-5.5", 256000],
|
||||
["gpt-5.6-sol", 368000]
|
||||
];
|
||||
|
||||
for (const [modelName, expectedContextWindow] of cases) {
|
||||
const model = catalogModelFor({
|
||||
Providers: [
|
||||
{
|
||||
api_base_url: "https://chatgpt.com/backend-api/codex",
|
||||
api_key: "ccr-local-agent-login",
|
||||
modelMetadata: {
|
||||
[modelName]: {
|
||||
contextWindow: 272000,
|
||||
maxContextWindow: 300000
|
||||
}
|
||||
},
|
||||
models: [modelName],
|
||||
name: "Codex API",
|
||||
type: "openai_responses"
|
||||
}
|
||||
]
|
||||
}, `Codex API/${modelName}`);
|
||||
|
||||
assert.equal(model.context_window, expectedContextWindow);
|
||||
assert.equal(model.max_context_window, expectedContextWindow);
|
||||
}
|
||||
});
|
||||
|
||||
test("codex catalog falls back to local Codex model cache metadata", () => {
|
||||
const previousCcrHome = process.env.CCR_INTERNAL_HOME_DIR;
|
||||
const previousHome = process.env.HOME;
|
||||
@@ -355,10 +386,10 @@ test("codex catalog falls back to local Codex model cache metadata", () => {
|
||||
}, "Codex API/gpt-5-codex");
|
||||
|
||||
assert.deepEqual(model.additional_speed_tiers, [{ id: "fast", label: "Fast" }]);
|
||||
assert.equal(model.context_window, 272000);
|
||||
assert.equal(model.context_window, 256000);
|
||||
assert.equal(model.default_reasoning_level, "high");
|
||||
assert.equal(model.effective_context_window_percent, 92);
|
||||
assert.equal(model.max_context_window, 300000);
|
||||
assert.equal(model.max_context_window, 256000);
|
||||
assert.deepEqual(model.service_tiers, [{ id: "auto" }]);
|
||||
assert.deepEqual(model.supported_reasoning_levels.map((level) => level.effort), ["low", "high"]);
|
||||
} finally {
|
||||
|
||||
@@ -80,6 +80,58 @@ test("Codex local provider account config upgrades persisted usage mapping", ()
|
||||
assert.ok(upgradedManualResetMeter.remaining.includes("$.rate_limit_reset_credits.available_count"));
|
||||
});
|
||||
|
||||
test("Codex local provider config caps imported GPT-5 context metadata", () => {
|
||||
const provider = normalizeCodexProviderAccountConfig({
|
||||
api_base_url: codexDefaultBaseUrl,
|
||||
api_key: localAgentProviderApiKey,
|
||||
modelMetadata: {
|
||||
"custom-model": {
|
||||
contextWindow: 64000,
|
||||
maxContextWindow: 64000
|
||||
},
|
||||
"gpt-5.6-sol": {
|
||||
contextWindow: 1050000,
|
||||
maxContextWindow: 1050000
|
||||
}
|
||||
},
|
||||
models: ["gpt-5-codex", "gpt-5.6-sol", "custom-model"],
|
||||
name: "Codex API",
|
||||
protocol: "openai_responses"
|
||||
});
|
||||
|
||||
assert.deepEqual(provider.modelMetadata["gpt-5-codex"], {
|
||||
contextWindow: 256000,
|
||||
maxContextWindow: 256000
|
||||
});
|
||||
assert.deepEqual(provider.modelMetadata["gpt-5.6-sol"], {
|
||||
contextWindow: 368000,
|
||||
maxContextWindow: 368000
|
||||
});
|
||||
assert.deepEqual(provider.modelMetadata["custom-model"], {
|
||||
contextWindow: 64000,
|
||||
maxContextWindow: 64000
|
||||
});
|
||||
|
||||
const nonLocal = normalizeCodexProviderAccountConfig({
|
||||
api_base_url: codexDefaultBaseUrl,
|
||||
api_key: "real-api-key",
|
||||
modelMetadata: {
|
||||
"gpt-5.6-sol": {
|
||||
contextWindow: 1050000,
|
||||
maxContextWindow: 1050000
|
||||
}
|
||||
},
|
||||
models: ["gpt-5.6-sol"],
|
||||
name: "OpenAI",
|
||||
protocol: "openai_responses"
|
||||
});
|
||||
|
||||
assert.deepEqual(nonLocal.modelMetadata["gpt-5.6-sol"], {
|
||||
contextWindow: 1050000,
|
||||
maxContextWindow: 1050000
|
||||
});
|
||||
});
|
||||
|
||||
test("Codex reset credit details include available effective and expiry times", () => {
|
||||
const details = codexRateLimitResetCreditDetails({
|
||||
rate_limit_reset_credits: {
|
||||
@@ -240,10 +292,10 @@ test("Codex model catalog parser accepts live model endpoint shapes", () => {
|
||||
});
|
||||
assert.deepEqual(catalog.modelMetadata["gpt-5-codex"], {
|
||||
additionalSpeedTiers: [{ id: "fast", label: "Fast" }],
|
||||
contextWindow: 272000,
|
||||
contextWindow: 256000,
|
||||
defaultReasoningLevel: "high",
|
||||
effectiveContextWindowPercent: 95,
|
||||
maxContextWindow: 300000,
|
||||
maxContextWindow: 256000,
|
||||
serviceTiers: [{ id: "auto" }],
|
||||
supportedReasoningLevels: [
|
||||
{ description: "Low", effort: "low" },
|
||||
@@ -251,6 +303,49 @@ test("Codex model catalog parser accepts live model endpoint shapes", () => {
|
||||
],
|
||||
supportsReasoningSummaries: true
|
||||
});
|
||||
assert.deepEqual(catalog.modelMetadata["gpt-5.1-codex"], {
|
||||
contextWindow: 256000,
|
||||
maxContextWindow: 256000
|
||||
});
|
||||
assert.deepEqual(catalog.modelMetadata["gpt-5.2-codex"], {
|
||||
contextWindow: 256000,
|
||||
maxContextWindow: 256000
|
||||
});
|
||||
});
|
||||
|
||||
test("Codex model catalog parser overrides imported GPT-5 context windows", () => {
|
||||
const catalog = codexModelCatalogFromPayloadForTest({
|
||||
models: [
|
||||
"gpt-5-codex",
|
||||
"gpt-5.5",
|
||||
"gpt-5.6",
|
||||
"gpt-5.6-sol",
|
||||
"gpt-5.6-terra-2026-07-28",
|
||||
"gpt-50"
|
||||
]
|
||||
});
|
||||
|
||||
assert.deepEqual(catalog.modelMetadata["gpt-5-codex"], {
|
||||
contextWindow: 256000,
|
||||
maxContextWindow: 256000
|
||||
});
|
||||
assert.deepEqual(catalog.modelMetadata["gpt-5.5"], {
|
||||
contextWindow: 256000,
|
||||
maxContextWindow: 256000
|
||||
});
|
||||
assert.deepEqual(catalog.modelMetadata["gpt-5.6"], {
|
||||
contextWindow: 368000,
|
||||
maxContextWindow: 368000
|
||||
});
|
||||
assert.deepEqual(catalog.modelMetadata["gpt-5.6-sol"], {
|
||||
contextWindow: 368000,
|
||||
maxContextWindow: 368000
|
||||
});
|
||||
assert.deepEqual(catalog.modelMetadata["gpt-5.6-terra-2026-07-28"], {
|
||||
contextWindow: 368000,
|
||||
maxContextWindow: 368000
|
||||
});
|
||||
assert.equal(catalog.modelMetadata?.["gpt-50"], undefined);
|
||||
});
|
||||
|
||||
test("Codex model catalog parser ignores invalid context metadata", () => {
|
||||
|
||||
Reference in New Issue
Block a user