Merge pull request #344 from default-anton/feature/zai-openai-migration

Migrate zai provider from Anthropic to OpenAI-compatible API
This commit is contained in:
Mario Zechner 2025-12-29 13:37:43 +01:00 committed by GitHub
commit 557af13042
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 50 additions and 41 deletions

View file

@ -260,28 +260,34 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
// Process xAi models
if (data.zai?.models) {
for (const [modelId, model] of Object.entries(data.zai.models)) {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;
for (const [modelId, model] of Object.entries(data.zai.models)) {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;
const supportsImage = m.modalities?.input?.includes("image")
models.push({
id: modelId,
name: m.name || modelId,
api: "anthropic-messages",
provider: "zai",
baseUrl: "https://api.z.ai/api/anthropic",
reasoning: m.reasoning === true,
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
cost: {
input: m.cost?.input || 0,
output: m.cost?.output || 0,
cacheRead: m.cost?.cache_read || 0,
cacheWrite: m.cost?.cache_write || 0,
},
contextWindow: m.limit?.context || 4096,
maxTokens: m.limit?.output || 4096,
});
}
models.push({
id: modelId,
name: m.name || modelId,
api: supportsImage ? "openai-completions" : "anthropic-messages",
provider: "zai",
baseUrl: supportsImage ? "https://api.z.ai/api/coding/paas/v4" : "https://api.z.ai/api/anthropic",
reasoning: m.reasoning === true,
input: supportsImage ? ["text", "image"] : ["text"],
cost: {
input: m.cost?.input || 0,
output: m.cost?.output || 0,
cacheRead: m.cost?.cache_read || 0,
cacheWrite: m.cost?.cache_write || 0,
},
...(supportsImage ? {
compat: {
supportsDeveloperRole: false,
},
} : {}),
contextWindow: m.limit?.context || 4096,
maxTokens: m.limit?.output || 4096,
});
}
}
// Process Mistral models

View file

@ -7029,9 +7029,10 @@ export const MODELS = {
"glm-4.5v": {
id: "glm-4.5v",
name: "GLM-4.5V",
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", "image"],
cost: {
@ -7042,7 +7043,7 @@ export const MODELS = {
},
contextWindow: 64000,
maxTokens: 16384,
} satisfies Model<"anthropic-messages">,
} satisfies Model<"openai-completions">,
"glm-4.6": {
id: "glm-4.6",
name: "GLM-4.6",
@ -7063,9 +7064,10 @@ export const MODELS = {
"glm-4.6v": {
id: "glm-4.6v",
name: "GLM-4.6V",
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", "image"],
cost: {
@ -7076,7 +7078,7 @@ export const MODELS = {
},
contextWindow: 128000,
maxTokens: 32768,
} satisfies Model<"anthropic-messages">,
} satisfies Model<"openai-completions">,
"glm-4.7": {
id: "glm-4.7",
name: "GLM-4.7",

View file

@ -474,10 +474,14 @@ function convertMessages(
// Handle thinking blocks
const thinkingBlocks = msg.content.filter((b) => b.type === "thinking") as ThinkingContent[];
if (thinkingBlocks.length > 0) {
// Filter out empty thinking blocks to avoid API validation errors
const nonEmptyThinkingBlocks = thinkingBlocks.filter((b) => b.thinking && b.thinking.trim().length > 0);
if (nonEmptyThinkingBlocks.length > 0) {
if (compat.requiresThinkingAsText) {
// Convert thinking blocks to text with <thinking> delimiters
const thinkingText = thinkingBlocks.map((b) => `<thinking>\n${b.thinking}\n</thinking>`).join("\n");
const thinkingText = nonEmptyThinkingBlocks
.map((b) => `<thinking>\n${b.thinking}\n</thinking>`)
.join("\n");
const textContent = assistantMsg.content as Array<{ type: "text"; text: string }> | null;
if (textContent) {
textContent.unshift({ type: "text", text: thinkingText });
@ -486,9 +490,9 @@ function convertMessages(
}
} else {
// Use the signature from the first thinking block if available (for llama.cpp server + gpt-oss)
const signature = thinkingBlocks[0].thinkingSignature;
const signature = nonEmptyThinkingBlocks[0].thinkingSignature;
if (signature && signature.length > 0) {
(assistantMsg as any)[signature] = thinkingBlocks.map((b) => b.thinking).join("\n");
(assistantMsg as any)[signature] = nonEmptyThinkingBlocks.map((b) => b.thinking).join("\n");
}
}
}

View file

@ -571,9 +571,8 @@ describe("Generate E2E Tests", () => {
await handleStreaming(llm);
});
it("should handle thinking", { retry: 3 }, async () => {
// Prompt doesn't trigger thinking
// await handleThinking(llm, { thinkingEnabled: true, thinkingBudgetTokens: 2048 });
it.skip("should handle thinking mode", { retry: 3 }, async () => {
await handleThinking(llm, { thinkingEnabled: true, thinkingBudgetTokens: 2048 });
});
it("should handle multi-turn with thinking and tools", { retry: 3 }, async () => {
@ -581,7 +580,7 @@ describe("Generate E2E Tests", () => {
});
});
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider (glm-4.5v via Anthropic Messages)", () => {
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider (glm-4.5v via OpenAI Completions)", () => {
const llm = getModel("zai", "glm-4.5v");
it("should complete basic text generation", { retry: 3 }, async () => {
@ -596,18 +595,16 @@ describe("Generate E2E Tests", () => {
await handleStreaming(llm);
});
it("should handle thinking", { retry: 3 }, async () => {
// Prompt doesn't trigger thinking
// await handleThinking(llm, { thinkingEnabled: true, thinkingBudgetTokens: 2048 });
it("should handle thinking mode", { retry: 3 }, async () => {
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" });
});
it("should handle image input", { retry: 3 }, async () => {
// Can't see image for some reason?
// await handleImage(llm);
await handleImage(llm);
});
});