FN-6401: add built-in Z.ai GLM-5.2 support

Add Z.ai's built-in provider registration so GLM-5.2 is selectable across Fusion model settings.

- Define and export a shared Z.ai provider registration with existing GLM models plus GLM-5.2.
- Register the built-in provider for CLI server and engine pi model registries.
- Cover provider availability and CLI auth-provider wrapping with focused tests.
- Document Z.ai model selection and add a minor changeset.

Files changed:
 .changeset/FN-6401-glm-5-2.md                      |   5 +
 docs/settings-reference.md                         |   2 +
 packages/cli/src/commands/__tests__/daemon.test.ts |  11 ++
 .../cli/src/commands/__tests__/dashboard.test.ts   |  13 +++
 packages/cli/src/commands/__tests__/serve.test.ts  |  11 ++
 packages/cli/src/commands/daemon.ts                |   8 ++
 packages/cli/src/commands/dashboard.ts             |   8 ++
 packages/cli/src/commands/serve.ts                 |   8 ++
 packages/core/src/__tests__/zai-provider.test.ts   |  48 ++++++++
 packages/core/src/index.ts                         |   2 +
 packages/core/src/zai-provider.ts                  | 125 +++++++++++++++++++++
 .../src/__tests__/pi-create-fn-agent.test.ts       |   5 +-
 packages/engine/src/pi.ts                          |  20 +++-
 13 files changed, 264 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-6401

Fusion-Task-Lineage: e1f7a90d-0614-4d9f-826f-015605a08baa
This commit is contained in:
gsxdsm
2026-06-13 16:19:29 -07:00
parent 3387a9b298
commit 9149121bb6
13 changed files with 264 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": minor
---
Enable Z.ai GLM-5.2 model selection.

View File

@@ -801,6 +801,8 @@ Short-lived token bounds are enforced server-side:
Fusion resolves task models through workflow-backed lane values first, then global lane defaults, then the project/global default model fallback. The common workflow lanes are stored as setting values on the project's default workflow and can be edited with dropdown controls from Settings -> Project Models -> Default workflow model lanes (persisted by the Settings modal's primary Save) or from workflow editor -> Settings -> Values for declared workflow lanes and fallbacks.
Z.ai's built-in provider uses the existing `zai` auth entry / `ZAI_API_KEY` environment variable and includes `zai/glm-5.2` as a selectable model in the same dropdowns and workflow lane controls as the other built-in GLM models.
### Planning model
1. Per-task `planningModelProvider` + `planningModelId`

View File

@@ -682,6 +682,17 @@ describe("runDaemon", () => {
await runDaemon({});
expect(mockSyncStartupModels).toHaveBeenCalledTimes(1);
});
it("registers built-in zai GLM-5.2 before refreshing models", async () => {
await runDaemon({});
expect(mocks.modelRegistry.registerProvider).toHaveBeenCalledWith("zai", expect.objectContaining({
models: expect.arrayContaining([expect.objectContaining({ id: "glm-5.2" })]),
}));
expect(mocks.modelRegistry.refresh).toHaveBeenCalled();
await triggerSignal("SIGINT");
});
const originalCwd = process.cwd;
const originalExit = process.exit;

View File

@@ -832,10 +832,23 @@ async function runDashboard(...args: Parameters<typeof runDashboardImpl>): Retur
// ── Tests ───────────────────────────────────────────────────────────
describe("runDashboard — startup model sync", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("invokes shared startup model sync", async () => {
await runDashboard(0, { open: false });
expect(mockSyncStartupModels).toHaveBeenCalledTimes(1);
});
it("registers built-in zai GLM-5.2 before refreshing models", async () => {
await runDashboard(0, { open: false });
expect(mockModelRegistry.registerProvider).toHaveBeenCalledWith("zai", expect.objectContaining({
models: expect.arrayContaining([expect.objectContaining({ id: "glm-5.2" })]),
}));
expect(mockModelRegistry.refresh).toHaveBeenCalled();
});
});
function resetGitHubMocks() {

View File

@@ -752,6 +752,17 @@ describe("runServe", () => {
await runServe(4040, {});
expect(mockSyncStartupModels).toHaveBeenCalledTimes(1);
});
it("registers built-in zai GLM-5.2 before refreshing models", async () => {
await runServe(0, {});
expect(mocks.modelRegistry.registerProvider).toHaveBeenCalledWith("zai", expect.objectContaining({
models: expect.arrayContaining([expect.objectContaining({ id: "glm-5.2" })]),
}));
expect(mocks.modelRegistry.refresh).toHaveBeenCalled();
await triggerSignal("SIGINT");
});
const originalCwd = process.cwd;
const originalOn = process.on;
const originalExit = process.exit;

View File

@@ -21,6 +21,8 @@ import {
resolveGlobalDir,
getEnabledPiExtensionPaths,
reconcileClaudeCliPaths,
ZAI_PROVIDER_ID,
ZAI_PROVIDER_REGISTRATION,
} from "@fusion/core";
import type { AutomationRunResult, ScheduledTask } from "@fusion/core";
import { createServer, GitHubClient, createSkillsAdapter, getProjectSettingsPath, loadTlsCredentialsFromEnv, registerGithubTrackingHook } from "@fusion/dashboard";
@@ -552,6 +554,12 @@ export async function runDaemon(opts: DaemonOptions = {}) {
]);
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
try {
modelRegistry.registerProvider(ZAI_PROVIDER_ID, ZAI_PROVIDER_REGISTRATION);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`[extensions] Failed to register built-in ${ZAI_PROVIDER_ID} provider: ${message}`);
}
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
// PackageManager may be used for skills adapter even if extension loading fails

