diff --git a/config.example.json b/config.example.json index e162cbb..bc6b373 100644 --- a/config.example.json +++ b/config.example.json @@ -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": {} diff --git a/scripts/check-runtime.ts b/scripts/check-runtime.ts index 48cc87f..5cb852a 100755 --- a/scripts/check-runtime.ts +++ b/scripts/check-runtime.ts @@ -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"); @@ -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"') diff --git a/scripts/smoke-tui.ts b/scripts/smoke-tui.ts index 78e7449..658a256 100755 --- a/scripts/smoke-tui.ts +++ b/scripts/smoke-tui.ts @@ -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"); @@ -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) { diff --git a/scripts/sync-runtime.ts b/scripts/sync-runtime.ts index cab6fcf..80e2815 100755 --- a/scripts/sync-runtime.ts +++ b/scripts/sync-runtime.ts @@ -715,17 +715,60 @@ async function installLocalTui(nextVendor: string): Promise { 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 { 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)) + ) ) ) ) diff --git a/test/config-bootstrap.test.ts b/test/config-bootstrap.test.ts index 60cb4f6..e830d49 100644 --- a/test/config-bootstrap.test.ts +++ b/test/config-bootstrap.test.ts @@ -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(); diff --git a/test/config-template.test.ts b/test/config-template.test.ts index 4766d21..c4cad05 100644 --- a/test/config-template.test.ts +++ b/test/config-template.test.ts @@ -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"]); }); diff --git a/test/sync-runtime.test.ts b/test/sync-runtime.test.ts index d923de3..a08a1a0 100644 --- a/test/sync-runtime.test.ts +++ b/test/sync-runtime.test.ts @@ -8,8 +8,9 @@ import { patchRuntimeAgentAutoBackground, patchRuntimeBackgroundTaskProjection, patchRuntimeDetachedAgentLifecycle, - patchRuntimeTerminalToolProjection, + patchRuntimeLoginModelDefaults, patchRuntimeOAuthHttpErrors, + patchRuntimeTerminalToolProjection, patchRuntimeTuiBridge, patchRuntimeZaiDesktopOAuth, resolveArtifactUrl, @@ -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, + providerId: string, + apiKey: string + ) => { + model: { lite: string; main: string }; + provider: Record }>; + }; + + 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$/);