pi-provider-qoder 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 (3) hide show
  1. package/README.md +85 -0
  2. package/dist/index.js +1530 -0
  3. package/package.json +43 -0
package/dist/index.js ADDED
@@ -0,0 +1,1530 @@
1
+ // src/login-ui.ts
2
+ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
3
+ import { Container, SelectList, Text } from "@earendil-works/pi-tui";
4
+ var _ctx;
5
+ function setExtensionContext(ctx) {
6
+ _ctx = ctx;
7
+ }
8
+ function hasExtensionContext() {
9
+ return _ctx !== void 0;
10
+ }
11
+ async function showLoginUI() {
12
+ if (!_ctx) return null;
13
+ const ctx = _ctx;
14
+ return ctx.ui.custom((tui, theme, _kb, done) => {
15
+ const mainItems = [
16
+ { value: "web", label: "Browser Login", description: "Sign in via browser (OAuth device flow)" }
17
+ ];
18
+ const container = new Container();
19
+ const border = new DynamicBorder((s) => theme.fg("accent", s));
20
+ const title = new Text(theme.fg("accent", theme.bold("Qoder Login")), 1, 0);
21
+ const hint = new Text(theme.fg("dim", "\u2191\u2193 navigate \u2022 enter select \u2022 esc cancel"), 1, 0);
22
+ const borderBottom = new DynamicBorder((s) => theme.fg("accent", s));
23
+ const selectList = new SelectList(mainItems, mainItems.length, {
24
+ selectedPrefix: (t) => theme.fg("accent", t),
25
+ selectedText: (t) => theme.fg("accent", t),
26
+ description: (t) => theme.fg("muted", t),
27
+ scrollInfo: (t) => theme.fg("dim", t),
28
+ noMatch: (t) => theme.fg("warning", t)
29
+ });
30
+ selectList.onSelect = (item) => {
31
+ done({ method: item.value });
32
+ };
33
+ selectList.onCancel = () => done(null);
34
+ container.addChild(border);
35
+ container.addChild(title);
36
+ container.addChild(selectList);
37
+ container.addChild(hint);
38
+ container.addChild(borderBottom);
39
+ tui.requestRender();
40
+ return {
41
+ render(width) {
42
+ return container.render(width);
43
+ },
44
+ invalidate() {
45
+ container.invalidate();
46
+ },
47
+ handleInput(data) {
48
+ selectList.handleInput(data);
49
+ tui.requestRender();
50
+ }
51
+ };
52
+ });
53
+ }
54
+ async function showWaitingUI(outerCallbacks, runAuth) {
55
+ if (!_ctx) {
56
+ return runAuth(outerCallbacks);
57
+ }
58
+ const ctx = _ctx;
59
+ return ctx.ui.custom((tui, theme, _kb, done) => {
60
+ const container = new Container();
61
+ const border = new DynamicBorder((s) => theme.fg("accent", s));
62
+ const title = new Text(theme.fg("accent", theme.bold("Qoder Login - Authorization")), 1, 0);
63
+ const borderBottom = new DynamicBorder((s) => theme.fg("accent", s));
64
+ const statusText = new Text("Initiating login flow...", 1, 0);
65
+ const urlText = new Text("", 1, 0);
66
+ const instructionsText = new Text("", 1, 0);
67
+ const hint = new Text(theme.fg("dim", "esc cancel / back"), 1, 0);
68
+ container.addChild(border);
69
+ container.addChild(title);
70
+ container.addChild(statusText);
71
+ container.addChild(urlText);
72
+ container.addChild(instructionsText);
73
+ container.addChild(hint);
74
+ container.addChild(borderBottom);
75
+ const abortCtrl = new AbortController();
76
+ let onAuthCalled = false;
77
+ const mergedCallbacks = {
78
+ ...outerCallbacks,
79
+ onProgress: (msg) => {
80
+ outerCallbacks.onProgress?.(msg);
81
+ statusText.setText(msg);
82
+ tui.requestRender();
83
+ },
84
+ onAuth: (info) => {
85
+ if (!onAuthCalled) {
86
+ onAuthCalled = true;
87
+ outerCallbacks.onAuth?.(info);
88
+ }
89
+ urlText.setText(`URL: ${info.url}`);
90
+ instructionsText.setText(info.instructions || "");
91
+ tui.requestRender();
92
+ },
93
+ signal: abortCtrl.signal
94
+ };
95
+ runAuth(mergedCallbacks).then(
96
+ (creds) => {
97
+ done(creds);
98
+ },
99
+ (err) => {
100
+ if (abortCtrl.signal.aborted) {
101
+ done(null);
102
+ } else {
103
+ statusText.setText(theme.fg("warning", `Error: ${err.message || err}`));
104
+ tui.requestRender();
105
+ setTimeout(() => done(null), 3e3);
106
+ }
107
+ }
108
+ );
109
+ return {
110
+ render(width) {
111
+ return container.render(width);
112
+ },
113
+ invalidate() {
114
+ container.invalidate();
115
+ },
116
+ handleInput(data) {
117
+ if (data.length === 1 && data.charCodeAt(0) === 27 || data === "q") {
118
+ abortCtrl.abort();
119
+ done(null);
120
+ }
121
+ }
122
+ };
123
+ });
124
+ }
125
+
126
+ // src/models.ts
127
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
128
+ import { homedir as homedir2 } from "node:os";
129
+ import { dirname as dirname2, join as join2 } from "node:path";
130
+
131
+ // src/cosy.ts
132
+ import crypto from "node:crypto";
133
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
134
+ import { homedir } from "node:os";
135
+ import { dirname, join } from "node:path";
136
+ var qoderRSAPublicKey = `-----BEGIN PUBLIC KEY-----
137
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
138
+ 4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
139
+ 6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
140
+ XcW+ML9FoCI6AOvOzwIDAQAB
141
+ -----END PUBLIC KEY-----`;
142
+ var QoderIDEVersion = "1.0.0";
143
+ var QoderClientType = "5";
144
+ var QoderDataPolicy = "disagree";
145
+ var QoderLoginVersion = "v2";
146
+ var QoderMachineOS = "x86_64_windows";
147
+ var QoderMachineTypeMagic = "5";
148
+ function rsaEncryptBase64(data) {
149
+ const key = {
150
+ key: qoderRSAPublicKey,
151
+ padding: crypto.constants.RSA_PKCS1_PADDING
152
+ };
153
+ const encrypted = crypto.publicEncrypt(key, typeof data === "string" ? Buffer.from(data) : data);
154
+ return encrypted.toString("base64");
155
+ }
156
+ function aesEncryptCBCBase64(plaintext, keyStr) {
157
+ const cipher = crypto.createCipheriv("aes-128-cbc", Buffer.from(keyStr), Buffer.from(keyStr));
158
+ let encrypted = cipher.update(plaintext, "utf8", "base64");
159
+ encrypted += cipher.final("base64");
160
+ return encrypted;
161
+ }
162
+ function computeSigPath(urlStr) {
163
+ const parsed = new URL(urlStr);
164
+ let sigPath = parsed.pathname;
165
+ if (sigPath.startsWith("/algo")) {
166
+ sigPath = sigPath.substring("/algo".length);
167
+ }
168
+ return sigPath;
169
+ }
170
+ function getMachineId() {
171
+ const paths = [join(homedir(), ".qoder", ".auth", "machine_id"), join(homedir(), ".pi", "agent", "qoder-machine-id")];
172
+ for (const p of paths) {
173
+ if (existsSync(p)) {
174
+ try {
175
+ const val = readFileSync(p, "utf8").trim();
176
+ if (val) return val;
177
+ } catch {
178
+ }
179
+ }
180
+ }
181
+ const newId = crypto.randomUUID();
182
+ try {
183
+ const savePath = paths[1];
184
+ mkdirSync(dirname(savePath), { recursive: true });
185
+ writeFileSync(savePath, newId, "utf8");
186
+ } catch {
187
+ }
188
+ return newId;
189
+ }
190
+ function buildAuthHeaders(body, requestURL, creds) {
191
+ if (!creds.userID) {
192
+ throw new Error("cosy: user id is empty");
193
+ }
194
+ if (!creds.authToken) {
195
+ throw new Error("cosy: auth token is empty");
196
+ }
197
+ const aesKey = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
198
+ const userInfo = {
199
+ uid: creds.userID,
200
+ security_oauth_token: creds.authToken,
201
+ name: creds.name || "",
202
+ aid: "",
203
+ email: creds.email || ""
204
+ };
205
+ const infoB64 = aesEncryptCBCBase64(JSON.stringify(userInfo), aesKey);
206
+ const cosyKey = rsaEncryptBase64(aesKey);
207
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
208
+ const requestId = crypto.randomUUID();
209
+ const cosyPayload = {
210
+ version: "v1",
211
+ requestId,
212
+ info: infoB64,
213
+ cosyVersion: QoderIDEVersion,
214
+ ideVersion: ""
215
+ };
216
+ const payloadB64 = Buffer.from(JSON.stringify(cosyPayload)).toString("base64");
217
+ const sigPath = computeSigPath(requestURL);
218
+ const bodyStr = body ? Buffer.isBuffer(body) ? body.toString("utf8") : body : "";
219
+ const sigInput = `${payloadB64}
220
+ ${cosyKey}
221
+ ${timestamp}
222
+ ${bodyStr}
223
+ ${sigPath}`;
224
+ const sig = crypto.createHash("md5").update(sigInput).digest("hex");
225
+ const bodyHash = crypto.createHash("md5").update(body || "").digest("hex");
226
+ const bodyLen = body ? (Buffer.isBuffer(body) ? body.length : Buffer.from(body).length).toString() : "0";
227
+ const machineID = creds.machineID || getMachineId();
228
+ return {
229
+ Authorization: `Bearer COSY.${payloadB64}.${sig}`,
230
+ "Cosy-Key": cosyKey,
231
+ "Cosy-User": creds.userID,
232
+ "Cosy-Date": timestamp,
233
+ "Cosy-Version": QoderIDEVersion,
234
+ "Cosy-Machineid": machineID,
235
+ "Cosy-Machinetoken": machineID,
236
+ "Cosy-Machinetype": QoderMachineTypeMagic,
237
+ "Cosy-Machineos": QoderMachineOS,
238
+ "Cosy-Clienttype": QoderClientType,
239
+ "Cosy-Clientip": "127.0.0.1",
240
+ "Cosy-Bodyhash": bodyHash,
241
+ "Cosy-Bodylength": bodyLen,
242
+ "Cosy-Sigpath": sigPath,
243
+ "Cosy-Data-Policy": QoderDataPolicy,
244
+ "Cosy-Organization-Id": "",
245
+ "Cosy-Organization-Tags": "",
246
+ "Login-Version": QoderLoginVersion,
247
+ "X-Request-Id": crypto.randomUUID()
248
+ };
249
+ }
250
+
251
+ // src/models.ts
252
+ var CACHE_PATH = join2(homedir2(), ".pi", "agent", "qoder-models-cache.json");
253
+ var ZERO_COST = Object.freeze({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
254
+ var staticModels = [
255
+ {
256
+ id: "auto",
257
+ name: "Qoder Auto",
258
+ api: "qoder-api",
259
+ provider: "qoder",
260
+ baseUrl: "https://api3.qoder.sh/",
261
+ reasoning: true,
262
+ supportsEffort: false,
263
+ input: ["text", "image"],
264
+ cost: ZERO_COST,
265
+ contextWindow: 18e4,
266
+ maxTokens: 32768
267
+ },
268
+ {
269
+ id: "ultimate",
270
+ name: "Qoder Ultimate",
271
+ api: "qoder-api",
272
+ provider: "qoder",
273
+ baseUrl: "https://api3.qoder.sh/",
274
+ reasoning: true,
275
+ supportsEffort: true,
276
+ input: ["text", "image"],
277
+ cost: ZERO_COST,
278
+ contextWindow: 1e6,
279
+ maxTokens: 32768
280
+ },
281
+ {
282
+ id: "performance",
283
+ name: "Qoder Performance",
284
+ api: "qoder-api",
285
+ provider: "qoder",
286
+ baseUrl: "https://api3.qoder.sh/",
287
+ reasoning: true,
288
+ supportsEffort: true,
289
+ input: ["text", "image"],
290
+ cost: ZERO_COST,
291
+ contextWindow: 1e6,
292
+ maxTokens: 32768
293
+ },
294
+ {
295
+ id: "efficient",
296
+ name: "Qoder Efficient",
297
+ api: "qoder-api",
298
+ provider: "qoder",
299
+ baseUrl: "https://api3.qoder.sh/",
300
+ reasoning: false,
301
+ supportsEffort: false,
302
+ input: ["text", "image"],
303
+ cost: ZERO_COST,
304
+ contextWindow: 18e4,
305
+ maxTokens: 32768
306
+ },
307
+ {
308
+ id: "lite",
309
+ name: "Qoder Lite",
310
+ api: "qoder-api",
311
+ provider: "qoder",
312
+ baseUrl: "https://api3.qoder.sh/",
313
+ reasoning: false,
314
+ supportsEffort: false,
315
+ input: ["text"],
316
+ cost: ZERO_COST,
317
+ contextWindow: 18e4,
318
+ maxTokens: 32768
319
+ },
320
+ {
321
+ id: "qmodel",
322
+ name: "Qwen3.7 Plus (Qoder)",
323
+ api: "qoder-api",
324
+ provider: "qoder",
325
+ baseUrl: "https://api3.qoder.sh/",
326
+ reasoning: false,
327
+ supportsEffort: false,
328
+ input: ["text", "image"],
329
+ cost: ZERO_COST,
330
+ contextWindow: 1e6,
331
+ maxTokens: 32768
332
+ },
333
+ {
334
+ id: "qmodel_latest",
335
+ name: "Qwen3.7 Max (Qoder)",
336
+ api: "qoder-api",
337
+ provider: "qoder",
338
+ baseUrl: "https://api3.qoder.sh/",
339
+ reasoning: false,
340
+ supportsEffort: false,
341
+ input: ["text", "image"],
342
+ cost: ZERO_COST,
343
+ contextWindow: 1e6,
344
+ maxTokens: 32768
345
+ },
346
+ {
347
+ id: "dmodel",
348
+ name: "DeepSeek V4 Pro (Qoder)",
349
+ api: "qoder-api",
350
+ provider: "qoder",
351
+ baseUrl: "https://api3.qoder.sh/",
352
+ reasoning: true,
353
+ supportsEffort: true,
354
+ input: ["text", "image"],
355
+ cost: ZERO_COST,
356
+ contextWindow: 1e6,
357
+ maxTokens: 32768
358
+ },
359
+ {
360
+ id: "dfmodel",
361
+ name: "DeepSeek V4 Flash (Qoder)",
362
+ api: "qoder-api",
363
+ provider: "qoder",
364
+ baseUrl: "https://api3.qoder.sh/",
365
+ reasoning: true,
366
+ supportsEffort: true,
367
+ input: ["text", "image"],
368
+ cost: ZERO_COST,
369
+ contextWindow: 1e6,
370
+ maxTokens: 32768
371
+ },
372
+ {
373
+ id: "gm51model",
374
+ name: "GLM 5.1 (Qoder)",
375
+ api: "qoder-api",
376
+ provider: "qoder",
377
+ baseUrl: "https://api3.qoder.sh/",
378
+ reasoning: true,
379
+ supportsEffort: true,
380
+ input: ["text", "image"],
381
+ cost: ZERO_COST,
382
+ contextWindow: 18e4,
383
+ maxTokens: 32768
384
+ },
385
+ {
386
+ id: "kmodel",
387
+ name: "Kimi K2.6 (Qoder)",
388
+ api: "qoder-api",
389
+ provider: "qoder",
390
+ baseUrl: "https://api3.qoder.sh/",
391
+ reasoning: false,
392
+ supportsEffort: false,
393
+ input: ["text", "image"],
394
+ cost: ZERO_COST,
395
+ contextWindow: 256e3,
396
+ maxTokens: 32768
397
+ },
398
+ {
399
+ id: "mmodel",
400
+ name: "MiniMax M3 (Qoder)",
401
+ api: "qoder-api",
402
+ provider: "qoder",
403
+ baseUrl: "https://api3.qoder.sh/",
404
+ reasoning: false,
405
+ supportsEffort: false,
406
+ input: ["text", "image"],
407
+ cost: ZERO_COST,
408
+ contextWindow: 1e6,
409
+ maxTokens: 32768
410
+ }
411
+ ];
412
+ function getCachedModels() {
413
+ if (existsSync2(CACHE_PATH)) {
414
+ try {
415
+ const data = JSON.parse(readFileSync2(CACHE_PATH, "utf8"));
416
+ if (data && Array.isArray(data.models)) {
417
+ return data.models;
418
+ }
419
+ } catch {
420
+ }
421
+ }
422
+ return staticModels;
423
+ }
424
+ function getCachedModelConfig(modelKey) {
425
+ if (existsSync2(CACHE_PATH)) {
426
+ try {
427
+ const data = JSON.parse(readFileSync2(CACHE_PATH, "utf8"));
428
+ if (data && data.configs && data.configs[modelKey]) {
429
+ return data.configs[modelKey];
430
+ }
431
+ } catch {
432
+ }
433
+ }
434
+ return null;
435
+ }
436
+ function isCacheStale() {
437
+ if (!existsSync2(CACHE_PATH)) return true;
438
+ try {
439
+ const data = JSON.parse(readFileSync2(CACHE_PATH, "utf8"));
440
+ if (!data || typeof data.updatedAt !== "number") return true;
441
+ return Date.now() - data.updatedAt > 36e5;
442
+ } catch {
443
+ return true;
444
+ }
445
+ }
446
+ async function updateQoderModelsCache(authToken, userID, name, email) {
447
+ const modelListURL = "https://api3.qoder.sh/algo/api/v2/model/list";
448
+ try {
449
+ const headers = buildAuthHeaders(null, modelListURL, {
450
+ userID,
451
+ authToken,
452
+ name,
453
+ email
454
+ });
455
+ const response = await fetch(modelListURL, {
456
+ method: "GET",
457
+ headers: {
458
+ Accept: "application/json",
459
+ ...headers
460
+ }
461
+ });
462
+ if (!response.ok) {
463
+ return;
464
+ }
465
+ const resData = await response.json();
466
+ const chatModels = resData.chat || [];
467
+ if (chatModels.length === 0) return;
468
+ const newModels = [];
469
+ const configs = {};
470
+ for (const entry of chatModels) {
471
+ const key = entry.key;
472
+ if (!key || !entry.enable) continue;
473
+ const display = entry.display_name || key;
474
+ let ctxLen = entry.max_input_tokens || 18e4;
475
+ if (entry.context_config && typeof entry.context_config === "object") {
476
+ for (const configVal of Object.values(entry.context_config)) {
477
+ if (configVal && typeof configVal === "object" && typeof configVal.token_count === "number") {
478
+ const tc = configVal.token_count;
479
+ if (tc > ctxLen) {
480
+ ctxLen = tc;
481
+ }
482
+ }
483
+ }
484
+ }
485
+ const isVL = !!entry.is_vl;
486
+ const isReasoning = !!entry.is_reasoning || !!entry.thinking_config;
487
+ const supportsEffort = !!entry.thinking_config?.enabled?.efforts;
488
+ configs[key] = entry;
489
+ newModels.push({
490
+ id: key,
491
+ name: display,
492
+ api: "qoder-api",
493
+ provider: "qoder",
494
+ baseUrl: "https://api3.qoder.sh/",
495
+ reasoning: isReasoning,
496
+ supportsEffort,
497
+ input: isVL ? ["text", "image"] : ["text"],
498
+ cost: ZERO_COST,
499
+ contextWindow: ctxLen,
500
+ maxTokens: entry.max_output_tokens || 32768
501
+ });
502
+ }
503
+ if (newModels.length === 0) return;
504
+ if (!newModels.some((m) => m.id === "auto")) {
505
+ newModels.unshift({
506
+ id: "auto",
507
+ name: "Qoder Auto",
508
+ api: "qoder-api",
509
+ provider: "qoder",
510
+ baseUrl: "https://api3.qoder.sh/",
511
+ reasoning: true,
512
+ supportsEffort: false,
513
+ input: ["text", "image"],
514
+ cost: ZERO_COST,
515
+ contextWindow: 18e4,
516
+ maxTokens: 32768
517
+ });
518
+ }
519
+ const cacheData = {
520
+ updatedAt: Date.now(),
521
+ models: newModels,
522
+ configs
523
+ };
524
+ mkdirSync2(dirname2(CACHE_PATH), { recursive: true });
525
+ writeFileSync2(CACHE_PATH, JSON.stringify(cacheData, null, 2), "utf-8");
526
+ } catch {
527
+ }
528
+ }
529
+
530
+ // src/oauth.ts
531
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
532
+ import { homedir as homedir3 } from "node:os";
533
+ import { join as join3 } from "node:path";
534
+
535
+ // src/login.ts
536
+ import crypto2 from "node:crypto";
537
+ function getPrompt(callbacks) {
538
+ return callbacks.onPrompt;
539
+ }
540
+ function getProgress(callbacks) {
541
+ return callbacks.onProgress;
542
+ }
543
+ function getSignal(callbacks) {
544
+ return callbacks.signal;
545
+ }
546
+ function generatePKCE() {
547
+ const codeVerifier = crypto2.randomBytes(32).toString("base64url");
548
+ const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
549
+ return { codeVerifier, codeChallenge };
550
+ }
551
+ function parseExpiresAt(s, expiresInSeconds) {
552
+ if (s) {
553
+ const t = Date.parse(s);
554
+ if (!Number.isNaN(t)) return t;
555
+ const ms = Number.parseInt(s, 10);
556
+ if (!Number.isNaN(ms) && ms > 0) return ms;
557
+ }
558
+ if (expiresInSeconds && expiresInSeconds > 0) {
559
+ return Date.now() + expiresInSeconds * 1e3;
560
+ }
561
+ return Date.now() + 30 * 24 * 60 * 60 * 1e3;
562
+ }
563
+ async function interactiveLogin(callbacks) {
564
+ if (hasExtensionContext()) {
565
+ const choice = await showLoginUI();
566
+ if (!choice) {
567
+ throw new Error("Login cancelled");
568
+ }
569
+ const runAuth = async (mergedCallbacks) => {
570
+ return runDeviceFlow(mergedCallbacks);
571
+ };
572
+ const creds = await showWaitingUI(callbacks, runAuth);
573
+ if (!creds) {
574
+ throw new Error("Login cancelled");
575
+ }
576
+ return creds;
577
+ }
578
+ const prompt = getPrompt(callbacks);
579
+ const proceed = await prompt({
580
+ message: "Press Enter to start browser login for Qoder",
581
+ placeholder: "press enter",
582
+ allowEmpty: true
583
+ });
584
+ if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
585
+ return runDeviceFlow(callbacks);
586
+ }
587
+ function abortableDelay(ms, signal) {
588
+ if (signal?.aborted) return Promise.reject(signal.reason || new Error("Login cancelled"));
589
+ return new Promise((resolve, reject) => {
590
+ const timer = setTimeout(() => {
591
+ signal?.removeEventListener("abort", onAbort);
592
+ resolve();
593
+ }, ms);
594
+ const onAbort = () => {
595
+ clearTimeout(timer);
596
+ reject(signal?.reason || new Error("Login cancelled"));
597
+ };
598
+ signal?.addEventListener("abort", onAbort, { once: true });
599
+ });
600
+ }
601
+ async function runDeviceFlow(callbacks) {
602
+ const { codeVerifier, codeChallenge } = generatePKCE();
603
+ const nonce = crypto2.randomUUID();
604
+ const machineID = getMachineId();
605
+ const verificationURI = `https://qoder.com/device/selectAccounts?challenge=${codeChallenge}&challenge_method=S256&machine_id=${machineID}&nonce=${nonce}`;
606
+ getProgress(callbacks)?.("Please complete login in your browser...");
607
+ callbacks.onAuth({
608
+ url: verificationURI,
609
+ instructions: "Click to sign in with your Qoder account in the browser."
610
+ });
611
+ const pollURL = `https://openapi.qoder.sh/api/v1/deviceToken/poll?nonce=${encodeURIComponent(nonce)}&verifier=${encodeURIComponent(codeVerifier)}&challenge_method=S256`;
612
+ const pollInterval = 2e3;
613
+ const maxAttempts = 90;
614
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
615
+ if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
616
+ await abortableDelay(pollInterval, getSignal(callbacks));
617
+ try {
618
+ const response = await fetch(pollURL, {
619
+ method: "GET",
620
+ headers: {
621
+ Accept: "application/json",
622
+ "User-Agent": "pi-provider-qoder"
623
+ },
624
+ signal: getSignal(callbacks)
625
+ });
626
+ if (response.status === 202 || response.status === 404) {
627
+ continue;
628
+ }
629
+ if (!response.ok) {
630
+ const errText = await response.text();
631
+ throw new Error(`Device token poll failed: ${response.status} ${response.statusText}. Response: ${errText}`);
632
+ }
633
+ const tokenData = await response.json();
634
+ if (!tokenData.token) {
635
+ throw new Error("Device token poll returned empty access token");
636
+ }
637
+ const expireMs = parseExpiresAt(tokenData.expires_at, tokenData.expires_in);
638
+ getProgress(callbacks)?.("Fetching user profile...");
639
+ let email = "";
640
+ let name = "";
641
+ try {
642
+ const userinfoRes = await fetch("https://openapi.qoder.sh/api/v1/userinfo", {
643
+ method: "GET",
644
+ headers: {
645
+ Authorization: `Bearer ${tokenData.token}`,
646
+ Accept: "application/json",
647
+ "User-Agent": "pi-provider-qoder"
648
+ }
649
+ });
650
+ if (userinfoRes.ok) {
651
+ const userinfo = await userinfoRes.json();
652
+ email = userinfo.email || "";
653
+ name = userinfo.name || userinfo.username || "";
654
+ }
655
+ } catch {
656
+ }
657
+ getProgress(callbacks)?.("Login successful!");
658
+ return {
659
+ refresh: `${tokenData.refresh_token}|${tokenData.user_id}|${machineID}`,
660
+ access: tokenData.token,
661
+ expires: expireMs - 5 * 60 * 1e3,
662
+ // 5 min buffer
663
+ userID: tokenData.user_id,
664
+ email,
665
+ name,
666
+ machineID
667
+ };
668
+ } catch (e) {
669
+ if (e.name === "AbortError" || getSignal(callbacks)?.aborted) {
670
+ throw new Error("Login cancelled");
671
+ }
672
+ throw e;
673
+ }
674
+ }
675
+ throw new Error("Authorization timed out");
676
+ }
677
+
678
+ // src/oauth.ts
679
+ var AUTH_FILE = join3(homedir3(), ".pi", "agent", "auth.json");
680
+ function getCachedCredentials(_accessToken) {
681
+ if (existsSync3(AUTH_FILE)) {
682
+ try {
683
+ const auth = JSON.parse(readFileSync3(AUTH_FILE, "utf-8"));
684
+ const creds = auth?.qoder;
685
+ if (creds && creds.userID) {
686
+ return creds;
687
+ }
688
+ } catch {
689
+ }
690
+ }
691
+ return null;
692
+ }
693
+ async function loginQoder(callbacks) {
694
+ const pat = process.env.QODER_PERSONAL_ACCESS_TOKEN || process.env.QODER_PAT;
695
+ if (pat) {
696
+ try {
697
+ const userinfoRes = await fetch("https://openapi.qoder.sh/api/v1/userinfo", {
698
+ headers: {
699
+ Authorization: `Bearer ${pat}`,
700
+ Accept: "application/json",
701
+ "User-Agent": "pi-provider-qoder"
702
+ }
703
+ });
704
+ if (userinfoRes.ok) {
705
+ const userinfo = await userinfoRes.json();
706
+ const email = userinfo.email || "";
707
+ const name = userinfo.name || userinfo.username || "";
708
+ const userID = userinfo.id || "pat";
709
+ const machineID = getMachineId();
710
+ const creds2 = {
711
+ refresh: `pat|${userID}|${machineID}`,
712
+ access: pat,
713
+ expires: Date.now() + 30 * 24 * 60 * 60 * 1e3,
714
+ // 30 days
715
+ userID,
716
+ email,
717
+ name,
718
+ machineID
719
+ };
720
+ updateQoderModelsCache(pat, userID, name, email).catch(() => {
721
+ });
722
+ return creds2;
723
+ }
724
+ } catch {
725
+ }
726
+ }
727
+ const creds = await interactiveLogin(callbacks);
728
+ try {
729
+ const qCreds = creds;
730
+ updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email).catch(() => {
731
+ });
732
+ } catch {
733
+ }
734
+ return creds;
735
+ }
736
+ async function refreshQoderToken(credentials) {
737
+ const parts = credentials.refresh.split("|");
738
+ const refreshToken = parts[0] || "";
739
+ const userID = parts[1] || "";
740
+ const machineID = parts[2] || getMachineId();
741
+ if (refreshToken === "pat") {
742
+ return {
743
+ ...credentials,
744
+ expires: Date.now() + 30 * 24 * 60 * 60 * 1e3
745
+ };
746
+ }
747
+ const refreshURL = "https://center.qoder.sh/algo/api/v3/user/refresh_token";
748
+ try {
749
+ const response = await fetch(refreshURL, {
750
+ method: "POST",
751
+ headers: {
752
+ "Content-Type": "application/json",
753
+ Authorization: `Bearer ${credentials.access}`,
754
+ Accept: "application/json",
755
+ "User-Agent": "pi-provider-qoder"
756
+ },
757
+ body: JSON.stringify({ refreshToken })
758
+ });
759
+ if (response.ok) {
760
+ const data = await response.json();
761
+ const newAccess = data.token;
762
+ const newRefresh = data.refresh_token || refreshToken;
763
+ let expireMs = Date.now() + 30 * 24 * 60 * 60 * 1e3;
764
+ if (data.expires_at) {
765
+ const parsed = Date.parse(data.expires_at);
766
+ if (!Number.isNaN(parsed)) expireMs = parsed;
767
+ } else if (data.expires_in) {
768
+ expireMs = Date.now() + data.expires_in * 1e3;
769
+ }
770
+ const refreshed = {
771
+ ...credentials,
772
+ refresh: `${newRefresh}|${userID}|${machineID}`,
773
+ access: newAccess,
774
+ expires: expireMs - 5 * 60 * 1e3,
775
+ userID,
776
+ email: credentials.email || "",
777
+ name: credentials.name || "",
778
+ machineID
779
+ };
780
+ updateQoderModelsCache(
781
+ newAccess,
782
+ userID,
783
+ credentials.name || "",
784
+ credentials.email || ""
785
+ ).catch(() => {
786
+ });
787
+ return refreshed;
788
+ }
789
+ } catch {
790
+ }
791
+ const refreshedFallback = {
792
+ ...credentials,
793
+ expires: Date.now() + 60 * 60 * 1e3
794
+ // extend for 1 hour
795
+ };
796
+ return refreshedFallback;
797
+ }
798
+
799
+ // src/stream.ts
800
+ import crypto3 from "node:crypto";
801
+ import * as PiAi from "@earendil-works/pi-ai";
802
+
803
+ // src/qoder-encoding.ts
804
+ var qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
805
+ var qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
806
+ function qoderEncodeBody(plaintext) {
807
+ const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
808
+ const n = std.length;
809
+ const a = Math.floor(n / 3);
810
+ const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
811
+ let out = "";
812
+ for (let i = 0; i < n; i++) {
813
+ const c = rearranged[i];
814
+ if (c === "=") {
815
+ out += "$";
816
+ } else {
817
+ const idx = qoderStdAlphabet.indexOf(c);
818
+ if (idx >= 0) {
819
+ out += qoderCustomAlphabet[idx];
820
+ } else {
821
+ out += c;
822
+ }
823
+ }
824
+ }
825
+ return out;
826
+ }
827
+
828
+ // src/thinking-parser.ts
829
+ var THINKING_END_TAG = "</thinking>";
830
+ var THINKING_TAG_VARIANTS = [
831
+ { open: "<thinking>", close: "</thinking>" },
832
+ { open: "<think>", close: "</think>" },
833
+ { open: "<reasoning>", close: "</reasoning>" },
834
+ { open: "<thought>", close: "</thought>" }
835
+ ];
836
+ function getTrailingPossibleTagPrefixLength(text, tag) {
837
+ const maxPrefixLength = Math.min(text.length, tag.length - 1);
838
+ for (let len = maxPrefixLength; len > 0; len--) {
839
+ if (text.endsWith(tag.slice(0, len))) return len;
840
+ }
841
+ return 0;
842
+ }
843
+ function getMaxTrailingPossibleTagPrefixLength(text, tags) {
844
+ let maxLength = 0;
845
+ for (const tag of tags) {
846
+ maxLength = Math.max(maxLength, getTrailingPossibleTagPrefixLength(text, tag));
847
+ }
848
+ return maxLength;
849
+ }
850
+ var ThinkingTagParser = class {
851
+ constructor(output, stream) {
852
+ this.output = output;
853
+ this.stream = stream;
854
+ }
855
+ textBuffer = "";
856
+ inThinking = false;
857
+ thinkingExtracted = false;
858
+ thinkingBlockIndex = null;
859
+ textBlockIndex = null;
860
+ lastTextBlockIndex = null;
861
+ activeEndTag = THINKING_END_TAG;
862
+ processChunk(chunk) {
863
+ this.textBuffer += chunk;
864
+ while (this.textBuffer.length > 0) {
865
+ const prevLength = this.textBuffer.length;
866
+ if (!this.inThinking && !this.thinkingExtracted) {
867
+ this.processBeforeThinking();
868
+ if (this.textBuffer.length === 0) break;
869
+ }
870
+ if (this.inThinking) {
871
+ this.processInsideThinking();
872
+ if (this.textBuffer.length === 0) break;
873
+ }
874
+ if (this.thinkingExtracted) {
875
+ this.processAfterThinking();
876
+ break;
877
+ }
878
+ if (this.textBuffer.length >= prevLength) break;
879
+ }
880
+ }
881
+ finalize() {
882
+ if (this.textBuffer.length === 0) return;
883
+ if (this.inThinking && this.thinkingBlockIndex !== null) {
884
+ const block = this.output.content[this.thinkingBlockIndex];
885
+ block.thinking += this.textBuffer;
886
+ this.stream.push({
887
+ type: "thinking_delta",
888
+ contentIndex: this.thinkingBlockIndex,
889
+ delta: this.textBuffer,
890
+ partial: this.output
891
+ });
892
+ this.stream.push({
893
+ type: "thinking_end",
894
+ contentIndex: this.thinkingBlockIndex,
895
+ content: block.thinking,
896
+ partial: this.output
897
+ });
898
+ } else {
899
+ this.emitText(this.textBuffer);
900
+ }
901
+ this.textBuffer = "";
902
+ }
903
+ getTextBlockIndex() {
904
+ return this.textBlockIndex ?? this.lastTextBlockIndex;
905
+ }
906
+ processBeforeThinking() {
907
+ let bestPos = -1;
908
+ let bestVariant = null;
909
+ for (const variant of THINKING_TAG_VARIANTS) {
910
+ const pos = this.textBuffer.indexOf(variant.open);
911
+ if (pos !== -1 && (bestPos === -1 || pos < bestPos)) {
912
+ bestPos = pos;
913
+ bestVariant = variant;
914
+ }
915
+ }
916
+ if (bestPos !== -1 && bestVariant) {
917
+ if (bestPos > 0) this.emitText(this.textBuffer.slice(0, bestPos));
918
+ this.textBuffer = this.textBuffer.slice(bestPos + bestVariant.open.length);
919
+ this.activeEndTag = bestVariant.close;
920
+ this.inThinking = true;
921
+ return;
922
+ }
923
+ const trailingPrefixLength = getMaxTrailingPossibleTagPrefixLength(
924
+ this.textBuffer,
925
+ THINKING_TAG_VARIANTS.map((variant) => variant.open)
926
+ );
927
+ const safeLen = this.textBuffer.length - trailingPrefixLength;
928
+ if (safeLen > 0) {
929
+ this.emitText(this.textBuffer.slice(0, safeLen));
930
+ this.textBuffer = this.textBuffer.slice(safeLen);
931
+ }
932
+ }
933
+ processInsideThinking() {
934
+ const endPos = this.textBuffer.indexOf(this.activeEndTag);
935
+ if (endPos !== -1) {
936
+ if (endPos > 0) this.emitThinking(this.textBuffer.slice(0, endPos));
937
+ if (this.thinkingBlockIndex !== null) {
938
+ const block = this.output.content[this.thinkingBlockIndex];
939
+ this.stream.push({
940
+ type: "thinking_end",
941
+ contentIndex: this.thinkingBlockIndex,
942
+ content: block.thinking,
943
+ partial: this.output
944
+ });
945
+ }
946
+ this.textBuffer = this.textBuffer.slice(endPos + this.activeEndTag.length);
947
+ this.inThinking = false;
948
+ this.thinkingExtracted = true;
949
+ this.lastTextBlockIndex = this.textBlockIndex;
950
+ this.textBlockIndex = null;
951
+ if (this.textBuffer.startsWith("\n\n")) this.textBuffer = this.textBuffer.slice(2);
952
+ return;
953
+ }
954
+ const trailingPrefixLength = getTrailingPossibleTagPrefixLength(this.textBuffer, this.activeEndTag);
955
+ const safeLen = this.textBuffer.length - trailingPrefixLength;
956
+ if (safeLen > 0) {
957
+ this.emitThinking(this.textBuffer.slice(0, safeLen));
958
+ this.textBuffer = this.textBuffer.slice(safeLen);
959
+ }
960
+ }
961
+ processAfterThinking() {
962
+ this.emitText(this.textBuffer);
963
+ this.textBuffer = "";
964
+ }
965
+ emitText(text) {
966
+ if (!text) return;
967
+ if (this.textBlockIndex === null) {
968
+ this.textBlockIndex = this.output.content.length;
969
+ this.output.content.push({ type: "text", text: "" });
970
+ this.stream.push({ type: "text_start", contentIndex: this.textBlockIndex, partial: this.output });
971
+ }
972
+ const block = this.output.content[this.textBlockIndex];
973
+ block.text += text;
974
+ this.stream.push({ type: "text_delta", contentIndex: this.textBlockIndex, delta: text, partial: this.output });
975
+ }
976
+ emitThinking(thinking) {
977
+ if (!thinking) return;
978
+ if (this.thinkingBlockIndex === null) {
979
+ if (this.textBlockIndex !== null) {
980
+ this.thinkingBlockIndex = this.textBlockIndex;
981
+ this.output.content.splice(this.thinkingBlockIndex, 0, { type: "thinking", thinking: "" });
982
+ this.textBlockIndex = this.textBlockIndex + 1;
983
+ } else {
984
+ this.thinkingBlockIndex = this.output.content.length;
985
+ this.output.content.push({ type: "thinking", thinking: "" });
986
+ }
987
+ this.stream.push({ type: "thinking_start", contentIndex: this.thinkingBlockIndex, partial: this.output });
988
+ }
989
+ const block = this.output.content[this.thinkingBlockIndex];
990
+ block.thinking += thinking;
991
+ this.stream.push({
992
+ type: "thinking_delta",
993
+ contentIndex: this.thinkingBlockIndex,
994
+ delta: thinking,
995
+ partial: this.output
996
+ });
997
+ }
998
+ };
999
+
1000
+ // src/transform.ts
1001
+ function getContentText(msg) {
1002
+ if (typeof msg.content === "string") return msg.content;
1003
+ if (Array.isArray(msg.content)) {
1004
+ return msg.content.map((c) => {
1005
+ if (c.type === "text") return c.text;
1006
+ if (c.type === "thinking") return c.thinking;
1007
+ return "";
1008
+ }).join("");
1009
+ }
1010
+ return "";
1011
+ }
1012
+ function transformTools(tools) {
1013
+ return tools.map((t) => ({
1014
+ type: "function",
1015
+ function: {
1016
+ name: t.name,
1017
+ description: t.description,
1018
+ parameters: t.parameters
1019
+ }
1020
+ }));
1021
+ }
1022
+ function transformMessagesForQoder(messages) {
1023
+ const normalizedMessages = [];
1024
+ for (const msg of messages) {
1025
+ if (msg.role === "assistant" && (msg.stopReason === "error" || msg.stopReason === "aborted")) {
1026
+ continue;
1027
+ }
1028
+ if (msg.role === "user") {
1029
+ let content = "";
1030
+ if (typeof msg.content === "string") {
1031
+ content = msg.content;
1032
+ } else if (Array.isArray(msg.content)) {
1033
+ const hasImage = msg.content.some((c) => c.type === "image");
1034
+ if (hasImage) {
1035
+ content = msg.content.map((c) => {
1036
+ if (c.type === "text") {
1037
+ return { type: "text", text: c.text };
1038
+ }
1039
+ if (c.type === "image") {
1040
+ const img = c;
1041
+ return {
1042
+ type: "image_url",
1043
+ image_url: {
1044
+ url: `data:${img.mimeType};base64,${img.data}`
1045
+ }
1046
+ };
1047
+ }
1048
+ return null;
1049
+ }).filter(Boolean);
1050
+ } else {
1051
+ content = getContentText(msg);
1052
+ }
1053
+ }
1054
+ normalizedMessages.push({
1055
+ role: "user",
1056
+ content
1057
+ });
1058
+ } else if (msg.role === "assistant") {
1059
+ const am = msg;
1060
+ let content = "";
1061
+ const toolCalls = [];
1062
+ if (Array.isArray(am.content)) {
1063
+ for (const block of am.content) {
1064
+ if (block.type === "text") {
1065
+ content += block.text;
1066
+ } else if (block.type === "thinking") {
1067
+ content += `<thinking>${block.thinking}</thinking>
1068
+
1069
+ `;
1070
+ } else if (block.type === "toolCall") {
1071
+ const tc = block;
1072
+ toolCalls.push({
1073
+ id: tc.id,
1074
+ type: "function",
1075
+ function: {
1076
+ name: tc.name,
1077
+ arguments: typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments)
1078
+ }
1079
+ });
1080
+ }
1081
+ }
1082
+ } else {
1083
+ content = am.content || "";
1084
+ }
1085
+ const mapped = {
1086
+ role: "assistant",
1087
+ content: content || null
1088
+ };
1089
+ if (toolCalls.length > 0) {
1090
+ mapped.tool_calls = toolCalls;
1091
+ }
1092
+ normalizedMessages.push(mapped);
1093
+ } else if (msg.role === "toolResult") {
1094
+ const tr = msg;
1095
+ normalizedMessages.push({
1096
+ role: "tool",
1097
+ tool_call_id: tr.toolCallId,
1098
+ content: getContentText(tr)
1099
+ });
1100
+ }
1101
+ }
1102
+ return normalizedMessages;
1103
+ }
1104
+
1105
+ // src/stream.ts
1106
+ function stableHash(prefix, ...inputs) {
1107
+ const hash = crypto3.createHash("sha256");
1108
+ hash.update(prefix);
1109
+ for (const input of inputs) {
1110
+ hash.update("\0");
1111
+ hash.update(input);
1112
+ }
1113
+ return hash.digest("hex").slice(0, 16);
1114
+ }
1115
+ function stableChatRecordID(model, messages, tools, maxTokens) {
1116
+ const hash = crypto3.createHash("sha256");
1117
+ hash.update("qoder-record");
1118
+ hash.update("\0");
1119
+ hash.update(model);
1120
+ for (const msg of messages) {
1121
+ if (msg && msg.role) {
1122
+ hash.update("\0");
1123
+ hash.update(msg.role);
1124
+ }
1125
+ if (msg && msg.content) {
1126
+ hash.update("\0");
1127
+ hash.update(typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content));
1128
+ }
1129
+ }
1130
+ if (tools) {
1131
+ hash.update("\0");
1132
+ hash.update(JSON.stringify(tools));
1133
+ }
1134
+ hash.update("\0");
1135
+ hash.update(`mt=${maxTokens}`);
1136
+ return hash.digest("hex").slice(0, 16);
1137
+ }
1138
+ function streamQoder(model, context, options) {
1139
+ const StreamCtor = PiAi.AssistantMessageEventStream;
1140
+ const stream = new StreamCtor();
1141
+ const output = {
1142
+ role: "assistant",
1143
+ content: [],
1144
+ api: model.api,
1145
+ provider: model.provider,
1146
+ model: model.id,
1147
+ usage: {
1148
+ input: 0,
1149
+ output: 0,
1150
+ cacheRead: 0,
1151
+ cacheWrite: 0,
1152
+ totalTokens: 0,
1153
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
1154
+ },
1155
+ stopReason: "stop",
1156
+ timestamp: Date.now()
1157
+ };
1158
+ (async () => {
1159
+ try {
1160
+ const accessToken = options?.apiKey;
1161
+ if (!accessToken) {
1162
+ throw new Error("Qoder credentials not set. Run /login qoder or set QODER_PERSONAL_ACCESS_TOKEN.");
1163
+ }
1164
+ const cachedCreds = getCachedCredentials(accessToken);
1165
+ const userID = cachedCreds?.userID || "qoder-user";
1166
+ const name = cachedCreds?.name || "Qoder User";
1167
+ const email = cachedCreds?.email || "user@qoder.com";
1168
+ const machineID = cachedCreds?.machineID || getMachineId();
1169
+ if (isCacheStale()) {
1170
+ updateQoderModelsCache(accessToken, userID, name, email).catch(() => {
1171
+ });
1172
+ }
1173
+ const qoderModel = model.id;
1174
+ const modelConfig = getCachedModelConfig(qoderModel) || {
1175
+ key: qoderModel,
1176
+ is_reasoning: qoderModel === "ultimate" || qoderModel === "performance" || qoderModel.includes("dmodel") || qoderModel.includes("dfmodel"),
1177
+ max_output_tokens: 32768,
1178
+ source: "system"
1179
+ };
1180
+ modelConfig.key = qoderModel;
1181
+ const isReasoning = !!modelConfig.is_reasoning;
1182
+ const maxOutputTokens = modelConfig.max_output_tokens || 32768;
1183
+ const normalizedMessages = transformMessagesForQoder(context.messages);
1184
+ const systemText = context.systemPrompt || "";
1185
+ let lastUserText = "";
1186
+ for (let i = normalizedMessages.length - 1; i >= 0; i--) {
1187
+ if (normalizedMessages[i].role === "user") {
1188
+ const content = normalizedMessages[i].content;
1189
+ lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => c.text || "").join("") : "";
1190
+ break;
1191
+ }
1192
+ }
1193
+ const sessionID = stableHash("qoder-session", userID, qoderModel);
1194
+ let maxTokens = 32768;
1195
+ if (maxOutputTokens > 0) {
1196
+ maxTokens = maxOutputTokens;
1197
+ }
1198
+ if (options?.maxTokens && options.maxTokens < maxTokens) {
1199
+ maxTokens = options.maxTokens;
1200
+ }
1201
+ const toolsRaw = context.tools && context.tools.length > 0 ? transformTools(context.tools) : void 0;
1202
+ const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
1203
+ const reqBody = {
1204
+ request_id: crypto3.randomUUID(),
1205
+ request_set_id: recordID,
1206
+ chat_record_id: recordID,
1207
+ session_id: sessionID,
1208
+ stream: true,
1209
+ chat_task: "FREE_INPUT",
1210
+ is_reply: true,
1211
+ is_retry: false,
1212
+ source: 1,
1213
+ version: "3",
1214
+ session_type: "qodercli",
1215
+ agent_id: "agent_common",
1216
+ task_id: "common",
1217
+ code_language: "",
1218
+ chat_prompt: "",
1219
+ image_urls: null,
1220
+ aliyun_user_type: "",
1221
+ system: systemText,
1222
+ messages: normalizedMessages,
1223
+ tools: toolsRaw || [],
1224
+ parameters: { max_tokens: maxTokens },
1225
+ chat_context: {
1226
+ chatPrompt: "",
1227
+ imageUrls: null,
1228
+ extra: {
1229
+ context: [],
1230
+ modelConfig: {
1231
+ key: qoderModel,
1232
+ is_reasoning: isReasoning
1233
+ },
1234
+ originalContent: lastUserText
1235
+ },
1236
+ features: [],
1237
+ text: lastUserText
1238
+ },
1239
+ model_config: modelConfig,
1240
+ business: {
1241
+ product: "cli",
1242
+ version: "1.0.0",
1243
+ type: "agent",
1244
+ stage: "start",
1245
+ id: crypto3.randomUUID(),
1246
+ name: lastUserText.substring(0, 30),
1247
+ begin_at: Date.now()
1248
+ }
1249
+ };
1250
+ const bodyBytes = Buffer.from(JSON.stringify(reqBody));
1251
+ const encodedBody = qoderEncodeBody(bodyBytes);
1252
+ const encodedBytes = Buffer.from(encodedBody, "utf8");
1253
+ const chatURL = "https://api3.qoder.sh/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1";
1254
+ const headers = buildAuthHeaders(encodedBytes, chatURL, {
1255
+ userID,
1256
+ authToken: accessToken,
1257
+ name,
1258
+ email,
1259
+ machineID
1260
+ });
1261
+ const modelSource = modelConfig.source || "system";
1262
+ const response = await fetch(chatURL, {
1263
+ method: "POST",
1264
+ headers: {
1265
+ "Content-Type": "application/json",
1266
+ Accept: "text/event-stream",
1267
+ "Cache-Control": "no-cache",
1268
+ "Accept-Encoding": "identity",
1269
+ "X-Model-Key": qoderModel,
1270
+ "X-Model-Source": modelSource,
1271
+ ...headers
1272
+ },
1273
+ body: encodedBytes,
1274
+ signal: options?.signal
1275
+ });
1276
+ if (!response.ok) {
1277
+ const errText = await response.text();
1278
+ throw new Error(`Qoder API request failed: ${response.status} ${response.statusText}. Response: ${errText}`);
1279
+ }
1280
+ const reader = response.body?.getReader();
1281
+ if (!reader) throw new Error("No response body");
1282
+ const decoder = new TextDecoder();
1283
+ let buffer = "";
1284
+ let contentBlockIndex = -1;
1285
+ let thinkingBlockIndex = -1;
1286
+ const toolCallsState = [];
1287
+ const thinkingEnabled = options?.reasoning !== false && options?.reasoning !== "off";
1288
+ const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
1289
+ stream.push({ type: "start", partial: output });
1290
+ while (true) {
1291
+ const { done, value } = await reader.read();
1292
+ if (done) break;
1293
+ buffer += decoder.decode(value, { stream: true });
1294
+ while (true) {
1295
+ const lineEnd = buffer.indexOf("\n");
1296
+ if (lineEnd === -1) break;
1297
+ const line = buffer.substring(0, lineEnd).trim();
1298
+ buffer = buffer.substring(lineEnd + 1);
1299
+ if (!line.startsWith("data:")) continue;
1300
+ const dataStr = line.substring(5).trim();
1301
+ if (dataStr === "[DONE]") {
1302
+ break;
1303
+ }
1304
+ try {
1305
+ const envelope = JSON.parse(dataStr);
1306
+ if (envelope.statusCodeValue && envelope.statusCodeValue !== 200) {
1307
+ throw new Error(`Upstream status ${envelope.statusCodeValue}: ${envelope.body}`);
1308
+ }
1309
+ const innerStr = envelope.body;
1310
+ if (!innerStr || innerStr === "[DONE]") continue;
1311
+ const inner = JSON.parse(innerStr);
1312
+ if (inner.choices && inner.choices.length > 0) {
1313
+ const choice = inner.choices[0];
1314
+ const delta = choice.delta;
1315
+ if (delta) {
1316
+ if (delta.reasoning_content) {
1317
+ if (thinkingBlockIndex === -1) {
1318
+ thinkingBlockIndex = output.content.length;
1319
+ output.content.push({ type: "thinking", thinking: "" });
1320
+ stream.push({ type: "thinking_start", contentIndex: thinkingBlockIndex, partial: output });
1321
+ }
1322
+ const block = output.content[thinkingBlockIndex];
1323
+ block.thinking += delta.reasoning_content;
1324
+ stream.push({
1325
+ type: "thinking_delta",
1326
+ contentIndex: thinkingBlockIndex,
1327
+ delta: delta.reasoning_content,
1328
+ partial: output
1329
+ });
1330
+ }
1331
+ if (delta.content) {
1332
+ if (thinkingBlockIndex !== -1) {
1333
+ const block = output.content[thinkingBlockIndex];
1334
+ stream.push({
1335
+ type: "thinking_end",
1336
+ contentIndex: thinkingBlockIndex,
1337
+ content: block.thinking,
1338
+ partial: output
1339
+ });
1340
+ thinkingBlockIndex = -1;
1341
+ }
1342
+ if (thinkingParser) {
1343
+ thinkingParser.processChunk(delta.content);
1344
+ } else {
1345
+ if (contentBlockIndex === -1) {
1346
+ contentBlockIndex = output.content.length;
1347
+ output.content.push({ type: "text", text: "" });
1348
+ stream.push({ type: "text_start", contentIndex: contentBlockIndex, partial: output });
1349
+ }
1350
+ const block = output.content[contentBlockIndex];
1351
+ block.text += delta.content;
1352
+ stream.push({
1353
+ type: "text_delta",
1354
+ contentIndex: contentBlockIndex,
1355
+ delta: delta.content,
1356
+ partial: output
1357
+ });
1358
+ }
1359
+ }
1360
+ if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
1361
+ for (const tc of delta.tool_calls) {
1362
+ const idx = tc.index ?? 0;
1363
+ if (!toolCallsState[idx]) {
1364
+ toolCallsState[idx] = { arguments: "" };
1365
+ }
1366
+ const state = toolCallsState[idx];
1367
+ if (tc.id) state.id = tc.id;
1368
+ if (tc.function?.name) state.name = tc.function.name;
1369
+ if (tc.function?.arguments) {
1370
+ const argDelta = tc.function.arguments;
1371
+ state.arguments += argDelta;
1372
+ if (state.emittedStart === void 0) {
1373
+ state.emittedStart = true;
1374
+ state.contentIndex = output.content.length;
1375
+ const block = { type: "toolCall", id: state.id, name: state.name, arguments: {} };
1376
+ output.content.push(block);
1377
+ stream.push({ type: "toolcall_start", contentIndex: state.contentIndex, partial: output });
1378
+ }
1379
+ stream.push({
1380
+ type: "toolcall_delta",
1381
+ contentIndex: state.contentIndex,
1382
+ delta: argDelta,
1383
+ partial: output
1384
+ });
1385
+ }
1386
+ }
1387
+ }
1388
+ }
1389
+ if (choice.finish_reason) {
1390
+ output.stopReason = choice.finish_reason;
1391
+ }
1392
+ }
1393
+ } catch {
1394
+ }
1395
+ }
1396
+ }
1397
+ if (thinkingParser) {
1398
+ thinkingParser.finalize();
1399
+ }
1400
+ if (thinkingBlockIndex !== -1) {
1401
+ const block = output.content[thinkingBlockIndex];
1402
+ stream.push({
1403
+ type: "thinking_end",
1404
+ contentIndex: thinkingBlockIndex,
1405
+ content: block.thinking,
1406
+ partial: output
1407
+ });
1408
+ }
1409
+ for (const state of toolCallsState) {
1410
+ if (state && state.emittedStart && !state.emittedEnd) {
1411
+ state.emittedEnd = true;
1412
+ let args = {};
1413
+ try {
1414
+ args = JSON.parse(state.arguments || "{}");
1415
+ } catch {
1416
+ }
1417
+ const block = output.content[state.contentIndex];
1418
+ block.arguments = args;
1419
+ stream.push({
1420
+ type: "toolcall_end",
1421
+ contentIndex: state.contentIndex,
1422
+ toolCall: {
1423
+ type: "toolCall",
1424
+ id: state.id,
1425
+ name: state.name,
1426
+ arguments: args
1427
+ },
1428
+ partial: output
1429
+ });
1430
+ }
1431
+ }
1432
+ if (toolCallsState.length > 0) {
1433
+ output.stopReason = "toolUse";
1434
+ } else {
1435
+ output.stopReason = "stop";
1436
+ }
1437
+ stream.push({ type: "done", reason: output.stopReason, message: output });
1438
+ stream.end();
1439
+ } catch (e) {
1440
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
1441
+ output.errorMessage = e instanceof Error ? e.message : String(e);
1442
+ stream.push({ type: "error", reason: output.stopReason, error: output });
1443
+ try {
1444
+ stream.end();
1445
+ } catch {
1446
+ }
1447
+ }
1448
+ })();
1449
+ return stream;
1450
+ }
1451
+
1452
+ // src/usage.ts
1453
+ async function fetchQoderUsage(credentials) {
1454
+ const usageURL = "https://openapi.qoder.sh/api/v2/quota/usage";
1455
+ const response = await fetch(usageURL, {
1456
+ method: "GET",
1457
+ headers: {
1458
+ Authorization: `Bearer ${credentials.access}`,
1459
+ Accept: "application/json",
1460
+ "User-Agent": "pi-provider-qoder"
1461
+ }
1462
+ });
1463
+ if (!response.ok) {
1464
+ throw new Error(`Failed to fetch Qoder usage: ${response.status} ${response.statusText}`);
1465
+ }
1466
+ const raw = await response.json();
1467
+ const usageBuckets = [];
1468
+ if (raw.userQuota) {
1469
+ usageBuckets.push({
1470
+ id: "user-quota",
1471
+ label: "User Quota",
1472
+ usedDisplay: raw.userQuota.used.toFixed(2),
1473
+ limitDisplay: raw.userQuota.total.toFixed(2),
1474
+ unit: raw.userQuota.unit,
1475
+ resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0
1476
+ });
1477
+ }
1478
+ if (raw.orgResourcePackage && raw.orgResourcePackage.total > 0) {
1479
+ usageBuckets.push({
1480
+ id: "org-resource-package",
1481
+ label: "Org Resource Package",
1482
+ usedDisplay: raw.orgResourcePackage.used.toFixed(2),
1483
+ limitDisplay: raw.orgResourcePackage.total.toFixed(2),
1484
+ unit: raw.orgResourcePackage.unit,
1485
+ resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0
1486
+ });
1487
+ }
1488
+ const remainingText = raw.userQuota ? `${raw.userQuota.remaining.toFixed(2)} ${raw.userQuota.unit} remaining` : "";
1489
+ return {
1490
+ summary: remainingText,
1491
+ subscriptionTitle: "Qoder AI Plan",
1492
+ resetAt: raw.expiresAt ? new Date(raw.expiresAt).toISOString() : void 0,
1493
+ manageUrl: "https://qoder.com",
1494
+ usageBuckets,
1495
+ raw
1496
+ };
1497
+ }
1498
+
1499
+ // src/index.ts
1500
+ function index_default(pi) {
1501
+ pi.on("session_start", async (_event, ctx) => {
1502
+ setExtensionContext(ctx);
1503
+ });
1504
+ pi.registerProvider("qoder", {
1505
+ baseUrl: "https://api3.qoder.sh/",
1506
+ api: "qoder-api",
1507
+ models: getCachedModels(),
1508
+ oauth: {
1509
+ name: "Qoder (Browser OAuth / PAT)",
1510
+ login: loginQoder,
1511
+ refreshToken: refreshQoderToken,
1512
+ getApiKey: (cred) => cred.access,
1513
+ modifyModels: (models, cred) => {
1514
+ const cached = getCachedModels();
1515
+ const nonQoder = models.filter((m) => m.provider !== "qoder");
1516
+ const modelsToUse = cached.length > 0 ? cached : staticModels;
1517
+ const modifiedQoder = modelsToUse.map((m) => ({
1518
+ ...m,
1519
+ baseUrl: "https://api3.qoder.sh/"
1520
+ }));
1521
+ return [...nonQoder, ...modifiedQoder];
1522
+ },
1523
+ fetchUsage: fetchQoderUsage
1524
+ },
1525
+ streamSimple: streamQoder
1526
+ });
1527
+ }
1528
+ export {
1529
+ index_default as default
1530
+ };