diff --git a/CHANGELOG.md b/CHANGELOG.md index d017ef798..7a4141152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Features - Talk mode: continuous speech conversations (macOS/iOS/Android) with ElevenLabs TTS, reply directives, and optional interrupt-on-speech. - UI: add optional `ui.seamColor` accent to tint the Talk Mode side bubble (macOS/iOS/Android). +- Agent runtime: accept legacy `Z_AI_API_KEY` for Z.AI provider auth (maps to `ZAI_API_KEY`). +- Tests: add a Z.AI live test gate for smoke validation when keys are present. ### Fixes - Docs/agent tools: clarify that browser `wait` should be avoided by default and used only in exceptional cases. diff --git a/docs/configuration.md b/docs/configuration.md index 91f1baddf..cd7801fba 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -259,6 +259,8 @@ Controls the embedded agent runtime (model/thinking/verbose/timeouts). If `modelAliases` is configured, you may also use the alias key (e.g. `Opus`). If you omit the provider, CLAWDIS currently assumes `anthropic` as a temporary deprecation fallback. +Z.AI models are available as `zai/` (e.g. `zai/glm-4.7`) and require +`ZAI_API_KEY` (or legacy `Z_AI_API_KEY`) in the environment. `agent.heartbeat` configures periodic heartbeat runs: - `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Omit or set diff --git a/src/agents/zai.live.test.ts b/src/agents/zai.live.test.ts new file mode 100644 index 000000000..d5f2424bd --- /dev/null +++ b/src/agents/zai.live.test.ts @@ -0,0 +1,31 @@ +import { completeSimple, getModel } from "@mariozechner/pi-ai"; +import { describe, expect, it } from "vitest"; + +const ZAI_KEY = process.env.ZAI_API_KEY ?? process.env.Z_AI_API_KEY ?? ""; +const LIVE = process.env.ZAI_LIVE_TEST === "1" || process.env.LIVE === "1"; + +const describeLive = LIVE && ZAI_KEY ? describe : describe.skip; + +describeLive("zai live", () => { + it("returns assistant text", async () => { + const model = getModel("zai", "glm-4.7"); + const res = await completeSimple( + model, + { + messages: [ + { + role: "user", + content: "Reply with the word ok.", + timestamp: Date.now(), + }, + ], + }, + { apiKey: ZAI_KEY, maxTokens: 64 }, + ); + const text = res.content + .filter((block) => block.type === "text") + .map((block) => block.text.trim()) + .join(" "); + expect(text.length).toBeGreaterThan(0); + }); +}); diff --git a/src/index.ts b/src/index.ts index 2c7a38762..b3bfd7c30 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ import { saveSessionStore, } from "./config/sessions.js"; import { ensureBinary } from "./infra/binaries.js"; +import { normalizeEnv } from "./infra/env.js"; import { isMainModule } from "./infra/is-main.js"; import { ensureClawdisCliOnPath } from "./infra/path-env.js"; import { @@ -32,6 +33,7 @@ import { monitorWebProvider } from "./provider-web.js"; import { assertProvider, normalizeE164, toWhatsappJid } from "./utils.js"; dotenv.config({ quiet: true }); +normalizeEnv(); ensureClawdisCliOnPath(); // Capture all console output into structured logs while keeping stdout/stderr behavior. diff --git a/src/infra/env.test.ts b/src/infra/env.test.ts new file mode 100644 index 000000000..30c97d3e8 --- /dev/null +++ b/src/infra/env.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeZaiEnv } from "./env.js"; + +describe("normalizeZaiEnv", () => { + it("copies Z_AI_API_KEY to ZAI_API_KEY when missing", () => { + const prevZai = process.env.ZAI_API_KEY; + const prevZAi = process.env.Z_AI_API_KEY; + process.env.ZAI_API_KEY = ""; + process.env.Z_AI_API_KEY = "zai-legacy"; + + normalizeZaiEnv(); + + expect(process.env.ZAI_API_KEY).toBe("zai-legacy"); + + if (prevZai === undefined) delete process.env.ZAI_API_KEY; + else process.env.ZAI_API_KEY = prevZai; + if (prevZAi === undefined) delete process.env.Z_AI_API_KEY; + else process.env.Z_AI_API_KEY = prevZAi; + }); + + it("does not override existing ZAI_API_KEY", () => { + const prevZai = process.env.ZAI_API_KEY; + const prevZAi = process.env.Z_AI_API_KEY; + process.env.ZAI_API_KEY = "zai-current"; + process.env.Z_AI_API_KEY = "zai-legacy"; + + normalizeZaiEnv(); + + expect(process.env.ZAI_API_KEY).toBe("zai-current"); + + if (prevZai === undefined) delete process.env.ZAI_API_KEY; + else process.env.ZAI_API_KEY = prevZai; + if (prevZAi === undefined) delete process.env.Z_AI_API_KEY; + else process.env.Z_AI_API_KEY = prevZAi; + }); +}); diff --git a/src/infra/env.ts b/src/infra/env.ts new file mode 100644 index 000000000..8e51c5a9f --- /dev/null +++ b/src/infra/env.ts @@ -0,0 +1,9 @@ +export function normalizeZaiEnv(): void { + if (!process.env.ZAI_API_KEY?.trim() && process.env.Z_AI_API_KEY?.trim()) { + process.env.ZAI_API_KEY = process.env.Z_AI_API_KEY; + } +} + +export function normalizeEnv(): void { + normalizeZaiEnv(); +}