From 0250b7ac03414012aff3c595997de0ad3e58beb1 Mon Sep 17 00:00:00 2001 From: Anton Kuzmenko <1917237+default-anton@users.noreply.github.com> Date: Mon, 29 Dec 2025 11:54:10 -0800 Subject: [PATCH] Migrate zai provider from Anthropic to OpenAI-compatible API - Migrate glm-4.5, glm-4.5-air, glm-4.5-flash, glm-4.6, glm-4.7 from anthropic-messages to openai-completions API - Updated baseUrl from https://api.z.ai/api/anthropic to https://api.z.ai/api/coding/paas/v4 - Added compat setting to disable developer role for zai models - Filter empty text blocks in openai-completions to avoid zai API validation errors - Fixed zai provider tests to use OpenAI-style options (reasoningEffort) --- packages/ai/scripts/generate-models.ts | 14 ++++---- packages/ai/src/models.generated.ts | 35 +++++++++++-------- .../ai/src/providers/openai-completions.ts | 8 +++-- packages/ai/test/stream.test.ts | 6 ++-- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index dd51a992..13acb42e 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -258,7 +258,7 @@ async function loadModelsDevData(): Promise[]> { } } - // Process xAi models + // Process zAi models if (data.zai?.models) { for (const [modelId, model] of Object.entries(data.zai.models)) { const m = model as ModelsDevModel; @@ -268,9 +268,9 @@ async function loadModelsDevData(): Promise[]> { models.push({ id: modelId, name: m.name || modelId, - api: supportsImage ? "openai-completions" : "anthropic-messages", + api: "openai-completions", provider: "zai", - baseUrl: supportsImage ? "https://api.z.ai/api/coding/paas/v4" : "https://api.z.ai/api/anthropic", + baseUrl: "https://api.z.ai/api/coding/paas/v4", reasoning: m.reasoning === true, input: supportsImage ? ["text", "image"] : ["text"], cost: { @@ -279,11 +279,9 @@ async function loadModelsDevData(): Promise[]> { cacheRead: m.cost?.cache_read || 0, cacheWrite: m.cost?.cache_write || 0, }, - ...(supportsImage ? { - compat: { - supportsDeveloperRole: false, - }, - } : {}), + compat: { + supportsDeveloperRole: false, + }, contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 4fd67bf1..6cde070c 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -6978,9 +6978,10 @@ export const MODELS = { "glm-4.5": { id: "glm-4.5", name: "GLM-4.5", - api: "anthropic-messages", + api: "openai-completions", provider: "zai", - baseUrl: "https://api.z.ai/api/anthropic", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false}, reasoning: true, input: ["text"], cost: { @@ -6991,13 +6992,14 @@ export const MODELS = { }, contextWindow: 131072, maxTokens: 98304, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "glm-4.5-air": { id: "glm-4.5-air", name: "GLM-4.5-Air", - api: "anthropic-messages", + api: "openai-completions", provider: "zai", - baseUrl: "https://api.z.ai/api/anthropic", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false}, reasoning: true, input: ["text"], cost: { @@ -7008,13 +7010,14 @@ export const MODELS = { }, contextWindow: 131072, maxTokens: 98304, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "glm-4.5-flash": { id: "glm-4.5-flash", name: "GLM-4.5-Flash", - api: "anthropic-messages", + api: "openai-completions", provider: "zai", - baseUrl: "https://api.z.ai/api/anthropic", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false}, reasoning: true, input: ["text"], cost: { @@ -7025,7 +7028,7 @@ export const MODELS = { }, contextWindow: 131072, maxTokens: 98304, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "glm-4.5v": { id: "glm-4.5v", name: "GLM-4.5V", @@ -7047,9 +7050,10 @@ export const MODELS = { "glm-4.6": { id: "glm-4.6", name: "GLM-4.6", - api: "anthropic-messages", + api: "openai-completions", provider: "zai", - baseUrl: "https://api.z.ai/api/anthropic", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false}, reasoning: true, input: ["text"], cost: { @@ -7060,7 +7064,7 @@ export const MODELS = { }, contextWindow: 204800, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, "glm-4.6v": { id: "glm-4.6v", name: "GLM-4.6V", @@ -7082,9 +7086,10 @@ export const MODELS = { "glm-4.7": { id: "glm-4.7", name: "GLM-4.7", - api: "anthropic-messages", + api: "openai-completions", provider: "zai", - baseUrl: "https://api.z.ai/api/anthropic", + baseUrl: "https://api.z.ai/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false}, reasoning: true, input: ["text"], cost: { @@ -7095,6 +7100,6 @@ export const MODELS = { }, contextWindow: 204800, maxTokens: 131072, - } satisfies Model<"anthropic-messages">, + } satisfies Model<"openai-completions">, }, } as const; diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index e2d74cb3..e13c0e89 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -460,13 +460,15 @@ function convertMessages( }; const textBlocks = msg.content.filter((b) => b.type === "text") as TextContent[]; - if (textBlocks.length > 0) { + // Filter out empty text blocks to avoid API validation errors + const nonEmptyTextBlocks = textBlocks.filter((b) => b.text && b.text.trim().length > 0); + if (nonEmptyTextBlocks.length > 0) { // GitHub Copilot requires assistant content as a string, not an array. // Sending as array causes Claude models to re-answer all previous prompts. if (model.provider === "github-copilot") { - assistantMsg.content = textBlocks.map((b) => sanitizeSurrogates(b.text)).join(""); + assistantMsg.content = nonEmptyTextBlocks.map((b) => sanitizeSurrogates(b.text)).join(""); } else { - assistantMsg.content = textBlocks.map((b) => { + assistantMsg.content = nonEmptyTextBlocks.map((b) => { return { type: "text", text: sanitizeSurrogates(b.text) }; }); } diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 6389f49b..29d9111f 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -556,7 +556,7 @@ describe("Generate E2E Tests", () => { }); }); - describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider (glm-4.5-air via Anthropic Messages)", () => { + describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider (glm-4.5-air via OpenAI Completions)", () => { const llm = getModel("zai", "glm-4.5-air"); it("should complete basic text generation", { retry: 3 }, async () => { @@ -572,11 +572,11 @@ describe("Generate E2E Tests", () => { }); it.skip("should handle thinking mode", { retry: 3 }, async () => { - await handleThinking(llm, { thinkingEnabled: true, thinkingBudgetTokens: 2048 }); + await handleThinking(llm, { reasoningEffort: "medium" }); }); it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => { - await multiTurn(llm, { thinkingEnabled: true, thinkingBudgetTokens: 2048 }); + await multiTurn(llm, { reasoningEffort: "medium" }); }); });