pi-provider-qoder 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -5
- package/dist/index.js +178 -211
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -30,11 +30,25 @@ Then log in from pi:
|
|
|
30
30
|
/login qoder
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
The login
|
|
33
|
+
The login menu offers two methods:
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
- **Browser Login** — OAuth device-code flow; complete authorization in your browser.
|
|
36
|
+
- **Use API Key (PAT)** — paste a Qoder Personal Access Token (`pt-...`).
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
### Personal Access Token (PAT)
|
|
39
|
+
|
|
40
|
+
A Qoder PAT (`pt-...`) cannot authenticate API calls directly — the provider
|
|
41
|
+
exchanges it for a short-lived job token (mirroring the official `qodercli`
|
|
42
|
+
flow) and resolves your account identity automatically. You can supply a PAT in
|
|
43
|
+
two ways:
|
|
44
|
+
|
|
45
|
+
- Run `/login qoder` and choose **Use API Key (PAT)**, then paste the token.
|
|
46
|
+
- Set the `QODER_PERSONAL_ACCESS_TOKEN` (or `QODER_PAT`) environment variable,
|
|
47
|
+
then run `/login qoder` — the PAT is picked up automatically and exchanged
|
|
48
|
+
without further prompts. This is the recommended path for headless/CI setups.
|
|
49
|
+
|
|
50
|
+
> The exchanged job token is short-lived; the provider transparently re-exchanges
|
|
51
|
+
> the stored PAT when it expires.
|
|
38
52
|
|
|
39
53
|
## Models
|
|
40
54
|
|
|
@@ -70,8 +84,8 @@ Or let Qoder select automatically:
|
|
|
70
84
|
src/
|
|
71
85
|
├── index.ts # Extension registration
|
|
72
86
|
├── cosy.ts # COSY Signature and Machine ID resolver
|
|
73
|
-
├── login.ts # OAuth Device Flow login sequence
|
|
74
|
-
├──
|
|
87
|
+
├── login.ts # OAuth Device Flow + PAT login sequence
|
|
88
|
+
├── pat.ts # PAT → job-token exchange + identity resolution
|
|
75
89
|
├── models.ts # Model definitions and Dynamic Config Cache
|
|
76
90
|
├── oauth.ts # PAT / OAuth callback orchestrator
|
|
77
91
|
├── stream.ts # Main streaming response handler
|
package/dist/index.js
CHANGED
|
@@ -1,128 +1,3 @@
|
|
|
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
1
|
// src/models.ts
|
|
127
2
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
128
3
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -425,7 +300,7 @@ function getCachedModelConfig(modelKey) {
|
|
|
425
300
|
if (existsSync2(CACHE_PATH)) {
|
|
426
301
|
try {
|
|
427
302
|
const data = JSON.parse(readFileSync2(CACHE_PATH, "utf8"));
|
|
428
|
-
if (data
|
|
303
|
+
if (data?.configs?.[modelKey]) {
|
|
429
304
|
return data.configs[modelKey];
|
|
430
305
|
}
|
|
431
306
|
} catch {
|
|
@@ -534,6 +409,101 @@ import { join as join3 } from "node:path";
|
|
|
534
409
|
|
|
535
410
|
// src/login.ts
|
|
536
411
|
import crypto2 from "node:crypto";
|
|
412
|
+
|
|
413
|
+
// src/pat.ts
|
|
414
|
+
var UA = "pi-provider-qoder";
|
|
415
|
+
var EXCHANGE_URL = "https://openapi.qoder.sh/api/v1/jobToken/exchange";
|
|
416
|
+
var USERINFO_URL = "https://openapi.qoder.sh/api/v1/userinfo";
|
|
417
|
+
var PAT_REFRESH_PREFIX = "pat";
|
|
418
|
+
function isPatRefresh(refresh) {
|
|
419
|
+
return refresh.startsWith(`${PAT_REFRESH_PREFIX}|`);
|
|
420
|
+
}
|
|
421
|
+
function encodePatRefresh(pat, jobRefreshToken, userID, machineID) {
|
|
422
|
+
return [PAT_REFRESH_PREFIX, pat, jobRefreshToken, userID, machineID].join("|");
|
|
423
|
+
}
|
|
424
|
+
function decodePatRefresh(refresh) {
|
|
425
|
+
const parts = refresh.split("|");
|
|
426
|
+
return {
|
|
427
|
+
pat: parts[1] || "",
|
|
428
|
+
jobRefreshToken: parts[2] || "",
|
|
429
|
+
userID: parts[3] || "",
|
|
430
|
+
machineID: parts[4] || ""
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
async function exchangeJobToken(pat) {
|
|
434
|
+
const res = await fetch(EXCHANGE_URL, {
|
|
435
|
+
method: "POST",
|
|
436
|
+
headers: {
|
|
437
|
+
"Content-Type": "application/json",
|
|
438
|
+
Accept: "application/json",
|
|
439
|
+
"User-Agent": UA,
|
|
440
|
+
"Cosy-Version": "1.0.1",
|
|
441
|
+
"Cosy-ClientType": "5"
|
|
442
|
+
},
|
|
443
|
+
body: JSON.stringify({ personal_token: pat })
|
|
444
|
+
});
|
|
445
|
+
if (!res.ok) {
|
|
446
|
+
const text = await res.text().catch(() => "");
|
|
447
|
+
throw new Error(`Qoder PAT exchange failed: ${res.status} ${res.statusText}. ${text.slice(0, 200)}`);
|
|
448
|
+
}
|
|
449
|
+
const data = await res.json();
|
|
450
|
+
if (!data.token) {
|
|
451
|
+
throw new Error("Qoder PAT exchange returned no job token");
|
|
452
|
+
}
|
|
453
|
+
let expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
|
|
454
|
+
if (data.expires_at) {
|
|
455
|
+
const parsed = Date.parse(data.expires_at);
|
|
456
|
+
if (!Number.isNaN(parsed)) expiresAt = parsed;
|
|
457
|
+
} else if (data.expires_in) {
|
|
458
|
+
expiresAt = Date.now() + data.expires_in;
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
jobToken: data.token,
|
|
462
|
+
jobRefreshToken: data.refresh_token || "",
|
|
463
|
+
expiresAt
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
async function fetchUserInfo(jobToken) {
|
|
467
|
+
let userID = "";
|
|
468
|
+
let email = "";
|
|
469
|
+
let name = "";
|
|
470
|
+
try {
|
|
471
|
+
const res = await fetch(USERINFO_URL, {
|
|
472
|
+
headers: {
|
|
473
|
+
Authorization: `Bearer ${jobToken}`,
|
|
474
|
+
Accept: "application/json",
|
|
475
|
+
"User-Agent": UA,
|
|
476
|
+
"Cosy-Version": "1.0.1",
|
|
477
|
+
"Cosy-ClientType": "5"
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
if (res.ok) {
|
|
481
|
+
const info = await res.json();
|
|
482
|
+
userID = info.id || "";
|
|
483
|
+
email = info.email || "";
|
|
484
|
+
name = info.name || info.username || "";
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
return { userID, email, name };
|
|
489
|
+
}
|
|
490
|
+
async function credentialsFromPat(pat) {
|
|
491
|
+
const { jobToken, jobRefreshToken, expiresAt } = await exchangeJobToken(pat);
|
|
492
|
+
const { userID, email, name } = await fetchUserInfo(jobToken);
|
|
493
|
+
const machineID = getMachineId();
|
|
494
|
+
return {
|
|
495
|
+
refresh: encodePatRefresh(pat, jobRefreshToken, userID, machineID),
|
|
496
|
+
access: jobToken,
|
|
497
|
+
expires: expiresAt - 5 * 60 * 1e3,
|
|
498
|
+
// 5 min buffer
|
|
499
|
+
userID,
|
|
500
|
+
email,
|
|
501
|
+
name,
|
|
502
|
+
machineID
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/login.ts
|
|
537
507
|
function getPrompt(callbacks) {
|
|
538
508
|
return callbacks.onPrompt;
|
|
539
509
|
}
|
|
@@ -561,29 +531,39 @@ function parseExpiresAt(s, expiresInSeconds) {
|
|
|
561
531
|
return Date.now() + 30 * 24 * 60 * 60 * 1e3;
|
|
562
532
|
}
|
|
563
533
|
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
534
|
const prompt = getPrompt(callbacks);
|
|
579
|
-
const
|
|
580
|
-
message: "
|
|
581
|
-
placeholder: "
|
|
535
|
+
const pat = await prompt({
|
|
536
|
+
message: "Paste a Qoder Personal Access Token (pt-...), or leave empty for browser login",
|
|
537
|
+
placeholder: "pt-...",
|
|
582
538
|
allowEmpty: true
|
|
583
539
|
});
|
|
584
540
|
if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
|
|
541
|
+
if (pat?.trim()) {
|
|
542
|
+
return patLogin(callbacks, pat.trim());
|
|
543
|
+
}
|
|
544
|
+
if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
|
|
585
545
|
return runDeviceFlow(callbacks);
|
|
586
546
|
}
|
|
547
|
+
async function patLogin(callbacks, providedPat) {
|
|
548
|
+
let pat = providedPat;
|
|
549
|
+
if (!pat) {
|
|
550
|
+
const prompt = getPrompt(callbacks);
|
|
551
|
+
const entered = await prompt({
|
|
552
|
+
message: "Paste your Qoder Personal Access Token (pt-...)",
|
|
553
|
+
placeholder: "pt-...",
|
|
554
|
+
allowEmpty: false
|
|
555
|
+
});
|
|
556
|
+
if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
|
|
557
|
+
pat = entered?.trim();
|
|
558
|
+
}
|
|
559
|
+
if (!pat) {
|
|
560
|
+
throw new Error("No Personal Access Token provided");
|
|
561
|
+
}
|
|
562
|
+
getProgress(callbacks)?.("Exchanging access token...");
|
|
563
|
+
const creds = await credentialsFromPat(pat);
|
|
564
|
+
getProgress(callbacks)?.("Login successful!");
|
|
565
|
+
return creds;
|
|
566
|
+
}
|
|
587
567
|
function abortableDelay(ms, signal) {
|
|
588
568
|
if (signal?.aborted) return Promise.reject(signal.reason || new Error("Login cancelled"));
|
|
589
569
|
return new Promise((resolve, reject) => {
|
|
@@ -666,7 +646,8 @@ async function runDeviceFlow(callbacks) {
|
|
|
666
646
|
machineID
|
|
667
647
|
};
|
|
668
648
|
} catch (e) {
|
|
669
|
-
|
|
649
|
+
const err = e;
|
|
650
|
+
if (err.name === "AbortError" || getSignal(callbacks)?.aborted) {
|
|
670
651
|
throw new Error("Login cancelled");
|
|
671
652
|
}
|
|
672
653
|
throw e;
|
|
@@ -682,7 +663,7 @@ function getCachedCredentials(_accessToken) {
|
|
|
682
663
|
try {
|
|
683
664
|
const auth = JSON.parse(readFileSync3(AUTH_FILE, "utf-8"));
|
|
684
665
|
const creds = auth?.qoder;
|
|
685
|
-
if (creds
|
|
666
|
+
if (creds?.userID) {
|
|
686
667
|
return creds;
|
|
687
668
|
}
|
|
688
669
|
} catch {
|
|
@@ -694,33 +675,11 @@ async function loginQoder(callbacks) {
|
|
|
694
675
|
const pat = process.env.QODER_PERSONAL_ACCESS_TOKEN || process.env.QODER_PAT;
|
|
695
676
|
if (pat) {
|
|
696
677
|
try {
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
Accept: "application/json",
|
|
701
|
-
"User-Agent": "pi-provider-qoder"
|
|
702
|
-
}
|
|
678
|
+
const creds2 = await credentialsFromPat(pat);
|
|
679
|
+
const qCreds = creds2;
|
|
680
|
+
updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email).catch(() => {
|
|
703
681
|
});
|
|
704
|
-
|
|
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
|
-
}
|
|
682
|
+
return creds2;
|
|
724
683
|
} catch {
|
|
725
684
|
}
|
|
726
685
|
}
|
|
@@ -734,16 +693,31 @@ async function loginQoder(callbacks) {
|
|
|
734
693
|
return creds;
|
|
735
694
|
}
|
|
736
695
|
async function refreshQoderToken(credentials) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
696
|
+
if (isPatRefresh(credentials.refresh)) {
|
|
697
|
+
const { pat } = decodePatRefresh(credentials.refresh);
|
|
698
|
+
if (pat) {
|
|
699
|
+
try {
|
|
700
|
+
const refreshed = await credentialsFromPat(pat);
|
|
701
|
+
const qCreds = refreshed;
|
|
702
|
+
updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email).catch(() => {
|
|
703
|
+
});
|
|
704
|
+
return refreshed;
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
}
|
|
742
708
|
return {
|
|
743
709
|
...credentials,
|
|
744
|
-
expires: Date.now() +
|
|
710
|
+
expires: Date.now() + 60 * 60 * 1e3
|
|
711
|
+
// extend 1 hour to retry later
|
|
745
712
|
};
|
|
746
713
|
}
|
|
714
|
+
const parts = credentials.refresh.split("|");
|
|
715
|
+
const refreshToken = parts[0] || "";
|
|
716
|
+
const userID = parts[1] || "";
|
|
717
|
+
const machineID = parts[2] || getMachineId();
|
|
718
|
+
const prev = credentials;
|
|
719
|
+
const prevName = prev.name || "";
|
|
720
|
+
const prevEmail = prev.email || "";
|
|
747
721
|
const refreshURL = "https://center.qoder.sh/algo/api/v3/user/refresh_token";
|
|
748
722
|
try {
|
|
749
723
|
const response = await fetch(refreshURL, {
|
|
@@ -773,16 +747,11 @@ async function refreshQoderToken(credentials) {
|
|
|
773
747
|
access: newAccess,
|
|
774
748
|
expires: expireMs - 5 * 60 * 1e3,
|
|
775
749
|
userID,
|
|
776
|
-
email:
|
|
777
|
-
name:
|
|
750
|
+
email: prevEmail,
|
|
751
|
+
name: prevName,
|
|
778
752
|
machineID
|
|
779
753
|
};
|
|
780
|
-
updateQoderModelsCache(
|
|
781
|
-
newAccess,
|
|
782
|
-
userID,
|
|
783
|
-
credentials.name || "",
|
|
784
|
-
credentials.email || ""
|
|
785
|
-
).catch(() => {
|
|
754
|
+
updateQoderModelsCache(newAccess, userID, prevName, prevEmail).catch(() => {
|
|
786
755
|
});
|
|
787
756
|
return refreshed;
|
|
788
757
|
}
|
|
@@ -1046,7 +1015,7 @@ function transformMessagesForQoder(messages) {
|
|
|
1046
1015
|
};
|
|
1047
1016
|
}
|
|
1048
1017
|
return null;
|
|
1049
|
-
}).filter(
|
|
1018
|
+
}).filter((p) => p !== null);
|
|
1050
1019
|
} else {
|
|
1051
1020
|
content = getContentText(msg);
|
|
1052
1021
|
}
|
|
@@ -1118,11 +1087,11 @@ function stableChatRecordID(model, messages, tools, maxTokens) {
|
|
|
1118
1087
|
hash.update("\0");
|
|
1119
1088
|
hash.update(model);
|
|
1120
1089
|
for (const msg of messages) {
|
|
1121
|
-
if (msg
|
|
1090
|
+
if (msg?.role) {
|
|
1122
1091
|
hash.update("\0");
|
|
1123
1092
|
hash.update(msg.role);
|
|
1124
1093
|
}
|
|
1125
|
-
if (msg
|
|
1094
|
+
if (msg?.content) {
|
|
1126
1095
|
hash.update("\0");
|
|
1127
1096
|
hash.update(typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content));
|
|
1128
1097
|
}
|
|
@@ -1186,7 +1155,7 @@ function streamQoder(model, context, options) {
|
|
|
1186
1155
|
for (let i = normalizedMessages.length - 1; i >= 0; i--) {
|
|
1187
1156
|
if (normalizedMessages[i].role === "user") {
|
|
1188
1157
|
const content = normalizedMessages[i].content;
|
|
1189
|
-
lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => c.text
|
|
1158
|
+
lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => "text" in c ? c.text : "").join("") : "";
|
|
1190
1159
|
break;
|
|
1191
1160
|
}
|
|
1192
1161
|
}
|
|
@@ -1361,7 +1330,7 @@ function streamQoder(model, context, options) {
|
|
|
1361
1330
|
for (const tc of delta.tool_calls) {
|
|
1362
1331
|
const idx = tc.index ?? 0;
|
|
1363
1332
|
if (!toolCallsState[idx]) {
|
|
1364
|
-
toolCallsState[idx] = { arguments: "" };
|
|
1333
|
+
toolCallsState[idx] = { arguments: "", id: "", name: "", contentIndex: 0 };
|
|
1365
1334
|
}
|
|
1366
1335
|
const state = toolCallsState[idx];
|
|
1367
1336
|
if (tc.id) state.id = tc.id;
|
|
@@ -1407,7 +1376,7 @@ function streamQoder(model, context, options) {
|
|
|
1407
1376
|
});
|
|
1408
1377
|
}
|
|
1409
1378
|
for (const state of toolCallsState) {
|
|
1410
|
-
if (state
|
|
1379
|
+
if (state?.emittedStart && !state.emittedEnd) {
|
|
1411
1380
|
state.emittedEnd = true;
|
|
1412
1381
|
let args = {};
|
|
1413
1382
|
try {
|
|
@@ -1498,30 +1467,28 @@ async function fetchQoderUsage(credentials) {
|
|
|
1498
1467
|
|
|
1499
1468
|
// src/index.ts
|
|
1500
1469
|
function index_default(pi) {
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1470
|
+
const oauth = {
|
|
1471
|
+
name: "Qoder (Browser OAuth / PAT)",
|
|
1472
|
+
login: loginQoder,
|
|
1473
|
+
refreshToken: refreshQoderToken,
|
|
1474
|
+
getApiKey: (cred) => cred.access,
|
|
1475
|
+
modifyModels: (models, _cred) => {
|
|
1476
|
+
const cached = getCachedModels();
|
|
1477
|
+
const nonQoder = models.filter((m) => m.provider !== "qoder");
|
|
1478
|
+
const modelsToUse = cached.length > 0 ? cached : staticModels;
|
|
1479
|
+
const modifiedQoder = modelsToUse.map((m) => ({
|
|
1480
|
+
...m,
|
|
1481
|
+
baseUrl: "https://api3.qoder.sh/"
|
|
1482
|
+
}));
|
|
1483
|
+
return [...nonQoder, ...modifiedQoder];
|
|
1484
|
+
},
|
|
1485
|
+
fetchUsage: fetchQoderUsage
|
|
1486
|
+
};
|
|
1504
1487
|
pi.registerProvider("qoder", {
|
|
1505
1488
|
baseUrl: "https://api3.qoder.sh/",
|
|
1506
1489
|
api: "qoder-api",
|
|
1507
1490
|
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
|
-
},
|
|
1491
|
+
oauth,
|
|
1525
1492
|
streamSimple: streamQoder
|
|
1526
1493
|
});
|
|
1527
1494
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-qoder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Pi extension for Qoder AI — with OAuth authentication, COSY signatures and WAF bypass",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"README.md"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"build": "./node_modules/esbuild/bin/esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@earendil-works/pi-ai --external:@earendil-works/pi-coding-agent
|
|
22
|
+
"build": "./node_modules/esbuild/bin/esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@earendil-works/pi-ai --external:@earendil-works/pi-coding-agent",
|
|
23
23
|
"check": "node node_modules/typescript/bin/tsc --noEmit",
|
|
24
24
|
"lint": "node node_modules/@biomejs/biome/bin/biome check .",
|
|
25
25
|
"lint:fix": "node node_modules/@biomejs/biome/bin/biome check --write .",
|
|
@@ -36,8 +36,15 @@
|
|
|
36
36
|
"@biomejs/biome": "2.4.2",
|
|
37
37
|
"@earendil-works/pi-ai": "^0.75.5",
|
|
38
38
|
"@earendil-works/pi-coding-agent": "^0.75.5",
|
|
39
|
-
"@earendil-works/pi-tui": "^0.75.5",
|
|
40
39
|
"esbuild": "^0.25.0",
|
|
41
40
|
"typescript": "^5.7.0"
|
|
42
|
-
}
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/simonsmh/pi-provider-qoder.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/simonsmh/pi-provider-qoder/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/simonsmh/pi-provider-qoder#readme"
|
|
43
50
|
}
|