diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 5a00ea9cd..d8c4640da 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -2380,6 +2380,8 @@ Notes: - `z.ai/*` and `z-ai/*` are accepted aliases and normalize to `zai/*`. - If `ZAI_API_KEY` is missing, requests to `zai/*` will fail with an auth error at runtime. - Example error: `No API key found for provider "zai".` +- Optional: set `ZAI_BASE_URL` to override the `zai` provider endpoint without editing config. + (Legacy alias: `Z_AI_BASE_URL`.) Example for Chinese Z.AI endpoint: `ZAI_BASE_URL=https://open.bigmodel.cn/api/paas/v4/` - Z.AI’s general API endpoint is `https://api.z.ai/api/paas/v4`. GLM coding requests use the dedicated Coding endpoint `https://api.z.ai/api/coding/paas/v4`. The built-in `zai` provider uses the Coding endpoint. If you need the general diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 0cd034c82..e07f5b552 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -86,6 +86,17 @@ const OLLAMA_DEFAULT_COST = { cacheWrite: 0, }; +function normalizeProviderBaseUrl(url: string): string { + return url.trim().replace(/\/+$/, ""); +} + +function resolveZaiBaseUrlEnv(env: NodeJS.ProcessEnv): string | null { + // Keep a legacy alias to mirror Z_AI_API_KEY support. + const raw = env.ZAI_BASE_URL?.trim() || env.Z_AI_BASE_URL?.trim() || ""; + if (!raw) return null; + return normalizeProviderBaseUrl(raw); +} + interface OllamaModel { name: string; modified_at: string; @@ -392,6 +403,12 @@ export async function resolveImplicitProviders(params: { agentDir: string; }): Promise { const providers: Record = {}; + const zaiBaseUrl = resolveZaiBaseUrlEnv(process.env); + if (zaiBaseUrl) { + // Override baseUrl for pi-ai built-in z.ai models without redefining models. + // This matches the "models: [] means baseUrl override only" pattern used for Copilot. + providers.zai = { baseUrl: zaiBaseUrl, models: [] }; + } const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, }); diff --git a/src/agents/models-config.providers.zai-base-url.test.ts b/src/agents/models-config.providers.zai-base-url.test.ts new file mode 100644 index 000000000..b15740275 --- /dev/null +++ b/src/agents/models-config.providers.zai-base-url.test.ts @@ -0,0 +1,64 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js"; +import type { MoltbotConfig } from "../config/config.js"; + +async function withTempHome(fn: (home: string) => Promise): Promise { + return withTempHomeBase(fn, { prefix: "moltbot-zai-baseurl-" }); +} + +describe("models-config: ZAI_BASE_URL", () => { + let prevHome: string | undefined; + let prevZaiBaseUrl: string | undefined; + let prevZaiLegacyBaseUrl: string | undefined; + + beforeEach(() => { + prevHome = process.env.HOME; + prevZaiBaseUrl = process.env.ZAI_BASE_URL; + prevZaiLegacyBaseUrl = process.env.Z_AI_BASE_URL; + }); + + afterEach(() => { + process.env.HOME = prevHome; + if (prevZaiBaseUrl === undefined) delete process.env.ZAI_BASE_URL; + else process.env.ZAI_BASE_URL = prevZaiBaseUrl; + if (prevZaiLegacyBaseUrl === undefined) delete process.env.Z_AI_BASE_URL; + else process.env.Z_AI_BASE_URL = prevZaiLegacyBaseUrl; + }); + + it("writes providers.zai.baseUrl from ZAI_BASE_URL (normalized)", async () => { + await withTempHome(async () => { + vi.resetModules(); + process.env.ZAI_BASE_URL = "https://proxy.example.test/v1/"; + delete process.env.Z_AI_BASE_URL; + + const { ensureMoltbotModelsJson } = await import("./models-config.js"); + const { resolveMoltbotAgentDir } = await import("./agent-paths.js"); + + await ensureMoltbotModelsJson({} as MoltbotConfig); + + const raw = await fs.readFile(path.join(resolveMoltbotAgentDir(), "models.json"), "utf8"); + const parsed = JSON.parse(raw) as { providers?: Record }; + expect(parsed.providers?.zai?.baseUrl).toBe("https://proxy.example.test/v1"); + }); + }); + + it("supports legacy Z_AI_BASE_URL when ZAI_BASE_URL is unset", async () => { + await withTempHome(async () => { + vi.resetModules(); + delete process.env.ZAI_BASE_URL; + process.env.Z_AI_BASE_URL = "http://localhost:9999/v1/"; + + const { ensureMoltbotModelsJson } = await import("./models-config.js"); + const { resolveMoltbotAgentDir } = await import("./agent-paths.js"); + + await ensureMoltbotModelsJson({} as MoltbotConfig); + + const raw = await fs.readFile(path.join(resolveMoltbotAgentDir(), "models.json"), "utf8"); + const parsed = JSON.parse(raw) as { providers?: Record }; + expect(parsed.providers?.zai?.baseUrl).toBe("http://localhost:9999/v1"); + }); + }); +});