From 6e2480c736bf21b6560fa782f56846482cee6065 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Thu, 20 Aug 2026 11:01:28 +0100 Subject: [PATCH 1/6] feat: add GLM-5.3 Coding Plan support --- CHANGELOG.md | 4 ++ packages/cli/package.json | 2 +- packages/cli/src/provider/provider.ts | 53 +++++++++++++++++++ packages/cli/src/provider/transform.ts | 7 +++ packages/cli/test/provider/transform.test.ts | 17 ++++++ packages/cli/test/session/llm.test.ts | 9 ++-- .../cli/test/tool/fixtures/models-api.json | 19 +++++++ 7 files changed, 105 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd05e13..ee5be54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- **GLM-5.3 model support** — Added the latest Z.AI Coding Plan model with its 1M-token context window and native `low`, `high`, and `max` reasoning efforts. + ### Compatibility - **NDJSON v1 terminal reasons are an open set** — `session_error.reason` now includes `interrupted` for `SIGINT` and `terminated` for `SIGTERM`, and `code` may contain the conventional signal-derived exit code (`130` or `143`). Schema v1 consumers should treat unknown event types, fields, and enum-like string values as forward-compatible additions. diff --git a/packages/cli/package.json b/packages/cli/package.json index db36150..eca491f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "0.4.1", + "version": "0.4.2", "name": "@aictrl/cli", "description": "Headless execution engine for AI agent skills", "type": "module", diff --git a/packages/cli/src/provider/provider.ts b/packages/cli/src/provider/provider.ts index a504fe6..f975895 100644 --- a/packages/cli/src/provider/provider.ts +++ b/packages/cli/src/provider/provider.ts @@ -580,6 +580,59 @@ export namespace Provider { }, "zai-coding-plan": async (input) => { const apiKey = Env.get("ZHIPU_API_KEY") + input.models["glm-5.3"] ??= { + id: "glm-5.3", + providerID: input.id, + api: { + id: "glm-5.3", + url: "https://api.z.ai/api/coding/paas/v4", + npm: "@ai-sdk/openai-compatible", + }, + name: "GLM-5.3", + family: "glm", + capabilities: { + temperature: true, + reasoning: true, + attachment: false, + toolcall: true, + input: { + text: true, + audio: false, + image: false, + video: false, + pdf: false, + }, + output: { + text: true, + audio: false, + image: false, + video: false, + pdf: false, + }, + interleaved: { field: "reasoning_content" }, + }, + cost: { + input: 0, + output: 0, + cache: { + read: 0, + write: 0, + }, + }, + limit: { + context: 1_000_000, + output: 131_072, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2026-08-14", + variants: { + low: { reasoningEffort: "low" }, + high: { reasoningEffort: "high" }, + max: { reasoningEffort: "max" }, + }, + } input.models["glm-5.2"] ??= { id: "glm-5.2", providerID: input.id, diff --git a/packages/cli/src/provider/transform.ts b/packages/cli/src/provider/transform.ts index 0716ade..9ebf6c7 100644 --- a/packages/cli/src/provider/transform.ts +++ b/packages/cli/src/provider/transform.ts @@ -343,6 +343,13 @@ export namespace ProviderTransform { model.api.id.includes(v), ) const adaptiveEfforts = ["low", "medium", "high", "max"] + if (model.providerID === "zai-coding-plan" && model.api.id === "glm-5.3") { + return { + low: { reasoningEffort: "low" }, + high: { reasoningEffort: "high" }, + max: { reasoningEffort: "max" }, + } + } if (model.providerID === "zai-coding-plan" && model.api.id === "glm-5.2") { return { low: { reasoningEffort: "high" }, diff --git a/packages/cli/test/provider/transform.test.ts b/packages/cli/test/provider/transform.test.ts index df85927..9110f77 100644 --- a/packages/cli/test/provider/transform.test.ts +++ b/packages/cli/test/provider/transform.test.ts @@ -1633,6 +1633,23 @@ describe("ProviderTransform.variants", () => { expect(result.max).toEqual({ reasoningEffort: "max" }) }) + test("zai coding plan glm-5.3 exposes native effort variants", () => { + const model = createMockModel({ + id: "glm-5.3", + providerID: "zai-coding-plan", + api: { + id: "glm-5.3", + url: "https://api.z.ai/api/coding/paas/v4", + npm: "@ai-sdk/openai-compatible", + }, + }) + expect(ProviderTransform.variants(model)).toEqual({ + low: { reasoningEffort: "low" }, + high: { reasoningEffort: "high" }, + max: { reasoningEffort: "max" }, + }) + }) + test("mistral returns empty object", () => { const model = createMockModel({ id: "mistral/mistral-large", diff --git a/packages/cli/test/session/llm.test.ts b/packages/cli/test/session/llm.test.ts index be7c69e..820d1ad 100644 --- a/packages/cli/test/session/llm.test.ts +++ b/packages/cli/test/session/llm.test.ts @@ -323,14 +323,13 @@ describe("session.llm.stream", () => { }) }) - test("sends Coding Plan payload for GLM-5.2", async () => { + test.each(["glm-5.2", "glm-5.3"])("sends Coding Plan payload for %s", async (modelID) => { const server = state.server if (!server) { throw new Error("Server not initialized") } const providerID = "zai-coding-plan" - const modelID = "glm-5.2" const fixture = await loadFixture(providerID, modelID) const model = fixture.model @@ -366,7 +365,7 @@ describe("session.llm.stream", () => { directory: tmp.path, fn: async () => { const resolved = await Provider.getModel(providerID, model.id) - const sessionID = "session-test-glm-52" + const sessionID = `session-test-${modelID}` const agent = { name: "test", mode: "primary", @@ -375,7 +374,7 @@ describe("session.llm.stream", () => { } satisfies Agent.Info const user = { - id: "user-glm-52", + id: `user-${modelID}`, sessionID, role: "user", time: { created: Date.now() }, @@ -403,7 +402,7 @@ describe("session.llm.stream", () => { expect(capture.url.pathname.endsWith("/chat/completions")).toBe(true) expect(capture.headers.get("Authorization")).toBe("Bearer test-zai-key") - expect(body.model).toBe("glm-5.2") + expect(body.model).toBe(modelID) expect(body.temperature).toBe(1) expect(body.max_tokens).toBe(ProviderTransform.maxOutputTokens(resolved)) expect(body.thinking).toEqual({ diff --git a/packages/cli/test/tool/fixtures/models-api.json b/packages/cli/test/tool/fixtures/models-api.json index c79d15f..c933ae5 100644 --- a/packages/cli/test/tool/fixtures/models-api.json +++ b/packages/cli/test/tool/fixtures/models-api.json @@ -1747,6 +1747,25 @@ "name": "Z.AI Coding Plan", "doc": "https://docs.z.ai/devpack/overview", "models": { + "glm-5.3": { + "id": "glm-5.3", + "name": "GLM-5.3", + "description": "Flagship GLM model for long-horizon coding, agents, and complex project delivery", + "family": "glm", + "attachment": false, + "reasoning": true, + "reasoning_options": [{ "type": "effort", "values": ["low", "high", "max"] }], + "tool_call": true, + "interleaved": { "field": "reasoning_content" }, + "structured_output": true, + "temperature": true, + "release_date": "2026-08-14", + "last_updated": "2026-08-14", + "modalities": { "input": ["text"], "output": ["text"] }, + "open_weights": false, + "cost": { "input": 0, "output": 0, "cache_read": 0, "cache_write": 0 }, + "limit": { "context": 1000000, "output": 131072 } + }, "glm-5.2": { "id": "glm-5.2", "name": "GLM-5.2", From 243c9742c9a014ab265b3fc245eab56d5e9e624f Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Thu, 20 Aug 2026 11:13:32 +0100 Subject: [PATCH 2/6] fix: handle tool output timestamp rollover --- packages/cli/src/id/id.ts | 10 +++++++--- packages/cli/src/tool/truncation.ts | 6 +++--- packages/cli/test/tool/truncation.test.ts | 7 ++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/id/id.ts b/packages/cli/src/id/id.ts index 6673297..246b499 100644 --- a/packages/cli/src/id/id.ts +++ b/packages/cli/src/id/id.ts @@ -19,6 +19,7 @@ export namespace Identifier { } const LENGTH = 26 + export const TIMESTAMP_CYCLE = 2 ** 36 // State for monotonic ID generation let lastTimestamp = 0 @@ -74,11 +75,14 @@ export namespace Identifier { return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12) } - /** Extract timestamp from an ascending ID. Does not work with descending IDs. */ - export function timestamp(id: string): number { + /** Reconstruct the most recent timestamp at or before reference from an ascending ID. */ + export function timestamp(id: string, reference = Date.now()): number { const prefix = id.split("_")[0] const hex = id.slice(prefix.length + 1, prefix.length + 13) const encoded = BigInt("0x" + hex) - return Number(encoded / BigInt(0x1000)) + const value = Number(encoded / BigInt(0x1000)) + const base = Math.floor(reference / TIMESTAMP_CYCLE) * TIMESTAMP_CYCLE + const result = base + value + return result > reference ? result - TIMESTAMP_CYCLE : result } } diff --git a/packages/cli/src/tool/truncation.ts b/packages/cli/src/tool/truncation.ts index 58b0cc1..01a419d 100644 --- a/packages/cli/src/tool/truncation.ts +++ b/packages/cli/src/tool/truncation.ts @@ -33,11 +33,11 @@ export namespace Truncate { }) } - export async function cleanup() { - const cutoff = Identifier.timestamp(Identifier.create("tool", false, Date.now() - RETENTION_MS)) + export async function cleanup(now = Date.now()) { + const cutoff = now - RETENTION_MS const entries = await Glob.scan("tool_*", { cwd: DIR, include: "file" }).catch(() => [] as string[]) for (const entry of entries) { - if (Identifier.timestamp(entry) >= cutoff) continue + if (Identifier.timestamp(entry, now) >= cutoff) continue await fs.unlink(path.join(DIR, entry)).catch(() => {}) } } diff --git a/packages/cli/test/tool/truncation.test.ts b/packages/cli/test/tool/truncation.test.ts index 9e141b2..fe6695d 100644 --- a/packages/cli/test/tool/truncation.test.ts +++ b/packages/cli/test/tool/truncation.test.ts @@ -135,20 +135,21 @@ describe("Truncate", () => { test("deletes files older than 7 days and preserves recent files", async () => { await fs.mkdir(Truncate.DIR, { recursive: true }) + const now = 26 * Identifier.TIMESTAMP_CYCLE + 3 * DAY_MS // Create an old file (10 days ago) - const oldTimestamp = Date.now() - 10 * DAY_MS + const oldTimestamp = now - 10 * DAY_MS const oldId = Identifier.create("tool", false, oldTimestamp) oldFile = path.join(Truncate.DIR, oldId) await Filesystem.write(oldFile, "old content") // Create a recent file (3 days ago) - const recentTimestamp = Date.now() - 3 * DAY_MS + const recentTimestamp = now - 3 * DAY_MS const recentId = Identifier.create("tool", false, recentTimestamp) recentFile = path.join(Truncate.DIR, recentId) await Filesystem.write(recentFile, "recent content") - await Truncate.cleanup() + await Truncate.cleanup(now) // Old file should be deleted expect(await Filesystem.exists(oldFile)).toBe(false) From 293f9714166f23b126b861932297c26cb63ef900 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Thu, 20 Aug 2026 11:17:01 +0100 Subject: [PATCH 3/6] fix: address GLM-5.3 review findings --- packages/cli/src/provider/provider.ts | 5 ----- packages/cli/src/provider/transform.ts | 2 ++ packages/cli/test/provider/transform.test.ts | 4 +++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/provider/provider.ts b/packages/cli/src/provider/provider.ts index f975895..866cb59 100644 --- a/packages/cli/src/provider/provider.ts +++ b/packages/cli/src/provider/provider.ts @@ -627,11 +627,6 @@ export namespace Provider { options: {}, headers: {}, release_date: "2026-08-14", - variants: { - low: { reasoningEffort: "low" }, - high: { reasoningEffort: "high" }, - max: { reasoningEffort: "max" }, - }, } input.models["glm-5.2"] ??= { id: "glm-5.2", diff --git a/packages/cli/src/provider/transform.ts b/packages/cli/src/provider/transform.ts index 9ebf6c7..98b2a29 100644 --- a/packages/cli/src/provider/transform.ts +++ b/packages/cli/src/provider/transform.ts @@ -346,7 +346,9 @@ export namespace ProviderTransform { if (model.providerID === "zai-coding-plan" && model.api.id === "glm-5.3") { return { low: { reasoningEffort: "low" }, + medium: { reasoningEffort: "high" }, high: { reasoningEffort: "high" }, + xhigh: { reasoningEffort: "max" }, max: { reasoningEffort: "max" }, } } diff --git a/packages/cli/test/provider/transform.test.ts b/packages/cli/test/provider/transform.test.ts index 9110f77..68dd026 100644 --- a/packages/cli/test/provider/transform.test.ts +++ b/packages/cli/test/provider/transform.test.ts @@ -1633,7 +1633,7 @@ describe("ProviderTransform.variants", () => { expect(result.max).toEqual({ reasoningEffort: "max" }) }) - test("zai coding plan glm-5.3 exposes native effort variants", () => { + test("zai coding plan glm-5.3 maps native efforts and compatibility aliases", () => { const model = createMockModel({ id: "glm-5.3", providerID: "zai-coding-plan", @@ -1645,7 +1645,9 @@ describe("ProviderTransform.variants", () => { }) expect(ProviderTransform.variants(model)).toEqual({ low: { reasoningEffort: "low" }, + medium: { reasoningEffort: "high" }, high: { reasoningEffort: "high" }, + xhigh: { reasoningEffort: "max" }, max: { reasoningEffort: "max" }, }) }) From 36cc46e48c20dfa5ba1769c03afa3a9f3c995539 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Thu, 20 Aug 2026 11:23:00 +0100 Subject: [PATCH 4/6] fix: use GLM-5.3 for AI review --- .github/workflows/code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index 5044ba1..8a2efb3 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -141,7 +141,7 @@ jobs: cd "$REVIEW_DIR" aictrl run --format json \ - --model zai-coding-plan/glm-5 \ + --model zai-coding-plan/glm-5.3 \ "You are reviewing PR #${PR_NUMBER} on ${GH_REPO} (SHA: ${PR_SHA}, base: ${PR_BASE_REF}). You have access to the gh CLI, git, and file reading tools. Use them to understand the changes. From 6a50d381c8c40e1da9f29829e5555f5f1ecd4b35 Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Thu, 20 Aug 2026 11:25:30 +0100 Subject: [PATCH 5/6] fix: use GLM-5.2 for AI review --- .github/workflows/code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml index 8a2efb3..c117f05 100644 --- a/.github/workflows/code-review.yml +++ b/.github/workflows/code-review.yml @@ -141,7 +141,7 @@ jobs: cd "$REVIEW_DIR" aictrl run --format json \ - --model zai-coding-plan/glm-5.3 \ + --model zai-coding-plan/glm-5.2 \ "You are reviewing PR #${PR_NUMBER} on ${GH_REPO} (SHA: ${PR_SHA}, base: ${PR_BASE_REF}). You have access to the gh CLI, git, and file reading tools. Use them to understand the changes. From 36ad5f6fab2c519c9a0c3113456a4a4fea834c7b Mon Sep 17 00:00:00 2001 From: Bulat Yapparov Date: Thu, 20 Aug 2026 11:32:07 +0100 Subject: [PATCH 6/6] fix: preserve future tool output files --- packages/cli/src/tool/truncation.ts | 2 +- packages/cli/test/tool/truncation.test.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/tool/truncation.ts b/packages/cli/src/tool/truncation.ts index 01a419d..0ea2743 100644 --- a/packages/cli/src/tool/truncation.ts +++ b/packages/cli/src/tool/truncation.ts @@ -37,7 +37,7 @@ export namespace Truncate { const cutoff = now - RETENTION_MS const entries = await Glob.scan("tool_*", { cwd: DIR, include: "file" }).catch(() => [] as string[]) for (const entry of entries) { - if (Identifier.timestamp(entry, now) >= cutoff) continue + if (Identifier.timestamp(entry, now + HOUR_MS) >= cutoff) continue await fs.unlink(path.join(DIR, entry)).catch(() => {}) } } diff --git a/packages/cli/test/tool/truncation.test.ts b/packages/cli/test/tool/truncation.test.ts index fe6695d..34865df 100644 --- a/packages/cli/test/tool/truncation.test.ts +++ b/packages/cli/test/tool/truncation.test.ts @@ -127,10 +127,12 @@ describe("Truncate", () => { const DAY_MS = 24 * 60 * 60 * 1000 let oldFile: string let recentFile: string + let futureFile: string afterAll(async () => { await fs.unlink(oldFile).catch(() => {}) await fs.unlink(recentFile).catch(() => {}) + await fs.unlink(futureFile).catch(() => {}) }) test("deletes files older than 7 days and preserves recent files", async () => { @@ -157,5 +159,16 @@ describe("Truncate", () => { // Recent file should still exist expect(await Filesystem.exists(recentFile)).toBe(true) }) + + test("preserves files created shortly after cleanup starts", async () => { + await fs.mkdir(Truncate.DIR, { recursive: true }) + const now = 26 * Identifier.TIMESTAMP_CYCLE + 3 * DAY_MS + futureFile = path.join(Truncate.DIR, Identifier.create("tool", false, now + 1)) + await Filesystem.write(futureFile, "future content") + + await Truncate.cleanup(now) + + expect(await Filesystem.exists(futureFile)).toBe(true) + }) }) })