glm-coding-router 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/cli.js +5 -0
- package/dist/commands/usage.js +162 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -233,6 +233,26 @@ Notes:
|
|
|
233
233
|
orchestration isn't drivable today); the harness is stack-shaped so it can
|
|
234
234
|
be added later.
|
|
235
235
|
|
|
236
|
+
## usage
|
|
237
|
+
|
|
238
|
+
Provider usage snapshots (spec §54 v0.5) — what is reliably retrievable:
|
|
239
|
+
|
|
240
|
+
```powershell
|
|
241
|
+
glm-router usage
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- **Z.ai Coding Plan quota** (network): queries the Z.ai monitor endpoint
|
|
245
|
+
(`/api/monitor/usage/quota/limit`) with your key and shows each credit
|
|
246
|
+
window — consumed/total, percentage, reset time — plus the plan level.
|
|
247
|
+
Unreachable endpoint or a rejected request renders `✗ <reason>` and exits 1.
|
|
248
|
+
- **Local totals** (offline): aggregates saved benchmark reports — run count
|
|
249
|
+
and summed input/output tokens (`glm-router benchmark` writes them).
|
|
250
|
+
- **Claude quota / Codex usage**: always shown as "not available" — neither
|
|
251
|
+
exposes a headless usage API today (and claude.ai quota is irrelevant while
|
|
252
|
+
traffic is routed to GLM).
|
|
253
|
+
|
|
254
|
+
`--json` emits the same data machine-readably. No key configured → `ERROR [10]`.
|
|
255
|
+
|
|
236
256
|
## CLI reference
|
|
237
257
|
|
|
238
258
|
```text
|
|
@@ -245,6 +265,7 @@ glm-router config show
|
|
|
245
265
|
glm-router config set models.main glm-5.3
|
|
246
266
|
glm-router delegate <name> run a GLM worker in an isolated git worktree
|
|
247
267
|
glm-router benchmark measure the Claude+GLM stack on built-in tasks
|
|
268
|
+
glm-router usage Z.ai quota snapshot + local benchmark totals
|
|
248
269
|
glm-router project init CLAUDE.md / AGENTS.md managed blocks (--dry-run supported)
|
|
249
270
|
glm-router project remove
|
|
250
271
|
glm-router skill install optional Codex delegation skill
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { skillInstallCommand, skillRemoveCommand } from "./commands/skill.js";
|
|
|
15
15
|
import { uninstallCommand } from "./commands/uninstall.js";
|
|
16
16
|
import { delegateCommand } from "./commands/delegate.js";
|
|
17
17
|
import { benchmarkCommand } from "./commands/benchmark.js";
|
|
18
|
+
import { usageCommand } from "./commands/usage.js";
|
|
18
19
|
const program = new Command();
|
|
19
20
|
program
|
|
20
21
|
.name("glm-router")
|
|
@@ -103,6 +104,10 @@ program
|
|
|
103
104
|
.option("--max-turns <n>", "worker --max-turns override", (value) => Number(value))
|
|
104
105
|
.option("--repeat <n>", "run each task N times", (value) => Number(value))
|
|
105
106
|
.action((commandOptions) => execute(() => benchmarkCommand({ ...globalOptions(), ...commandOptions })));
|
|
107
|
+
program
|
|
108
|
+
.command("usage")
|
|
109
|
+
.description("provider usage snapshots: Z.ai Coding Plan quota + local benchmark totals")
|
|
110
|
+
.action(() => execute(() => usageCommand(globalOptions())));
|
|
106
111
|
program
|
|
107
112
|
.command("uninstall")
|
|
108
113
|
.description("guided removal (keeps ZAI_API_KEY by default)")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Errors } from "../core/errors.js";
|
|
5
|
+
import { configDir } from "../core/paths.js";
|
|
6
|
+
import { version } from "../core/version.js";
|
|
7
|
+
import { resolveZaiApiKey } from "../core/zai-key.js";
|
|
8
|
+
import { emitJson } from "./context.js";
|
|
9
|
+
/** Z.ai monitor API used by their own dashboard (specs/usage.md). */
|
|
10
|
+
const ZAI_QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
|
|
11
|
+
/** Window labels for the observed enum values (specs/usage.md); unknown values stay generic. */
|
|
12
|
+
function describeWindow(limit) {
|
|
13
|
+
if (limit.unit === 3 && typeof limit.number === "number") {
|
|
14
|
+
return `${limit.number}-hour window`;
|
|
15
|
+
}
|
|
16
|
+
if (limit.unit === 6 && limit.number === 1) {
|
|
17
|
+
return "weekly";
|
|
18
|
+
}
|
|
19
|
+
return `window unit=${String(limit.unit)} x ${String(limit.number)}`;
|
|
20
|
+
}
|
|
21
|
+
/** Fetch and validate the Z.ai quota snapshot. Never logs the Authorization header. */
|
|
22
|
+
async function fetchZaiQuota(key, fetchImpl) {
|
|
23
|
+
let response;
|
|
24
|
+
try {
|
|
25
|
+
response = await fetchImpl(ZAI_QUOTA_URL, {
|
|
26
|
+
method: "GET",
|
|
27
|
+
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
|
28
|
+
signal: AbortSignal.timeout(10_000),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Error(`Z.ai monitor endpoint unreachable (${error instanceof Error ? error.message : "network error"})`);
|
|
33
|
+
}
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(`Z.ai monitor endpoint returned HTTP ${response.status}`);
|
|
36
|
+
}
|
|
37
|
+
let body;
|
|
38
|
+
try {
|
|
39
|
+
body = (await response.json());
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error("Z.ai monitor endpoint returned a non-JSON body");
|
|
43
|
+
}
|
|
44
|
+
if (body.code !== 200 || typeof body.data !== "object" || body.data === null) {
|
|
45
|
+
throw new Error(`Z.ai monitor endpoint rejected the request (${body.msg ?? `code ${String(body.code)}`})`);
|
|
46
|
+
}
|
|
47
|
+
return body.data;
|
|
48
|
+
}
|
|
49
|
+
/** Aggregate saved benchmark reports (specs/benchmark.md) into one local summary. */
|
|
50
|
+
export function aggregateLocalUsage(home) {
|
|
51
|
+
const dir = path.join(configDir(home), "benchmarks");
|
|
52
|
+
let runs = 0;
|
|
53
|
+
let tokensIn = 0;
|
|
54
|
+
let tokensOut = 0;
|
|
55
|
+
let lastFinishedAt = null;
|
|
56
|
+
if (!fs.existsSync(dir)) {
|
|
57
|
+
return { runs, tokensIn, tokensOut, lastFinishedAt };
|
|
58
|
+
}
|
|
59
|
+
for (const name of fs.readdirSync(dir)) {
|
|
60
|
+
if (!name.endsWith(".json"))
|
|
61
|
+
continue;
|
|
62
|
+
try {
|
|
63
|
+
const report = JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
|
|
64
|
+
for (const run of report.tasks ?? []) {
|
|
65
|
+
runs += 1;
|
|
66
|
+
tokensIn += run.inputTokens ?? 0;
|
|
67
|
+
tokensOut += run.outputTokens ?? 0;
|
|
68
|
+
}
|
|
69
|
+
if (typeof report.finishedAt === "string" && (!lastFinishedAt || report.finishedAt > lastFinishedAt)) {
|
|
70
|
+
lastFinishedAt = report.finishedAt;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Malformed report files are skipped — usage must never crash on bad data.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { runs, tokensIn, tokensOut, lastFinishedAt };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* glm-router usage (spec §54 v0.5, specs/usage.md): provider usage snapshots
|
|
81
|
+
* where APIs allow reliable retrieval. Z.ai quota via the monitor endpoint;
|
|
82
|
+
* Claude/Codex have no headless usage surface and say so; local totals come
|
|
83
|
+
* from saved benchmark reports.
|
|
84
|
+
*/
|
|
85
|
+
export async function usageCommand(options, deps = {}) {
|
|
86
|
+
const home = deps.home ?? os.homedir();
|
|
87
|
+
const env = deps.env ?? process.env;
|
|
88
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
89
|
+
const resolved = resolveZaiApiKey({ env, readUserEnv: deps.readUserEnv });
|
|
90
|
+
if (!resolved) {
|
|
91
|
+
throw Errors.zaiKeyMissing();
|
|
92
|
+
}
|
|
93
|
+
let quota;
|
|
94
|
+
let quotaError;
|
|
95
|
+
try {
|
|
96
|
+
quota = await fetchZaiQuota(resolved.key, fetchImpl);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
quotaError = error instanceof Error ? error.message : String(error);
|
|
100
|
+
}
|
|
101
|
+
const local = aggregateLocalUsage(home);
|
|
102
|
+
const limits = quota?.limits ?? [];
|
|
103
|
+
const json = {
|
|
104
|
+
version,
|
|
105
|
+
zai: quotaError
|
|
106
|
+
? { ok: false, error: quotaError }
|
|
107
|
+
: {
|
|
108
|
+
ok: true,
|
|
109
|
+
level: quota?.level ?? null,
|
|
110
|
+
limits: limits.map((limit) => ({
|
|
111
|
+
window: describeWindow(limit),
|
|
112
|
+
consumed: limit.currentValue ?? null,
|
|
113
|
+
total: limit.usage ?? null,
|
|
114
|
+
remaining: limit.remaining ?? null,
|
|
115
|
+
percentage: limit.percentage ?? null,
|
|
116
|
+
resetsAt: typeof limit.nextResetTime === "number" ? new Date(limit.nextResetTime).toISOString() : null,
|
|
117
|
+
})),
|
|
118
|
+
},
|
|
119
|
+
local,
|
|
120
|
+
claude: "not available — Claude Code exposes no headless usage API",
|
|
121
|
+
codex: "not available — Codex exposes no plan-usage API",
|
|
122
|
+
};
|
|
123
|
+
if (options.json) {
|
|
124
|
+
emitJson(json);
|
|
125
|
+
return quotaError ? 1 : 0;
|
|
126
|
+
}
|
|
127
|
+
const lines = [`GLM Coding Router v${version} — usage snapshot`, ""];
|
|
128
|
+
if (quotaError) {
|
|
129
|
+
lines.push(`Z.ai Coding Plan`);
|
|
130
|
+
lines.push(` ✗ ${quotaError}`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
lines.push(`Z.ai Coding Plan${quota?.level ? ` (level: ${quota.level})` : ""}`);
|
|
134
|
+
if (limits.length === 0) {
|
|
135
|
+
lines.push(" (no quota windows reported)");
|
|
136
|
+
}
|
|
137
|
+
for (const limit of limits) {
|
|
138
|
+
const consumed = limit.currentValue ?? "?";
|
|
139
|
+
const total = limit.usage ?? "?";
|
|
140
|
+
const percentage = typeof limit.percentage === "number"
|
|
141
|
+
? limit.percentage
|
|
142
|
+
: typeof limit.currentValue === "number" && typeof limit.usage === "number" && limit.usage > 0
|
|
143
|
+
? Math.round((limit.currentValue / limit.usage) * 100)
|
|
144
|
+
: "?";
|
|
145
|
+
const resets = typeof limit.nextResetTime === "number" ? ` — resets ${new Date(limit.nextResetTime).toISOString()}` : "";
|
|
146
|
+
lines.push(` ${describeWindow(limit).padEnd(15)} ${consumed} / ${total} credits (${percentage}%)${resets}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
lines.push("");
|
|
150
|
+
lines.push("Local (benchmark reports)");
|
|
151
|
+
if (local.runs === 0) {
|
|
152
|
+
lines.push(" (none yet — run glm-router benchmark)");
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
lines.push(` runs ${local.runs} · tokens ${local.tokensIn} in / ${local.tokensOut} out · last ${local.lastFinishedAt}`);
|
|
156
|
+
}
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(`Claude quota ${json.claude}`);
|
|
159
|
+
lines.push(`Codex usage ${json.codex}`);
|
|
160
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
161
|
+
return quotaError ? 1 : 0;
|
|
162
|
+
}
|