@d3ara1n/pi-provider-zhipu-coding-plan 1.0.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.
Files changed (2) hide show
  1. package/index.ts +205 -0
  2. package/package.json +22 -0
package/index.ts ADDED
@@ -0,0 +1,205 @@
1
+ /**
2
+ * pi-provider-zhipu-coding-plan
3
+ *
4
+ * Registers "zhipu-coding" provider with dynamic model discovery
5
+ * and usage quota reporting via the shared UsageRegistry.
6
+ *
7
+ * Auth: API key stored in ~/.pi/agent/auth.json under "zhipu-coding"
8
+ */
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import { usageRegistry } from "@d3ara1n/pi-usage-block-core";
11
+ import type { UsageProvider, UsageWindow } from "@d3ara1n/pi-usage-block-core";
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import os from "node:os";
15
+
16
+ // ── Config ────────────────────────────────────────────────────────────────
17
+
18
+ const PROVIDER_ID = "zhipu-coding";
19
+ const PROVIDER_NAME = "Zhipu AI (Coding Plan)";
20
+ const CODING_BASE_URL = "https://open.bigmodel.cn/api/coding/paas/v4";
21
+ const QUOTA_API_URL = "https://bigmodel.cn/api/monitor/usage/quota/limit";
22
+ const API_KEY_ENV = "ZHIPUAI_API_KEY";
23
+
24
+ // ── Types ─────────────────────────────────────────────────────────────────
25
+
26
+ interface ModelMeta {
27
+ contextWindow: number;
28
+ maxTokens: number;
29
+ reasoning: boolean;
30
+ input: ("text" | "image")[];
31
+ compat: Record<string, unknown>;
32
+ }
33
+
34
+ interface QuotaLimitItem {
35
+ type: "TOKENS_LIMIT" | "TIME_LIMIT";
36
+ percentage?: number;
37
+ usage?: number;
38
+ currentValue?: number;
39
+ remaining?: number;
40
+ nextResetTime?: number;
41
+ /** Window duration count (e.g. 5) paired with unit */
42
+ number?: number;
43
+ /** Window duration unit: 3 = hours */
44
+ unit?: number;
45
+ }
46
+
47
+ interface QuotaResponse {
48
+ code: number;
49
+ success: boolean;
50
+ data?: { limits?: QuotaLimitItem[]; level?: string };
51
+ }
52
+
53
+ interface ApiModel { id: string }
54
+
55
+ // ── Known model metadata (fallback) ───────────────────────────────────────
56
+
57
+ const DEFAULT_META: ModelMeta = {
58
+ contextWindow: 128_000,
59
+ maxTokens: 16_384,
60
+ reasoning: true,
61
+ input: ["text"],
62
+ compat: { supportsDeveloperRole: false, thinkingFormat: "zai" },
63
+ };
64
+
65
+ const ZAI_STREAM = {
66
+ supportsDeveloperRole: false,
67
+ thinkingFormat: "zai",
68
+ zaiToolStream: true,
69
+ } as const;
70
+
71
+ const KNOWN_MODELS: Record<string, Partial<ModelMeta>> = {
72
+ "glm-4.5": { contextWindow: 131_072, maxTokens: 98_304 },
73
+ "glm-4.5-air": { contextWindow: 131_072, maxTokens: 98_304 },
74
+ "glm-4.5-flash": { contextWindow: 131_072, maxTokens: 98_304 },
75
+ "glm-4.5v": { contextWindow: 65_536, maxTokens: 4_096, input: ["text", "image"] },
76
+ "glm-4.6": { contextWindow: 205_000, maxTokens: 131_072 },
77
+ "glm-4.6v": { contextWindow: 128_000, maxTokens: 4_096, input: ["text", "image"] },
78
+ "glm-4.6v-flash": { contextWindow: 128_000, maxTokens: 4_096, input: ["text", "image"] },
79
+ "glm-4.7": { contextWindow: 205_000, maxTokens: 131_072, compat: ZAI_STREAM },
80
+ "glm-4.7-flash": { contextWindow: 200_000, maxTokens: 131_072, compat: ZAI_STREAM },
81
+ "glm-4.7-flashx": { contextWindow: 200_000, maxTokens: 131_072, compat: ZAI_STREAM },
82
+ "glm-5": { contextWindow: 205_000, maxTokens: 131_072, compat: ZAI_STREAM },
83
+ "glm-5-turbo": { contextWindow: 200_000, maxTokens: 131_072, compat: ZAI_STREAM },
84
+ "glm-5.1": { contextWindow: 200_000, maxTokens: 131_072, compat: ZAI_STREAM },
85
+ "glm-5v-turbo": { contextWindow: 200_000, maxTokens: 131_072, input: ["text", "image"], compat: ZAI_STREAM },
86
+ };
87
+
88
+ // ── API helpers ───────────────────────────────────────────────────────────
89
+
90
+ function resolveApiKey(): string | undefined {
91
+ try {
92
+ const piHome = process.env.PI_HOME ?? path.join(os.homedir(), ".pi");
93
+ const authPath = path.join(piHome, "agent", "auth.json");
94
+ if (!fs.existsSync(authPath)) return undefined;
95
+ const auth = JSON.parse(fs.readFileSync(authPath, "utf8"));
96
+ return auth[PROVIDER_ID]?.apiKey ?? auth[PROVIDER_ID]?.key;
97
+ } catch { return undefined; }
98
+ }
99
+
100
+ async function fetchModelList(apiKey: string): Promise<ApiModel[]> {
101
+ const res = await fetch(`${CODING_BASE_URL}/models`, {
102
+ headers: { Authorization: `Bearer ${apiKey}` },
103
+ signal: AbortSignal.timeout(10_000),
104
+ });
105
+ if (!res.ok) throw new Error(`GET /models ${res.status}`);
106
+ return (await res.json() as { data?: ApiModel[] }).data ?? [];
107
+ }
108
+
109
+ async function fetchQuota(apiKey: string): Promise<QuotaResponse | null> {
110
+ try {
111
+ const res = await fetch(QUOTA_API_URL, {
112
+ headers: { Authorization: apiKey, "Content-Type": "application/json" },
113
+ signal: AbortSignal.timeout(10_000),
114
+ });
115
+ if (!res.ok) return null;
116
+ const body = await res.json() as QuotaResponse;
117
+ return body.success ? body : null;
118
+ } catch { return null; }
119
+ }
120
+
121
+ // ── Model config builder ──────────────────────────────────────────────────
122
+
123
+ function buildModelConfig(id: string) {
124
+ const k = KNOWN_MODELS[id];
125
+ return {
126
+ id,
127
+ name: id
128
+ .replace(/^glm-/, "GLM-")
129
+ .replace(/-/g, " ")
130
+ .replace(/\b(\w)/g, (_, c: string) => c.toUpperCase()),
131
+ api: "openai-completions" as const,
132
+ reasoning: k?.reasoning ?? DEFAULT_META.reasoning,
133
+ input: k?.input ?? DEFAULT_META.input,
134
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, // subscription plan
135
+ contextWindow: k?.contextWindow ?? DEFAULT_META.contextWindow,
136
+ maxTokens: k?.maxTokens ?? DEFAULT_META.maxTokens,
137
+ compat: { ...DEFAULT_META.compat, ...(k?.compat ?? {}) },
138
+ };
139
+ }
140
+
141
+ const FALLBACK_MODELS = Object.keys(KNOWN_MODELS).map(buildModelConfig);
142
+
143
+ // ── Usage provider ────────────────────────────────────────────────────────
144
+
145
+ function createUsageProvider(): UsageProvider {
146
+ return {
147
+ id: PROVIDER_ID,
148
+ name: "Zhipu Coding",
149
+ icon: "⚡",
150
+ async fetchUsage(): Promise<UsageWindow[]> {
151
+ const apiKey = resolveApiKey();
152
+ if (!apiKey) return [];
153
+
154
+ const quota = await fetchQuota(apiKey);
155
+ if (!quota?.data?.limits?.length) return [];
156
+
157
+ const limits = quota.data.limits;
158
+
159
+ // Each TOKENS_LIMIT is a quota window (5h, weekly, etc.)
160
+ // It carries its own percentage and nextResetTime.
161
+ // Lite plan has one; Pro plan may have two (5h + weekly).
162
+ const windows = limits
163
+ .filter(l => l.type === "TOKENS_LIMIT" && typeof l.percentage === "number")
164
+ .map(l => {
165
+ const period = l.number && l.unit ? `${l.number}${l.unit === 3 ? "h" : "w"}` : "";
166
+ return {
167
+ period,
168
+ used: l.percentage!,
169
+ limit: 100,
170
+ unit: "tokens" as const,
171
+ resetAt: l.nextResetTime ? new Date(l.nextResetTime) : undefined,
172
+ };
173
+ });
174
+
175
+ return windows;
176
+ },
177
+ };
178
+ }
179
+
180
+ // ── Entry point ───────────────────────────────────────────────────────────
181
+
182
+ export default async function (pi: ExtensionAPI) {
183
+ const apiKey = resolveApiKey();
184
+
185
+ let models = FALLBACK_MODELS;
186
+ if (apiKey) {
187
+ try {
188
+ const apiModels = await fetchModelList(apiKey);
189
+ if (apiModels.length) models = apiModels.map(m => buildModelConfig(m.id));
190
+ } catch (e) {
191
+ console.error("[zhipu-coding] model discovery failed, using fallback:", e);
192
+ }
193
+ }
194
+
195
+ pi.registerProvider(PROVIDER_ID, {
196
+ name: PROVIDER_NAME,
197
+ baseUrl: CODING_BASE_URL,
198
+ apiKey: `$${API_KEY_ENV}`,
199
+ api: "openai-completions",
200
+ authHeader: true,
201
+ models,
202
+ });
203
+
204
+ usageRegistry.register(createUsageProvider());
205
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@d3ara1n/pi-provider-zhipu-coding-plan",
3
+ "version": "1.0.0",
4
+ "description": "Zhipu AI Coding Plan provider for pi — auto-discovers models, reports usage quota",
5
+ "keywords": ["pi-package"],
6
+ "main": "index.ts",
7
+ "dependencies": {
8
+ "@d3ara1n/pi-usage-block-core": "^1.0.0"
9
+ },
10
+ "peerDependencies": {
11
+ "@earendil-works/pi-coding-agent": "*"
12
+ },
13
+ "pi": {
14
+ "extensions": ["./index.ts"]
15
+ },
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/d3ara1n/pi-extensions",
20
+ "directory": "packages/pi-provider-zhipu-coding-plan"
21
+ }
22
+ }