dsh-provider-qoder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,1778 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, attributionHeaders } from "@deepseek-ai/dsh-llm";
3
+ import crypto from "node:crypto";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
8
+ //#region src/endpoints.ts
9
+ const qoderRegionEndpoints = {
10
+ global: {
11
+ baseUrl: "https://api3.qoder.sh/",
12
+ openApiUrl: "https://openapi.qoder.sh"
13
+ },
14
+ china: {
15
+ baseUrl: "https://gateway.qoder.com.cn/",
16
+ openApiUrl: "https://openapi.qoder.com.cn"
17
+ }
18
+ };
19
+ const qoderGlobalBaseUrl = qoderRegionEndpoints.global.baseUrl;
20
+ const qoderGlobalOpenApiUrl = qoderRegionEndpoints.global.openApiUrl;
21
+ function resolveQoderEndpoints(region = "global") {
22
+ return qoderRegionEndpoints[region] ?? qoderRegionEndpoints.global;
23
+ }
24
+ function getQoderChatUrl(region = "global") {
25
+ const { baseUrl } = resolveQoderEndpoints(region);
26
+ return `${baseUrl}algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1`;
27
+ }
28
+ function getQoderModelListUrl(region = "global") {
29
+ const { baseUrl } = resolveQoderEndpoints(region);
30
+ return `${baseUrl}algo/api/v2/model/list?Encode=1`;
31
+ }
32
+ function getQoderExchangeUrl(region = "global") {
33
+ const { openApiUrl } = resolveQoderEndpoints(region);
34
+ return `${openApiUrl}/api/v1/jobToken/exchange`;
35
+ }
36
+ function getQoderUserInfoUrl(region = "global") {
37
+ const { openApiUrl } = resolveQoderEndpoints(region);
38
+ return `${openApiUrl}/api/v1/userinfo`;
39
+ }
40
+ function getQoderUsageUrl(region = "global") {
41
+ const { openApiUrl } = resolveQoderEndpoints(region);
42
+ return `${openApiUrl}/api/v2/quota/usage`;
43
+ }
44
+ //#endregion
45
+ //#region src/errors.ts
46
+ /**
47
+ * DSH-compatible LLM error representation.
48
+ *
49
+ * @module dsh-provider-qoder/errors
50
+ */
51
+ var QoderLlmError = class extends LlmError {
52
+ constructor(message, code = "UNKNOWN_ERROR", options) {
53
+ super(message, code, options);
54
+ }
55
+ };
56
+ function qoderHttpError(message, response) {
57
+ const { status } = response;
58
+ const code = status === 401 || status === 403 ? "AUTH" : status === 408 ? "TIMEOUT" : status === 429 ? "RATE_LIMIT" : status >= 500 && status <= 599 ? "SERVER" : status >= 400 && status <= 499 ? "INVALID_REQUEST" : "PROVIDER_ERROR";
59
+ const providerRetryAfterMs = retryAfterMs(response.headers?.get("retry-after") ?? null);
60
+ return new QoderLlmError(message, code, {
61
+ status,
62
+ ...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs }
63
+ });
64
+ }
65
+ function retryAfterMs(value, nowMs = Date.now()) {
66
+ const normalized = value?.trim();
67
+ if (!normalized) return void 0;
68
+ if (/^\d+$/u.test(normalized)) {
69
+ const delayMs = Number(normalized) * 1e3;
70
+ return Number.isFinite(delayMs) && delayMs > 0 ? delayMs : void 0;
71
+ }
72
+ const retryAt = Date.parse(normalized);
73
+ if (Number.isNaN(retryAt)) return void 0;
74
+ const delayMs = retryAt - nowMs;
75
+ return delayMs > 0 ? delayMs : void 0;
76
+ }
77
+ //#endregion
78
+ //#region src/logging.ts
79
+ const sensitiveKey = /(?:authorization|credential|password|secret|token)/iu;
80
+ const identifierKey = /^(?:accountId|id|uid|userId)$/iu;
81
+ const tokenValue = /\b(?:jrt|jt|pt)-[\w.-]+\b/giu;
82
+ const bearerValue = /Bearer\s+[^\s,;]+/giu;
83
+ const maxDepth = 4;
84
+ const maxEntries = 50;
85
+ const maxStringLength = 500;
86
+ function maskIdentifier(value) {
87
+ const text = String(value);
88
+ return text.length <= 4 ? "[REDACTED]" : `…${text.slice(-4)}`;
89
+ }
90
+ function maskEmail(value) {
91
+ const text = String(value);
92
+ const at = text.indexOf("@");
93
+ if (at <= 0) return "[REDACTED]";
94
+ return `${text.slice(0, 1)}***${text.slice(at)}`;
95
+ }
96
+ function redactString(value) {
97
+ return (value.length > maxStringLength ? `${value.slice(0, maxStringLength)}…` : value).replace(tokenValue, "[REDACTED]").replace(bearerValue, "Bearer [REDACTED]");
98
+ }
99
+ /** Redact credentials and bound arbitrary provider values before logging them. */
100
+ function redactLogValue(value, depth = 0) {
101
+ if (typeof value === "string") return redactString(value);
102
+ if (value === null || typeof value !== "object") return value;
103
+ if (depth >= maxDepth) return "[TRUNCATED]";
104
+ if (value instanceof Error) return {
105
+ name: value.name,
106
+ message: redactString(value.message),
107
+ ..."code" in value ? { code: redactLogValue(value.code, depth + 1) } : {}
108
+ };
109
+ if (Array.isArray(value)) return value.slice(0, maxEntries).map((item) => redactLogValue(item, depth + 1));
110
+ return Object.fromEntries(Object.entries(value).slice(0, maxEntries).map(([key, item]) => {
111
+ if (sensitiveKey.test(key)) return [key, "[REDACTED]"];
112
+ if (identifierKey.test(key)) return [key, maskIdentifier(item)];
113
+ if (key.toLowerCase() === "email") return [key, maskEmail(item)];
114
+ return [key, redactLogValue(item, depth + 1)];
115
+ }));
116
+ }
117
+ /** Parse JSON-shaped diagnostics when possible, then apply the same redaction boundary. */
118
+ function redactLogPayload(text) {
119
+ try {
120
+ return redactLogValue(JSON.parse(text));
121
+ } catch {
122
+ return redactLogValue(text);
123
+ }
124
+ }
125
+ //#endregion
126
+ //#region src/machine-id.ts
127
+ function defaultMachineIdPaths() {
128
+ return [join(homedir(), ".qoder", ".auth", "machine_id"), join(homedir(), ".dsh", "qoder", "machine_id")];
129
+ }
130
+ /** Read Qoder's machine id or create the DSH-owned fallback. */
131
+ function getMachineId(paths = defaultMachineIdPaths()) {
132
+ for (const path of paths) {
133
+ if (!existsSync(path)) continue;
134
+ try {
135
+ const value = readFileSync(path, "utf8").trim();
136
+ if (value) return value;
137
+ } catch {}
138
+ }
139
+ const machineId = crypto.randomUUID();
140
+ const savePath = paths.at(-1);
141
+ if (savePath !== void 0) try {
142
+ mkdirSync(dirname(savePath), { recursive: true });
143
+ writeFileSync(savePath, machineId, {
144
+ encoding: "utf8",
145
+ flag: "wx"
146
+ });
147
+ } catch {
148
+ try {
149
+ const existing = readFileSync(savePath, "utf8").trim();
150
+ if (existing) return existing;
151
+ } catch {}
152
+ }
153
+ return machineId;
154
+ }
155
+ //#endregion
156
+ //#region src/auth.ts
157
+ const userAgent$1 = "dsh-provider-qoder";
158
+ const expiryBufferMs = 300 * 1e3;
159
+ const defaultExpiryMs = 1440 * 60 * 1e3;
160
+ const defaultAuthTimeoutMs = 15e3;
161
+ function abortedError() {
162
+ return new QoderLlmError("Qoder authentication was aborted.", "ABORTED");
163
+ }
164
+ async function waitForFlight(promise, signal) {
165
+ if (signal === void 0) return promise;
166
+ if (signal.aborted) throw abortedError();
167
+ return new Promise((resolve, reject) => {
168
+ const onAbort = () => reject(abortedError());
169
+ signal.addEventListener("abort", onAbort, { once: true });
170
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
171
+ });
172
+ }
173
+ var QoderAuthService = class {
174
+ cache = /* @__PURE__ */ new Map();
175
+ inFlight = /* @__PURE__ */ new Map();
176
+ fetchImpl;
177
+ timeoutMs;
178
+ resolveMachineId;
179
+ resolveRegion;
180
+ region;
181
+ logger;
182
+ constructor(options = {}) {
183
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
184
+ this.timeoutMs = options.timeoutMs ?? defaultAuthTimeoutMs;
185
+ this.resolveMachineId = options.resolveMachineId ?? getMachineId;
186
+ this.resolveRegion = options.resolveRegion;
187
+ this.region = options.region;
188
+ this.logger = options.logger;
189
+ }
190
+ setRegion(region) {
191
+ this.region = region;
192
+ }
193
+ currentRegion() {
194
+ if (this.resolveRegion) return this.resolveRegion();
195
+ return this.region ?? "global";
196
+ }
197
+ clear(pat) {
198
+ if (pat) {
199
+ for (const key of this.cache.keys()) if (key.endsWith(`:${pat}`) || key === pat) this.cache.delete(key);
200
+ } else this.cache.clear();
201
+ }
202
+ async getCredentials(pat, signal, region) {
203
+ if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
204
+ if (signal?.aborted) throw abortedError();
205
+ const effectiveRegion = region ?? this.currentRegion();
206
+ const cacheKey = `${effectiveRegion}:${pat}`;
207
+ const cached = this.cache.get(cacheKey);
208
+ if (cached && cached.expiresAt > Date.now() + expiryBufferMs) return cached.creds;
209
+ let entry = this.inFlight.get(cacheKey);
210
+ if (entry === void 0) {
211
+ const controller = new AbortController();
212
+ const created = {};
213
+ created.controller = controller;
214
+ created.waiters = 0;
215
+ created.settled = false;
216
+ created.timeout = setTimeout(() => controller.abort("authentication timeout"), this.timeoutMs);
217
+ created.promise = this.exchangeAndResolve(pat, controller.signal, effectiveRegion).finally(() => {
218
+ created.settled = true;
219
+ clearTimeout(created.timeout);
220
+ if (this.inFlight.get(cacheKey) === created) this.inFlight.delete(cacheKey);
221
+ });
222
+ entry = created;
223
+ this.inFlight.set(cacheKey, entry);
224
+ }
225
+ entry.waiters++;
226
+ try {
227
+ return await waitForFlight(entry.promise, signal);
228
+ } finally {
229
+ entry.waiters--;
230
+ if (entry.waiters === 0 && !entry.settled) entry.controller.abort("all callers aborted");
231
+ }
232
+ }
233
+ async exchangeAndResolve(pat, signal, region) {
234
+ let jobToken;
235
+ let expiresAt = Date.now() + defaultExpiryMs;
236
+ try {
237
+ const url = getQoderExchangeUrl(region);
238
+ this.logger?.debug?.("[Qoder Auth] Exchanging PAT for job token", {
239
+ url,
240
+ region
241
+ });
242
+ const response = await this.fetchImpl(url, {
243
+ method: "POST",
244
+ headers: {
245
+ "content-type": "application/json",
246
+ "accept": "application/json",
247
+ "user-agent": userAgent$1,
248
+ "cosy-version": "1.0.1",
249
+ "cosy-clienttype": "5"
250
+ },
251
+ body: JSON.stringify({ personal_token: pat }),
252
+ signal
253
+ });
254
+ this.logger?.debug?.("[Qoder Auth] Exchange completed", {
255
+ status: response.status,
256
+ statusText: response.statusText
257
+ });
258
+ if (!response.ok) {
259
+ const errorText = await response.text();
260
+ this.logger?.error?.("[Qoder Auth] Exchange failed", redactLogPayload(errorText));
261
+ throw qoderHttpError(`Qoder PAT exchange failed with HTTP status ${response.status}.`, response);
262
+ }
263
+ const data = await response.json();
264
+ if (!data.token) throw new QoderLlmError("Qoder PAT exchange returned no job token.", "AUTH");
265
+ jobToken = data.token;
266
+ if (data.expires_at) {
267
+ const parsed = Date.parse(data.expires_at);
268
+ if (!Number.isNaN(parsed)) expiresAt = parsed;
269
+ } else if (typeof data.expires_in === "number" && data.expires_in > 0) expiresAt = Date.now() + data.expires_in;
270
+ } catch (error) {
271
+ if (error instanceof QoderLlmError) throw error;
272
+ if (signal.aborted) throw new QoderLlmError("Qoder authentication request timed out or was cancelled.", "TIMEOUT");
273
+ throw new QoderLlmError("Qoder PAT exchange network request failed.", "TRANSPORT", { cause: error });
274
+ }
275
+ const userInfo = await this.fetchUserInfo(jobToken, signal, region);
276
+ const creds = {
277
+ userID: userInfo.userID,
278
+ authToken: jobToken,
279
+ name: userInfo.name || "Qoder User",
280
+ email: userInfo.email,
281
+ machineID: this.resolveMachineId()
282
+ };
283
+ const cacheKey = `${region}:${pat}`;
284
+ this.cache.set(cacheKey, {
285
+ creds,
286
+ expiresAt
287
+ });
288
+ return creds;
289
+ }
290
+ async fetchUserInfo(jobToken, signal, region) {
291
+ const url = getQoderUserInfoUrl(region);
292
+ this.logger?.debug?.("[Qoder UserInfo] Requesting subscriber profile", { url });
293
+ try {
294
+ const response = await this.fetchImpl(url, {
295
+ headers: {
296
+ "authorization": `Bearer ${jobToken}`,
297
+ "accept": "application/json",
298
+ "user-agent": userAgent$1,
299
+ "cosy-version": "1.0.1",
300
+ "cosy-clienttype": "5"
301
+ },
302
+ signal
303
+ });
304
+ this.logger?.debug?.("[Qoder UserInfo] Request completed", {
305
+ status: response.status,
306
+ statusText: response.statusText
307
+ });
308
+ const text = await response.text();
309
+ if (!response.ok) {
310
+ this.logger?.error?.("[Qoder UserInfo] Request failed", redactLogPayload(text));
311
+ throw qoderHttpError(`Qoder identity lookup failed with HTTP status ${response.status}.`, response);
312
+ }
313
+ let info;
314
+ try {
315
+ info = JSON.parse(text);
316
+ } catch {
317
+ this.logger?.error?.("[Qoder UserInfo] Invalid JSON response", redactLogPayload(text));
318
+ throw new QoderLlmError("Qoder identity lookup returned invalid JSON.", "AUTH");
319
+ }
320
+ this.logger?.debug?.("[Qoder UserInfo] Subscriber profile resolved", redactLogValue(info));
321
+ if (!info.id) throw new QoderLlmError("Qoder identity lookup returned no user id.", "AUTH");
322
+ return {
323
+ userID: info.id,
324
+ email: info.email ?? "",
325
+ name: info.name ?? info.username ?? ""
326
+ };
327
+ } catch (error) {
328
+ if (error instanceof QoderLlmError) throw error;
329
+ if (signal.aborted) throw new QoderLlmError("Qoder identity lookup timed out or was cancelled.", "TIMEOUT");
330
+ throw new QoderLlmError("Qoder identity lookup network request failed.", "TRANSPORT", { cause: error });
331
+ }
332
+ }
333
+ };
334
+ //#endregion
335
+ //#region src/cosy.ts
336
+ /**
337
+ * COSY authentication headers and signature algorithm for Qoder services.
338
+ *
339
+ * Implements RSA + AES + MD5 signature generation required by the upstream
340
+ * Qoder gateway.
341
+ *
342
+ * @module dsh-provider-qoder/cosy
343
+ */
344
+ const qoderRSAPublicKey = `-----BEGIN PUBLIC KEY-----
345
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
346
+ 4k6Pz1EaDicBMpdpxKduSZu5OANqUq8er4GM95omAGIOPOh+Nx0spthYA2BqGz+l
347
+ 6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
348
+ XcW+ML9FoCI6AOvOzwIDAQAB
349
+ -----END PUBLIC KEY-----`;
350
+ const qoderIdeVersion = "1.1.3";
351
+ const qoderClientType = "5";
352
+ const qoderDataPolicy = "disagree";
353
+ const qoderLoginVersion = "v2";
354
+ const qoderMachineOs = process.platform === "win32" ? process.arch === "arm64" ? "aarch64_windows" : "x86_64_windows" : process.arch === "arm64" ? "aarch64_linux" : "x86_64_linux";
355
+ const qoderMachineTypeMagic = "5";
356
+ function computeSigPath(urlStr) {
357
+ let sigPath = new URL(urlStr).pathname;
358
+ if (sigPath.startsWith("/algo")) sigPath = sigPath.substring(5);
359
+ return sigPath;
360
+ }
361
+ function rsaEncryptBase64(data) {
362
+ const key = {
363
+ key: qoderRSAPublicKey,
364
+ padding: crypto.constants.RSA_PKCS1_PADDING
365
+ };
366
+ return crypto.publicEncrypt(key, typeof data === "string" ? Buffer.from(data) : data).toString("base64");
367
+ }
368
+ function aesEncryptCBCBase64(plaintext, keyStr) {
369
+ const cipher = crypto.createCipheriv("aes-128-cbc", Buffer.from(keyStr), Buffer.from(keyStr));
370
+ let encrypted = cipher.update(plaintext, "utf8", "base64");
371
+ encrypted += cipher.final("base64");
372
+ return encrypted;
373
+ }
374
+ function buildAuthHeaders(body, requestURL, creds) {
375
+ if (!creds.userID) throw new Error("cosy: user id is empty");
376
+ if (!creds.authToken) throw new Error("cosy: auth token is empty");
377
+ const aesKey = crypto.randomUUID().replace(/-/g, "").slice(0, 16);
378
+ const userInfo = {
379
+ uid: creds.userID,
380
+ security_oauth_token: creds.authToken,
381
+ name: creds.name || "",
382
+ aid: "",
383
+ email: creds.email || ""
384
+ };
385
+ const infoB64 = aesEncryptCBCBase64(JSON.stringify(userInfo), aesKey);
386
+ const cosyKey = rsaEncryptBase64(aesKey);
387
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
388
+ const cosyPayload = {
389
+ version: "v1",
390
+ requestId: crypto.randomUUID(),
391
+ info: infoB64,
392
+ cosyVersion: qoderIdeVersion,
393
+ ideVersion: ""
394
+ };
395
+ const payloadB64 = Buffer.from(JSON.stringify(cosyPayload)).toString("base64");
396
+ const sigPath = computeSigPath(requestURL);
397
+ const sigInput = `${payloadB64}\n${cosyKey}\n${timestamp}\n${body ? Buffer.isBuffer(body) ? body.toString("utf8") : body : ""}\n${sigPath}`;
398
+ const sig = crypto.createHash("md5").update(sigInput).digest("hex");
399
+ const bodyHash = crypto.createHash("md5").update(body || "").digest("hex");
400
+ const bodyLen = body ? (Buffer.isBuffer(body) ? body.length : Buffer.from(body).length).toString() : "0";
401
+ const machineID = creds.machineID || getMachineId();
402
+ return {
403
+ Authorization: `Bearer COSY.${payloadB64}.${sig}`,
404
+ "Cosy-Key": cosyKey,
405
+ "Cosy-User": creds.userID,
406
+ "Cosy-Date": timestamp,
407
+ "Cosy-Version": qoderIdeVersion,
408
+ "Cosy-Machineid": machineID,
409
+ "Cosy-Machinetoken": machineID,
410
+ "Cosy-Machinetype": qoderMachineTypeMagic,
411
+ "Cosy-Machineos": qoderMachineOs,
412
+ "Cosy-Clienttype": qoderClientType,
413
+ "Cosy-Clientip": "127.0.0.1",
414
+ "Cosy-Bodyhash": bodyHash,
415
+ "Cosy-Bodylength": bodyLen,
416
+ "Cosy-Sigpath": sigPath,
417
+ "Cosy-Data-Policy": qoderDataPolicy,
418
+ "Cosy-Organization-Id": "",
419
+ "Cosy-Organization-Tags": "",
420
+ "Login-Version": qoderLoginVersion,
421
+ "X-Request-Id": crypto.randomUUID()
422
+ };
423
+ }
424
+ //#endregion
425
+ //#region src/encoding.ts
426
+ /**
427
+ * Qoder WAF Body encoding algorithm.
428
+ *
429
+ * Re-arranges standard Base64 chunks and translates characters against
430
+ * a custom alphabet to satisfy Qoder upstream WAF requirements.
431
+ *
432
+ * @module dsh-provider-qoder/encoding
433
+ */
434
+ const qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
435
+ const qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
436
+ function qoderEncodeBody(plaintext) {
437
+ const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
438
+ const n = std.length;
439
+ const a = Math.floor(n / 3);
440
+ const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
441
+ let out = "";
442
+ for (let i = 0; i < n; i++) {
443
+ const c = rearranged[i];
444
+ if (c === "=") out += "$";
445
+ else {
446
+ const idx = qoderStdAlphabet.indexOf(c);
447
+ if (idx >= 0) out += qoderCustomAlphabet[idx];
448
+ else out += c;
449
+ }
450
+ }
451
+ return out;
452
+ }
453
+ //#endregion
454
+ //#region src/translate.ts
455
+ function unsupported(message) {
456
+ return new QoderLlmError(message, "UNSUPPORTED_CONTENT");
457
+ }
458
+ function toolResultText(block) {
459
+ let text = "";
460
+ for (const nested of block.content) {
461
+ if (nested.type !== "text") throw unsupported(`Qoder tool results support text only; received nested ${String(nested.type)} content.`);
462
+ text += nested.text;
463
+ }
464
+ return text;
465
+ }
466
+ function translateTools(tools) {
467
+ return (tools ?? []).map((tool) => ({
468
+ type: "function",
469
+ function: {
470
+ name: tool.name,
471
+ description: tool.description,
472
+ parameters: tool.parameters
473
+ }
474
+ }));
475
+ }
476
+ function validateAndTranslateMessages(messages, systemPrompt) {
477
+ const output = [];
478
+ if (typeof systemPrompt === "string" && systemPrompt.trim().length > 0) output.push({
479
+ role: "system",
480
+ content: systemPrompt
481
+ });
482
+ for (const message of messages) {
483
+ const toolResults = message.content.filter((block) => block.type === "tool-result");
484
+ if (toolResults.length > 0) {
485
+ if (message.role !== "user" || toolResults.length !== message.content.length) throw unsupported("Qoder tool-result messages cannot contain sibling content or use a non-user role.");
486
+ for (const result of toolResults) output.push({
487
+ role: "tool",
488
+ tool_call_id: String(result.toolCallId),
489
+ content: toolResultText(result)
490
+ });
491
+ continue;
492
+ }
493
+ let text = "";
494
+ const toolCalls = [];
495
+ for (const block of message.content) {
496
+ if (block.type === "text") {
497
+ text += block.text;
498
+ continue;
499
+ }
500
+ if (block.type === "image") throw unsupported("Qoder transport does not support image content.");
501
+ if (block.type === "tool-call") {
502
+ if (message.role !== "assistant") throw unsupported("Qoder tool calls are valid only in assistant messages.");
503
+ toolCalls.push({
504
+ id: String(block.id),
505
+ type: "function",
506
+ function: {
507
+ name: block.name,
508
+ arguments: block.arguments
509
+ }
510
+ });
511
+ continue;
512
+ }
513
+ if (block.type === "tool-result") throw unsupported("Qoder encountered an invalid mixed tool-result message.");
514
+ if (block.type === "reasoning") {
515
+ if (message.role !== "assistant") throw unsupported("Qoder historical reasoning is valid only in assistant messages.");
516
+ continue;
517
+ }
518
+ throw unsupported(`Qoder transport encountered unsupported block type: ${String(block.type)}`);
519
+ }
520
+ if (message.role === "assistant") {
521
+ if (!text && toolCalls.length === 0) continue;
522
+ output.push({
523
+ role: "assistant",
524
+ content: text || " ",
525
+ ...toolCalls.length === 0 ? {} : { tool_calls: toolCalls }
526
+ });
527
+ continue;
528
+ }
529
+ output.push({
530
+ role: message.role,
531
+ content: text
532
+ });
533
+ }
534
+ return output;
535
+ }
536
+ //#endregion
537
+ //#region src/serialize.ts
538
+ /** Build the minimum qodercli request envelope for a validated DSH request. */
539
+ function stableHash(prefix, ...inputs) {
540
+ const hash = crypto.createHash("sha256");
541
+ hash.update(prefix);
542
+ for (const input of inputs) {
543
+ hash.update("\0");
544
+ hash.update(input);
545
+ }
546
+ return hash.digest("hex").slice(0, 16);
547
+ }
548
+ function stableChatRecordId(model, messages, tools, maxTokens) {
549
+ const hash = crypto.createHash("sha256");
550
+ hash.update("qoder-record");
551
+ hash.update("\0");
552
+ hash.update(model);
553
+ hash.update("\0");
554
+ hash.update(JSON.stringify(messages));
555
+ hash.update("\0");
556
+ hash.update(JSON.stringify(tools));
557
+ hash.update("\0");
558
+ hash.update(`mt=${maxTokens}`);
559
+ return hash.digest("hex").slice(0, 16);
560
+ }
561
+ function validateQoderRequest(options, model) {
562
+ if (options.reasoningEffort !== void 0) {
563
+ const effort = String(options.reasoningEffort);
564
+ if (!model?.reasoningEfforts?.some((candidate) => candidate.id === effort)) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise reasoning effort "${effort}".`, "UNSUPPORTED_REASONING_EFFORT");
565
+ }
566
+ return validateAndTranslateMessages(options.messages, options.system);
567
+ }
568
+ function buildQoderRequestBody(options, userId, translatedMessages, model) {
569
+ if (!userId) throw new QoderLlmError("Qoder request identity is missing.", "AUTH");
570
+ const modelKey = options.model || "cmodel";
571
+ const messages = translatedMessages ?? validateQoderRequest(options, model);
572
+ const modelMaxTokens = model?.maxTokens ?? 32768;
573
+ const maxTokens = Math.min(options.maxTokens ?? modelMaxTokens, modelMaxTokens);
574
+ const isReasoning = model?.isReasoning ?? false;
575
+ const tools = translateTools(options.tools);
576
+ const contextConfig = model?.contextOptions === void 0 ? void 0 : Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, {
577
+ ...value.tokenCount === void 0 ? {} : { token_count: value.tokenCount },
578
+ ...value.isDefault === void 0 ? {} : { is_default: value.isDefault }
579
+ }]));
580
+ let lastUserText = "";
581
+ for (let index = messages.length - 1; index >= 0; index--) if (messages[index].role === "user") {
582
+ lastUserText = messages[index].content ?? "";
583
+ break;
584
+ }
585
+ const stablePart = stableHash("qoder-session", userId, modelKey);
586
+ const sessionId = options.sessionId === void 0 ? `${stablePart}-${crypto.randomUUID()}` : `${stablePart}-${String(options.sessionId)}`;
587
+ const recordId = stableChatRecordId(modelKey, messages, tools, maxTokens);
588
+ return {
589
+ request_id: crypto.randomUUID(),
590
+ request_set_id: recordId,
591
+ chat_record_id: recordId,
592
+ session_id: sessionId,
593
+ stream: true,
594
+ chat_task: "FREE_INPUT",
595
+ is_reply: true,
596
+ is_retry: false,
597
+ source: 1,
598
+ version: "3",
599
+ session_type: "qodercli",
600
+ agent_id: "agent_common",
601
+ task_id: "common",
602
+ code_language: "",
603
+ chat_prompt: "",
604
+ image_urls: null,
605
+ aliyun_user_type: "",
606
+ system: "",
607
+ messages,
608
+ tools,
609
+ parameters: {
610
+ max_tokens: maxTokens,
611
+ ...options.reasoningEffort === void 0 ? {} : { reasoning_effort: String(options.reasoningEffort) }
612
+ },
613
+ chat_context: {
614
+ chatPrompt: "",
615
+ imageUrls: null,
616
+ extra: {
617
+ context: [],
618
+ modelConfig: {
619
+ key: modelKey,
620
+ is_reasoning: isReasoning
621
+ },
622
+ originalContent: lastUserText
623
+ },
624
+ features: [],
625
+ text: lastUserText
626
+ },
627
+ model_config: {
628
+ key: modelKey,
629
+ is_reasoning: isReasoning,
630
+ max_output_tokens: maxTokens,
631
+ source: model?.source || "system",
632
+ ...contextConfig === void 0 ? {} : { context_config: contextConfig }
633
+ },
634
+ business: {
635
+ product: "cli",
636
+ version: "1.0.0",
637
+ type: "agent",
638
+ stage: "start",
639
+ id: crypto.randomUUID(),
640
+ name: lastUserText.substring(0, 30),
641
+ begin_at: Date.now()
642
+ }
643
+ };
644
+ }
645
+ //#endregion
646
+ //#region src/thinking.ts
647
+ const thinkingTags = [{
648
+ open: "<thinking>",
649
+ close: "</thinking>"
650
+ }, {
651
+ open: "<think>",
652
+ close: "</think>"
653
+ }];
654
+ const allTags = thinkingTags.flatMap((tag) => [tag.open, tag.close]);
655
+ function trailingPrefixLength(text, candidates) {
656
+ let best = 0;
657
+ for (const candidate of candidates) {
658
+ const limit = Math.min(text.length, candidate.length - 1);
659
+ for (let length = limit; length > best; length--) if (text.endsWith(candidate.slice(0, length))) {
660
+ best = length;
661
+ break;
662
+ }
663
+ }
664
+ return best;
665
+ }
666
+ function earliestTag(text, kind) {
667
+ let found;
668
+ for (const tag of thinkingTags) {
669
+ const position = text.indexOf(tag[kind]);
670
+ if (position !== -1 && (found === void 0 || position < found.position)) found = {
671
+ position,
672
+ tag
673
+ };
674
+ }
675
+ return found;
676
+ }
677
+ function trimSeparator(text) {
678
+ if (text.startsWith("\n\n")) return text.slice(2);
679
+ if (text.startsWith("\n")) return text.slice(1);
680
+ return text;
681
+ }
682
+ var NativeReasoningStripper = class {
683
+ buffer = "";
684
+ push(chunk) {
685
+ this.buffer += chunk;
686
+ let output = "";
687
+ while (this.buffer) {
688
+ let firstPosition = -1;
689
+ let firstTag = "";
690
+ for (const tag of allTags) {
691
+ const position = this.buffer.indexOf(tag);
692
+ if (position !== -1 && (firstPosition === -1 || position < firstPosition)) {
693
+ firstPosition = position;
694
+ firstTag = tag;
695
+ }
696
+ }
697
+ if (firstPosition !== -1) {
698
+ output += this.buffer.slice(0, firstPosition);
699
+ this.buffer = this.buffer.slice(firstPosition + firstTag.length);
700
+ continue;
701
+ }
702
+ const held = trailingPrefixLength(this.buffer, allTags);
703
+ output += this.buffer.slice(0, this.buffer.length - held);
704
+ this.buffer = this.buffer.slice(this.buffer.length - held);
705
+ break;
706
+ }
707
+ return output;
708
+ }
709
+ finish() {
710
+ const output = allTags.some((tag) => tag.startsWith(this.buffer)) ? "" : this.buffer;
711
+ this.buffer = "";
712
+ return output;
713
+ }
714
+ };
715
+ var QoderThinkingParser = class {
716
+ buffer = "";
717
+ mode = "text";
718
+ closingTag = thinkingTags[0].close;
719
+ native = new NativeReasoningStripper();
720
+ pushReasoning(chunk) {
721
+ const text = this.native.push(chunk);
722
+ return text ? [{
723
+ type: "reasoning",
724
+ text
725
+ }] : [];
726
+ }
727
+ pushContent(chunk) {
728
+ const output = [];
729
+ const pendingReasoning = this.native.finish();
730
+ if (pendingReasoning) output.push({
731
+ type: "reasoning",
732
+ text: pendingReasoning
733
+ });
734
+ this.buffer += chunk;
735
+ while (this.buffer) {
736
+ if (this.mode === "reasoning") {
737
+ const closeAt = this.buffer.indexOf(this.closingTag);
738
+ if (closeAt !== -1) {
739
+ if (closeAt > 0) output.push({
740
+ type: "reasoning",
741
+ text: this.buffer.slice(0, closeAt)
742
+ });
743
+ this.buffer = trimSeparator(this.buffer.slice(closeAt + this.closingTag.length));
744
+ this.mode = "text";
745
+ continue;
746
+ }
747
+ const held = trailingPrefixLength(this.buffer, [this.closingTag]);
748
+ const safeLength = this.buffer.length - held;
749
+ if (safeLength > 0) output.push({
750
+ type: "reasoning",
751
+ text: this.buffer.slice(0, safeLength)
752
+ });
753
+ this.buffer = this.buffer.slice(safeLength);
754
+ break;
755
+ }
756
+ const opener = earliestTag(this.buffer, "open");
757
+ const closer = earliestTag(this.buffer, "close");
758
+ if (opener !== void 0 && (closer === void 0 || opener.position < closer.position)) {
759
+ if (opener.position > 0) output.push({
760
+ type: "text",
761
+ text: this.buffer.slice(0, opener.position)
762
+ });
763
+ this.buffer = this.buffer.slice(opener.position + opener.tag.open.length);
764
+ this.closingTag = opener.tag.close;
765
+ this.mode = "reasoning";
766
+ continue;
767
+ }
768
+ if (closer !== void 0) {
769
+ if (closer.position > 0) output.push({
770
+ type: "text",
771
+ text: this.buffer.slice(0, closer.position)
772
+ });
773
+ this.buffer = trimSeparator(this.buffer.slice(closer.position + closer.tag.close.length));
774
+ continue;
775
+ }
776
+ const held = trailingPrefixLength(this.buffer, allTags);
777
+ const safeLength = this.buffer.length - held;
778
+ if (safeLength > 0) output.push({
779
+ type: "text",
780
+ text: this.buffer.slice(0, safeLength)
781
+ });
782
+ this.buffer = this.buffer.slice(safeLength);
783
+ break;
784
+ }
785
+ return output;
786
+ }
787
+ finish() {
788
+ const output = [];
789
+ const pendingReasoning = this.native.finish();
790
+ if (pendingReasoning) output.push({
791
+ type: "reasoning",
792
+ text: pendingReasoning
793
+ });
794
+ if (this.buffer) output.push({
795
+ type: this.mode,
796
+ text: this.buffer
797
+ });
798
+ this.buffer = "";
799
+ return output;
800
+ }
801
+ };
802
+ //#endregion
803
+ //#region src/sse.ts
804
+ /** Decode Qoder's outer SSE envelope and inner model chunks. */
805
+ const doneMarker = "[DONE]";
806
+ function malformed(message) {
807
+ return new QoderLlmError(message, "MALFORMED_RESPONSE");
808
+ }
809
+ function parseEnvelope(rawData) {
810
+ let value;
811
+ try {
812
+ value = JSON.parse(rawData);
813
+ } catch {
814
+ throw malformed("Malformed outer SSE JSON payload received from Qoder.");
815
+ }
816
+ if (value === null || typeof value !== "object") throw malformed("Malformed outer SSE envelope received from Qoder.");
817
+ const envelope = value;
818
+ if (envelope.statusCodeValue !== void 0 && typeof envelope.statusCodeValue !== "number") throw malformed("Qoder SSE envelope has an invalid status code.");
819
+ if (envelope.statusCodeValue !== void 0 && (!Number.isInteger(envelope.statusCodeValue) || envelope.statusCodeValue < 100 || envelope.statusCodeValue > 599)) throw malformed("Qoder SSE envelope has an invalid status code.");
820
+ if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) throw qoderHttpError(`Qoder service returned upstream error status ${envelope.statusCodeValue}.`, { status: envelope.statusCodeValue });
821
+ if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
822
+ return envelope;
823
+ }
824
+ function parseInner(body) {
825
+ try {
826
+ const value = JSON.parse(body);
827
+ if (value === null || typeof value !== "object") throw new Error("not an object");
828
+ return value;
829
+ } catch {
830
+ throw malformed("Malformed inner model payload received from Qoder.");
831
+ }
832
+ }
833
+ function tokenUsage(innerChunk) {
834
+ if (!innerChunk.usage) return void 0;
835
+ const usage = innerChunk.usage;
836
+ const promptTokens = usage.prompt_tokens ?? 0;
837
+ const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens ?? 0;
838
+ const cacheWriteTokens = usage.prompt_tokens_details?.cache_write_tokens ?? 0;
839
+ const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens;
840
+ return {
841
+ inputTokens: Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens),
842
+ outputTokens: usage.completion_tokens ?? 0,
843
+ ...cacheReadTokens > 0 ? { cacheReadTokens } : {},
844
+ ...cacheWriteTokens > 0 ? { cacheWriteTokens } : {},
845
+ ...reasoningTokens === void 0 ? {} : { reasoningTokens }
846
+ };
847
+ }
848
+ function finishKind(value) {
849
+ if (value === void 0 || value === null || value === "") return void 0;
850
+ if (value === "stop") return "stop";
851
+ if (value === "length") return "max-tokens";
852
+ if (value === "tool_calls" || value === "toolUse") return "tool-calls";
853
+ if (value === "content_filter") throw new QoderLlmError("Qoder blocked the response through its content filter.", "PROVIDER_ERROR");
854
+ throw malformed(`Qoder returned unknown finish reason "${value}".`);
855
+ }
856
+ async function* parseQoderSse(stream, options = {}) {
857
+ const reader = stream.getReader();
858
+ const decoder = new TextDecoder();
859
+ const thinkingParser = new QoderThinkingParser();
860
+ const toolCalls = /* @__PURE__ */ new Map();
861
+ let buffer = "";
862
+ let sourceEnded = false;
863
+ let sawDone = false;
864
+ let nextBlockIndex = 0;
865
+ let activeTextual;
866
+ let pendingUsage;
867
+ let terminalKind = "stop";
868
+ let sawContent = false;
869
+ const closeTextual = () => {
870
+ if (activeTextual === void 0) return [];
871
+ const state = activeTextual;
872
+ activeTextual = void 0;
873
+ return [{
874
+ type: "block-end",
875
+ index: state.index,
876
+ block: state.type === "text" ? {
877
+ type: "text",
878
+ text: state.text
879
+ } : {
880
+ type: "reasoning",
881
+ text: state.text
882
+ }
883
+ }];
884
+ };
885
+ const appendSegment = (segment) => {
886
+ if (!segment.text) return [];
887
+ const chunks = [];
888
+ sawContent = true;
889
+ if (activeTextual?.type !== segment.type) {
890
+ chunks.push(...closeTextual());
891
+ activeTextual = {
892
+ type: segment.type,
893
+ index: nextBlockIndex++,
894
+ text: ""
895
+ };
896
+ chunks.push({
897
+ type: "block-start",
898
+ index: activeTextual.index,
899
+ blockType: segment.type
900
+ });
901
+ }
902
+ activeTextual.text += segment.text;
903
+ chunks.push(segment.type === "text" ? {
904
+ type: "text-delta",
905
+ index: activeTextual.index,
906
+ text: segment.text
907
+ } : {
908
+ type: "reasoning-delta",
909
+ index: activeTextual.index,
910
+ text: segment.text
911
+ });
912
+ return chunks;
913
+ };
914
+ const openToolCall = (state) => {
915
+ if (!state.id || state.blockIndex !== void 0) return [];
916
+ const chunks = closeTextual();
917
+ state.blockIndex = nextBlockIndex++;
918
+ sawContent = true;
919
+ chunks.push({
920
+ type: "block-start",
921
+ index: state.blockIndex,
922
+ blockType: "tool-call"
923
+ });
924
+ const argumentsDelta = state.arguments.slice(state.emittedArguments);
925
+ state.emittedArguments = state.arguments.length;
926
+ chunks.push({
927
+ type: "tool-call-delta",
928
+ index: state.blockIndex,
929
+ id: ToolCallId(state.id),
930
+ ...state.name ? { name: state.name } : {},
931
+ argumentsDelta
932
+ });
933
+ return chunks;
934
+ };
935
+ try {
936
+ while (!sourceEnded && !sawDone) {
937
+ const { done, value } = await reader.read();
938
+ if (done) {
939
+ sourceEnded = true;
940
+ buffer += decoder.decode();
941
+ if (buffer.length > 0 && !buffer.endsWith("\n")) buffer += "\n";
942
+ } else {
943
+ options.onActivity?.();
944
+ buffer += decoder.decode(value, { stream: true });
945
+ }
946
+ while (!sawDone) {
947
+ const lineEnd = buffer.indexOf("\n");
948
+ if (lineEnd === -1) break;
949
+ let line = buffer.substring(0, lineEnd);
950
+ buffer = buffer.substring(lineEnd + 1);
951
+ if (line.endsWith("\r")) line = line.slice(0, -1);
952
+ line = line.trim();
953
+ if (!line.startsWith("data:")) continue;
954
+ const rawData = line.substring(5).trim();
955
+ if (!rawData) continue;
956
+ if (rawData === doneMarker) {
957
+ sawDone = true;
958
+ break;
959
+ }
960
+ const body = parseEnvelope(rawData).body?.trim();
961
+ if (!body) continue;
962
+ if (body === doneMarker) {
963
+ sawDone = true;
964
+ break;
965
+ }
966
+ const innerChunk = parseInner(body);
967
+ pendingUsage = tokenUsage(innerChunk) ?? pendingUsage;
968
+ for (const choice of innerChunk.choices ?? []) {
969
+ terminalKind = finishKind(choice.finish_reason) ?? terminalKind;
970
+ const delta = choice.delta;
971
+ if (!delta) continue;
972
+ if (typeof delta.reasoning_content === "string" && delta.reasoning_content) for (const segment of thinkingParser.pushReasoning(delta.reasoning_content)) for (const chunk of appendSegment(segment)) yield chunk;
973
+ if (typeof delta.content === "string" && delta.content) for (const segment of thinkingParser.pushContent(delta.content)) for (const chunk of appendSegment(segment)) yield chunk;
974
+ if (delta.tool_calls !== void 0) {
975
+ if (!Array.isArray(delta.tool_calls)) throw malformed("Qoder tool-call delta is not an array.");
976
+ for (const rawCall of delta.tool_calls) {
977
+ if (typeof rawCall !== "object" || rawCall === null || Array.isArray(rawCall)) throw malformed("Qoder tool-call delta is not an object.");
978
+ const upstreamIndex = rawCall.index ?? 0;
979
+ if (!Number.isInteger(upstreamIndex) || upstreamIndex < 0) throw malformed("Qoder tool-call delta has an invalid index.");
980
+ let state = toolCalls.get(upstreamIndex);
981
+ if (state === void 0) {
982
+ state = {
983
+ id: "",
984
+ name: "",
985
+ arguments: "",
986
+ emittedArguments: 0
987
+ };
988
+ toolCalls.set(upstreamIndex, state);
989
+ }
990
+ if (rawCall.id !== void 0) {
991
+ if (typeof rawCall.id !== "string" || !rawCall.id) throw malformed("Qoder tool call has an invalid id.");
992
+ if (state.id && state.id !== rawCall.id) throw malformed("Qoder changed a streamed tool-call id.");
993
+ state.id = rawCall.id;
994
+ }
995
+ if (rawCall.function?.name !== void 0) {
996
+ const name = rawCall.function.name;
997
+ if (typeof name !== "string" || !name) throw malformed("Qoder tool call has an invalid name.");
998
+ if (state.name && state.name !== name) throw malformed("Qoder changed a streamed tool-call name.");
999
+ state.name = name;
1000
+ }
1001
+ if (rawCall.function?.arguments !== void 0) {
1002
+ if (typeof rawCall.function.arguments !== "string") throw malformed("Qoder tool-call arguments delta is not a string.");
1003
+ state.arguments += rawCall.function.arguments;
1004
+ }
1005
+ const wasOpen = state.blockIndex !== void 0;
1006
+ for (const chunk of openToolCall(state)) yield chunk;
1007
+ if (wasOpen && state.blockIndex !== void 0 && state.emittedArguments < state.arguments.length) {
1008
+ const argumentsDelta = state.arguments.slice(state.emittedArguments);
1009
+ state.emittedArguments = state.arguments.length;
1010
+ yield {
1011
+ type: "tool-call-delta",
1012
+ index: state.blockIndex,
1013
+ id: ToolCallId(state.id),
1014
+ ...state.name ? { name: state.name } : {},
1015
+ argumentsDelta
1016
+ };
1017
+ } else if (wasOpen && state.blockIndex !== void 0 && rawCall.function?.name !== void 0) yield {
1018
+ type: "tool-call-delta",
1019
+ index: state.blockIndex,
1020
+ id: ToolCallId(state.id),
1021
+ name: state.name,
1022
+ argumentsDelta: ""
1023
+ };
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+ }
1029
+ } finally {
1030
+ await reader.cancel().catch(() => void 0);
1031
+ reader.releaseLock();
1032
+ }
1033
+ if (!sawDone) throw new QoderLlmError("SSE stream ended prematurely without [DONE].", "TRANSPORT");
1034
+ for (const segment of thinkingParser.finish()) for (const chunk of appendSegment(segment)) yield chunk;
1035
+ for (const chunk of closeTextual()) yield chunk;
1036
+ for (const [, state] of [...toolCalls.entries()].sort(([left], [right]) => left - right)) {
1037
+ if (!state.id || !state.name) throw malformed("Qoder completed an unidentifiable tool call.");
1038
+ const normalizedArguments = state.arguments.trim() ? state.arguments : "{}";
1039
+ try {
1040
+ JSON.parse(normalizedArguments);
1041
+ } catch {
1042
+ throw malformed(`Qoder completed tool call "${state.name}" with malformed JSON arguments.`);
1043
+ }
1044
+ for (const chunk of openToolCall(state)) yield chunk;
1045
+ if (state.blockIndex === void 0) throw malformed("Qoder failed to open a completed tool call.");
1046
+ if (state.emittedArguments === 0 && normalizedArguments === "{}") yield {
1047
+ type: "tool-call-delta",
1048
+ index: state.blockIndex,
1049
+ id: ToolCallId(state.id),
1050
+ name: state.name,
1051
+ argumentsDelta: "{}"
1052
+ };
1053
+ yield {
1054
+ type: "block-end",
1055
+ index: state.blockIndex,
1056
+ block: {
1057
+ type: "tool-call",
1058
+ id: ToolCallId(state.id),
1059
+ name: state.name,
1060
+ arguments: normalizedArguments
1061
+ }
1062
+ };
1063
+ }
1064
+ if (pendingUsage) yield {
1065
+ type: "usage",
1066
+ usage: pendingUsage
1067
+ };
1068
+ if (!sawContent) {
1069
+ yield {
1070
+ type: "finish",
1071
+ reason: {
1072
+ kind: "error",
1073
+ failure: {
1074
+ message: "Model returned a completed response with no content.",
1075
+ code: EMPTY_RESPONSE_CODE
1076
+ }
1077
+ }
1078
+ };
1079
+ return;
1080
+ }
1081
+ yield {
1082
+ type: "finish",
1083
+ reason: { kind: toolCalls.size > 0 ? "tool-calls" : terminalKind }
1084
+ };
1085
+ }
1086
+ //#endregion
1087
+ //#region src/adapter.ts
1088
+ /** DSH LlmAdapter for the Global Qoder subscription transport. */
1089
+ const defaultMaxTokens = 32768;
1090
+ const defaultStreamIdleTimeoutMs = 300 * 1e3;
1091
+ const defaultModels = [
1092
+ {
1093
+ id: "cmodel",
1094
+ name: "Cantus (Qoder)",
1095
+ description: "Default Global Qoder subscription model for quick validation",
1096
+ contextWindow: 1e6,
1097
+ maxTokens: defaultMaxTokens
1098
+ },
1099
+ {
1100
+ id: "auto",
1101
+ name: "Qoder Auto",
1102
+ description: "Server-routed Global Qoder model pool",
1103
+ contextWindow: 18e4,
1104
+ maxTokens: defaultMaxTokens
1105
+ },
1106
+ {
1107
+ id: "ultimate",
1108
+ name: "Qoder Ultimate",
1109
+ description: "Highest-capability Global Qoder model pool",
1110
+ contextWindow: 1e6,
1111
+ maxTokens: defaultMaxTokens
1112
+ },
1113
+ {
1114
+ id: "performance",
1115
+ name: "Qoder Performance",
1116
+ description: "Performance-oriented Global Qoder model pool",
1117
+ contextWindow: 1e6,
1118
+ maxTokens: defaultMaxTokens
1119
+ },
1120
+ {
1121
+ id: "efficient",
1122
+ name: "Qoder Efficient",
1123
+ description: "Efficiency-oriented Global Qoder model pool",
1124
+ contextWindow: 18e4,
1125
+ maxTokens: defaultMaxTokens
1126
+ },
1127
+ {
1128
+ id: "lite",
1129
+ name: "Qoder Lite",
1130
+ description: "Basic Global Qoder model pool",
1131
+ contextWindow: 18e4,
1132
+ maxTokens: defaultMaxTokens
1133
+ }
1134
+ ];
1135
+ function modelInfo(provider, model) {
1136
+ return {
1137
+ provider,
1138
+ id: model.id,
1139
+ name: model.priceFactor === void 0 ? model.name : `${model.name} (${model.priceFactor}x)`,
1140
+ description: model.description,
1141
+ inputModalities: ["text"]
1142
+ };
1143
+ }
1144
+ var QoderAdapter = class extends LlmAdapter {
1145
+ resolvePat;
1146
+ catalogModels;
1147
+ authService;
1148
+ fetchImpl;
1149
+ logger;
1150
+ streamIdleTimeoutMs;
1151
+ providerId;
1152
+ providerName;
1153
+ region;
1154
+ constructor(options) {
1155
+ super();
1156
+ this.resolvePat = options.resolvePat;
1157
+ this.catalogModels = options.models && options.models.length > 0 ? options.models : defaultModels;
1158
+ this.authService = options.authService ?? new QoderAuthService({ fetch: options.fetch });
1159
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
1160
+ this.logger = options.logger;
1161
+ this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 3e5;
1162
+ this.providerId = options.providerId ?? "qoder-official";
1163
+ this.providerName = options.providerName ?? "Qoder";
1164
+ this.region = options.region ?? "global";
1165
+ }
1166
+ providerInfo(provider) {
1167
+ return {
1168
+ id: provider,
1169
+ name: this.providerName
1170
+ };
1171
+ }
1172
+ listModels(provider) {
1173
+ return Promise.resolve(this.catalogModels.map((model) => modelInfo(provider, model)));
1174
+ }
1175
+ replaceConfig(models, streamIdleTimeoutMs, region = "global") {
1176
+ this.catalogModels = models;
1177
+ this.streamIdleTimeoutMs = streamIdleTimeoutMs;
1178
+ this.region = region;
1179
+ }
1180
+ resolveModel(provider, modelId, signal) {
1181
+ if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
1182
+ const configured = this.catalogModels.find((model) => model.id === modelId);
1183
+ if (configured === void 0) return Promise.resolve({
1184
+ provider,
1185
+ id: modelId,
1186
+ name: modelId,
1187
+ inputModalities: ["text"]
1188
+ });
1189
+ return Promise.resolve({
1190
+ ...modelInfo(provider, configured),
1191
+ ...configured.contextWindow === void 0 ? {} : { context: { contextWindow: configured.contextWindow } },
1192
+ ...configured.maxTokens === void 0 ? {} : { defaultMaxTokens: configured.maxTokens },
1193
+ ...configured.reasoningEfforts === void 0 || configured.reasoningEfforts.length === 0 ? {} : { reasoning: {
1194
+ efforts: configured.reasoningEfforts.map((effort) => ({
1195
+ id: ReasoningEffortId(effort.id),
1196
+ name: effort.name,
1197
+ ...effort.description === void 0 ? {} : { description: effort.description }
1198
+ })),
1199
+ ...configured.defaultReasoningEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(configured.defaultReasoningEffort) }
1200
+ } }
1201
+ });
1202
+ }
1203
+ stream(options) {
1204
+ return this.generate(options);
1205
+ }
1206
+ async *generate(options) {
1207
+ if (options.provider !== this.providerId) throw new QoderLlmError(`Qoder adapter does not own provider "${options.provider}".`, "INVALID_PROVIDER");
1208
+ if (options.signal?.aborted) throw new QoderLlmError("Request was aborted prior to generation.", "ABORTED");
1209
+ const model = this.catalogModels.find((candidate) => candidate.id === (options.model || "cmodel"));
1210
+ const messages = validateQoderRequest(options, model);
1211
+ const pat = await this.resolvePat();
1212
+ if (!pat) throw new QoderLlmError("Qoder Personal Access Token is missing. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
1213
+ if (options.signal?.aborted) throw new QoderLlmError("Request was aborted.", "ABORTED");
1214
+ const credentials = await this.authService.getCredentials(pat, options.signal, this.region);
1215
+ const request = buildQoderRequestBody(options, credentials.userID, messages, model);
1216
+ const encodedBody = qoderEncodeBody(JSON.stringify(request));
1217
+ const encodedBytes = Buffer.from(encodedBody, "utf8");
1218
+ const chatUrl = getQoderChatUrl(this.region);
1219
+ const requestController = new AbortController();
1220
+ let idleTimedOut = false;
1221
+ let idleTimer;
1222
+ const onCallerAbort = () => requestController.abort(options.signal?.reason);
1223
+ options.signal?.addEventListener("abort", onCallerAbort, { once: true });
1224
+ const resetIdleTimer = () => {
1225
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
1226
+ idleTimer = setTimeout(() => {
1227
+ idleTimedOut = true;
1228
+ requestController.abort("stream idle timeout");
1229
+ }, this.streamIdleTimeoutMs);
1230
+ };
1231
+ resetIdleTimer();
1232
+ try {
1233
+ const response = await this.fetchImpl(chatUrl, {
1234
+ method: "POST",
1235
+ headers: {
1236
+ "content-type": "application/json",
1237
+ "accept": "text/event-stream",
1238
+ "cache-control": "no-cache",
1239
+ "accept-encoding": "identity",
1240
+ "x-model-key": options.model || "cmodel",
1241
+ "x-model-source": model?.source || "system",
1242
+ ...attributionHeaders(),
1243
+ ...buildAuthHeaders(encodedBytes, chatUrl, credentials)
1244
+ },
1245
+ body: encodedBytes,
1246
+ signal: requestController.signal
1247
+ });
1248
+ resetIdleTimer();
1249
+ if (!response.ok) throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}.`, response);
1250
+ if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
1251
+ yield* parseQoderSse(response.body, { onActivity: resetIdleTimer });
1252
+ } catch (error) {
1253
+ if (options.signal?.aborted) throw new QoderLlmError("Request was aborted.", "ABORTED");
1254
+ if (idleTimedOut) {
1255
+ const failure = new QoderLlmError("Qoder model stream exceeded its idle timeout.", "TIMEOUT");
1256
+ this.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure));
1257
+ throw failure;
1258
+ }
1259
+ if (error instanceof QoderLlmError) {
1260
+ this.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(error));
1261
+ throw error;
1262
+ }
1263
+ const failure = new QoderLlmError("Qoder transport request failed.", "TRANSPORT", { cause: error });
1264
+ this.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(failure), { cause: redactLogValue(error) });
1265
+ throw failure;
1266
+ } finally {
1267
+ if (idleTimer !== void 0) clearTimeout(idleTimer);
1268
+ options.signal?.removeEventListener("abort", onCallerAbort);
1269
+ requestController.abort("request complete");
1270
+ }
1271
+ }
1272
+ };
1273
+ //#endregion
1274
+ //#region src/credential-contract.ts
1275
+ /** Shared Host/browser contract for the one managed Qoder credential. */
1276
+ const qoderCredentialRef = "QODER_MANAGED_CREDENTIAL";
1277
+ /** DSH's local provider uses `env` and `*-env` for ambient credential layers. */
1278
+ function isEnvironmentCredentialSource(source) {
1279
+ return source === "env" || source?.endsWith("-env") === true;
1280
+ }
1281
+ //#endregion
1282
+ //#region src/credential.ts
1283
+ /** Host-side resolution of the one managed Qoder credential. */
1284
+ const managedQoderCredentialRef = credentialRef(qoderCredentialRef);
1285
+ /** Resolve only provider-managed storage; ambient environment layers are intentionally unsupported. */
1286
+ async function resolveManagedQoderPat(credentials) {
1287
+ const hit = await credentials.resolve(managedQoderCredentialRef);
1288
+ if (hit === void 0 || isEnvironmentCredentialSource(hit.source)) return "";
1289
+ return hit.value.trim();
1290
+ }
1291
+ //#endregion
1292
+ //#region src/models.ts
1293
+ const discoveredMetadataKeys = [
1294
+ "description",
1295
+ "source",
1296
+ "isReasoning",
1297
+ "supportsEffort",
1298
+ "reasoningEfforts",
1299
+ "defaultReasoningEffort",
1300
+ "priceFactor",
1301
+ "contextOptions"
1302
+ ];
1303
+ function positiveNumber(value) {
1304
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
1305
+ }
1306
+ const reasoningEffortOrder = /* @__PURE__ */ new Map([
1307
+ ["low", 0],
1308
+ ["medium", 1],
1309
+ ["high", 2],
1310
+ ["xhigh", 3],
1311
+ ["max", 4]
1312
+ ]);
1313
+ function contextOptionsOf(value) {
1314
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1315
+ const options = {};
1316
+ for (const [key, raw] of Object.entries(value)) {
1317
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
1318
+ const entry = raw;
1319
+ const tokenCount = positiveNumber(entry.token_count);
1320
+ const isDefault = typeof entry.is_default === "boolean" ? entry.is_default : void 0;
1321
+ if (tokenCount === void 0 && isDefault === void 0) continue;
1322
+ options[key] = {
1323
+ ...tokenCount === void 0 ? {} : { tokenCount },
1324
+ ...isDefault === void 0 ? {} : { isDefault }
1325
+ };
1326
+ }
1327
+ if (Object.keys(options).length === 0) return void 0;
1328
+ const largest = Math.max(...Object.values(options).map((option) => option.tokenCount ?? 0));
1329
+ if (largest <= 0) return options;
1330
+ return Object.fromEntries(Object.entries(options).map(([key, option]) => [key, {
1331
+ ...option,
1332
+ isDefault: option.tokenCount === largest
1333
+ }]));
1334
+ }
1335
+ function reasoningEffortsOf(value) {
1336
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
1337
+ const enabled = value.enabled;
1338
+ if (typeof enabled !== "object" || enabled === null || Array.isArray(enabled)) return {};
1339
+ const rawEfforts = enabled.efforts;
1340
+ if (typeof rawEfforts !== "object" || rawEfforts === null || Array.isArray(rawEfforts)) return {};
1341
+ const efforts = [];
1342
+ let defaultEffort;
1343
+ for (const [id, raw] of Object.entries(rawEfforts)) {
1344
+ if (!id.trim() || typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
1345
+ const entry = raw;
1346
+ efforts.push({
1347
+ id,
1348
+ name: id,
1349
+ ...typeof entry.description === "string" && entry.description.trim() ? { description: entry.description.trim() } : {}
1350
+ });
1351
+ if (entry.is_default === true) defaultEffort = id;
1352
+ }
1353
+ return {
1354
+ ...efforts.length === 0 ? {} : { efforts: efforts.sort((left, right) => (reasoningEffortOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (reasoningEffortOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER)) },
1355
+ ...defaultEffort === void 0 ? {} : { defaultEffort }
1356
+ };
1357
+ }
1358
+ function normalizeQoderModels(payload) {
1359
+ if (typeof payload !== "object" || payload === null || !Array.isArray(payload.chat)) return [];
1360
+ const models = [];
1361
+ const seen = /* @__PURE__ */ new Set();
1362
+ for (const raw of payload.chat) {
1363
+ if (typeof raw !== "object" || raw === null || raw.enable !== true) continue;
1364
+ const id = typeof raw.key === "string" ? raw.key.trim() : "";
1365
+ if (!id || seen.has(id)) continue;
1366
+ seen.add(id);
1367
+ const contextOptions = contextOptionsOf(raw.context_config);
1368
+ const contextWindow = Math.max(positiveNumber(raw.max_input_tokens) ?? 18e4, ...Object.values(contextOptions ?? {}).map((option) => option.tokenCount ?? 0));
1369
+ const thinking = typeof raw.thinking_config === "object" && raw.thinking_config !== null;
1370
+ const reasoning = reasoningEffortsOf(raw.thinking_config);
1371
+ models.push({
1372
+ id,
1373
+ name: typeof raw.display_name === "string" && raw.display_name.trim() ? raw.display_name.trim() : id,
1374
+ contextWindow,
1375
+ maxTokens: positiveNumber(raw.max_output_tokens) ?? 32768,
1376
+ source: typeof raw.source === "string" && raw.source.trim() ? raw.source.trim() : "system",
1377
+ isReasoning: raw.is_reasoning === true || thinking,
1378
+ supportsEffort: reasoning.efforts !== void 0,
1379
+ ...reasoning.efforts === void 0 ? {} : { reasoningEfforts: reasoning.efforts },
1380
+ ...reasoning.defaultEffort === void 0 ? {} : { defaultReasoningEffort: reasoning.defaultEffort },
1381
+ ...positiveNumber(raw.price_factor) === void 0 ? {} : { priceFactor: positiveNumber(raw.price_factor) },
1382
+ ...contextOptions === void 0 ? {} : { contextOptions }
1383
+ });
1384
+ }
1385
+ return models;
1386
+ }
1387
+ /**
1388
+ * Restore Qoder-only metadata after DSH's provider-neutral discovery contract
1389
+ * has retained only id, name, and token limits.
1390
+ */
1391
+ function mergeQoderDiscoveryMetadata(configured, discovered) {
1392
+ const catalog = new Map(discovered.map((model) => [model.id, model]));
1393
+ return configured.map((model) => {
1394
+ const advertised = catalog.get(model.id);
1395
+ if (advertised === void 0) return { ...model };
1396
+ const merged = { ...model };
1397
+ for (const key of discoveredMetadataKeys) delete merged[key];
1398
+ for (const key of discoveredMetadataKeys) if (advertised[key] !== void 0) Object.assign(merged, { [key]: advertised[key] });
1399
+ return merged;
1400
+ });
1401
+ }
1402
+ function hasSameQoderDiscoveryMetadata(left, right) {
1403
+ return left?.length === right.length && left.every((model, index) => {
1404
+ const candidate = right[index];
1405
+ return candidate?.id === model.id && discoveredMetadataKeys.every((key) => JSON.stringify(model[key]) === JSON.stringify(candidate[key]));
1406
+ });
1407
+ }
1408
+ async function fetchQoderModels(credentials, options = {}) {
1409
+ const url = getQoderModelListUrl(options.region);
1410
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1411
+ options.logger?.debug?.("[Qoder Models] Requesting model catalog", { url });
1412
+ let response;
1413
+ try {
1414
+ response = await fetchImpl(url, {
1415
+ method: "GET",
1416
+ headers: {
1417
+ accept: "application/json",
1418
+ ...buildAuthHeaders(null, url, credentials)
1419
+ },
1420
+ signal: options.signal
1421
+ });
1422
+ } catch (error) {
1423
+ if (options.signal?.aborted) throw new QoderLlmError("Qoder model discovery was aborted.", "ABORTED");
1424
+ options.logger?.error?.("[Qoder Models] Catalog network request failed", redactLogValue(error));
1425
+ throw new QoderLlmError("Qoder model discovery network request failed.", "TRANSPORT", { cause: error });
1426
+ }
1427
+ options.logger?.debug?.("[Qoder Models] Catalog request completed", {
1428
+ url,
1429
+ status: response.status
1430
+ });
1431
+ const text = await response.text();
1432
+ if (!response.ok) {
1433
+ options.logger?.error?.("[Qoder Models] Catalog request failed", redactLogPayload(text));
1434
+ throw qoderHttpError(`Qoder model discovery failed with HTTP status ${response.status}.`, response);
1435
+ }
1436
+ let payload;
1437
+ try {
1438
+ payload = JSON.parse(text);
1439
+ } catch {
1440
+ throw new QoderLlmError("Qoder model discovery returned invalid JSON.", "MALFORMED_RESPONSE");
1441
+ }
1442
+ const models = normalizeQoderModels(payload);
1443
+ if (models.length === 0) throw new QoderLlmError("Qoder model discovery returned no enabled models.", "EMPTY_RESPONSE");
1444
+ options.logger?.debug?.("[Qoder Models] Model catalog resolved", { models });
1445
+ return models;
1446
+ }
1447
+ //#endregion
1448
+ //#region src/usage.ts
1449
+ const userAgent = "dsh-provider-qoder";
1450
+ const defaultUsageTtlMs = 6e4;
1451
+ const defaultUsageTimeoutMs = 15e3;
1452
+ function normalizeQuota(raw) {
1453
+ if (!raw || typeof raw !== "object") return void 0;
1454
+ const total = typeof raw.total === "number" && Number.isFinite(raw.total) ? raw.total : typeof raw.cap === "number" && Number.isFinite(raw.cap) ? raw.cap : typeof raw.remaining === "number" && typeof raw.used === "number" ? raw.used + raw.remaining : 0;
1455
+ const used = typeof raw.used === "number" && Number.isFinite(raw.used) ? raw.used : 0;
1456
+ const remaining = typeof raw.remaining === "number" && Number.isFinite(raw.remaining) ? raw.remaining : Math.max(0, total - used);
1457
+ let percentage;
1458
+ if (typeof raw.percentage === "number" && Number.isFinite(raw.percentage)) percentage = raw.percentage <= 1 && total > 1 ? raw.percentage * 100 : raw.percentage;
1459
+ else percentage = total > 0 ? used / total * 100 : 0;
1460
+ const unit = typeof raw.unit === "string" && raw.unit.length > 0 ? raw.unit : "credits";
1461
+ return {
1462
+ total,
1463
+ used,
1464
+ remaining,
1465
+ percentage,
1466
+ unit
1467
+ };
1468
+ }
1469
+ function normalizeExpiresAt(rawExpires) {
1470
+ if (rawExpires === void 0 || rawExpires === null) return void 0;
1471
+ if (typeof rawExpires === "number" && rawExpires > 0) return new Date(rawExpires).toISOString();
1472
+ if (typeof rawExpires === "string" && rawExpires.length > 0) {
1473
+ const parsed = Date.parse(rawExpires);
1474
+ if (!Number.isNaN(parsed) && parsed > 0) return new Date(parsed).toISOString();
1475
+ }
1476
+ }
1477
+ var QoderUsageReader = class {
1478
+ authService;
1479
+ fetchImpl;
1480
+ ttlMs;
1481
+ timeoutMs;
1482
+ resolveRegion;
1483
+ region;
1484
+ logger;
1485
+ cache = /* @__PURE__ */ new Map();
1486
+ constructor(options) {
1487
+ this.authService = options.authService;
1488
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
1489
+ this.ttlMs = options.ttlMs ?? defaultUsageTtlMs;
1490
+ this.timeoutMs = options.timeoutMs ?? defaultUsageTimeoutMs;
1491
+ this.resolveRegion = options.resolveRegion;
1492
+ this.region = options.region;
1493
+ this.logger = options.logger;
1494
+ }
1495
+ setRegion(region) {
1496
+ this.region = region;
1497
+ }
1498
+ currentRegion() {
1499
+ if (this.resolveRegion) return this.resolveRegion();
1500
+ return this.region ?? "global";
1501
+ }
1502
+ async readAccount(pat, options) {
1503
+ if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid.", "MISSING_CREDENTIAL");
1504
+ const effectiveRegion = options?.region ?? this.currentRegion();
1505
+ const cacheKey = `${effectiveRegion}:${pat}`;
1506
+ if (!options?.force) {
1507
+ const cached = this.cache.get(cacheKey);
1508
+ if (cached && cached.expiresAt > Date.now()) return cached.info;
1509
+ }
1510
+ const creds = await this.authService.getCredentials(pat, options?.signal, effectiveRegion);
1511
+ const accountInfo = {
1512
+ profile: {
1513
+ id: creds.userID,
1514
+ name: creds.name || "Qoder User",
1515
+ email: creds.email || ""
1516
+ },
1517
+ usage: await this.fetchUsage(creds.authToken, options?.signal, effectiveRegion),
1518
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1519
+ };
1520
+ this.cache.set(cacheKey, {
1521
+ info: accountInfo,
1522
+ expiresAt: Date.now() + this.ttlMs
1523
+ });
1524
+ return accountInfo;
1525
+ }
1526
+ clear(pat) {
1527
+ if (pat) {
1528
+ for (const key of this.cache.keys()) if (key.endsWith(`:${pat}`) || key === pat) this.cache.delete(key);
1529
+ } else this.cache.clear();
1530
+ }
1531
+ async fetchUsage(jobToken, signal, region) {
1532
+ const url = getQoderUsageUrl(region ?? this.currentRegion());
1533
+ const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
1534
+ const requestSignal = signal === void 0 ? timeoutSignal : AbortSignal.any([signal, timeoutSignal]);
1535
+ this.logger?.debug?.("[Qoder Usage] Requesting quota usage", { url });
1536
+ try {
1537
+ const response = await this.fetchImpl(url, {
1538
+ method: "GET",
1539
+ headers: {
1540
+ authorization: `Bearer ${jobToken}`,
1541
+ accept: "application/json",
1542
+ "user-agent": userAgent,
1543
+ "cosy-version": "1.0.1",
1544
+ "cosy-clienttype": "5"
1545
+ },
1546
+ signal: requestSignal
1547
+ });
1548
+ this.logger?.debug?.("[Qoder Usage] Request completed", {
1549
+ status: response.status,
1550
+ statusText: response.statusText
1551
+ });
1552
+ const text = await response.text();
1553
+ if (!response.ok) {
1554
+ this.logger?.error?.("[Qoder Usage] Request failed", redactLogPayload(text));
1555
+ throw qoderHttpError(`Failed to fetch Qoder quota usage with status ${response.status}: ${text}`, response);
1556
+ }
1557
+ let data;
1558
+ try {
1559
+ data = JSON.parse(text);
1560
+ } catch {
1561
+ this.logger?.error?.("[Qoder Usage] Invalid JSON response", redactLogPayload(text));
1562
+ throw new QoderLlmError("Failed to parse Qoder quota JSON response", "USAGE_FETCH_FAILED");
1563
+ }
1564
+ this.logger?.debug?.("[Qoder Usage] Quota usage resolved", redactLogValue(data));
1565
+ return {
1566
+ userQuota: normalizeQuota(data.userQuota),
1567
+ orgResourcePackage: normalizeQuota(data.orgResourcePackage),
1568
+ totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
1569
+ isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
1570
+ expiresAt: normalizeExpiresAt(data.expiresAt),
1571
+ raw: data
1572
+ };
1573
+ } catch (error) {
1574
+ if (error instanceof QoderLlmError) throw error;
1575
+ if (signal?.aborted) throw new QoderLlmError("Qoder quota usage request was aborted.", "ABORTED");
1576
+ if (timeoutSignal.aborted) throw new QoderLlmError("Qoder quota usage request timed out.", "TIMEOUT");
1577
+ throw new QoderLlmError("Qoder quota usage network request failed.", "TRANSPORT", { cause: error });
1578
+ }
1579
+ }
1580
+ };
1581
+ //#endregion
1582
+ //#region src/index.ts
1583
+ const name = "provider-qoder";
1584
+ const inject = [
1585
+ "llm",
1586
+ "credentials",
1587
+ "connection"
1588
+ ];
1589
+ const providerQoder = "qoder-official";
1590
+ const settingsNamespace = "provider-qoder";
1591
+ const qoderChannel = "/qoder-subscription";
1592
+ const fiberDisposed = 4;
1593
+ const fiberUnloading = 5;
1594
+ const catalogModel = z.object({
1595
+ id: z.string().required(),
1596
+ name: z.string().required(),
1597
+ description: z.string(),
1598
+ contextWindow: z.number().step(1).min(1),
1599
+ maxTokens: z.number().step(1).min(1),
1600
+ source: z.string(),
1601
+ isReasoning: z.boolean(),
1602
+ supportsEffort: z.boolean(),
1603
+ reasoningEfforts: z.array(z.object({
1604
+ id: z.string().required(),
1605
+ name: z.string().required(),
1606
+ description: z.string()
1607
+ })),
1608
+ defaultReasoningEffort: z.string(),
1609
+ priceFactor: z.number().min(0),
1610
+ contextOptions: z.dict(z.object({
1611
+ tokenCount: z.number().step(1).min(1),
1612
+ isDefault: z.boolean()
1613
+ }))
1614
+ });
1615
+ const Config = z.object({
1616
+ region: z.union(["global", "china"]).default("global"),
1617
+ models: z.array(catalogModel).default(defaultModels),
1618
+ streamIdleTimeoutMs: z.number().step(1).min(1).default(defaultStreamIdleTimeoutMs)
1619
+ });
1620
+ function resolveModels(models) {
1621
+ const resolved = models ?? defaultModels;
1622
+ if (resolved.length === 0) throw new Error("provider-qoder: at least one model is required");
1623
+ const seen = /* @__PURE__ */ new Set();
1624
+ return resolved.map((model) => {
1625
+ if (!model.id || !model.name) throw new Error("provider-qoder: model id and name must be non-empty");
1626
+ if (seen.has(model.id)) throw new Error(`provider-qoder: duplicate model id "${model.id}"`);
1627
+ seen.add(model.id);
1628
+ return { ...model };
1629
+ });
1630
+ }
1631
+ function publicError(message) {
1632
+ return {
1633
+ ok: false,
1634
+ error: {
1635
+ code: "internal",
1636
+ message,
1637
+ details: { issues: [] }
1638
+ }
1639
+ };
1640
+ }
1641
+ function apply(ctx, config = {}) {
1642
+ const logger = ctx.logger;
1643
+ let current = () => baseConfig;
1644
+ const authService = new QoderAuthService({
1645
+ logger,
1646
+ resolveRegion: () => current().region ?? "global"
1647
+ });
1648
+ const usageReader = new QoderUsageReader({
1649
+ authService,
1650
+ logger,
1651
+ resolveRegion: () => current().region ?? "global"
1652
+ });
1653
+ const baseConfig = {
1654
+ region: config.region ?? "global",
1655
+ models: resolveModels(config.models),
1656
+ streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5
1657
+ };
1658
+ let discoveredCatalog = [];
1659
+ let previousRegion = baseConfig.region ?? "global";
1660
+ const resolveConfig = () => ({
1661
+ region: current().region ?? "global",
1662
+ models: mergeQoderDiscoveryMetadata(resolveModels(current().models), discoveredCatalog),
1663
+ streamIdleTimeoutMs: current().streamIdleTimeoutMs ?? 3e5
1664
+ });
1665
+ const initial = resolveConfig();
1666
+ const adapter = new QoderAdapter({
1667
+ resolvePat: () => resolveManagedQoderPat(ctx.credentials),
1668
+ models: initial.models,
1669
+ providerId: providerQoder,
1670
+ providerName: "Qoder",
1671
+ authService,
1672
+ logger,
1673
+ streamIdleTimeoutMs: initial.streamIdleTimeoutMs,
1674
+ region: initial.region
1675
+ });
1676
+ const registration = ctx.llm.registerAdapter([providerQoder], adapter);
1677
+ const refreshAdapter = () => {
1678
+ const next = resolveConfig();
1679
+ if (next.region !== previousRegion) {
1680
+ authService.clear();
1681
+ usageReader.clear();
1682
+ previousRegion = next.region;
1683
+ }
1684
+ adapter.replaceConfig(next.models, next.streamIdleTimeoutMs, next.region);
1685
+ registration.replace([providerQoder]);
1686
+ };
1687
+ ctx.inject(["settings"], (settingsCtx) => {
1688
+ const scope = settingsCtx.settings.register(settingsNamespace, Config, {
1689
+ base: baseConfig,
1690
+ validate: (value) => {
1691
+ resolveModels(value.models);
1692
+ }
1693
+ });
1694
+ current = () => scope.get();
1695
+ refreshAdapter();
1696
+ scope.watch(async (next) => {
1697
+ if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
1698
+ refreshAdapter();
1699
+ const enriched = mergeQoderDiscoveryMetadata(resolveModels(next.models), discoveredCatalog);
1700
+ if (!hasSameQoderDiscoveryMetadata(next.models, enriched)) await scope.update({ models: enriched });
1701
+ });
1702
+ settingsCtx.effect(() => () => {
1703
+ if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
1704
+ current = () => baseConfig;
1705
+ refreshAdapter();
1706
+ });
1707
+ });
1708
+ const discoverModels = async (signal, suppliedPat) => {
1709
+ const pat = suppliedPat?.trim() || await resolveManagedQoderPat(ctx.credentials);
1710
+ if (!pat) throw new QoderLlmError("Qoder PAT is not configured.", "MISSING_CREDENTIAL");
1711
+ const region = current().region ?? "global";
1712
+ const models = await fetchQoderModels(await authService.getCredentials(pat, signal, region), {
1713
+ signal,
1714
+ logger,
1715
+ region
1716
+ });
1717
+ discoveredCatalog = models;
1718
+ return models;
1719
+ };
1720
+ ctx.llm.registerModelDiscovery(settingsNamespace, async (request, signal) => {
1721
+ if (request.provider !== void 0 && request.provider !== providerQoder) throw new QoderLlmError(`Qoder discovery does not own provider "${request.provider}".`, "INVALID_PROVIDER");
1722
+ return (await discoverModels(signal, request.apiKey)).map((model) => ({
1723
+ id: model.id,
1724
+ name: model.name,
1725
+ contextWindow: model.contextWindow,
1726
+ maxTokens: model.maxTokens
1727
+ }));
1728
+ });
1729
+ if (ctx.connection?.rpc) {
1730
+ const handler = async (endpoint, payload, signal) => {
1731
+ if (endpoint !== "account" && endpoint !== "models") return publicError(`Unknown endpoint: ${endpoint}`);
1732
+ signal.throwIfAborted();
1733
+ if (endpoint === "models") try {
1734
+ return {
1735
+ ok: true,
1736
+ value: await discoverModels(signal)
1737
+ };
1738
+ } catch (error) {
1739
+ if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") throw error;
1740
+ logger?.error?.("[Qoder RPC] Failed to discover models", redactLogValue(error));
1741
+ return publicError(error instanceof Error ? error.message : "Failed to discover Qoder models");
1742
+ }
1743
+ const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
1744
+ logger?.debug?.("[Qoder RPC] Reading subscriber account", { force });
1745
+ let pat;
1746
+ try {
1747
+ pat = await resolveManagedQoderPat(ctx.credentials);
1748
+ } catch (error) {
1749
+ if (signal.aborted) throw error;
1750
+ logger?.error?.("[Qoder RPC] Failed to resolve managed PAT", redactLogValue(error));
1751
+ return publicError(error instanceof Error ? error.message : "No Qoder PAT configured");
1752
+ }
1753
+ if (!pat) {
1754
+ logger?.warn?.("[Qoder RPC] No managed Qoder PAT is configured");
1755
+ return publicError("Qoder PAT is not configured");
1756
+ }
1757
+ try {
1758
+ const account = await usageReader.readAccount(pat, {
1759
+ force,
1760
+ signal,
1761
+ region: current().region ?? "global"
1762
+ });
1763
+ logger?.debug?.("[Qoder RPC] Subscriber account resolved");
1764
+ return {
1765
+ ok: true,
1766
+ value: account
1767
+ };
1768
+ } catch (error) {
1769
+ if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") throw error;
1770
+ logger?.error?.("[Qoder RPC] Failed to read subscriber account", redactLogValue(error));
1771
+ return publicError(error instanceof Error ? error.message : "Failed to load Qoder account");
1772
+ }
1773
+ };
1774
+ ctx.effect(() => ctx.connection.rpc.handle(qoderChannel, handler), "provider-qoder: loopback account RPC");
1775
+ }
1776
+ }
1777
+ //#endregion
1778
+ export { Config, QoderAdapter, QoderAuthService, QoderLlmError, QoderUsageReader, apply, buildAuthHeaders, buildQoderRequestBody, computeSigPath, defaultMaxTokens, defaultModels, defaultStreamIdleTimeoutMs, fetchQoderModels, getMachineId, getQoderChatUrl, getQoderExchangeUrl, getQoderModelListUrl, getQoderUsageUrl, getQoderUserInfoUrl, hasSameQoderDiscoveryMetadata, inject, isEnvironmentCredentialSource, managedQoderCredentialRef, mergeQoderDiscoveryMetadata, name, normalizeQoderModels, parseQoderSse, qoderCredentialRef, qoderEncodeBody, qoderGlobalBaseUrl, qoderGlobalOpenApiUrl, qoderHttpError, qoderRegionEndpoints, redactLogPayload, redactLogValue, resolveManagedQoderPat, resolveQoderEndpoints, retryAfterMs, translateTools, validateAndTranslateMessages, validateQoderRequest };