Reviewed PR description, comments, commits, and changed files.
UnderstandChecked linked issues and relevant source files.
UnderstandIdentified provider rename causing detection break.
UnderstandVerified changelog entry placement and classification.
UnderstandYou are given one or more GitHub PR URLs: https://github.com/badlogic/pi-mono/pull/899/changes
For each PR URL, do the following in order:
packages/*/CHANGELOG.md files. Report whether an entry exists. If missing, state that a changelog entry is required before merge and that you will add it if the user decides to merge. Follow the changelog format rules in AGENTS.md. Verify:
### Breaking Changes, ### Added, ### Fixed, etc.)Fixed foo ([#123](https://github.com/badlogic/pi-mono/pull/123) by [@user](https://github.com/user))### Breaking Changes, not just ### FixedOutput format per PR: PR: Changelog:
If no issues are found, say so under Bad and Ugly.
>gh pr view 899 --repo badlogic/pi-mono --json title,body,comments,commits,labels,state,author,headRefName,baseRefName{"author":{"id":"MDQ6VXNlcjIyMzYyNQ==","is_bot":false,"login":"kaofelix","name":"Kao Félix"},"baseRefName":"main","body":"Opening this as a draft here just to discuss a few points.\r\n\r\n@assagman I will pull your doc changes in here too and adapt them to whatever becomes the final idea here.\r\n\r\nBasically what I find annoying is that z.ai API keys work for both API and Coding Plan. The only difference is the endpoint itself. The way it currently is, whenever someone has `ZAI_API_KEY` defined, they will have both providers available, which is a bit noisy for what I assume to be the most cases of people only using one option.\r\n\r\nThe ideas that occurred to me were:\r\n\r\n1. Introduce a different environment var e.g. `ZAI_CODING_API_KEY` so that you know which provider you are configuring. The only caveat is that every other tool I encountered uses `ZAI_API_KEY` for the config\r\n2. Replace the `zai` provider entirely with `zai-coding-plan`\r\n\r\nI personally prefer 2 as it keeps things simpler while making it more aligned with models.dev and being explicit that we are supporting the Coding Plan specifically and not the regular API. So far, pi only worked with the coding plan anyways, since it always used coding plan endpoints. I can add examples to the docs on how to setup the regular API by hand. \r\n\r\nFrom the fact that it has always been Coding Plan and no one complained, I assume there are not many API users out there. API users tend to prefer other providers and even avoid zai, from what I've seen.","comments":[{"id":"IC_kwDOPbFNk87hgmc9","author":{"login":"assagman"},"authorAssociation":"NONE","body":"Hey @kaofelix , thank you!\r\n\r\nI also find it a bit confusing that z-ai team's decided that one api key can be used for both. Anyways, +1 for option (2) here. It's fine as soon as it's aligned with `models.dev`","createdAt":"2026-01-22T09:32:46Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/899#issuecomment-3783419709","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87hja5i","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"I'm OK with 2 as well! Which means we entirely rip out the zai provider. Can you amend the PR accordingly? Also needs a breaking changes changelog entry in that case.","createdAt":"2026-01-22T12:34:18Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/899#issuecomment-3784158818","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87hr__A","author":{"login":"kaofelix"},"authorAssociation":"CONTRIBUTOR","body":"@badlogic cool, made all the changes! Did a quick test with GLM flash here with thinking on and off to make sure the thinking param still works: https://buildwithpi.ai/session/#2cc3a9696a480de2262751a35e692215\r\n\r\nOops, actually forgot about the breaking change changelog, will do that now","createdAt":"2026-01-22T19:57:42Z","includesCreatedEdit":true,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/899#issuecomment-3786407872","viewerDidAuthor":false}],"commits":[{"authoredDate":"2026-01-22T08:36:59Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjIyMzYyNQ==","login":"kaofelix","name":"Kao Félix"}],"committedDate":"2026-01-22T08:36:59Z","messageBody":"","messageHeadline":"feat(ai): add zai-coding-plan provider and make zai point to regular API","oid":"b02b6a5c4691a6bc837f1bf46a9420b2c1e83c88"},{"authoredDate":"2026-01-20T13:20:45Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjEyOTk4MjEx","login":"assagman","name":"assagman"}],"committedDate":"2026-01-22T08:47:25Z","messageBody":"Add documentation for the new Z.AI GLM Coding Plan provider,\nincluding auth configuration, environment variables, and model\ndetails (GLM-4.7, GLM-4.6, GLM-4.5 and vision variants).\n\nSigned-off-by: assagman <[REDACTED]>","messageHeadline":"docs(coding-agent): add Z.AI provider documentation","oid":"cdb817d61e4ab0fe55832585fe427d701207394b"},{"authoredDate":"2026-01-22T19:37:19Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjIyMzYyNQ==","login":"kaofelix","name":"Kao Félix"}],"committedDate":"2026-01-22T19:37:19Z","messageBody":"","messageHeadline":"replace zai with zai-coding-plan","oid":"c2459e463ee881ba6e5994d8799e9a11380d9d78"},{"authoredDate":"2026-01-22T19:51:54Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjIyMzYyNQ==","login":"kaofelix","name":"Kao Félix"}],"committedDate":"2026-01-22T19:51:54Z","messageBody":"","messageHeadline":"simplify documentation for zai coding plan","oid":"af60ba5fd552a353d49e99f194a9087988ae7d90"},{"authoredDate":"2026-01-22T19:59:15Z","authors":[{"email":"[REDACTED]","id":"MDQ6VXNlcjIyMzYyNQ==","login":"kaofelix","name":"Kao Félix"}],"committedDate":"2026-01-22T19:59:15Z","messageBody":"","messageHeadline":"update changelog","oid":"6f10c0a37d794b60c7da74456b580d4fe801684d"}],"headRefName":"zai-coding-plan-provider","labels":[],"state":"OPEN","title":"Add zai-coding-plan provider and make zai point to regular API"}>gh pr diff 899 --repo badlogic/pi-monodiff --git a/packages/agent/test/e2e.test.ts b/packages/agent/test/e2e.test.ts
index 075de86c8..57e691d3c 100644
--- a/packages/agent/test/e2e.test.ts
+++ b/packages/agent/test/e2e.test.ts
@@ -302,8 +302,8 @@ describe("Agent E2E Tests", () => {
});
});
- describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider (glm-4.5-air)", () => {
- const model = getModel("zai", "glm-4.5-air");
+ describe.skipIf(!process.env.ZAI_API_KEY)("zAI Coding Plan Provider (glm-4.5-air)", () => {
+ const model = getModel("zai-coding-plan", "glm-4.5-air");
it("should handle basic text prompt", async () => {
await basicPrompt(model);
diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md
index 7ecdc53aa..09bb48fb7 100644
--- a/packages/ai/CHANGELOG.md
+++ b/packages/ai/CHANGELOG.md
@@ -2,6 +2,10 @@
## [Unreleased]
+### Breaking Changes
+
+- Renamed `zai` provider to `zai-coding-plan` to align with naming in models.dev and be explicit about which endpoint we use
+
## [0.49.3] - 2026-01-22
### Added
diff --git a/packages/ai/README.md b/packages/ai/README.md
index c033da383..313636124 100644
--- a/packages/ai/README.md
+++ b/packages/ai/README.md
@@ -55,6 +55,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an
- **Groq**
- **Cerebras**
- **xAI**
+- **zAI Coding Plan**
- **OpenRouter**
- **Vercel AI Gateway**
- **MiniMax**
@@ -883,7 +884,7 @@ In Node.js environments, you can set environment variables to avoid passing API
| xAI | `XAI_API_KEY` |
| OpenRouter | `OPENROUTER_API_KEY` |
| Vercel AI Gateway | `AI_GATEWAY_API_KEY` |
-| zAI | `ZAI_API_KEY` |
+| zAI Coding Plan | `ZAI_API_KEY` |
| MiniMax | `MINIMAX_API_KEY` |
| GitHub Copilot | `COPILOT_GITHUB_TOKEN` or `GH_TOKEN` or `GITHUB_TOKEN` |
diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts
index 41e34de45..f13ff9a0e 100644
--- a/packages/ai/scripts/generate-models.ts
+++ b/packages/ai/scripts/generate-models.ts
@@ -417,33 +417,33 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
}
}
- // Process zAi models
- if (data.zai?.models) {
- for (const [modelId, model] of Object.entries(data.zai.models)) {
+ // Process zAi Coding Plan models
+ if (data["zai-coding-plan"]?.models) {
+ for (const [modelId, model] of Object.entries(data["zai-coding-plan"].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: "openai-completions",
- provider: "zai",
- baseUrl: "https://api.z.ai/api/coding/paas/v4",
- 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,
- },
- compat: {
- supportsDeveloperRole: false,
- thinkingFormat: "zai",
- },
- contextWindow: m.limit?.context || 4096,
- maxTokens: m.limit?.output || 4096,
+ id: modelId,
+ name: m.name || modelId,
+ api: "openai-completions",
+ provider: "zai-coding-plan",
+ baseUrl: "https://api.z.ai/api/coding/paas/v4",
+ 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,
+ },
+ compat: {
+ supportsDeveloperRole: false,
+ thinkingFormat: "zai",
+ },
+ 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 f41f8c41c..23abedede 100644
--- a/packages/ai/src/models.generated.ts
+++ b/packages/ai/src/models.generated.ts
@@ -4374,7 +4374,7 @@ export const MODELS = {
input: ["text"],
cost: {
input: 0.09,
- output: 0.39999999999999997,
+ output: 0.44999999999999996,
cacheRead: 0,
cacheWrite: 0,
},
@@ -5056,7 +5056,7 @@ export const MODELS = {
input: 0.09999999999999999,
output: 0.39999999999999997,
cacheRead: 0.024999999999999998,
- cacheWrite: 0.0833,
+ cacheWrite: 0.08333333333333334,
},
contextWindow: 1048576,
maxTokens: 8192,
@@ -5124,7 +5124,7 @@ export const MODELS = {
input: 0.09999999999999999,
output: 0.39999999999999997,
cacheRead: 0.01,
- cacheWrite: 0.0833,
+ cacheWrite: 0.08333333333333334,
},
contextWindow: 1048576,
maxTokens: 65535,
@@ -5141,7 +5141,7 @@ export const MODELS = {
input: 0.09999999999999999,
output: 0.39999999999999997,
cacheRead: 0.01,
- cacheWrite: 0.0833,
+ cacheWrite: 0.08333333333333334,
},
contextWindow: 1048576,
maxTokens: 65535,
@@ -5158,7 +5158,7 @@ export const MODELS = {
input: 0.3,
output: 2.5,
cacheRead: 0.03,
- cacheWrite: 0.0833,
+ cacheWrite: 0.08333333333333334,
},
contextWindow: 1048576,
maxTokens: 65535,
@@ -7271,23 +7271,6 @@ export const MODELS = {
contextWindow: 131072,
maxTokens: 8192,
} satisfies Model<"openai-completions">,
- "qwen/qwen2.5-vl-72b-instruct": {
- id: "qwen/qwen2.5-vl-72b-instruct",
- name: "Qwen: Qwen2.5 VL 72B Instruct",
- api: "openai-completions",
- provider: "openrouter",
- baseUrl: "https://openrouter.ai/api/v1",
- reasoning: false,
- input: ["text", "image"],
- cost: {
- input: 0.15,
- output: 0.6,
- cacheRead: 0,
- cacheWrite: 0,
- },
- contextWindow: 32768,
- maxTokens: 32768,
- } satisfies Model<"openai-completions">,
"qwen/qwen3-14b": {
id: "qwen/qwen3-14b",
name: "Qwen: Qwen3 14B",
@@ -8372,7 +8355,7 @@ export const MODELS = {
cost: {
input: 1,
output: 5,
- cacheRead: 0,
+ cacheRead: 0.19999999999999998,
cacheWrite: 0,
},
contextWindow: 1000000,
@@ -10208,7 +10191,7 @@ export const MODELS = {
cost: {
input: 0.19999999999999998,
output: 1.1,
- cacheRead: 0,
+ cacheRead: 0.03,
cacheWrite: 0,
},
contextWindow: 128000,
@@ -10693,20 +10676,20 @@ export const MODELS = {
maxTokens: 4096,
} satisfies Model<"openai-completions">,
},
- "zai": {
+ "zai-coding-plan": {
"glm-4.5": {
id: "glm-4.5",
name: "GLM-4.5",
api: "openai-completions",
- provider: "zai",
+ provider: "zai-coding-plan",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
reasoning: true,
input: ["text"],
cost: {
- input: 0.6,
- output: 2.2,
- cacheRead: 0.11,
+ input: 0,
+ output: 0,
+ cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
@@ -10716,15 +10699,15 @@ export const MODELS = {
id: "glm-4.5-air",
name: "GLM-4.5-Air",
api: "openai-completions",
- provider: "zai",
+ provider: "zai-coding-plan",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
reasoning: true,
input: ["text"],
cost: {
- input: 0.2,
- output: 1.1,
- cacheRead: 0.03,
+ input: 0,
+ output: 0,
+ cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
@@ -10734,7 +10717,7 @@ export const MODELS = {
id: "glm-4.5-flash",
name: "GLM-4.5-Flash",
api: "openai-completions",
- provider: "zai",
+ provider: "zai-coding-plan",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
reasoning: true,
@@ -10752,14 +10735,14 @@ export const MODELS = {
id: "glm-4.5v",
name: "GLM-4.5V",
api: "openai-completions",
- provider: "zai",
+ provider: "zai-coding-plan",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
reasoning: true,
input: ["text", "image"],
cost: {
- input: 0.6,
- output: 1.8,
+ input: 0,
+ output: 0,
cacheRead: 0,
cacheWrite: 0,
},
@@ -10770,15 +10753,15 @@ export const MODELS = {
id: "glm-4.6",
name: "GLM-4.6",
api: "openai-completions",
- provider: "zai",
+ provider: "zai-coding-plan",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
reasoning: true,
input: ["text"],
cost: {
- input: 0.6,
- output: 2.2,
- cacheRead: 0.11,
+ input: 0,
+ output: 0,
+ cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 204800,
@@ -10788,14 +10771,14 @@ export const MODELS = {
id: "glm-4.6v",
name: "GLM-4.6V",
api: "openai-completions",
- provider: "zai",
+ provider: "zai-coding-plan",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
reasoning: true,
input: ["text", "image"],
cost: {
- input: 0.3,
- output: 0.9,
+ input: 0,
+ output: 0,
cacheRead: 0,
cacheWrite: 0,
},
@@ -10806,19 +10789,37 @@ export const MODELS = {
id: "glm-4.7",
name: "GLM-4.7",
api: "openai-completions",
- provider: "zai",
+ provider: "zai-coding-plan",
baseUrl: "https://api.z.ai/api/coding/paas/v4",
compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
reasoning: true,
input: ["text"],
cost: {
- input: 0.6,
- output: 2.2,
- cacheRead: 0.11,
+ input: 0,
+ output: 0,
+ cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 204800,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
+ "glm-4.7-flash": {
+ id: "glm-4.7-flash",
+ name: "GLM-4.7-Flash",
+ api: "openai-completions",
+ provider: "zai-coding-plan",
+ baseUrl: "https://api.z.ai/api/coding/paas/v4",
+ compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
+ reasoning: true,
+ input: ["text"],
+ cost: {
+ input: 0,
+ output: 0,
+ cacheRead: 0,
+ cacheWrite: 0,
+ },
+ contextWindow: 200000,
+ maxTokens: 131072,
+ } 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 42576f0fa..737fa7c5a 100644
--- a/packages/ai/src/providers/openai-completions.ts
+++ b/packages/ai/src/providers/openai-completions.ts
@@ -102,7 +102,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
const client = createClient(model, context, apiKey, options?.headers);
const params = buildParams(model, context, options);
options?.onPayload?.(params);
- const openaiStream = await client.chat.completions.create(params, { signal: options?.signal });
+ const openaiStream = await client.chat.completions.create(params, {
+ signal: options?.signal,
+ });
stream.push({ type: "start", partial: output });
let currentBlock: TextContent | ThinkingContent | (ToolCall & { partialArgs?: string }) | null = null;
@@ -180,7 +182,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
finishCurrentBlock(currentBlock);
currentBlock = { type: "text", text: "" };
output.content.push(currentBlock);
- stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
+ stream.push({
+ type: "text_start",
+ contentIndex: blockIndex(),
+ partial: output,
+ });
}
if (currentBlock.type === "text") {
@@ -222,7 +228,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
thinkingSignature: foundReasoningField,
};
output.content.push(currentBlock);
- stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
+ stream.push({
+ type: "thinking_start",
+ contentIndex: blockIndex(),
+ partial: output,
+ });
}
if (currentBlock.type === "thinking") {
@@ -253,7 +263,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
partialArgs: "",
};
output.content.push(currentBlock);
- stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
+ stream.push({
+ type: "toolcall_start",
+ contentIndex: blockIndex(),
+ partial: output,
+ });
}
if (currentBlock.type === "toolCall") {
@@ -417,7 +431,9 @@ function buildParams(model: Model<"openai-completions">, context: Context, optio
if (compat.thinkingFormat === "zai" && model.reasoning) {
// Z.ai uses binary thinking: { type: "enabled" | "disabled" }
// Must explicitly disable since z.ai defaults to thinking enabled
- (params as any).thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" };
+ (params as any).thinking = {
+ type: options?.reasoningEffort ? "enabled" : "disabled",
+ };
} else if (options?.reasoningEffort && model.reasoning && compat.supportsReasoningEffort) {
// OpenAI-style reasoning_effort
params.reasoning_effort = options.reasoningEffort;
@@ -481,7 +497,10 @@ function convertMessages(
if (context.systemPrompt) {
const useDeveloperRole = model.reasoning && compat.supportsDeveloperRole;
const role = useDeveloperRole ? "developer" : "system";
- params.push({ role: role, content: sanitizeSurrogates(context.systemPrompt) });
+ params.push({
+ role: role,
+ content: sanitizeSurrogates(context.systemPrompt),
+ });
}
let lastRole: string | null = null;
@@ -557,7 +576,10 @@ function convertMessages(
if (compat.requiresThinkingAsText) {
// Convert thinking blocks to plain text (no tags to avoid model mimicking them)
const thinkingText = nonEmptyThinkingBlocks.map((b) => b.thinking).join("\n\n");
- const textContent = assistantMsg.content as Array<{ type: "text"; text: string }> | null;
+ const textContent = assistantMsg.content as Array<{
+ type: "text";
+ text: string;
+ }> | null;
if (textContent) {
textContent.unshift({ type: "text", text: thinkingText });
} else {
diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts
index c3804617d..94129fdff 100644
--- a/packages/ai/src/stream.ts
+++ b/packages/ai/src/stream.ts
@@ -124,7 +124,7 @@ export function getEnvApiKey(provider: any): string | undefined {
xai: "XAI_API_KEY",
openrouter: "OPENROUTER_API_KEY",
"vercel-ai-gateway": "AI_GATEWAY_API_KEY",
- zai: "ZAI_API_KEY",
+ "zai-coding-plan": "ZAI_API_KEY",
mistral: "MISTRAL_API_KEY",
minimax: "MINIMAX_API_KEY",
"minimax-cn": "MINIMAX_CN_API_KEY",
diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts
index 4463ec5f8..65290849e 100644
--- a/packages/ai/src/types.ts
+++ b/packages/ai/src/types.ts
@@ -57,7 +57,7 @@ export type KnownProvider =
| "cerebras"
| "openrouter"
| "vercel-ai-gateway"
- | "zai"
+ | "zai-coding-plan"
| "mistral"
| "minimax"
| "minimax-cn"
diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts
index fae10364d..c9a942ef9 100644
--- a/packages/ai/test/context-overflow.test.ts
+++ b/packages/ai/test/context-overflow.test.ts
@@ -361,7 +361,7 @@ describe("Context overflow error handling", () => {
describe.skipIf(!process.env.ZAI_API_KEY)("z.ai", () => {
it("glm-4.5-flash - should detect overflow via isContextOverflow (silent overflow or rate limit)", async () => {
- const model = getModel("zai", "glm-4.5-flash");
+ const model = getModel("zai-coding-plan", "glm-4.5-flash");
const result = await testContextOverflow(model, process.env.ZAI_API_KEY!);
logResult(result);
diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts
index 12415f6c7..f7c95635f 100644
--- a/packages/ai/test/empty.test.ts
+++ b/packages/ai/test/empty.test.ts
@@ -283,7 +283,7 @@ describe("AI Providers Empty Message Tests", () => {
});
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider Empty Messages", () => {
- const llm = getModel("zai", "glm-4.5-air");
+ const llm = getModel("zai-coding-plan", "glm-4.5-air");
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
await testEmptyMessage(llm);
diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts
index 2a140292a..db54bc33e 100644
--- a/packages/ai/test/stream.test.ts
+++ b/packages/ai/test/stream.test.ts
@@ -688,7 +688,7 @@ describe("Generate E2E Tests", () => {
);
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider (glm-4.5-air via OpenAI Completions)", () => {
- const llm = getModel("zai", "glm-4.5-air");
+ const llm = getModel("zai-coding-plan", "glm-4.5-air");
it("should complete basic text generation", { retry: 3 }, async () => {
await basicTextGeneration(llm);
@@ -712,7 +712,35 @@ describe("Generate E2E Tests", () => {
});
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider (glm-4.5v via OpenAI Completions)", () => {
- const llm = getModel("zai", "glm-4.5v");
+ const llm = getModel("zai-coding-plan", "glm-4.5v");
+
+ it("should complete basic text generation", { retry: 3 }, async () => {
+ await basicTextGeneration(llm);
+ });
+
+ it("should handle tool calling", { retry: 3 }, async () => {
+ await handleToolCall(llm);
+ });
+
+ it("should handle streaming", { retry: 3 }, async () => {
+ await handleStreaming(llm);
+ });
+
+ 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, { reasoningEffort: "medium" });
+ });
+
+ it("should handle image input", { retry: 3 }, async () => {
+ await handleImage(llm);
+ });
+ });
+
+ describe.skipIf(!process.env.ZAI_API_KEY)("zAI Coding Plan Provider (glm-4.5v via OpenAI Completions)", () => {
+ const llm = getModel("zai-coding-plan", "glm-4.5v");
it("should complete basic text generation", { retry: 3 }, async () => {
await basicTextGeneration(llm);
diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts
index dc897bb58..f358c3fec 100644
--- a/packages/ai/test/tokens.test.ts
+++ b/packages/ai/test/tokens.test.ts
@@ -54,7 +54,7 @@ async function testTokensOnAbort<TApi extends Api>(llm: Model<TApi>, options: Op
llm.api === "openai-responses" ||
llm.api === "openai-codex-responses" ||
llm.provider === "google-gemini-cli" ||
- llm.provider === "zai" ||
+ llm.provider === "zai-coding-plan" ||
llm.provider === "amazon-bedrock" ||
llm.provider === "vercel-ai-gateway" ||
(llm.provider === "google-antigravity" && llm.id.includes("gpt-oss"))
@@ -140,7 +140,15 @@ describe("Token Statistics on Abort", () => {
});
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider", () => {
- const llm = getModel("zai", "glm-4.5-flash");
+ const llm = getModel("zai-coding-plan", "glm-4.5-flash");
+
+ it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => {
+ await testTokensOnAbort(llm);
+ });
+ });
+
+ describe.skipIf(!process.env.ZAI_API_KEY)("zAI Coding Plan Provider", () => {
+ const llm = getModel("zai-coding-plan", "glm-4.5-flash");
it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => {
await testTokensOnAbort(llm);
diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts
index 8cc61e90a..16d6f608a 100644
--- a/packages/ai/test/tool-call-without-result.test.ts
+++ b/packages/ai/test/tool-call-without-result.test.ts
@@ -158,7 +158,7 @@ describe("Tool Call Without Result Tests", () => {
});
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider", () => {
- const model = getModel("zai", "glm-4.5-flash");
+ const model = getModel("zai-coding-plan", "glm-4.5-flash");
it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => {
await testToolCallWithoutResult(model);
diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts
index b0a40ebd2..dba08239c 100644
--- a/packages/ai/test/total-tokens.test.ts
+++ b/packages/ai/test/total-tokens.test.ts
@@ -290,7 +290,7 @@ describe("totalTokens field", () => {
"glm-4.5-flash - should return totalTokens equal to sum of components",
{ retry: 3, timeout: 60000 },
async () => {
- const llm = getModel("zai", "glm-4.5-flash");
+ const llm = getModel("zai-coding-plan", "glm-4.5-flash");
console.log(`\nz.ai / ${llm.id}:`);
const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.ZAI_API_KEY });
diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts
index 4087d306e..65f31de16 100644
--- a/packages/ai/test/unicode-surrogate.test.ts
+++ b/packages/ai/test/unicode-surrogate.test.ts
@@ -590,7 +590,7 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => {
});
describe.skipIf(!process.env.ZAI_API_KEY)("zAI Provider Unicode Handling", () => {
- const llm = getModel("zai", "glm-4.5-air");
+ const llm = getModel("zai-coding-plan", "glm-4.5-air");
it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => {
await testEmojiInToolResults(llm);
diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index e9ee71354..29de187c8 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -31,6 +31,10 @@
## [0.49.2] - 2026-01-19
+### Changed
+
+- Changed `zai` provider to use the Coding Plan API for better coding performance.
+
### Added
- Added widget placement option for extension widgets via `widgetPlacement` in `pi.addWidget()` ([#850](https://github.com/badlogic/pi-mono/pull/850) by [@marckrenn](https://github.com/marckrenn))
diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md
index a6f35ee36..aa33706cc 100644
--- a/packages/coding-agent/README.md
+++ b/packages/coding-agent/README.md
@@ -187,7 +187,8 @@ Add API keys to `~/.pi/agent/auth.json`:
{
"anthropic": { "type": "api_key", "key": "sk-ant-..." },
"openai": { "type": "api_key", "key": "sk-..." },
- "google": { "type": "api_key", "key": "..." }
+ "google": { "type": "api_key", "key": "..." },
+ "zai-coding-plan": { "type": "api_key", "key": "..." }
}
```
@@ -204,7 +205,7 @@ Add API keys to `~/.pi/agent/auth.json`:
| xAI | `xai` | `XAI_API_KEY` |
| OpenRouter | `openrouter` | `OPENROUTER_API_KEY` |
| Vercel AI Gateway | `vercel-ai-gateway` | `AI_GATEWAY_API_KEY` |
-| ZAI | `zai` | `ZAI_API_KEY` |
+| ZAI Coding Plan | `zai-coding-plan` | `ZAI_API_KEY` |
| OpenCode Zen | `opencode` | `OPENCODE_API_KEY` |
| MiniMax | `minimax` | `MINIMAX_API_KEY` |
| MiniMax (China) | `minimax-cn` | `MINIMAX_CN_API_KEY` |
@@ -245,6 +246,12 @@ pi
- Prompt cache stored under `~/.pi/agent/cache/openai-codex/`
- Intended for personal use with your own subscription; not for resale or multi-user services. For production, use the OpenAI Platform API.
+**Z.AI GLM Coding Plan notes:**
+- Pi uses the dedicated coding plan endpoint: `https://api.z.ai/api/coding/paas/v4`
+- Requires Z.AI API key from [Z.AI Open Platform](https://z.ai) with an active [Coding Plan](https://z.ai/subscribe) subscription
+- Z.AI API keys are the same for both the subscription and regular API. In order to use the regular API, you need to configure a custom provider pointing at the [regular endpoint](https://docs.z.ai/api-reference/llm/chat-completion)
+- Consult the [docs](https://docs.z.ai/guides/llm/glm-4.7) to learn more about the models
+
Credentials stored in `~/.pi/agent/auth.json`. Use `/logout` to clear.
**Troubleshooting (OAuth):**
@@ -1218,7 +1225,7 @@ pi [options] [@files...] [messages...]
| Option | Description |
|--------|-------------|
-| `--provider <name>` | Provider: `anthropic`, `openai`, `openai-codex`, `google`, `google-vertex`, `amazon-bedrock`, `mistral`, `xai`, `groq`, `cerebras`, `openrouter`, `vercel-ai-gateway`, `zai`, `opencode`, `minimax`, `minimax-cn`, `github-copilot`, `google-gemini-cli`, `google-antigravity`, or custom |
+| `--provider <name>` | Provider: `anthropic`, `openai`, `openai-codex`, `google`, `google-vertex`, `amazon-bedrock`, `mistral`, `xai`, `groq`, `cerebras`, `openrouter`, `vercel-ai-gateway`, `zai-coding-plan`, `opencode`, `minimax`, `minimax-cn`, `github-copilot`, `google-gemini-cli`, `google-antigravity`, or custom |
| `--model <id>` | Model ID |
| `--api-key <key>` | API key (overrides environment) |
| `--system-prompt <text\|file>` | Custom system prompt (text or file path) |
diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts
index 3d24c77dc..f9aa36572 100644
--- a/packages/coding-agent/src/cli/args.ts
+++ b/packages/coding-agent/src/cli/args.ts
@@ -243,7 +243,7 @@ ${chalk.bold("Environment Variables:")}
XAI_API_KEY - xAI Grok API key
OPENROUTER_API_KEY - OpenRouter API key
AI_GATEWAY_API_KEY - Vercel AI Gateway API key
- ZAI_API_KEY - ZAI API key
+ ZAI_API_KEY - ZAI Coding Plan API key
MISTRAL_API_KEY - Mistral API key
MINIMAX_API_KEY - MiniMax API key
AWS_PROFILE - AWS profile for Amazon Bedrock
diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts
index c28a8b0eb..d3e27a4aa 100644
--- a/packages/coding-agent/src/core/model-resolver.ts
+++ b/packages/coding-agent/src/core/model-resolver.ts
@@ -25,7 +25,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
xai: "grok-4-fast-non-reasoning",
groq: "openai/gpt-oss-120b",
cerebras: "zai-glm-4.6",
- zai: "glm-4.6",
+ "zai-coding-plan": "glm-4.6",
mistral: "devstral-medium-latest",
minimax: "MiniMax-M2.1",
"minimax-cn": "MiniMax-M2.1",
diff --git a/packages/web-ui/README.md b/packages/web-ui/README.md
index 684caac61..79328e401 100644
--- a/packages/web-ui/README.md
+++ b/packages/web-ui/README.md
@@ -508,7 +508,7 @@ agent.streamFn = createStreamFn(async () => {
});
// Providers requiring proxy:
-// - zai: always
+// - zai-coding-plan: always
// - anthropic: only OAuth tokens (sk-ant-oat-*)
```
diff --git a/packages/web-ui/src/components/ProviderKeyInput.ts b/packages/web-ui/src/components/ProviderKeyInput.ts
index 011af2004..ebdc1a379 100644
--- a/packages/web-ui/src/components/ProviderKeyInput.ts
+++ b/packages/web-ui/src/components/ProviderKeyInput.ts
@@ -18,7 +18,7 @@ const TEST_MODELS: Record<string, string> = {
"vercel-ai-gateway": "anthropic/claude-opus-4.5",
cerebras: "gpt-oss-120b",
xai: "grok-4-fast-non-reasoning",
- zai: "glm-4.5-air",
+ "zai-coding-plan": "glm-4.5-air",
};
@customElement("provider-key-input")
diff --git a/packages/web-ui/src/utils/proxy-utils.ts b/packages/web-ui/src/utils/proxy-utils.ts
index 27816b18d..7901a15e5 100644
--- a/packages/web-ui/src/utils/proxy-utils.ts
+++ b/packages/web-ui/src/utils/proxy-utils.ts
@@ -12,13 +12,13 @@ import { streamSimple } from "@mariozechner/pi-ai";
/**
* Check if a provider/API key combination requires a CORS proxy.
*
- * @param provider - Provider name (e.g., "anthropic", "openai", "zai")
+ * @param provider - Provider name (e.g., "anthropic", "openai", "zai-coding-plan")
* @param apiKey - API key for the provider
* @returns true if proxy is required, false otherwise
*/
export function shouldUseProxyForProvider(provider: string, apiKey: string): boolean {
switch (provider.toLowerCase()) {
- case "zai":
+ case "zai-coding-plan":
// Z-AI always requires proxy
return true;Let me check if there are any linked issues and read the relevant source files to validate the changes:
>rg -l "zai" packages/ai/src/ packages/coding-agent/src/ --type ts | head -20packages/ai/src/models.generated.ts
packages/ai/src/types.ts
packages/ai/src/env-api-keys.ts
packages/coding-agent/src/core/model-registry.ts
packages/ai/src/providers/openai-completions.ts
packages/coding-agent/src/core/model-resolver.ts>git pullAlready up to date.
>git pullerror: cannot pull with rebase: You have unstaged changes.
error: Please commit or stash them.
continue
>rg -n "zai-coding-plan" packages/ai/src/env-api-keys.ts(no output)
Command exited with code 1There's an issue: the env-api-keys.ts file still has the old zai key in the envMap, not zai-coding-plan. Let me verify by checking the current state on main:
>rg -n "zai" packages/ai/src/env-api-keys.ts104: zai: "ZAI_API_KEY",>rg -n '"zai"' packages/ai/src/ packages/coding-agent/src/packages/ai/src/types.ts:34: | "zai"
packages/ai/src/types.ts:215: /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "zai" uses thinking: { type: "enabled" }. Default: "openai". */
packages/ai/src/types.ts:216: thinkingFormat?: "openai" | "zai";
packages/ai/src/models.generated.ts:11242: "zai": {
packages/ai/src/models.generated.ts:11247: provider: "zai",
packages/ai/src/models.generated.ts:11249: compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
packages/ai/src/models.generated.ts:11265: provider: "zai",
packages/ai/src/models.generated.ts:11267: compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
packages/ai/src/models.generated.ts:11283: provider: "zai",
packages/ai/src/models.generated.ts:11285: compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
packages/ai/src/models.generated.ts:11301: provider: "zai",
packages/ai/src/models.generated.ts:11303: compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
packages/ai/src/models.generated.ts:11319: provider: "zai",
packages/ai/src/models.generated.ts:11321: compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
packages/ai/src/models.generated.ts:11337: provider: "zai",
packages/ai/src/models.generated.ts:11339: compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
packages/ai/src/models.generated.ts:11355: provider: "zai",
packages/ai/src/models.generated.ts:11357: compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"},
packages/coding-agent/src/core/model-registry.ts:45: thinkingFormat: Type.Optional(Type.Union([Type.Literal("openai"), Type.Literal("zai")])),
packages/ai/src/providers/openai-completions.ts:439: if (compat.thinkingFormat === "zai" && model.reasoning) {
packages/ai/src/providers/openai-completions.ts:754: const isZai = provider === "zai" || baseUrl.includes("api.z.ai");
packages/ai/src/providers/openai-completions.ts:784: thinkingFormat: isZai ? "zai" : "openai",Now I see the issue. The PR changes packages/ai/src/stream.ts but the actual env key mapping is in packages/ai/src/env-api-keys.ts. The diff shows stream.ts being changed, but that's outdated code. Let me check the current structure more carefully:
>rg -n "getEnvApiKey" packages/ai/src/stream.ts15:export { getEnvApiKey } from "./env-api-keys.js";>cat packages/ai/src/stream.ts | head -30import "./providers/register-builtins.js";
import { getApiProvider } from "./api-registry.js";
import type {
Api,
AssistantMessage,
AssistantMessageEventStream,
Context,
Model,
ProviderStreamOptions,
SimpleStreamOptions,
StreamOptions,
} from "./types.js";
export { getEnvApiKey } from "./env-api-keys.js";
// Set up http proxy according to env variables for `fetch` based SDKs in Node.js.
// Bun has builtin support for this.
if (typeof process !== "undefined" && process.versions?.node) {
import("undici").then((m) => {
const { EnvHttpProxyAgent, setGlobalDispatcher } = m;
setGlobalDispatcher(new EnvHttpProxyAgent());
});
}
function resolveApiProvider(api: Api) {
const provider = getApiProvider(api);
if (!provider) {
throw new Error(`No API provider registered for api: ${api}`);
}I see. The diff shows changes to stream.ts that don't exist on main. The PR is based on an outdated version of main. The getEnvApiKey function used to be in stream.ts but has since been moved to env-api-keys.ts.
Let me also check if the PR has missing package labels and review other aspects:
>gh pr view 899 --repo badlogic/pi-mono --json labels{"labels":[]}Now I have a clear picture. The detectCompat function in openai-completions.ts still uses provider === "zai" for detection. The PR needs to update this to provider === "zai-coding-plan".
Let me check the cross-provider-handoff test as mentioned in AGENTS.md:
>rg -n "zai" packages/ai/test/cross-provider-handoff.test.ts 2>/dev/null || echo "File not found"86: { provider: "cerebras", model: "zai-glm-4.7", label: "cerebras-zai-glm-4.7" },>rg -n "zai" packages/ai/test/abort.test.ts packages/ai/test/image-limits.test.ts packages/ai/test/image-tool-result.test.ts 2>/dev/null | head -30(no output)I notice the cross-provider-handoff test doesn't include zai (or zai-coding-plan). According to AGENTS.md, new providers should be added to this test.
Now let me check for documentation update requirements:
>rg -n "zai" packages/coding-agent/docs/*.md 2>/dev/null | head -20packages/coding-agent/docs/custom-provider.md:535: thinkingFormat?: "openai" | "zai";The thinkingFormat description mentions "zai" which is correct as this is an internal format identifier, not the provider name.
Now let me check if there are any remaining issues to summarize:
>rg -n 'provider === "zai"' packages/ai/src/packages/ai/src/providers/openai-completions.ts:754: const isZai = provider === "zai" || baseUrl.includes("api.z.ai");This is a critical issue. The detectCompat function still checks for provider === "zai" but the PR renames the provider to zai-coding-plan. This will break detection for the new provider name.
Let me also verify the coding-agent changelog entry placement:
I see. The PR has added a changelog entry under ## [0.49.2] - 2026-01-19 which is incorrect. The changelog entry should be under ## [Unreleased]. Additionally, the entry says "Changed" but this is a breaking change since it renames the provider.
Let me verify the exact diff for the coding-agent changelog:
>gh pr diff 899 --repo badlogic/pi-mono | grep -A20 "coding-agent/CHANGELOG"diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index e9ee71354..29de187c8 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -31,6 +31,10 @@
## [0.49.2] - 2026-01-19
+### Changed
+
+- Changed `zai` provider to use the Coding Plan API for better coding performance.
+
### Added
- Added widget placement option for extension widgets via `widgetPlacement` in `pi.addWidget()` ([#850](https://github.com/badlogic/pi-mono/pull/850) by [@marckrenn](https://github.com/marckrenn))
diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md
index a6f35ee36..aa33706cc 100644
--- a/packages/coding-agent/README.md
+++ b/packages/coding-agent/README.md
@@ -187,7 +187,8 @@ Add API keys to `~/.pi/agent/auth.json`:
{
"anthropic": { "type": "api_key", "key": "sk-ant-..." },
"openai": { "type": "api_key", "key": "sk-..." },
- "google": { "type": "api_key", "key": "..." }Confirmed: the PR adds a changelog entry to a released version (0.49.2) instead of [Unreleased]. This is a violation of the changelog rules.
PR: https://github.com/badlogic/pi-mono/pull/899
Changelog:
## [Unreleased] → ### Breaking Changes ✅## [0.49.2] (released version) instead of ## [Unreleased]. Must be moved. Also marked as ### Changed but should be ### Breaking Changes since renaming a provider breaks existing configurations.Good:
packages/ai/CHANGELOG.md with correct ### Breaking Changes sectionpackages/coding-agent/README.md with detailed Z.AI Coding Plan notesBad:
env-api-keys.ts not updated - The diff shows changes to stream.ts with an old getEnvApiKey function, but on main this function lives in packages/ai/src/env-api-keys.ts. The PR is based on an outdated main branch. The envMap in env-api-keys.ts still has zai: "ZAI_API_KEY" and needs to be changed to "zai-coding-plan": "ZAI_API_KEY".detectCompat() in openai-completions.ts not updated - Line 754 still checks provider === "zai" but should check provider === "zai-coding-plan". Without this fix, the compatibility detection (thinking format, supportsStore, etc.) will fall back to defaults instead of detecting zai-specific settings.KnownProvider type in types.ts not updated - The diff shows "zai-coding-plan" but main still has "zai". The PR needs rebasing.packages/coding-agent/CHANGELOG.md entry added to released [0.49.2] instead of [Unreleased]pkg:* labels on the PR (should have pkg:ai, pkg:coding-agent, pkg:web-ui, pkg:agent)cross-provider-handoff.test.ts: Per AGENTS.md, new providers should be added to this testUgly:
stream.ts that don't exist on current main). This will cause merge conflicts and the key changes are missing.Questions or Assumptions:
Change summary:
zai → zai-coding-plan to align with models.dev namingZAI_API_KEY configured will need to update their model selections from zai/... to zai-coding-plan/...Tests:
cross-provider-handoff.test.ts entry for zai-coding-plan provider