Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- Docs: tighten landing-page mobile layout so hero, cards, code blocks, and nav stay readable on narrow screens (#118, thanks @Acidias).
- Release: build macOS x64 Bun artifacts and add regression coverage for Homebrew formula rewrites during dual-arch releases (#122, thanks @androidshu).
- YouTube: tighten hostname validation across core, slides, and extension helpers so attacker-controlled lookalike hosts are no longer treated as YouTube URLs (#91, thanks @RinZ27).
- Config: honor `zai.baseUrl` config fallback for blank env values and keep Z.AI base URL overrides working outside the summary flow (#102, thanks @liuy).
- Slides: warn in summary mode when `--slides` dependencies are missing, and document required local installs for `ffmpeg`, `yt-dlp`, and optional `tesseract`.
- Docs: fix broken docs index links by setting an empty Jekyll `baseurl` (#113, thanks @Youpen-y).
- Models: preserve model id casing after the provider prefix so OpenAI-compatible proxies can route exact names correctly (#128, thanks @WinnCook).
Expand Down
4 changes: 3 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,8 @@ Override API endpoints for any provider to use proxies, gateways, or compatible
"nvidia": { "baseUrl": "https://integrate.api.nvidia.com/v1" },
"anthropic": { "baseUrl": "https://my-anthropic-proxy.example.com" },
"google": { "baseUrl": "https://my-google-proxy.example.com" },
"xai": { "baseUrl": "https://my-xai-proxy.example.com" }
"xai": { "baseUrl": "https://my-xai-proxy.example.com" },
"zai": { "baseUrl": "https://api.zhipuai.cn/paas/v4" }
}
```

Expand All @@ -375,3 +376,4 @@ Or via environment variables (which take precedence over config):
| Anthropic | `ANTHROPIC_BASE_URL` |
| Google | `GOOGLE_BASE_URL` (alias: `GEMINI_BASE_URL`) |
| xAI | `XAI_BASE_URL` |
| Z.AI | `Z_AI_BASE_URL` (alias: `ZAI_BASE_URL`) |
15 changes: 15 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ export type XaiConfig = {
baseUrl?: string;
};

export type ZaiConfig = {
/**
* Override the Z.AI API base URL (e.g. use China endpoint).
*
* Default: https://api.z.ai/api/paas/v4
* China: https://api.zhipuai.cn/paas/v4
*
* Prefer env `Z_AI_BASE_URL` when you need per-run overrides.
*/
baseUrl?: string;
};

export type AutoRule = {
/**
* Input kinds this rule applies to.
Expand Down Expand Up @@ -217,6 +229,7 @@ export type SummarizeConfig = {
anthropic?: AnthropicConfig;
google?: GoogleConfig;
xai?: XaiConfig;
zai?: ZaiConfig;
logging?: LoggingConfig;
/**
* Generic environment variable defaults.
Expand Down Expand Up @@ -1163,6 +1176,7 @@ export function loadSummarizeConfig({ env }: { env: Record<string, string | unde
const anthropic = parseProviderBaseUrlConfig(parsed.anthropic, path, "anthropic");
const google = parseProviderBaseUrlConfig(parsed.google, path, "google");
const xai = parseProviderBaseUrlConfig(parsed.xai, path, "xai");
const zai = parseProviderBaseUrlConfig((parsed as Record<string, unknown>).zai, path, "zai");

const configEnv = (() => {
const value = (parsed as Record<string, unknown>).env;
Expand Down Expand Up @@ -1235,6 +1249,7 @@ export function loadSummarizeConfig({ env }: { env: Record<string, string | unde
...(anthropic ? { anthropic } : {}),
...(google ? { google } : {}),
...(xai ? { xai } : {}),
...(zai ? { zai } : {}),
...(logging ? { logging } : {}),
...(configEnv ? { env: configEnv } : {}),
...(apiKeys ? { apiKeys } : {}),
Expand Down
8 changes: 7 additions & 1 deletion src/llm/generate-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export async function generateTextWithModelId({
anthropicBaseUrlOverride,
googleBaseUrlOverride,
xaiBaseUrlOverride,
zaiBaseUrlOverride,
forceChatCompletions,
retries = 0,
onRetry,
Expand All @@ -180,6 +181,7 @@ export async function generateTextWithModelId({
anthropicBaseUrlOverride?: string | null;
googleBaseUrlOverride?: string | null;
xaiBaseUrlOverride?: string | null;
zaiBaseUrlOverride?: string | null;
forceChatCompletions?: boolean;
retries?: number;
onRetry?: (notice: RetryNotice) => void;
Expand Down Expand Up @@ -398,7 +400,11 @@ export async function generateTextWithModelId({
if (parsed.provider === "zai") {
const apiKey = apiKeys.openaiApiKey;
if (!apiKey) throw new Error("Missing Z_AI_API_KEY for zai/... model");
const model = resolveZaiModel({ modelId: parsed.model, context, openaiBaseUrlOverride });
const model = resolveZaiModel({
modelId: parsed.model,
context,
openaiBaseUrlOverride: zaiBaseUrlOverride ?? openaiBaseUrlOverride,
});
const result = await completeSimpleText({ model, apiKey, signal: controller.signal });
return {
text: result.text,
Expand Down
19 changes: 11 additions & 8 deletions src/run/run-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,21 @@ export function resolveEnvState({
envValue: envForRun.XAI_BASE_URL,
configValue: configForCli?.xai?.baseUrl,
});
const zaiBaseUrl = resolveConfiguredBaseUrl({
envValue:
typeof envForRun.Z_AI_BASE_URL === "string"
? envForRun.Z_AI_BASE_URL
: typeof envForRun.ZAI_BASE_URL === "string"
? envForRun.ZAI_BASE_URL
: null,
configValue: configForCli?.zai?.baseUrl,
});
const zaiKeyRaw =
typeof envForRun.Z_AI_API_KEY === "string"
? envForRun.Z_AI_API_KEY
: typeof envForRun.ZAI_API_KEY === "string"
? envForRun.ZAI_API_KEY
: null;
const zaiBaseUrlRaw =
typeof envForRun.Z_AI_BASE_URL === "string"
? envForRun.Z_AI_BASE_URL
: typeof envForRun.ZAI_BASE_URL === "string"
? envForRun.ZAI_BASE_URL
: null;
const openRouterKeyRaw =
typeof envForRun.OPENROUTER_API_KEY === "string" ? envForRun.OPENROUTER_API_KEY : null;
const openaiKeyRaw =
Expand Down Expand Up @@ -127,7 +130,7 @@ export function resolveEnvState({
const firecrawlConfigured = firecrawlApiKey !== null;
const xaiApiKey = xaiKeyRaw?.trim() ?? null;
const zaiApiKey = zaiKeyRaw?.trim() ?? null;
const zaiBaseUrl = (zaiBaseUrlRaw?.trim() ?? "") || "https://api.z.ai/api/paas/v4";
const zaiBaseUrlEffective = (zaiBaseUrl?.trim() ?? "") || "https://api.z.ai/api/paas/v4";
const nvidiaApiKey = nvidiaKeyRaw?.trim() ?? null;
const nvidiaBaseUrlEffective =
(nvidiaBaseUrl?.trim() ?? "") || "https://integrate.api.nvidia.com/v1";
Expand Down Expand Up @@ -167,7 +170,7 @@ export function resolveEnvState({
googleApiKey,
anthropicApiKey,
zaiApiKey,
zaiBaseUrl,
zaiBaseUrl: zaiBaseUrlEffective,
nvidiaApiKey,
nvidiaBaseUrl: nvidiaBaseUrlEffective,
firecrawlApiKey,
Expand Down
3 changes: 3 additions & 0 deletions src/run/summary-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ export function createSummaryEngine(deps: SummaryEngineDeps) {
anthropicBaseUrlOverride: deps.providerBaseUrls.anthropic,
googleBaseUrlOverride: deps.providerBaseUrls.google,
xaiBaseUrlOverride: deps.providerBaseUrls.xai,
zaiBaseUrlOverride: deps.zai.baseUrl,
forceChatCompletions,
retries: deps.retries,
onRetry: createRetryLogger({
Expand Down Expand Up @@ -383,6 +384,7 @@ export function createSummaryEngine(deps: SummaryEngineDeps) {
anthropicBaseUrlOverride: deps.providerBaseUrls.anthropic,
googleBaseUrlOverride: deps.providerBaseUrls.google,
xaiBaseUrlOverride: deps.providerBaseUrls.xai,
zaiBaseUrlOverride: deps.zai.baseUrl,
forceChatCompletions,
retries: deps.retries,
onRetry: createRetryLogger({
Expand Down Expand Up @@ -424,6 +426,7 @@ export function createSummaryEngine(deps: SummaryEngineDeps) {
anthropicBaseUrlOverride: deps.providerBaseUrls.anthropic,
googleBaseUrlOverride: deps.providerBaseUrls.google,
xaiBaseUrlOverride: deps.providerBaseUrls.xai,
zaiBaseUrlOverride: deps.zai.baseUrl,
retries: deps.retries,
onRetry: createRetryLogger({
stderr: deps.stderr,
Expand Down
3 changes: 3 additions & 0 deletions src/run/summary-llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export async function summarizeWithModelId({
anthropicBaseUrlOverride,
googleBaseUrlOverride,
xaiBaseUrlOverride,
zaiBaseUrlOverride,
forceChatCompletions,
retries,
onRetry,
Expand All @@ -72,6 +73,7 @@ export async function summarizeWithModelId({
anthropicBaseUrlOverride?: string | null;
googleBaseUrlOverride?: string | null;
xaiBaseUrlOverride?: string | null;
zaiBaseUrlOverride?: string | null;
forceChatCompletions?: boolean;
retries: number;
onRetry?: (notice: {
Expand All @@ -94,6 +96,7 @@ export async function summarizeWithModelId({
anthropicBaseUrlOverride,
googleBaseUrlOverride,
xaiBaseUrlOverride,
zaiBaseUrlOverride,
forceChatCompletions,
prompt,
temperature: 0,
Expand Down
7 changes: 7 additions & 0 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ describe("config loading", () => {
anthropic: { baseUrl: "https://anthropic-proxy.example.com" },
google: { baseUrl: "https://google-proxy.example.com" },
xai: { baseUrl: "https://xai-proxy.example.com" },
zai: { baseUrl: "https://api.zhipuai.cn/paas/v4" },
});
const result = loadSummarizeConfig({ env: { HOME: root } });
expect(result.config).toEqual({
Expand All @@ -532,6 +533,7 @@ describe("config loading", () => {
anthropic: { baseUrl: "https://anthropic-proxy.example.com" },
google: { baseUrl: "https://google-proxy.example.com" },
xai: { baseUrl: "https://xai-proxy.example.com" },
zai: { baseUrl: "https://api.zhipuai.cn/paas/v4" },
});
});

Expand All @@ -548,16 +550,21 @@ describe("config loading", () => {

const { root: root3 } = writeJsonConfig({ xai: [] });
expect(() => loadSummarizeConfig({ env: { HOME: root3 } })).toThrow(/"xai" must be an object/i);

const { root: root4 } = writeJsonConfig({ zai: 123 });
expect(() => loadSummarizeConfig({ env: { HOME: root4 } })).toThrow(/"zai" must be an object/i);
});

it("trims provider baseUrl strings and ignores empty strings", () => {
const { root } = writeJsonConfig({
openai: { baseUrl: " https://example.com/v1 " },
anthropic: { baseUrl: " " },
zai: { baseUrl: " https://api.zhipuai.cn/paas/v4 " },
});
const result = loadSummarizeConfig({ env: { HOME: root } });
expect(result.config).toEqual({
openai: { baseUrl: "https://example.com/v1" },
zai: { baseUrl: "https://api.zhipuai.cn/paas/v4" },
});
});
});
21 changes: 20 additions & 1 deletion tests/llm.generate-text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ describe("llm generate/stream", () => {
expect(model.headers?.["X-Title"]).toBe("summarize");
});

it("applies provider baseUrl overrides (google/xai)", async () => {
it("applies provider baseUrl overrides (google/xai/zai)", async () => {
mocks.completeSimple.mockClear();

await generateTextWithModelId({
Expand Down Expand Up @@ -633,6 +633,25 @@ describe("llm generate/stream", () => {

const xaiModel = mocks.completeSimple.mock.calls[0]?.[0] as { baseUrl?: string };
expect(xaiModel.baseUrl).toBe("https://xai-proxy.example.com/v1");

mocks.completeSimple.mockClear();
await generateTextWithModelId({
modelId: "zai/glm-4.7",
apiKeys: {
openaiApiKey: "k",
openrouterApiKey: null,
xaiApiKey: null,
googleApiKey: null,
anthropicApiKey: null,
},
prompt: { userText: "hi" },
timeoutMs: 2000,
fetchImpl: globalThis.fetch.bind(globalThis),
openaiBaseUrlOverride: "https://zai-proxy.example.com/v4",
});

const zaiModel = mocks.completeSimple.mock.calls[0]?.[0] as { baseUrl?: string };
expect(zaiModel.baseUrl).toBe("https://zai-proxy.example.com/v4");
});

it("wraps anthropic model access errors with a helpful message", async () => {
Expand Down
19 changes: 19 additions & 0 deletions tests/run-env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import type { SummarizeConfig } from "../src/config.js";
import { resolveEnvState } from "../src/run/run-env.js";

describe("run env", () => {
it("falls back to config zai.baseUrl when env is blank", () => {
const configForCli: SummarizeConfig = {
zai: { baseUrl: "https://api.zhipuai.cn/paas/v4" },
};

const state = resolveEnvState({
env: {},
envForRun: { Z_AI_BASE_URL: " " },
configForCli,
});

expect(state.zaiBaseUrl).toBe("https://api.zhipuai.cn/paas/v4");
});
});