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.
- package/README.md +85 -0
- package/dist/index.js +1530 -0
- 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
|
+
};
|