View File

@@ -20,6 +20,8 @@ import {
resolveColumnFlags,
BUILTIN_CODING_WORKFLOW_IR,
parseWorkflowIr,
ZAI_PROVIDER_ID,
ZAI_PROVIDER_REGISTRATION,
type WorkflowIrColumn,
type TraitFlags,
} from "@fusion/core";
@@ -1369,6 +1371,12 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
]);
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
try {
modelRegistry.registerProvider(ZAI_PROVIDER_ID, ZAI_PROVIDER_REGISTRATION);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logSink.log(`Failed to register built-in ${ZAI_PROVIDER_ID} provider: ${message}`, "extensions");
}
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
// PackageManager may be used for skills adapter even if extension loading fails.

View File

@@ -21,6 +21,8 @@ import {
GlobalSettingsStore,
resolveGlobalDir,
getEnabledPiExtensionPaths,
ZAI_PROVIDER_ID,
ZAI_PROVIDER_REGISTRATION,
} from "@fusion/core";
import type { AutomationRunResult, ScheduledTask } from "@fusion/core";
import { createServer, GitHubClient, createSkillsAdapter, getProjectSettingsPath, loadTlsCredentialsFromEnv, registerGithubTrackingHook } from "@fusion/dashboard";
@@ -602,6 +604,12 @@ export async function runServe(
]);
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
try {
modelRegistry.registerProvider(ZAI_PROVIDER_ID, ZAI_PROVIDER_REGISTRATION);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`[extensions] Failed to register built-in ${ZAI_PROVIDER_ID} provider: ${message}`);
}
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
// PackageManager may be used for skills adapter even if extension loading fails

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { ZAI_PROVIDER_ID, ZAI_PROVIDER_REGISTRATION } from "../zai-provider.js";
const EXISTING_ZAI_MODELS = [
"glm-4.5-air",
"glm-4.7",
"glm-5-turbo",
"glm-5.1",
"glm-5v-turbo",
];
describe("ZAI_PROVIDER_REGISTRATION", () => {
it("uses the existing zai auth surface and API endpoint", () => {
expect(ZAI_PROVIDER_ID).toBe("zai");
expect(ZAI_PROVIDER_REGISTRATION).toMatchObject({
name: "ZAI",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
apiKey: "$ZAI_API_KEY",
api: "openai-completions",
});
});
it("preserves existing built-in models and appends glm-5.2", () => {
const modelIds = ZAI_PROVIDER_REGISTRATION.models.map((model) => model.id);
expect(modelIds).toEqual([...EXISTING_ZAI_MODELS, "glm-5.2"]);
for (const id of EXISTING_ZAI_MODELS) {
expect(modelIds).toContain(id);
}
});
it("registers GLM-5.2 with upstream model capabilities", () => {
expect(ZAI_PROVIDER_REGISTRATION.models.find((model) => model.id === "glm-5.2")).toMatchObject({
id: "glm-5.2",
name: "GLM-5.2",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: 131_072,
compat: {
supportsDeveloperRole: false,
thinkingFormat: "zai",
zaiToolStream: true,
},
});
});
});

View File

