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
2 changes: 1 addition & 1 deletion .github/workflows/code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ jobs:
cd "$REVIEW_DIR"

aictrl run --format json \
--model zai-coding-plan/glm-5 \
--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.
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
10 changes: 7 additions & 3 deletions packages/cli/src/id/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export namespace Identifier {
}

const LENGTH = 26
export const TIMESTAMP_CYCLE = 2 ** 36

// State for monotonic ID generation
let lastTimestamp = 0
Expand Down Expand Up @@ -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
}
}
48 changes: 48 additions & 0 deletions packages/cli/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,54 @@ 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",
}
input.models["glm-5.2"] ??= {
id: "glm-5.2",
providerID: input.id,
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,15 @@ 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") {
Comment thread
byapparov marked this conversation as resolved.
return {
low: { reasoningEffort: "low" },
medium: { reasoningEffort: "high" },
high: { reasoningEffort: "high" },
xhigh: { reasoningEffort: "max" },
max: { reasoningEffort: "max" },
}
}
if (model.providerID === "zai-coding-plan" && model.api.id === "glm-5.2") {
return {
low: { reasoningEffort: "high" },
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/tool/truncation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 + HOUR_MS) >= cutoff) continue
await fs.unlink(path.join(DIR, entry)).catch(() => {})
}
}
Expand Down
19 changes: 19 additions & 0 deletions packages/cli/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1633,6 +1633,25 @@ describe("ProviderTransform.variants", () => {
expect(result.max).toEqual({ reasoningEffort: "max" })
})

test("zai coding plan glm-5.3 maps native efforts and compatibility aliases", () => {
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" },
medium: { reasoningEffort: "high" },
high: { reasoningEffort: "high" },
xhigh: { reasoningEffort: "max" },
max: { reasoningEffort: "max" },
})
})

test("mistral returns empty object", () => {
const model = createMockModel({
id: "mistral/mistral-large",
Expand Down
9 changes: 4 additions & 5 deletions packages/cli/test/session/llm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand All @@ -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() },
Expand Down Expand Up @@ -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({
Expand Down
19 changes: 19 additions & 0 deletions packages/cli/test/tool/fixtures/models-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 17 additions & 3 deletions packages/cli/test/tool/truncation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,34 +127,48 @@ 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 () => {
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)

// 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)
})
})
})
Loading