Merge pull request #1589 from jesieleo/fix/zcode-model-context-metadata

fix(zcode): 保证模型同步正确的上下文长度
This commit is contained in:
musi
2026-07-27 16:57:35 +08:00
committed by GitHub
5 changed files with 317 additions and 15 deletions
+16 -5
View File
@@ -8,6 +8,7 @@ import { buildCodexModelCatalog, type CodexModelCatalog, type CodexModelCatalogI
import { prepareCodexAppCdpUserDataDir } from "@ccr/core/agents/codex/media-preview-bridge";
import { buildProfileLaunchPlan, resolveCodexConfigFile } from "@ccr/core/profiles/launch-core";
import { normalizeWindowsDesktopAppCandidate, windowsDesktopAppCandidates } from "@ccr/core/platform/windows-app-discovery";
import { buildZcodeModelCatalog } from "@ccr/core/agents/zcode/model-catalog";
import { writeZcodeGatewayConfig, zcodeHomeFromConfigFile } from "@ccr/core/agents/zcode/profile-config";
export type CodexAppLookupResult = {
@@ -203,7 +204,7 @@ export function writeCodexCompatibleAppModelCatalog(
const userDataDir = codexElectronUserDataDir(codexHome, profile, spec);
mkdirSync(userDataDir, { recursive: true });
const file = codexAppModelCatalogFile(userDataDir, spec);
const content = codexCompatibleAppModelCatalogJson(config, profile.model);
const content = codexCompatibleAppModelCatalogJson(config, profile.model, spec.kind);
const previous = existsSync(file) ? readFileSync(file, "utf8") : undefined;
if (previous !== content) {
writeFileSync(file, content, "utf8");
@@ -211,12 +212,22 @@ export function writeCodexCompatibleAppModelCatalog(
return { changed: previous !== content, file, userDataDir };
}
function codexCompatibleAppModelCatalogJson(config?: CodexCompatibleAppModelCatalogConfig, selectedModel?: string): string {
return `${JSON.stringify(codexCompatibleAppModelCatalog(config, selectedModel), null, 2)}\n`;
function codexCompatibleAppModelCatalogJson(
config?: CodexCompatibleAppModelCatalogConfig,
selectedModel?: string,
kind: CodexCompatibleAppKind = "codex"
): string {
return `${JSON.stringify(codexCompatibleAppModelCatalog(config, selectedModel, kind), null, 2)}\n`;
}
function codexCompatibleAppModelCatalog(config?: CodexCompatibleAppModelCatalogConfig, selectedModel?: string): CodexModelCatalog {
const catalog = buildCodexModelCatalog(config, selectedModel);
function codexCompatibleAppModelCatalog(
config?: CodexCompatibleAppModelCatalogConfig,
selectedModel?: string,
kind: CodexCompatibleAppKind = "codex"
): CodexModelCatalog {
const catalog = kind === "zcode"
? buildZcodeModelCatalog(config, selectedModel)
: buildCodexModelCatalog(config, selectedModel);
return {
models: catalog.models.map((model) => codexCompatibleAppModelCatalogItem(model, config))
};
@@ -0,0 +1,80 @@
import type { AppConfig } from "@ccr/core/contracts/app";
import {
buildCodexModelCatalog,
type CodexModelCatalog
} from "@ccr/core/agents/codex/model-catalog";
import {
findModelCatalogEntry,
type ModelCatalogEntry,
modelCatalogMaxInputTokens
} from "@ccr/core/gateway/model-catalog";
import { modelRegistryForConfig } from "@ccr/core/routing/model-registry";
import { resolveUsageModelAttribution } from "@ccr/core/usage/model-attribution";
type ZcodeModelCatalogConfig = Partial<Pick<
AppConfig,
"Providers" | "Router" | "virtualModelProfiles"
>>;
type ZcodeModelResolutionConfig = Pick<
AppConfig,
"Providers" | "virtualModelProfiles"
>;
export function buildZcodeModelCatalog(
config?: ZcodeModelCatalogConfig,
selectedModel?: string
): CodexModelCatalog {
const catalog = buildCodexModelCatalog(config, selectedModel);
const resolutionConfig = config
? {
Providers: config.Providers ?? [],
virtualModelProfiles: config.virtualModelProfiles ?? []
}
: undefined;
return {
models: catalog.models.map((item) => {
const contextWindow = Math.max(
item.context_window,
modelCatalogMaxInputTokens(zcodeModelCatalogEntry(resolutionConfig, item.slug))
);
return {
...item,
context_window: contextWindow,
max_context_window: Math.max(item.max_context_window, contextWindow)
};
})
};
}
function zcodeModelCatalogEntry(
config: ZcodeModelResolutionConfig | undefined,
model: string
): ModelCatalogEntry | undefined {
if (!config) {
return findModelCatalogEntry(model);
}
const registry = modelRegistryForConfig(config);
const attribution = resolveUsageModelAttribution(config, model);
const physicalModel = attribution.model?.trim();
const physicalProvider = registry.findProvider(attribution.provider);
if (physicalProvider && physicalModel) {
return findModelCatalogEntry(`${physicalProvider.name}/${physicalModel}`);
}
// Request-routed virtual models do not have one physical model until the
// request is evaluated, so retain the context already produced by Codex.
if (registry.resolve(model)?.kind === "gateway") {
return undefined;
}
return findModelCatalogEntry(physicalModel || model);
}
export function zcodeModelCatalogJson(
config?: ZcodeModelCatalogConfig,
selectedModel?: string
): string {
return `${JSON.stringify(buildZcodeModelCatalog(config, selectedModel), null, 2)}\n`;
}
@@ -3,7 +3,7 @@ import os from "node:os";
import path from "node:path";
import { isGatewayProviderEnabled, type AppConfig, type ProfileConfig } from "@ccr/core/contracts/app";
import { normalizeRouteSelector } from "@ccr/core/gateway/claude-code-router-plugin";
import { buildCodexModelCatalogIds } from "@ccr/core/agents/codex/model-catalog";
import { buildZcodeModelCatalog } from "@ccr/core/agents/zcode/model-catalog";
export type ZcodeProfileConfigWriteResult = {
backupFile?: string;
@@ -15,6 +15,7 @@ export type ZcodeProfileConfigWriteResult = {
type ZcodeGatewayConfigValues = {
baseUrl: string;
model: string;
modelContextWindows: Record<string, number>;
providerId: string;
providerName: string;
token: string;
@@ -25,6 +26,8 @@ const legacyZcodeTomlConfigFile = "~/.zcode/config.toml";
const defaultZcodeConfigFile = "~/.zcode/cli/config.json";
const originalBackupSuffix = ".ccr-original";
const originalMissingSuffix = ".ccr-original-missing";
const defaultZcodeContextWindow = 128_000;
const defaultZcodeMaxOutputTokens = 8_192;
export function resolveZcodeConfigFile(profile: Pick<ProfileConfig, "codexHome" | "configFile">): string {
const configured = profile.configFile?.trim();
@@ -51,10 +54,15 @@ export function writeZcodeGatewayConfig(
const file = resolveZcodeConfigFile(profile);
const model = normalizeClientModel(profile.model) || defaultClientModel(config);
const providerId = sanitizeZcodeProviderId(profile.providerId || "") || "claude-code-router";
const modelCatalog = buildZcodeModelCatalog(config, model);
const values: ZcodeGatewayConfigValues = {
baseUrl: gatewayEndpoint(config),
model,
models: buildCodexModelCatalogIds(config, model),
modelContextWindows: Object.fromEntries(modelCatalog.models.map((item) => [
item.slug,
positiveNumber(item.context_window) ?? defaultZcodeContextWindow
])),
models: modelCatalog.models.map((item) => item.slug),
providerId,
providerName: profile.providerName?.trim() || "Claude Code Router",
token
@@ -117,7 +125,10 @@ function zcodeConfigProvider(values: ZcodeGatewayConfigValues): Record<string, u
apiKeyRequired: true,
baseURL: values.baseUrl
},
models: Object.fromEntries(uniqueStrings(values.models).map((model) => [model, zcodeModelConfig(model)]))
models: Object.fromEntries(uniqueStrings(values.models).map((model) => [
model,
zcodeModelConfig(model, values.modelContextWindows[model])
]))
};
}
@@ -145,13 +156,13 @@ function buildZcodeV2ModelCache(source: Record<string, unknown>, values: ZcodeGa
};
}
function zcodeModelConfig(model: string): Record<string, unknown> {
function zcodeModelConfig(model: string, contextWindow: number | undefined): Record<string, unknown> {
return {
id: model,
name: model,
limit: {
context: 128_000,
output: 8_192
context: contextWindow ?? defaultZcodeContextWindow,
output: defaultZcodeMaxOutputTokens
},
modalities: {
input: ["text", "image"],
@@ -185,13 +196,16 @@ function zcodeV2ModelCacheProvider(
apiKey: "__zcode_cached_api_key_present__",
apiKeyRequired: true,
defaultKind: "anthropic",
models: uniqueStrings(values.models).map((model) => zcodeV2ModelCacheModel(model)),
models: uniqueStrings(values.models).map((model) => zcodeV2ModelCacheModel(
model,
values.modelContextWindows[model]
)),
createdAt: positiveNumber(previousProvider?.createdAt) ?? now,
updatedAt: now
};
}
function zcodeV2ModelCacheModel(model: string): Record<string, unknown> {
function zcodeV2ModelCacheModel(model: string, contextWindow: number | undefined): Record<string, unknown> {
return {
id: model,
name: model,
@@ -201,8 +215,8 @@ function zcodeV2ModelCacheModel(model: string): Record<string, unknown> {
input: ["text", "image"],
output: ["text"]
},
contextWindow: 128_000,
maxOutputTokens: 8_192,
contextWindow: contextWindow ?? defaultZcodeContextWindow,
maxOutputTokens: defaultZcodeMaxOutputTokens,
supportsStructuredOutput: true,
supportsTools: true
};
@@ -173,6 +173,56 @@ test("ChatGPT model catalog write gives gateway GPT models reasoning effort fall
}
});
test("ZCode app model catalog uses the public model context window", () => {
const configDir = mkdtempSync(path.join(os.tmpdir(), "ccr-zcode-app-catalog-"));
try {
const config = {
Providers: [{
api_base_url: "https://chatgpt.com/backend-api/codex",
modelMetadata: {
"gpt-5.6-sol": {
contextWindow: 272_000,
maxContextWindow: 272_000
}
},
models: ["gpt-5.6-sol"],
name: "Codex API",
type: "openai_responses"
}],
virtualModelProfiles: [{
baseModel: { fixedModel: "Codex API/gpt-5.6-sol", mode: "fixed" },
enabled: true,
match: { exactAliases: ["catalog-context"], prefixes: [], suffixes: [] },
materialization: { enabled: true, includeInGatewayModels: true }
}]
};
const profile = {
agent: "zcode",
codexHome: configDir,
enabled: true,
id: "zcode-main",
model: "Codex API/gpt-5.6-sol",
name: "ZCode Main",
providerId: "claude-code-router",
scope: "global",
surface: "app"
};
const result = writeCodexCompatibleAppModelCatalog(configDir, profile, config);
const catalog = JSON.parse(readFileSync(result.file, "utf8"));
const fusionModel = catalog.models.find((item) => item.slug === "Fusion/catalog-context");
const model = catalog.models.find((item) => item.slug === "Codex API/gpt-5.6-sol");
assert.equal(path.basename(result.file), "ccr-zcode-model-catalog.json");
assert.equal(fusionModel.context_window, 1_050_000);
assert.equal(fusionModel.max_context_window, 1_050_000);
assert.equal(model.context_window, 1_050_000);
assert.equal(model.max_context_window, 1_050_000);
} finally {
rmSync(configDir, { force: true, recursive: true });
}
});
test("ChatGPT desktop app path override discovers the renamed executable", () => {
const root = mkdtempSync(path.join(os.tmpdir(), "ccr-chatgpt-app-"));
const previous = process.env.CHATGPT_APP_PATH;
@@ -0,0 +1,147 @@
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { buildZcodeModelCatalog } from "@ccr/core/agents/zcode/model-catalog.ts";
import { writeZcodeGatewayConfig } from "@ccr/core/agents/zcode/profile-config.ts";
import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts";
const fusionModel = "Fusion/catalog-context";
const knownModel = "Codex API/gpt-5.6-sol";
const unknownModel = "Codex API/unknown-model";
function testConfig(root) {
const config = createDefaultAppConfig({ generatedConfigFile: path.join(root, "gateway.config.json") });
config.Providers = [{
api_base_url: "https://example.test/v1",
modelMetadata: {
"gpt-5.6-sol": {
contextWindow: 272_000,
maxContextWindow: 272_000
}
},
models: ["gpt-5.6-sol", "unknown-model"],
name: "Codex API",
type: "openai_responses"
}];
config.virtualModelProfiles = [{
baseModel: { fixedModel: knownModel, mode: "fixed" },
enabled: true,
match: { exactAliases: ["catalog-context"], prefixes: [], suffixes: [] },
materialization: { enabled: true, includeInGatewayModels: true }
}];
config.preferredProvider = "Codex API";
config.gateway.host = "127.0.0.1";
config.gateway.port = 4567;
return config;
}
function testProfile(root) {
return {
agent: "zcode",
codexHome: root,
enabled: true,
env: {},
id: "zcode-main",
model: "Codex API,gpt-5.6-sol",
name: "ZCode Main",
providerId: "claude-code-router",
providerName: "Claude Code Router",
scope: "global",
surface: "app"
};
}
test("ZCode profile config writes resolved model limits instead of fixed defaults", () => {
const root = mkdtempSync(path.join(os.tmpdir(), "ccr-zcode-profile-"));
try {
const result = writeZcodeGatewayConfig(
testConfig(root),
testProfile(root),
"ccr-profile-key",
{ backup: false }
);
const cliConfig = JSON.parse(readFileSync(result.file, "utf8"));
const v2Config = JSON.parse(readFileSync(path.join(root, "v2", "config.json"), "utf8"));
const cache = JSON.parse(readFileSync(path.join(root, "v2", "bots-model-cache.v2.json"), "utf8"));
for (const config of [cliConfig, v2Config]) {
const models = config.provider["claude-code-router"].models;
assert.equal(models[fusionModel].limit.context, 1_050_000);
assert.equal(models[fusionModel].limit.output, 8_192);
assert.equal(models[knownModel].limit.context, 1_050_000);
assert.equal(models[knownModel].limit.output, 8_192);
assert.equal(models[unknownModel].limit.context, 128_000);
assert.equal(models[unknownModel].limit.output, 8_192);
}
const cachedProvider = cache.providers.find((provider) => provider.id === "claude-code-router");
const cachedModels = Object.fromEntries(cachedProvider.models.map((model) => [model.id, model]));
assert.equal(cachedModels[fusionModel].contextWindow, 1_050_000);
assert.equal(cachedModels[fusionModel].maxOutputTokens, 8_192);
assert.equal(cachedModels[knownModel].contextWindow, 1_050_000);
assert.equal(cachedModels[knownModel].maxOutputTokens, 8_192);
assert.equal(cachedModels[unknownModel].contextWindow, 128_000);
assert.equal(cachedModels[unknownModel].maxOutputTokens, 8_192);
} finally {
rmSync(root, { force: true, recursive: true });
}
});
test("ZCode model catalog resolves physical models for materialized Fusion selectors", () => {
const root = mkdtempSync(path.join(os.tmpdir(), "ccr-zcode-catalog-"));
try {
const config = testConfig(root);
config.Providers.push({
api_base_url: "https://api.deepseek.com",
modelMetadata: {
"deepseek-v4-flash": {
contextWindow: 128_000,
maxContextWindow: 128_000
}
},
models: ["deepseek-v4-flash"],
name: "DeepSeek",
type: "openai_responses"
});
config.virtualModelProfiles.push(
{
baseModel: { fixedModel: "DeepSeek/deepseek-v4-flash", mode: "fixed" },
enabled: true,
match: { exactAliases: ["deepseek-context"], prefixes: [], suffixes: [] },
materialization: { enabled: true, includeInGatewayModels: true }
},
{
baseModel: { mode: "request" },
enabled: true,
match: { exactAliases: [], prefixes: ["fusion-"], suffixes: [] },
materialization: { enabled: true, includeInGatewayModels: true }
},
{
baseModel: { mode: "request" },
enabled: true,
match: { exactAliases: [], prefixes: [], suffixes: ["-fusion"] },
materialization: { enabled: true, includeInGatewayModels: true }
},
{
baseModel: { mode: "request" },
enabled: true,
match: { exactAliases: ["dynamic-route"], prefixes: [], suffixes: [] },
materialization: { enabled: true, includeInGatewayModels: true }
}
);
const models = Object.fromEntries(
buildZcodeModelCatalog(config).models.map((model) => [model.slug, model])
);
assert.equal(models[fusionModel].context_window, 1_050_000);
assert.equal(models["Fusion/deepseek-context"].context_window, 1_050_000);
assert.equal(models["Codex API/fusion-gpt-5.6-sol"].context_window, 1_050_000);
assert.equal(models["Codex API/gpt-5.6-sol-fusion"].context_window, 1_050_000);
assert.equal(models["Fusion/dynamic-route"].context_window, 128_000);
} finally {
rmSync(root, { force: true, recursive: true });
}
});