@@ -20,6 +20,8 @@ export { redactSecrets } from "./redact-secrets.js";
export * from "./frontend-ux-policy.js";
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";
export type { MockProviderId, MockSessionPurpose } from "./mock-provider-constants.js";
export { ZAI_PROVIDER_ID, ZAI_PROVIDER_REGISTRATION } from "./zai-provider.js";
export type { ZaiProviderRegistration } from "./zai-provider.js";
export {
resolveWorktrunkSettings,
requiresWorktrunkInstallVerification,

View File

@@ -0,0 +1,125 @@
export const ZAI_PROVIDER_ID = "zai";
type ZaiModelInput = "text" | "image";
interface ZaiModelRegistration {
id: string;
name: string;
reasoning: boolean;
input: ZaiModelInput[];
cost: {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
};
contextWindow: number;
maxTokens: number;
compat: {
supportsDeveloperRole: boolean;
thinkingFormat: "zai";
zaiToolStream?: boolean;
};
}
export interface ZaiProviderRegistration {
name: string;
baseUrl: string;
apiKey: string;
api: "openai-completions";
models: ZaiModelRegistration[];
}
// pi registerProvider() replaces the provider's model list, so keep every
// currently built-in Z.ai model here and append new models such as GLM-5.2.
export const ZAI_PROVIDER_REGISTRATION: ZaiProviderRegistration = {
name: "ZAI",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
apiKey: "$ZAI_API_KEY",
api: "openai-completions",
models: [
{
id: "glm-4.5-air",
name: "GLM-4.5-Air",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 131072,
maxTokens: 98304,
compat: {
supportsDeveloperRole: false,
thinkingFormat: "zai",
},
},
{
id: "glm-4.7",
name: "GLM-4.7",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 204800,
maxTokens: 131072,
compat: {
supportsDeveloperRole: false,
thinkingFormat: "zai",
zaiToolStream: true,
},
},
{
id: "glm-5-turbo",
name: "GLM-5-Turbo",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 131072,
compat: {
supportsDeveloperRole: false,
thinkingFormat: "zai",
zaiToolStream: true,
},
},
{
id: "glm-5.1",
name: "GLM-5.1",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 131072,
compat: {
supportsDeveloperRole: false,
thinkingFormat: "zai",
zaiToolStream: true,
},
},
{
id: "glm-5v-turbo",
name: "GLM-5V-Turbo",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 131072,
compat: {
supportsDeveloperRole: false,
thinkingFormat: "zai",
zaiToolStream: true,
},
},
{
id: "glm-5.2",
name: "GLM-5.2",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1000000,
maxTokens: 131072,
compat: {
supportsDeveloperRole: false,
thinkingFormat: "zai",
zaiToolStream: true,
},
},
],
};

View File

@@ -1336,7 +1336,10 @@ describe("createFnAgent", () => {
"/tmp",
"/tmp/.fusion/disabled-auto-extension-discovery",
);
expect(registerProviderMock).toHaveBeenCalledWith("zai", expect.objectContaining({
expect(registerProviderMock).toHaveBeenNthCalledWith(1, "zai", expect.objectContaining({
models: expect.arrayContaining([expect.objectContaining({ id: "glm-5.2" })]),
}));
expect(registerProviderMock).toHaveBeenNthCalledWith(2, "zai", expect.objectContaining({
models: [{ id: "glm-5.1" }],
}));
expect(refreshMock).toHaveBeenCalled();

View File

@@ -34,7 +34,18 @@ import {
type AgentSession,
type ToolDefinition,
} from "@earendil-works/pi-coding-agent";
import { customProviderRegistryKey, getEnabledPiExtensionPaths, getFusionAgentDir, getLegacyPiAgentDir, getProjectRootFromWorktree, reconcileClaudeCliPaths, reconcileDroidCliPaths, resolvePiExtensionProjectRoot } from "@fusion/core";
import {
customProviderRegistryKey,
getEnabledPiExtensionPaths,
getFusionAgentDir,
getLegacyPiAgentDir,
getProjectRootFromWorktree,
reconcileClaudeCliPaths,
reconcileDroidCliPaths,
resolvePiExtensionProjectRoot,
ZAI_PROVIDER_ID,
ZAI_PROVIDER_REGISTRATION,
} from "@fusion/core";
import type {
AgentPermissionPolicyActionCategory,
PermanentAgentActionCategory,
@@ -1353,6 +1364,13 @@ function resolveVendoredDroidCliEntry(): string | null {
}
async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegistry): Promise<void> {
try {
modelRegistry.registerProvider(ZAI_PROVIDER_ID, ZAI_PROVIDER_REGISTRATION);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
extensionsLog.warn(`Failed to register built-in ${ZAI_PROVIDER_ID} provider: ${message}`);
}
try {
const agentDir = getPackageManagerAgentDir();
const packageManager = new DefaultPackageManager({