@v1nvn/zai 0.43.0 → 0.45.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/src/resolve.ts ADDED
@@ -0,0 +1,146 @@
1
+ import {
2
+ buildCli,
3
+ type CliRun,
4
+ type CliSpec,
5
+ HelpRequested,
6
+ parseCli,
7
+ } from '@v1nvn/agentic-core';
8
+
9
+ const DEFAULT_BASE_URL = 'https://api.z.ai';
10
+
11
+ const GLM_HOSTS = new Set(['api.z.ai', 'dev.bigmodel.cn', 'open.bigmodel.cn']);
12
+
13
+ export interface ParsedArgs {
14
+ readonly authToken: string | undefined;
15
+ readonly baseUrl: string | undefined;
16
+ readonly command?: 'usage';
17
+ readonly json: boolean;
18
+ }
19
+
20
+ export interface ResolvedConfig {
21
+ readonly token: string;
22
+ readonly url: string;
23
+ }
24
+
25
+ interface BaseUrl {
26
+ readonly glm: boolean;
27
+ readonly origin: string;
28
+ }
29
+
30
+ function fromEnv(value: string | undefined): string | undefined {
31
+ return value ? value : undefined;
32
+ }
33
+
34
+ function originOf(value: string): string | undefined {
35
+ try {
36
+ const url = new URL(value);
37
+ return url.protocol === 'https:' || url.protocol === 'http:'
38
+ ? url.origin
39
+ : undefined;
40
+ } catch {
41
+ return undefined;
42
+ }
43
+ }
44
+
45
+ function isGlm(origin: string): boolean {
46
+ return GLM_HOSTS.has(new URL(origin).hostname);
47
+ }
48
+
49
+ function resolveBaseUrl(
50
+ flag: string | undefined,
51
+ env: NodeJS.ProcessEnv,
52
+ ): BaseUrl {
53
+ if (flag !== undefined) {
54
+ const origin = originOf(flag);
55
+ if (origin === undefined) {
56
+ throw new Error(`invalid base URL: ${flag}`);
57
+ }
58
+ return { glm: isGlm(origin), origin };
59
+ }
60
+ const inherited = fromEnv(env.ANTHROPIC_BASE_URL);
61
+ const origin = inherited === undefined ? undefined : originOf(inherited);
62
+ if (origin !== undefined && isGlm(origin)) {
63
+ return { glm: true, origin };
64
+ }
65
+ return { glm: false, origin: DEFAULT_BASE_URL };
66
+ }
67
+
68
+ export function cliSpec(run?: CliRun): CliSpec {
69
+ return {
70
+ name: 'zai-usage',
71
+ description: 'GLM Coding Plan usage — one command, usage',
72
+ commands: [
73
+ {
74
+ name: 'usage',
75
+ description: 'GLM Coding Plan quota and usage report',
76
+ options: [
77
+ {
78
+ flags: '--auth-token <token>',
79
+ description: 'API key',
80
+ env: 'ZAI_AUTH_TOKEN',
81
+ },
82
+ {
83
+ flags: '--base-url <url>',
84
+ description: 'base URL',
85
+ env: 'ZAI_BASE_URL',
86
+ },
87
+ {
88
+ flags: '--json',
89
+ description: 'print the report lines as JSON for the zai mod',
90
+ },
91
+ ],
92
+ run,
93
+ },
94
+ ],
95
+ };
96
+ }
97
+
98
+ export function buildProgram() {
99
+ return buildCli(cliSpec());
100
+ }
101
+
102
+ export function parseArgs(
103
+ args: readonly string[],
104
+ ): HelpRequested | ParsedArgs | undefined {
105
+ const parsed = parseCli(cliSpec(), args);
106
+ if (parsed === undefined || parsed instanceof HelpRequested) {
107
+ return parsed;
108
+ }
109
+ if (parsed.command !== 'usage') {
110
+ return {
111
+ authToken: undefined,
112
+ baseUrl: undefined,
113
+ command: undefined,
114
+ json: false,
115
+ };
116
+ }
117
+ const { authToken, baseUrl, json } = parsed.options as {
118
+ authToken?: string;
119
+ baseUrl?: string;
120
+ json?: boolean;
121
+ };
122
+ return {
123
+ authToken: authToken || undefined,
124
+ baseUrl: baseUrl || undefined,
125
+ command: 'usage',
126
+ json: json === true,
127
+ };
128
+ }
129
+
130
+ export function resolveConfig(
131
+ env: NodeJS.ProcessEnv,
132
+ parsed: ParsedArgs,
133
+ ): ResolvedConfig {
134
+ const base = resolveBaseUrl(parsed.baseUrl, env);
135
+ const token =
136
+ parsed.authToken ??
137
+ (base.glm ? fromEnv(env.ANTHROPIC_AUTH_TOKEN) : undefined);
138
+ if (token === undefined) {
139
+ throw new Error(
140
+ env.ANTHROPIC_AUTH_TOKEN
141
+ ? 'ANTHROPIC_AUTH_TOKEN is set, but the resolved base URL does not name a GLM host, so it is not a GLM Coding Plan token — set ZAI_AUTH_TOKEN (or --auth-token)'
142
+ : 'no API key: set ZAI_AUTH_TOKEN (or --auth-token)',
143
+ );
144
+ }
145
+ return { token, url: base.origin };
146
+ }
package/src/usage.ts ADDED
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Usage query: the three GLM monitor endpoints of the resolved base URL,
3
+ * assembled into the report input.
4
+ */
5
+
6
+ import { pad2 } from '@v1nvn/agentic-core/draw';
7
+
8
+ import type {
9
+ RenderInput,
10
+ ZaiModelUsage,
11
+ ZaiQuota,
12
+ ZaiToolUsage,
13
+ } from './format.ts';
14
+ import type { ResolvedConfig } from './resolve.ts';
15
+
16
+ interface RawLimit {
17
+ currentValue?: number;
18
+ nextResetTime?: string;
19
+ percentage?: number;
20
+ type?: string;
21
+ usage?: number;
22
+ usageDetails?: { modelCode?: string; usage?: number }[];
23
+ }
24
+
25
+ interface RawQuota {
26
+ level?: string;
27
+ limits?: RawLimit[];
28
+ }
29
+
30
+ // Auth failures arrive as HTTP 200 with success false.
31
+ interface RawEnvelope {
32
+ code?: number;
33
+ data?: unknown;
34
+ msg?: string;
35
+ success?: boolean;
36
+ }
37
+
38
+ // The monitor API labels every bucket in Beijing time; the query window runs
39
+ // from yesterday at the current hour to the end of today's current hour.
40
+ function queryParams(now = new Date()): string {
41
+ const startDate = new Date(
42
+ now.getFullYear(),
43
+ now.getMonth(),
44
+ now.getDate() - 1,
45
+ now.getHours(),
46
+ 0,
47
+ 0,
48
+ 0,
49
+ );
50
+ const endDate = new Date(
51
+ now.getFullYear(),
52
+ now.getMonth(),
53
+ now.getDate(),
54
+ now.getHours(),
55
+ 59,
56
+ 59,
57
+ 999,
58
+ );
59
+ function formatDateTime(date: Date): string {
60
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`;
61
+ }
62
+ return `?startTime=${encodeURIComponent(formatDateTime(startDate))}&endTime=${encodeURIComponent(formatDateTime(endDate))}`;
63
+ }
64
+
65
+ // Rename the API's limit kinds to their display names; `totol` is the field the
66
+ // renderer reads (sic — the upstream spelling).
67
+ function processQuotaLimit(data: RawQuota): ZaiQuota {
68
+ if (!data.limits) {
69
+ return data;
70
+ }
71
+ data.limits = data.limits.map(item => {
72
+ if (item.type === 'TOKENS_LIMIT') {
73
+ return {
74
+ type: 'Token usage(5 Hour)',
75
+ percentage: item.percentage,
76
+ nextResetTime: item.nextResetTime,
77
+ };
78
+ }
79
+ if (item.type === 'TIME_LIMIT') {
80
+ return {
81
+ type: 'MCP usage(1 Month)',
82
+ percentage: item.percentage,
83
+ currentUsage: item.currentValue,
84
+ totol: item.usage,
85
+ usageDetails: item.usageDetails,
86
+ nextResetTime: item.nextResetTime,
87
+ };
88
+ }
89
+ return item;
90
+ });
91
+ return data;
92
+ }
93
+
94
+ async function fetchJson(
95
+ apiUrl: string,
96
+ label: string,
97
+ authToken: string,
98
+ query = '',
99
+ ): Promise<unknown> {
100
+ const res = await fetch(apiUrl + query, {
101
+ headers: {
102
+ Authorization: authToken,
103
+ 'Accept-Language': 'en-US,en',
104
+ 'Content-Type': 'application/json',
105
+ },
106
+ });
107
+ const body = await res.text();
108
+ if (res.status !== 200) {
109
+ throw new Error(`[${label}] HTTP ${res.status}\n${body}`);
110
+ }
111
+ let json: RawEnvelope;
112
+ try {
113
+ json = JSON.parse(body) as RawEnvelope;
114
+ } catch (e) {
115
+ throw new Error(
116
+ `[${label}] could not parse response: ${(e as Error).message}`,
117
+ { cause: e },
118
+ );
119
+ }
120
+ if (json.success === false) {
121
+ throw new Error(
122
+ `[${label}] ${json.msg ?? 'request failed'} (code ${json.code ?? '?'})`,
123
+ );
124
+ }
125
+ return json.data ?? json;
126
+ }
127
+
128
+ export async function fetchReport(
129
+ config: ResolvedConfig,
130
+ ): Promise<RenderInput> {
131
+ const platform = config.url.includes('bigmodel') ? 'ZHIPU' : 'ZAI';
132
+
133
+ const query = queryParams();
134
+ try {
135
+ const [model, tool, quotaRaw] = await Promise.all([
136
+ fetchJson(
137
+ `${config.url}/api/monitor/usage/model-usage`,
138
+ 'Model usage',
139
+ config.token,
140
+ query,
141
+ ),
142
+ fetchJson(
143
+ `${config.url}/api/monitor/usage/tool-usage`,
144
+ 'Tool usage',
145
+ config.token,
146
+ query,
147
+ ),
148
+ fetchJson(
149
+ `${config.url}/api/monitor/usage/quota/limit`,
150
+ 'Quota limit',
151
+ config.token,
152
+ ),
153
+ ]);
154
+ return {
155
+ platform,
156
+ model: model as ZaiModelUsage,
157
+ tool: tool as ZaiToolUsage,
158
+ quota: processQuotaLimit(quotaRaw as RawQuota),
159
+ };
160
+ } catch (e) {
161
+ throw new Error(`Request failed: ${(e as Error).message}`, { cause: e });
162
+ }
163
+ }