FN-6414: preserve built-in Z.ai models after extensions
Keep GLM-5.2 visible when user Z.ai extensions replace the provider registration. - Add shared helpers to clone and register the built-in Z.ai provider safely. - Re-merge missing built-in Z.ai models after extension provider registration in daemon, serve, dashboard, and pi flows. - Cover provider replacement behavior in core and dashboard model route tests, and document the extension-preserving behavior. Files changed: .changeset/FN-6414-glm-5-2-visible.md | 5 ++ docs/settings-reference.md | 2 +- packages/cli/src/commands/daemon.ts | 12 +-- packages/cli/src/commands/dashboard.ts | 12 +-- packages/cli/src/commands/serve.ts | 12 +-- packages/core/src/__tests__/zai-provider.test.ts | 33 +++++++- packages/core/src/index.ts | 7 +- packages/core/src/zai-provider.ts | 93 +++++++++++++++++++++ ...register-model-routes-zai-real-registry.test.ts | 97 ++++++++++++++++++++++ packages/engine/src/pi.ts | 12 +-- 10 files changed, 250 insertions(+), 35 deletions(-) Fusion-Task-Id: FN-6414 Fusion-Task-Lineage: f0990da2-968d-4782-8c00-fbc350d6954f
This commit is contained in:
5
.changeset/FN-6414-glm-5-2-visible.md
Normal file
5
.changeset/FN-6414-glm-5-2-visible.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Ensure `zai/glm-5.2` reliably appears in the model list after user Z.ai provider extensions load.
|
||||||
@@ -801,7 +801,7 @@ 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.
|
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.
|
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. If a pi extension also registers the `zai` provider, Fusion preserves the extension's models and re-adds any missing built-in Z.ai models so built-in GLM choices remain available.
|
||||||
|
|
||||||
### Planning model
|
### Planning model
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import {
|
|||||||
GlobalSettingsStore,
|
GlobalSettingsStore,
|
||||||
resolveGlobalDir,
|
resolveGlobalDir,
|
||||||
getEnabledPiExtensionPaths,
|
getEnabledPiExtensionPaths,
|
||||||
|
mergeBuiltInZaiProviderModels,
|
||||||
reconcileClaudeCliPaths,
|
reconcileClaudeCliPaths,
|
||||||
ZAI_PROVIDER_ID,
|
registerBuiltInZaiProvider,
|
||||||
ZAI_PROVIDER_REGISTRATION,
|
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import type { AutomationRunResult, ScheduledTask } from "@fusion/core";
|
import type { AutomationRunResult, ScheduledTask } from "@fusion/core";
|
||||||
import { createServer, GitHubClient, createSkillsAdapter, getProjectSettingsPath, loadTlsCredentialsFromEnv, registerGithubTrackingHook } from "@fusion/dashboard";
|
import { createServer, GitHubClient, createSkillsAdapter, getProjectSettingsPath, loadTlsCredentialsFromEnv, registerGithubTrackingHook } from "@fusion/dashboard";
|
||||||
@@ -554,12 +554,7 @@ export async function runDaemon(opts: DaemonOptions = {}) {
|
|||||||
]);
|
]);
|
||||||
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
|
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
|
||||||
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
|
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
|
||||||
try {
|
registerBuiltInZaiProvider(modelRegistry, (message) => console.log(`[extensions] ${message}`));
|
||||||
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);
|
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
|
||||||
|
|
||||||
// PackageManager may be used for skills adapter even if extension loading fails
|
// PackageManager may be used for skills adapter even if extension loading fails
|
||||||
@@ -674,6 +669,7 @@ export async function runDaemon(opts: DaemonOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extensionsResult.runtime.pendingProviderRegistrations = [];
|
extensionsResult.runtime.pendingProviderRegistrations = [];
|
||||||
|
mergeBuiltInZaiProviderModels(modelRegistry, (message) => console.log(`[extensions] ${message}`));
|
||||||
modelRegistry.refresh();
|
modelRegistry.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import {
|
|||||||
isWorkflowColumnsEnabled,
|
isWorkflowColumnsEnabled,
|
||||||
resolveColumnFlags,
|
resolveColumnFlags,
|
||||||
BUILTIN_CODING_WORKFLOW_IR,
|
BUILTIN_CODING_WORKFLOW_IR,
|
||||||
|
mergeBuiltInZaiProviderModels,
|
||||||
parseWorkflowIr,
|
parseWorkflowIr,
|
||||||
ZAI_PROVIDER_ID,
|
registerBuiltInZaiProvider,
|
||||||
ZAI_PROVIDER_REGISTRATION,
|
|
||||||
type WorkflowIrColumn,
|
type WorkflowIrColumn,
|
||||||
type TraitFlags,
|
type TraitFlags,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
@@ -1371,12 +1371,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
|||||||
]);
|
]);
|
||||||
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
|
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
|
||||||
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
|
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
|
||||||
try {
|
registerBuiltInZaiProvider(modelRegistry, (message) => logSink.log(message, "extensions"));
|
||||||
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);
|
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
|
||||||
|
|
||||||
// PackageManager may be used for skills adapter even if extension loading fails.
|
// PackageManager may be used for skills adapter even if extension loading fails.
|
||||||
@@ -1496,6 +1491,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
|||||||
}
|
}
|
||||||
|
|
||||||
extensionsResult.runtime.pendingProviderRegistrations = [];
|
extensionsResult.runtime.pendingProviderRegistrations = [];
|
||||||
|
mergeBuiltInZaiProviderModels(modelRegistry, (message) => logSink.log(message, "extensions"));
|
||||||
modelRegistry.refresh();
|
modelRegistry.refresh();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import {
|
|||||||
GlobalSettingsStore,
|
GlobalSettingsStore,
|
||||||
resolveGlobalDir,
|
resolveGlobalDir,
|
||||||
getEnabledPiExtensionPaths,
|
getEnabledPiExtensionPaths,
|
||||||
ZAI_PROVIDER_ID,
|
mergeBuiltInZaiProviderModels,
|
||||||
ZAI_PROVIDER_REGISTRATION,
|
registerBuiltInZaiProvider,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import type { AutomationRunResult, ScheduledTask } from "@fusion/core";
|
import type { AutomationRunResult, ScheduledTask } from "@fusion/core";
|
||||||
import { createServer, GitHubClient, createSkillsAdapter, getProjectSettingsPath, loadTlsCredentialsFromEnv, registerGithubTrackingHook } from "@fusion/dashboard";
|
import { createServer, GitHubClient, createSkillsAdapter, getProjectSettingsPath, loadTlsCredentialsFromEnv, registerGithubTrackingHook } from "@fusion/dashboard";
|
||||||
@@ -604,12 +604,7 @@ export async function runServe(
|
|||||||
]);
|
]);
|
||||||
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
|
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [supplementalAuthStorage]);
|
||||||
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
|
const modelRegistry = ModelRegistry.create(mergedAuthStorage, getModelRegistryModelsPath());
|
||||||
try {
|
registerBuiltInZaiProvider(modelRegistry, (message) => console.log(`[extensions] ${message}`));
|
||||||
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);
|
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
|
||||||
|
|
||||||
// PackageManager may be used for skills adapter even if extension loading fails
|
// PackageManager may be used for skills adapter even if extension loading fails
|
||||||
@@ -725,6 +720,7 @@ export async function runServe(
|
|||||||
}
|
}
|
||||||
|
|
||||||
extensionsResult.runtime.pendingProviderRegistrations = [];
|
extensionsResult.runtime.pendingProviderRegistrations = [];
|
||||||
|
mergeBuiltInZaiProviderModels(modelRegistry, (message) => console.log(`[extensions] ${message}`));
|
||||||
modelRegistry.refresh();
|
modelRegistry.refresh();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { ZAI_PROVIDER_ID, ZAI_PROVIDER_REGISTRATION } from "../zai-provider.js";
|
import {
|
||||||
|
mergeBuiltInZaiProviderModels,
|
||||||
|
registerBuiltInZaiProvider,
|
||||||
|
ZAI_PROVIDER_ID,
|
||||||
|
ZAI_PROVIDER_REGISTRATION,
|
||||||
|
} from "../zai-provider.js";
|
||||||
|
|
||||||
const EXISTING_ZAI_MODELS = [
|
const EXISTING_ZAI_MODELS = [
|
||||||
"glm-4.5-air",
|
"glm-4.5-air",
|
||||||
@@ -45,4 +50,30 @@ describe("ZAI_PROVIDER_REGISTRATION", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("re-merges missing built-in models after a user zai extension replacement", () => {
|
||||||
|
const extensionModels = ZAI_PROVIDER_REGISTRATION.models
|
||||||
|
.filter((model) => model.id !== "glm-5.2")
|
||||||
|
.map((model) => ({ ...model }));
|
||||||
|
const registeredProviders = new Map<string, Partial<typeof ZAI_PROVIDER_REGISTRATION>>();
|
||||||
|
const registry = {
|
||||||
|
registeredProviders,
|
||||||
|
registerProvider(providerName: string, config: typeof ZAI_PROVIDER_REGISTRATION) {
|
||||||
|
registeredProviders.set(providerName, { ...registeredProviders.get(providerName), ...config });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerBuiltInZaiProvider(registry);
|
||||||
|
registry.registerProvider(ZAI_PROVIDER_ID, {
|
||||||
|
...ZAI_PROVIDER_REGISTRATION,
|
||||||
|
name: "User ZAI extension",
|
||||||
|
models: extensionModels,
|
||||||
|
});
|
||||||
|
|
||||||
|
mergeBuiltInZaiProviderModels(registry);
|
||||||
|
|
||||||
|
const mergedIds = registeredProviders.get(ZAI_PROVIDER_ID)?.models?.map((model) => model.id);
|
||||||
|
expect(mergedIds).toEqual([...EXISTING_ZAI_MODELS, "glm-5.2"]);
|
||||||
|
expect(registeredProviders.get(ZAI_PROVIDER_ID)?.name).toBe("User ZAI extension");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ export { redactSecrets } from "./redact-secrets.js";
|
|||||||
export * from "./frontend-ux-policy.js";
|
export * from "./frontend-ux-policy.js";
|
||||||
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";
|
export { MOCK_PROVIDER_ID } from "./mock-provider-constants.js";
|
||||||
export type { MockProviderId, MockSessionPurpose } 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 {
|
||||||
|
ZAI_PROVIDER_ID,
|
||||||
|
ZAI_PROVIDER_REGISTRATION,
|
||||||
|
mergeBuiltInZaiProviderModels,
|
||||||
|
registerBuiltInZaiProvider,
|
||||||
|
} from "./zai-provider.js";
|
||||||
export type { ZaiProviderRegistration } from "./zai-provider.js";
|
export type { ZaiProviderRegistration } from "./zai-provider.js";
|
||||||
export {
|
export {
|
||||||
resolveWorktrunkSettings,
|
resolveWorktrunkSettings,
|
||||||
|
|||||||
@@ -123,3 +123,96 @@ export const ZAI_PROVIDER_REGISTRATION: ZaiProviderRegistration = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ZaiModelLike = Partial<Omit<ZaiModelRegistration, "name" | "api" | "baseUrl" | "compat">> & {
|
||||||
|
id: string;
|
||||||
|
name?: unknown;
|
||||||
|
provider?: string;
|
||||||
|
baseUrl?: unknown;
|
||||||
|
api?: unknown;
|
||||||
|
compat?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ZaiModelRegistryLike {
|
||||||
|
registerProvider(providerName: string, config: ZaiProviderRegistration): void;
|
||||||
|
getAll?: () => ZaiModelLike[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegistryWithProviderState = ZaiModelRegistryLike & {
|
||||||
|
registeredProviders?: Map<string, Partial<ZaiProviderRegistration>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function toZaiModelRegistration(model: ZaiModelLike): ZaiModelRegistration & { baseUrl?: string; api?: string } {
|
||||||
|
return {
|
||||||
|
id: model.id,
|
||||||
|
name: String(model.name ?? model.id),
|
||||||
|
api: typeof model.api === "string" ? model.api : undefined,
|
||||||
|
baseUrl: typeof model.baseUrl === "string" ? model.baseUrl : undefined,
|
||||||
|
reasoning: model.reasoning === true,
|
||||||
|
input: Array.isArray(model.input) ? model.input as ZaiModelInput[] : ["text"],
|
||||||
|
cost: model.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
|
contextWindow: Number(model.contextWindow ?? 0),
|
||||||
|
maxTokens: Number(model.maxTokens ?? 0),
|
||||||
|
compat: typeof model.compat === "object" && model.compat !== null
|
||||||
|
? { ...(model.compat as ZaiModelRegistration["compat"]) }
|
||||||
|
: ZAI_PROVIDER_REGISTRATION.models.find((builtInModel) => builtInModel.id === model.id)?.compat ?? {
|
||||||
|
supportsDeveloperRole: false,
|
||||||
|
thinkingFormat: "zai",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneZaiProviderRegistration(config: ZaiProviderRegistration): ZaiProviderRegistration {
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
models: config.models.map((model) => toZaiModelRegistration(model)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FNXC:ModelRegistry 2026-06-13-22:04:
|
||||||
|
* pi's registerProvider() treats a provider config with models as a full provider replacement, and user extensions load after Fusion's built-in provider registration.
|
||||||
|
* Re-merge missing built-in Z.ai models after extension registration so zai/glm-5.2 remains visible wherever the user's existing Z.ai extension models are visible, without deleting extension-supplied models.
|
||||||
|
* Always pass cloned configs because pi stores and mutates registered provider objects during later upserts.
|
||||||
|
*/
|
||||||
|
export function registerBuiltInZaiProvider(
|
||||||
|
modelRegistry: ZaiModelRegistryLike,
|
||||||
|
logWarning: (message: string) => void = () => {},
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
modelRegistry.registerProvider(ZAI_PROVIDER_ID, cloneZaiProviderRegistration(ZAI_PROVIDER_REGISTRATION));
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
logWarning(`Failed to register built-in ${ZAI_PROVIDER_ID} provider: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeBuiltInZaiProviderModels(
|
||||||
|
modelRegistry: ZaiModelRegistryLike,
|
||||||
|
logWarning: (message: string) => void = () => {},
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
const registryWithState = modelRegistry as RegistryWithProviderState;
|
||||||
|
const registeredProvider = registryWithState.registeredProviders?.get(ZAI_PROVIDER_ID);
|
||||||
|
if (!registeredProvider && !modelRegistry.getAll) return;
|
||||||
|
const registeredModels = registeredProvider?.models?.map((model) => toZaiModelRegistration(model)) ?? [];
|
||||||
|
const currentModels = registeredModels.length > 0
|
||||||
|
? registeredModels
|
||||||
|
: modelRegistry.getAll?.()
|
||||||
|
.filter((model) => model.provider === ZAI_PROVIDER_ID)
|
||||||
|
.map((model) => toZaiModelRegistration(model)) ?? [];
|
||||||
|
const currentModelIds = new Set(currentModels.map((model) => model.id));
|
||||||
|
const missingBuiltInModels = ZAI_PROVIDER_REGISTRATION.models.filter((model) => !currentModelIds.has(model.id));
|
||||||
|
|
||||||
|
if (missingBuiltInModels.length === 0) return;
|
||||||
|
|
||||||
|
modelRegistry.registerProvider(ZAI_PROVIDER_ID, {
|
||||||
|
...cloneZaiProviderRegistration(ZAI_PROVIDER_REGISTRATION),
|
||||||
|
...registeredProvider,
|
||||||
|
models: [...currentModels, ...missingBuiltInModels.map((model) => toZaiModelRegistration(model))],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
logWarning(`Failed to merge built-in ${ZAI_PROVIDER_ID} models: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
||||||
|
import {
|
||||||
|
mergeBuiltInZaiProviderModels,
|
||||||
|
registerBuiltInZaiProvider,
|
||||||
|
ZAI_PROVIDER_REGISTRATION,
|
||||||
|
} from "@fusion/core";
|
||||||
|
import type { Router } from "express";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { registerModelRoutes } from "../routes/register-model-routes.js";
|
||||||
|
|
||||||
|
const EXISTING_ZAI_MODELS = [
|
||||||
|
"glm-4.5-air",
|
||||||
|
"glm-4.7",
|
||||||
|
"glm-5-turbo",
|
||||||
|
"glm-5.1",
|
||||||
|
"glm-5v-turbo",
|
||||||
|
];
|
||||||
|
|
||||||
|
async function withTempHome() {
|
||||||
|
const originalHome = process.env.HOME;
|
||||||
|
const home = await mkdtemp(join(tmpdir(), "fusion-zai-models-"));
|
||||||
|
const authDir = join(home, ".fusion", "agent");
|
||||||
|
await mkdir(authDir, { recursive: true });
|
||||||
|
await writeFile(join(authDir, "auth.json"), JSON.stringify({ zai: { type: "api_key", key: "test-zai-key" } }));
|
||||||
|
process.env.HOME = home;
|
||||||
|
return () => {
|
||||||
|
if (originalHome === undefined) {
|
||||||
|
delete process.env.HOME;
|
||||||
|
} else {
|
||||||
|
process.env.HOME = originalHome;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRouterHarness(modelRegistry: ModelRegistry) {
|
||||||
|
const getHandlers = new Map<string, (req: unknown, res: { json: (body: unknown) => void }) => Promise<void>>();
|
||||||
|
const router = {
|
||||||
|
get: vi.fn((path: string, handler: (req: unknown, res: { json: (body: unknown) => void }) => Promise<void>) => {
|
||||||
|
getHandlers.set(path, handler);
|
||||||
|
}),
|
||||||
|
} as unknown as Router;
|
||||||
|
const store = {
|
||||||
|
getGlobalSettingsStore: () => ({ getSettings: vi.fn().mockResolvedValue({}) }),
|
||||||
|
getSettingsFast: vi.fn().mockResolvedValue({}),
|
||||||
|
};
|
||||||
|
const runtimeLogger = { child: vi.fn(() => ({ warn: vi.fn() })) };
|
||||||
|
|
||||||
|
registerModelRoutes({
|
||||||
|
router,
|
||||||
|
store: store as never,
|
||||||
|
runtimeLogger: runtimeLogger as never,
|
||||||
|
options: { modelRegistry },
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
return getHandlers.get("/models")!;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("registerModelRoutes Z.ai real registry", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("surfaces glm-5.2 through /api/models after a user zai extension replacement", async () => {
|
||||||
|
const restoreHome = await withTempHome();
|
||||||
|
try {
|
||||||
|
const authStorage = AuthStorage.inMemory({ zai: { type: "api_key", key: "test-zai-key" } });
|
||||||
|
const modelRegistry = ModelRegistry.inMemory(authStorage);
|
||||||
|
registerBuiltInZaiProvider(modelRegistry);
|
||||||
|
|
||||||
|
modelRegistry.registerProvider("zai", {
|
||||||
|
...ZAI_PROVIDER_REGISTRATION,
|
||||||
|
name: "User ZAI extension",
|
||||||
|
models: ZAI_PROVIDER_REGISTRATION.models.filter((model) => model.id !== "glm-5.2"),
|
||||||
|
});
|
||||||
|
expect(modelRegistry.getAvailable().some((model) => model.provider === "zai" && model.id === "glm-5.2")).toBe(false);
|
||||||
|
|
||||||
|
mergeBuiltInZaiProviderModels(modelRegistry);
|
||||||
|
modelRegistry.refresh();
|
||||||
|
|
||||||
|
const allZaiIds = modelRegistry.getAll().filter((model) => model.provider === "zai").map((model) => model.id);
|
||||||
|
expect(allZaiIds).toEqual([...EXISTING_ZAI_MODELS, "glm-5.2"]);
|
||||||
|
|
||||||
|
const handler = createRouterHarness(modelRegistry);
|
||||||
|
const json = vi.fn();
|
||||||
|
await handler({}, { json });
|
||||||
|
|
||||||
|
const response = json.mock.calls[0][0] as { models: Array<{ provider: string; id: string }> };
|
||||||
|
const zaiIds = response.models.filter((model) => model.provider === "zai").map((model) => model.id);
|
||||||
|
expect(zaiIds).toEqual([...EXISTING_ZAI_MODELS, "glm-5.2"]);
|
||||||
|
} finally {
|
||||||
|
restoreHome();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -42,9 +42,9 @@ import {
|
|||||||
getProjectRootFromWorktree,
|
getProjectRootFromWorktree,
|
||||||
reconcileClaudeCliPaths,
|
reconcileClaudeCliPaths,
|
||||||
reconcileDroidCliPaths,
|
reconcileDroidCliPaths,
|
||||||
|
mergeBuiltInZaiProviderModels,
|
||||||
|
registerBuiltInZaiProvider,
|
||||||
resolvePiExtensionProjectRoot,
|
resolvePiExtensionProjectRoot,
|
||||||
ZAI_PROVIDER_ID,
|
|
||||||
ZAI_PROVIDER_REGISTRATION,
|
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import type {
|
import type {
|
||||||
AgentPermissionPolicyActionCategory,
|
AgentPermissionPolicyActionCategory,
|
||||||
@@ -1364,12 +1364,7 @@ function resolveVendoredDroidCliEntry(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegistry): Promise<void> {
|
async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegistry): Promise<void> {
|
||||||
try {
|
registerBuiltInZaiProvider(modelRegistry, (message) => extensionsLog.warn(message));
|
||||||
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 {
|
try {
|
||||||
const agentDir = getPackageManagerAgentDir();
|
const agentDir = getPackageManagerAgentDir();
|
||||||
@@ -1423,6 +1418,7 @@ async function registerExtensionProviders(cwd: string, modelRegistry: ModelRegis
|
|||||||
}
|
}
|
||||||
|
|
||||||
extensionsResult.runtime.pendingProviderRegistrations = [];
|
extensionsResult.runtime.pendingProviderRegistrations = [];
|
||||||
|
mergeBuiltInZaiProviderModels(modelRegistry, (message) => extensionsLog.warn(message));
|
||||||
modelRegistry.refresh();
|
modelRegistry.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
|||||||
Reference in New Issue
Block a user