@wengine-ai/ccr-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/dist/README.md +55 -0
- package/dist/THIRD_PARTY_NOTICES.md +63 -0
- package/dist/card.js +425 -0
- package/dist/package.json +3 -0
- package/dist/plugin.js +2534 -0
- package/dist/plugin.json +6 -0
- package/package.json +2 -2
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,2534 @@
|
|
|
1
|
+
// src/auth-service.ts
|
|
2
|
+
import { constants as constants2 } from "node:fs";
|
|
3
|
+
import { open, mkdir, rename, rm, unlink } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
// src/vendor/engine.mjs
|
|
8
|
+
import { createCipheriv, createHash, publicEncrypt, randomUUID, constants } from "node:crypto";
|
|
9
|
+
var SITES = Object.freeze({
|
|
10
|
+
qoder: Object.freeze({
|
|
11
|
+
id: "qoder",
|
|
12
|
+
name: "Qoder",
|
|
13
|
+
web: "https://qoder.com",
|
|
14
|
+
openapi: "https://openapi.qoder.sh",
|
|
15
|
+
api: "https://api3.qoder.sh"
|
|
16
|
+
}),
|
|
17
|
+
"qoder-cn": Object.freeze({
|
|
18
|
+
id: "qoder-cn",
|
|
19
|
+
name: "Qoder CN",
|
|
20
|
+
web: "https://qoder.cn",
|
|
21
|
+
openapi: "https://openapi.qoder.com.cn",
|
|
22
|
+
api: "https://gateway.qoder.com.cn"
|
|
23
|
+
})
|
|
24
|
+
});
|
|
25
|
+
function createQoderEngine(options) {
|
|
26
|
+
if (typeof options?.fetch !== "function") throw new TypeError("Qoder requires an injected fetch");
|
|
27
|
+
const site = SITES[options.site ?? "qoder"];
|
|
28
|
+
if (!site) throw new TypeError(`Qoder: unknown site "${options.site}"`);
|
|
29
|
+
const now = options.now ?? (() => Date.now());
|
|
30
|
+
const combine = (...signals) => AbortSignal.any([options.signal, ...signals].filter(Boolean));
|
|
31
|
+
const checkAbort = (signal) => {
|
|
32
|
+
const aborted = [signal, options.signal].find((s) => s?.aborted);
|
|
33
|
+
if (aborted) throw aborted.reason ?? new DOMException("aborted", "AbortError");
|
|
34
|
+
};
|
|
35
|
+
const fetch2 = async (input, init = {}) => {
|
|
36
|
+
const signal = combine(init.signal);
|
|
37
|
+
checkAbort(signal);
|
|
38
|
+
try {
|
|
39
|
+
const res = await options.fetch(input, { ...init, signal, redirect: "error" });
|
|
40
|
+
checkAbort(signal);
|
|
41
|
+
return res;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
checkAbort(signal);
|
|
44
|
+
throw e;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const STATUS_CODES = { 400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type", 416: "Range Not Satisfiable", 417: "Expectation Failed", 418: "I'm a Teapot", 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", 425: "Too Early", 426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected", 509: "Bandwidth Limit Exceeded", 510: "Not Extended", 511: "Network Authentication Required" };
|
|
48
|
+
const CHAT_PATH2 = "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1";
|
|
49
|
+
const MODELS_PATH = "/algo/api/v2/model/list?Encode=1";
|
|
50
|
+
const COSY_VERSION = "1.1.69";
|
|
51
|
+
const REFRESH_LEAD = 5 * 60 * 1e3;
|
|
52
|
+
const KEEP_SPENT_MS = 60 * 60 * 1e3;
|
|
53
|
+
const DAY = 24 * 60 * 60 * 1e3;
|
|
54
|
+
const RSA_KEY = `-----BEGIN PUBLIC KEY-----
|
|
55
|
+
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
56
|
+
4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
|
|
57
|
+
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
|
|
58
|
+
XcW+ML9FoCI6AOvOzwIDAQAB
|
|
59
|
+
-----END PUBLIC KEY-----`;
|
|
60
|
+
const QODER_SYS = "You are a Qoder agent. Use the instructions below and the tools available to you to assist the user.";
|
|
61
|
+
const hexID = () => randomUUID().replaceAll("-", "");
|
|
62
|
+
function machineOS() {
|
|
63
|
+
const arch = { x64: "x86_64", ia32: "x86", arm64: "aarch64" }[process.arch] ?? process.arch;
|
|
64
|
+
return arch + "_" + process.platform;
|
|
65
|
+
}
|
|
66
|
+
function userBlob(u, rsaKey = RSA_KEY) {
|
|
67
|
+
const raw = JSON.stringify({ uid: u.uid, aid: "", name: u.name ?? "", email: u.email ?? "", security_oauth_token: u.access });
|
|
68
|
+
const key = Buffer.from(hexID().slice(0, 16));
|
|
69
|
+
const c = createCipheriv("aes-128-cbc", key, key);
|
|
70
|
+
const info = Buffer.concat([c.update(raw, "utf8"), c.final()]).toString("base64");
|
|
71
|
+
const wrapped = publicEncrypt({ key: rsaKey, padding: constants.RSA_PKCS1_PADDING }, key).toString("base64");
|
|
72
|
+
return { info, key: wrapped };
|
|
73
|
+
}
|
|
74
|
+
function cosyHeaders(url, u, body, site2, ts = Math.floor(now() / 1e3)) {
|
|
75
|
+
if (!u?.machineId) throw new Error(`${site2?.name ?? "Qoder"}: the account has no machine id; sign in again`);
|
|
76
|
+
const cosyVersion = site2?.cosyVersion ?? COSY_VERSION;
|
|
77
|
+
const blob = userBlob(u, site2?.rsaKey);
|
|
78
|
+
const payload = Buffer.from(
|
|
79
|
+
JSON.stringify({ version: "v1", requestId: hexID(), info: blob.info, cosyVersion, ideVersion: "" })
|
|
80
|
+
).toString("base64");
|
|
81
|
+
let path = new URL(url).pathname;
|
|
82
|
+
if (path.startsWith("/algo")) path = path.slice(5);
|
|
83
|
+
const sig = createHash("md5").update(`${payload}
|
|
84
|
+
${blob.key}
|
|
85
|
+
${ts}
|
|
86
|
+
${body}
|
|
87
|
+
${path}`).digest("hex");
|
|
88
|
+
const m = u.machineId;
|
|
89
|
+
return {
|
|
90
|
+
Accept: "application/json",
|
|
91
|
+
"Accept-Encoding": "identity",
|
|
92
|
+
"Content-Type": "application/json",
|
|
93
|
+
Authorization: `Bearer COSY.${payload}.${sig}`,
|
|
94
|
+
"Cosy-Business-Product": site2?.businessProduct ?? "app",
|
|
95
|
+
"Cosy-Business-Type": "agent",
|
|
96
|
+
"Cosy-ClientIp": m,
|
|
97
|
+
"Cosy-ClientType": site2?.clientType ?? "10",
|
|
98
|
+
"Cosy-Data-Policy": "disagree",
|
|
99
|
+
"Cosy-Date": String(ts),
|
|
100
|
+
"Cosy-Key": blob.key,
|
|
101
|
+
"Cosy-MachineId": m,
|
|
102
|
+
"Cosy-MachineToken": m,
|
|
103
|
+
"Cosy-MachineType": "5",
|
|
104
|
+
"Cosy-MachineOS": machineOS(),
|
|
105
|
+
"Cosy-Scene": site2?.modelScene ?? "app",
|
|
106
|
+
"Cosy-User": u.uid,
|
|
107
|
+
"Cosy-Version": cosyVersion,
|
|
108
|
+
"Login-Version": "v2"
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const ALPHABET = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
|
|
112
|
+
function segmentEncode(bytes) {
|
|
113
|
+
let s = "";
|
|
114
|
+
let acc = 0;
|
|
115
|
+
let nb = 0;
|
|
116
|
+
for (const b of bytes) {
|
|
117
|
+
acc = (acc << 8 | b) & 16777215;
|
|
118
|
+
nb += 8;
|
|
119
|
+
while (nb >= 6) {
|
|
120
|
+
nb -= 6;
|
|
121
|
+
s += ALPHABET[acc >> nb & 63];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (nb > 0) s += ALPHABET[acc << 6 - nb & 63];
|
|
125
|
+
while (s.length % 4) s += "$";
|
|
126
|
+
return s;
|
|
127
|
+
}
|
|
128
|
+
function swapThirds(s) {
|
|
129
|
+
const t = Math.floor(s.length / 3);
|
|
130
|
+
return t ? s.slice(s.length - t) + s.slice(t, s.length - t) + s.slice(0, t) : s;
|
|
131
|
+
}
|
|
132
|
+
const encodeBody = (text) => swapThirds(segmentEncode(Buffer.from(text, "utf8")));
|
|
133
|
+
function decodeBody(wire) {
|
|
134
|
+
const s = swapThirds(wire);
|
|
135
|
+
const out = [];
|
|
136
|
+
for (let g = 0; g + 4 <= s.length; g += 4) {
|
|
137
|
+
const vals = [...s.slice(g, g + 4)].filter((c) => c !== "$").map((c) => ALPHABET.indexOf(c));
|
|
138
|
+
let x = 0;
|
|
139
|
+
for (const v of vals) x = x * 64 + v;
|
|
140
|
+
const n = Math.floor(6 * vals.length / 8);
|
|
141
|
+
for (let k = 0; k < n; k++) out.push(Math.floor(x / 2 ** (6 * vals.length - 8 * (k + 1))) & 255);
|
|
142
|
+
}
|
|
143
|
+
return Buffer.from(out).toString("utf8");
|
|
144
|
+
}
|
|
145
|
+
async function openapi(site2, path, { method = "GET", token, body, query, signal } = {}) {
|
|
146
|
+
const url = new URL(site2.openapi + path);
|
|
147
|
+
for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, v);
|
|
148
|
+
const headers = { Accept: "application/json" };
|
|
149
|
+
if (body) headers["Content-Type"] = "application/json";
|
|
150
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
151
|
+
const res = await fetch2(url, { method, headers, body: body ? JSON.stringify(body) : void 0, signal: combine(signal, AbortSignal.timeout(2e4)) });
|
|
152
|
+
const text = await res.text();
|
|
153
|
+
checkAbort(signal);
|
|
154
|
+
let json = null;
|
|
155
|
+
try {
|
|
156
|
+
json = JSON.parse(text);
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
return { status: res.status, json, text };
|
|
160
|
+
}
|
|
161
|
+
const expiresAt = (jt) => now() + (jt.expires_in > 0 ? jt.expires_in : DAY);
|
|
162
|
+
function exchangeExpiry(jt) {
|
|
163
|
+
for (const k of ["expire_time", "expires_at"]) {
|
|
164
|
+
const at = Date.parse(String(jt?.[k] ?? "").trim());
|
|
165
|
+
if (!isNaN(at)) return at;
|
|
166
|
+
}
|
|
167
|
+
if (jt?.expires_in > 0) return now() + jt.expires_in;
|
|
168
|
+
return now() + DAY;
|
|
169
|
+
}
|
|
170
|
+
function exchangeToken(jt) {
|
|
171
|
+
return String(jt?.token ?? jt?.device_token ?? jt?.access_token ?? "").trim();
|
|
172
|
+
}
|
|
173
|
+
async function patSignIn(site2, pat, signal) {
|
|
174
|
+
const machineId = randomUUID();
|
|
175
|
+
const r = await openapi(site2, "/api/v1/jobToken/exchange", { method: "POST", body: { personal_token: pat }, signal });
|
|
176
|
+
if (r.status === 400 || r.status === 401 || r.status === 403)
|
|
177
|
+
throw Object.assign(new Error(`${site2.name} refused the PAT \u2014 check it is still valid`), { status: r.status });
|
|
178
|
+
if (r.status !== 200) throw new Error(`${site2.name} PAT exchange: status ${r.status}`);
|
|
179
|
+
const token = exchangeToken(r.json);
|
|
180
|
+
if (!token) throw new Error(`${site2.name} PAT exchange: empty token in response`);
|
|
181
|
+
const refresh2 = String(r.json?.refresh_token ?? r.json?.refreshToken ?? "").trim();
|
|
182
|
+
const chat2 = { access: token, refresh: refresh2, expires: exchangeExpiry(r.json) };
|
|
183
|
+
let email = "";
|
|
184
|
+
let name = "";
|
|
185
|
+
let uid = "";
|
|
186
|
+
try {
|
|
187
|
+
const ui = await openapi(site2, "/api/v1/userinfo", { token, signal });
|
|
188
|
+
if (ui.status >= 200 && ui.status < 300) {
|
|
189
|
+
;
|
|
190
|
+
[email, name] = [ui.json?.email ?? "", ui.json?.name ?? ""];
|
|
191
|
+
uid = String(ui.json?.id ?? ui.json?.user_id ?? ui.json?.uid ?? "");
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
checkAbort(signal);
|
|
195
|
+
}
|
|
196
|
+
if (!uid) throw new Error(`${site2.name} PAT sign-in: could not determine user id`);
|
|
197
|
+
return {
|
|
198
|
+
...chat2,
|
|
199
|
+
// The same name its browser sign-in takes: magpie's plugin host replaces
|
|
200
|
+
// an account on a new sign-in when both the accountId and the uid match,
|
|
201
|
+
// so a renewed PAT sign-in must keep naming the same account. A suffix
|
|
202
|
+
// here would add a second row beside the lapsed one instead.
|
|
203
|
+
accountId: email || uid,
|
|
204
|
+
uid,
|
|
205
|
+
email,
|
|
206
|
+
name: name || "",
|
|
207
|
+
machineId,
|
|
208
|
+
pat
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
class SignInGone extends Error {
|
|
212
|
+
constructor(message, expired = false) {
|
|
213
|
+
super(message);
|
|
214
|
+
this.expired = expired;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function refreshJob(site2, refresh2, user, signal) {
|
|
218
|
+
if (!String(refresh2 ?? "").trim()) throw new SignInGone(`${site2.name}: the sign-in lapsed; sign in again`);
|
|
219
|
+
const r = await openapi(site2, "/api/v1/jobToken/refresh", { method: "POST", body: { refresh_token: refresh2 }, signal });
|
|
220
|
+
if (r.status === 401 || r.status === 403)
|
|
221
|
+
throw new SignInGone(`${user}'s ${site2.name} sign-in has expired \u2014 sign in again (qoder job token refresh: status ${r.status})`, true);
|
|
222
|
+
if (r.status !== 200) throw new Error(`qoder job token refresh: status ${r.status}`);
|
|
223
|
+
if (!String(r.json?.token ?? "").trim() || !String(r.json?.refresh_token ?? "").trim())
|
|
224
|
+
throw new Error("qoder job token refresh: incomplete token pair");
|
|
225
|
+
return r.json;
|
|
226
|
+
}
|
|
227
|
+
const EFFORT_ORDER = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
228
|
+
function freeOf(raw) {
|
|
229
|
+
const price = raw.price_factor ?? raw.priceFactor;
|
|
230
|
+
if (typeof price === "number" && Number.isFinite(price)) {
|
|
231
|
+
if (price !== 0) return false;
|
|
232
|
+
const p = raw.promotion ?? raw.prommotion;
|
|
233
|
+
const before = p?.before_promotion_price_factor ?? p?.beforePromotionPriceFactor;
|
|
234
|
+
return !(p?.active === true && typeof before === "number" && before > 0);
|
|
235
|
+
}
|
|
236
|
+
return (raw.is_free ?? raw.isFree) === true;
|
|
237
|
+
}
|
|
238
|
+
function rateOf(raw) {
|
|
239
|
+
const price = raw.price_factor ?? raw.priceFactor;
|
|
240
|
+
if (typeof price !== "number" || !Number.isFinite(price)) return { rate: 0, rateWas: 0 };
|
|
241
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
242
|
+
const p = raw.promotion ?? raw.prommotion;
|
|
243
|
+
const before = p?.before_promotion_price_factor ?? p?.beforePromotionPriceFactor;
|
|
244
|
+
const discount = num(p?.discount_factor) || num(p?.discountFactor);
|
|
245
|
+
const discounted = p?.active === true && typeof before === "number" && before > 0;
|
|
246
|
+
let rate = Math.max(price, 0);
|
|
247
|
+
let rateWas = Math.max(num(raw.original_price_factor), num(raw.originalPriceFactor));
|
|
248
|
+
if (discounted) {
|
|
249
|
+
rateWas = before;
|
|
250
|
+
if (rate === 0) rate = before * discount;
|
|
251
|
+
}
|
|
252
|
+
if (rateWas <= rate) rateWas = 0;
|
|
253
|
+
return { rate, rateWas };
|
|
254
|
+
}
|
|
255
|
+
function windowsOf(raw) {
|
|
256
|
+
const n = (v) => Number.isInteger(v) && v > 0 ? v : 0;
|
|
257
|
+
const windows = [];
|
|
258
|
+
let def = 0;
|
|
259
|
+
const cc = raw?.context_config ?? raw?.contextConfig;
|
|
260
|
+
if (cc && typeof cc === "object" && !Array.isArray(cc)) {
|
|
261
|
+
for (const e of Object.values(cc)) {
|
|
262
|
+
const t = n(e?.token_count);
|
|
263
|
+
if (!t) continue;
|
|
264
|
+
if (!windows.includes(t)) windows.push(t);
|
|
265
|
+
if (e.is_default === true || e.is_default === 1 || e.is_default === "1") def = t;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!windows.length) {
|
|
269
|
+
const list = raw?.available_context_windows ?? raw?.availableContextWindows;
|
|
270
|
+
for (const v of Array.isArray(list) ? list : []) if (n(v) && !windows.includes(v)) windows.push(v);
|
|
271
|
+
def = n(raw?.default_context_window ?? raw?.defaultContextWindow);
|
|
272
|
+
if (!windows.includes(def)) def = 0;
|
|
273
|
+
}
|
|
274
|
+
windows.sort((a, b) => a - b);
|
|
275
|
+
return { windows, defaultWindow: def };
|
|
276
|
+
}
|
|
277
|
+
function modelInfo(raw) {
|
|
278
|
+
const { windows, defaultWindow } = windowsOf(raw);
|
|
279
|
+
const m = {
|
|
280
|
+
key: raw.key,
|
|
281
|
+
source: raw.source ?? "",
|
|
282
|
+
name: raw.display_name || raw.key,
|
|
283
|
+
images: !!raw.is_vl,
|
|
284
|
+
context: windows.length ? windows[windows.length - 1] : raw.max_input_tokens || 0,
|
|
285
|
+
windows,
|
|
286
|
+
defaultWindow,
|
|
287
|
+
thinks: !!raw.is_reasoning,
|
|
288
|
+
alwaysThinks: false,
|
|
289
|
+
efforts: Array.isArray(raw.reasoning_efforts) ? [...raw.reasoning_efforts] : [],
|
|
290
|
+
defaultEffort: "",
|
|
291
|
+
free: freeOf(raw),
|
|
292
|
+
...rateOf(raw),
|
|
293
|
+
config: raw
|
|
294
|
+
};
|
|
295
|
+
const tc = raw.thinking_config;
|
|
296
|
+
if (tc && typeof tc === "object") {
|
|
297
|
+
m.thinks = tc.enabled != null;
|
|
298
|
+
m.alwaysThinks = m.thinks && tc.disabled == null;
|
|
299
|
+
m.efforts = [];
|
|
300
|
+
if (m.thinks) {
|
|
301
|
+
for (const [name, e] of Object.entries(tc.enabled.efforts ?? {})) {
|
|
302
|
+
m.efforts.push(name);
|
|
303
|
+
if (e?.is_default) m.defaultEffort = name;
|
|
304
|
+
}
|
|
305
|
+
const rank = (s) => EFFORT_ORDER.includes(s) ? EFFORT_ORDER.indexOf(s) : EFFORT_ORDER.length;
|
|
306
|
+
m.efforts.sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return m;
|
|
310
|
+
}
|
|
311
|
+
function sceneModels(listing) {
|
|
312
|
+
const app = Array.isArray(listing?.app) ? listing.app : [];
|
|
313
|
+
const chat2 = Array.isArray(listing?.chat) ? listing.chat : [];
|
|
314
|
+
const out = [...app];
|
|
315
|
+
let after = -1;
|
|
316
|
+
for (const m of chat2) {
|
|
317
|
+
const i = out.findIndex((x) => x?.key === m?.key);
|
|
318
|
+
if (i >= 0) {
|
|
319
|
+
after = i;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
out.splice(++after, 0, m);
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
function modelInfos(listing, site2) {
|
|
327
|
+
if (site2?.modelScene) {
|
|
328
|
+
return (Array.isArray(listing?.[site2.modelScene]) ? listing[site2.modelScene] : []).filter((m) => (site2.listAllModels || m?.enable) && !["", "auto", "default"].includes(String(m.key ?? "").trim())).map(modelInfo);
|
|
329
|
+
}
|
|
330
|
+
return sceneModels(listing).filter((m) => m?.enable && !["", "auto", "default"].includes(String(m.key ?? "").trim())).map(modelInfo);
|
|
331
|
+
}
|
|
332
|
+
async function fetchListing(site2, cred, signal) {
|
|
333
|
+
const url = site2.api + MODELS_PATH;
|
|
334
|
+
const res = await fetch2(url, { headers: cosyHeaders(url, cred, "", site2), signal: combine(signal, AbortSignal.timeout(15e3)) });
|
|
335
|
+
const text = await res.text();
|
|
336
|
+
checkAbort(signal);
|
|
337
|
+
if (!res.ok) {
|
|
338
|
+
throw new Error(`${site2.name} models: HTTP ${res.status}: ${text.trim().slice(0, 512)}`);
|
|
339
|
+
}
|
|
340
|
+
return JSON.parse(text);
|
|
341
|
+
}
|
|
342
|
+
const EFFORT_RANK = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
343
|
+
function fitEffort(want, levels) {
|
|
344
|
+
if (want === "ultra" && !levels.includes(want)) want = "max";
|
|
345
|
+
if (!levels.length || levels.includes(want)) return want;
|
|
346
|
+
const at = EFFORT_RANK.indexOf(want);
|
|
347
|
+
if (at < 0) return want;
|
|
348
|
+
let best = want;
|
|
349
|
+
let dist = EFFORT_RANK.length;
|
|
350
|
+
for (const l of levels) {
|
|
351
|
+
const i = EFFORT_RANK.indexOf(l);
|
|
352
|
+
if (i < 0 || l === "none") continue;
|
|
353
|
+
const d = Math.abs(i - at);
|
|
354
|
+
if (d < dist || d === dist && i > at) [best, dist] = [l, d];
|
|
355
|
+
}
|
|
356
|
+
return best;
|
|
357
|
+
}
|
|
358
|
+
function effortFor(asked, m) {
|
|
359
|
+
let want = String(asked ?? "").toLowerCase();
|
|
360
|
+
if (!EFFORT_RANK.includes(want)) want = "";
|
|
361
|
+
if (!m.thinks || want === "none" && !m.alwaysThinks) return [false, ""];
|
|
362
|
+
if (!m.efforts.length) return [true, ""];
|
|
363
|
+
if (want === "") return [true, m.defaultEffort];
|
|
364
|
+
if (want === "none") return [true, m.efforts[0]];
|
|
365
|
+
return [true, fitEffort(want, m.efforts)];
|
|
366
|
+
}
|
|
367
|
+
function imageBlock(p) {
|
|
368
|
+
const url = typeof p.image_url === "string" ? p.image_url : p.image_url?.url;
|
|
369
|
+
return url ? { type: "image_url", image_url: { url } } : null;
|
|
370
|
+
}
|
|
371
|
+
function blocksOf(content) {
|
|
372
|
+
if (typeof content === "string") return content ? [{ type: "text", text: content }] : [];
|
|
373
|
+
const out = [];
|
|
374
|
+
for (const p of content ?? []) {
|
|
375
|
+
if (p?.type === "text" && p.text != null) out.push({ type: "text", text: p.text });
|
|
376
|
+
else if (p?.type === "image_url") {
|
|
377
|
+
const b = imageBlock(p);
|
|
378
|
+
if (b) out.push(b);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
const textOf = (c) => typeof c === "string" ? c : blocksOf(c).filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
384
|
+
function callID(at, k, c) {
|
|
385
|
+
const args = c.function?.arguments;
|
|
386
|
+
const said = typeof args === "string" ? args : JSON.stringify(args ?? {});
|
|
387
|
+
return "call_" + createHash("sha256").update(`${at}\0${k}\0${c.function?.name ?? ""}\0${said}`).digest("hex").slice(0, 32);
|
|
388
|
+
}
|
|
389
|
+
function qoderMessages(msgs) {
|
|
390
|
+
const out = [];
|
|
391
|
+
const names = {};
|
|
392
|
+
let seen = [];
|
|
393
|
+
const showSeen = () => {
|
|
394
|
+
if (seen.length) out.push({ role: "user", content: seen });
|
|
395
|
+
seen = [];
|
|
396
|
+
};
|
|
397
|
+
let turn = null;
|
|
398
|
+
const asText = (txt, ims) => {
|
|
399
|
+
const said = txt.trim();
|
|
400
|
+
if (!said && !ims.length) return;
|
|
401
|
+
if (seen.length) seen.push({ type: "text", text: "\n\n" });
|
|
402
|
+
if (said) seen.push({ type: "text", text: said });
|
|
403
|
+
seen.push(...ims);
|
|
404
|
+
};
|
|
405
|
+
for (const [i, m] of msgs.entries()) {
|
|
406
|
+
if (m.role === "system" || m.role === "developer") continue;
|
|
407
|
+
if (m.role === "user") turn = null;
|
|
408
|
+
if (m.role !== "user" && m.role !== "tool") showSeen();
|
|
409
|
+
if (m.role === "tool") {
|
|
410
|
+
let txt = textOf(m.content);
|
|
411
|
+
const ims = Array.isArray(m.content) ? m.content.filter((p) => p?.type === "image_url").map(imageBlock).filter(Boolean) : [];
|
|
412
|
+
const named = String(m.tool_call_id ?? "").trim();
|
|
413
|
+
const at = named ? turn?.open.findIndex((id) => id === named) ?? -1 : -1;
|
|
414
|
+
const pick = at >= 0 ? at : turn ? turn.open.findIndex((id) => !turn.kept.has(id)) : -1;
|
|
415
|
+
if (pick < 0) {
|
|
416
|
+
asText(txt, ims);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const answered = turn.open.splice(pick, 1)[0];
|
|
420
|
+
if (ims.length) {
|
|
421
|
+
let of = "tool call " + answered;
|
|
422
|
+
const name = names[answered] || m.name;
|
|
423
|
+
if (name) of = `${name} (${of})`;
|
|
424
|
+
seen.push({ type: "text", text: `[From the result of ${of}:]` }, ...ims);
|
|
425
|
+
const note = ims.length === 1 ? "[The tool returned an image; it follows in the next message.]" : `[The tool returned ${ims.length} images; they follow in the next message.]`;
|
|
426
|
+
txt = txt.trim() ? txt + "\n\n" + note : txt + note;
|
|
427
|
+
}
|
|
428
|
+
out.push({ role: "tool", tool_call_id: answered, content: txt });
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (m.role === "assistant" && m.tool_calls?.length) {
|
|
432
|
+
const calls = m.tool_calls.map((c, k) => {
|
|
433
|
+
const id = String(c.id ?? "").trim() || callID(i, k, c);
|
|
434
|
+
names[id] = c.function?.name;
|
|
435
|
+
const args = c.function?.arguments;
|
|
436
|
+
return { id, type: "function", function: { name: c.function?.name, arguments: typeof args === "string" ? args : JSON.stringify(args ?? {}) } };
|
|
437
|
+
});
|
|
438
|
+
out.push({ role: "assistant", content: textOf(m.content), tool_calls: calls });
|
|
439
|
+
const open2 = calls.map((c) => c.id);
|
|
440
|
+
const kept = /* @__PURE__ */ new Set();
|
|
441
|
+
for (let j = i + 1; j < msgs.length && msgs[j].role === "tool"; j++) {
|
|
442
|
+
const id = String(msgs[j].tool_call_id ?? "").trim();
|
|
443
|
+
if (open2.includes(id)) kept.add(id);
|
|
444
|
+
}
|
|
445
|
+
turn = { open: open2, kept };
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
let blocks = blocksOf(m.content);
|
|
449
|
+
if (m.role === "assistant") blocks = blocks.filter((b) => b.type === "text");
|
|
450
|
+
if (!blocks.length) continue;
|
|
451
|
+
if (m.role === "user" && seen.length) [blocks, seen] = [[...seen, ...blocks], []];
|
|
452
|
+
out.push({ role: m.role, content: blocks });
|
|
453
|
+
turn = null;
|
|
454
|
+
}
|
|
455
|
+
showSeen();
|
|
456
|
+
return out;
|
|
457
|
+
}
|
|
458
|
+
function windowFor(chat2, m, reply = 0) {
|
|
459
|
+
const base = m.defaultWindow || (m.config?.max_input_tokens > 0 ? m.config.max_input_tokens : 0) || m.context;
|
|
460
|
+
if (!m.windows?.length) return m.context;
|
|
461
|
+
const need = Math.ceil(Buffer.byteLength(JSON.stringify({ m: chat2.messages ?? [], t: chat2.tools ?? [] })) / 3) + Math.max(0, reply);
|
|
462
|
+
if (base > 0 && need <= base) return base;
|
|
463
|
+
return m.windows.find((w) => w >= need) ?? m.windows[m.windows.length - 1];
|
|
464
|
+
}
|
|
465
|
+
const UNCACHED = /* @__PURE__ */ new Set(["thinking", "redacted_thinking", "tool_use", "tool_result"]);
|
|
466
|
+
function cacheMarked(msgs) {
|
|
467
|
+
const out = msgs.map((m) => m.role === "user" && Array.isArray(m.content) ? { ...m, contents: m.content } : m);
|
|
468
|
+
const last = out.at(-1);
|
|
469
|
+
if (last?.role !== "user" || !Array.isArray(last.content)) return out;
|
|
470
|
+
const at = last.content.findLastIndex((b) => b?.type && !UNCACHED.has(b.type));
|
|
471
|
+
if (at < 0) return out;
|
|
472
|
+
const blocks = last.content.slice();
|
|
473
|
+
blocks[at] = { ...blocks[at], cache_control: { type: "ephemeral" } };
|
|
474
|
+
out[out.length - 1] = { ...last, content: blocks, contents: blocks };
|
|
475
|
+
return out;
|
|
476
|
+
}
|
|
477
|
+
function sessionOf(chat2, named = "") {
|
|
478
|
+
let seed = named ? "session\0" + named : String(chat2.prompt_cache_key ?? "").trim() ? "key\0" + chat2.prompt_cache_key : String(chat2.user ?? "").trim() ? "user\0" + chat2.user : "";
|
|
479
|
+
if (!seed) {
|
|
480
|
+
const msgs = chat2.messages ?? [];
|
|
481
|
+
const sys = msgs.filter((x) => x.role === "system" || x.role === "developer").map((x) => textOf(x.content));
|
|
482
|
+
const first = msgs.find((x) => x.role === "user");
|
|
483
|
+
seed = "chat\0" + JSON.stringify([sys, first?.content ?? ""]);
|
|
484
|
+
}
|
|
485
|
+
const h = createHash("sha256").update(seed).digest("hex");
|
|
486
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
487
|
+
}
|
|
488
|
+
const TASKS = /* @__PURE__ */ new Map();
|
|
489
|
+
const TASK_MAX = 512;
|
|
490
|
+
const TASK_IDLE = 2 * 60 * 60 * 1e3;
|
|
491
|
+
const taskName = (prompt) => prompt.length > 10 ? prompt.slice(0, 10) : prompt;
|
|
492
|
+
function taskOf(sid, chat2, atTime = now()) {
|
|
493
|
+
const msgs = chat2.messages ?? [];
|
|
494
|
+
const at = msgs.findLastIndex((x) => x?.role === "user");
|
|
495
|
+
const key = createHash("sha256").update(JSON.stringify([sid, at, at < 0 ? null : msgs[at].content ?? null])).digest("hex");
|
|
496
|
+
for (const [k, t2] of TASKS) {
|
|
497
|
+
if (atTime - t2.used <= TASK_IDLE) break;
|
|
498
|
+
TASKS.delete(k);
|
|
499
|
+
}
|
|
500
|
+
let t = TASKS.get(key);
|
|
501
|
+
if (t && atTime - t.used > TASK_IDLE) t = void 0;
|
|
502
|
+
TASKS.delete(key);
|
|
503
|
+
t ??= { id: randomUUID(), begin: atTime, answered: false, name: taskName(at < 0 ? "" : textOf(msgs[at].content)) };
|
|
504
|
+
t.used = atTime;
|
|
505
|
+
TASKS.set(key, t);
|
|
506
|
+
while (TASKS.size > TASK_MAX) TASKS.delete(TASKS.keys().next().value);
|
|
507
|
+
return t;
|
|
508
|
+
}
|
|
509
|
+
const SUB_ONE = ["items", "additionalItems", "unevaluatedItems", "contains", "not", "if", "then", "else", "propertyNames", "additionalProperties", "unevaluatedProperties"];
|
|
510
|
+
const SUB_LIST = ["anyOf", "oneOf", "allOf", "prefixItems"];
|
|
511
|
+
const SUB_MAP = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"];
|
|
512
|
+
function typedUnions(schema) {
|
|
513
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
514
|
+
const root = structuredClone(schema);
|
|
515
|
+
const isObj = (v) => v && typeof v === "object" && !Array.isArray(v);
|
|
516
|
+
const target = (ref) => {
|
|
517
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
518
|
+
let at = root;
|
|
519
|
+
for (const k of ref.slice(2).split("/")) at = at?.[k.replaceAll("~1", "/").replaceAll("~0", "~")];
|
|
520
|
+
return isObj(at) ? at : void 0;
|
|
521
|
+
};
|
|
522
|
+
const object = (b, seen = []) => {
|
|
523
|
+
if (!isObj(b)) return false;
|
|
524
|
+
if ("type" in b) return b.type === "object";
|
|
525
|
+
if (typeof b.$ref !== "string" || seen.includes(b.$ref) || seen.length > 16) return false;
|
|
526
|
+
return object(target(b.$ref), [...seen, b.$ref]);
|
|
527
|
+
};
|
|
528
|
+
const walk = (s, depth) => {
|
|
529
|
+
if (!isObj(s) || depth > 64) return;
|
|
530
|
+
if (!("type" in s) && Array.isArray(s.oneOf) && s.oneOf.length && s.oneOf.every((b) => object(b))) s.type = "object";
|
|
531
|
+
for (const k of SUB_ONE) walk(s[k], depth + 1);
|
|
532
|
+
for (const k of SUB_LIST) for (const v of Array.isArray(s[k]) ? s[k] : []) walk(v, depth + 1);
|
|
533
|
+
for (const k of SUB_MAP) if (isObj(s[k])) for (const v of Object.values(s[k])) walk(v, depth + 1);
|
|
534
|
+
};
|
|
535
|
+
walk(root, 0);
|
|
536
|
+
return root;
|
|
537
|
+
}
|
|
538
|
+
function qoderBody(chat2, m, named = "", task, site2) {
|
|
539
|
+
const system = (chat2.messages ?? []).filter((x) => x.role === "system" || x.role === "developer").map((x) => textOf(x.content)).filter(Boolean).join("\n\n");
|
|
540
|
+
const tools = chat2.tool_choice !== "none" ? (chat2.tools ?? []).filter((t) => t?.type === "function" && t.function?.name) : [];
|
|
541
|
+
const baseSystem = site2?.systemPrompt ?? QODER_SYS;
|
|
542
|
+
let sysText = system ? baseSystem + "\n\n" + system : baseSystem;
|
|
543
|
+
if (chat2.tool_choice === "required" && tools.length) sysText += "\nYou must call an available function in this response.";
|
|
544
|
+
const sys = { type: "text", text: sysText };
|
|
545
|
+
const [thinking, effort] = effortFor(chat2.reasoning_effort, m);
|
|
546
|
+
const params = { enable_thinking: thinking, max_tokens: chat2.max_completion_tokens || chat2.max_tokens || 32e3 };
|
|
547
|
+
if (effort) params.reasoning_effort = effort;
|
|
548
|
+
const window = windowFor(chat2, m, params.max_tokens);
|
|
549
|
+
if (window > 0) params.context_length = window;
|
|
550
|
+
const request = randomUUID();
|
|
551
|
+
const session = sessionOf(chat2, named);
|
|
552
|
+
task ??= taskOf(session, chat2);
|
|
553
|
+
const body = {
|
|
554
|
+
request_id: request,
|
|
555
|
+
request_set_id: task.id,
|
|
556
|
+
chat_record_id: request,
|
|
557
|
+
session_id: session,
|
|
558
|
+
parameters: params,
|
|
559
|
+
business: { product: site2?.businessProduct ?? "app", version: site2?.cosyVersion ?? COSY_VERSION, type: "agent", id: task.id, name: task.name ?? "", begin_at: task.begin, stage: task.answered ? "processing" : "start" },
|
|
560
|
+
agent_id: "agent_common",
|
|
561
|
+
task_id: "common",
|
|
562
|
+
session_type: site2?.sessionType ?? "app",
|
|
563
|
+
model_config: m.config,
|
|
564
|
+
system: [sys],
|
|
565
|
+
messages: [{ role: "system", content: [sys] }, ...cacheMarked(qoderMessages(chat2.messages ?? []))]
|
|
566
|
+
};
|
|
567
|
+
if (tools.length)
|
|
568
|
+
body.tools = tools.map((t) => ({
|
|
569
|
+
type: "function",
|
|
570
|
+
function: { name: t.function.name, description: t.function.description ?? "", parameters: typedUnions(t.function.parameters) ?? { type: "object", properties: {} } }
|
|
571
|
+
}));
|
|
572
|
+
return body;
|
|
573
|
+
}
|
|
574
|
+
const QUEUED = "10605";
|
|
575
|
+
const QUOTA_CODES = /* @__PURE__ */ new Set(["110", "112", "113", "114", "115", "116", "117", "118", "119", "122"]);
|
|
576
|
+
const REQUEST_CODES = /* @__PURE__ */ new Set(["406", "416", "430", "80411", "80412", "100400", "100401", "100403"]);
|
|
577
|
+
const BUSY_WAIT = 30;
|
|
578
|
+
const UPSTREAM_FAILED = /\berror in upstream response\b/i;
|
|
579
|
+
function failure(status, text) {
|
|
580
|
+
if (status < 400 || status > 599) status = 502;
|
|
581
|
+
let msg = String(text ?? "").trim();
|
|
582
|
+
let code = "";
|
|
583
|
+
let queue;
|
|
584
|
+
try {
|
|
585
|
+
let j = JSON.parse(msg);
|
|
586
|
+
const outerCode = j?.code == null ? "" : String(j.code);
|
|
587
|
+
if (["", "401", "403"].includes(outerCode) && typeof j?.message === "string") {
|
|
588
|
+
try {
|
|
589
|
+
const inner = JSON.parse(j.message.trim());
|
|
590
|
+
if (inner && !Array.isArray(inner) && String(inner.code) === QUEUED) j = inner;
|
|
591
|
+
} catch {
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
code = j?.code == null ? "" : String(j.code);
|
|
595
|
+
if (j?.message) msg = String(j.message);
|
|
596
|
+
const d = typeof j?.details === "string" ? JSON.parse(j.details.trim()) : j?.details;
|
|
597
|
+
if (d?.error?.message) msg += ": " + d.error.message;
|
|
598
|
+
queue = j?.queue && typeof j.queue === "object" ? j.queue : void 0;
|
|
599
|
+
if (!queue && typeof j?.message === "string" && j.message.trim().startsWith("{")) {
|
|
600
|
+
const q = JSON.parse(j.message.trim());
|
|
601
|
+
if (q && typeof q === "object" && ("isQueued" in q || "serviceAvailable" in q || "retryAfterSeconds" in q)) queue = q;
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
msg ||= STATUS_CODES[status] ?? `HTTP ${status}`;
|
|
606
|
+
if (code === QUEUED || queue?.isQueued === true || queue?.serviceAvailable === false) {
|
|
607
|
+
const n = Number(queue?.retryAfterSeconds);
|
|
608
|
+
const wait = Number.isFinite(n) && n > 0 ? Math.ceil(n) : BUSY_WAIT;
|
|
609
|
+
const model = queue?.modelKey ? ` for ${queue.modelKey}` : "";
|
|
610
|
+
const ahead = Number(queue?.queueCount) > 0 ? `, ${Math.floor(Number(queue.queueCount))} ahead` : "";
|
|
611
|
+
const tier = queue?.queueType ? ` (this plan's queue: ${queue.queueType}${ahead})` : ahead ? ` (${ahead.slice(2)})` : "";
|
|
612
|
+
const what = queue?.isQueued === true ? "queued the request" : "is busy";
|
|
613
|
+
return { status: 503, message: `Qoder ${what}${model}${tier}; try again in ${wait}s`, retryAfter: wait, signIn: "", queue: queue ?? {} };
|
|
614
|
+
}
|
|
615
|
+
if (QUOTA_CODES.has(code)) return { status: 429, message: "usage limit reached: " + msg, signIn: "" };
|
|
616
|
+
if (REQUEST_CODES.has(code)) return { status: 400, message: `Qoder refused the request (${code}): ${msg}`, signIn: "" };
|
|
617
|
+
if (UPSTREAM_FAILED.test(msg)) return { status: 502, message: msg, signIn: "" };
|
|
618
|
+
if (status === 401 || status === 403) {
|
|
619
|
+
const said = msg && msg !== STATUS_CODES[status] ? ` (Qoder said ${status}: ${msg.slice(0, 300)})` : "";
|
|
620
|
+
return { status: 401, message: "the sign-in lapsed \u2014 sign in again" + said };
|
|
621
|
+
}
|
|
622
|
+
if (status === 429 || msg.toLowerCase().includes("quota")) return { status: 429, message: "usage limit reached: " + msg };
|
|
623
|
+
return { status, message: msg };
|
|
624
|
+
}
|
|
625
|
+
const errorResponse = ({ status, message, signIn = status === 401 ? "kept" : void 0, retryAfter }) => new Response(JSON.stringify({ error: { message, type: "qoder_error", code: status }, ...status === 503 && retryAfter > 0 ? { retry_after: Math.ceil(Number(retryAfter)) } : {} }), {
|
|
626
|
+
status,
|
|
627
|
+
headers: {
|
|
628
|
+
"Content-Type": "application/json",
|
|
629
|
+
...signIn ? { "X-Magpie-Sign-In": signIn } : {},
|
|
630
|
+
...retryAfter > 0 ? { "Retry-After": String(retryAfter) } : {}
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
const QUEUE_STATUS_PATH = "/algo/api/v2/service/ask/queue/status";
|
|
634
|
+
const QUEUE = {
|
|
635
|
+
budget: 4 * 60 * 1e3,
|
|
636
|
+
// the longest a request is held, its first answer's wait included
|
|
637
|
+
shortest: 500,
|
|
638
|
+
longest: 3e4,
|
|
639
|
+
unsaid: 3e4,
|
|
640
|
+
// a pause when the queue gives no retryAfterSeconds
|
|
641
|
+
recoveries: 10,
|
|
642
|
+
// the queue waited out this many times, at most, for one request
|
|
643
|
+
failures: 3,
|
|
644
|
+
// status asks failing in a row that end the wait
|
|
645
|
+
ask: 3e4,
|
|
646
|
+
// the longest one status ask may take
|
|
647
|
+
now: () => now(),
|
|
648
|
+
pause: (ms, signal) => new Promise((ok, no) => {
|
|
649
|
+
if (signal?.aborted) return no(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
650
|
+
const t = setTimeout(() => {
|
|
651
|
+
signal?.removeEventListener("abort", stop);
|
|
652
|
+
ok();
|
|
653
|
+
}, ms);
|
|
654
|
+
const stop = () => {
|
|
655
|
+
clearTimeout(t);
|
|
656
|
+
no(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
657
|
+
};
|
|
658
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
659
|
+
})
|
|
660
|
+
};
|
|
661
|
+
function queuePause(seconds) {
|
|
662
|
+
const n = Number(seconds);
|
|
663
|
+
const ms = Number.isFinite(n) ? n * 1e3 : QUEUE.unsaid;
|
|
664
|
+
return Math.min(QUEUE.longest, Math.max(QUEUE.shortest, ms));
|
|
665
|
+
}
|
|
666
|
+
class QueueEnd extends Error {
|
|
667
|
+
}
|
|
668
|
+
function queueState(text) {
|
|
669
|
+
let root;
|
|
670
|
+
try {
|
|
671
|
+
root = JSON.parse(text);
|
|
672
|
+
} catch {
|
|
673
|
+
return void 0;
|
|
674
|
+
}
|
|
675
|
+
const todo = [root];
|
|
676
|
+
const seen = /* @__PURE__ */ new Set();
|
|
677
|
+
while (todo.length) {
|
|
678
|
+
const o = todo.shift();
|
|
679
|
+
if (!o || typeof o !== "object" || Array.isArray(o) || seen.has(o)) continue;
|
|
680
|
+
seen.add(o);
|
|
681
|
+
const flag = (v) => typeof v === "boolean" ? v : v === "true" ? true : v === "false" ? false : void 0;
|
|
682
|
+
const isQueued = flag(o.isQueued);
|
|
683
|
+
const serviceAvailable = flag(o.serviceAvailable);
|
|
684
|
+
if (isQueued !== void 0 || serviceAvailable !== void 0) {
|
|
685
|
+
const num = (v) => v == null || v === "" || !Number.isFinite(Number(v)) ? void 0 : Number(v);
|
|
686
|
+
const str = (v) => typeof v === "string" && v ? v : void 0;
|
|
687
|
+
return { isQueued, serviceAvailable, modelKey: str(o.modelKey), queueType: str(o.queueType), retryAfterSeconds: num(o.retryAfterSeconds) };
|
|
688
|
+
}
|
|
689
|
+
for (const k of ["data", "result", "message", "body"]) {
|
|
690
|
+
const v = o[k];
|
|
691
|
+
if (v && typeof v === "object") todo.push(v);
|
|
692
|
+
else if (typeof v === "string")
|
|
693
|
+
try {
|
|
694
|
+
todo.push(JSON.parse(v));
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return void 0;
|
|
700
|
+
}
|
|
701
|
+
async function queueStatus(api, cred, { requestSetId, modelKey, queueType, requestId, sessionId }, signal, deadline, site2) {
|
|
702
|
+
const q = new URLSearchParams({ requestSetId, modelKey });
|
|
703
|
+
if (queueType) q.set("queueType", queueType);
|
|
704
|
+
const url = `${api}${QUEUE_STATUS_PATH}?${q}`;
|
|
705
|
+
const headers = { ...cosyHeaders(url, cred, "", site2), "X-Request-ID": requestId, "X-Session-ID": sessionId };
|
|
706
|
+
delete headers["Content-Type"];
|
|
707
|
+
const left = Math.min(QUEUE.ask, deadline - QUEUE.now());
|
|
708
|
+
if (left <= 0) throw new QueueEnd("the queue's wait ran out");
|
|
709
|
+
const res = await fetch2(url, { headers, signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(left)]) : AbortSignal.timeout(left) });
|
|
710
|
+
const text = await res.text();
|
|
711
|
+
checkAbort(signal);
|
|
712
|
+
if (res.status === 404) throw new QueueEnd("Qoder has no queue status here");
|
|
713
|
+
if (!res.ok) throw new Error(`queue status: HTTP ${res.status}`);
|
|
714
|
+
const st = queueState(text);
|
|
715
|
+
if (!st) throw new QueueEnd("the queue status said nothing of the queue");
|
|
716
|
+
return st;
|
|
717
|
+
}
|
|
718
|
+
async function queueWait(api, cred, ask, first, signal, deadline, site2) {
|
|
719
|
+
let q = first ?? {};
|
|
720
|
+
let asked = false;
|
|
721
|
+
let failed = 0;
|
|
722
|
+
for (; ; ) {
|
|
723
|
+
checkAbort(signal);
|
|
724
|
+
if (QUEUE.now() >= deadline) return false;
|
|
725
|
+
if (asked) await QUEUE.pause(Math.min(queuePause(q.retryAfterSeconds), Math.max(0, deadline - QUEUE.now())), signal);
|
|
726
|
+
asked = true;
|
|
727
|
+
if (QUEUE.now() >= deadline) return false;
|
|
728
|
+
let st;
|
|
729
|
+
try {
|
|
730
|
+
st = await queueStatus(api, cred, { ...ask, modelKey: q.modelKey ?? ask.modelKey, queueType: q.queueType ?? ask.queueType }, signal, deadline, site2);
|
|
731
|
+
} catch (e) {
|
|
732
|
+
checkAbort(signal);
|
|
733
|
+
if (e instanceof QueueEnd) return false;
|
|
734
|
+
if (++failed >= QUEUE.failures) return false;
|
|
735
|
+
q = { ...q, retryAfterSeconds: void 0 };
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
failed = 0;
|
|
739
|
+
q = { ...st, modelKey: st.modelKey ?? q.modelKey, queueType: st.queueType ?? q.queueType };
|
|
740
|
+
if (st.isQueued === false) {
|
|
741
|
+
if (st.retryAfterSeconds > 0) await QUEUE.pause(Math.min(queuePause(st.retryAfterSeconds), Math.max(0, deadline - QUEUE.now())), signal);
|
|
742
|
+
return QUEUE.now() < deadline;
|
|
743
|
+
}
|
|
744
|
+
if (st.serviceAvailable === false || st.isQueued === true) continue;
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const CALL_OPEN = "<tool_call>";
|
|
749
|
+
const CALL_CLOSE = "</tool_call>";
|
|
750
|
+
const FUNC = /\x3cfunction=([^>]+)\x3e([\s\S]*?)\x3c\/function\x3e/;
|
|
751
|
+
const PARAM = /\x3cparameter=([^>]+)\x3e([\s\S]*?)\x3c\/parameter\x3e/g;
|
|
752
|
+
function parseCall(v, tools = []) {
|
|
753
|
+
try {
|
|
754
|
+
const f = JSON.parse(v.trim());
|
|
755
|
+
if (String(f?.name ?? "").trim() && f.arguments && typeof f.arguments === "object" && !Array.isArray(f.arguments))
|
|
756
|
+
return { name: f.name.trim(), args: JSON.stringify(f.arguments) };
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
const m = FUNC.exec(v);
|
|
760
|
+
if (!m || !m[1].trim()) return null;
|
|
761
|
+
const name = m[1].trim();
|
|
762
|
+
const props = propsOf(tools, name);
|
|
763
|
+
const args = {};
|
|
764
|
+
for (const pm of m[2].matchAll(PARAM)) {
|
|
765
|
+
const key = pm[1].trim();
|
|
766
|
+
if (!key) continue;
|
|
767
|
+
args[key] = valueOf(pm[2].trim(), props[key]);
|
|
768
|
+
}
|
|
769
|
+
return { name, args: JSON.stringify(args) };
|
|
770
|
+
}
|
|
771
|
+
const propsOf = (tools, name) => (tools.find((t) => (t.function ?? t)?.name === name)?.function ?? {}).parameters?.properties ?? {};
|
|
772
|
+
function valueOf(v, prop) {
|
|
773
|
+
if (prop?.type === "string") return v;
|
|
774
|
+
try {
|
|
775
|
+
return JSON.parse(v.trim());
|
|
776
|
+
} catch {
|
|
777
|
+
return v;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const XML_AT = /<(?:antml:)?(?:function_calls>|invoke\s+name\s*=|function=)/;
|
|
781
|
+
const XML_HOLD = /<(?:antml:)?(?:invoke\s[^<>]{0,300}|function=[^<>]{0,300})$/;
|
|
782
|
+
const XML_STARTS = ["<invoke ", "<antml:invoke ", "<function="];
|
|
783
|
+
const XML_MARKS = ["<function_calls>", "<antml:function_calls>", ...XML_STARTS];
|
|
784
|
+
const XML_NEXT = /<(?:antml:)?invoke\s+name\s*=|<function=/;
|
|
785
|
+
const XML_PARAM = /<(?:antml:)?parameter(?:\s+name\s*=\s*"([^"]+)"|=([^\s>]+))\s*>/g;
|
|
786
|
+
const XML_CLOSES = ["</function_calls>", "</antml:function_calls>", "</invoke>", "</antml:invoke>", "</function>"];
|
|
787
|
+
const XML_LEFT = /^(?:\s+|<\/(?:antml:)?(?:function_calls|invoke|function)>)+/;
|
|
788
|
+
function xmlArgs(s, props) {
|
|
789
|
+
const opens = [...s.matchAll(XML_PARAM)];
|
|
790
|
+
const args = {};
|
|
791
|
+
let closed = opens.length > 0;
|
|
792
|
+
opens.forEach((m, k) => {
|
|
793
|
+
let v = s.slice(m.index + m[0].length, k + 1 < opens.length ? opens[k + 1].index : s.length);
|
|
794
|
+
const c = v.search(/<\/(?:antml:)?parameter>/);
|
|
795
|
+
if (c >= 0) v = v.slice(0, c);
|
|
796
|
+
else closed = false;
|
|
797
|
+
args[m[1] ?? m[2]] = valueOf(v.replace(/^\r?\n|\r?\n$/g, ""), props[m[1] ?? m[2]]);
|
|
798
|
+
});
|
|
799
|
+
return { args, closed };
|
|
800
|
+
}
|
|
801
|
+
function xmlCall(s, end, tools) {
|
|
802
|
+
const w = s.match(/^<(?:antml:)?function_calls>/);
|
|
803
|
+
if (w) {
|
|
804
|
+
const rest = s.slice(w[0].length).replace(/^\s+/, "");
|
|
805
|
+
if (!end && (!rest || XML_STARTS.some((k) => k.startsWith(rest)))) return { wait: true };
|
|
806
|
+
s = rest;
|
|
807
|
+
}
|
|
808
|
+
let m = s.match(/^<(?:antml:)?invoke\s+name\s*=\s*"([^"<>]+)"\s*>/);
|
|
809
|
+
let closeRe = /<\/(?:antml:)?invoke\s*>/;
|
|
810
|
+
if (!m) {
|
|
811
|
+
m = s.match(/^<function=([^\s<>]+?)\s*>/);
|
|
812
|
+
closeRe = /<\/function\s*>/;
|
|
813
|
+
}
|
|
814
|
+
if (!m) {
|
|
815
|
+
if (!end && (XML_STARTS.some((k) => k.startsWith(s)) || /^<(?:antml:)?(?:invoke\s|function=)[^<>]{0,300}$/.test(s))) return { wait: true };
|
|
816
|
+
return { text: true };
|
|
817
|
+
}
|
|
818
|
+
const name = m[1].trim();
|
|
819
|
+
if (!tools.some((t) => (t.function ?? t)?.name === name)) return { text: true };
|
|
820
|
+
const body = s.slice(m[0].length);
|
|
821
|
+
const c = body.match(closeRe);
|
|
822
|
+
const n = body.search(XML_NEXT);
|
|
823
|
+
let stop = c ? c.index : -1;
|
|
824
|
+
if (n >= 0 && (stop < 0 || n < stop)) stop = n;
|
|
825
|
+
if (stop < 0) {
|
|
826
|
+
if (!end) return { wait: true };
|
|
827
|
+
stop = body.length;
|
|
828
|
+
}
|
|
829
|
+
const whole = !!c && c.index === stop;
|
|
830
|
+
const p = xmlArgs(body.slice(0, stop), propsOf(tools, name));
|
|
831
|
+
if (!whole && !p.closed) {
|
|
832
|
+
if (!/<(?:antml:)?parameter\b/.test(body.slice(0, stop))) return { text: true };
|
|
833
|
+
return { error: CUT_SHORT };
|
|
834
|
+
}
|
|
835
|
+
return { call: { name, args: JSON.stringify(p.args) }, rest: whole ? body.slice(stop + c[0].length) : body.slice(stop) };
|
|
836
|
+
}
|
|
837
|
+
function quoted(text) {
|
|
838
|
+
if ((text.match(/```/g)?.length ?? 0) % 2) return true;
|
|
839
|
+
const line = text.slice(text.lastIndexOf("\n") + 1).replace(/```/g, "");
|
|
840
|
+
return (line.match(/`/g)?.length ?? 0) % 2 === 1;
|
|
841
|
+
}
|
|
842
|
+
const callish = (v) => v.trimStart().startsWith("{") || v.includes("<function=");
|
|
843
|
+
const CUT_SHORT = "the model's tool call, written as text, was cut short; it is not sent on as the answer";
|
|
844
|
+
const UNREADABLE = "the model's tool call, written as text, can't be read; it is not sent on as the answer";
|
|
845
|
+
class Splitter {
|
|
846
|
+
textBuf = "";
|
|
847
|
+
callBuf = "";
|
|
848
|
+
inCall = false;
|
|
849
|
+
sawTool = false;
|
|
850
|
+
after = false;
|
|
851
|
+
// a bare call just ended: the closes and blanks after it are dropped
|
|
852
|
+
sent = "";
|
|
853
|
+
// the text given out so far, to tell a block in code from a call
|
|
854
|
+
constructor(tools = []) {
|
|
855
|
+
this.tools = tools.filter((t) => (t?.function ?? t)?.name);
|
|
856
|
+
}
|
|
857
|
+
text(out, t) {
|
|
858
|
+
this.sent += t;
|
|
859
|
+
out.push({ text: t });
|
|
860
|
+
}
|
|
861
|
+
// bare reads text that may hold a bare call, comb starting at its tag x:
|
|
862
|
+
// the input left to read, null when comb is held for more, or false
|
|
863
|
+
// when the turn ended in an error
|
|
864
|
+
bare(out, comb, x, end) {
|
|
865
|
+
const head = comb.slice(0, x);
|
|
866
|
+
const at = comb.slice(x);
|
|
867
|
+
const opener = at.match(/^<[^<>]*>?/)[0];
|
|
868
|
+
if (quoted(this.sent + head)) {
|
|
869
|
+
this.text(out, head + opener);
|
|
870
|
+
return at.slice(opener.length);
|
|
871
|
+
}
|
|
872
|
+
const b = xmlCall(at, end, this.tools);
|
|
873
|
+
if (b.wait) {
|
|
874
|
+
if (head) this.text(out, head);
|
|
875
|
+
this.textBuf = at;
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
if (b.text) {
|
|
879
|
+
this.text(out, head + opener);
|
|
880
|
+
return at.slice(opener.length);
|
|
881
|
+
}
|
|
882
|
+
if (head) this.text(out, head);
|
|
883
|
+
if (b.error) {
|
|
884
|
+
out.push({ error: b.error });
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
this.sawTool = true;
|
|
888
|
+
out.push({ call: b.call });
|
|
889
|
+
this.after = true;
|
|
890
|
+
return b.rest;
|
|
891
|
+
}
|
|
892
|
+
// left drops what is left after a bare call; null while it may still be
|
|
893
|
+
// one of its closes
|
|
894
|
+
left(comb, end) {
|
|
895
|
+
const rest = comb.replace(XML_LEFT, "");
|
|
896
|
+
if (!end && (!rest || XML_CLOSES.some((c) => c.length > rest.length && c.startsWith(rest)))) return null;
|
|
897
|
+
this.after = false;
|
|
898
|
+
return rest;
|
|
899
|
+
}
|
|
900
|
+
feed(input) {
|
|
901
|
+
const out = [];
|
|
902
|
+
while (input) {
|
|
903
|
+
if (!this.inCall) {
|
|
904
|
+
let comb2 = this.textBuf + input;
|
|
905
|
+
this.textBuf = "";
|
|
906
|
+
if (this.after) {
|
|
907
|
+
const rest = this.left(comb2, false);
|
|
908
|
+
if (rest === null) {
|
|
909
|
+
this.textBuf = comb2;
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
comb2 = rest;
|
|
913
|
+
if (!comb2) break;
|
|
914
|
+
}
|
|
915
|
+
const i = comb2.indexOf(CALL_OPEN);
|
|
916
|
+
const x = this.tools.length ? comb2.search(XML_AT) : -1;
|
|
917
|
+
if (x >= 0 && (i < 0 || x < i)) {
|
|
918
|
+
const next = this.bare(out, comb2, x, false);
|
|
919
|
+
if (next === false) return out;
|
|
920
|
+
if (next === null) break;
|
|
921
|
+
input = next;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (i < 0) {
|
|
925
|
+
let keep = 0;
|
|
926
|
+
for (const mark of this.tools.length ? [CALL_OPEN, ...XML_MARKS] : [CALL_OPEN])
|
|
927
|
+
for (let n = Math.min(mark.length - 1, comb2.length); n > keep; n--)
|
|
928
|
+
if (comb2.endsWith(mark.slice(0, n))) {
|
|
929
|
+
keep = n;
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
const h = this.tools.length ? comb2.search(XML_HOLD) : -1;
|
|
933
|
+
if (h >= 0) keep = Math.max(keep, comb2.length - h);
|
|
934
|
+
if (keep < comb2.length) this.text(out, comb2.slice(0, comb2.length - keep));
|
|
935
|
+
this.textBuf = comb2.slice(comb2.length - keep);
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
if (i > 0) this.text(out, comb2.slice(0, i));
|
|
939
|
+
input = comb2.slice(i + CALL_OPEN.length);
|
|
940
|
+
this.inCall = true;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const comb = this.callBuf + input;
|
|
944
|
+
this.callBuf = "";
|
|
945
|
+
const j = comb.indexOf(CALL_CLOSE);
|
|
946
|
+
if (j < 0) {
|
|
947
|
+
this.callBuf = comb;
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
const body = comb.slice(0, j);
|
|
951
|
+
const c = parseCall(body, this.tools);
|
|
952
|
+
if (c) {
|
|
953
|
+
this.sawTool = true;
|
|
954
|
+
out.push({ call: c });
|
|
955
|
+
} else if (callish(body) && !quoted(this.sent)) {
|
|
956
|
+
this.inCall = false;
|
|
957
|
+
out.push({ error: UNREADABLE });
|
|
958
|
+
return out;
|
|
959
|
+
} else this.text(out, CALL_OPEN + body + CALL_CLOSE);
|
|
960
|
+
input = comb.slice(j + CALL_CLOSE.length);
|
|
961
|
+
this.inCall = false;
|
|
962
|
+
}
|
|
963
|
+
return out;
|
|
964
|
+
}
|
|
965
|
+
flush() {
|
|
966
|
+
if (this.inCall) {
|
|
967
|
+
const body = this.callBuf;
|
|
968
|
+
this.inCall = false;
|
|
969
|
+
this.callBuf = "";
|
|
970
|
+
if (callish(body) && !quoted(this.sent)) {
|
|
971
|
+
const c = parseCall(body, this.tools);
|
|
972
|
+
if (!c) return [{ error: CUT_SHORT }];
|
|
973
|
+
this.sawTool = true;
|
|
974
|
+
return [{ call: c }];
|
|
975
|
+
}
|
|
976
|
+
const t = CALL_OPEN + body;
|
|
977
|
+
this.sent += t;
|
|
978
|
+
return [{ text: t }];
|
|
979
|
+
}
|
|
980
|
+
if (this.textBuf) {
|
|
981
|
+
let t = this.textBuf;
|
|
982
|
+
this.textBuf = "";
|
|
983
|
+
if (this.after) t = this.left(t, true);
|
|
984
|
+
const out = [];
|
|
985
|
+
while (t) {
|
|
986
|
+
const x = this.tools.length ? t.search(XML_AT) : -1;
|
|
987
|
+
if (x < 0) {
|
|
988
|
+
this.text(out, t);
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
const next = this.bare(out, t, x, true);
|
|
992
|
+
if (next === false) break;
|
|
993
|
+
t = this.after ? this.left(next, true) : next;
|
|
994
|
+
}
|
|
995
|
+
return out;
|
|
996
|
+
}
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
async function* sseLines(body, signal) {
|
|
1001
|
+
const dec = new TextDecoder();
|
|
1002
|
+
let buf = "";
|
|
1003
|
+
for await (const chunk of body) {
|
|
1004
|
+
checkAbort(signal);
|
|
1005
|
+
buf += dec.decode(chunk, { stream: true });
|
|
1006
|
+
let n;
|
|
1007
|
+
while ((n = buf.indexOf("\n")) >= 0) {
|
|
1008
|
+
yield buf.slice(0, n).trim();
|
|
1009
|
+
buf = buf.slice(n + 1);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
checkAbort(signal);
|
|
1013
|
+
if (buf.trim()) yield buf.trim();
|
|
1014
|
+
}
|
|
1015
|
+
async function* events(body, tools = [], signal) {
|
|
1016
|
+
const a = new Splitter(tools);
|
|
1017
|
+
let index = -1;
|
|
1018
|
+
const native = /* @__PURE__ */ new Map();
|
|
1019
|
+
const piece = (f) => f.call ? { tool: { index: ++index, id: "call_" + hexID(), name: f.call.name, args: f.call.args } } : f.error ? { error: { status: 502, message: f.error } } : { text: f.text };
|
|
1020
|
+
let usage2;
|
|
1021
|
+
let fr = "";
|
|
1022
|
+
let sawData = false;
|
|
1023
|
+
let parsed = false;
|
|
1024
|
+
let semantic = false;
|
|
1025
|
+
for await (const line of sseLines(body, signal)) {
|
|
1026
|
+
if (!line.startsWith("data:")) continue;
|
|
1027
|
+
sawData = true;
|
|
1028
|
+
const payload = line.slice(5).trim();
|
|
1029
|
+
let env;
|
|
1030
|
+
try {
|
|
1031
|
+
env = JSON.parse(payload);
|
|
1032
|
+
} catch {
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
const inner = typeof env?.body === "string" ? env.body : "";
|
|
1036
|
+
if (env?.statusCodeValue != null && env.statusCodeValue !== 200) {
|
|
1037
|
+
yield { error: failure(Number(env.statusCodeValue), inner || payload) };
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (inner.trim() === "[DONE]") break;
|
|
1041
|
+
let chunk;
|
|
1042
|
+
try {
|
|
1043
|
+
chunk = JSON.parse(inner.trim());
|
|
1044
|
+
} catch {
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
parsed = true;
|
|
1048
|
+
const delta = chunk?.choices?.[0]?.delta ?? {};
|
|
1049
|
+
const reasoning = delta.reasoning_content;
|
|
1050
|
+
if (typeof reasoning === "string" && reasoning) {
|
|
1051
|
+
semantic = true;
|
|
1052
|
+
yield { reasoning };
|
|
1053
|
+
}
|
|
1054
|
+
if (typeof delta.content === "string")
|
|
1055
|
+
for (const f of a.feed(delta.content)) {
|
|
1056
|
+
if (!f.call && !f.error && !f.text) continue;
|
|
1057
|
+
const e = piece(f);
|
|
1058
|
+
if (e.text || e.tool) semantic = true;
|
|
1059
|
+
yield e;
|
|
1060
|
+
if (e.error) return;
|
|
1061
|
+
}
|
|
1062
|
+
for (const tc of delta.tool_calls ?? []) {
|
|
1063
|
+
const i = Number(tc.index ?? 0);
|
|
1064
|
+
const name = typeof tc.function?.name === "string" ? tc.function.name.trim() : "";
|
|
1065
|
+
const args = tc.function?.arguments ?? "";
|
|
1066
|
+
if (!tc.id && !name && !args) continue;
|
|
1067
|
+
let call = native.get(i);
|
|
1068
|
+
if (!call) {
|
|
1069
|
+
call = { id: "", args: "" };
|
|
1070
|
+
native.set(i, call);
|
|
1071
|
+
}
|
|
1072
|
+
if (tc.id) call.id = tc.id;
|
|
1073
|
+
if (call.index === void 0) {
|
|
1074
|
+
call.args += args;
|
|
1075
|
+
if (!name) continue;
|
|
1076
|
+
for (const f of a.flush()) {
|
|
1077
|
+
const e = piece(f);
|
|
1078
|
+
yield e;
|
|
1079
|
+
if (e.error) return;
|
|
1080
|
+
}
|
|
1081
|
+
a.sawTool = true;
|
|
1082
|
+
call.index = ++index;
|
|
1083
|
+
semantic = true;
|
|
1084
|
+
yield { tool: { index: call.index, id: call.id || "call_" + hexID(), name, args: call.args } };
|
|
1085
|
+
} else if (args) yield { args: { index: call.index, text: args } };
|
|
1086
|
+
}
|
|
1087
|
+
fr = chunk?.choices?.[0]?.finish_reason || fr;
|
|
1088
|
+
const u = chunk?.usage;
|
|
1089
|
+
if (u) {
|
|
1090
|
+
usage2 = {
|
|
1091
|
+
prompt_tokens: u.prompt_tokens ?? 0,
|
|
1092
|
+
completion_tokens: u.completion_tokens ?? 0,
|
|
1093
|
+
total_tokens: (u.prompt_tokens ?? 0) + (u.completion_tokens ?? 0),
|
|
1094
|
+
...u.prompt_tokens_details?.cached_tokens ? { prompt_tokens_details: { cached_tokens: u.prompt_tokens_details.cached_tokens } } : {},
|
|
1095
|
+
...u.completion_tokens_details?.reasoning_tokens ? { completion_tokens_details: { reasoning_tokens: u.completion_tokens_details.reasoning_tokens } } : {}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
for (const f of a.flush()) {
|
|
1100
|
+
const e = piece(f);
|
|
1101
|
+
if (e.text || e.tool) semantic = true;
|
|
1102
|
+
yield e;
|
|
1103
|
+
if (e.error) return;
|
|
1104
|
+
}
|
|
1105
|
+
if ([...native.values()].some((call) => call.index === void 0)) {
|
|
1106
|
+
yield { error: { status: 502, message: "the model's native tool call has no name; it is not sent on as a tool" } };
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (!sawData) return;
|
|
1110
|
+
if (!parsed || !semantic) {
|
|
1111
|
+
yield { error: { status: 502, message: "Qoder sent an unreadable answer" } };
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
yield { stop: a.sawTool ? "tool_calls" : fr === "length" ? "length" : "stop", usage: usage2 };
|
|
1115
|
+
}
|
|
1116
|
+
async function opened(res, tools, signal) {
|
|
1117
|
+
const it = events(res.body, tools, signal);
|
|
1118
|
+
const first = await it.next();
|
|
1119
|
+
checkAbort(signal);
|
|
1120
|
+
return { it, first };
|
|
1121
|
+
}
|
|
1122
|
+
async function answer({ it, first }, chat2, servedName = chat2.model, signal) {
|
|
1123
|
+
const id = "chatcmpl-" + hexID();
|
|
1124
|
+
const created = Math.floor(now() / 1e3);
|
|
1125
|
+
if (first.value?.error) return errorResponse(first.value.error);
|
|
1126
|
+
if (first.done) return errorResponse({ status: 502, message: "Qoder ended without an answer" });
|
|
1127
|
+
if (!chat2.stream) {
|
|
1128
|
+
const msg = { role: "assistant", content: "" };
|
|
1129
|
+
let reasoning = "";
|
|
1130
|
+
const calls = [];
|
|
1131
|
+
let stop = "stop";
|
|
1132
|
+
let usage2;
|
|
1133
|
+
for (let r = first; !r.done; r = await it.next()) {
|
|
1134
|
+
checkAbort(signal);
|
|
1135
|
+
const e = r.value;
|
|
1136
|
+
if (e.error) return errorResponse(e.error);
|
|
1137
|
+
if (e.text) msg.content += e.text;
|
|
1138
|
+
if (e.reasoning) reasoning += e.reasoning;
|
|
1139
|
+
if (e.tool) calls.push({ id: e.tool.id, type: "function", function: { name: e.tool.name, arguments: e.tool.args } });
|
|
1140
|
+
if (e.args) calls[e.args.index].function.arguments += e.args.text;
|
|
1141
|
+
if (e.stop) [stop, usage2] = [e.stop, e.usage];
|
|
1142
|
+
}
|
|
1143
|
+
checkAbort(signal);
|
|
1144
|
+
if (reasoning) msg.reasoning_content = reasoning;
|
|
1145
|
+
if (calls.length) msg.tool_calls = calls;
|
|
1146
|
+
return Response.json({ id, object: "chat.completion", created, model: servedName, choices: [{ index: 0, message: msg, finish_reason: stop }], ...usage2 ? { usage: usage2 } : {} });
|
|
1147
|
+
}
|
|
1148
|
+
const enc = new TextEncoder();
|
|
1149
|
+
const chunk = (delta, finish_reason = null, extra = {}) => enc.encode(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model: servedName, choices: [{ index: 0, delta, finish_reason }], ...extra })}
|
|
1150
|
+
|
|
1151
|
+
`);
|
|
1152
|
+
let pending = first;
|
|
1153
|
+
let began = false;
|
|
1154
|
+
const stream = new ReadableStream({
|
|
1155
|
+
async pull(ctl) {
|
|
1156
|
+
const r = pending ?? await it.next();
|
|
1157
|
+
checkAbort(signal);
|
|
1158
|
+
pending = null;
|
|
1159
|
+
if (!began) {
|
|
1160
|
+
began = true;
|
|
1161
|
+
ctl.enqueue(chunk({ role: "assistant", content: "" }));
|
|
1162
|
+
}
|
|
1163
|
+
if (r.done) {
|
|
1164
|
+
ctl.enqueue(enc.encode("data: [DONE]\n\n"));
|
|
1165
|
+
return ctl.close();
|
|
1166
|
+
}
|
|
1167
|
+
const e = r.value;
|
|
1168
|
+
if (e.text) ctl.enqueue(chunk({ content: e.text }));
|
|
1169
|
+
else if (e.reasoning) ctl.enqueue(chunk({ reasoning_content: e.reasoning }));
|
|
1170
|
+
else if (e.tool) ctl.enqueue(chunk({ tool_calls: [{ index: e.tool.index, id: e.tool.id, type: "function", function: { name: e.tool.name, arguments: e.tool.args } }] }));
|
|
1171
|
+
else if (e.args) ctl.enqueue(chunk({ tool_calls: [{ index: e.args.index, function: { arguments: e.args.text } }] }));
|
|
1172
|
+
else if (e.stop) ctl.enqueue(chunk({}, e.stop, e.usage ? { usage: e.usage } : {}));
|
|
1173
|
+
else if (e.error) {
|
|
1174
|
+
ctl.enqueue(enc.encode(`data: ${JSON.stringify({ error: { message: e.error.message, code: e.error.status } })}
|
|
1175
|
+
|
|
1176
|
+
`));
|
|
1177
|
+
ctl.close();
|
|
1178
|
+
}
|
|
1179
|
+
},
|
|
1180
|
+
cancel() {
|
|
1181
|
+
it.return?.();
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
return new Response(stream, { status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" } });
|
|
1185
|
+
}
|
|
1186
|
+
class UsageStatus extends Error {
|
|
1187
|
+
constructor(status) {
|
|
1188
|
+
super(`qoder usage: upstream HTTP ${status}`);
|
|
1189
|
+
this.status = status;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
async function fetchUsage(site2, deviceToken, signal) {
|
|
1193
|
+
if (!String(deviceToken ?? "").trim()) throw new Error("qoder usage: missing device token");
|
|
1194
|
+
let res;
|
|
1195
|
+
try {
|
|
1196
|
+
res = await fetch2(site2.openapi + "/sash/api/v2/me/usage", {
|
|
1197
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${deviceToken}`, "Cosy-ClientType": "10", "User-Agent": "Qoder" },
|
|
1198
|
+
signal: combine(signal, AbortSignal.timeout(2e4))
|
|
1199
|
+
});
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
checkAbort(signal);
|
|
1202
|
+
throw new Error(`qoder usage: request failed: ${e?.message ?? e}`);
|
|
1203
|
+
}
|
|
1204
|
+
if (res.status !== 200) throw new UsageStatus(res.status);
|
|
1205
|
+
let env;
|
|
1206
|
+
try {
|
|
1207
|
+
env = JSON.parse(await res.text());
|
|
1208
|
+
} catch (e) {
|
|
1209
|
+
checkAbort(signal);
|
|
1210
|
+
throw new Error(`qoder usage: decode response: ${e?.message ?? e}`);
|
|
1211
|
+
}
|
|
1212
|
+
checkAbort(signal);
|
|
1213
|
+
if (env?.displayMode !== "qoder" && env?.displayMode !== "enterprise") throw new Error("qoder usage: unknown display mode");
|
|
1214
|
+
if (env.displayMode === "qoder" && (env.qoderUsage === void 0 || env.qoderUsage === null)) throw new Error("qoder usage: missing quota data");
|
|
1215
|
+
return env;
|
|
1216
|
+
}
|
|
1217
|
+
function gfmt(n) {
|
|
1218
|
+
const [m, e] = n.toExponential().split("e");
|
|
1219
|
+
const x = Number(e);
|
|
1220
|
+
if (x < -4 || x >= 6) return `${m}e${x < 0 ? "-" : "+"}${String(Math.abs(x)).padStart(2, "0")}`;
|
|
1221
|
+
return String(n);
|
|
1222
|
+
}
|
|
1223
|
+
const RFC3339 = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(Z|[+-]\d\d:\d\d)$/i;
|
|
1224
|
+
function when(v) {
|
|
1225
|
+
if (typeof v === "string" && RFC3339.test(v) && !isNaN(Date.parse(v))) return new Date(v).toISOString();
|
|
1226
|
+
const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v.trim()) : NaN;
|
|
1227
|
+
if (!isFinite(n) || n <= 0) return void 0;
|
|
1228
|
+
return new Date(Math.trunc(n < 1e12 ? n * 1e3 : n)).toISOString();
|
|
1229
|
+
}
|
|
1230
|
+
function parseUsage(env) {
|
|
1231
|
+
if (env.displayMode === "enterprise") return { plan: "Enterprise" };
|
|
1232
|
+
const u = env.qoderUsage;
|
|
1233
|
+
if (env.displayMode !== "qoder" || !u || typeof u !== "object" || Array.isArray(u)) return { error: "Qoder: missing quota data" };
|
|
1234
|
+
const field2 = (a, b) => u[a] !== void 0 ? u[a] : u[b];
|
|
1235
|
+
const out = { windows: [] };
|
|
1236
|
+
const plan = field2("userType", "user_type");
|
|
1237
|
+
if (typeof plan === "string") out.plan = plan;
|
|
1238
|
+
const until = when(field2("expiresAt", "expires_at"));
|
|
1239
|
+
if (until) out.until = until;
|
|
1240
|
+
const num = (v) => v === void 0 || v === null || typeof v === "number";
|
|
1241
|
+
const str = (v) => v === void 0 || v === null || typeof v === "string";
|
|
1242
|
+
const add = (b, name) => {
|
|
1243
|
+
if (b === null || typeof b !== "object" || Array.isArray(b)) return;
|
|
1244
|
+
if (![b.total, b.cap, b.used, b.remaining].every(num) || !str(b.name) || !str(b.unit)) return;
|
|
1245
|
+
const total = b.total ?? b.cap;
|
|
1246
|
+
if (total == null || total <= 0 || b.used == null && b.remaining == null) return;
|
|
1247
|
+
const used = b.used != null ? b.used : total - b.remaining;
|
|
1248
|
+
if (used < 0) return;
|
|
1249
|
+
out.windows.push({ name: b.name || name, used: Math.min(100, 100 * used / total), display: `${gfmt(used)} / ${gfmt(total)} ${b.unit || "credits"}` });
|
|
1250
|
+
};
|
|
1251
|
+
add(field2("userQuota", "user_quota"), "Credits");
|
|
1252
|
+
add(field2("addOnQuota", "add_on_quota"), "Add-on credits");
|
|
1253
|
+
add(field2("orgResourcePackage", "org_resource_package"), "Shared credits");
|
|
1254
|
+
const dedicated = field2("dedicatedResourcePackages", "dedicated_resource_packages");
|
|
1255
|
+
for (const b of Array.isArray(dedicated) ? dedicated : []) add(b, "Dedicated credits");
|
|
1256
|
+
return out;
|
|
1257
|
+
}
|
|
1258
|
+
let lock = Promise.resolve();
|
|
1259
|
+
const locked = (fn) => {
|
|
1260
|
+
const run = lock.then(fn, fn);
|
|
1261
|
+
lock = run.catch(() => {
|
|
1262
|
+
});
|
|
1263
|
+
return run;
|
|
1264
|
+
};
|
|
1265
|
+
const spent = /* @__PURE__ */ new Map();
|
|
1266
|
+
const pruneSpent = () => {
|
|
1267
|
+
for (const [key, value] of spent) if (now() - value.at > KEEP_SPENT_MS) spent.delete(key);
|
|
1268
|
+
};
|
|
1269
|
+
const latest = (cred) => {
|
|
1270
|
+
pruneSpent();
|
|
1271
|
+
for (let i = 0; i < 16 && cred?.refresh; i++) {
|
|
1272
|
+
const entry = spent.get(cred.refresh);
|
|
1273
|
+
if (!entry) break;
|
|
1274
|
+
cred = { ...cred, ...entry.got };
|
|
1275
|
+
}
|
|
1276
|
+
return cred;
|
|
1277
|
+
};
|
|
1278
|
+
const spend = (refresh2, got) => {
|
|
1279
|
+
pruneSpent();
|
|
1280
|
+
spent.set(refresh2, { got, at: now() });
|
|
1281
|
+
return got;
|
|
1282
|
+
};
|
|
1283
|
+
const connectPAT = async (pat) => {
|
|
1284
|
+
if (typeof pat !== "string" || !pat.startsWith("pt-") || pat.length > 4096)
|
|
1285
|
+
throw new Error("PAT must start with pt- and be at most 4096 characters");
|
|
1286
|
+
const cred = await patSignIn(site, pat);
|
|
1287
|
+
checkAbort();
|
|
1288
|
+
return cred;
|
|
1289
|
+
};
|
|
1290
|
+
const renew = async (cred, signal) => {
|
|
1291
|
+
let jt;
|
|
1292
|
+
try {
|
|
1293
|
+
jt = await refreshJob(site, cred.refresh, cred.accountId || cred.email || cred.uid, signal);
|
|
1294
|
+
checkAbort(signal);
|
|
1295
|
+
} catch (e) {
|
|
1296
|
+
checkAbort(signal);
|
|
1297
|
+
if (!(e instanceof SignInGone) || !cred.pat) throw e;
|
|
1298
|
+
let exchanged;
|
|
1299
|
+
try {
|
|
1300
|
+
exchanged = await patSignIn(site, cred.pat, signal);
|
|
1301
|
+
checkAbort(signal);
|
|
1302
|
+
} catch (p) {
|
|
1303
|
+
checkAbort(signal);
|
|
1304
|
+
if (!p?.status) throw p;
|
|
1305
|
+
throw new SignInGone("Qoder refused the PAT \u2014 sign in again with a new one", true);
|
|
1306
|
+
}
|
|
1307
|
+
return spend(cred.refresh, { access: exchanged.access, refresh: exchanged.refresh, expires: exchanged.expires });
|
|
1308
|
+
}
|
|
1309
|
+
return spend(cred.refresh, { access: jt.token, refresh: jt.refresh_token, expires: expiresAt(jt) });
|
|
1310
|
+
};
|
|
1311
|
+
const refresh = (cred) => locked(async () => {
|
|
1312
|
+
checkAbort();
|
|
1313
|
+
const current = latest(cred);
|
|
1314
|
+
if (current !== cred && current.expires - now() > REFRESH_LEAD) return current;
|
|
1315
|
+
const next = { ...current, ...await renew(current) };
|
|
1316
|
+
checkAbort();
|
|
1317
|
+
return next;
|
|
1318
|
+
});
|
|
1319
|
+
const abortable = (work, signal) => {
|
|
1320
|
+
checkAbort(signal);
|
|
1321
|
+
if (!signal) return work;
|
|
1322
|
+
return new Promise((resolve, reject) => {
|
|
1323
|
+
const stop = () => reject(signal.reason ?? new DOMException("aborted", "AbortError"));
|
|
1324
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
1325
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener("abort", stop));
|
|
1326
|
+
});
|
|
1327
|
+
};
|
|
1328
|
+
const fresh = (cred, signal) => abortable(locked(async () => {
|
|
1329
|
+
checkAbort(signal);
|
|
1330
|
+
if (!cred?.access || !cred.uid) throw new SignInGone("Qoder: not signed in");
|
|
1331
|
+
const current = latest(cred);
|
|
1332
|
+
if (current.expires - now() >= REFRESH_LEAD) return current;
|
|
1333
|
+
const next = { ...current, ...await renew(current) };
|
|
1334
|
+
try {
|
|
1335
|
+
Promise.resolve(options.onRefresh?.(next)).catch(() => {
|
|
1336
|
+
});
|
|
1337
|
+
} catch {
|
|
1338
|
+
}
|
|
1339
|
+
return next;
|
|
1340
|
+
}), signal);
|
|
1341
|
+
const listings = /* @__PURE__ */ new Map();
|
|
1342
|
+
const modelsFor = async (cred, again = false, signal) => {
|
|
1343
|
+
let listing = listings.get(cred.uid);
|
|
1344
|
+
if (!listing || again) {
|
|
1345
|
+
listing = modelInfos(await fetchListing(site, cred, signal), site);
|
|
1346
|
+
checkAbort(signal);
|
|
1347
|
+
listings.set(cred.uid, listing);
|
|
1348
|
+
}
|
|
1349
|
+
return listing;
|
|
1350
|
+
};
|
|
1351
|
+
const models = async (cred, again = false) => {
|
|
1352
|
+
const current = await fresh(cred);
|
|
1353
|
+
checkAbort();
|
|
1354
|
+
return modelsFor(current, again);
|
|
1355
|
+
};
|
|
1356
|
+
const usage = async (cred) => {
|
|
1357
|
+
const current = await fresh(cred);
|
|
1358
|
+
checkAbort();
|
|
1359
|
+
const env = await fetchUsage(site, current.access);
|
|
1360
|
+
checkAbort();
|
|
1361
|
+
return { windows: [], ...parseUsage(env), signIn: "kept" };
|
|
1362
|
+
};
|
|
1363
|
+
const signedInError = (e) => errorResponse({
|
|
1364
|
+
status: e instanceof SignInGone || e?.expired ? 401 : 502,
|
|
1365
|
+
message: String(e?.message ?? e).replace(/^Qoder: /, ""),
|
|
1366
|
+
signIn: e?.expired ? "expired" : void 0
|
|
1367
|
+
});
|
|
1368
|
+
const chat = async (credential, chatBody, { session = "", signal: requestSignal } = {}) => {
|
|
1369
|
+
const signal = combine(requestSignal);
|
|
1370
|
+
checkAbort(signal);
|
|
1371
|
+
try {
|
|
1372
|
+
const cred = await fresh(credential, signal);
|
|
1373
|
+
checkAbort(signal);
|
|
1374
|
+
const m = (await modelsFor(cred, false, signal)).find((x) => x.key === chatBody.model) ?? (await modelsFor(cred, true, signal)).find((x) => x.key === chatBody.model);
|
|
1375
|
+
checkAbort(signal);
|
|
1376
|
+
if (!m) return errorResponse({ status: 400, message: `unknown or disabled model "${chatBody.model}"` });
|
|
1377
|
+
const name = m.name?.trim();
|
|
1378
|
+
const servedName = name && name !== m.key ? name : chatBody.model;
|
|
1379
|
+
const api = site.api;
|
|
1380
|
+
const url = api + CHAT_PATH2;
|
|
1381
|
+
const task = taskOf(sessionOf(chatBody, session), chatBody);
|
|
1382
|
+
const body = qoderBody(chatBody, m, session, task, site);
|
|
1383
|
+
const deadline = QUEUE.now() + QUEUE.budget;
|
|
1384
|
+
const ask = { requestSetId: body.request_set_id, requestId: body.request_id, sessionId: body.session_id, modelKey: m.key };
|
|
1385
|
+
for (let waited = 0; ; waited++) {
|
|
1386
|
+
checkAbort(signal);
|
|
1387
|
+
if (waited) {
|
|
1388
|
+
body.request_id = body.chat_record_id = randomUUID();
|
|
1389
|
+
ask.requestId = body.request_id;
|
|
1390
|
+
}
|
|
1391
|
+
const wire = encodeBody(JSON.stringify(body));
|
|
1392
|
+
const headers = {
|
|
1393
|
+
...cosyHeaders(url, cred, wire, site),
|
|
1394
|
+
Accept: "text/event-stream",
|
|
1395
|
+
"Cache-Control": "no-cache",
|
|
1396
|
+
"X-Model-Key": m.key,
|
|
1397
|
+
"X-Model-Source": m.source
|
|
1398
|
+
};
|
|
1399
|
+
const call = new AbortController();
|
|
1400
|
+
const res = await fetch2(url, { method: "POST", headers, body: wire, signal: combine(signal, call.signal) });
|
|
1401
|
+
checkAbort(signal);
|
|
1402
|
+
let open2, f;
|
|
1403
|
+
if (res.ok) {
|
|
1404
|
+
open2 = await opened(res, chatBody.tool_choice !== "none" ? (chatBody.tools ?? []).filter((t) => t?.type === "function") : [], signal);
|
|
1405
|
+
checkAbort(signal);
|
|
1406
|
+
f = open2.first.value?.error;
|
|
1407
|
+
} else {
|
|
1408
|
+
f = failure(res.status, (await res.text()).slice(0, 1 << 20));
|
|
1409
|
+
checkAbort(signal);
|
|
1410
|
+
}
|
|
1411
|
+
if (f) {
|
|
1412
|
+
call.abort();
|
|
1413
|
+
try {
|
|
1414
|
+
if (open2) await open2.it.return?.();
|
|
1415
|
+
else if (!res.bodyUsed) await res.body?.cancel();
|
|
1416
|
+
} catch (e) {
|
|
1417
|
+
if (e !== call.signal.reason) throw e;
|
|
1418
|
+
}
|
|
1419
|
+
checkAbort(signal);
|
|
1420
|
+
} else task.answered = true;
|
|
1421
|
+
if (!f?.queue || waited >= QUEUE.recoveries) {
|
|
1422
|
+
const result = f ? errorResponse(f) : await answer(open2, chatBody, servedName, signal);
|
|
1423
|
+
checkAbort(signal);
|
|
1424
|
+
return result;
|
|
1425
|
+
}
|
|
1426
|
+
if (!await queueWait(api, cred, ask, f.queue, signal, deadline, site)) {
|
|
1427
|
+
checkAbort(signal);
|
|
1428
|
+
return errorResponse(f);
|
|
1429
|
+
}
|
|
1430
|
+
checkAbort(signal);
|
|
1431
|
+
}
|
|
1432
|
+
} catch (e) {
|
|
1433
|
+
checkAbort(signal);
|
|
1434
|
+
return signedInError(e);
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
return {
|
|
1438
|
+
connectPAT,
|
|
1439
|
+
refresh,
|
|
1440
|
+
models,
|
|
1441
|
+
chat,
|
|
1442
|
+
usage,
|
|
1443
|
+
dispose() {
|
|
1444
|
+
listings.clear();
|
|
1445
|
+
spent.clear();
|
|
1446
|
+
TASKS.clear();
|
|
1447
|
+
},
|
|
1448
|
+
_internal: {
|
|
1449
|
+
cosyHeaders,
|
|
1450
|
+
userBlob,
|
|
1451
|
+
machineOS,
|
|
1452
|
+
encodeBody,
|
|
1453
|
+
decodeBody,
|
|
1454
|
+
segmentEncode,
|
|
1455
|
+
swapThirds,
|
|
1456
|
+
qoderBody,
|
|
1457
|
+
qoderMessages,
|
|
1458
|
+
cacheMarked,
|
|
1459
|
+
sessionOf,
|
|
1460
|
+
taskOf,
|
|
1461
|
+
failure,
|
|
1462
|
+
errorResponse,
|
|
1463
|
+
parseUsage,
|
|
1464
|
+
modelInfo,
|
|
1465
|
+
typedUnions,
|
|
1466
|
+
events,
|
|
1467
|
+
QUEUE,
|
|
1468
|
+
SITES,
|
|
1469
|
+
site
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// src/protocol.ts
|
|
1475
|
+
var CHAT_PATH = "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1";
|
|
1476
|
+
var QODER_SITES = Object.freeze([
|
|
1477
|
+
{
|
|
1478
|
+
id: "qoder",
|
|
1479
|
+
label: "Qoder",
|
|
1480
|
+
openapi: "https://openapi.qoder.sh",
|
|
1481
|
+
api: "https://api3.qoder.sh",
|
|
1482
|
+
web: "https://qoder.com",
|
|
1483
|
+
chatUrl: `https://api3.qoder.sh${CHAT_PATH}`,
|
|
1484
|
+
provider: "qoder-subscription",
|
|
1485
|
+
marker: "ccr-managed-qoder",
|
|
1486
|
+
credentialFile: "qoder-credentials.json",
|
|
1487
|
+
routeBase: "/api/qoder"
|
|
1488
|
+
},
|
|
1489
|
+
{
|
|
1490
|
+
id: "qoder-cn",
|
|
1491
|
+
label: "Qoder CN",
|
|
1492
|
+
openapi: "https://openapi.qoder.com.cn",
|
|
1493
|
+
api: "https://gateway.qoder.com.cn",
|
|
1494
|
+
web: "https://qoder.cn",
|
|
1495
|
+
chatUrl: `https://gateway.qoder.com.cn${CHAT_PATH}`,
|
|
1496
|
+
provider: "qoder-cn-subscription",
|
|
1497
|
+
marker: "ccr-managed-qoder-cn",
|
|
1498
|
+
credentialFile: "qoder-cn-credentials.json",
|
|
1499
|
+
routeBase: "/api/qoder/cn"
|
|
1500
|
+
}
|
|
1501
|
+
]);
|
|
1502
|
+
var QODER_CHAT = QODER_SITES[0].chatUrl;
|
|
1503
|
+
var QODER_PROVIDER = QODER_SITES[0].provider;
|
|
1504
|
+
var QODER_MARKER = QODER_SITES[0].marker;
|
|
1505
|
+
function qoderSite(site) {
|
|
1506
|
+
return QODER_SITES.find((s) => s.id === site) ?? QODER_SITES[0];
|
|
1507
|
+
}
|
|
1508
|
+
function qoderError(code, status = 422) {
|
|
1509
|
+
return Object.assign(new Error(code), { code, statusCode: status, nonRetryable: true });
|
|
1510
|
+
}
|
|
1511
|
+
function isQoderManaged(p, site = "qoder") {
|
|
1512
|
+
const managedBy = qoderSite(site);
|
|
1513
|
+
const name = p?.name;
|
|
1514
|
+
const owner = typeof name === "string" ? QODER_SITES.find((s) => s.provider === name) : void 0;
|
|
1515
|
+
if (owner) {
|
|
1516
|
+
return owner.id === managedBy.id && (p?.qoder_managed === true || p?.qoderManaged === true || (p?.api_key ?? p?.apiKey) === managedBy.marker);
|
|
1517
|
+
}
|
|
1518
|
+
if (managedBy.id !== QODER_SITES[0].id) return false;
|
|
1519
|
+
return p?.qoder_managed === true || p?.qoderManaged === true;
|
|
1520
|
+
}
|
|
1521
|
+
function lockQoderProvider(p, site = "qoder") {
|
|
1522
|
+
const managedBy = qoderSite(site);
|
|
1523
|
+
return {
|
|
1524
|
+
...p,
|
|
1525
|
+
name: managedBy.provider,
|
|
1526
|
+
qoder_managed: true,
|
|
1527
|
+
qoderManaged: true,
|
|
1528
|
+
api_base_url: managedBy.chatUrl,
|
|
1529
|
+
api_key: managedBy.marker,
|
|
1530
|
+
transformer: { use: [managedBy.id] }
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
function protectQoderConfig(next, previous) {
|
|
1534
|
+
if (!next || typeof next !== "object") return;
|
|
1535
|
+
const key = Array.isArray(next?.providers) && !Array.isArray(next?.Providers) ? "providers" : "Providers";
|
|
1536
|
+
const old = previous?.Providers ?? previous?.providers ?? [];
|
|
1537
|
+
const list = Array.isArray(next[key]) ? next[key] : [];
|
|
1538
|
+
next[key] = list.map((p) => {
|
|
1539
|
+
for (const managedBy of QODER_SITES) {
|
|
1540
|
+
const previousEntry = old.find((o) => isQoderManaged(o, managedBy.id));
|
|
1541
|
+
if (isQoderManaged(p, managedBy.id) || previousEntry && p?.name === managedBy.provider) {
|
|
1542
|
+
return lockQoderProvider(p, managedBy.id);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return p;
|
|
1546
|
+
});
|
|
1547
|
+
for (const managedBy of QODER_SITES) {
|
|
1548
|
+
const previousEntry = old.find((o) => isQoderManaged(o, managedBy.id));
|
|
1549
|
+
if (previousEntry && !next[key].some((p) => isQoderManaged(p, managedBy.id))) {
|
|
1550
|
+
next[key].push(lockQoderProvider(previousEntry, managedBy.id));
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
var configWriteQueues = /* @__PURE__ */ new Map();
|
|
1555
|
+
function serializeQoderConfigWrite(configDir, fn) {
|
|
1556
|
+
const previous = configWriteQueues.get(configDir) ?? Promise.resolve();
|
|
1557
|
+
const job = previous.then(fn, fn);
|
|
1558
|
+
configWriteQueues.set(configDir, job.catch(() => {
|
|
1559
|
+
}));
|
|
1560
|
+
return job;
|
|
1561
|
+
}
|
|
1562
|
+
var RETRY_AFTER_MIN_MS = 3e4;
|
|
1563
|
+
var RETRY_AFTER_MAX_MS = 30 * 6e4;
|
|
1564
|
+
var DEFAULT_429_COOLDOWN_MS = 3e5;
|
|
1565
|
+
function classifyQoderError(status, body) {
|
|
1566
|
+
if (status === 0) return { code: "qoder_upstream_error", message: "qoder_upstream_error", httpStatus: 502, retryable: true };
|
|
1567
|
+
let hint = body;
|
|
1568
|
+
if (typeof body === "string") {
|
|
1569
|
+
try {
|
|
1570
|
+
hint = JSON.parse(body);
|
|
1571
|
+
} catch {
|
|
1572
|
+
hint = void 0;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (status === 401) return { code: "qoder_reauth_required", message: "qoder_reauth_required", httpStatus: 401, retryable: false };
|
|
1576
|
+
if (status === 429) {
|
|
1577
|
+
const seconds = Number(hint?.retry_after);
|
|
1578
|
+
const cooldownMs = Number.isFinite(seconds) && seconds > 0 ? Math.min(Math.max(seconds * 1e3, RETRY_AFTER_MIN_MS), RETRY_AFTER_MAX_MS) : DEFAULT_429_COOLDOWN_MS;
|
|
1579
|
+
return { code: "qoder_rate_limited", message: "qoder_rate_limited", httpStatus: 429, cooldownMs, retryable: false };
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
code: "qoder_upstream_error",
|
|
1583
|
+
message: "qoder_upstream_error",
|
|
1584
|
+
httpStatus: status,
|
|
1585
|
+
retryable: status >= 500
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
function isQoderErrorEnvelope(body) {
|
|
1589
|
+
return body?.error?.type === "qoder_error";
|
|
1590
|
+
}
|
|
1591
|
+
function validPAT(value) {
|
|
1592
|
+
return typeof value === "string" && value.startsWith("pt-") && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/auth-service.ts
|
|
1596
|
+
var REFRESH_LEAD_MS = 5 * 6e4;
|
|
1597
|
+
var USAGE_CACHE_MS = 6e4;
|
|
1598
|
+
var COOLDOWN_CAP_MS = 36e5;
|
|
1599
|
+
var DEFAULT_429_COOLDOWN_MS2 = 3e5;
|
|
1600
|
+
var FILE_MAX_BYTES = 1048576;
|
|
1601
|
+
var field = (v, max) => typeof v === "string" && v.length > 0 && v.length <= max && !/[\x00-\x1f]/.test(v);
|
|
1602
|
+
var softField = (v, max) => typeof v === "string" && v.length <= max;
|
|
1603
|
+
var validModels = (v) => Array.isArray(v) && v.length <= 2048 && v.every((m) => field(m, 256));
|
|
1604
|
+
var validModelRecords = (v) => Array.isArray(v) && v.length <= 512 && v.every((m) => m != null && typeof m === "object" && field(m.key, 256) && softField(m.name, 256) && typeof m.context === "number" && Number.isFinite(m.context) && m.context >= 0 && typeof m.free === "boolean");
|
|
1605
|
+
function validate(value) {
|
|
1606
|
+
const c = value?.credential;
|
|
1607
|
+
if (value?.version !== 1 || !c || typeof c !== "object" || !field(c.access, 32768) || !softField(c.refresh, 32768) || !Number.isFinite(c.expires) || !field(c.uid, 256) || !softField(c.email, 1024) || !softField(c.name, 1024) || !field(c.accountId, 256) || !field(c.machineId, 128) || !validPAT(c.pat) || !validModelRecords(value.models) || !validModels(value.hiddenModels)) throw qoderError("qoder_credential_invalid");
|
|
1608
|
+
return {
|
|
1609
|
+
version: 1,
|
|
1610
|
+
credential: {
|
|
1611
|
+
access: c.access,
|
|
1612
|
+
refresh: c.refresh,
|
|
1613
|
+
expires: c.expires,
|
|
1614
|
+
uid: c.uid,
|
|
1615
|
+
email: c.email,
|
|
1616
|
+
name: c.name,
|
|
1617
|
+
accountId: c.accountId,
|
|
1618
|
+
machineId: c.machineId,
|
|
1619
|
+
pat: c.pat
|
|
1620
|
+
},
|
|
1621
|
+
models: value.models.map((m) => ({ key: m.key, name: m.name, context: m.context, free: m.free })),
|
|
1622
|
+
hiddenModels: [...new Set(value.hiddenModels)]
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function maskEmail(email) {
|
|
1626
|
+
if (!email) return null;
|
|
1627
|
+
const at = email.indexOf("@");
|
|
1628
|
+
if (at <= 0) return null;
|
|
1629
|
+
return `${email[0]}***${email.slice(at)}`;
|
|
1630
|
+
}
|
|
1631
|
+
function toRecord(m) {
|
|
1632
|
+
return { key: String(m.key), name: String(m.name ?? m.key), context: Number(m.context) || 0, free: m.free === true };
|
|
1633
|
+
}
|
|
1634
|
+
var QoderAuthService = class {
|
|
1635
|
+
constructor(options) {
|
|
1636
|
+
this.options = options;
|
|
1637
|
+
this.site = qoderSite(options.site);
|
|
1638
|
+
const network = options.fetch ?? ((input, init) => fetch(input, init));
|
|
1639
|
+
const hostFetch = (input, init) => network(input, {
|
|
1640
|
+
...init,
|
|
1641
|
+
redirect: "error",
|
|
1642
|
+
dispatcher: options.host.getSubscriptionDispatcher?.() ?? options.host.getDirectDispatcher()
|
|
1643
|
+
});
|
|
1644
|
+
this.engine = options.engine ?? createQoderEngine({
|
|
1645
|
+
fetch: hostFetch,
|
|
1646
|
+
// The engine cannot know the host's connection generation, but a
|
|
1647
|
+
// rotation preserves the credential machineId while every new PAT
|
|
1648
|
+
// connect generates one. The bound machineId inside the rotated
|
|
1649
|
+
// credential therefore identifies its originating connection, so a
|
|
1650
|
+
// rotation delivered after a disconnect+reconnect (even with the SAME
|
|
1651
|
+
// PAT) cannot overwrite the new connection.
|
|
1652
|
+
onRefresh: (n) => this.handleEngineRefresh(n),
|
|
1653
|
+
// The engine resolves its hosts from the site (see vendor/engine.mjs).
|
|
1654
|
+
site: this.site.id
|
|
1655
|
+
});
|
|
1656
|
+
this.file = join(options.host.configDir, this.site.credentialFile);
|
|
1657
|
+
}
|
|
1658
|
+
engine;
|
|
1659
|
+
site;
|
|
1660
|
+
file;
|
|
1661
|
+
loaded;
|
|
1662
|
+
saved;
|
|
1663
|
+
refresh;
|
|
1664
|
+
queue = Promise.resolve();
|
|
1665
|
+
persistChain = Promise.resolve();
|
|
1666
|
+
generation = 0;
|
|
1667
|
+
disconnecting = false;
|
|
1668
|
+
expired = false;
|
|
1669
|
+
lastError;
|
|
1670
|
+
coolingUntil = 0;
|
|
1671
|
+
usage;
|
|
1672
|
+
usageJob;
|
|
1673
|
+
/**
|
|
1674
|
+
* Persist a credential the engine rotated on its own during a request
|
|
1675
|
+
* (engine options.onRefresh). Credential-only mutation (no config write),
|
|
1676
|
+
* guarded by generation + uid+pat so a stale rotation after disconnect or a
|
|
1677
|
+
* different account can never resurrect state. Failures are swallowed: a
|
|
1678
|
+
* crash before persistence still recovers through PAT re-exchange.
|
|
1679
|
+
*/
|
|
1680
|
+
handleEngineRefresh(next) {
|
|
1681
|
+
this.persistChain = this.persistChain.then(async () => {
|
|
1682
|
+
const current = this.saved;
|
|
1683
|
+
if (this.disconnecting || !current || current.credential.uid !== next.uid || current.credential.pat !== next.pat || current.credential.machineId !== next.machineId) return;
|
|
1684
|
+
await this.persist({ ...current, credential: next });
|
|
1685
|
+
this.expired = false;
|
|
1686
|
+
this.lastError = void 0;
|
|
1687
|
+
}).catch(() => {
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
exclusive(fn) {
|
|
1691
|
+
const job = this.queue.then(fn);
|
|
1692
|
+
this.queue = job.catch(() => {
|
|
1693
|
+
});
|
|
1694
|
+
return job;
|
|
1695
|
+
}
|
|
1696
|
+
async load() {
|
|
1697
|
+
this.loaded ??= (async () => {
|
|
1698
|
+
let handle;
|
|
1699
|
+
try {
|
|
1700
|
+
handle = await open(this.file, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
|
|
1701
|
+
const stat = await handle.stat();
|
|
1702
|
+
if (!stat.isFile() || stat.size > FILE_MAX_BYTES) throw qoderError("qoder_credential_invalid");
|
|
1703
|
+
await handle.chmod(384);
|
|
1704
|
+
this.saved = validate(JSON.parse(await handle.readFile("utf8")));
|
|
1705
|
+
} catch (e) {
|
|
1706
|
+
if (e?.code !== "ENOENT") throw qoderError("qoder_credential_unreadable", 500);
|
|
1707
|
+
} finally {
|
|
1708
|
+
await handle?.close();
|
|
1709
|
+
}
|
|
1710
|
+
})().catch((error) => {
|
|
1711
|
+
this.loaded = void 0;
|
|
1712
|
+
throw error;
|
|
1713
|
+
});
|
|
1714
|
+
return this.loaded;
|
|
1715
|
+
}
|
|
1716
|
+
/** Validate and serialize before replacing the file. The loader must never
|
|
1717
|
+
* accept a state this writer created, and JSON byte growth is bounded. */
|
|
1718
|
+
serializeSaved(next) {
|
|
1719
|
+
const checked = validate(next);
|
|
1720
|
+
const text = JSON.stringify(checked);
|
|
1721
|
+
if (Buffer.byteLength(text, "utf8") > FILE_MAX_BYTES) throw qoderError("qoder_persist_failed", 500);
|
|
1722
|
+
return text;
|
|
1723
|
+
}
|
|
1724
|
+
async persist(next) {
|
|
1725
|
+
await mkdir(this.options.host.configDir, { recursive: true, mode: 448 });
|
|
1726
|
+
const text = this.serializeSaved(next);
|
|
1727
|
+
const temp = `${this.file}.${randomBytes(12).toString("hex")}.tmp`;
|
|
1728
|
+
let handle;
|
|
1729
|
+
try {
|
|
1730
|
+
handle = await open(temp, constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY | constants2.O_NOFOLLOW, 384);
|
|
1731
|
+
await handle.writeFile(text);
|
|
1732
|
+
await handle.sync();
|
|
1733
|
+
await handle.close();
|
|
1734
|
+
handle = void 0;
|
|
1735
|
+
await rename(temp, this.file);
|
|
1736
|
+
this.saved = next;
|
|
1737
|
+
} catch {
|
|
1738
|
+
throw qoderError("qoder_persist_failed", 500);
|
|
1739
|
+
} finally {
|
|
1740
|
+
await handle?.close();
|
|
1741
|
+
await unlink(temp).catch(() => {
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* The ONLY writers of durable state (grok parity): each job re-reads the
|
|
1747
|
+
* latest saved state at execution time, writes config first and persists
|
|
1748
|
+
* after; a persist failure after a successful config write rolls the managed
|
|
1749
|
+
* entry back so config never names models the private file lost.
|
|
1750
|
+
*/
|
|
1751
|
+
mutate(merge, writeConfig = true) {
|
|
1752
|
+
const job = this.persistChain.then(async () => {
|
|
1753
|
+
const current = this.saved;
|
|
1754
|
+
if (!current || this.disconnecting) throw qoderError("qoder_not_connected", 409);
|
|
1755
|
+
const next = merge(current);
|
|
1756
|
+
if (writeConfig) {
|
|
1757
|
+
await this.updateConfig(next);
|
|
1758
|
+
try {
|
|
1759
|
+
await this.persist(next);
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
await this.updateConfig(current).catch(() => {
|
|
1762
|
+
});
|
|
1763
|
+
throw error;
|
|
1764
|
+
}
|
|
1765
|
+
} else {
|
|
1766
|
+
await this.persist(next);
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
this.persistChain = job.catch(() => {
|
|
1770
|
+
});
|
|
1771
|
+
return job;
|
|
1772
|
+
}
|
|
1773
|
+
async rawConfig() {
|
|
1774
|
+
if (!this.options.host.readRawConfig) throw qoderError("qoder_raw_config_required", 500);
|
|
1775
|
+
return structuredClone(await this.options.host.readRawConfig());
|
|
1776
|
+
}
|
|
1777
|
+
async updateConfig(saved) {
|
|
1778
|
+
await serializeQoderConfigWrite(this.options.host.configDir, async () => {
|
|
1779
|
+
const cfg = await this.rawConfig();
|
|
1780
|
+
const before = structuredClone(cfg);
|
|
1781
|
+
const key = Array.isArray(cfg.providers) && !Array.isArray(cfg.Providers) ? "providers" : "Providers";
|
|
1782
|
+
const list = cfg[key] ?? [];
|
|
1783
|
+
const at = list.findIndex((p) => isQoderManaged(p, this.site.id));
|
|
1784
|
+
if (saved) {
|
|
1785
|
+
if (at < 0 && list.some((p) => p?.name === this.site.provider)) throw qoderError("qoder_provider_name_conflict");
|
|
1786
|
+
const provider = lockQoderProvider({
|
|
1787
|
+
...at >= 0 ? list[at] : {},
|
|
1788
|
+
models: saved.models.filter((m) => !saved.hiddenModels.includes(m.key)).map((m) => m.key),
|
|
1789
|
+
enabled: at >= 0 ? list[at].enabled !== false : true
|
|
1790
|
+
}, this.site.id);
|
|
1791
|
+
if (at >= 0) list[at] = provider;
|
|
1792
|
+
else list.push(provider);
|
|
1793
|
+
} else if (at >= 0) list.splice(at, 1);
|
|
1794
|
+
cfg[key] = list;
|
|
1795
|
+
try {
|
|
1796
|
+
await this.options.host.writeConfig(cfg);
|
|
1797
|
+
await this.options.host.reloadRuntime(cfg);
|
|
1798
|
+
} catch {
|
|
1799
|
+
await this.options.host.writeConfig(before).then(() => this.options.host.reloadRuntime(before)).catch(() => {
|
|
1800
|
+
});
|
|
1801
|
+
throw qoderError("qoder_config_write_failed", 500);
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
/** Exchange a PAT for a job token: persist FIRST, then config, rollback on failure. */
|
|
1806
|
+
async connectPAT(pat) {
|
|
1807
|
+
return this.exclusive(async () => {
|
|
1808
|
+
await this.load();
|
|
1809
|
+
if (this.saved) throw qoderError("qoder_already_connected", 409);
|
|
1810
|
+
if (!validPAT(pat)) throw qoderError("qoder_pat_invalid", 422);
|
|
1811
|
+
let credential;
|
|
1812
|
+
try {
|
|
1813
|
+
credential = await this.engine.connectPAT(pat);
|
|
1814
|
+
} catch (e) {
|
|
1815
|
+
if (e?.code) throw e;
|
|
1816
|
+
throw qoderError("qoder_upstream_error", 502);
|
|
1817
|
+
}
|
|
1818
|
+
const next = { version: 1, credential, models: [], hiddenModels: [] };
|
|
1819
|
+
await this.persist(next);
|
|
1820
|
+
try {
|
|
1821
|
+
await this.updateConfig(next);
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
await rm(this.file, { force: true });
|
|
1824
|
+
this.saved = void 0;
|
|
1825
|
+
throw e;
|
|
1826
|
+
}
|
|
1827
|
+
this.expired = false;
|
|
1828
|
+
this.lastError = void 0;
|
|
1829
|
+
this.coolingUntil = 0;
|
|
1830
|
+
return { success: true };
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Single-flight job-token refresh (contract): rotated credentials persist
|
|
1835
|
+
* through the mutate chain; a refresh that lands after a disconnect (or any
|
|
1836
|
+
* generation bump) is discarded WITHOUT latching reauth. The engine's own
|
|
1837
|
+
* PAT re-exchange recovery clears the expired latch on success.
|
|
1838
|
+
*/
|
|
1839
|
+
async forceTokenRefresh() {
|
|
1840
|
+
await this.load();
|
|
1841
|
+
if (!this.saved || this.disconnecting) throw qoderError("qoder_not_connected", 409);
|
|
1842
|
+
if (this.refresh) return this.refresh;
|
|
1843
|
+
const saved = this.saved, generation = this.generation;
|
|
1844
|
+
this.refresh = (async () => {
|
|
1845
|
+
try {
|
|
1846
|
+
const c = await this.engine.refresh(saved.credential);
|
|
1847
|
+
if (generation !== this.generation || this.saved?.credential.machineId !== saved.credential.machineId || this.disconnecting) {
|
|
1848
|
+
throw qoderError("qoder_not_connected", 409);
|
|
1849
|
+
}
|
|
1850
|
+
await this.mutate((current) => ({ ...current, credential: c }), false);
|
|
1851
|
+
this.expired = false;
|
|
1852
|
+
this.lastError = void 0;
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
if (generation !== this.generation || this.disconnecting || !this.saved) throw qoderError("qoder_not_connected", 409);
|
|
1855
|
+
if (error?.expired === true) {
|
|
1856
|
+
this.expired = true;
|
|
1857
|
+
this.lastError = "qoder_reauth_required";
|
|
1858
|
+
throw qoderError("qoder_reauth_required", 401);
|
|
1859
|
+
}
|
|
1860
|
+
if (["qoder_persist_failed", "qoder_credential_invalid", "qoder_raw_config_required", "qoder_proxy_invalid"].includes(error?.code)) {
|
|
1861
|
+
this.lastError = "qoder_upstream_error";
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1864
|
+
this.lastError = "qoder_upstream_error";
|
|
1865
|
+
throw qoderError("qoder_upstream_error", 502);
|
|
1866
|
+
} finally {
|
|
1867
|
+
this.refresh = void 0;
|
|
1868
|
+
}
|
|
1869
|
+
})();
|
|
1870
|
+
return this.refresh;
|
|
1871
|
+
}
|
|
1872
|
+
/** Circuit breaker BEFORE any refresh/network work (Gemini lesson, contract). */
|
|
1873
|
+
async ensureUsable() {
|
|
1874
|
+
await this.load();
|
|
1875
|
+
if (!this.saved || this.disconnecting) throw qoderError("qoder_not_connected", 409);
|
|
1876
|
+
if (this.coolingUntil > Date.now()) throw qoderError("qoder_rate_limited", 429);
|
|
1877
|
+
if (this.expired) throw qoderError("qoder_reauth_required", 401);
|
|
1878
|
+
}
|
|
1879
|
+
/** Credential with the engine's refresh lead applied; single-flight coalesces callers. */
|
|
1880
|
+
async waitWithSignal(work, signal) {
|
|
1881
|
+
if (!signal) return work;
|
|
1882
|
+
if (signal.aborted) throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
|
1883
|
+
return await new Promise((resolve, reject) => {
|
|
1884
|
+
const stop = () => reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
|
|
1885
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
1886
|
+
work.then(
|
|
1887
|
+
(value) => {
|
|
1888
|
+
signal.removeEventListener("abort", stop);
|
|
1889
|
+
resolve(value);
|
|
1890
|
+
},
|
|
1891
|
+
(error) => {
|
|
1892
|
+
signal.removeEventListener("abort", stop);
|
|
1893
|
+
reject(error);
|
|
1894
|
+
}
|
|
1895
|
+
);
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
/** Credential with the engine's refresh lead applied; the shared rotation
|
|
1899
|
+
* may continue after this caller stops waiting, so a token is never lost. */
|
|
1900
|
+
async freshCredential(signal) {
|
|
1901
|
+
await this.load();
|
|
1902
|
+
if (!this.saved || this.disconnecting) throw qoderError("qoder_not_connected", 409);
|
|
1903
|
+
if (this.saved.credential.expires - Date.now() < REFRESH_LEAD_MS) {
|
|
1904
|
+
await this.waitWithSignal(this.forceTokenRefresh(), signal);
|
|
1905
|
+
}
|
|
1906
|
+
return this.saved.credential;
|
|
1907
|
+
}
|
|
1908
|
+
/** One bounded engine chat transaction for the managed runtime's dispatchRequest. */
|
|
1909
|
+
async dispatch(chatBody, signal) {
|
|
1910
|
+
await this.ensureUsable();
|
|
1911
|
+
const cred = await this.freshCredential(signal);
|
|
1912
|
+
const session = typeof chatBody?.prompt_cache_key === "string" && chatBody.prompt_cache_key ? chatBody.prompt_cache_key : "";
|
|
1913
|
+
return this.engine.chat(cred, chatBody, { session, signal });
|
|
1914
|
+
}
|
|
1915
|
+
async syncModels() {
|
|
1916
|
+
return this.exclusive(async () => {
|
|
1917
|
+
const cred = await this.freshCredential();
|
|
1918
|
+
let listing;
|
|
1919
|
+
try {
|
|
1920
|
+
listing = await this.engine.models(cred, true);
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
if (error?.expired === true) {
|
|
1923
|
+
this.expired = true;
|
|
1924
|
+
this.lastError = "qoder_reauth_required";
|
|
1925
|
+
throw qoderError("qoder_reauth_required", 401);
|
|
1926
|
+
}
|
|
1927
|
+
if (error?.code) throw error;
|
|
1928
|
+
throw qoderError("qoder_upstream_error", 502);
|
|
1929
|
+
}
|
|
1930
|
+
const models = (Array.isArray(listing) ? listing : []).map(toRecord).filter((m) => m.key && !["auto", "default"].includes(m.key)).slice(0, 512);
|
|
1931
|
+
if (!models.length) throw qoderError("qoder_upstream_error");
|
|
1932
|
+
this.noteUpstreamSuccess();
|
|
1933
|
+
await this.mutate((current) => ({ ...current, models }));
|
|
1934
|
+
return { success: true, models: models.map((m) => m.key), modelCount: models.length };
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
async setModelHidden(key, hidden) {
|
|
1938
|
+
return this.exclusive(async () => {
|
|
1939
|
+
await this.load();
|
|
1940
|
+
if (!this.saved || this.disconnecting) throw qoderError("qoder_not_connected", 409);
|
|
1941
|
+
if (typeof key !== "string" || !this.saved.models.some((m) => m.key === key)) throw qoderError("qoder_model_unknown");
|
|
1942
|
+
await this.mutate((current) => ({
|
|
1943
|
+
...current,
|
|
1944
|
+
hiddenModels: [.../* @__PURE__ */ new Set([...current.hiddenModels.filter((m) => m !== key), ...hidden ? [key] : []])]
|
|
1945
|
+
}));
|
|
1946
|
+
return { success: true };
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
async getUsage() {
|
|
1950
|
+
await this.load();
|
|
1951
|
+
if (!this.saved || this.disconnecting) throw qoderError("qoder_not_connected", 409);
|
|
1952
|
+
const snapshot = () => this.usage ? { ...this.usage, stale: Date.now() - this.usage.observedAt > USAGE_CACHE_MS, coolingUntil: this.coolingUntil || void 0 } : { known: false, stale: false, windows: [], coolingUntil: this.coolingUntil || void 0 };
|
|
1953
|
+
if (this.usage && Date.now() - this.usage.observedAt < USAGE_CACHE_MS) return snapshot();
|
|
1954
|
+
if (this.usageJob) return this.usageJob;
|
|
1955
|
+
this.usageJob = (async () => {
|
|
1956
|
+
try {
|
|
1957
|
+
const cred = await this.freshCredential();
|
|
1958
|
+
const result = await this.engine.usage(cred);
|
|
1959
|
+
if (!result || result.error || !Array.isArray(result.windows)) throw 0;
|
|
1960
|
+
this.noteUpstreamSuccess();
|
|
1961
|
+
this.usage = { known: true, stale: false, observedAt: Date.now(), windows: result.windows, plan: result.plan, until: result.until };
|
|
1962
|
+
} catch {
|
|
1963
|
+
return snapshot();
|
|
1964
|
+
}
|
|
1965
|
+
return snapshot();
|
|
1966
|
+
})().finally(() => {
|
|
1967
|
+
this.usageJob = void 0;
|
|
1968
|
+
});
|
|
1969
|
+
return this.usageJob;
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Latched state heals ONLY per contract: success clears cooling + lastError,
|
|
1973
|
+
* never the expired latch — that one clears solely through a successful
|
|
1974
|
+
* engine refresh (PAT re-exchange) or an explicit reconnect.
|
|
1975
|
+
*/
|
|
1976
|
+
noteUpstreamSuccess() {
|
|
1977
|
+
this.lastError = void 0;
|
|
1978
|
+
this.coolingUntil = 0;
|
|
1979
|
+
}
|
|
1980
|
+
async noteUpstreamError(info) {
|
|
1981
|
+
if (info.code === "qoder_reauth_required" || info.httpStatus === 401) {
|
|
1982
|
+
this.expired = true;
|
|
1983
|
+
this.lastError = "qoder_reauth_required";
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
this.lastError = ["qoder_rate_limited", "qoder_upstream_error"].includes(info.code) ? info.code : "qoder_upstream_error";
|
|
1987
|
+
if (info.code === "qoder_rate_limited") {
|
|
1988
|
+
this.coolingUntil = Date.now() + Math.min(Math.max(info.cooldownMs ?? DEFAULT_429_COOLDOWN_MS2, 0), COOLDOWN_CAP_MS);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
async getStatus() {
|
|
1992
|
+
await this.load();
|
|
1993
|
+
const saved = this.saved;
|
|
1994
|
+
const models = (saved?.models ?? []).map((m) => ({
|
|
1995
|
+
id: m.key,
|
|
1996
|
+
name: m.name,
|
|
1997
|
+
context: m.context,
|
|
1998
|
+
free: m.free,
|
|
1999
|
+
hidden: saved.hiddenModels.includes(m.key)
|
|
2000
|
+
}));
|
|
2001
|
+
return {
|
|
2002
|
+
status: saved ? "connected" : "not_connected",
|
|
2003
|
+
providerName: this.site.provider,
|
|
2004
|
+
account: saved ? maskEmail(saved.credential.email) ?? maskEmail(saved.credential.accountId) ?? void 0 : void 0,
|
|
2005
|
+
models,
|
|
2006
|
+
modelCount: models.length,
|
|
2007
|
+
...this.coolingUntil > Date.now() ? { coolingUntil: this.coolingUntil } : {},
|
|
2008
|
+
...this.expired ? { expired: true } : {},
|
|
2009
|
+
...this.lastError ? { lastError: this.lastError } : {}
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
async disconnect() {
|
|
2013
|
+
return this.exclusive(async () => {
|
|
2014
|
+
await this.load().catch(() => {
|
|
2015
|
+
});
|
|
2016
|
+
this.disconnecting = true;
|
|
2017
|
+
this.generation++;
|
|
2018
|
+
await this.refresh?.catch(() => {
|
|
2019
|
+
});
|
|
2020
|
+
await this.usageJob?.catch(() => {
|
|
2021
|
+
});
|
|
2022
|
+
await this.persistChain.catch(() => {
|
|
2023
|
+
});
|
|
2024
|
+
try {
|
|
2025
|
+
await this.updateConfig();
|
|
2026
|
+
} catch (e) {
|
|
2027
|
+
this.disconnecting = false;
|
|
2028
|
+
throw e;
|
|
2029
|
+
}
|
|
2030
|
+
await rm(this.file, { force: true });
|
|
2031
|
+
this.saved = void 0;
|
|
2032
|
+
try {
|
|
2033
|
+
this.engine.dispose();
|
|
2034
|
+
} catch {
|
|
2035
|
+
}
|
|
2036
|
+
this.usage = void 0;
|
|
2037
|
+
this.expired = false;
|
|
2038
|
+
this.lastError = void 0;
|
|
2039
|
+
this.coolingUntil = 0;
|
|
2040
|
+
this.disconnecting = false;
|
|
2041
|
+
return { success: true };
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
// src/transformer.ts
|
|
2047
|
+
function qoderRequest(request, context) {
|
|
2048
|
+
const body = structuredClone(request ?? {});
|
|
2049
|
+
const reasoning = body.reasoning;
|
|
2050
|
+
delete body.system;
|
|
2051
|
+
delete body.reasoning;
|
|
2052
|
+
delete body.prompt_cache_key;
|
|
2053
|
+
body.messages = (request?.messages ?? []).map((message) => {
|
|
2054
|
+
const { thinking, ...next } = message;
|
|
2055
|
+
delete next.thinking;
|
|
2056
|
+
if (thinking && typeof thinking.content === "string" && thinking.content) next.reasoning_content = thinking.content;
|
|
2057
|
+
else if (typeof next.reasoning_content !== "string" || !next.reasoning_content) delete next.reasoning_content;
|
|
2058
|
+
return next;
|
|
2059
|
+
});
|
|
2060
|
+
if (request?.system) {
|
|
2061
|
+
const text = typeof request.system === "string" ? request.system : Array.isArray(request.system) ? request.system.filter((p) => p?.type === "text").map((p) => p.text).join("\n") : "";
|
|
2062
|
+
if (text) body.messages.unshift({ role: "system", content: text });
|
|
2063
|
+
}
|
|
2064
|
+
const session = request?.prompt_cache_key ?? context?.req?.clientContext?.stableSessionId;
|
|
2065
|
+
if (typeof session === "string" && session && session.length <= 1024) body.prompt_cache_key = session;
|
|
2066
|
+
if (reasoning?.effort) body.reasoning_effort = reasoning.effort;
|
|
2067
|
+
return body;
|
|
2068
|
+
}
|
|
2069
|
+
function mapMessage(message) {
|
|
2070
|
+
if (!message || typeof message !== "object") return;
|
|
2071
|
+
if (typeof message.reasoning_content === "string") {
|
|
2072
|
+
message.thinking = { content: message.reasoning_content };
|
|
2073
|
+
delete message.reasoning_content;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
function mapUsage(usage) {
|
|
2077
|
+
if (!usage || typeof usage !== "object") return;
|
|
2078
|
+
const cached = usage.prompt_tokens_details?.cached_tokens;
|
|
2079
|
+
const written = usage.prompt_tokens_details?.cache_write_tokens;
|
|
2080
|
+
if (Number.isFinite(usage.prompt_tokens) && Number.isFinite(cached) && Number.isFinite(written) && usage.prompt_tokens < cached + written) usage.prompt_tokens += cached + written;
|
|
2081
|
+
if (Number.isFinite(written)) usage.cache_creation_input_tokens = written;
|
|
2082
|
+
}
|
|
2083
|
+
function frameInfo(raw) {
|
|
2084
|
+
const status = Number(raw?.code ?? raw?.status ?? 0);
|
|
2085
|
+
const text = `${raw?.message ?? ""} ${raw?.type ?? ""}`.toLowerCase();
|
|
2086
|
+
if (status === 429 || /rate.?limit|usage.?limit|quota/.test(text)) {
|
|
2087
|
+
const seconds = Number(raw?.retry_after);
|
|
2088
|
+
return {
|
|
2089
|
+
code: "qoder_rate_limited",
|
|
2090
|
+
httpStatus: 429,
|
|
2091
|
+
cooldownMs: Number.isFinite(seconds) && seconds > 0 ? Math.min(Math.max(seconds * 1e3, 3e4), 30 * 6e4) : 3e5
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
if (status === 401 || /unauthorized|invalid.?token|authentication|sign.?in/.test(text)) {
|
|
2095
|
+
return { code: "qoder_reauth_required", httpStatus: 401 };
|
|
2096
|
+
}
|
|
2097
|
+
return { code: "qoder_upstream_error", httpStatus: status >= 400 && status <= 599 ? status : 502 };
|
|
2098
|
+
}
|
|
2099
|
+
function mapChunk(event) {
|
|
2100
|
+
if (event?.error) {
|
|
2101
|
+
const info = frameInfo(event.error);
|
|
2102
|
+
throw Object.assign(new Error(info.code), {
|
|
2103
|
+
code: info.code,
|
|
2104
|
+
statusCode: info.httpStatus,
|
|
2105
|
+
nonRetryable: true,
|
|
2106
|
+
...info.cooldownMs ? { cooldownMs: info.cooldownMs } : {}
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
for (const c of event?.choices ?? []) {
|
|
2110
|
+
mapMessage(c.message);
|
|
2111
|
+
mapMessage(c.delta);
|
|
2112
|
+
}
|
|
2113
|
+
mapUsage(event?.usage);
|
|
2114
|
+
return event;
|
|
2115
|
+
}
|
|
2116
|
+
function fire(callback, ...args) {
|
|
2117
|
+
try {
|
|
2118
|
+
const noted = callback?.(...args);
|
|
2119
|
+
if (noted && typeof noted.then === "function") noted.then(void 0, () => {
|
|
2120
|
+
});
|
|
2121
|
+
} catch {
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
async function qoderResponse(response, note, heal) {
|
|
2125
|
+
if (!response.ok) {
|
|
2126
|
+
let body;
|
|
2127
|
+
try {
|
|
2128
|
+
body = await response.json();
|
|
2129
|
+
} catch {
|
|
2130
|
+
}
|
|
2131
|
+
const info = classifyQoderError(response.status, body);
|
|
2132
|
+
await response.body?.cancel().catch(() => {
|
|
2133
|
+
});
|
|
2134
|
+
throw Object.assign(new Error(info.code), {
|
|
2135
|
+
code: info.code,
|
|
2136
|
+
statusCode: info.httpStatus,
|
|
2137
|
+
nonRetryable: !info.retryable,
|
|
2138
|
+
...info.cooldownMs ? { cooldownMs: info.cooldownMs } : {}
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
const headers = new Headers(response.headers);
|
|
2142
|
+
headers.delete("content-length");
|
|
2143
|
+
if (!headers.get("content-type")?.includes("text/event-stream")) {
|
|
2144
|
+
let event;
|
|
2145
|
+
try {
|
|
2146
|
+
event = mapChunk(await response.json());
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
throw error?.code ? error : qoderError("qoder_upstream_error", 502);
|
|
2149
|
+
}
|
|
2150
|
+
const message = event?.choices?.[0]?.message;
|
|
2151
|
+
if (!message || !(message.content || message.tool_calls?.length)) throw qoderError("qoder_upstream_error", 502);
|
|
2152
|
+
fire(heal);
|
|
2153
|
+
return Response.json(event, { headers });
|
|
2154
|
+
}
|
|
2155
|
+
if (!response.body) throw qoderError("qoder_stream_truncated", 502);
|
|
2156
|
+
const reader = response.body.getReader();
|
|
2157
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
2158
|
+
const encoder = new TextEncoder();
|
|
2159
|
+
let pending = "", done = false, finished = false, visible = false, healed = false;
|
|
2160
|
+
let pendingBytes = 0;
|
|
2161
|
+
const stream = new ReadableStream({
|
|
2162
|
+
async pull(controller) {
|
|
2163
|
+
try {
|
|
2164
|
+
while (true) {
|
|
2165
|
+
const boundary = /\r\n\r\n|\n\n|\r\r/.exec(pending);
|
|
2166
|
+
if (boundary) {
|
|
2167
|
+
const raw = pending.slice(0, boundary.index);
|
|
2168
|
+
const frameBytes = encoder.encode(raw).byteLength;
|
|
2169
|
+
if (frameBytes > 1048576) throw qoderError("qoder_stream_invalid", 502);
|
|
2170
|
+
pending = pending.slice(boundary.index + boundary[0].length);
|
|
2171
|
+
pendingBytes -= frameBytes + boundary[0].length;
|
|
2172
|
+
const data = raw.split(/\r\n|\n|\r/).filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart()).join("\n");
|
|
2173
|
+
if (!data) continue;
|
|
2174
|
+
if (data === "[DONE]") {
|
|
2175
|
+
if (!finished || !visible) throw qoderError("qoder_stream_truncated", 502);
|
|
2176
|
+
done = true;
|
|
2177
|
+
if (!healed) {
|
|
2178
|
+
healed = true;
|
|
2179
|
+
fire(heal);
|
|
2180
|
+
}
|
|
2181
|
+
await reader.cancel();
|
|
2182
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
2183
|
+
controller.close();
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
let event;
|
|
2187
|
+
try {
|
|
2188
|
+
event = mapChunk(JSON.parse(data));
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
if (error?.code === "qoder_rate_limited" || error?.code === "qoder_reauth_required" || error?.code === "qoder_upstream_error") {
|
|
2191
|
+
fire(note, {
|
|
2192
|
+
code: error.code,
|
|
2193
|
+
httpStatus: error.statusCode,
|
|
2194
|
+
...error.cooldownMs ? { cooldownMs: error.cooldownMs } : {}
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
throw error;
|
|
2198
|
+
}
|
|
2199
|
+
for (const c of event.choices ?? []) {
|
|
2200
|
+
const reasoningVisible = typeof c.delta?.reasoning_content === "string" && c.delta.reasoning_content.length > 0;
|
|
2201
|
+
if (reasoningVisible || c.delta?.content || c.delta?.tool_calls?.length) visible = true;
|
|
2202
|
+
if (c.finish_reason) finished = true;
|
|
2203
|
+
}
|
|
2204
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}
|
|
2205
|
+
|
|
2206
|
+
`));
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
if (pending.length > 1048576) throw qoderError("qoder_stream_invalid", 502);
|
|
2210
|
+
const next = await reader.read();
|
|
2211
|
+
if (next.done) {
|
|
2212
|
+
pending += decoder.decode();
|
|
2213
|
+
if (!done) throw qoderError("qoder_stream_truncated", 502);
|
|
2214
|
+
controller.close();
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
pending += decoder.decode(next.value, { stream: true });
|
|
2218
|
+
pendingBytes += next.value.byteLength;
|
|
2219
|
+
if (pendingBytes > 2097152) throw qoderError("qoder_stream_invalid", 502);
|
|
2220
|
+
}
|
|
2221
|
+
} catch (error) {
|
|
2222
|
+
await reader.cancel().catch(() => {
|
|
2223
|
+
});
|
|
2224
|
+
controller.error(error);
|
|
2225
|
+
}
|
|
2226
|
+
},
|
|
2227
|
+
cancel(reason) {
|
|
2228
|
+
return reader.cancel(reason);
|
|
2229
|
+
}
|
|
2230
|
+
});
|
|
2231
|
+
return new Response(stream, { headers });
|
|
2232
|
+
}
|
|
2233
|
+
var QoderTransformer = class {
|
|
2234
|
+
constructor(engine, note, heal) {
|
|
2235
|
+
this.engine = engine;
|
|
2236
|
+
this.note = note;
|
|
2237
|
+
this.heal = heal;
|
|
2238
|
+
}
|
|
2239
|
+
static TransformerName = "qoder";
|
|
2240
|
+
name = "qoder";
|
|
2241
|
+
async transformRequestIn(request, _provider, context) {
|
|
2242
|
+
return { body: qoderRequest(request, context) };
|
|
2243
|
+
}
|
|
2244
|
+
async transformResponseOut(response) {
|
|
2245
|
+
return qoderResponse(response, this.note, this.heal);
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
|
|
2249
|
+
// src/managed-runtime.ts
|
|
2250
|
+
async function chatFailure(response) {
|
|
2251
|
+
let body;
|
|
2252
|
+
try {
|
|
2253
|
+
body = await response.json();
|
|
2254
|
+
} catch {
|
|
2255
|
+
}
|
|
2256
|
+
const info = classifyQoderError(response.status, body);
|
|
2257
|
+
return Object.assign(new Error(info.code), {
|
|
2258
|
+
code: info.code,
|
|
2259
|
+
message: info.code,
|
|
2260
|
+
statusCode: info.httpStatus,
|
|
2261
|
+
pluginClassified: true,
|
|
2262
|
+
nonRetryable: !info.retryable,
|
|
2263
|
+
...info.cooldownMs ? { cooldownMs: info.cooldownMs } : {}
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
function classifiedInfo(error) {
|
|
2267
|
+
if (!error?.pluginClassified) return null;
|
|
2268
|
+
return {
|
|
2269
|
+
code: String(error.code ?? "qoder_upstream_error"),
|
|
2270
|
+
message: String(error.message ?? error.code ?? "qoder_upstream_error"),
|
|
2271
|
+
httpStatus: Number(error.statusCode) || 502,
|
|
2272
|
+
...Number.isFinite(error.cooldownMs) ? { cooldownMs: error.cooldownMs } : {}
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
function streamError(error, aborted) {
|
|
2276
|
+
if (aborted || error instanceof Error && error.name === "AbortError") {
|
|
2277
|
+
return Object.assign(new Error(error instanceof Error ? error.message : "aborted"), { name: "AbortError" });
|
|
2278
|
+
}
|
|
2279
|
+
return qoderError("qoder_upstream_error", 502);
|
|
2280
|
+
}
|
|
2281
|
+
function createQoderManagedRuntime(service, host, site = "qoder", timeoutMs = 6e5) {
|
|
2282
|
+
const managedBy = qoderSite(site);
|
|
2283
|
+
return {
|
|
2284
|
+
// Name = site id: the core keys runtimes (and 401 bookkeeping) by it.
|
|
2285
|
+
name: managedBy.id,
|
|
2286
|
+
isManaged: (provider) => isQoderManaged(provider, managedBy.id),
|
|
2287
|
+
suppressDebugLog: true,
|
|
2288
|
+
ensureUsable: () => service.ensureUsable(),
|
|
2289
|
+
lockUpstream: async (config) => {
|
|
2290
|
+
config.url = managedBy.chatUrl;
|
|
2291
|
+
config.redirect = "error";
|
|
2292
|
+
config.dispatcher = host.getSubscriptionDispatcher?.() ?? host.getDirectDispatcher();
|
|
2293
|
+
},
|
|
2294
|
+
// COSY signing happens inside the vendored engine's chat(); the core's
|
|
2295
|
+
// header chain carries nothing provider-specific.
|
|
2296
|
+
signRequest: async () => ({}),
|
|
2297
|
+
classifyError: (status, body) => classifyQoderError(status, body),
|
|
2298
|
+
// Secondary net for HTTP-200 hidden error envelopes (host contract types.ts).
|
|
2299
|
+
isErrorEnvelope: (body) => isQoderErrorEnvelope(body),
|
|
2300
|
+
noteUpstreamError: (info) => service.noteUpstreamError(info),
|
|
2301
|
+
// The core owns exactly one data-plane 401 replay; the engine owns the rest.
|
|
2302
|
+
authRefresh: { forceRefresh: () => service.forceTokenRefresh() },
|
|
2303
|
+
// No core attempt(): that would send the marker credential to the SSE API.
|
|
2304
|
+
// The vendored engine owns one bounded chat transaction (queue wait and
|
|
2305
|
+
// this wrapper owns exactly one bounded 401 force-refresh/replay).
|
|
2306
|
+
async dispatchRequest({ requestBody, config, req }) {
|
|
2307
|
+
const controller = new AbortController();
|
|
2308
|
+
const abort = () => controller.abort();
|
|
2309
|
+
const socket = req?.raw?.socket;
|
|
2310
|
+
req?.raw?.on?.("aborted", abort);
|
|
2311
|
+
socket?.on?.("close", abort);
|
|
2312
|
+
if (req?.raw?.aborted) controller.abort();
|
|
2313
|
+
const deadline = Number(config?.TIMEOUT ?? timeoutMs);
|
|
2314
|
+
const signal = AbortSignal.any([
|
|
2315
|
+
controller.signal,
|
|
2316
|
+
AbortSignal.timeout(Number.isFinite(deadline) && deadline > 0 ? Math.min(deadline, 36e5) : timeoutMs),
|
|
2317
|
+
...config?.signal instanceof AbortSignal ? [config.signal] : [],
|
|
2318
|
+
...req?.signal instanceof AbortSignal ? [req.signal] : []
|
|
2319
|
+
]);
|
|
2320
|
+
const cleanup = () => {
|
|
2321
|
+
req?.raw?.off?.("aborted", abort);
|
|
2322
|
+
socket?.off?.("close", abort);
|
|
2323
|
+
};
|
|
2324
|
+
const send = async () => service.dispatch(requestBody, signal);
|
|
2325
|
+
const wrap = (response) => {
|
|
2326
|
+
if (!response.body) {
|
|
2327
|
+
cleanup();
|
|
2328
|
+
return response;
|
|
2329
|
+
}
|
|
2330
|
+
const reader = response.body.getReader();
|
|
2331
|
+
return new Response(new ReadableStream({
|
|
2332
|
+
async pull(ctl) {
|
|
2333
|
+
try {
|
|
2334
|
+
const next = await reader.read();
|
|
2335
|
+
if (next.done) {
|
|
2336
|
+
cleanup();
|
|
2337
|
+
ctl.close();
|
|
2338
|
+
} else ctl.enqueue(next.value);
|
|
2339
|
+
} catch (error) {
|
|
2340
|
+
cleanup();
|
|
2341
|
+
ctl.error(streamError(error, signal.aborted));
|
|
2342
|
+
}
|
|
2343
|
+
},
|
|
2344
|
+
async cancel(reason) {
|
|
2345
|
+
abort();
|
|
2346
|
+
cleanup();
|
|
2347
|
+
await reader.cancel(reason).catch(() => {
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
}), { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
2351
|
+
};
|
|
2352
|
+
try {
|
|
2353
|
+
let response = await send();
|
|
2354
|
+
if (response.status === 401) {
|
|
2355
|
+
await response.body?.cancel().catch(() => {
|
|
2356
|
+
});
|
|
2357
|
+
await service.forceTokenRefresh();
|
|
2358
|
+
response = await send();
|
|
2359
|
+
}
|
|
2360
|
+
if (!response.ok) {
|
|
2361
|
+
const error = await chatFailure(response);
|
|
2362
|
+
const info = classifiedInfo(error);
|
|
2363
|
+
if (info) await service.noteUpstreamError(info).catch(() => {
|
|
2364
|
+
});
|
|
2365
|
+
throw error;
|
|
2366
|
+
}
|
|
2367
|
+
return wrap(response);
|
|
2368
|
+
} catch (error) {
|
|
2369
|
+
cleanup();
|
|
2370
|
+
if (signal.aborted || error?.name === "AbortError") {
|
|
2371
|
+
throw Object.assign(new Error(error?.message ?? "aborted"), { name: "AbortError" });
|
|
2372
|
+
}
|
|
2373
|
+
const info = classifiedInfo(error);
|
|
2374
|
+
if (info) await service.noteUpstreamError(info).catch(() => {
|
|
2375
|
+
});
|
|
2376
|
+
throw error?.pluginClassified ? error : Object.assign(qoderError("qoder_upstream_error", 502), { pluginClassified: true });
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// src/admin-routes.ts
|
|
2383
|
+
function assertLocalQoderRequest(req, app) {
|
|
2384
|
+
const deny = () => {
|
|
2385
|
+
throw Object.assign(new Error("qoder_unauthorized"), { statusCode: 403 });
|
|
2386
|
+
};
|
|
2387
|
+
const remote = req.raw?.socket?.remoteAddress ?? req.socket?.remoteAddress ?? req.connection?.remoteAddress ?? req.ip;
|
|
2388
|
+
if (!["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(remote)) deny();
|
|
2389
|
+
const address = app.server.address();
|
|
2390
|
+
const port = address && typeof address === "object" ? address.port : 3456;
|
|
2391
|
+
const allowed = /* @__PURE__ */ new Set([`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`]);
|
|
2392
|
+
if (typeof req.headers?.host !== "string" || !allowed.has(req.headers.host)) deny();
|
|
2393
|
+
if (req.headers.origin !== void 0) {
|
|
2394
|
+
try {
|
|
2395
|
+
const origin = new URL(req.headers.origin);
|
|
2396
|
+
if (!["http:", "https:"].includes(origin.protocol) || origin.username || origin.password || !allowed.has(origin.host)) deny();
|
|
2397
|
+
} catch {
|
|
2398
|
+
deny();
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
function registerQoderAdminRoutes(app, service, site = "qoder") {
|
|
2403
|
+
const routeBase = qoderSite(site).routeBase;
|
|
2404
|
+
const routes = [
|
|
2405
|
+
["GET", "status", () => service.getStatus()],
|
|
2406
|
+
["POST", "connect", (body) => {
|
|
2407
|
+
if (!validPAT(body?.pat)) throw Object.assign(new Error(), { code: "qoder_pat_invalid", statusCode: 422 });
|
|
2408
|
+
return service.connectPAT(body.pat);
|
|
2409
|
+
}],
|
|
2410
|
+
["POST", "sync", () => service.syncModels()],
|
|
2411
|
+
["POST", "usage", () => service.getUsage()],
|
|
2412
|
+
["POST", "models/visibility", (body) => {
|
|
2413
|
+
if (typeof body?.model !== "string" || typeof body.hidden !== "boolean") throw Object.assign(new Error(), { code: "qoder_model_unknown", statusCode: 422 });
|
|
2414
|
+
return service.setModelHidden(body.model, body.hidden);
|
|
2415
|
+
}],
|
|
2416
|
+
["POST", "disconnect", () => service.disconnect()]
|
|
2417
|
+
];
|
|
2418
|
+
const safe = /* @__PURE__ */ new Set(["qoder_unauthorized", "qoder_pat_invalid", "qoder_already_connected", "qoder_not_connected", "qoder_reauth_required", "qoder_persist_failed", "qoder_config_write_failed", "qoder_provider_name_conflict", "qoder_credential_invalid", "qoder_credential_unreadable", "qoder_raw_config_required", "qoder_proxy_invalid", "qoder_upstream_error", "qoder_rate_limited", "qoder_request_failed", "qoder_model_unknown"]);
|
|
2419
|
+
for (const [method, path, action] of routes) app.route({ method, url: `${routeBase}/${path}`, handler: async (req, reply) => {
|
|
2420
|
+
try {
|
|
2421
|
+
assertLocalQoderRequest(req, app);
|
|
2422
|
+
return await action(req.body);
|
|
2423
|
+
} catch (e) {
|
|
2424
|
+
const unauthorized = e?.statusCode === 403;
|
|
2425
|
+
const code = unauthorized ? "qoder_unauthorized" : safe.has(e?.code) ? e.code : "qoder_request_failed";
|
|
2426
|
+
const status = unauthorized ? 403 : Number.isInteger(e?.statusCode) && e.statusCode !== 401 && e.statusCode >= 400 && e.statusCode <= 599 ? e.statusCode : 500;
|
|
2427
|
+
reply.status(status);
|
|
2428
|
+
return { success: false, error: code };
|
|
2429
|
+
}
|
|
2430
|
+
} });
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// src/locales.ts
|
|
2434
|
+
var qoderLocales = {
|
|
2435
|
+
en: { qoder: {
|
|
2436
|
+
title: "Qoder Subscription",
|
|
2437
|
+
description: "Connects a qoder.com personal access token (PAT). Tokens stay on this machine, are never logged and are cleared from the form after connecting.",
|
|
2438
|
+
cnTitle: "Qoder CN Subscription",
|
|
2439
|
+
cnDescription: "Connects a qoder.cn (Qoder China) personal access token (PAT). Tokens stay on this machine, are never logged and are cleared from the form after connecting.",
|
|
2440
|
+
loading: "Loading\u2026",
|
|
2441
|
+
not_connected: "Not connected",
|
|
2442
|
+
connected: "Connected",
|
|
2443
|
+
expired: "Sign-in lapsed \u2014 reconnect with a fresh PAT",
|
|
2444
|
+
error: "Management request failed",
|
|
2445
|
+
account: "Account",
|
|
2446
|
+
models: "Models",
|
|
2447
|
+
models_count: "{{count}} models",
|
|
2448
|
+
pat: "Personal access token",
|
|
2449
|
+
patPlaceholder: "pt-\u2026",
|
|
2450
|
+
connect: "Connect",
|
|
2451
|
+
sync: "Refresh models",
|
|
2452
|
+
usage: "Refresh quota",
|
|
2453
|
+
unknown: "Quota unknown",
|
|
2454
|
+
stale: "Last known quota (stale)",
|
|
2455
|
+
used: "Allowance",
|
|
2456
|
+
restore_partial: "Partially completed: restored {{count}} of {{total}} models. Management request failed.",
|
|
2457
|
+
hide_partial: "Partially completed: hid {{count}} of {{total}} models. Management request failed.",
|
|
2458
|
+
restore_model: "Restore {{model}}",
|
|
2459
|
+
disconnect: "Disconnect",
|
|
2460
|
+
hide_model: "Hide {{model}}",
|
|
2461
|
+
hidden_models: "Hidden models ({{count}})",
|
|
2462
|
+
restore: "Restore",
|
|
2463
|
+
restore_all: "Restore all",
|
|
2464
|
+
cooling: "Rate limited until",
|
|
2465
|
+
noModels: "Refresh models after connecting",
|
|
2466
|
+
free: "free",
|
|
2467
|
+
refresh: "Refresh status"
|
|
2468
|
+
} },
|
|
2469
|
+
zh: { qoder: {
|
|
2470
|
+
title: "Qoder \u8BA2\u9605",
|
|
2471
|
+
description: "\u4F7F\u7528 qoder.com \u4E2A\u4EBA\u8BBF\u95EE\u4EE4\u724C\uFF08PAT\uFF09\u8FDE\u63A5\u3002\u4EE4\u724C\u4EC5\u4FDD\u5B58\u5728\u672C\u673A\uFF0C\u4E0D\u4F1A\u88AB\u8BB0\u5F55\uFF0C\u8FDE\u63A5\u6210\u529F\u540E\u5373\u4ECE\u8868\u5355\u6E05\u9664\u3002",
|
|
2472
|
+
cnTitle: "Qoder \u56FD\u5185\u7248\u8BA2\u9605",
|
|
2473
|
+
cnDescription: "\u4F7F\u7528 qoder.cn\uFF08Qoder \u56FD\u5185\u7248\uFF09\u4E2A\u4EBA\u8BBF\u95EE\u4EE4\u724C\uFF08PAT\uFF09\u8FDE\u63A5\u3002\u4EE4\u724C\u4EC5\u4FDD\u5B58\u5728\u672C\u673A\uFF0C\u4E0D\u4F1A\u88AB\u8BB0\u5F55\uFF0C\u8FDE\u63A5\u6210\u529F\u540E\u5373\u4ECE\u8868\u5355\u6E05\u9664\u3002",
|
|
2474
|
+
loading: "\u52A0\u8F7D\u4E2D\u2026",
|
|
2475
|
+
not_connected: "\u672A\u8FDE\u63A5",
|
|
2476
|
+
connected: "\u5DF2\u8FDE\u63A5",
|
|
2477
|
+
expired: "\u767B\u5F55\u5DF2\u5931\u6548 \u2014 \u8BF7\u4F7F\u7528\u65B0\u7684 PAT \u91CD\u65B0\u8FDE\u63A5",
|
|
2478
|
+
error: "\u7BA1\u7406\u64CD\u4F5C\u5931\u8D25",
|
|
2479
|
+
account: "\u8D26\u53F7",
|
|
2480
|
+
models: "\u6A21\u578B",
|
|
2481
|
+
models_count: "{{count}} \u4E2A\u6A21\u578B",
|
|
2482
|
+
pat: "\u4E2A\u4EBA\u8BBF\u95EE\u4EE4\u724C",
|
|
2483
|
+
patPlaceholder: "pt-\u2026",
|
|
2484
|
+
connect: "\u8FDE\u63A5",
|
|
2485
|
+
sync: "\u5237\u65B0\u6A21\u578B",
|
|
2486
|
+
usage: "\u5237\u65B0\u989D\u5EA6",
|
|
2487
|
+
unknown: "\u989D\u5EA6\u672A\u77E5",
|
|
2488
|
+
stale: "\u4E0A\u6B21\u989D\u5EA6\uFF08\u5DF2\u8FC7\u671F\uFF09",
|
|
2489
|
+
used: "\u5DF2\u7528\u989D\u5EA6",
|
|
2490
|
+
restore_partial: "\u90E8\u5206\u5B8C\u6210\uFF1A\u5DF2\u6062\u590D {{count}} \u4E2A\u6A21\u578B\uFF0C\u5171 {{total}} \u4E2A\u3002\u7BA1\u7406\u64CD\u4F5C\u5931\u8D25\u3002",
|
|
2491
|
+
hide_partial: "\u90E8\u5206\u5B8C\u6210\uFF1A\u5DF2\u9690\u85CF {{count}} \u4E2A\u6A21\u578B\uFF0C\u5171 {{total}} \u4E2A\u3002\u7BA1\u7406\u64CD\u4F5C\u5931\u8D25\u3002",
|
|
2492
|
+
restore_model: "\u6062\u590D {{model}}",
|
|
2493
|
+
disconnect: "\u65AD\u5F00\u8FDE\u63A5",
|
|
2494
|
+
hide_model: "\u9690\u85CF {{model}}",
|
|
2495
|
+
hidden_models: "\u5DF2\u9690\u85CF\u6A21\u578B\uFF08{{count}}\uFF09",
|
|
2496
|
+
restore: "\u6062\u590D",
|
|
2497
|
+
restore_all: "\u6062\u590D\u5168\u90E8",
|
|
2498
|
+
cooling: "\u9650\u6D41\u81F3",
|
|
2499
|
+
noModels: "\u8FDE\u63A5\u540E\u8BF7\u5237\u65B0\u6A21\u578B",
|
|
2500
|
+
free: "\u514D\u8D39",
|
|
2501
|
+
refresh: "\u5237\u65B0\u72B6\u6001"
|
|
2502
|
+
} }
|
|
2503
|
+
};
|
|
2504
|
+
|
|
2505
|
+
// src/plugin.ts
|
|
2506
|
+
var manifest = { name: "qoder", version: "0.2.0", coreApi: "1", description: "Qoder (qoder.com / qoder.cn) subscription provider with PAT sign-in, model management and usage." };
|
|
2507
|
+
function activatedTransformer(site, service) {
|
|
2508
|
+
return class ActivatedQoderTransformer extends QoderTransformer {
|
|
2509
|
+
static TransformerName = site;
|
|
2510
|
+
name = site;
|
|
2511
|
+
constructor() {
|
|
2512
|
+
super(service.engine, (info) => service.noteUpstreamError(info), () => service.noteUpstreamSuccess());
|
|
2513
|
+
}
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
async function activate(ctx) {
|
|
2517
|
+
const services = new Map(QODER_SITES.map((site) => [site.id, new QoderAuthService({ host: ctx.host, site: site.id })]));
|
|
2518
|
+
for (const site of QODER_SITES) {
|
|
2519
|
+
const service = services.get(site.id);
|
|
2520
|
+
ctx.registerTransformer(activatedTransformer(site.id, service));
|
|
2521
|
+
ctx.registerManagedRuntime(createQoderManagedRuntime(service, ctx.host, site.id));
|
|
2522
|
+
ctx.registerAdminRoutes((app) => registerQoderAdminRoutes(app, service, site.id));
|
|
2523
|
+
}
|
|
2524
|
+
ctx.registerConfigProtector(protectQoderConfig);
|
|
2525
|
+
ctx.registerUiCard({ cardPath: "card.js" });
|
|
2526
|
+
ctx.registerLocales(qoderLocales);
|
|
2527
|
+
ctx.log.info("qoder plugin active");
|
|
2528
|
+
}
|
|
2529
|
+
var plugin_default = { manifest, activate };
|
|
2530
|
+
export {
|
|
2531
|
+
activate,
|
|
2532
|
+
plugin_default as default,
|
|
2533
|
+
manifest
|
|
2534
|
+
};
|