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:
5
.changeset/FN-6401-glm-5-2.md
Normal file
5
.changeset/FN-6401-glm-5-2.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
Enable Z.ai GLM-5.2 model selection.
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
48
packages/core/src/__tests__/zai-provider.test.ts
Normal file
48
packages/core/src/__tests__/zai-provider.test.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
125
packages/core/src/zai-provider.ts
Normal file
125
packages/core/src/zai-provider.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user