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
5 changes: 4 additions & 1 deletion config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
},
"glm-5.1": {
"name": "GLM-5.1"
},
"glm-5-turbo": {
"name": "GLM-5-Turbo"
}
}
}
},
"model": {
"main": "zai/glm-5.2",
"lite": "zai/glm-5.1"
"lite": "zai/glm-5-turbo"
},
"modelCatalog": {
"overrides": {}
Expand Down
8 changes: 6 additions & 2 deletions scripts/check-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

import { formatVersionOutput, readDistributionVersion } from "../src/launcher.ts";
import { supportsMultiMessageFileRewind } from "./sync-runtime.ts";
import {
patchRuntimeLoginModelDefaults,
supportsMultiMessageFileRewind
} from "./sync-runtime.ts";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const runtime = join(root, "vendor", "zcode.cjs");
Expand All @@ -30,7 +33,8 @@ if (packageManifest.dependencies?.["playwright-core"] !== "1.59.1"
}

const runtimeSource = await Bun.file(runtime).text();
if (!runtimeSource.includes('"plugin://"')
if (patchRuntimeLoginModelDefaults(runtimeSource) !== runtimeSource
|| !runtimeSource.includes('"plugin://"')
|| !runtimeSource.includes('return await import("playwright-core")')
|| !runtimeSource.includes('pluginsReferenceCatalog:"plugins/referenceCatalog"')
|| !runtimeSource.includes('pluginsMarketplaceAdd:"plugins/marketplace/add"')
Expand Down
23 changes: 21 additions & 2 deletions scripts/smoke-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,26 @@ try {
);
await waitFor(
"API key turn completion",
/(?:Configured Z\.AI Coding Plan|已配置 Z\.AI Coding Plan)[\s\S]*◈ zai\/glm-5\.1/i,
/(?:Configured Z\.AI Coding Plan|已配置 Z\.AI Coding Plan)[\s\S]*◈ zai\/glm-5\.2/i,
apiKeySetupStart
);
await sendAndWait("/login\r", "reopened login setup picker", /Set Up Coding Plan|配置 Coding Plan/i);
await sendAndWait(
"\x1b[B\x1b[B\x1b[B\r",
"BigModel masked API key prompt",
/Enter BigModel Coding Plan API Key|输入 BigModel Coding Plan API Key/i
);
await sendAndWait(smokeApiKey, "BigModel masked API key value", /\*{20,}/i);
const bigmodelSetupStart = await sendAndWait(
"\r",
"BigModel API key setup",
/Configured BigModel Coding Plan|已配置 BigModel Coding Plan/i
);
await waitFor(
"BigModel API key turn completion",
/(?:Configured BigModel Coding Plan|已配置 BigModel Coding Plan)[\s\S]*◈ bigmodel\/glm-5\.2/i,
bigmodelSetupStart
);
await sendAndWait("/help\r", "help output", /Slash commands:|Usage:/i);
await sendAndWait("/mode plan\r", "plan mode", /mode switched to plan|current mode: plan|◈ default ─ ◉ plan/i);
terminal.write("/exit\r");
Expand Down Expand Up @@ -259,7 +276,9 @@ if (/Model config is missing/i.test(plain)) {
if (plain.includes(smokeApiKey)) {
throw new Error(`The API key leaked into terminal output.\n${plain.slice(-4_000)}`);
}
if (!configured.includes(smokeApiKey) || !configured.includes('"main": "zai/')) {
if (!configured.includes(smokeApiKey)
|| !configured.includes('"main": "bigmodel/glm-5.2"')
|| !configured.includes('"lite": "bigmodel/glm-4.7"')) {
throw new Error("The official runtime did not persist the Coding Plan configuration.");
}
if (leakedFiles.length > 0) {
Expand Down
55 changes: 49 additions & 6 deletions scripts/sync-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,17 +715,60 @@ async function installLocalTui(nextVendor: string): Promise<void> {
await cp(join(source, "dist"), join(target, "dist"), { recursive: true });
}

/** Align Coding Plan defaults without depending on platform-specific minifier names. */
export function patchRuntimeLoginModelDefaults(runtime: string): string {
const presetPattern = /([A-Za-z_$][\w$]*)="zai\/glm-(?:5\.1|5\.2)",([A-Za-z_$][\w$]*)="zai\/glm-(?:4\.7|5-turbo)",([A-Za-z_$][\w$]*)="bigmodel\/glm-(?:5\.1|5\.2)",([A-Za-z_$][\w$]*)="bigmodel\/glm-4\.7"/u;
const modelIdPattern = /([A-Za-z_$][\w$]*)="glm-(?:5\.1|5\.2)",([A-Za-z_$][\w$]*)="glm-(?:4\.7|5-turbo)"/u;
const modelEntriesPattern = /models:\{\.\.\.([A-Za-z_$][\w$]*),\[([A-Za-z_$][\w$]*)\]:\{\.\.\.([A-Za-z_$][\w$]*),name:"GLM-5\.(?:1|2)"\},\[([A-Za-z_$][\w$]*)\]:\{\.\.\.([A-Za-z_$][\w$]*),name:"GLM-(?:4\.7|5-Turbo)"\}(?:,\["glm-5-turbo"\]:\{\.\.\.\1\["glm-5-turbo"\],name:"GLM-5-Turbo"\})?\}/u;
const legacyLiteSelectionPattern = /([A-Za-z_$][\w$]*)=typeof ([A-Za-z_$][\w$]*)\.lite=="string"\?\2\.lite:([A-Za-z_$][\w$]*)\.liteModel/u;
const scopedLiteSelectionPattern = /([A-Za-z_$][\w$]*)=typeof ([A-Za-z_$][\w$]*)\.lite=="string"&&\2\.lite\.startsWith\(([A-Za-z_$][\w$]*)\.mainModel\.slice\(0,\3\.mainModel\.indexOf\("\/"\)\+1\)\)\?\2\.lite:\3\.liteModel/u;

const preset = presetPattern.exec(runtime);
const modelIds = modelIdPattern.exec(runtime);
const modelEntries = modelEntriesPattern.exec(runtime);
if (!preset || !modelIds || !modelEntries
|| modelIds[1] !== modelEntries[2]
|| modelIds[2] !== modelEntries[4]) {
throw new Error("ZCode runtime is incompatible with the login model defaults patch (model preset/catalog anchors missing).");
}

let patched = runtime
.replace(
preset[0],
`${preset[1]}="zai/glm-5.2",${preset[2]}="zai/glm-5-turbo",${preset[3]}="bigmodel/glm-5.2",${preset[4]}="bigmodel/glm-4.7"`
)
.replace(modelIds[0], `${modelIds[1]}="glm-5.2",${modelIds[2]}="glm-4.7"`)
.replace(
modelEntries[0],
`models:{...${modelEntries[1]},[${modelEntries[2]}]:{...${modelEntries[3]},name:"GLM-5.2"},[${modelEntries[4]}]:{...${modelEntries[5]},name:"GLM-4.7"},["glm-5-turbo"]:{...${modelEntries[1]}["glm-5-turbo"],name:"GLM-5-Turbo"}}`
);

const legacyLiteSelection = legacyLiteSelectionPattern.exec(patched);
if (legacyLiteSelection) {
const [, selection, model, selectedPreset] = legacyLiteSelection;
patched = patched.replace(
legacyLiteSelection[0],
`${selection}=typeof ${model}.lite=="string"&&${model}.lite.startsWith(${selectedPreset}.mainModel.slice(0,${selectedPreset}.mainModel.indexOf("/")+1))?${model}.lite:${selectedPreset}.liteModel`
);
} else if (!scopedLiteSelectionPattern.test(patched)) {
throw new Error("ZCode runtime is incompatible with the login model defaults patch (lite model anchor missing).");
}
return patched;
}

async function installTuiBridge(nextVendor: string): Promise<void> {
const runtimePath = join(nextVendor, "zcode.cjs");
const runtime = await readFile(runtimePath, "utf8");
await writeFile(
runtimePath,
patchRuntimeZaiDesktopOAuth(
patchRuntimeOAuthHttpErrors(
patchRuntimeAgentAutoBackground(
patchRuntimeDetachedAgentLifecycle(
patchRuntimeTerminalToolProjection(
patchRuntimeBackgroundTaskProjection(patchRuntimeTuiBridge(runtime))
patchRuntimeLoginModelDefaults(
patchRuntimeZaiDesktopOAuth(
patchRuntimeOAuthHttpErrors(
patchRuntimeAgentAutoBackground(
patchRuntimeDetachedAgentLifecycle(
patchRuntimeTerminalToolProjection(
patchRuntimeBackgroundTaskProjection(patchRuntimeTuiBridge(runtime))
)
)
)
)
Expand Down
3 changes: 2 additions & 1 deletion test/config-bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ describe("user config bootstrap", () => {
expect(config.provider.zai.options.apiKey).toBeUndefined();
expect(config.provider.zai.models["glm-5.2"]).toBeDefined();
expect(config.provider.zai.models["glm-5.1"]).toBeDefined();
expect(config.model).toEqual({ main: "zai/glm-5.2", lite: "zai/glm-5.1" });
expect(config.provider.zai.models["glm-5-turbo"]).toBeDefined();
expect(config.model).toEqual({ main: "zai/glm-5.2", lite: "zai/glm-5-turbo" });
expect(config.modelStream.idleTimeoutMs).toBe(60_000);
expect(config.subagents.autoBackgroundMs).toBe(1_000);
expect(await readConfiguredModelAccess(env)).toBeNull();
Expand Down
3 changes: 3 additions & 0 deletions test/config-template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,7 @@ test("custom-provider config template is internally consistent", async () => {
expect(config.subagents.autoBackgroundMs).toBe(1_000);
expect(config.ui.theme).toBe("auto");
expect(config.ui.notifications).toEqual({ method: "auto", condition: "unfocused" });
// The runtime's user-config model schema is strict: only main/lite are accepted
// (model.available is a runtime-internal key injected by the app host, not user config).
expect(Object.keys(config.model).sort()).toEqual(["lite", "main"]);
});
71 changes: 70 additions & 1 deletion test/sync-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import {
patchRuntimeAgentAutoBackground,
patchRuntimeBackgroundTaskProjection,
patchRuntimeDetachedAgentLifecycle,
patchRuntimeTerminalToolProjection,
patchRuntimeLoginModelDefaults,
patchRuntimeOAuthHttpErrors,
patchRuntimeTerminalToolProjection,
patchRuntimeTuiBridge,
patchRuntimeZaiDesktopOAuth,
resolveArtifactUrl,
Expand Down Expand Up @@ -203,6 +204,74 @@ describe("runtime synchronization", () => {
});
});

test("updates the login model defaults to the current server catalog", () => {
const runtime = [
'function Cs(e){return typeof e==="object"&&e!==null&&!Array.isArray(e)}',
'function Pni(e,t,r){let o=ICt[t],n=Cs(e.provider)?e.provider:{},',
'i=Cs(n[t])?n[t]:{},a=Cs(i.options)?i.options:{},u=Cs(i.models)?i.models:{},',
'l=Cs(u[kCt])?u[kCt]:{},c=Cs(u[SCt])?u[SCt]:{},d=Cs(e.model)?e.model:{},',
'p=typeof d.lite=="string"?d.lite:o.liteModel,m={...a,apiKeyRequired:true,baseURL:o.baseURL};',
'return r.length>0&&(m.apiKey=r),{...e,provider:{...n,[t]:{...i,kind:xni,name:o.displayName,options:m,',
'models:{...u,[kCt]:{...l,name:"GLM-5.1"},[SCt]:{...c,name:"GLM-4.7"}}}},',
'model:{...d,main:o.mainModel,lite:p}}}',
'var fni="bigmodel",hni="zai",',
'gni="zai/glm-5.1",_ni="zai/glm-4.7",vni="bigmodel/glm-5.1",yni="bigmodel/glm-4.7",',
'kCt="glm-5.1",SCt="glm-4.7",xni="anthropic",',
'ICt={[fni]:{baseURL:"https://open.bigmodel.cn/api/anthropic",displayName:"BigModel Coding Plan",',
'liteModel:yni,mainModel:vni},[hni]:{baseURL:"https://api.z.ai/api/anthropic",',
'displayName:"Z.AI Coding Plan",liteModel:_ni,mainModel:gni}};'
].join("");
const patched = patchRuntimeLoginModelDefaults(runtime);
const updateConfig = new Function(`${patched};return Pni;`)() as (
config: Record<string, unknown>,
providerId: string,
apiKey: string
) => {
model: { lite: string; main: string };
provider: Record<string, { models: Record<string, unknown> }>;
};

expect(patched).toContain(
'gni="zai/glm-5.2",_ni="zai/glm-5-turbo",vni="bigmodel/glm-5.2",yni="bigmodel/glm-4.7"'
);
expect(patched).toContain('kCt="glm-5.2",SCt="glm-4.7"');
expect(patched).toContain('["glm-5-turbo"]:{...u["glm-5-turbo"],name:"GLM-5-Turbo"}');
expect(patched).not.toContain('gni="zai/glm-5.1"');
expect(patched).not.toContain('"GLM-5.1"');

const zai = updateConfig({}, "zai", "zai-key");
expect(zai.model).toEqual({ main: "zai/glm-5.2", lite: "zai/glm-5-turbo" });
expect(Object.keys(zai.provider.zai!.models).sort()).toEqual([
"glm-4.7",
"glm-5-turbo",
"glm-5.2"
]);

const bigmodel = updateConfig({}, "bigmodel", "bigmodel-key");
expect(bigmodel.model).toEqual({ main: "bigmodel/glm-5.2", lite: "bigmodel/glm-4.7" });
expect(bigmodel.provider.bigmodel!.models["glm-4.7"]).toBeDefined();
expect(updateConfig(
{ model: { lite: "zai/custom-lite" } },
"bigmodel",
"bigmodel-key"
).model.lite).toBe("bigmodel/glm-4.7");
expect(updateConfig(
{ model: { lite: "bigmodel/custom-lite" } },
"bigmodel",
"bigmodel-key"
).model.lite).toBe("bigmodel/custom-lite");

const partiallyUpdated = runtime.replace(
'gni="zai/glm-5.1",_ni="zai/glm-4.7",vni="bigmodel/glm-5.1",yni="bigmodel/glm-4.7"',
'gni="zai/glm-5.2",_ni="zai/glm-5-turbo",vni="bigmodel/glm-5.2",yni="bigmodel/glm-4.7"'
);
expect(patchRuntimeLoginModelDefaults(partiallyUpdated)).toBe(patched);
expect(patchRuntimeLoginModelDefaults(patched)).toBe(patched);
expect(() => patchRuntimeLoginModelDefaults("incompatible runtime")).toThrow(
/login model defaults patch/
);
});

test("maps supported static updater manifests", () => {
expect(manifestUrl("linux", "x64")).toMatch(/update\/linux\/x64\/latest-linux\.yml$/);
expect(manifestUrl("darwin", "arm64")).toMatch(/update\/mac\/arm64\/latest-mac\.yml$/);
Expand Down