pi-provider-qoder-cn 0.1.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 (4) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +234 -0
  3. package/dist/index.js +2796 -0
  4. package/package.json +57 -0
package/dist/index.js ADDED
@@ -0,0 +1,2796 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // src/cn-naming.ts
17
+ function slugifyQoderCNModel(displayName) {
18
+ const slug = (displayName || "").trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9._-]/g, "").replace(/^-+|-+$/g, "");
19
+ return slug || "qodermodel";
20
+ }
21
+ function prettifyQoderCNModelName(displayName) {
22
+ const pretty = (displayName || "Qoder CN Model").replace(/Qwen(\d)/g, "Qwen $1").replace(/Qwen([\d.]+)-/g, "Qwen $1 ").replace(/DeepSeek\s*V(\d)-/g, "DeepSeek V$1 ").replace(/\s+/g, " ").trim();
23
+ return pretty.includes("Qoder CN") ? pretty : `${pretty} \xB7 Qoder CN`;
24
+ }
25
+ function toQoderCNModelInfo(upstreamKey, displayName) {
26
+ const friendly = upstreamKey ? QODER_CN_FRIENDLY_MODELS[upstreamKey] : void 0;
27
+ if (friendly) return friendly;
28
+ return {
29
+ id: slugifyQoderCNModel(displayName || upstreamKey),
30
+ name: prettifyQoderCNModelName(displayName || upstreamKey)
31
+ };
32
+ }
33
+ function toQoderCNUpstreamKey(modelId) {
34
+ const id = modelId || "";
35
+ const alias = CN_MODEL_KEY_ALIASES[id];
36
+ if (alias) return alias;
37
+ if (QODER_CN_FRIENDLY_MODELS[id]) return id;
38
+ if (Object.hasOwn(QODER_CN_FRIENDLY_MODELS, id)) return id;
39
+ return id || "auto";
40
+ }
41
+ function isQoderCNModelId(entry, modelId) {
42
+ if (!entry) return false;
43
+ const info = toQoderCNModelInfo(entry.key, entry.display_name);
44
+ if (info.id === modelId) return true;
45
+ if (entry.key === modelId) return true;
46
+ const stripped = (entry.display_name || "").replace(/\s+/g, "");
47
+ return stripped.toLowerCase() === modelId.toLowerCase();
48
+ }
49
+ var QODER_CN_FRIENDLY_MODELS, CN_MODEL_KEY_ALIASES;
50
+ var init_cn_naming = __esm({
51
+ "src/cn-naming.ts"() {
52
+ "use strict";
53
+ QODER_CN_FRIENDLY_MODELS = Object.freeze({
54
+ auto: { id: "auto", name: "Auto \xB7 Qoder CN" },
55
+ "qoder-cn": { id: "auto", name: "Auto \xB7 Qoder CN" },
56
+ qmodel_38max: { id: "qwen3.8-max", name: "Qwen 3.8-Max \xB7 Qoder CN" },
57
+ qfmodel: { id: "qwen3.8-flash", name: "Qwen 3.8-Flash \xB7 Qoder CN" },
58
+ qmodel_latest: { id: "qwen3.7-max", name: "Qwen 3.7-Max \xB7 Qoder CN" },
59
+ qmodel: { id: "qwen3.7-plus", name: "Qwen 3.7-Plus \xB7 Qoder CN" },
60
+ q37fmodel: { id: "qwen3.7-flash", name: "Qwen 3.7-Flash \xB7 Qoder CN" },
61
+ dmodel: { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro \xB7 Qoder CN" },
62
+ dfmodel: { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash \xB7 Qoder CN" },
63
+ gmodel: { id: "glm-5.3", name: "GLM-5.3 \xB7 Qoder CN" },
64
+ gfmodel: { id: "glm-5.3-flash", name: "GLM-5.3-Flash \xB7 Qoder CN" },
65
+ gm51model: { id: "glm-5.2", name: "GLM 5.2 \xB7 Qoder CN" },
66
+ kmodel_latest: { id: "kimi-k3", name: "Kimi-K3 \xB7 Qoder CN" },
67
+ kmodel: { id: "kimi-k2.7-code", name: "Kimi-K2.7-Code \xB7 Qoder CN" },
68
+ mmodel: { id: "minimax-m2.7", name: "MiniMax M2.7 \xB7 Qoder CN" }
69
+ });
70
+ CN_MODEL_KEY_ALIASES = Object.freeze({
71
+ "qwen3.8-max": "qmodel_38max",
72
+ "qwen3.8-flash": "qfmodel",
73
+ "qwen3.7-max": "qmodel_latest",
74
+ "qwen3.7-plus": "qmodel",
75
+ "qwen3.7-flash": "q37fmodel",
76
+ "deepseek-v4-pro": "dmodel",
77
+ "deepseek-v4-flash": "dfmodel",
78
+ "glm-5.3": "gmodel",
79
+ "glm-5.3-flash": "gfmodel",
80
+ "glm-5.2": "gm51model",
81
+ "glm-5.1": "gm51model",
82
+ "kimi-k3": "kmodel_latest",
83
+ "kimi-k2.7-code": "kmodel",
84
+ "kimi-k2.7": "kmodel",
85
+ "kimi-k2.6": "kmodel",
86
+ "kimi-k2.8-preview": "kmodel",
87
+ "qwen3.6-flash": "qfmodel",
88
+ "qwen3.6-plus": "qmodel",
89
+ "minimax-m2.7": "mmodel",
90
+ "minimax-m3": "mmodel"
91
+ });
92
+ }
93
+ });
94
+
95
+ // src/cosy.ts
96
+ import crypto from "node:crypto";
97
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
98
+ import { homedir } from "node:os";
99
+ import { dirname, join } from "node:path";
100
+ function rsaEncryptBase64(data) {
101
+ const key = {
102
+ key: qoderRSAPublicKey,
103
+ padding: crypto.constants.RSA_PKCS1_PADDING
104
+ };
105
+ const encrypted = crypto.publicEncrypt(key, typeof data === "string" ? Buffer.from(data) : data);
106
+ return encrypted.toString("base64");
107
+ }
108
+ function aesEncryptCBCBase64(plaintext, keyStr) {
109
+ const cipher = crypto.createCipheriv("aes-128-cbc", Buffer.from(keyStr), Buffer.from(keyStr));
110
+ let encrypted = cipher.update(plaintext, "utf8", "base64");
111
+ encrypted += cipher.final("base64");
112
+ return encrypted;
113
+ }
114
+ function computeSigPath(urlStr) {
115
+ const parsed = new URL(urlStr);
116
+ let sigPath = parsed.pathname;
117
+ if (sigPath.startsWith("/algo")) {
118
+ sigPath = sigPath.substring("/algo".length);
119
+ }
120
+ return sigPath;
121
+ }
122
+ function getHomeDir() {
123
+ return process.env.HOME || process.env.USERPROFILE || homedir();
124
+ }
125
+ function getMachineId() {
126
+ const home = getHomeDir();
127
+ const paths = [join(home, ".qoder", ".auth", "machine_id"), join(home, ".pi", "agent", "qoder-machine-id")];
128
+ for (const p of paths) {
129
+ if (existsSync(p)) {
130
+ try {
131
+ const val = readFileSync(p, "utf8").trim();
132
+ if (val) return val;
133
+ } catch {
134
+ }
135
+ }
136
+ }
137
+ const newId = crypto.randomUUID();
138
+ try {
139
+ const savePath = paths[1];
140
+ mkdirSync(dirname(savePath), { recursive: true });
141
+ writeFileSync(savePath, newId, "utf8");
142
+ } catch {
143
+ }
144
+ return newId;
145
+ }
146
+ function buildAuthHeaders(body, requestURL, creds) {
147
+ if (!creds.userID) {
148
+ throw new Error("cosy: user id is empty");
149
+ }
150
+ if (!creds.authToken) {
151
+ throw new Error("cosy: auth token is empty");
152
+ }
153
+ const aesKey = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
154
+ const userInfo = {
155
+ uid: creds.userID,
156
+ security_oauth_token: creds.authToken,
157
+ name: creds.name || "",
158
+ aid: "",
159
+ email: creds.email || ""
160
+ };
161
+ const infoB64 = aesEncryptCBCBase64(JSON.stringify(userInfo), aesKey);
162
+ const cosyKey = rsaEncryptBase64(aesKey);
163
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
164
+ const requestId = crypto.randomUUID();
165
+ const cosyPayload = {
166
+ version: "v1",
167
+ requestId,
168
+ info: infoB64,
169
+ cosyVersion: QODER_GATEWAY_COSY_VERSION,
170
+ ideVersion: ""
171
+ };
172
+ const payloadB64 = Buffer.from(JSON.stringify(cosyPayload)).toString("base64");
173
+ const sigPath = computeSigPath(requestURL);
174
+ const bodyBytes = body ? Buffer.isBuffer(body) ? body : Buffer.from(body) : Buffer.alloc(0);
175
+ const sig = crypto.createHash("md5").update(payloadB64).update("\n").update(cosyKey).update("\n").update(timestamp).update("\n").update(bodyBytes).update("\n").update(sigPath).digest("hex");
176
+ const bodyHash = crypto.createHash("md5").update(bodyBytes).digest("hex");
177
+ const bodyLen = bodyBytes.length.toString();
178
+ const machineID = creds.machineID || getMachineId();
179
+ return {
180
+ Authorization: `Bearer COSY.${payloadB64}.${sig}`,
181
+ "Cosy-Key": cosyKey,
182
+ "Cosy-User": creds.userID,
183
+ "Cosy-Date": timestamp,
184
+ "Cosy-Version": QODER_GATEWAY_COSY_VERSION,
185
+ "Cosy-Machineid": machineID,
186
+ "Cosy-Machinetoken": machineID,
187
+ "Cosy-Machinetype": QoderMachineTypeMagic,
188
+ "Cosy-Machineos": QoderMachineOS,
189
+ "Cosy-Clienttype": QODER_CLIENT_TYPE,
190
+ "Cosy-Clientip": "127.0.0.1",
191
+ "Cosy-Bodyhash": bodyHash,
192
+ "Cosy-Bodylength": bodyLen,
193
+ "Cosy-Sigpath": sigPath,
194
+ "Cosy-Data-Policy": QoderDataPolicy,
195
+ "Cosy-Organization-Id": "",
196
+ "Cosy-Organization-Tags": "",
197
+ "Login-Version": QoderLoginVersion,
198
+ "X-Request-Id": crypto.randomUUID()
199
+ };
200
+ }
201
+ var qoderRSAPublicKey, QODER_GATEWAY_COSY_VERSION, QODER_OPENAPI_COSY_VERSION, QODER_CLIENT_TYPE, QoderDataPolicy, QoderLoginVersion, QoderMachineOS, QoderMachineTypeMagic;
202
+ var init_cosy = __esm({
203
+ "src/cosy.ts"() {
204
+ "use strict";
205
+ qoderRSAPublicKey = `-----BEGIN PUBLIC KEY-----
206
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
207
+ 4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
208
+ 6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
209
+ XcW+ML9FoCI6AOvOzwIDAQAB
210
+ -----END PUBLIC KEY-----`;
211
+ QODER_GATEWAY_COSY_VERSION = "1.1.38";
212
+ QODER_OPENAPI_COSY_VERSION = "1.0.1";
213
+ QODER_CLIENT_TYPE = "5";
214
+ QoderDataPolicy = "disagree";
215
+ QoderLoginVersion = "v2";
216
+ QoderMachineOS = process.platform === "win32" ? process.arch === "arm64" ? "aarch64_windows" : "x86_64_windows" : process.arch === "arm64" ? "aarch64_linux" : "x86_64_linux";
217
+ QoderMachineTypeMagic = "5";
218
+ }
219
+ });
220
+
221
+ // src/vpc.ts
222
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
223
+ import { homedir as homedir2 } from "node:os";
224
+ import { join as join2 } from "node:path";
225
+ function getHomeDir2() {
226
+ return process.env.HOME || process.env.USERPROFILE || homedir2();
227
+ }
228
+ function piAgentDir() {
229
+ return join2(getHomeDir2(), ".pi", "agent");
230
+ }
231
+ function settingsFilePath() {
232
+ return join2(piAgentDir(), "qoder-cn-settings.json");
233
+ }
234
+ function parseQoderVpcInstance(raw) {
235
+ const value = String(raw ?? "").trim();
236
+ if (!value) return void 0;
237
+ let host = value;
238
+ if (host.startsWith("https://")) host = host.slice(8);
239
+ else if (host.startsWith("http://")) host = host.slice(7);
240
+ host = host.replace(/\/.*$/, "").toLowerCase();
241
+ if (host.endsWith(`.${QODER_VPC_SUFFIX}`)) {
242
+ host = host.slice(0, host.length - QODER_VPC_SUFFIX.length - 1);
243
+ }
244
+ if (host.endsWith("-gateway")) host = host.slice(0, -"-gateway".length);
245
+ if (host.endsWith("-openapi")) host = host.slice(0, -"-openapi".length);
246
+ return INSTANCE_NAME_RE.test(host) ? host : void 0;
247
+ }
248
+ function readVpcEndpointFromSettings() {
249
+ try {
250
+ const path = settingsFilePath();
251
+ if (!existsSync2(path)) return void 0;
252
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
253
+ const value = parsed?.vpc_endpoint ?? parsed?.vpcEndpoint ?? parsed?.vpcInstanceName;
254
+ if (value !== void 0 && value !== "") return String(value);
255
+ } catch {
256
+ }
257
+ return void 0;
258
+ }
259
+ function readVpcEndpointFromAuth() {
260
+ try {
261
+ const path = join2(piAgentDir(), "auth.json");
262
+ if (!existsSync2(path)) return void 0;
263
+ const auth = JSON.parse(readFileSync2(path, "utf8"));
264
+ const entry = auth?.["qoder-cn"];
265
+ const value = entry?.vpc_endpoint ?? entry?.vpcInstance;
266
+ if (value !== void 0 && value !== "") return String(value);
267
+ } catch {
268
+ }
269
+ return void 0;
270
+ }
271
+ function readVpcEndpointFromQoderIde() {
272
+ try {
273
+ const path = join2(getHomeDir2(), ".qoder-cn", "settings.json");
274
+ if (!existsSync2(path)) return void 0;
275
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
276
+ const value = parsed?.vpc_endpoint ?? parsed?.vpcEndpoint ?? parsed?.vpcInstanceName;
277
+ if (value !== void 0 && value !== "") return String(value);
278
+ } catch {
279
+ }
280
+ return void 0;
281
+ }
282
+ function readQoderVpcEndpoint() {
283
+ const fromEnv = process.env.QODER_VPC_ENDPOINT || process.env.QODERCN_VPC_ENDPOINT;
284
+ if (fromEnv) return fromEnv;
285
+ return readVpcEndpointFromSettings() ?? readVpcEndpointFromAuth() ?? readVpcEndpointFromQoderIde() ?? "";
286
+ }
287
+ function isResetValue(value) {
288
+ const lowered = value.toLowerCase();
289
+ return !value || lowered === "official" || lowered === "default" || lowered === "none" || lowered === "clear";
290
+ }
291
+ function resolveQoderCNEndpoints(rawInput) {
292
+ const value = String(rawInput ?? "").trim();
293
+ const official = { raw: "", ...QODER_CN_OFFICIAL, isDefault: true };
294
+ if (isResetValue(value)) return official;
295
+ let host = value;
296
+ if (host.startsWith("https://")) host = host.slice(8);
297
+ else if (host.startsWith("http://")) host = host.slice(7);
298
+ host = host.replace(/\/.*$/, "").trim().toLowerCase();
299
+ if (OFFICIAL_HOSTS.has(host)) return official;
300
+ const isVpcHost = host.endsWith(`.${QODER_VPC_SUFFIX}`);
301
+ if (isVpcHost || !host.includes(".")) {
302
+ const instance = parseQoderVpcInstance(host);
303
+ if (instance) {
304
+ return {
305
+ raw: `${instance}.${QODER_VPC_SUFFIX}`,
306
+ instance,
307
+ baseUrl: `https://${instance}-gateway.${QODER_VPC_SUFFIX}/`,
308
+ openApiUrl: `https://${instance}-openapi.${QODER_VPC_SUFFIX}`,
309
+ centerUrl: `https://${instance}-gateway.${QODER_VPC_SUFFIX}`,
310
+ manageUrl: `https://${instance}.${QODER_VPC_SUFFIX}`,
311
+ isDefault: false
312
+ };
313
+ }
314
+ }
315
+ const baseOrigin = `https://${host}`;
316
+ return {
317
+ raw: host,
318
+ baseUrl: `${baseOrigin}/`,
319
+ openApiUrl: baseOrigin,
320
+ centerUrl: baseOrigin,
321
+ manageUrl: baseOrigin,
322
+ isDefault: false
323
+ };
324
+ }
325
+ function getQoderCNEndpoints() {
326
+ if (!cachedEndpoints) cachedEndpoints = resolveQoderCNEndpoints(readQoderVpcEndpoint());
327
+ return cachedEndpoints;
328
+ }
329
+ function saveQoderVpcEndpoint(raw) {
330
+ try {
331
+ const dir = piAgentDir();
332
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
333
+ writeFileSync2(settingsFilePath(), JSON.stringify({ vpc_endpoint: raw || "" }, null, 2), "utf8");
334
+ } catch (error) {
335
+ console.error("[pi-provider-qoder-cn] Failed to save qoder-cn-settings.json:", error);
336
+ }
337
+ }
338
+ function setQoderCNEndpoint(raw) {
339
+ cachedEndpoints = resolveQoderCNEndpoints(raw);
340
+ saveQoderVpcEndpoint(cachedEndpoints.raw);
341
+ return cachedEndpoints;
342
+ }
343
+ var QODER_VPC_SUFFIX, OFFICIAL_HOSTS, QODER_CN_OFFICIAL, INSTANCE_NAME_RE, cachedEndpoints;
344
+ var init_vpc = __esm({
345
+ "src/vpc.ts"() {
346
+ "use strict";
347
+ QODER_VPC_SUFFIX = "vpc.qoder.com.cn";
348
+ OFFICIAL_HOSTS = /* @__PURE__ */ new Set(["gateway.qoder.com.cn", "openapi.qoder.com.cn", "qoder.com.cn"]);
349
+ QODER_CN_OFFICIAL = Object.freeze({
350
+ baseUrl: "https://gateway.qoder.com.cn/",
351
+ openApiUrl: "https://openapi.qoder.com.cn",
352
+ centerUrl: "https://gateway.qoder.com.cn",
353
+ manageUrl: "https://qoder.com.cn"
354
+ });
355
+ INSTANCE_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
356
+ cachedEndpoints = null;
357
+ }
358
+ });
359
+
360
+ // src/region.ts
361
+ function getQoderRegionConfig(mode) {
362
+ return QODER_REGIONS[mode];
363
+ }
364
+ function getRegionValue(mode, field) {
365
+ if (mode === "cn") return getQoderCNEndpoints()[field];
366
+ return getQoderRegionConfig(mode)[field];
367
+ }
368
+ function getQoderBaseUrl(mode) {
369
+ return getRegionValue(mode, "baseUrl");
370
+ }
371
+ function getQoderOpenApiUrl(mode) {
372
+ return getRegionValue(mode, "openApiUrl");
373
+ }
374
+ function getQoderCenterUrl(mode) {
375
+ return getRegionValue(mode, "centerUrl");
376
+ }
377
+ function getQoderModelListURL(mode) {
378
+ return `${getQoderBaseUrl(mode)}algo/api/v2/model/list?Encode=1`;
379
+ }
380
+ function getQoderChatURL(mode) {
381
+ return `${getQoderBaseUrl(mode)}algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1`;
382
+ }
383
+ function getQoderExchangeURL(mode) {
384
+ return `${getQoderOpenApiUrl(mode)}/api/v1/jobToken/exchange`;
385
+ }
386
+ function getQoderUserInfoURL(mode) {
387
+ return `${getQoderOpenApiUrl(mode)}/api/v1/userinfo`;
388
+ }
389
+ function getQoderUsageURL(mode) {
390
+ return `${getQoderOpenApiUrl(mode)}/api/v2/quota/usage`;
391
+ }
392
+ function getQoderRefreshURL(mode) {
393
+ return `${getQoderCenterUrl(mode)}/algo/api/v3/user/refresh_token`;
394
+ }
395
+ function getQoderDeviceLoginURL(codeChallenge, machineID, nonce) {
396
+ const baseUrl = getQoderRegionConfig("global").deviceLoginUrl;
397
+ if (!baseUrl) throw new Error("Qoder browser login URL is not configured");
398
+ return `${baseUrl}?challenge=${codeChallenge}&challenge_method=S256&machine_id=${machineID}&nonce=${nonce}`;
399
+ }
400
+ function getQoderDevicePollURL(nonce, codeVerifier) {
401
+ const baseUrl = getQoderRegionConfig("global").openApiUrl;
402
+ return `${baseUrl}/api/v1/deviceToken/poll?nonce=${encodeURIComponent(nonce)}&verifier=${encodeURIComponent(codeVerifier)}&challenge_method=S256`;
403
+ }
404
+ var QODER_REGIONS, QODER_PROVIDER_MODES;
405
+ var init_region = __esm({
406
+ "src/region.ts"() {
407
+ "use strict";
408
+ init_vpc();
409
+ QODER_REGIONS = {
410
+ global: {
411
+ mode: "global",
412
+ providerID: "qoder",
413
+ baseUrl: "https://api3.qoder.sh/",
414
+ openApiUrl: "https://openapi.qoder.sh",
415
+ centerUrl: "https://center.qoder.sh",
416
+ manageUrl: "https://qoder.com",
417
+ patManageUrl: "https://qoder.com/account/integrations",
418
+ deviceLoginUrl: "https://qoder.com/device/selectAccounts",
419
+ modelCacheFile: "qoder-models-cache.json",
420
+ patEnvNames: ["QODER_API_KEY", "QODER_PERSONAL_ACCESS_TOKEN", "QODER_PAT"],
421
+ loginName: "Qoder (Browser OAuth / PAT)",
422
+ userNameFallback: "Qoder User",
423
+ userEmailFallback: "user@qoder.com",
424
+ usageTitle: "Qoder AI Plan",
425
+ supportsBrowserLogin: true
426
+ },
427
+ cn: {
428
+ mode: "cn",
429
+ providerID: "qoder-cn",
430
+ baseUrl: QODER_CN_OFFICIAL.baseUrl,
431
+ openApiUrl: QODER_CN_OFFICIAL.openApiUrl,
432
+ centerUrl: QODER_CN_OFFICIAL.centerUrl,
433
+ manageUrl: QODER_CN_OFFICIAL.manageUrl,
434
+ patManageUrl: "https://qoder.com.cn/account/integrations",
435
+ modelCacheFile: "qoder-cn-models-cache.json",
436
+ patEnvNames: ["QODERCN_API_KEY", "QODERCN_PERSONAL_ACCESS_TOKEN", "QODERCN_PAT"],
437
+ loginName: "Qoder CN (PAT)",
438
+ userNameFallback: "Qoder CN User",
439
+ userEmailFallback: "user@qoder.com.cn",
440
+ usageTitle: "Qoder CN Plan",
441
+ supportsBrowserLogin: false
442
+ }
443
+ };
444
+ QODER_PROVIDER_MODES = ["cn"];
445
+ }
446
+ });
447
+
448
+ // src/catalog.ts
449
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
450
+ import { homedir as homedir3 } from "node:os";
451
+ import { dirname as dirname2, join as join3 } from "node:path";
452
+ function getHomeDir3() {
453
+ return process.env.HOME || process.env.USERPROFILE || homedir3();
454
+ }
455
+ function getQoderCachePath(mode) {
456
+ return join3(getHomeDir3(), ".pi", "agent", getQoderRegionConfig(mode).modelCacheFile);
457
+ }
458
+ function readParsedModelCache(mode) {
459
+ const cachePath = getQoderCachePath(mode);
460
+ if (modelCacheMem.has(cachePath)) {
461
+ return modelCacheMem.get(cachePath) ?? null;
462
+ }
463
+ if (!existsSync3(cachePath)) {
464
+ modelCacheMem.set(cachePath, null);
465
+ return null;
466
+ }
467
+ try {
468
+ const data = JSON.parse(readFileSync3(cachePath, "utf8"));
469
+ modelCacheMem.set(cachePath, data);
470
+ return data;
471
+ } catch {
472
+ modelCacheMem.set(cachePath, null);
473
+ return null;
474
+ }
475
+ }
476
+ function writeParsedModelCache(mode, data) {
477
+ const cachePath = getQoderCachePath(mode);
478
+ mkdirSync3(dirname2(cachePath), { recursive: true });
479
+ writeFileSync3(cachePath, JSON.stringify(data, null, 2), "utf-8");
480
+ modelCacheMem.set(cachePath, data);
481
+ }
482
+ function toQoderModelId(displayName) {
483
+ return (displayName || "QoderModel").replace(/\s+/g, "");
484
+ }
485
+ function buildThinkingLevelMap(entry) {
486
+ const tc = entry.thinking_config;
487
+ if (!tc) return void 0;
488
+ const efforts = tc.enabled?.efforts;
489
+ if (efforts && typeof efforts === "object") {
490
+ const supported = new Set(Object.keys(efforts));
491
+ const map = { off: tc.disabled ? "disabled" : null };
492
+ for (const level of PI_THINKING_LEVELS) {
493
+ map[level] = supported.has(level) ? level : null;
494
+ }
495
+ return map;
496
+ }
497
+ if (tc.enabled) {
498
+ const map = { off: tc.disabled ? "disabled" : null };
499
+ for (const level of PI_THINKING_LEVELS) {
500
+ map[level] = "enabled";
501
+ }
502
+ return map;
503
+ }
504
+ return void 0;
505
+ }
506
+ function getCachedModels(mode) {
507
+ const data = readParsedModelCache(mode);
508
+ if (data && Array.isArray(data.models)) {
509
+ const models = data.models.map((model) => {
510
+ const config = data.configs?.[model.id];
511
+ const display = config?.display_name;
512
+ if (mode === "cn") return applyCnModelIdentity(model, config, display);
513
+ const staticModel = staticModels.find((seed) => seed.upstreamKey === model.id);
514
+ if (display) return { ...model, id: toQoderModelId(display), name: display };
515
+ if (staticModel) return { ...model, id: staticModel.id, name: staticModel.name };
516
+ return model.name ? { ...model, id: toQoderModelId(model.name) } : model;
517
+ });
518
+ if (data.configs && typeof data.configs === "object" && !hasAutoCatalogEntry(data.configs, mode)) {
519
+ return models.filter((model) => model.id.toLowerCase() !== "auto");
520
+ }
521
+ return models;
522
+ }
523
+ return mode === "cn" ? staticCnModels : staticModels;
524
+ }
525
+ function hasAutoCatalogEntry(configs, mode) {
526
+ return Object.values(configs).some((entry) => {
527
+ if (!entry || typeof entry !== "object") return false;
528
+ const config = entry;
529
+ if (config.key?.toLowerCase() === "auto") return true;
530
+ return mode === "cn" ? toQoderCNModelInfo(config.key, config.display_name).id === "auto" : toQoderModelId(config.display_name) === "auto";
531
+ });
532
+ }
533
+ function applyCnModelIdentity(model, config, display) {
534
+ const key = config?.key ?? model.upstreamKey ?? model.id;
535
+ const info = toQoderCNModelInfo(key, display ?? model.name);
536
+ return { ...model, id: info.id, name: info.name, upstreamKey: key };
537
+ }
538
+ function getCachedModelConfig(modelId, mode) {
539
+ const data = readParsedModelCache(mode);
540
+ if (data) {
541
+ const direct = data.configs?.[modelId];
542
+ const matchesDirect = direct && (mode === "cn" ? isQoderCNModelId(direct, modelId) : toQoderModelId(direct.display_name) === modelId);
543
+ if (matchesDirect) {
544
+ return withMaxContextAsDefault(direct);
545
+ }
546
+ const legacyEntry = Object.values(data.configs || {}).find(
547
+ (entry) => entry && typeof entry === "object" && // CN accepts the slug, the raw upstream key, and the upstream id scheme.
548
+ (mode === "cn" ? isQoderCNModelId(entry, modelId) : toQoderModelId(entry.display_name) === modelId)
549
+ );
550
+ if (legacyEntry) {
551
+ return withMaxContextAsDefault(legacyEntry);
552
+ }
553
+ }
554
+ const staticModel = (mode === "cn" ? staticCnModels : staticModels).find((model) => model.id === modelId);
555
+ if (staticModel) {
556
+ return {
557
+ key: staticModel.upstreamKey || modelId,
558
+ is_reasoning: staticModel.reasoning,
559
+ source: "system"
560
+ };
561
+ }
562
+ if (mode === "cn") {
563
+ const key = toQoderCNUpstreamKey(modelId);
564
+ if (key) {
565
+ const seed = staticCnModels.find((model) => model.upstreamKey === key);
566
+ return {
567
+ key,
568
+ is_reasoning: seed?.reasoning ?? true,
569
+ source: "system"
570
+ };
571
+ }
572
+ }
573
+ return null;
574
+ }
575
+ function contextWindowFromCatalog(entry) {
576
+ const contextConfig = entry.context_config;
577
+ if (contextConfig && typeof contextConfig === "object") {
578
+ let advertised = 0;
579
+ for (const configVal of Object.values(contextConfig)) {
580
+ if (configVal && typeof configVal === "object" && typeof configVal.token_count === "number") {
581
+ if (configVal.token_count > advertised) advertised = configVal.token_count;
582
+ }
583
+ }
584
+ if (advertised > 0) return advertised;
585
+ }
586
+ return DEFAULT_CONTEXT_WINDOW;
587
+ }
588
+ function withMaxContextAsDefault(entry) {
589
+ const contextConfig = entry.context_config;
590
+ if (!contextConfig || typeof contextConfig !== "object") return entry;
591
+ const maxTokenCount = Math.max(
592
+ ...Object.values(contextConfig).map((config) => typeof config?.token_count === "number" ? config.token_count : 0)
593
+ );
594
+ if (maxTokenCount <= 0) return entry;
595
+ return {
596
+ ...entry,
597
+ context_config: Object.fromEntries(
598
+ Object.entries(contextConfig).map(([name, config]) => [
599
+ name,
600
+ { ...config, is_default: config.token_count === maxTokenCount }
601
+ ])
602
+ )
603
+ };
604
+ }
605
+ function isCacheStale(mode) {
606
+ const data = readParsedModelCache(mode);
607
+ if (!data || typeof data.updatedAt !== "number") return true;
608
+ return Date.now() - data.updatedAt > 36e5;
609
+ }
610
+ async function updateQoderModelsCache(authToken, userID, name, email, mode) {
611
+ const modelListURL = getQoderModelListURL(mode);
612
+ try {
613
+ const headers = buildAuthHeaders(null, modelListURL, {
614
+ userID,
615
+ authToken,
616
+ name,
617
+ email
618
+ });
619
+ const response = await fetch(modelListURL, {
620
+ method: "GET",
621
+ headers: {
622
+ Accept: "application/json",
623
+ ...headers
624
+ }
625
+ });
626
+ if (!response.ok) {
627
+ return;
628
+ }
629
+ const resData = await response.json();
630
+ const chatModels = resData.chat || [];
631
+ if (chatModels.length === 0) return;
632
+ const newModels = [];
633
+ const configs = {};
634
+ for (const entry of chatModels) {
635
+ const key = entry.key;
636
+ if (!key || !entry.enable || !entry.display_name) continue;
637
+ const display = entry.display_name;
638
+ const ctxLen = contextWindowFromCatalog(entry);
639
+ const isVL = !!entry.is_vl;
640
+ const isReasoning = !!entry.is_reasoning || !!entry.thinking_config;
641
+ const supportsEffort = !!entry.thinking_config?.enabled?.efforts;
642
+ const thinkingLevelMap = buildThinkingLevelMap(entry);
643
+ const modelInfo = mode === "cn" ? toQoderCNModelInfo(key, display) : { id: toQoderModelId(display), name: display };
644
+ configs[modelInfo.id] = entry;
645
+ if (mode === "cn" && modelInfo.id !== key) configs[key] = entry;
646
+ if (newModels.some((model) => model.id === modelInfo.id)) continue;
647
+ newModels.push({
648
+ id: modelInfo.id,
649
+ name: modelInfo.name,
650
+ api: "qoder-api",
651
+ provider: getQoderRegionConfig(mode).providerID,
652
+ baseUrl: getQoderBaseUrl(mode),
653
+ reasoning: isReasoning,
654
+ supportsEffort,
655
+ thinkingLevelMap,
656
+ input: isVL ? ["text", "image"] : ["text"],
657
+ cost: ZERO_COST,
658
+ contextWindow: ctxLen,
659
+ maxTokens: MAX_OUTPUT_TOKENS
660
+ });
661
+ }
662
+ if (newModels.length === 0) return;
663
+ const cacheData = {
664
+ updatedAt: Date.now(),
665
+ models: newModels,
666
+ configs
667
+ };
668
+ writeParsedModelCache(mode, cacheData);
669
+ } catch {
670
+ }
671
+ }
672
+ var ZERO_COST, MAX_OUTPUT_TOKENS, DEFAULT_CONTEXT_WINDOW, modelCacheMem, staticModels, staticCnModels, PI_THINKING_LEVELS;
673
+ var init_catalog = __esm({
674
+ "src/catalog.ts"() {
675
+ "use strict";
676
+ init_cn_naming();
677
+ init_cosy();
678
+ init_region();
679
+ ZERO_COST = Object.freeze({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
680
+ MAX_OUTPUT_TOKENS = 131072;
681
+ DEFAULT_CONTEXT_WINDOW = 1e6;
682
+ modelCacheMem = /* @__PURE__ */ new Map();
683
+ staticModels = [
684
+ {
685
+ id: "Auto",
686
+ upstreamKey: "auto",
687
+ name: "Auto",
688
+ api: "qoder-api",
689
+ provider: "qoder",
690
+ baseUrl: getQoderBaseUrl("global"),
691
+ reasoning: true,
692
+ supportsEffort: false,
693
+ input: ["text", "image"],
694
+ cost: ZERO_COST,
695
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
696
+ maxTokens: MAX_OUTPUT_TOKENS
697
+ },
698
+ {
699
+ id: "Ultimate",
700
+ upstreamKey: "ultimate",
701
+ name: "Ultimate",
702
+ api: "qoder-api",
703
+ provider: "qoder",
704
+ baseUrl: getQoderBaseUrl("global"),
705
+ reasoning: true,
706
+ supportsEffort: true,
707
+ input: ["text", "image"],
708
+ cost: ZERO_COST,
709
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
710
+ maxTokens: MAX_OUTPUT_TOKENS
711
+ },
712
+ {
713
+ id: "Performance",
714
+ upstreamKey: "performance",
715
+ name: "Performance",
716
+ api: "qoder-api",
717
+ provider: "qoder",
718
+ baseUrl: getQoderBaseUrl("global"),
719
+ reasoning: true,
720
+ supportsEffort: true,
721
+ input: ["text", "image"],
722
+ cost: ZERO_COST,
723
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
724
+ maxTokens: MAX_OUTPUT_TOKENS
725
+ },
726
+ {
727
+ id: "Efficient",
728
+ upstreamKey: "efficient",
729
+ name: "Efficient",
730
+ api: "qoder-api",
731
+ provider: "qoder",
732
+ baseUrl: getQoderBaseUrl("global"),
733
+ reasoning: false,
734
+ supportsEffort: false,
735
+ input: ["text", "image"],
736
+ cost: ZERO_COST,
737
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
738
+ maxTokens: MAX_OUTPUT_TOKENS
739
+ },
740
+ {
741
+ id: "Lite",
742
+ upstreamKey: "lite",
743
+ name: "Lite",
744
+ api: "qoder-api",
745
+ provider: "qoder",
746
+ baseUrl: getQoderBaseUrl("global"),
747
+ reasoning: false,
748
+ supportsEffort: false,
749
+ input: ["text"],
750
+ cost: ZERO_COST,
751
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
752
+ maxTokens: MAX_OUTPUT_TOKENS
753
+ },
754
+ {
755
+ id: "Qwen3.7Plus",
756
+ upstreamKey: "qmodel",
757
+ name: "Qwen3.7 Plus",
758
+ api: "qoder-api",
759
+ provider: "qoder",
760
+ baseUrl: getQoderBaseUrl("global"),
761
+ reasoning: false,
762
+ supportsEffort: false,
763
+ input: ["text", "image"],
764
+ cost: ZERO_COST,
765
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
766
+ maxTokens: MAX_OUTPUT_TOKENS
767
+ },
768
+ {
769
+ id: "Cantus",
770
+ upstreamKey: "cmodel",
771
+ name: "Cantus",
772
+ api: "qoder-api",
773
+ provider: "qoder",
774
+ baseUrl: getQoderBaseUrl("global"),
775
+ reasoning: true,
776
+ supportsEffort: true,
777
+ input: ["text", "image"],
778
+ cost: ZERO_COST,
779
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
780
+ maxTokens: MAX_OUTPUT_TOKENS
781
+ },
782
+ {
783
+ id: "Qwen3.8-Max",
784
+ upstreamKey: "qmodel_preview",
785
+ name: "Qwen3.8-Max",
786
+ api: "qoder-api",
787
+ provider: "qoder",
788
+ baseUrl: getQoderBaseUrl("global"),
789
+ reasoning: true,
790
+ supportsEffort: true,
791
+ input: ["text", "image"],
792
+ cost: ZERO_COST,
793
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
794
+ maxTokens: MAX_OUTPUT_TOKENS
795
+ },
796
+ {
797
+ id: "Qwen3.7-Max",
798
+ upstreamKey: "qmodel_latest",
799
+ name: "Qwen3.7-Max",
800
+ api: "qoder-api",
801
+ provider: "qoder",
802
+ baseUrl: getQoderBaseUrl("global"),
803
+ reasoning: false,
804
+ supportsEffort: false,
805
+ input: ["text", "image"],
806
+ cost: ZERO_COST,
807
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
808
+ maxTokens: MAX_OUTPUT_TOKENS
809
+ },
810
+ {
811
+ id: "DeepSeek-V4-Pro",
812
+ upstreamKey: "dmodel",
813
+ name: "DeepSeek-V4-Pro",
814
+ api: "qoder-api",
815
+ provider: "qoder",
816
+ baseUrl: getQoderBaseUrl("global"),
817
+ reasoning: true,
818
+ supportsEffort: true,
819
+ input: ["text", "image"],
820
+ cost: ZERO_COST,
821
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
822
+ maxTokens: MAX_OUTPUT_TOKENS
823
+ },
824
+ {
825
+ id: "DeepSeek-V4-Flash",
826
+ upstreamKey: "dfmodel",
827
+ name: "DeepSeek-V4-Flash",
828
+ api: "qoder-api",
829
+ provider: "qoder",
830
+ baseUrl: getQoderBaseUrl("global"),
831
+ reasoning: true,
832
+ supportsEffort: true,
833
+ input: ["text", "image"],
834
+ cost: ZERO_COST,
835
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
836
+ maxTokens: MAX_OUTPUT_TOKENS
837
+ },
838
+ {
839
+ id: "GLM-5.2",
840
+ upstreamKey: "gm51model",
841
+ name: "GLM-5.2",
842
+ api: "qoder-api",
843
+ provider: "qoder",
844
+ baseUrl: getQoderBaseUrl("global"),
845
+ reasoning: true,
846
+ supportsEffort: true,
847
+ input: ["text", "image"],
848
+ cost: ZERO_COST,
849
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
850
+ maxTokens: MAX_OUTPUT_TOKENS
851
+ },
852
+ {
853
+ id: "Kimi-K2.7-Code",
854
+ upstreamKey: "kmodel",
855
+ name: "Kimi-K2.7-Code",
856
+ api: "qoder-api",
857
+ provider: "qoder",
858
+ baseUrl: getQoderBaseUrl("global"),
859
+ reasoning: false,
860
+ supportsEffort: false,
861
+ input: ["text", "image"],
862
+ cost: ZERO_COST,
863
+ // Catalog advertises 256K; not included in the 1M live test in issue #13.
864
+ contextWindow: 256e3,
865
+ maxTokens: MAX_OUTPUT_TOKENS
866
+ },
867
+ {
868
+ id: "Kimi-K3",
869
+ upstreamKey: "kmodel_latest",
870
+ name: "Kimi-K3",
871
+ api: "qoder-api",
872
+ provider: "qoder",
873
+ baseUrl: getQoderBaseUrl("global"),
874
+ reasoning: false,
875
+ supportsEffort: false,
876
+ input: ["text", "image"],
877
+ cost: ZERO_COST,
878
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
879
+ maxTokens: MAX_OUTPUT_TOKENS
880
+ },
881
+ {
882
+ id: "MiniMax-M3",
883
+ upstreamKey: "mmodel",
884
+ name: "MiniMax-M3",
885
+ api: "qoder-api",
886
+ provider: "qoder",
887
+ baseUrl: getQoderBaseUrl("global"),
888
+ reasoning: false,
889
+ supportsEffort: false,
890
+ input: ["text", "image"],
891
+ cost: ZERO_COST,
892
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
893
+ maxTokens: MAX_OUTPUT_TOKENS
894
+ }
895
+ ];
896
+ staticCnModels = [
897
+ {
898
+ id: "auto",
899
+ upstreamKey: "auto",
900
+ name: "Auto \xB7 Qoder CN",
901
+ api: "qoder-api",
902
+ provider: "qoder-cn",
903
+ baseUrl: getQoderBaseUrl("cn"),
904
+ reasoning: true,
905
+ supportsEffort: false,
906
+ input: ["text", "image"],
907
+ cost: ZERO_COST,
908
+ // CN Auto has not been live-tested at 1M; the live catalog reports 200K.
909
+ contextWindow: 2e5,
910
+ maxTokens: MAX_OUTPUT_TOKENS,
911
+ description: "Qoder CN smart routing; live catalog reports 200K max input."
912
+ },
913
+ {
914
+ id: "qwen3.8-max",
915
+ upstreamKey: "qmodel_38max",
916
+ name: "Qwen 3.8-Max \xB7 Qoder CN",
917
+ api: "qoder-api",
918
+ provider: "qoder-cn",
919
+ baseUrl: getQoderBaseUrl("cn"),
920
+ reasoning: true,
921
+ supportsEffort: true,
922
+ input: ["text", "image"],
923
+ cost: ZERO_COST,
924
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
925
+ maxTokens: MAX_OUTPUT_TOKENS,
926
+ description: "Qwen 3.8-Max (qmodel_38max); 1M context."
927
+ },
928
+ {
929
+ id: "qwen3.8-flash",
930
+ upstreamKey: "qfmodel",
931
+ name: "Qwen 3.8-Flash \xB7 Qoder CN",
932
+ api: "qoder-api",
933
+ provider: "qoder-cn",
934
+ baseUrl: getQoderBaseUrl("cn"),
935
+ reasoning: true,
936
+ supportsEffort: true,
937
+ input: ["text", "image"],
938
+ cost: ZERO_COST,
939
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
940
+ maxTokens: MAX_OUTPUT_TOKENS,
941
+ description: "Qwen 3.8-Flash (qfmodel); multimodal MoE, 1M context."
942
+ },
943
+ {
944
+ id: "qwen3.7-max",
945
+ upstreamKey: "qmodel_latest",
946
+ name: "Qwen 3.7-Max \xB7 Qoder CN",
947
+ api: "qoder-api",
948
+ provider: "qoder-cn",
949
+ baseUrl: getQoderBaseUrl("cn"),
950
+ reasoning: true,
951
+ supportsEffort: false,
952
+ input: ["text", "image"],
953
+ cost: ZERO_COST,
954
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
955
+ maxTokens: MAX_OUTPUT_TOKENS,
956
+ description: "Qwen 3.7-Max (qmodel_latest); context options 200K/400K/1M."
957
+ },
958
+ {
959
+ id: "qwen3.7-plus",
960
+ upstreamKey: "qmodel",
961
+ name: "Qwen 3.7-Plus \xB7 Qoder CN",
962
+ api: "qoder-api",
963
+ provider: "qoder-cn",
964
+ baseUrl: getQoderBaseUrl("cn"),
965
+ reasoning: true,
966
+ supportsEffort: false,
967
+ input: ["text", "image"],
968
+ cost: ZERO_COST,
969
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
970
+ maxTokens: MAX_OUTPUT_TOKENS,
971
+ description: "Qwen 3.7-Plus (qmodel); context options 200K/400K/1M."
972
+ },
973
+ {
974
+ id: "qwen3.7-flash",
975
+ upstreamKey: "q37fmodel",
976
+ name: "Qwen 3.7-Flash \xB7 Qoder CN",
977
+ api: "qoder-api",
978
+ provider: "qoder-cn",
979
+ baseUrl: getQoderBaseUrl("cn"),
980
+ reasoning: true,
981
+ supportsEffort: false,
982
+ input: ["text", "image"],
983
+ cost: ZERO_COST,
984
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
985
+ maxTokens: MAX_OUTPUT_TOKENS,
986
+ description: "Qwen 3.7-Flash (q37fmodel); vision-language Flash, 1M context."
987
+ },
988
+ {
989
+ id: "deepseek-v4-pro",
990
+ upstreamKey: "dmodel",
991
+ name: "DeepSeek V4 Pro \xB7 Qoder CN",
992
+ api: "qoder-api",
993
+ provider: "qoder-cn",
994
+ baseUrl: getQoderBaseUrl("cn"),
995
+ reasoning: true,
996
+ supportsEffort: true,
997
+ input: ["text", "image"],
998
+ cost: ZERO_COST,
999
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
1000
+ maxTokens: MAX_OUTPUT_TOKENS,
1001
+ description: "DeepSeek V4 Pro (dmodel); 1M context."
1002
+ },
1003
+ {
1004
+ id: "deepseek-v4-flash",
1005
+ upstreamKey: "dfmodel",
1006
+ name: "DeepSeek V4 Flash \xB7 Qoder CN",
1007
+ api: "qoder-api",
1008
+ provider: "qoder-cn",
1009
+ baseUrl: getQoderBaseUrl("cn"),
1010
+ reasoning: true,
1011
+ supportsEffort: true,
1012
+ input: ["text", "image"],
1013
+ cost: ZERO_COST,
1014
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
1015
+ maxTokens: MAX_OUTPUT_TOKENS,
1016
+ description: "DeepSeek V4 Flash (dfmodel); 1M context."
1017
+ },
1018
+ {
1019
+ id: "glm-5.3",
1020
+ upstreamKey: "gmodel",
1021
+ name: "GLM-5.3 \xB7 Qoder CN",
1022
+ api: "qoder-api",
1023
+ provider: "qoder-cn",
1024
+ baseUrl: getQoderBaseUrl("cn"),
1025
+ reasoning: true,
1026
+ supportsEffort: true,
1027
+ input: ["text", "image"],
1028
+ cost: ZERO_COST,
1029
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
1030
+ maxTokens: MAX_OUTPUT_TOKENS,
1031
+ description: "GLM-5.3 (gmodel); Zhipu flagship open-source, 1M context."
1032
+ },
1033
+ {
1034
+ id: "glm-5.3-flash",
1035
+ upstreamKey: "gfmodel",
1036
+ name: "GLM-5.3-Flash \xB7 Qoder CN",
1037
+ api: "qoder-api",
1038
+ provider: "qoder-cn",
1039
+ baseUrl: getQoderBaseUrl("cn"),
1040
+ reasoning: true,
1041
+ supportsEffort: true,
1042
+ input: ["text", "image"],
1043
+ cost: ZERO_COST,
1044
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
1045
+ maxTokens: MAX_OUTPUT_TOKENS,
1046
+ description: "GLM-5.3-Flash (gfmodel); 1M context."
1047
+ },
1048
+ {
1049
+ id: "glm-5.2",
1050
+ upstreamKey: "gm51model",
1051
+ name: "GLM 5.2 \xB7 Qoder CN",
1052
+ api: "qoder-api",
1053
+ provider: "qoder-cn",
1054
+ baseUrl: getQoderBaseUrl("cn"),
1055
+ reasoning: true,
1056
+ supportsEffort: true,
1057
+ input: ["text", "image"],
1058
+ cost: ZERO_COST,
1059
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
1060
+ maxTokens: MAX_OUTPUT_TOKENS,
1061
+ description: "GLM 5.2 (gm51model); 1M context."
1062
+ },
1063
+ {
1064
+ id: "kimi-k3",
1065
+ upstreamKey: "kmodel_latest",
1066
+ name: "Kimi-K3 \xB7 Qoder CN",
1067
+ api: "qoder-api",
1068
+ provider: "qoder-cn",
1069
+ baseUrl: getQoderBaseUrl("cn"),
1070
+ reasoning: true,
1071
+ supportsEffort: true,
1072
+ input: ["text", "image"],
1073
+ cost: ZERO_COST,
1074
+ contextWindow: DEFAULT_CONTEXT_WINDOW,
1075
+ maxTokens: MAX_OUTPUT_TOKENS,
1076
+ description: "Kimi-K3 (kmodel_latest); 1M context."
1077
+ },
1078
+ {
1079
+ id: "kimi-k2.7-code",
1080
+ upstreamKey: "kmodel",
1081
+ name: "Kimi-K2.7-Code \xB7 Qoder CN",
1082
+ api: "qoder-api",
1083
+ provider: "qoder-cn",
1084
+ baseUrl: getQoderBaseUrl("cn"),
1085
+ reasoning: true,
1086
+ supportsEffort: false,
1087
+ input: ["text", "image"],
1088
+ cost: ZERO_COST,
1089
+ // Live catalog advertises 256K for this entry.
1090
+ contextWindow: 256e3,
1091
+ maxTokens: MAX_OUTPUT_TOKENS,
1092
+ description: "Kimi-K2.7-Code (kmodel); 256K context."
1093
+ },
1094
+ {
1095
+ id: "minimax-m2.7",
1096
+ upstreamKey: "mmodel",
1097
+ name: "MiniMax M2.7 \xB7 Qoder CN",
1098
+ api: "qoder-api",
1099
+ provider: "qoder-cn",
1100
+ baseUrl: getQoderBaseUrl("cn"),
1101
+ reasoning: false,
1102
+ supportsEffort: false,
1103
+ input: ["text"],
1104
+ cost: ZERO_COST,
1105
+ // Live CN catalog reports 200K; not confirmed at 1M.
1106
+ contextWindow: 2e5,
1107
+ maxTokens: MAX_OUTPUT_TOKENS,
1108
+ description: "MiniMax M2.7 (mmodel); 200K context."
1109
+ }
1110
+ ];
1111
+ PI_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
1112
+ }
1113
+ });
1114
+
1115
+ // src/auth/pat.ts
1116
+ function isPatRefresh(refresh) {
1117
+ return refresh.startsWith(`${PAT_REFRESH_PREFIX}|`);
1118
+ }
1119
+ function encodePatRefresh(pat, jobRefreshToken, userID, machineID) {
1120
+ return [PAT_REFRESH_PREFIX, pat, jobRefreshToken, userID, machineID].join("|");
1121
+ }
1122
+ function decodePatRefresh(refresh) {
1123
+ const parts = refresh.split("|");
1124
+ return {
1125
+ pat: parts[1] || "",
1126
+ jobRefreshToken: parts[2] || "",
1127
+ userID: parts[3] || "",
1128
+ machineID: parts[4] || ""
1129
+ };
1130
+ }
1131
+ async function exchangeJobToken(pat, mode) {
1132
+ const res = await fetch(getQoderExchangeURL(mode), {
1133
+ method: "POST",
1134
+ headers: {
1135
+ "Content-Type": "application/json",
1136
+ Accept: "application/json",
1137
+ "User-Agent": UA,
1138
+ "Cosy-Version": QODER_OPENAPI_COSY_VERSION,
1139
+ "Cosy-ClientType": QODER_CLIENT_TYPE
1140
+ },
1141
+ body: JSON.stringify({ personal_token: pat })
1142
+ });
1143
+ if (!res.ok) {
1144
+ const text = await res.text().catch(() => "");
1145
+ throw new Error(`Qoder PAT exchange failed: ${res.status} ${res.statusText}. ${text.slice(0, 200)}`);
1146
+ }
1147
+ const data = await res.json();
1148
+ if (!data.token) {
1149
+ throw new Error("Qoder PAT exchange returned no job token");
1150
+ }
1151
+ let expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
1152
+ if (data.expires_at) {
1153
+ const parsed = Date.parse(data.expires_at);
1154
+ if (!Number.isNaN(parsed)) expiresAt = parsed;
1155
+ } else if (data.expires_in) {
1156
+ expiresAt = Date.now() + data.expires_in;
1157
+ }
1158
+ return {
1159
+ jobToken: data.token,
1160
+ jobRefreshToken: data.refresh_token || "",
1161
+ expiresAt
1162
+ };
1163
+ }
1164
+ async function fetchUserInfo(jobToken, mode) {
1165
+ let userID = "";
1166
+ let email = "";
1167
+ let name = "";
1168
+ try {
1169
+ const res = await fetch(getQoderUserInfoURL(mode), {
1170
+ headers: {
1171
+ Authorization: `Bearer ${jobToken}`,
1172
+ Accept: "application/json",
1173
+ "User-Agent": UA,
1174
+ "Cosy-Version": QODER_OPENAPI_COSY_VERSION,
1175
+ "Cosy-ClientType": QODER_CLIENT_TYPE
1176
+ }
1177
+ });
1178
+ if (res.ok) {
1179
+ const info = await res.json();
1180
+ userID = info.id || "";
1181
+ email = info.email || "";
1182
+ name = info.name || info.username || "";
1183
+ }
1184
+ } catch {
1185
+ }
1186
+ return { userID, email, name };
1187
+ }
1188
+ async function credentialsFromPat(pat, mode) {
1189
+ const region = getQoderRegionConfig(mode);
1190
+ const { jobToken, jobRefreshToken, expiresAt } = await exchangeJobToken(pat, mode);
1191
+ const { userID, email, name } = await fetchUserInfo(jobToken, mode);
1192
+ const machineID = getMachineId();
1193
+ return {
1194
+ refresh: encodePatRefresh(pat, jobRefreshToken, userID, machineID),
1195
+ access: jobToken,
1196
+ expires: expiresAt - 5 * 60 * 1e3,
1197
+ // 5 min buffer
1198
+ userID,
1199
+ email: email || region.userEmailFallback,
1200
+ name: name || region.userNameFallback,
1201
+ machineID
1202
+ };
1203
+ }
1204
+ var UA, PAT_REFRESH_PREFIX;
1205
+ var init_pat = __esm({
1206
+ "src/auth/pat.ts"() {
1207
+ "use strict";
1208
+ init_cosy();
1209
+ init_region();
1210
+ UA = "pi-provider-qoder";
1211
+ PAT_REFRESH_PREFIX = "pat";
1212
+ }
1213
+ });
1214
+
1215
+ // src/auth/login.ts
1216
+ import crypto2 from "node:crypto";
1217
+ function getPrompt(callbacks) {
1218
+ return callbacks.onPrompt;
1219
+ }
1220
+ function getProgress(callbacks) {
1221
+ return callbacks.onProgress;
1222
+ }
1223
+ function getSignal(callbacks) {
1224
+ return callbacks.signal;
1225
+ }
1226
+ function generatePKCE() {
1227
+ const codeVerifier = crypto2.randomBytes(32).toString("base64url");
1228
+ const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
1229
+ return { codeVerifier, codeChallenge };
1230
+ }
1231
+ function parseExpiresAt(s, expiresInSeconds) {
1232
+ if (s) {
1233
+ const t = Date.parse(s);
1234
+ if (!Number.isNaN(t)) return t;
1235
+ const ms = Number.parseInt(s, 10);
1236
+ if (!Number.isNaN(ms) && ms > 0) return ms;
1237
+ }
1238
+ if (expiresInSeconds && expiresInSeconds > 0) {
1239
+ return Date.now() + expiresInSeconds * 1e3;
1240
+ }
1241
+ return Date.now() + 30 * 24 * 60 * 60 * 1e3;
1242
+ }
1243
+ async function interactiveLogin(callbacks, mode) {
1244
+ const region = getQoderRegionConfig(mode);
1245
+ const prompt = getPrompt(callbacks);
1246
+ const pat = await prompt({
1247
+ message: !region.supportsBrowserLogin ? "Paste a Qoder CN Personal Access Token, or leave empty to cancel" : "Paste a Qoder Personal Access Token (pt-...), or leave empty for browser login",
1248
+ placeholder: "pt-...",
1249
+ allowEmpty: true
1250
+ });
1251
+ if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
1252
+ if (pat?.trim()) {
1253
+ return patLogin(callbacks, pat.trim(), mode);
1254
+ }
1255
+ if (!region.supportsBrowserLogin) {
1256
+ throw new Error(
1257
+ `Qoder CN browser login is not supported here. Paste a Qoder CN PAT from ${region.patManageUrl} or set QODERCN_PERSONAL_ACCESS_TOKEN.`
1258
+ );
1259
+ }
1260
+ if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
1261
+ return runDeviceFlow(callbacks);
1262
+ }
1263
+ async function patLogin(callbacks, providedPat, mode) {
1264
+ const region = getQoderRegionConfig(mode);
1265
+ let pat = providedPat;
1266
+ if (!pat) {
1267
+ const prompt = getPrompt(callbacks);
1268
+ const entered = await prompt({
1269
+ message: !region.supportsBrowserLogin ? "Paste your Qoder CN Personal Access Token" : "Paste your Qoder Personal Access Token (pt-...)",
1270
+ placeholder: "pt-...",
1271
+ allowEmpty: false
1272
+ });
1273
+ if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
1274
+ pat = entered?.trim();
1275
+ }
1276
+ if (!pat) {
1277
+ throw new Error("No Personal Access Token provided");
1278
+ }
1279
+ getProgress(callbacks)?.("Exchanging access token...");
1280
+ const creds = await credentialsFromPat(pat, mode);
1281
+ getProgress(callbacks)?.("Login successful!");
1282
+ return creds;
1283
+ }
1284
+ function abortableDelay(ms, signal) {
1285
+ if (signal?.aborted) return Promise.reject(signal.reason || new Error("Login cancelled"));
1286
+ return new Promise((resolve, reject) => {
1287
+ const timer = setTimeout(() => {
1288
+ signal?.removeEventListener("abort", onAbort);
1289
+ resolve();
1290
+ }, ms);
1291
+ const onAbort = () => {
1292
+ clearTimeout(timer);
1293
+ reject(signal?.reason || new Error("Login cancelled"));
1294
+ };
1295
+ signal?.addEventListener("abort", onAbort, { once: true });
1296
+ });
1297
+ }
1298
+ async function runDeviceFlow(callbacks) {
1299
+ const { codeVerifier, codeChallenge } = generatePKCE();
1300
+ const nonce = crypto2.randomUUID();
1301
+ const machineID = getMachineId();
1302
+ const verificationURI = getQoderDeviceLoginURL(codeChallenge, machineID, nonce);
1303
+ getProgress(callbacks)?.("Please complete login in your browser...");
1304
+ callbacks.onAuth({
1305
+ url: verificationURI,
1306
+ instructions: "Click to sign in with your Qoder account in the browser."
1307
+ });
1308
+ const pollURL = getQoderDevicePollURL(nonce, codeVerifier);
1309
+ const pollInterval = 2e3;
1310
+ const maxAttempts = 90;
1311
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1312
+ if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
1313
+ await abortableDelay(pollInterval, getSignal(callbacks));
1314
+ try {
1315
+ const response = await fetch(pollURL, {
1316
+ method: "GET",
1317
+ headers: {
1318
+ Accept: "application/json",
1319
+ "User-Agent": "pi-provider-qoder"
1320
+ },
1321
+ signal: getSignal(callbacks)
1322
+ });
1323
+ if (response.status === 202 || response.status === 404) {
1324
+ continue;
1325
+ }
1326
+ if (!response.ok) {
1327
+ const errText = await response.text();
1328
+ throw new Error(`Device token poll failed: ${response.status} ${response.statusText}. Response: ${errText}`);
1329
+ }
1330
+ const tokenData = await response.json();
1331
+ if (!tokenData.token) {
1332
+ throw new Error("Device token poll returned empty access token");
1333
+ }
1334
+ const expireMs = parseExpiresAt(tokenData.expires_at, tokenData.expires_in);
1335
+ getProgress(callbacks)?.("Fetching user profile...");
1336
+ let email = "";
1337
+ let name = "";
1338
+ try {
1339
+ const userinfoRes = await fetch(getQoderUserInfoURL("global"), {
1340
+ method: "GET",
1341
+ headers: {
1342
+ Authorization: `Bearer ${tokenData.token}`,
1343
+ Accept: "application/json",
1344
+ "User-Agent": "pi-provider-qoder"
1345
+ }
1346
+ });
1347
+ if (userinfoRes.ok) {
1348
+ const userinfo = await userinfoRes.json();
1349
+ email = userinfo.email || "";
1350
+ name = userinfo.name || userinfo.username || "";
1351
+ }
1352
+ } catch {
1353
+ }
1354
+ getProgress(callbacks)?.("Login successful!");
1355
+ return {
1356
+ refresh: `${tokenData.refresh_token}|${tokenData.user_id}|${machineID}`,
1357
+ access: tokenData.token,
1358
+ expires: expireMs - 5 * 60 * 1e3,
1359
+ // 5 min buffer
1360
+ userID: tokenData.user_id,
1361
+ email,
1362
+ name,
1363
+ machineID
1364
+ };
1365
+ } catch (e) {
1366
+ const err = e;
1367
+ if (err.name === "AbortError" || getSignal(callbacks)?.aborted) {
1368
+ throw new Error("Login cancelled");
1369
+ }
1370
+ throw e;
1371
+ }
1372
+ }
1373
+ throw new Error("Authorization timed out");
1374
+ }
1375
+ var init_login = __esm({
1376
+ "src/auth/login.ts"() {
1377
+ "use strict";
1378
+ init_cosy();
1379
+ init_region();
1380
+ init_pat();
1381
+ }
1382
+ });
1383
+
1384
+ // src/auth/oauth.ts
1385
+ var oauth_exports = {};
1386
+ __export(oauth_exports, {
1387
+ autoLoginQoderFromEnvironment: () => autoLoginQoderFromEnvironment,
1388
+ clearQoderAuthMemCache: () => clearQoderAuthMemCache,
1389
+ getCachedCredentials: () => getCachedCredentials,
1390
+ getQoderPatForMode: () => getQoderPatForMode,
1391
+ loginQoderForMode: () => loginQoderForMode,
1392
+ refreshQoderTokenForMode: () => refreshQoderTokenForMode,
1393
+ resolveQoderIdentity: () => resolveQoderIdentity
1394
+ });
1395
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1396
+ import { homedir as homedir4 } from "node:os";
1397
+ import { dirname as dirname3, join as join4 } from "node:path";
1398
+ import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
1399
+ function getHomeDir4() {
1400
+ return process.env.HOME || process.env.USERPROFILE || homedir4();
1401
+ }
1402
+ function getAuthFilePath() {
1403
+ return join4(getHomeDir4(), ".pi", "agent", "auth.json");
1404
+ }
1405
+ function clearQoderAuthMemCache() {
1406
+ authFileMem = void 0;
1407
+ identityCache.clear();
1408
+ }
1409
+ function readAuthFileCached() {
1410
+ const authPath = getAuthFilePath();
1411
+ if (authFileMem !== void 0) {
1412
+ if (authFileMem === null) return null;
1413
+ if (authFileMem.path === authPath) return authFileMem.data;
1414
+ }
1415
+ if (!existsSync4(authPath)) {
1416
+ authFileMem = null;
1417
+ return null;
1418
+ }
1419
+ try {
1420
+ const data = JSON.parse(readFileSync4(authPath, "utf-8"));
1421
+ authFileMem = { path: authPath, data };
1422
+ return data;
1423
+ } catch {
1424
+ authFileMem = null;
1425
+ return null;
1426
+ }
1427
+ }
1428
+ function getQoderPatForMode(mode) {
1429
+ for (const envName of getQoderRegionConfig(mode).patEnvNames) {
1430
+ const value = process.env[envName];
1431
+ if (value) return value;
1432
+ }
1433
+ return "";
1434
+ }
1435
+ function saveCredentialsToAuthFile(providerID, credentials) {
1436
+ try {
1437
+ const authPath = getAuthFilePath();
1438
+ const dir = dirname3(authPath);
1439
+ if (!existsSync4(dir)) {
1440
+ mkdirSync4(dir, { recursive: true, mode: 448 });
1441
+ }
1442
+ const existing = readAuthFileCached();
1443
+ const auth = existing ? { ...existing } : {};
1444
+ auth[providerID] = { type: "oauth", ...credentials };
1445
+ writeFileSync4(authPath, JSON.stringify(auth, null, 2), { encoding: "utf-8", mode: 384 });
1446
+ authFileMem = { path: authPath, data: auth };
1447
+ const q = credentials;
1448
+ if (q.access && q.userID) {
1449
+ identityCache.set(`${providerID}:${q.access}`, q);
1450
+ }
1451
+ } catch (err) {
1452
+ console.error(`[pi-provider-qoder] Failed to write auth storage for ${providerID}:`, err);
1453
+ }
1454
+ }
1455
+ async function autoLoginQoderFromEnvironment(providerID, mode) {
1456
+ const pat = getQoderPatForMode(mode);
1457
+ if (!pat) return;
1458
+ const credentials = await credentialsFromPat(pat, mode);
1459
+ if (typeof AuthStorage2?.create === "function") {
1460
+ try {
1461
+ const authStorage = AuthStorage2.create();
1462
+ authStorage.set(providerID, { type: "oauth", ...credentials });
1463
+ } catch {
1464
+ saveCredentialsToAuthFile(providerID, credentials);
1465
+ }
1466
+ } else {
1467
+ saveCredentialsToAuthFile(providerID, credentials);
1468
+ }
1469
+ const qCreds = credentials;
1470
+ await updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode);
1471
+ }
1472
+ function getCachedCredentials(_accessToken, providerID = "qoder") {
1473
+ const auth = readAuthFileCached();
1474
+ if (!auth) return null;
1475
+ const creds = auth[providerID] || (providerID === "qoder" ? auth.qoder : null);
1476
+ if (creds?.userID || creds?.access) {
1477
+ if (creds.access && creds.userID) {
1478
+ identityCache.set(`${providerID}:${creds.access}`, creds);
1479
+ }
1480
+ return creds;
1481
+ }
1482
+ return null;
1483
+ }
1484
+ async function resolveQoderIdentity(accessToken, providerID, mode) {
1485
+ const region = getQoderRegionConfig(mode);
1486
+ const cacheKey = `${providerID}:${accessToken}`;
1487
+ const mem = identityCache.get(cacheKey);
1488
+ if (mem?.userID) return mem;
1489
+ const cached = getCachedCredentials(accessToken, providerID);
1490
+ if (cached?.userID) {
1491
+ identityCache.set(cacheKey, cached);
1492
+ return cached;
1493
+ }
1494
+ const info = await fetchUserInfo(accessToken, mode);
1495
+ const machineID = getMachineId();
1496
+ const creds = {
1497
+ access: accessToken,
1498
+ userID: info.userID || "qoder-user",
1499
+ email: info.email || region.userEmailFallback,
1500
+ name: info.name || region.userNameFallback,
1501
+ machineID,
1502
+ refresh: "",
1503
+ expires: 0
1504
+ };
1505
+ identityCache.set(cacheKey, creds);
1506
+ saveCredentialsToAuthFile(providerID, creds);
1507
+ return creds;
1508
+ }
1509
+ async function loginQoderForMode(callbacks, mode) {
1510
+ const providerID = getQoderRegionConfig(mode).providerID;
1511
+ const pat = getQoderPatForMode(mode);
1512
+ if (pat) {
1513
+ try {
1514
+ const creds2 = await credentialsFromPat(pat, mode);
1515
+ const qCreds = creds2;
1516
+ updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
1517
+ });
1518
+ saveCredentialsToAuthFile(providerID, creds2);
1519
+ return creds2;
1520
+ } catch {
1521
+ }
1522
+ }
1523
+ const creds = await interactiveLogin(callbacks, mode);
1524
+ try {
1525
+ const qCreds = creds;
1526
+ updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
1527
+ });
1528
+ } catch {
1529
+ }
1530
+ saveCredentialsToAuthFile(providerID, creds);
1531
+ return creds;
1532
+ }
1533
+ async function refreshQoderTokenForMode(credentials, mode) {
1534
+ if (isPatRefresh(credentials.refresh)) {
1535
+ const { pat } = decodePatRefresh(credentials.refresh);
1536
+ if (pat) {
1537
+ try {
1538
+ const refreshed = await credentialsFromPat(pat, mode);
1539
+ const qCreds = refreshed;
1540
+ updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email, mode).catch(() => {
1541
+ });
1542
+ return refreshed;
1543
+ } catch {
1544
+ }
1545
+ }
1546
+ return {
1547
+ ...credentials,
1548
+ expires: Date.now() + 60 * 60 * 1e3
1549
+ // extend 1 hour to retry later
1550
+ };
1551
+ }
1552
+ const parts = credentials.refresh.split("|");
1553
+ const refreshToken = parts[0] || "";
1554
+ const userID = parts[1] || "";
1555
+ const machineID = parts[2] || getMachineId();
1556
+ const prev = credentials;
1557
+ const prevName = prev.name || "";
1558
+ const prevEmail = prev.email || "";
1559
+ const refreshURL = getQoderRefreshURL(mode);
1560
+ try {
1561
+ const response = await fetch(refreshURL, {
1562
+ method: "POST",
1563
+ headers: {
1564
+ "Content-Type": "application/json",
1565
+ Authorization: `Bearer ${credentials.access}`,
1566
+ Accept: "application/json",
1567
+ "User-Agent": "pi-provider-qoder"
1568
+ },
1569
+ body: JSON.stringify({ refreshToken })
1570
+ });
1571
+ if (response.ok) {
1572
+ const data = await response.json();
1573
+ const newAccess = data.token;
1574
+ const newRefresh = data.refresh_token || refreshToken;
1575
+ let expireMs = Date.now() + 30 * 24 * 60 * 60 * 1e3;
1576
+ if (data.expires_at) {
1577
+ const parsed = Date.parse(data.expires_at);
1578
+ if (!Number.isNaN(parsed)) expireMs = parsed;
1579
+ } else if (data.expires_in) {
1580
+ expireMs = Date.now() + data.expires_in * 1e3;
1581
+ }
1582
+ const refreshed = {
1583
+ ...credentials,
1584
+ refresh: `${newRefresh}|${userID}|${machineID}`,
1585
+ access: newAccess,
1586
+ expires: expireMs - 5 * 60 * 1e3,
1587
+ userID,
1588
+ email: prevEmail,
1589
+ name: prevName,
1590
+ machineID
1591
+ };
1592
+ updateQoderModelsCache(newAccess, userID, prevName, prevEmail, mode).catch(() => {
1593
+ });
1594
+ return refreshed;
1595
+ }
1596
+ } catch {
1597
+ }
1598
+ const refreshedFallback = {
1599
+ ...credentials,
1600
+ expires: Date.now() + 60 * 60 * 1e3
1601
+ // extend for 1 hour
1602
+ };
1603
+ return refreshedFallback;
1604
+ }
1605
+ var AuthStorage2, identityCache, authFileMem;
1606
+ var init_oauth = __esm({
1607
+ "src/auth/oauth.ts"() {
1608
+ "use strict";
1609
+ init_catalog();
1610
+ init_cosy();
1611
+ init_region();
1612
+ init_login();
1613
+ init_pat();
1614
+ AuthStorage2 = PiCodingAgent.AuthStorage;
1615
+ identityCache = /* @__PURE__ */ new Map();
1616
+ }
1617
+ });
1618
+
1619
+ // src/index.ts
1620
+ init_oauth();
1621
+
1622
+ // src/auth/usage.ts
1623
+ init_region();
1624
+ async function fetchQoderUsageForMode(credentials, mode) {
1625
+ const region = getQoderRegionConfig(mode);
1626
+ const response = await fetch(getQoderUsageURL(mode), {
1627
+ method: "GET",
1628
+ headers: {
1629
+ Authorization: `Bearer ${credentials.access}`,
1630
+ Accept: "application/json",
1631
+ "User-Agent": "pi-provider-qoder"
1632
+ }
1633
+ });
1634
+ if (!response.ok) {
1635
+ throw new Error(`Failed to fetch Qoder usage: ${response.status} ${response.statusText}`);
1636
+ }
1637
+ const raw = await response.json();
1638
+ const usageBuckets = [];
1639
+ if (raw.userQuota) {
1640
+ usageBuckets.push({
1641
+ id: "user-quota",
1642
+ label: "User Quota",
1643
+ usedDisplay: raw.userQuota.used.toFixed(2),
1644
+ limitDisplay: raw.userQuota.total.toFixed(2),
1645
+ unit: raw.userQuota.unit,
1646
+ resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0
1647
+ });
1648
+ }
1649
+ if (raw.orgResourcePackage && raw.orgResourcePackage.total > 0) {
1650
+ usageBuckets.push({
1651
+ id: "org-resource-package",
1652
+ label: "Org Resource Package",
1653
+ usedDisplay: raw.orgResourcePackage.used.toFixed(2),
1654
+ limitDisplay: raw.orgResourcePackage.total.toFixed(2),
1655
+ unit: raw.orgResourcePackage.unit,
1656
+ resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0
1657
+ });
1658
+ }
1659
+ const remainingText = raw.userQuota ? `${raw.userQuota.remaining.toFixed(2)} ${raw.userQuota.unit} remaining` : "";
1660
+ return {
1661
+ summary: remainingText,
1662
+ subscriptionTitle: region.usageTitle,
1663
+ resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0,
1664
+ manageUrl: region.manageUrl,
1665
+ usageBuckets,
1666
+ raw
1667
+ };
1668
+ }
1669
+
1670
+ // src/index.ts
1671
+ init_catalog();
1672
+
1673
+ // src/commands/endpoint.ts
1674
+ init_oauth();
1675
+ init_catalog();
1676
+ init_vpc();
1677
+ var RESET_VALUES = /* @__PURE__ */ new Set(["official", "default", "clear", "none", "reset"]);
1678
+ function describeEndpoint() {
1679
+ const current = getQoderCNEndpoints();
1680
+ if (current.isDefault) {
1681
+ return "Official Default Gateway (https://gateway.qoder.com.cn/)";
1682
+ }
1683
+ return `Enterprise Endpoint [${current.raw}]: ${current.baseUrl}`;
1684
+ }
1685
+ async function runEndpointCommand(args, ctx) {
1686
+ const input = (args || "").trim();
1687
+ if (!input) {
1688
+ ctx?.ui?.notify(
1689
+ `Current Qoder CN endpoint: ${describeEndpoint()}
1690
+ To set: /qoder-endpoint <domain>
1691
+ To reset: /qoder-endpoint default`,
1692
+ "info"
1693
+ );
1694
+ return;
1695
+ }
1696
+ try {
1697
+ const isReset = RESET_VALUES.has(input.toLowerCase());
1698
+ const resolved = setQoderCNEndpoint(isReset ? "" : input);
1699
+ ctx?.ui?.notify(
1700
+ isReset ? "Qoder CN endpoint reset to Official Default Gateway (https://gateway.qoder.com.cn/)" : `Qoder CN endpoint set to: ${resolved.baseUrl}`,
1701
+ "info"
1702
+ );
1703
+ const credentials = getCachedCredentials("", "qoder-cn");
1704
+ if (credentials?.access) {
1705
+ await updateQoderModelsCache(
1706
+ credentials.access,
1707
+ credentials.userID,
1708
+ credentials.name,
1709
+ credentials.email,
1710
+ "cn"
1711
+ ).catch(() => void 0);
1712
+ ctx?.ui?.notify("Qoder CN model catalog refreshed.", "info");
1713
+ }
1714
+ } catch (error) {
1715
+ const message = error instanceof Error ? error.message : String(error);
1716
+ ctx?.ui?.notify(`Failed to set Qoder CN endpoint: ${message}`, "error");
1717
+ }
1718
+ }
1719
+
1720
+ // src/commands/usage.ts
1721
+ init_region();
1722
+ function usageBar(percentage, width = 20) {
1723
+ if (typeof percentage !== "number" || !Number.isFinite(percentage)) return `[${"?".repeat(width)}]`;
1724
+ const ratio = Math.max(0, Math.min(1, percentage / 100));
1725
+ const filled = Math.max(0, Math.min(width, Math.round(ratio * width)));
1726
+ return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
1727
+ }
1728
+ function formatAmount(value, unit) {
1729
+ if (typeof value !== "number" || !Number.isFinite(value)) return "?";
1730
+ const rounded = Math.round(value * 100) / 100;
1731
+ return unit ? `${rounded} ${unit}` : `${rounded}`;
1732
+ }
1733
+ function formatPercent(value) {
1734
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
1735
+ return Math.round(value * 10) / 10;
1736
+ }
1737
+ function formatResetTime(expiresAt, now = Date.now()) {
1738
+ const ms = Number(expiresAt);
1739
+ if (!Number.isFinite(ms) || ms <= 0) return "n/a";
1740
+ const timestamp = ms < 1e12 ? ms * 1e3 : ms;
1741
+ if (new Date(timestamp).getUTCFullYear() >= 9999) return "never";
1742
+ const delta = timestamp - now;
1743
+ if (delta <= 0) return "expired";
1744
+ const totalMinutes = Math.round(delta / 6e4);
1745
+ const days = Math.floor(totalMinutes / 1440);
1746
+ const hours = Math.floor(totalMinutes % 1440 / 60);
1747
+ const minutes = totalMinutes % 60;
1748
+ const relative = days > 0 ? `${days}d ${hours}h` : hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
1749
+ return `${new Date(timestamp).toISOString().slice(0, 10)} (in ${relative})`;
1750
+ }
1751
+ function formatBucketLine(label, bucket) {
1752
+ const unit = bucket?.unit || "credits";
1753
+ const rawPercent = bucket?.percentage;
1754
+ const derivedPercent = typeof bucket?.used === "number" && typeof bucket?.total === "number" && bucket.total > 0 ? bucket.used / bucket.total * 100 : void 0;
1755
+ const percent = formatPercent(
1756
+ typeof rawPercent === "number" && Number.isFinite(rawPercent) ? rawPercent : derivedPercent
1757
+ );
1758
+ const percentText = percent === void 0 ? "?" : `${percent}%`;
1759
+ return ` ${usageBar(percent)} ${label}: ${formatAmount(bucket?.used, unit)} / ${formatAmount(
1760
+ bucket?.total,
1761
+ unit
1762
+ )} used (${percentText}) \xB7 ${formatAmount(bucket?.remaining, unit)} left`;
1763
+ }
1764
+ function hasBucket(bucket) {
1765
+ if (!bucket) return false;
1766
+ return (bucket.total ?? 0) > 0 || (bucket.used ?? 0) > 0;
1767
+ }
1768
+ function formatQoderUsage(raw, mode, now = Date.now()) {
1769
+ const region = getQoderRegionConfig(mode);
1770
+ const lines = [];
1771
+ const userType = raw?.userType ? ` (${raw.userType})` : "";
1772
+ const overallPercent = formatPercent(raw?.totalUsagePercentage);
1773
+ lines.push(`${region.usageTitle}${userType}`);
1774
+ lines.push(`${usageBar(overallPercent)} ${overallPercent === void 0 ? "?" : `${overallPercent}%`} used`);
1775
+ if (raw?.isQuotaExceeded) lines.push(" ! quota exceeded");
1776
+ const showUserQuota = hasBucket(raw?.userQuota);
1777
+ const showAddOnQuota = hasBucket(raw?.addOnQuota);
1778
+ if (showUserQuota) lines.push(formatBucketLine("Plan quota", raw.userQuota));
1779
+ if (showAddOnQuota) lines.push(formatBucketLine("Add-on quota", raw.addOnQuota));
1780
+ if (!showUserQuota && !showAddOnQuota) lines.push(" No quota buckets returned.");
1781
+ if (raw?.isPlanQuotaProrated) lines.push(" (plan quota is prorated)");
1782
+ lines.push(`Resets: ${formatResetTime(raw?.expiresAt, now)}`);
1783
+ const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
1784
+ if (manageUrl) lines.push(`Manage: ${manageUrl}`);
1785
+ return { title: region.usageTitle, lines };
1786
+ }
1787
+ async function fetchQoderQuota(accessToken, mode) {
1788
+ const response = await fetch(getQoderUsageURL(mode), {
1789
+ method: "GET",
1790
+ headers: {
1791
+ Authorization: `Bearer ${accessToken}`,
1792
+ Accept: "application/json",
1793
+ "User-Agent": "pi-provider-qoder-cn"
1794
+ }
1795
+ });
1796
+ const text = await response.text();
1797
+ let data;
1798
+ try {
1799
+ data = JSON.parse(text);
1800
+ } catch {
1801
+ data = void 0;
1802
+ }
1803
+ if (!response.ok) {
1804
+ const detail = data?.message || data?.error || text.slice(0, 200);
1805
+ const hint = response.status === 401 || response.status === 403 ? `Re-login with /login ${getQoderRegionConfig(mode).providerID}.` : "";
1806
+ const parts = [`Qoder usage request failed: ${response.status} ${response.statusText}`, hint, detail];
1807
+ throw new Error(parts.filter(Boolean).join("\n"));
1808
+ }
1809
+ if (!data) throw new Error("Qoder usage response was not JSON.");
1810
+ return data;
1811
+ }
1812
+ async function resolveAccessToken(providerID, ctx) {
1813
+ try {
1814
+ const fromRegistry = await ctx?.modelRegistry?.getApiKeyForProvider(providerID);
1815
+ if (fromRegistry) return fromRegistry;
1816
+ } catch {
1817
+ }
1818
+ const { getCachedCredentials: getCachedCredentials2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
1819
+ return getCachedCredentials2("", providerID)?.access || void 0;
1820
+ }
1821
+ var FORMAT_ARGS = /* @__PURE__ */ new Set(["json", "raw", "debug"]);
1822
+ async function runUsageCommand(mode, args, ctx) {
1823
+ const region = getQoderRegionConfig(mode);
1824
+ const providerID = region.providerID;
1825
+ const wantsRaw = FORMAT_ARGS.has((args || "").trim().toLowerCase());
1826
+ try {
1827
+ const accessToken = await resolveAccessToken(providerID, ctx);
1828
+ if (!accessToken) {
1829
+ ctx?.ui?.notify(`No ${providerID} credentials. Run /login ${providerID} first.`, "warning");
1830
+ return;
1831
+ }
1832
+ const raw = await fetchQoderQuota(accessToken, mode);
1833
+ const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode).lines.join("\n");
1834
+ ctx?.ui?.notify(output, "info");
1835
+ if (!ctx?.ui) console.log(output);
1836
+ } catch (error) {
1837
+ const message = error instanceof Error ? error.message : String(error);
1838
+ ctx?.ui?.notify(message, "error");
1839
+ if (!ctx?.ui) console.error(message);
1840
+ }
1841
+ }
1842
+
1843
+ // src/protocol/stream.ts
1844
+ init_oauth();
1845
+ init_catalog();
1846
+ init_cosy();
1847
+ init_region();
1848
+ import crypto3 from "node:crypto";
1849
+ import * as PiAi from "@earendil-works/pi-ai";
1850
+ import {
1851
+ clampThinkingLevel
1852
+ } from "@earendil-works/pi-ai";
1853
+
1854
+ // src/protocol/encoding.ts
1855
+ var qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
1856
+ var qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1857
+ var encodeTable = new Uint8Array(256);
1858
+ for (let i = 0; i < encodeTable.length; i++) {
1859
+ encodeTable[i] = i;
1860
+ }
1861
+ for (let i = 0; i < qoderStdAlphabet.length; i++) {
1862
+ encodeTable[qoderStdAlphabet.charCodeAt(i)] = qoderCustomAlphabet.charCodeAt(i);
1863
+ }
1864
+ encodeTable["=".charCodeAt(0)] = "$".charCodeAt(0);
1865
+ function qoderEncodeBody(plaintext) {
1866
+ const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
1867
+ const n = std.length;
1868
+ const a = Math.floor(n / 3);
1869
+ const out = Buffer.allocUnsafe(n);
1870
+ let dst = 0;
1871
+ for (let i = n - a; i < n; i++) {
1872
+ out[dst++] = encodeTable[std.charCodeAt(i)];
1873
+ }
1874
+ for (let i = a; i < n - a; i++) {
1875
+ out[dst++] = encodeTable[std.charCodeAt(i)];
1876
+ }
1877
+ for (let i = 0; i < a; i++) {
1878
+ out[dst++] = encodeTable[std.charCodeAt(i)];
1879
+ }
1880
+ return out;
1881
+ }
1882
+
1883
+ // src/protocol/thinking.ts
1884
+ var THINKING_TAG_VARIANTS = [
1885
+ { open: "<thinking>", close: "</thinking>" },
1886
+ { open: "<think>", close: "</think>" },
1887
+ { open: "<reasoning>", close: "</reasoning>" },
1888
+ { open: "<thought>", close: "</thought>" }
1889
+ ];
1890
+ var ALL_THINKING_TAGS = THINKING_TAG_VARIANTS.flatMap((variant) => [variant.open, variant.close]);
1891
+ function getTrailingPossibleTagPrefixLength(text, tag) {
1892
+ const maxPrefixLength = Math.min(text.length, tag.length - 1);
1893
+ for (let len = maxPrefixLength; len > 0; len--) {
1894
+ if (text.endsWith(tag.slice(0, len))) return len;
1895
+ }
1896
+ return 0;
1897
+ }
1898
+ function getMaxTrailingPossibleTagPrefixLength(text, tags) {
1899
+ let maxLength = 0;
1900
+ for (const tag of tags) {
1901
+ maxLength = Math.max(maxLength, getTrailingPossibleTagPrefixLength(text, tag));
1902
+ }
1903
+ return maxLength;
1904
+ }
1905
+ function stripThinkingTags(text) {
1906
+ let out = text;
1907
+ for (const { open, close } of THINKING_TAG_VARIANTS) {
1908
+ if (open.length > 0 && out.includes(open)) out = out.split(open).join("");
1909
+ if (close.length > 0 && out.includes(close)) out = out.split(close).join("");
1910
+ }
1911
+ return out;
1912
+ }
1913
+ var DSML_TAG = /<\/?[\uFF5C|]{0,2}\s*DSML\s*[\uFF5C|]{0,2}[^>]*>|<\/?(?:invoke|invocation|parameter|function_call|function_calls)\b[^>]*>/g;
1914
+ var DEGENERATE_DSML_TAIL = /(?:(?:<\/?(?:invoke|invocation|parameter|function_call|function_calls)\b[^>]*>)\s*|(?:<\/?[\uFF5C|]{0,2}\s*DSML\s*[\uFF5C|]{0,2}[^>]*>)\s*){2,}$/;
1915
+ function stripDsmlResidue(text) {
1916
+ return text.replace(DSML_TAG, "").replace(/\n{3,}/g, "\n\n");
1917
+ }
1918
+ function isDegenerateDsmlTurn(message, rawReasoning) {
1919
+ if (message.stopReason === "toolUse") return false;
1920
+ const content = Array.isArray(message.content) ? message.content : [];
1921
+ if (content.some((block) => block.type === "text" && block.text.trim().length > 0)) return false;
1922
+ return DEGENERATE_DSML_TAIL.test(rawReasoning);
1923
+ }
1924
+ var ThinkingTagParser = class {
1925
+ constructor(output, stream) {
1926
+ this.output = output;
1927
+ this.stream = stream;
1928
+ }
1929
+ output;
1930
+ stream;
1931
+ textBuffer = "";
1932
+ inThinking = false;
1933
+ thinkingExtracted = false;
1934
+ thinkingBlockIndex = null;
1935
+ textBlockIndex = null;
1936
+ lastTextBlockIndex = null;
1937
+ activeEndTag = THINKING_TAG_VARIANTS[0].close;
1938
+ processChunk(chunk) {
1939
+ this.textBuffer += chunk;
1940
+ while (this.textBuffer.length > 0) {
1941
+ const prevLength = this.textBuffer.length;
1942
+ if (!this.inThinking && !this.thinkingExtracted) {
1943
+ this.processBeforeThinking();
1944
+ if (this.textBuffer.length === 0) break;
1945
+ }
1946
+ if (this.inThinking) {
1947
+ this.processInsideThinking();
1948
+ if (this.textBuffer.length === 0) break;
1949
+ }
1950
+ if (this.thinkingExtracted) {
1951
+ this.processAfterThinking();
1952
+ break;
1953
+ }
1954
+ if (this.textBuffer.length >= prevLength) break;
1955
+ }
1956
+ }
1957
+ finalize() {
1958
+ if (this.textBuffer.length === 0) return;
1959
+ if (this.inThinking && this.thinkingBlockIndex !== null) {
1960
+ const block = this.output.content[this.thinkingBlockIndex];
1961
+ block.thinking += this.textBuffer;
1962
+ this.stream.push({
1963
+ type: "thinking_delta",
1964
+ contentIndex: this.thinkingBlockIndex,
1965
+ delta: this.textBuffer,
1966
+ partial: this.output
1967
+ });
1968
+ this.stream.push({
1969
+ type: "thinking_end",
1970
+ contentIndex: this.thinkingBlockIndex,
1971
+ content: block.thinking,
1972
+ partial: this.output
1973
+ });
1974
+ } else {
1975
+ this.emitText(this.textBuffer);
1976
+ }
1977
+ this.textBuffer = "";
1978
+ }
1979
+ getTextBlockIndex() {
1980
+ return this.textBlockIndex ?? this.lastTextBlockIndex;
1981
+ }
1982
+ processBeforeThinking() {
1983
+ let bestOpenPos = -1;
1984
+ let bestOpenVariant = null;
1985
+ let bestClosePos = -1;
1986
+ let bestCloseVariant = null;
1987
+ for (const variant of THINKING_TAG_VARIANTS) {
1988
+ const openPos = this.textBuffer.indexOf(variant.open);
1989
+ if (openPos !== -1 && (bestOpenPos === -1 || openPos < bestOpenPos)) {
1990
+ bestOpenPos = openPos;
1991
+ bestOpenVariant = variant;
1992
+ }
1993
+ const closePos = this.textBuffer.indexOf(variant.close);
1994
+ if (closePos !== -1 && (bestClosePos === -1 || closePos < bestClosePos)) {
1995
+ bestClosePos = closePos;
1996
+ bestCloseVariant = variant;
1997
+ }
1998
+ }
1999
+ if (bestOpenVariant !== null && (bestCloseVariant === null || bestOpenPos < bestClosePos)) {
2000
+ if (bestOpenPos > 0) this.emitText(this.textBuffer.slice(0, bestOpenPos));
2001
+ this.textBuffer = this.textBuffer.slice(bestOpenPos + bestOpenVariant.open.length);
2002
+ this.activeEndTag = bestOpenVariant.close;
2003
+ this.inThinking = true;
2004
+ return;
2005
+ }
2006
+ if (bestCloseVariant !== null) {
2007
+ if (bestClosePos > 0) this.emitText(this.textBuffer.slice(0, bestClosePos));
2008
+ this.textBuffer = this.textBuffer.slice(bestClosePos + bestCloseVariant.close.length);
2009
+ if (this.textBuffer.startsWith("\n\n")) this.textBuffer = this.textBuffer.slice(2);
2010
+ else if (this.textBuffer.startsWith("\n")) this.textBuffer = this.textBuffer.slice(1);
2011
+ return;
2012
+ }
2013
+ const trailingPrefixLength = getMaxTrailingPossibleTagPrefixLength(this.textBuffer, ALL_THINKING_TAGS);
2014
+ const safeLen = this.textBuffer.length - trailingPrefixLength;
2015
+ if (safeLen > 0) {
2016
+ this.emitText(this.textBuffer.slice(0, safeLen));
2017
+ this.textBuffer = this.textBuffer.slice(safeLen);
2018
+ }
2019
+ }
2020
+ processInsideThinking() {
2021
+ const endPos = this.textBuffer.indexOf(this.activeEndTag);
2022
+ if (endPos !== -1) {
2023
+ if (endPos > 0) this.emitThinking(this.textBuffer.slice(0, endPos));
2024
+ if (this.thinkingBlockIndex !== null) {
2025
+ const block = this.output.content[this.thinkingBlockIndex];
2026
+ this.stream.push({
2027
+ type: "thinking_end",
2028
+ contentIndex: this.thinkingBlockIndex,
2029
+ content: block.thinking,
2030
+ partial: this.output
2031
+ });
2032
+ }
2033
+ this.textBuffer = this.textBuffer.slice(endPos + this.activeEndTag.length);
2034
+ this.inThinking = false;
2035
+ this.thinkingExtracted = true;
2036
+ this.lastTextBlockIndex = this.textBlockIndex;
2037
+ this.textBlockIndex = null;
2038
+ if (this.textBuffer.startsWith("\n\n")) this.textBuffer = this.textBuffer.slice(2);
2039
+ return;
2040
+ }
2041
+ const trailingPrefixLength = getTrailingPossibleTagPrefixLength(this.textBuffer, this.activeEndTag);
2042
+ const safeLen = this.textBuffer.length - trailingPrefixLength;
2043
+ if (safeLen > 0) {
2044
+ this.emitThinking(this.textBuffer.slice(0, safeLen));
2045
+ this.textBuffer = this.textBuffer.slice(safeLen);
2046
+ }
2047
+ }
2048
+ processAfterThinking() {
2049
+ this.emitText(this.textBuffer);
2050
+ this.textBuffer = "";
2051
+ }
2052
+ emitText(text) {
2053
+ if (!text) return;
2054
+ if (this.textBlockIndex === null) {
2055
+ this.textBlockIndex = this.output.content.length;
2056
+ this.output.content.push({ type: "text", text: "" });
2057
+ this.stream.push({ type: "text_start", contentIndex: this.textBlockIndex, partial: this.output });
2058
+ }
2059
+ const block = this.output.content[this.textBlockIndex];
2060
+ block.text += text;
2061
+ this.stream.push({ type: "text_delta", contentIndex: this.textBlockIndex, delta: text, partial: this.output });
2062
+ }
2063
+ emitThinking(thinking) {
2064
+ if (!thinking) return;
2065
+ if (this.thinkingBlockIndex === null) {
2066
+ if (this.textBlockIndex !== null) {
2067
+ this.thinkingBlockIndex = this.textBlockIndex;
2068
+ this.output.content.splice(this.thinkingBlockIndex, 0, { type: "thinking", thinking: "" });
2069
+ this.textBlockIndex = this.textBlockIndex + 1;
2070
+ } else {
2071
+ this.thinkingBlockIndex = this.output.content.length;
2072
+ this.output.content.push({ type: "thinking", thinking: "" });
2073
+ }
2074
+ this.stream.push({ type: "thinking_start", contentIndex: this.thinkingBlockIndex, partial: this.output });
2075
+ }
2076
+ const block = this.output.content[this.thinkingBlockIndex];
2077
+ block.thinking += thinking;
2078
+ this.stream.push({
2079
+ type: "thinking_delta",
2080
+ contentIndex: this.thinkingBlockIndex,
2081
+ delta: thinking,
2082
+ partial: this.output
2083
+ });
2084
+ }
2085
+ };
2086
+
2087
+ // src/protocol/transform.ts
2088
+ function getContentText(msg) {
2089
+ if (typeof msg.content === "string") return msg.content;
2090
+ if (Array.isArray(msg.content)) {
2091
+ return msg.content.map((c) => {
2092
+ if (c.type === "text") return c.text;
2093
+ if (c.type === "thinking") return c.thinking;
2094
+ return "";
2095
+ }).join("");
2096
+ }
2097
+ return "";
2098
+ }
2099
+ function getContentImages(msg) {
2100
+ if (!Array.isArray(msg.content)) return [];
2101
+ return msg.content.filter((c) => c.type === "image");
2102
+ }
2103
+ function transformTools(tools) {
2104
+ return tools.map((t) => ({
2105
+ type: "function",
2106
+ function: {
2107
+ name: t.name,
2108
+ description: t.description,
2109
+ parameters: t.parameters
2110
+ }
2111
+ }));
2112
+ }
2113
+ function transformMessagesForQoder(messages) {
2114
+ const normalizedMessages = [];
2115
+ const droppedToolCallIds = /* @__PURE__ */ new Set();
2116
+ for (const msg of messages) {
2117
+ if (msg.role === "assistant" && (msg.stopReason === "error" || msg.stopReason === "aborted")) {
2118
+ const am = msg;
2119
+ if (Array.isArray(am.content)) {
2120
+ for (const block of am.content) {
2121
+ if (block.type === "toolCall") {
2122
+ const id = block.id;
2123
+ if (id) droppedToolCallIds.add(id);
2124
+ }
2125
+ }
2126
+ }
2127
+ continue;
2128
+ }
2129
+ if (msg.role === "toolResult" && droppedToolCallIds.has(msg.toolCallId)) {
2130
+ continue;
2131
+ }
2132
+ if (msg.role === "user") {
2133
+ let content = "";
2134
+ if (typeof msg.content === "string") {
2135
+ content = msg.content;
2136
+ } else if (Array.isArray(msg.content)) {
2137
+ const hasImage = msg.content.some((c) => c.type === "image");
2138
+ if (hasImage) {
2139
+ content = msg.content.map((c) => {
2140
+ if (c.type === "text") {
2141
+ return { type: "text", text: c.text };
2142
+ }
2143
+ if (c.type === "image") {
2144
+ const img = c;
2145
+ return {
2146
+ type: "image_url",
2147
+ image_url: {
2148
+ url: `data:${img.mimeType};base64,${img.data}`
2149
+ }
2150
+ };
2151
+ }
2152
+ return null;
2153
+ }).filter((p) => p !== null);
2154
+ } else {
2155
+ content = getContentText(msg);
2156
+ }
2157
+ }
2158
+ normalizedMessages.push({
2159
+ role: "user",
2160
+ content
2161
+ });
2162
+ } else if (msg.role === "assistant") {
2163
+ const am = msg;
2164
+ let content = "";
2165
+ let reasoningContent = "";
2166
+ const toolCalls = [];
2167
+ if (Array.isArray(am.content)) {
2168
+ for (const block of am.content) {
2169
+ if (block.type === "text") {
2170
+ content += block.text;
2171
+ } else if (block.type === "thinking") {
2172
+ reasoningContent += stripDsmlResidue(block.thinking);
2173
+ } else if (block.type === "toolCall") {
2174
+ const tc = block;
2175
+ toolCalls.push({
2176
+ id: tc.id,
2177
+ type: "function",
2178
+ function: {
2179
+ name: tc.name,
2180
+ arguments: typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments)
2181
+ }
2182
+ });
2183
+ }
2184
+ }
2185
+ } else {
2186
+ content = am.content || "";
2187
+ }
2188
+ const mapped = {
2189
+ role: "assistant",
2190
+ content: content || (toolCalls.length > 0 || reasoningContent ? " " : null)
2191
+ };
2192
+ if (reasoningContent) {
2193
+ mapped.reasoning_content = reasoningContent;
2194
+ }
2195
+ if (toolCalls.length > 0) {
2196
+ mapped.tool_calls = toolCalls;
2197
+ }
2198
+ normalizedMessages.push(mapped);
2199
+ } else if (msg.role === "toolResult") {
2200
+ const tr = msg;
2201
+ normalizedMessages.push({
2202
+ role: "tool",
2203
+ tool_call_id: tr.toolCallId,
2204
+ content: getContentText(tr)
2205
+ });
2206
+ const images = getContentImages(tr);
2207
+ if (images.length > 0) {
2208
+ normalizedMessages.push({
2209
+ role: "user",
2210
+ content: [
2211
+ {
2212
+ type: "text",
2213
+ text: `[${images.length} image${images.length === 1 ? "" : "s"} returned by the previous tool call]`
2214
+ },
2215
+ ...images.map(
2216
+ (img) => ({
2217
+ type: "image_url",
2218
+ image_url: { url: `data:${img.mimeType};base64,${img.data}` }
2219
+ })
2220
+ )
2221
+ ]
2222
+ });
2223
+ }
2224
+ }
2225
+ }
2226
+ return normalizedMessages;
2227
+ }
2228
+
2229
+ // src/protocol/stream.ts
2230
+ function stableHash(prefix, ...inputs) {
2231
+ const hash = crypto3.createHash("sha256");
2232
+ hash.update(prefix);
2233
+ for (const input of inputs) {
2234
+ hash.update("\0");
2235
+ hash.update(input);
2236
+ }
2237
+ return hash.digest("hex").slice(0, 16);
2238
+ }
2239
+ function stableChatRecordID(model, messages, tools, maxTokens) {
2240
+ const hash = crypto3.createHash("sha256");
2241
+ hash.update("qoder-record");
2242
+ hash.update("\0");
2243
+ hash.update(model);
2244
+ for (const msg of messages) {
2245
+ if (msg?.role) {
2246
+ hash.update("\0");
2247
+ hash.update(msg.role);
2248
+ }
2249
+ if (msg?.content) {
2250
+ hash.update("\0");
2251
+ hash.update(typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content));
2252
+ }
2253
+ }
2254
+ if (tools) {
2255
+ hash.update("\0");
2256
+ hash.update(JSON.stringify(tools));
2257
+ }
2258
+ hash.update("\0");
2259
+ hash.update(`mt=${maxTokens}`);
2260
+ return hash.digest("hex").slice(0, 16);
2261
+ }
2262
+ function contentToText(content) {
2263
+ if (typeof content === "string") return content;
2264
+ if (Array.isArray(content)) {
2265
+ return content.map((part) => {
2266
+ if (typeof part === "string") return part;
2267
+ if (part && typeof part === "object" && "text" in part) return part.text;
2268
+ return "";
2269
+ }).join("\n");
2270
+ }
2271
+ return "";
2272
+ }
2273
+ function resolveRequestContext(piAi, context) {
2274
+ const {
2275
+ collapseSystemMessages,
2276
+ getCurrentTools,
2277
+ getInitialSystemMessage,
2278
+ getSystemMessageText,
2279
+ withoutInitialSystemMessage
2280
+ } = piAi;
2281
+ const hasTranscript = typeof collapseSystemMessages === "function" && typeof getCurrentTools === "function" && typeof getInitialSystemMessage === "function" && typeof getSystemMessageText === "function" && typeof withoutInitialSystemMessage === "function";
2282
+ if (!hasTranscript) {
2283
+ return {
2284
+ messages: context.messages,
2285
+ systemText: contentToText(context.systemPrompt || ""),
2286
+ tools: context.tools ?? []
2287
+ };
2288
+ }
2289
+ const collapsed = collapseSystemMessages(context);
2290
+ const transcriptMessages = collapsed.messages;
2291
+ const initialSystem = getInitialSystemMessage(transcriptMessages);
2292
+ const tools = getCurrentTools(transcriptMessages);
2293
+ const systemText = contentToText(
2294
+ initialSystem && getSystemMessageText ? getSystemMessageText(initialSystem) : context.systemPrompt || ""
2295
+ );
2296
+ const messages = withoutInitialSystemMessage(transcriptMessages);
2297
+ return { messages, systemText, tools };
2298
+ }
2299
+ function streamQoder(model, context, options) {
2300
+ const StreamCtor = PiAi.AssistantMessageEventStream;
2301
+ const stream = new StreamCtor();
2302
+ const output = {
2303
+ role: "assistant",
2304
+ content: [],
2305
+ api: model.api,
2306
+ provider: model.provider,
2307
+ model: model.id,
2308
+ usage: {
2309
+ input: 0,
2310
+ output: 0,
2311
+ cacheRead: 0,
2312
+ cacheWrite: 0,
2313
+ totalTokens: 0,
2314
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
2315
+ },
2316
+ stopReason: "stop",
2317
+ timestamp: Date.now()
2318
+ };
2319
+ (async () => {
2320
+ try {
2321
+ const providerMode = model.provider === "qoder-cn" ? "cn" : "global";
2322
+ const region = getQoderRegionConfig(providerMode);
2323
+ const accessToken = options?.apiKey;
2324
+ if (!accessToken) {
2325
+ throw new Error(
2326
+ providerMode === "cn" ? "Qoder CN credentials not set. Run /login qoder-cn or set QODERCN_PERSONAL_ACCESS_TOKEN." : "Qoder credentials not set. Run /login qoder or set QODER_PERSONAL_ACCESS_TOKEN."
2327
+ );
2328
+ }
2329
+ const ident = await resolveQoderIdentity(accessToken, model.provider, providerMode);
2330
+ const userID = ident.userID || "qoder-user";
2331
+ const name = ident.name || region.userNameFallback;
2332
+ const email = ident.email || region.userEmailFallback;
2333
+ const machineID = ident.machineID || getMachineId();
2334
+ const modelConfig = getCachedModelConfig(model.id, providerMode);
2335
+ if (!modelConfig?.key) {
2336
+ throw new Error(`Unknown Qoder model id: ${model.id}`);
2337
+ }
2338
+ const qoderModel = modelConfig.key;
2339
+ const isReasoning = !!modelConfig.is_reasoning;
2340
+ const resolved = resolveRequestContext(PiAi, context);
2341
+ const normalizedMessages = transformMessagesForQoder(resolved.messages);
2342
+ const systemText = resolved.systemText;
2343
+ let lastUserText = "";
2344
+ for (let i = normalizedMessages.length - 1; i >= 0; i--) {
2345
+ if (normalizedMessages[i].role === "user") {
2346
+ const content = normalizedMessages[i].content;
2347
+ lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => "text" in c ? c.text : "").join("") : "";
2348
+ break;
2349
+ }
2350
+ }
2351
+ const stablePart = stableHash("qoder-session", userID, qoderModel);
2352
+ const sessionID = options?.sessionId ? `${stablePart}-${options.sessionId}` : `${stablePart}-${crypto3.randomUUID()}`;
2353
+ let maxTokens = MAX_OUTPUT_TOKENS;
2354
+ if (options?.maxTokens && options.maxTokens < maxTokens) {
2355
+ maxTokens = options.maxTokens;
2356
+ }
2357
+ const toolsRaw = resolved.tools.length > 0 ? transformTools(resolved.tools) : void 0;
2358
+ const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
2359
+ const requestedLevel = options?.reasoning;
2360
+ const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
2361
+ const reasoningLevel = clamped === "off" ? void 0 : clamped;
2362
+ const parameters = { max_tokens: maxTokens };
2363
+ if (reasoningLevel) {
2364
+ parameters.enable_thinking = true;
2365
+ const mapped = model.thinkingLevelMap?.[reasoningLevel];
2366
+ const effort = mapped && mapped !== "enabled" && mapped !== "disabled" ? mapped : reasoningLevel;
2367
+ if (modelConfig?.thinking_config?.enabled?.efforts && typeof effort === "string") {
2368
+ parameters.reasoning_effort = effort;
2369
+ }
2370
+ } else {
2371
+ parameters.enable_thinking = false;
2372
+ }
2373
+ const reqBody = {
2374
+ request_id: crypto3.randomUUID(),
2375
+ request_set_id: recordID,
2376
+ chat_record_id: recordID,
2377
+ session_id: sessionID,
2378
+ stream: true,
2379
+ chat_task: "FREE_INPUT",
2380
+ is_reply: true,
2381
+ is_retry: false,
2382
+ source: 1,
2383
+ version: "3",
2384
+ session_type: "qodercli",
2385
+ agent_id: "agent_common",
2386
+ task_id: "common",
2387
+ code_language: "",
2388
+ chat_prompt: "",
2389
+ image_urls: null,
2390
+ aliyun_user_type: "",
2391
+ // Qoder's server ignores the top-level `system` field (verified: the
2392
+ // model never sees it). Inject the system prompt as a leading
2393
+ // role:system message instead, which the server does honor.
2394
+ system: "",
2395
+ messages: systemText ? [{ role: "system", content: systemText }, ...normalizedMessages] : normalizedMessages,
2396
+ tools: toolsRaw || [],
2397
+ parameters,
2398
+ chat_context: {
2399
+ chatPrompt: "",
2400
+ imageUrls: null,
2401
+ extra: {
2402
+ context: [],
2403
+ modelConfig: {
2404
+ key: qoderModel,
2405
+ is_reasoning: isReasoning
2406
+ },
2407
+ originalContent: lastUserText
2408
+ },
2409
+ features: [],
2410
+ text: lastUserText
2411
+ },
2412
+ model_config: modelConfig,
2413
+ business: {
2414
+ product: "cli",
2415
+ version: "1.0.0",
2416
+ type: "agent",
2417
+ stage: "start",
2418
+ id: crypto3.randomUUID(),
2419
+ name: lastUserText.substring(0, 30),
2420
+ begin_at: Date.now()
2421
+ }
2422
+ };
2423
+ const bodyBytes = Buffer.from(JSON.stringify(reqBody));
2424
+ const encodedBytes = qoderEncodeBody(bodyBytes);
2425
+ const chatURL = getQoderChatURL(providerMode);
2426
+ const headers = buildAuthHeaders(encodedBytes, chatURL, {
2427
+ userID,
2428
+ authToken: accessToken,
2429
+ name,
2430
+ email,
2431
+ machineID
2432
+ });
2433
+ const modelSource = modelConfig.source || "system";
2434
+ const response = await fetch(chatURL, {
2435
+ method: "POST",
2436
+ headers: {
2437
+ "Content-Type": "application/json",
2438
+ Accept: "text/event-stream",
2439
+ "Cache-Control": "no-cache",
2440
+ "Accept-Encoding": "identity",
2441
+ "X-Model-Key": qoderModel,
2442
+ "X-Model-Source": modelSource,
2443
+ ...headers
2444
+ },
2445
+ body: encodedBytes,
2446
+ signal: options?.signal
2447
+ });
2448
+ if (!response.ok) {
2449
+ const errText = await response.text();
2450
+ throw new Error(`Qoder API request failed: ${response.status} ${response.statusText}. Response: ${errText}`);
2451
+ }
2452
+ const reader = response.body?.getReader();
2453
+ if (!reader) throw new Error("No response body");
2454
+ const decoder = new TextDecoder();
2455
+ let buffer = "";
2456
+ let bufferStart = 0;
2457
+ let contentBlockIndex = -1;
2458
+ let thinkingBlockIndex = -1;
2459
+ let rawReasoningTail = "";
2460
+ const toolCallsState = [];
2461
+ const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
2462
+ const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
2463
+ stream.push({ type: "start", partial: output });
2464
+ let sawDone = false;
2465
+ while (!sawDone) {
2466
+ const { done, value } = await reader.read();
2467
+ if (done) break;
2468
+ if (bufferStart > 0) {
2469
+ buffer = buffer.substring(bufferStart);
2470
+ bufferStart = 0;
2471
+ }
2472
+ buffer += decoder.decode(value, { stream: true });
2473
+ while (true) {
2474
+ const lineEnd = buffer.indexOf("\n", bufferStart);
2475
+ if (lineEnd === -1) break;
2476
+ const line = buffer.substring(bufferStart, lineEnd).trim();
2477
+ bufferStart = lineEnd + 1;
2478
+ if (!line.startsWith("data:")) continue;
2479
+ const dataStr = line.substring(5).trim();
2480
+ if (dataStr === "[DONE]") {
2481
+ sawDone = true;
2482
+ break;
2483
+ }
2484
+ try {
2485
+ const envelope = JSON.parse(dataStr);
2486
+ if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
2487
+ throw new Error(`Upstream status ${envelope.statusCodeValue}: ${envelope.body}`);
2488
+ }
2489
+ const innerStr = envelope.body;
2490
+ if (innerStr === "[DONE]") {
2491
+ sawDone = true;
2492
+ break;
2493
+ }
2494
+ if (!innerStr) continue;
2495
+ const inner = JSON.parse(innerStr);
2496
+ if (inner.id) output.responseId = inner.id;
2497
+ if (inner.model) output.responseModel = inner.model;
2498
+ if (inner.usage) {
2499
+ const u = inner.usage;
2500
+ const promptTokens = u.prompt_tokens ?? 0;
2501
+ const cacheReadTokens = u.prompt_tokens_details?.cached_tokens ?? 0;
2502
+ const cacheWriteTokens = u.prompt_tokens_details?.cache_write_tokens ?? 0;
2503
+ output.usage.input = Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens);
2504
+ output.usage.output = u.completion_tokens ?? 0;
2505
+ output.usage.totalTokens = u.total_tokens ?? 0;
2506
+ output.usage.cacheRead = cacheReadTokens;
2507
+ output.usage.cacheWrite = cacheWriteTokens;
2508
+ }
2509
+ if (inner.choices && inner.choices.length > 0) {
2510
+ const choice = inner.choices[0];
2511
+ const delta = choice.delta;
2512
+ if (delta) {
2513
+ if (delta.reasoning_content) {
2514
+ rawReasoningTail = (rawReasoningTail + delta.reasoning_content).slice(-512);
2515
+ const reasoningChunk = stripDsmlResidue(stripThinkingTags(delta.reasoning_content));
2516
+ if (reasoningChunk) {
2517
+ if (thinkingBlockIndex === -1) {
2518
+ thinkingBlockIndex = output.content.length;
2519
+ output.content.push({ type: "thinking", thinking: "" });
2520
+ stream.push({ type: "thinking_start", contentIndex: thinkingBlockIndex, partial: output });
2521
+ }
2522
+ const block = output.content[thinkingBlockIndex];
2523
+ block.thinking += reasoningChunk;
2524
+ stream.push({
2525
+ type: "thinking_delta",
2526
+ contentIndex: thinkingBlockIndex,
2527
+ delta: reasoningChunk,
2528
+ partial: output
2529
+ });
2530
+ }
2531
+ }
2532
+ if (delta.content) {
2533
+ if (thinkingBlockIndex !== -1) {
2534
+ const block = output.content[thinkingBlockIndex];
2535
+ stream.push({
2536
+ type: "thinking_end",
2537
+ contentIndex: thinkingBlockIndex,
2538
+ content: block.thinking,
2539
+ partial: output
2540
+ });
2541
+ thinkingBlockIndex = -1;
2542
+ }
2543
+ if (thinkingParser) {
2544
+ thinkingParser.processChunk(delta.content);
2545
+ } else {
2546
+ if (contentBlockIndex === -1) {
2547
+ contentBlockIndex = output.content.length;
2548
+ output.content.push({ type: "text", text: "" });
2549
+ stream.push({ type: "text_start", contentIndex: contentBlockIndex, partial: output });
2550
+ }
2551
+ const block = output.content[contentBlockIndex];
2552
+ block.text += delta.content;
2553
+ stream.push({
2554
+ type: "text_delta",
2555
+ contentIndex: contentBlockIndex,
2556
+ delta: delta.content,
2557
+ partial: output
2558
+ });
2559
+ }
2560
+ }
2561
+ if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
2562
+ for (const tc of delta.tool_calls) {
2563
+ const idx = tc.index ?? 0;
2564
+ if (!toolCallsState[idx]) {
2565
+ toolCallsState[idx] = { arguments: "", id: "", name: "", contentIndex: 0 };
2566
+ }
2567
+ const state = toolCallsState[idx];
2568
+ if (tc.id) state.id = tc.id;
2569
+ if (tc.function?.name) state.name = tc.function.name;
2570
+ if (state.emittedStart === void 0 && (state.id || state.name)) {
2571
+ state.emittedStart = true;
2572
+ state.contentIndex = output.content.length;
2573
+ output.content.push({
2574
+ type: "toolCall",
2575
+ id: state.id,
2576
+ name: state.name,
2577
+ arguments: {}
2578
+ });
2579
+ stream.push({ type: "toolcall_start", contentIndex: state.contentIndex, partial: output });
2580
+ }
2581
+ if (state.emittedStart) {
2582
+ const block = output.content[state.contentIndex];
2583
+ block.id = state.id;
2584
+ block.name = state.name;
2585
+ }
2586
+ if (tc.function?.arguments) {
2587
+ const argDelta = tc.function.arguments;
2588
+ state.arguments += argDelta;
2589
+ stream.push({
2590
+ type: "toolcall_delta",
2591
+ contentIndex: state.contentIndex,
2592
+ delta: argDelta,
2593
+ partial: output
2594
+ });
2595
+ }
2596
+ }
2597
+ }
2598
+ }
2599
+ if (choice.finish_reason) {
2600
+ output.stopReason = choice.finish_reason;
2601
+ }
2602
+ }
2603
+ } catch (e) {
2604
+ if (e instanceof SyntaxError) {
2605
+ if (process.env.QODER_DEBUG) {
2606
+ console.error("[pi-provider-qoder] skipping malformed SSE line:", dataStr.slice(0, 200));
2607
+ }
2608
+ continue;
2609
+ }
2610
+ throw e;
2611
+ }
2612
+ }
2613
+ }
2614
+ await reader.cancel().catch(() => {
2615
+ });
2616
+ if (thinkingParser) {
2617
+ thinkingParser.finalize();
2618
+ }
2619
+ if (thinkingBlockIndex !== -1) {
2620
+ const block = output.content[thinkingBlockIndex];
2621
+ stream.push({
2622
+ type: "thinking_end",
2623
+ contentIndex: thinkingBlockIndex,
2624
+ content: block.thinking,
2625
+ partial: output
2626
+ });
2627
+ }
2628
+ for (const state of toolCallsState) {
2629
+ if (state?.emittedStart && !state.emittedEnd) {
2630
+ state.emittedEnd = true;
2631
+ let args = {};
2632
+ try {
2633
+ args = JSON.parse(state.arguments || "{}");
2634
+ } catch {
2635
+ }
2636
+ const block = output.content[state.contentIndex];
2637
+ block.arguments = args;
2638
+ stream.push({
2639
+ type: "toolcall_end",
2640
+ contentIndex: state.contentIndex,
2641
+ toolCall: {
2642
+ type: "toolCall",
2643
+ id: state.id,
2644
+ name: state.name,
2645
+ arguments: args
2646
+ },
2647
+ partial: output
2648
+ });
2649
+ }
2650
+ }
2651
+ if (toolCallsState.some((state) => state?.emittedStart)) {
2652
+ output.stopReason = "toolUse";
2653
+ }
2654
+ if (isDegenerateDsmlTurn(output, rawReasoningTail)) {
2655
+ output.stopReason = "error";
2656
+ output.errorMessage = "Qoder server error: degenerate model output (unparseable DSML tool-call markup, no tool call executed)";
2657
+ stream.push({ type: "error", reason: "error", error: output });
2658
+ stream.end();
2659
+ return;
2660
+ }
2661
+ stream.push({
2662
+ type: "done",
2663
+ reason: output.stopReason,
2664
+ message: output
2665
+ });
2666
+ stream.end();
2667
+ } catch (e) {
2668
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
2669
+ output.errorMessage = e instanceof Error ? e.message : String(e);
2670
+ stream.push({ type: "error", reason: output.stopReason, error: output });
2671
+ try {
2672
+ stream.end();
2673
+ } catch {
2674
+ }
2675
+ }
2676
+ })();
2677
+ return stream;
2678
+ }
2679
+
2680
+ // src/index.ts
2681
+ init_region();
2682
+ var QODER_API = "qoder-api";
2683
+ async function registerQoderApi() {
2684
+ try {
2685
+ const compat = await import("@earendil-works/pi-ai/compat");
2686
+ const register = compat.registerApiProvider;
2687
+ if (typeof register !== "function") return;
2688
+ register(
2689
+ { api: QODER_API, stream: streamQoder, streamSimple: streamQoder },
2690
+ "provider:qoder"
2691
+ );
2692
+ } catch {
2693
+ }
2694
+ }
2695
+ function modelsForProvider(mode, providerID) {
2696
+ const cached = getCachedModels(mode);
2697
+ const modelsToUse = cached.length > 0 ? cached : mode === "cn" ? staticCnModels : staticModels;
2698
+ return modelsToUse.map((m) => ({
2699
+ ...m,
2700
+ provider: providerID,
2701
+ baseUrl: getQoderBaseUrl(mode)
2702
+ }));
2703
+ }
2704
+ function createQoderOAuth(mode) {
2705
+ const region = getQoderRegionConfig(mode);
2706
+ return {
2707
+ name: region.loginName,
2708
+ login: (callbacks) => loginQoderForMode(callbacks, mode),
2709
+ refreshToken: (credentials) => refreshQoderTokenForMode(credentials, mode),
2710
+ getApiKey: (cred) => cred.access,
2711
+ // NOTE: no `modifyModels` hook on purpose. OMP (Bun) does a whole-catalog
2712
+ // structuredClone before invoking it, and its bundled catalog contains a
2713
+ // model with a non-cloneable property -> "The object can not be cloned."
2714
+ // removes qoder from `omp models`. Models are supplied at registration
2715
+ // via `modelsForProvider` and refreshed by the startup/session cache hooks.
2716
+ fetchUsage: (credentials) => fetchQoderUsageForMode(credentials, mode)
2717
+ };
2718
+ }
2719
+ function registerQoderProvider(pi, mode) {
2720
+ const providerID = getQoderRegionConfig(mode).providerID;
2721
+ const oauth = createQoderOAuth(mode);
2722
+ pi.registerProvider(providerID, {
2723
+ baseUrl: getQoderBaseUrl(mode),
2724
+ api: QODER_API,
2725
+ models: modelsForProvider(mode, providerID),
2726
+ oauth,
2727
+ // pi-coding-agent resolves its own nested @earendil-works/pi-ai copy, so the
2728
+ // structurally identical Model/Context types are nominally distinct here.
2729
+ streamSimple: streamQoder
2730
+ });
2731
+ }
2732
+ async function refreshModelsAtStartup(mode) {
2733
+ const providerID = getQoderRegionConfig(mode).providerID;
2734
+ if (!isCacheStale(mode)) return;
2735
+ const credentials = getCachedCredentials("", providerID);
2736
+ if (!credentials?.access) return;
2737
+ const region = getQoderRegionConfig(mode);
2738
+ await updateQoderModelsCache(
2739
+ credentials.access,
2740
+ credentials.userID || "qoder-user",
2741
+ credentials.name || region.userNameFallback,
2742
+ credentials.email || region.userEmailFallback,
2743
+ mode
2744
+ );
2745
+ }
2746
+ async function index_default(pi) {
2747
+ await registerQoderApi();
2748
+ for (const mode of QODER_PROVIDER_MODES) {
2749
+ const providerID = getQoderRegionConfig(mode).providerID;
2750
+ try {
2751
+ await autoLoginQoderFromEnvironment(providerID, mode);
2752
+ await refreshModelsAtStartup(mode);
2753
+ } catch (error) {
2754
+ const message = error instanceof Error ? error.message : String(error);
2755
+ console.error(`[pi-provider-qoder-cn] Automatic login failed for ${providerID}: ${message}`);
2756
+ }
2757
+ }
2758
+ pi.on("session_start", async (_event, ctx) => {
2759
+ for (const mode of QODER_PROVIDER_MODES) {
2760
+ try {
2761
+ const region = getQoderRegionConfig(mode);
2762
+ const providerID = region.providerID;
2763
+ const accessToken = await ctx.modelRegistry.getApiKeyForProvider(providerID);
2764
+ if (!accessToken || !isCacheStale(mode)) continue;
2765
+ const creds = getCachedCredentials(accessToken, providerID);
2766
+ const userID = creds?.userID || "qoder-user";
2767
+ const name = creds?.name || region.userNameFallback;
2768
+ const email = creds?.email || region.userEmailFallback;
2769
+ await updateQoderModelsCache(accessToken, userID, name, email, mode);
2770
+ } catch {
2771
+ }
2772
+ }
2773
+ });
2774
+ for (const mode of QODER_PROVIDER_MODES) registerQoderProvider(pi, mode);
2775
+ registerCommands(pi);
2776
+ }
2777
+ function registerCommands(pi) {
2778
+ for (const mode of QODER_PROVIDER_MODES) {
2779
+ const providerID = getQoderRegionConfig(mode).providerID;
2780
+ pi.registerCommand(`${providerID}.usage`, {
2781
+ description: `Show ${providerID} quota: plan + add-on credits, used/limit and reset (append 'json' for the raw payload)`,
2782
+ handler: async (args, ctx) => {
2783
+ await runUsageCommand(mode, args, ctx);
2784
+ }
2785
+ });
2786
+ }
2787
+ pi.registerCommand("qoder-endpoint", {
2788
+ description: "Show or set the Qoder CN gateway endpoint (use 'default' to reset)",
2789
+ handler: async (args, ctx) => {
2790
+ await runEndpointCommand(args, ctx);
2791
+ }
2792
+ });
2793
+ }
2794
+ export {
2795
+ index_default as default
2796
+ };