dsh-provider-qoder 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/README_CN.md +12 -8
- package/lib/client.js +1355 -384
- package/lib/index.d.ts +69 -24
- package/lib/index.js +2669 -635
- package/package.json +3 -1
package/lib/index.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, ToolCallId, attributionHeaders, contentHasImage } from "@deepseek-ai/dsh-llm";
|
|
3
|
-
import crypto, { createHash } from "node:crypto";
|
|
2
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, ToolCallId, attributionHeaders, contentHasImage, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { homedir, hostname } from "node:os";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
8
|
+
import * as zlib from "node:zlib";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
8
11
|
import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
|
|
9
|
-
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
12
|
+
import { credentialKey, credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
13
|
+
//#region src/qoder/queue.ts
|
|
14
|
+
const defaultModelQueueMaxWaitMs = 3600 * 1e3;
|
|
15
|
+
//#endregion
|
|
10
16
|
//#region src/qoder/catalog.ts
|
|
11
17
|
const discoveredMetadataKeys = [
|
|
12
18
|
"description",
|
|
@@ -103,7 +109,7 @@ function normalizeQoderModels(payload, onConflict) {
|
|
|
103
109
|
const rawOriginalPrice = nonNegativeNumber(raw.original_price_factor) ?? nonNegativeNumber(raw.originPriceFactor);
|
|
104
110
|
const promotionPrice = typeof raw.promotion === "object" && raw.promotion !== null && raw.promotion.active === true ? nonNegativeNumber(raw.promotion.before_promotion_price_factor) : void 0;
|
|
105
111
|
const originalPriceFactor = rawOriginalPrice ?? promotionPrice;
|
|
106
|
-
const isFree = raw.is_free === true || raw.isFree === true || Array.isArray(raw.tags) && raw.tags.includes("limited_time_free")
|
|
112
|
+
const isFree = priceFactor === void 0 ? raw.is_free === true || raw.isFree === true || Array.isArray(raw.tags) && raw.tags.includes("limited_time_free") : priceFactor === 0;
|
|
107
113
|
const reasoning = reasoningEffortsOf(raw.thinking_config);
|
|
108
114
|
models.push({
|
|
109
115
|
id,
|
|
@@ -153,12 +159,12 @@ function formatRateFactor(value) {
|
|
|
153
159
|
}
|
|
154
160
|
/**
|
|
155
161
|
* Format model price factor / rate string.
|
|
156
|
-
* -
|
|
162
|
+
* - Price factor 0, or free models without a price factor: '免费' or 'Free'
|
|
157
163
|
* - Promotional models: '0.5x(原 1x)' or '0.5x (was 1x)'
|
|
158
164
|
* - Standard models: '1x'
|
|
159
165
|
*/
|
|
160
166
|
function formatModelRate(model, isZh = true) {
|
|
161
|
-
if (model.
|
|
167
|
+
if (model.priceFactor === 0 || model.priceFactor === void 0 && model.isFree) return isZh ? "免费" : "Free";
|
|
162
168
|
if (model.priceFactor === void 0) return void 0;
|
|
163
169
|
const current = `${formatRateFactor(model.priceFactor)}x`;
|
|
164
170
|
if (model.originalPriceFactor !== void 0 && model.originalPriceFactor > model.priceFactor) {
|
|
@@ -305,6 +311,21 @@ function getQoderChatUrl(region = "global") {
|
|
|
305
311
|
const { baseUrl } = resolveQoderEndpoints(region);
|
|
306
312
|
return `${baseUrl}algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1`;
|
|
307
313
|
}
|
|
314
|
+
function getQoderQueueStatusUrl(region, params) {
|
|
315
|
+
const query = new URLSearchParams({
|
|
316
|
+
requestSetId: params.requestSetId,
|
|
317
|
+
modelKey: params.modelKey
|
|
318
|
+
});
|
|
319
|
+
if (params.queueType) query.set("queueType", params.queueType);
|
|
320
|
+
return `${resolveQoderEndpoints(region).baseUrl}algo/api/v2/service/ask/queue/status?${query}`;
|
|
321
|
+
}
|
|
322
|
+
function getQoderQueueFinishUrl(region) {
|
|
323
|
+
return `${resolveQoderEndpoints(region).baseUrl}algo/api/v2/service/ask/finish?Encode=1`;
|
|
324
|
+
}
|
|
325
|
+
/** model-server HTTP endpoint. */
|
|
326
|
+
function getQoderModelServerUrl(region = "global") {
|
|
327
|
+
return `https://${region === "china" ? "gateway.qoder.com.cn" : "api2-v2.qoder.sh"}/model/v1/chat/completions`;
|
|
328
|
+
}
|
|
308
329
|
function getQoderModelListUrl(region = "global") {
|
|
309
330
|
const { baseUrl } = resolveQoderEndpoints(region);
|
|
310
331
|
return `${baseUrl}algo/api/v2/model/list?Encode=1`;
|
|
@@ -348,10 +369,222 @@ function getQoderWebSearchUrl(region = "global") {
|
|
|
348
369
|
* @module dsh-provider-qoder/qoder/errors
|
|
349
370
|
*/
|
|
350
371
|
var QoderLlmError = class extends LlmError {
|
|
372
|
+
upstreamCode;
|
|
373
|
+
source;
|
|
374
|
+
httpStatus;
|
|
351
375
|
constructor(message, code = "UNKNOWN_ERROR", options) {
|
|
352
376
|
super(message, code, options);
|
|
353
377
|
}
|
|
354
378
|
};
|
|
379
|
+
/** Internal protocol signal; the queue controller consumes it before DSH sees it. */
|
|
380
|
+
var QoderModelQueuedError = class extends QoderLlmError {
|
|
381
|
+
queue;
|
|
382
|
+
constructor(queue) {
|
|
383
|
+
super("Qoder model is queued.", "QODER_MODEL_QUEUED");
|
|
384
|
+
this.queue = queue;
|
|
385
|
+
this.upstreamCode = "10605";
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
const modelBusinessErrors = {
|
|
389
|
+
"103": {
|
|
390
|
+
code: "PROVIDER_ERROR",
|
|
391
|
+
message: "Qoder rejected a duplicate model request."
|
|
392
|
+
},
|
|
393
|
+
"105": {
|
|
394
|
+
code: "AUTH",
|
|
395
|
+
message: "Qoder subscriber authentication has expired."
|
|
396
|
+
},
|
|
397
|
+
"110": {
|
|
398
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
399
|
+
message: "Qoder daily usage limit has been reached."
|
|
400
|
+
},
|
|
401
|
+
"112": {
|
|
402
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
403
|
+
message: "Qoder personal Credits are exhausted."
|
|
404
|
+
},
|
|
405
|
+
"114": {
|
|
406
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
407
|
+
message: "Qoder free trial account limit has been reached."
|
|
408
|
+
},
|
|
409
|
+
"115": {
|
|
410
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
411
|
+
message: "Qoder free subscriber quota limit has been reached."
|
|
412
|
+
},
|
|
413
|
+
"116": {
|
|
414
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
415
|
+
message: "Qoder Teams administrator Credits are exhausted."
|
|
416
|
+
},
|
|
417
|
+
"117": {
|
|
418
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
419
|
+
message: "Qoder Teams member Credits are exhausted."
|
|
420
|
+
},
|
|
421
|
+
"119": {
|
|
422
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
423
|
+
message: "Qoder Vela model free usage limit has been reached."
|
|
424
|
+
},
|
|
425
|
+
"122": {
|
|
426
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
427
|
+
message: "Qoder billing group Credits limit has been reached."
|
|
428
|
+
},
|
|
429
|
+
"406": {
|
|
430
|
+
code: "PROVIDER_ERROR",
|
|
431
|
+
message: "Qoder rejected the request through its content safety policy."
|
|
432
|
+
},
|
|
433
|
+
"416": {
|
|
434
|
+
code: "INVALID_REQUEST",
|
|
435
|
+
message: "Qoder cannot satisfy the requested range."
|
|
436
|
+
},
|
|
437
|
+
"430": {
|
|
438
|
+
code: "INVALID_REQUEST",
|
|
439
|
+
message: "Qoder does not support this request; check client compatibility."
|
|
440
|
+
},
|
|
441
|
+
"10605": {
|
|
442
|
+
code: "RATE_LIMIT",
|
|
443
|
+
message: "Qoder model is queued."
|
|
444
|
+
},
|
|
445
|
+
"100400": {
|
|
446
|
+
code: "PROVIDER_ERROR",
|
|
447
|
+
message: "Qoder custom model service failed."
|
|
448
|
+
},
|
|
449
|
+
"100401": {
|
|
450
|
+
code: "AUTH",
|
|
451
|
+
message: "Qoder custom model authentication failed."
|
|
452
|
+
},
|
|
453
|
+
"100403": {
|
|
454
|
+
code: "PROVIDER_ERROR",
|
|
455
|
+
message: "Qoder custom model is unavailable."
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
/** Decode only model rejection diagnostics, with bounded depth, work and JSON size. */
|
|
459
|
+
function modelErrorDetails(raw, status) {
|
|
460
|
+
let upstreamCode;
|
|
461
|
+
let codeDepth = -1;
|
|
462
|
+
let detail;
|
|
463
|
+
let textCode;
|
|
464
|
+
let delayMs;
|
|
465
|
+
let remaining = 100;
|
|
466
|
+
const seen = /* @__PURE__ */ new Set();
|
|
467
|
+
function visit(value, depth) {
|
|
468
|
+
if (depth > 10 || --remaining < 0) return;
|
|
469
|
+
if (typeof value === "string") {
|
|
470
|
+
const text = value.trim();
|
|
471
|
+
if (!text) return;
|
|
472
|
+
if (text.length <= 16 * 1024) try {
|
|
473
|
+
visit(JSON.parse(text), depth + 1);
|
|
474
|
+
return;
|
|
475
|
+
} catch {}
|
|
476
|
+
const diagnostic = text.slice(0, 16 * 1024);
|
|
477
|
+
if (isContextWindowExceededError(diagnostic)) textCode = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
478
|
+
else if (textCode === void 0 && isQuotaExceededError(diagnostic)) textCode = QUOTA_EXCEEDED_CODE;
|
|
479
|
+
detail = text.slice(0, 300);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || seen.has(value)) return;
|
|
483
|
+
seen.add(value);
|
|
484
|
+
const obj = value;
|
|
485
|
+
for (const key of [
|
|
486
|
+
"code",
|
|
487
|
+
"error_code",
|
|
488
|
+
"errorCode",
|
|
489
|
+
"errCode"
|
|
490
|
+
]) {
|
|
491
|
+
const candidate = obj[key];
|
|
492
|
+
const code = (typeof candidate === "number" && Number.isFinite(candidate) ? String(candidate) : typeof candidate === "string" ? candidate.trim() : void 0)?.slice(0, 100);
|
|
493
|
+
if (!code || code === "0" || code === String(status) && !Object.hasOwn(modelBusinessErrors, code)) continue;
|
|
494
|
+
if (depth >= codeDepth) {
|
|
495
|
+
upstreamCode = code;
|
|
496
|
+
codeDepth = depth;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const seconds = obj.retryAfterSeconds;
|
|
500
|
+
if (typeof seconds === "number" && Number.isFinite(seconds * 1e3) && seconds > 0) delayMs = Math.max(delayMs ?? 0, seconds * 1e3);
|
|
501
|
+
for (const key of [
|
|
502
|
+
"message",
|
|
503
|
+
"body",
|
|
504
|
+
"details",
|
|
505
|
+
"error",
|
|
506
|
+
"cause",
|
|
507
|
+
"data",
|
|
508
|
+
"qoderApiError",
|
|
509
|
+
"providerError"
|
|
510
|
+
]) visit(obj[key], depth + 1);
|
|
511
|
+
}
|
|
512
|
+
visit(raw, 0);
|
|
513
|
+
return {
|
|
514
|
+
upstreamCode,
|
|
515
|
+
detail,
|
|
516
|
+
textCode,
|
|
517
|
+
delayMs
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/** Model business semantics precede HTTP status; other transport capabilities keep their own protocols. */
|
|
521
|
+
function qoderModelError(message, response) {
|
|
522
|
+
const details = modelErrorDetails(response.cause, response.status);
|
|
523
|
+
const known = details.upstreamCode === void 0 ? void 0 : Object.hasOwn(modelBusinessErrors, details.upstreamCode) ? modelBusinessErrors[details.upstreamCode] : void 0;
|
|
524
|
+
const fallback = qoderHttpError(message, response);
|
|
525
|
+
let code = fallback.code;
|
|
526
|
+
let summary = details.detail ? `${message}: ${details.detail}` : message;
|
|
527
|
+
if (details.upstreamCode !== void 0) {
|
|
528
|
+
code = known?.code ?? "PROVIDER_ERROR";
|
|
529
|
+
summary = `${known?.message ?? summary} (Qoder code ${details.upstreamCode}).`;
|
|
530
|
+
} else if (details.textCode === CONTEXT_WINDOW_EXCEEDED_CODE) {
|
|
531
|
+
code = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
532
|
+
summary = "Qoder model context window has been exceeded.";
|
|
533
|
+
} else if (details.textCode === QUOTA_EXCEEDED_CODE) {
|
|
534
|
+
code = QUOTA_EXCEEDED_CODE;
|
|
535
|
+
summary = "Qoder subscriber quota is exhausted.";
|
|
536
|
+
}
|
|
537
|
+
const providerRetryAfterMs = Math.max(fallback.failure.providerRetryAfterMs ?? 0, details.delayMs ?? 0) || void 0;
|
|
538
|
+
const error = new QoderLlmError(summary, code, {
|
|
539
|
+
status: response.status,
|
|
540
|
+
...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
|
|
541
|
+
...fallback.failure.requestId === void 0 ? {} : { requestId: fallback.failure.requestId },
|
|
542
|
+
...response.cause === void 0 ? {} : { cause: response.cause }
|
|
543
|
+
});
|
|
544
|
+
error.upstreamCode = details.upstreamCode;
|
|
545
|
+
error.source = response.source;
|
|
546
|
+
error.httpStatus = response.httpStatus ?? (response.source === "http" ? response.status : void 0);
|
|
547
|
+
return error;
|
|
548
|
+
}
|
|
549
|
+
function canRefreshModelCredentials(error) {
|
|
550
|
+
return error.source === "http" && (error.failure.status === 401 || error.failure.status === 403) && error.code === "AUTH" && (error.upstreamCode === void 0 || error.upstreamCode === "105");
|
|
551
|
+
}
|
|
552
|
+
/** Model-server errors have their own wire contract; do not apply legacy business codes. */
|
|
553
|
+
function qoderModelServerError(status, cause, source, headers) {
|
|
554
|
+
let payload = cause;
|
|
555
|
+
if (typeof payload === "string") try {
|
|
556
|
+
payload = JSON.parse(payload);
|
|
557
|
+
} catch {}
|
|
558
|
+
const root = payload !== null && typeof payload === "object" ? payload : {};
|
|
559
|
+
const nested = root.error !== null && typeof root.error === "object" ? root.error : root;
|
|
560
|
+
const rawCode = nested.code ?? root.code;
|
|
561
|
+
const upstreamCode = typeof rawCode === "string" || typeof rawCode === "number" ? String(rawCode) : void 0;
|
|
562
|
+
const effectiveStatus = upstreamCode === "105" ? 401 : status;
|
|
563
|
+
const fallback = qoderHttpError(`Qoder model-server returned HTTP ${status}.`, {
|
|
564
|
+
status: effectiveStatus,
|
|
565
|
+
headers
|
|
566
|
+
});
|
|
567
|
+
const code = upstreamCode === "105" ? "AUTH" : upstreamCode === "10605" ? "RATE_LIMIT" : upstreamCode && (/* @__PURE__ */ new Set([
|
|
568
|
+
"110",
|
|
569
|
+
"112",
|
|
570
|
+
"114",
|
|
571
|
+
"115",
|
|
572
|
+
"116",
|
|
573
|
+
"117",
|
|
574
|
+
"118",
|
|
575
|
+
"119",
|
|
576
|
+
"122"
|
|
577
|
+
])).has(upstreamCode) ? QUOTA_EXCEEDED_CODE : upstreamCode === "430" ? "INVALID_REQUEST" : upstreamCode === void 0 ? fallback.code : "PROVIDER_ERROR";
|
|
578
|
+
const error = new QoderLlmError(typeof nested.message === "string" ? nested.message.slice(0, 300) : fallback.message, code, {
|
|
579
|
+
status: effectiveStatus,
|
|
580
|
+
...fallback.failure.requestId === void 0 ? {} : { requestId: fallback.failure.requestId },
|
|
581
|
+
...fallback.failure.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: fallback.failure.providerRetryAfterMs }
|
|
582
|
+
});
|
|
583
|
+
error.source = source;
|
|
584
|
+
error.httpStatus = source === "http" ? status : void 0;
|
|
585
|
+
error.upstreamCode = upstreamCode;
|
|
586
|
+
return error;
|
|
587
|
+
}
|
|
355
588
|
function qoderHttpError(message, response) {
|
|
356
589
|
const { status } = response;
|
|
357
590
|
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";
|
|
@@ -382,11 +615,16 @@ function retryAfterMs(value, nowMs = Date.now()) {
|
|
|
382
615
|
}
|
|
383
616
|
//#endregion
|
|
384
617
|
//#region src/qoder/transport/machine-id.ts
|
|
385
|
-
|
|
386
|
-
|
|
618
|
+
/** Match the native CLI's region-specific config directory overrides. */
|
|
619
|
+
function qoderMachineIdPaths(region = "global", env = process.env, home = homedir()) {
|
|
620
|
+
const prefix = region === "china" ? "QODERCN_" : "QODER_";
|
|
621
|
+
const cliHome = env[`${prefix}CLI_HOME`] ?? env.GEMINI_CLI_HOME ?? home;
|
|
622
|
+
const configDir = env[`${prefix}CONFIG_DIR`];
|
|
623
|
+
return [join(configDir ? resolve(configDir) : join(cliHome, region === "china" ? ".qoder-cn" : ".qoder"), ".auth", "machine_id"), join(home, ".dsh", "qoder", "machine_id")];
|
|
387
624
|
}
|
|
625
|
+
const ephemeralMachineIds = /* @__PURE__ */ new Map();
|
|
388
626
|
/** Read Qoder's machine id or create the DSH-owned fallback. */
|
|
389
|
-
function getMachineId(paths =
|
|
627
|
+
function getMachineId(paths = qoderMachineIdPaths()) {
|
|
390
628
|
for (const path of paths) {
|
|
391
629
|
if (!existsSync(path)) continue;
|
|
392
630
|
try {
|
|
@@ -394,8 +632,8 @@ function getMachineId(paths = defaultMachineIdPaths()) {
|
|
|
394
632
|
if (value) return value;
|
|
395
633
|
} catch {}
|
|
396
634
|
}
|
|
397
|
-
const machineId = crypto.randomUUID();
|
|
398
635
|
const savePath = paths.at(-1);
|
|
636
|
+
const machineId = (savePath === void 0 ? void 0 : ephemeralMachineIds.get(savePath)) ?? crypto.randomUUID();
|
|
399
637
|
if (savePath !== void 0) try {
|
|
400
638
|
mkdirSync(dirname(savePath), { recursive: true });
|
|
401
639
|
writeFileSync(savePath, machineId, {
|
|
@@ -408,6 +646,7 @@ function getMachineId(paths = defaultMachineIdPaths()) {
|
|
|
408
646
|
if (existing) return existing;
|
|
409
647
|
} catch {}
|
|
410
648
|
}
|
|
649
|
+
if (savePath !== void 0) ephemeralMachineIds.set(savePath, machineId);
|
|
411
650
|
return machineId;
|
|
412
651
|
}
|
|
413
652
|
//#endregion
|
|
@@ -434,7 +673,12 @@ function redactString(value) {
|
|
|
434
673
|
}
|
|
435
674
|
/** Redact credentials and bound arbitrary provider values before logging them. */
|
|
436
675
|
function redactLogValue(value, depth = 0) {
|
|
437
|
-
if (typeof value === "string")
|
|
676
|
+
if (typeof value === "string") {
|
|
677
|
+
if (depth < maxDepth && value.length <= 16 * 1024 && /^[\s]*[{"]/u.test(value)) try {
|
|
678
|
+
return redactString(JSON.stringify(redactLogValue(JSON.parse(value), depth + 1)));
|
|
679
|
+
} catch {}
|
|
680
|
+
return redactString(value);
|
|
681
|
+
}
|
|
438
682
|
if (value === null || typeof value !== "object") return value;
|
|
439
683
|
if (depth >= maxDepth) return "[TRUNCATED]";
|
|
440
684
|
if (value instanceof Error) return {
|
|
@@ -442,6 +686,10 @@ function redactLogValue(value, depth = 0) {
|
|
|
442
686
|
message: redactString(value.message),
|
|
443
687
|
..."code" in value ? { code: redactLogValue(value.code, depth + 1) } : {},
|
|
444
688
|
..."cause" in value ? { cause: redactLogValue(value.cause, depth + 1) } : {},
|
|
689
|
+
..."upstreamCode" in value ? { upstreamCode: redactLogValue(value.upstreamCode, depth + 1) } : {},
|
|
690
|
+
..."source" in value ? { source: value.source } : {},
|
|
691
|
+
..."httpStatus" in value ? { httpStatus: value.httpStatus } : {},
|
|
692
|
+
..."failure" in value ? { failure: redactLogValue(value.failure, depth + 1) } : {},
|
|
445
693
|
...value instanceof AggregateError ? { errors: redactLogValue(value.errors, depth + 1) } : {}
|
|
446
694
|
};
|
|
447
695
|
if (Array.isArray(value)) return value.slice(0, maxEntries).map((item) => redactLogValue(item, depth + 1));
|
|
@@ -468,6 +716,24 @@ function logParsedResponse(logger, operation, result) {
|
|
|
468
716
|
}));
|
|
469
717
|
}
|
|
470
718
|
//#endregion
|
|
719
|
+
//#region src/qoder/transport/wire/client-identity.ts
|
|
720
|
+
/** Client identity shared by Qoder model protocols and Open API exchanges. */
|
|
721
|
+
const qoderIdeVersion = "1.1.73";
|
|
722
|
+
const defaultUserAgent = `qoder/${qoderIdeVersion}`;
|
|
723
|
+
function qoderMachineOs(platform = process.platform, arch = process.arch) {
|
|
724
|
+
return `${arch === "arm64" ? "aarch64" : arch === "x64" ? "x86_64" : arch}_${platform}`;
|
|
725
|
+
}
|
|
726
|
+
/** Identity injected by qodercli's common infer-sse HTTP request pipeline. */
|
|
727
|
+
function qoderModelClientHeaders() {
|
|
728
|
+
const machineHostname = hostname().replace(/[^\x20-\x7e]/gu, "").trim();
|
|
729
|
+
return {
|
|
730
|
+
"Cosy-Version": qoderIdeVersion,
|
|
731
|
+
"Cosy-ClientType": "5",
|
|
732
|
+
"Cosy-MachineOS": qoderMachineOs(),
|
|
733
|
+
...machineHostname ? { "Cosy-MachineHostname": machineHostname } : {}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
//#endregion
|
|
471
737
|
//#region src/qoder/transport/wire/cosy.ts
|
|
472
738
|
/**
|
|
473
739
|
* COSY authentication headers and signature algorithm for Qoder services.
|
|
@@ -483,11 +749,8 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
|
483
749
|
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
|
|
484
750
|
XcW+ML9FoCI6AOvOzwIDAQAB
|
|
485
751
|
-----END PUBLIC KEY-----`;
|
|
486
|
-
const qoderIdeVersion = "1.1.60";
|
|
487
|
-
const defaultUserAgent = `qoder/${qoderIdeVersion}`;
|
|
488
752
|
const qoderDataPolicy = "disagree";
|
|
489
753
|
const qoderLoginVersion = "v2";
|
|
490
|
-
const qoderMachineOs = process.platform === "win32" ? process.arch === "arm64" ? "aarch64_windows" : "x86_64_windows" : process.arch === "arm64" ? "aarch64_linux" : "x86_64_linux";
|
|
491
754
|
const qoderMachineTypeMagic = "5";
|
|
492
755
|
function computeSigPath(urlStr) {
|
|
493
756
|
let sigPath = new URL(urlStr).pathname;
|
|
@@ -514,9 +777,9 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
514
777
|
const userInfo = {
|
|
515
778
|
uid: creds.userID,
|
|
516
779
|
security_oauth_token: creds.authToken,
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
780
|
+
...creds.organizationId === void 0 ? {} : { organization_id: creds.organizationId },
|
|
781
|
+
...creds.organizationTags === void 0 ? {} : { organization_tags: creds.organizationTags },
|
|
782
|
+
...creds.dataPolicyAgreed === void 0 ? {} : { data_policy_agreed: creds.dataPolicyAgreed }
|
|
520
783
|
};
|
|
521
784
|
const infoB64 = aesEncryptCBCBase64(JSON.stringify(userInfo), aesKey);
|
|
522
785
|
const cosyKey = rsaEncryptBase64(aesKey);
|
|
@@ -532,9 +795,9 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
532
795
|
const sigPath = computeSigPath(requestURL);
|
|
533
796
|
const sigInput = `${payloadB64}\n${cosyKey}\n${timestamp}\n${body ? Buffer.isBuffer(body) ? body.toString("utf8") : body : ""}\n${sigPath}`;
|
|
534
797
|
const sig = crypto.createHash("md5").update(sigInput).digest("hex");
|
|
535
|
-
const bodyHash = crypto.createHash("md5").update(body || "").digest("hex");
|
|
536
|
-
const bodyLen = body ? (Buffer.isBuffer(body) ? body.length : Buffer.from(body).length).toString() : "0";
|
|
537
798
|
const machineID = creds.machineID || getMachineId();
|
|
799
|
+
const isModelRequest = sigPath === "/api/v2/service/pro/sse/agent_chat_generation";
|
|
800
|
+
const machineHostname = hostname().replace(/[^\x20-\x7e]/gu, "").trim();
|
|
538
801
|
return {
|
|
539
802
|
Authorization: `Bearer COSY.${payloadB64}.${sig}`,
|
|
540
803
|
"Cosy-Key": cosyKey,
|
|
@@ -544,17 +807,17 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
544
807
|
"Cosy-Machineid": machineID,
|
|
545
808
|
"Cosy-Machinetoken": machineID,
|
|
546
809
|
"Cosy-Machinetype": qoderMachineTypeMagic,
|
|
547
|
-
"Cosy-Machineos": qoderMachineOs,
|
|
810
|
+
"Cosy-Machineos": qoderMachineOs(),
|
|
811
|
+
...isModelRequest && machineHostname ? { "Cosy-MachineHostname": machineHostname } : {},
|
|
548
812
|
"Cosy-Clienttype": "5",
|
|
549
|
-
"Cosy-
|
|
550
|
-
"Cosy-
|
|
551
|
-
"Cosy-
|
|
552
|
-
"Cosy-
|
|
553
|
-
"Cosy-Data-Policy": qoderDataPolicy,
|
|
554
|
-
"Cosy-Organization-Id":
|
|
555
|
-
"Cosy-Organization-Tags": "",
|
|
556
|
-
"Login-Version": qoderLoginVersion
|
|
557
|
-
"X-Request-Id": crypto.randomUUID()
|
|
813
|
+
"Cosy-Business-Product": "cli",
|
|
814
|
+
"Cosy-Business-Type": "agent",
|
|
815
|
+
"Cosy-Scene": "assistant",
|
|
816
|
+
...isModelRequest ? {} : { "Cosy-Clientip": machineID },
|
|
817
|
+
"Cosy-Data-Policy": creds.dataPolicyAgreed === true ? "agree" : qoderDataPolicy,
|
|
818
|
+
...creds.organizationId ? { "Cosy-Organization-Id": creds.organizationId } : {},
|
|
819
|
+
...creds.organizationTags?.length ? { "Cosy-Organization-Tags": creds.organizationTags.join(",") } : {},
|
|
820
|
+
"Login-Version": qoderLoginVersion
|
|
558
821
|
};
|
|
559
822
|
}
|
|
560
823
|
const defaultResponseHeaderTimeoutMs = 6e4;
|
|
@@ -681,15 +944,12 @@ async function openApiJsonRequest(fetchImpl, options) {
|
|
|
681
944
|
const headers = {
|
|
682
945
|
accept: "application/json",
|
|
683
946
|
"user-agent": options.userAgent ?? defaultUserAgent,
|
|
684
|
-
"cosy-version":
|
|
947
|
+
"cosy-version": qoderIdeVersion,
|
|
948
|
+
"cosy-machineos": qoderMachineOs(),
|
|
685
949
|
"cosy-clienttype": "5",
|
|
686
950
|
...options.headers
|
|
687
951
|
};
|
|
688
952
|
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
689
|
-
if (options.machineId) {
|
|
690
|
-
headers["Cosy-MachineToken"] = options.machineId;
|
|
691
|
-
headers["Cosy-MachineType"] = "host";
|
|
692
|
-
}
|
|
693
953
|
let bodyText;
|
|
694
954
|
if (options.body !== void 0) {
|
|
695
955
|
headers["content-type"] = "application/json";
|
|
@@ -733,43 +993,83 @@ async function openApiJsonRequest(fetchImpl, options) {
|
|
|
733
993
|
const expiryBufferMs = 300 * 1e3;
|
|
734
994
|
const defaultExpiryMs = 1440 * 60 * 1e3;
|
|
735
995
|
const defaultAuthTimeoutMs = 15e3;
|
|
996
|
+
const defaultOrganizationTagsTimeoutMs = 3e3;
|
|
997
|
+
/** Match qodercli's seconds/milliseconds compatibility, preferring absolute expiry. */
|
|
998
|
+
function jobTokenExpiry(data, now = Date.now()) {
|
|
999
|
+
const absolute = data.expires_at ?? data.expiresAt ?? data.expire_time ?? data.expireTime;
|
|
1000
|
+
if (absolute !== void 0) {
|
|
1001
|
+
const parsed = typeof absolute === "string" ? Date.parse(absolute) : absolute > 0xe8d4a51000 ? absolute : absolute * 1e3;
|
|
1002
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
1003
|
+
}
|
|
1004
|
+
const relative = data.expires_in;
|
|
1005
|
+
if (typeof relative === "number" && Number.isFinite(relative) && relative > 0) return now + (relative > 86400 ? relative : relative * 1e3);
|
|
1006
|
+
return now + defaultExpiryMs;
|
|
1007
|
+
}
|
|
736
1008
|
function abortedError() {
|
|
737
1009
|
return new QoderLlmError("Qoder authentication was aborted.", "ABORTED");
|
|
738
1010
|
}
|
|
739
1011
|
async function waitForFlight(promise, signal) {
|
|
740
1012
|
if (signal === void 0) return promise;
|
|
741
|
-
if (signal.aborted) throw abortedError();
|
|
742
1013
|
return new Promise((resolve, reject) => {
|
|
743
1014
|
const onAbort = () => reject(abortedError());
|
|
744
1015
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
745
1016
|
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
1017
|
+
if (signal.aborted) onAbort();
|
|
746
1018
|
});
|
|
747
1019
|
}
|
|
748
|
-
var
|
|
1020
|
+
var QoderAuthManager = class {
|
|
749
1021
|
cache = /* @__PURE__ */ new Map();
|
|
750
1022
|
inFlight = /* @__PURE__ */ new Map();
|
|
1023
|
+
epochs = /* @__PURE__ */ new Map();
|
|
1024
|
+
nextVersion = 0;
|
|
751
1025
|
fetchImpl;
|
|
752
1026
|
timeoutMs;
|
|
1027
|
+
organizationTagsTimeoutMs;
|
|
753
1028
|
resolveMachineId;
|
|
754
1029
|
region;
|
|
755
1030
|
logger;
|
|
756
1031
|
constructor(options = {}) {
|
|
757
1032
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
758
1033
|
this.timeoutMs = options.timeoutMs ?? defaultAuthTimeoutMs;
|
|
759
|
-
this.
|
|
1034
|
+
this.organizationTagsTimeoutMs = options.organizationTagsTimeoutMs ?? defaultOrganizationTagsTimeoutMs;
|
|
760
1035
|
this.region = options.region ?? "global";
|
|
1036
|
+
this.resolveMachineId = options.resolveMachineId ?? (() => getMachineId(qoderMachineIdPaths(this.region)));
|
|
761
1037
|
this.logger = options.logger;
|
|
762
1038
|
}
|
|
763
1039
|
clear(pat) {
|
|
764
|
-
if (pat) this.
|
|
765
|
-
else this.cache.
|
|
1040
|
+
if (pat) this.invalidate(`${this.region}:${opaqueCredentialKey(pat)}`);
|
|
1041
|
+
else for (const key of /* @__PURE__ */ new Set([...this.cache.keys(), ...this.inFlight.keys()])) this.invalidate(key);
|
|
1042
|
+
}
|
|
1043
|
+
invalidate(key) {
|
|
1044
|
+
this.epochs.set(key, (this.epochs.get(key) ?? 0) + 1);
|
|
1045
|
+
this.cache.delete(key);
|
|
1046
|
+
const flight = this.inFlight.get(key);
|
|
1047
|
+
if (flight) {
|
|
1048
|
+
this.inFlight.delete(key);
|
|
1049
|
+
flight.controller.abort("credential invalidated");
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/** Recover a rejected credential once; concurrent rejections share the exchange. */
|
|
1053
|
+
async refreshCredentials(pat, rejected, signal) {
|
|
1054
|
+
if (signal?.aborted) throw abortedError();
|
|
1055
|
+
const key = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
1056
|
+
const current = this.cache.get(key)?.creds;
|
|
1057
|
+
if (current && current !== rejected) return current;
|
|
1058
|
+
this.cache.delete(key);
|
|
1059
|
+
if (this.inFlight.get(key)) return this.getCredentials(pat, signal);
|
|
1060
|
+
this.invalidate(key);
|
|
1061
|
+
return this.getCredentials(pat, signal);
|
|
766
1062
|
}
|
|
767
1063
|
async getCredentials(pat, signal) {
|
|
768
1064
|
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");
|
|
769
1065
|
if (signal?.aborted) throw abortedError();
|
|
770
1066
|
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
771
1067
|
const cached = this.cache.get(cacheKey);
|
|
772
|
-
if (cached
|
|
1068
|
+
if (cached) {
|
|
1069
|
+
const lifetime = cached.expiresAt - cached.issuedAt;
|
|
1070
|
+
const buffer = Math.min(expiryBufferMs, Math.max(0, lifetime / 5));
|
|
1071
|
+
if (cached.expiresAt > Date.now() + buffer) return cached.creds;
|
|
1072
|
+
}
|
|
773
1073
|
let entry = this.inFlight.get(cacheKey);
|
|
774
1074
|
if (entry === void 0 || entry.controller.signal.aborted) {
|
|
775
1075
|
const controller = new AbortController();
|
|
@@ -778,7 +1078,12 @@ var QoderAuthService = class {
|
|
|
778
1078
|
created.waiters = 0;
|
|
779
1079
|
created.settled = false;
|
|
780
1080
|
created.timeout = setTimeout(() => controller.abort("authentication timeout"), this.timeoutMs);
|
|
781
|
-
|
|
1081
|
+
const epoch = this.epochs.get(cacheKey) ?? 0;
|
|
1082
|
+
created.promise = this.exchangeAndResolve(pat, controller.signal, () => clearTimeout(created.timeout), epoch).catch((error) => {
|
|
1083
|
+
const stale = this.cache.get(cacheKey);
|
|
1084
|
+
if (error instanceof QoderLlmError && (error.code === "TRANSPORT" || error.code === "TIMEOUT" || error.code === "SERVER") && stale && stale.expiresAt > Date.now() && (this.epochs.get(cacheKey) ?? 0) === epoch && !controller.signal.aborted) return stale.creds;
|
|
1085
|
+
throw error;
|
|
1086
|
+
}).finally(() => {
|
|
782
1087
|
created.settled = true;
|
|
783
1088
|
clearTimeout(created.timeout);
|
|
784
1089
|
if (this.inFlight.get(cacheKey) === created) this.inFlight.delete(cacheKey);
|
|
@@ -797,37 +1102,61 @@ var QoderAuthService = class {
|
|
|
797
1102
|
}
|
|
798
1103
|
}
|
|
799
1104
|
}
|
|
800
|
-
async exchangeAndResolve(pat, signal) {
|
|
801
|
-
|
|
802
|
-
let expiresAt = Date.now() + defaultExpiryMs;
|
|
1105
|
+
async exchangeAndResolve(pat, signal, onAuthenticated, epoch) {
|
|
1106
|
+
const machineID = this.resolveMachineId().trim();
|
|
803
1107
|
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
804
1108
|
url: getQoderExchangeUrl(this.region),
|
|
805
|
-
body: {
|
|
1109
|
+
body: {
|
|
1110
|
+
personal_token: pat,
|
|
1111
|
+
...machineID && machineID !== "unknown" ? { machine_id: machineID } : {}
|
|
1112
|
+
},
|
|
806
1113
|
signal,
|
|
807
1114
|
timeoutMs: this.timeoutMs,
|
|
808
1115
|
logger: this.logger,
|
|
809
1116
|
operation: "Auth",
|
|
810
1117
|
logCategory: "auth.exchange"
|
|
811
1118
|
});
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1119
|
+
const jobToken = [
|
|
1120
|
+
data.token,
|
|
1121
|
+
data.device_token,
|
|
1122
|
+
data.access_token
|
|
1123
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
1124
|
+
if (!jobToken) throw new QoderLlmError("Qoder PAT exchange returned no job token.", "AUTH");
|
|
1125
|
+
const expiresAt = jobTokenExpiry(data);
|
|
1126
|
+
const creds = {
|
|
1127
|
+
...await this.credentialsForToken(jobToken, machineID, signal, onAuthenticated),
|
|
1128
|
+
expiresAt,
|
|
1129
|
+
version: ++this.nextVersion
|
|
1130
|
+
};
|
|
1131
|
+
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
1132
|
+
if ((this.epochs.get(cacheKey) ?? 0) !== epoch || signal.aborted) throw abortedError();
|
|
1133
|
+
this.cache.set(cacheKey, {
|
|
1134
|
+
creds,
|
|
1135
|
+
expiresAt,
|
|
1136
|
+
issuedAt: Date.now()
|
|
1137
|
+
});
|
|
1138
|
+
return creds;
|
|
1139
|
+
}
|
|
1140
|
+
/** Both PAT exchange and browser authorization use the same verified subscriber identity. */
|
|
1141
|
+
async credentialsForToken(jobToken, machineID, signal, onAuthenticated = () => {}) {
|
|
818
1142
|
const userInfo = await retryMetadataRead(signal, () => this.fetchUserInfo(jobToken, signal));
|
|
1143
|
+
if (signal.aborted) throw abortedError();
|
|
1144
|
+
onAuthenticated();
|
|
819
1145
|
const creds = {
|
|
820
1146
|
userID: userInfo.userID,
|
|
821
1147
|
authToken: jobToken,
|
|
822
1148
|
name: userInfo.name || "Qoder User",
|
|
823
1149
|
email: userInfo.email,
|
|
824
|
-
machineID
|
|
1150
|
+
machineID,
|
|
1151
|
+
...userInfo.organizationId === void 0 ? {} : { organizationId: userInfo.organizationId },
|
|
1152
|
+
...userInfo.organizationTags === void 0 ? {} : { organizationTags: userInfo.organizationTags },
|
|
1153
|
+
...userInfo.dataPolicyAgreed === void 0 ? {} : { dataPolicyAgreed: userInfo.dataPolicyAgreed }
|
|
825
1154
|
};
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
creds
|
|
829
|
-
|
|
830
|
-
|
|
1155
|
+
if (creds.organizationId && creds.organizationTags === void 0) {
|
|
1156
|
+
const tags = await this.fetchOrganizationTags(jobToken, creds.organizationId, signal);
|
|
1157
|
+
if (tags !== void 0) creds.organizationTags = tags;
|
|
1158
|
+
}
|
|
1159
|
+
if (signal.aborted) throw abortedError();
|
|
831
1160
|
return creds;
|
|
832
1161
|
}
|
|
833
1162
|
async fetchUserInfo(jobToken, signal) {
|
|
@@ -840,13 +1169,49 @@ var QoderAuthService = class {
|
|
|
840
1169
|
operation: "UserInfo",
|
|
841
1170
|
logCategory: "auth.user-info"
|
|
842
1171
|
});
|
|
843
|
-
|
|
1172
|
+
const userID = [
|
|
1173
|
+
info.id,
|
|
1174
|
+
info.user_id,
|
|
1175
|
+
info.uid
|
|
1176
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
1177
|
+
const organizationId = [
|
|
1178
|
+
info.orgId,
|
|
1179
|
+
info.organization_id,
|
|
1180
|
+
info.organizationId,
|
|
1181
|
+
info.organization?.org_id,
|
|
1182
|
+
info.organization?.orgId,
|
|
1183
|
+
info.organization?.id
|
|
1184
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
1185
|
+
if (!userID) throw new QoderLlmError("Qoder identity lookup returned no user id.", "AUTH");
|
|
1186
|
+
const organizationTags = Array.isArray(info.organization_tags) ? info.organization_tags.filter((tag) => typeof tag === "string") : void 0;
|
|
844
1187
|
return {
|
|
845
|
-
userID
|
|
1188
|
+
userID,
|
|
846
1189
|
email: info.email ?? "",
|
|
847
|
-
name: info.name ?? info.username ?? ""
|
|
1190
|
+
name: info.name ?? info.username ?? "",
|
|
1191
|
+
...organizationId ? { organizationId } : {},
|
|
1192
|
+
...organizationTags === void 0 ? {} : { organizationTags },
|
|
1193
|
+
...typeof info.data_policy_agreed === "boolean" ? { dataPolicyAgreed: info.data_policy_agreed } : {}
|
|
848
1194
|
};
|
|
849
1195
|
}
|
|
1196
|
+
async fetchOrganizationTags(jobToken, organizationId, signal) {
|
|
1197
|
+
const timeout = AbortSignal.timeout(this.organizationTagsTimeoutMs);
|
|
1198
|
+
const lookupSignal = AbortSignal.any([signal, timeout]);
|
|
1199
|
+
try {
|
|
1200
|
+
const { tags } = await retryMetadataRead(lookupSignal, () => openApiJsonRequest(this.fetchImpl, {
|
|
1201
|
+
url: `${resolveQoderEndpoints(this.region).openApiUrl}/api/v1/organizations/${encodeURIComponent(organizationId)}/tags`,
|
|
1202
|
+
token: jobToken,
|
|
1203
|
+
signal: lookupSignal,
|
|
1204
|
+
timeoutMs: this.organizationTagsTimeoutMs,
|
|
1205
|
+
logger: this.logger,
|
|
1206
|
+
operation: "OrganizationTags"
|
|
1207
|
+
}));
|
|
1208
|
+
return Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [];
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
if (signal.aborted) throw error;
|
|
1211
|
+
this.logger?.warn?.("[Qoder Auth] Organization tags unavailable; retaining subscriber organization.");
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
850
1215
|
};
|
|
851
1216
|
//#endregion
|
|
852
1217
|
//#region src/qoder/transport/catalog-reader.ts
|
|
@@ -900,14 +1265,19 @@ async function fetchQoderModels(credentials, options = {}) {
|
|
|
900
1265
|
function unsupported(message) {
|
|
901
1266
|
return new QoderLlmError(message, "UNSUPPORTED_CONTENT");
|
|
902
1267
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
return text;
|
|
1268
|
+
/** Keep multimodal tool results attached to their call. */
|
|
1269
|
+
async function translateToolResultContent(blocks, context) {
|
|
1270
|
+
const supportedBlocks = blocks.map((block) => {
|
|
1271
|
+
if (block.type === "text" || block.type === "image") return block;
|
|
1272
|
+
throw unsupported(`Qoder tool results support text and images only; received ${String(block.type)} content.`);
|
|
1273
|
+
});
|
|
1274
|
+
const images = supportedBlocks.filter((block) => block.type === "image");
|
|
1275
|
+
if (images.length === 0) return { content: supportedBlocks.map((block) => block.type === "text" ? block.text : "").join("") };
|
|
1276
|
+
if (context.attachments !== void 0) enforceImageLimits(images, context.attachments);
|
|
1277
|
+
return { content: await Promise.all(supportedBlocks.map((block) => block.type === "image" ? resolveImagePart(block, context) : {
|
|
1278
|
+
type: "text",
|
|
1279
|
+
text: block.text
|
|
1280
|
+
})) };
|
|
911
1281
|
}
|
|
912
1282
|
/** Reject a batch that exceeds the deployment image policy before any upload work starts. */
|
|
913
1283
|
function enforceImageLimits(images, attachments) {
|
|
@@ -1000,23 +1370,21 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1000
1370
|
output.push({
|
|
1001
1371
|
role: "tool",
|
|
1002
1372
|
tool_call_id: callId,
|
|
1003
|
-
|
|
1373
|
+
...await translateToolResultContent(result.content, context)
|
|
1004
1374
|
});
|
|
1005
|
-
const images = result.content.filter((block) => block.type === "image");
|
|
1006
|
-
if (images.length > 0) {
|
|
1007
|
-
if (attachments !== void 0) enforceImageLimits(images, attachments);
|
|
1008
|
-
output.push({
|
|
1009
|
-
role: "user",
|
|
1010
|
-
content: [{
|
|
1011
|
-
type: "text",
|
|
1012
|
-
text: `[${images.length} image${images.length === 1 ? "" : "s"} returned by the previous tool call]`
|
|
1013
|
-
}, ...await Promise.all(images.map((image) => resolveImagePart(image, context)))]
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
1375
|
}
|
|
1017
1376
|
if (nonToolBlocks.length === 0) continue;
|
|
1018
1377
|
}
|
|
1019
1378
|
const contentBlocks = toolResults.length > 0 ? nonToolBlocks : message.content;
|
|
1379
|
+
if (message.role === "tool") {
|
|
1380
|
+
const callId = consumeToolCallId(message, void 0, lastAssistantToolCallIds, output.length);
|
|
1381
|
+
output.push({
|
|
1382
|
+
role: "tool",
|
|
1383
|
+
tool_call_id: callId,
|
|
1384
|
+
...await translateToolResultContent(contentBlocks, context)
|
|
1385
|
+
});
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1020
1388
|
let text = "";
|
|
1021
1389
|
let reasoningText = "";
|
|
1022
1390
|
const userContent = [];
|
|
@@ -1024,7 +1392,6 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1024
1392
|
let hasImage = false;
|
|
1025
1393
|
const toolCalls = [];
|
|
1026
1394
|
for (const block of contentBlocks) {
|
|
1027
|
-
if (message.role === "tool" && block.type !== "text" && block.type !== "image") throw unsupported(`Qoder tool results support text and images only; received ${block.type} content.`);
|
|
1028
1395
|
if (block.type === "text") {
|
|
1029
1396
|
text += block.text;
|
|
1030
1397
|
if (message.role === "user" || toolResults.length > 0) userContent.push({
|
|
@@ -1034,7 +1401,7 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1034
1401
|
continue;
|
|
1035
1402
|
}
|
|
1036
1403
|
if (block.type === "image") {
|
|
1037
|
-
if (message.role !== "user" &&
|
|
1404
|
+
if (message.role !== "user" && toolResults.length === 0) throw unsupported("Qoder image content is valid only in user messages.");
|
|
1038
1405
|
hasImage = true;
|
|
1039
1406
|
pendingImages.push({
|
|
1040
1407
|
slot: userContent.length,
|
|
@@ -1071,25 +1438,6 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1071
1438
|
});
|
|
1072
1439
|
continue;
|
|
1073
1440
|
}
|
|
1074
|
-
if (message.role === "tool") {
|
|
1075
|
-
const callId = consumeToolCallId(message, void 0, lastAssistantToolCallIds, output.length);
|
|
1076
|
-
output.push({
|
|
1077
|
-
role: "tool",
|
|
1078
|
-
tool_call_id: callId,
|
|
1079
|
-
content: text
|
|
1080
|
-
});
|
|
1081
|
-
if (pendingImages.length > 0) {
|
|
1082
|
-
if (attachments !== void 0) enforceImageLimits(pendingImages.map((pending) => pending.block), attachments);
|
|
1083
|
-
output.push({
|
|
1084
|
-
role: "user",
|
|
1085
|
-
content: [{
|
|
1086
|
-
type: "text",
|
|
1087
|
-
text: `[${pendingImages.length} image${pendingImages.length === 1 ? "" : "s"} returned by the previous tool call]`
|
|
1088
|
-
}, ...await Promise.all(pendingImages.map((pending) => resolveImagePart(pending.block, context)))]
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
continue;
|
|
1092
|
-
}
|
|
1093
1441
|
if (pendingImages.length > 0) {
|
|
1094
1442
|
if (attachments !== void 0) enforceImageLimits(pendingImages.map((pending) => pending.block), attachments);
|
|
1095
1443
|
await Promise.all(pendingImages.map(async (pending) => {
|
|
@@ -1167,10 +1515,10 @@ const injectedContextOpenings = [
|
|
|
1167
1515
|
*/
|
|
1168
1516
|
const hostNotificationTemplate = /^(?:Agent|Background subagent) \S{3,} (?:sent a message:|finished\b)/;
|
|
1169
1517
|
/**
|
|
1170
|
-
*
|
|
1518
|
+
* Compatibility with image markers synthesized by older transport versions.
|
|
1171
1519
|
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1520
|
+
* Current tool images stay in tool-role messages. Historical user-role markers
|
|
1521
|
+
* still belong to the open record rather than to new subscriber input.
|
|
1174
1522
|
*/
|
|
1175
1523
|
const syntheticMarkerTemplate = /^\[\d+ images? returned by the previous tool call\]/;
|
|
1176
1524
|
/** Wire text of one user message, used for injection classification and text extraction. */
|
|
@@ -1245,7 +1593,8 @@ var QoderTurnTracker = class {
|
|
|
1245
1593
|
requestSetId: `qoder-request-${crypto.randomUUID()}`,
|
|
1246
1594
|
chatRecordId: requestId,
|
|
1247
1595
|
businessId: crypto.randomUUID(),
|
|
1248
|
-
beginAt: now
|
|
1596
|
+
beginAt: now,
|
|
1597
|
+
businessStage: "start"
|
|
1249
1598
|
};
|
|
1250
1599
|
if (kind === "auxiliary") {
|
|
1251
1600
|
if (this.sessions.has(sessionId)) this.sessions.get(sessionId).lastTouchedAt = now;
|
|
@@ -1253,7 +1602,8 @@ var QoderTurnTracker = class {
|
|
|
1253
1602
|
requestSetId: `qoder-request-${crypto.randomUUID()}`,
|
|
1254
1603
|
chatRecordId: requestId,
|
|
1255
1604
|
businessId: crypto.randomUUID(),
|
|
1256
|
-
beginAt: now
|
|
1605
|
+
beginAt: now,
|
|
1606
|
+
businessStage: "start"
|
|
1257
1607
|
};
|
|
1258
1608
|
}
|
|
1259
1609
|
let state = this.sessions.get(sessionId);
|
|
@@ -1301,12 +1651,15 @@ var QoderTurnTracker = class {
|
|
|
1301
1651
|
if (current === void 0) state.claimed.delete(fingerprint);
|
|
1302
1652
|
else if (current.count < claimed) state.claimed.set(fingerprint, current.count);
|
|
1303
1653
|
}
|
|
1654
|
+
let businessStage = "processing";
|
|
1304
1655
|
if (opening !== void 0 && unclaimed > 0 || modelChanged) {
|
|
1656
|
+
businessStage = "start";
|
|
1305
1657
|
state.turnCount += 1;
|
|
1306
1658
|
state.currentTurnId = turnIdFor(sessionId, state.turnCount, opening ?? state.currentModel ?? "");
|
|
1307
1659
|
state.currentBusinessId = crypto.randomUUID();
|
|
1308
1660
|
state.turnOpenedAt = now;
|
|
1309
1661
|
} else if (state.currentTurnId === "") {
|
|
1662
|
+
businessStage = "start";
|
|
1310
1663
|
state.turnCount += 1;
|
|
1311
1664
|
state.currentTurnId = turnIdFor(sessionId, state.turnCount, state.currentModel ?? "");
|
|
1312
1665
|
state.currentBusinessId = crypto.randomUUID();
|
|
@@ -1316,7 +1669,8 @@ var QoderTurnTracker = class {
|
|
|
1316
1669
|
requestSetId: state.currentTurnId,
|
|
1317
1670
|
chatRecordId: requestId,
|
|
1318
1671
|
businessId: state.currentBusinessId,
|
|
1319
|
-
beginAt: state.turnOpenedAt
|
|
1672
|
+
beginAt: state.turnOpenedAt,
|
|
1673
|
+
businessStage
|
|
1320
1674
|
};
|
|
1321
1675
|
}
|
|
1322
1676
|
/**
|
|
@@ -1380,8 +1734,8 @@ var QoderTurnTracker = class {
|
|
|
1380
1734
|
}
|
|
1381
1735
|
};
|
|
1382
1736
|
//#endregion
|
|
1383
|
-
//#region src/qoder/transport/wire/
|
|
1384
|
-
/**
|
|
1737
|
+
//#region src/qoder/transport/wire/prepare.ts
|
|
1738
|
+
/** Shared model request preparation, before selecting a wire protocol. */
|
|
1385
1739
|
function stableHash(prefix, ...inputs) {
|
|
1386
1740
|
const hash = crypto.createHash("sha256");
|
|
1387
1741
|
hash.update(prefix);
|
|
@@ -1391,33 +1745,11 @@ function stableHash(prefix, ...inputs) {
|
|
|
1391
1745
|
}
|
|
1392
1746
|
return hash.digest("hex").slice(0, 16);
|
|
1393
1747
|
}
|
|
1394
|
-
/**
|
|
1395
|
-
* Turn identity shared by every request the process serves.
|
|
1396
|
-
*
|
|
1397
|
-
* Qoder's Credits panel aggregates consumption per agent run — the
|
|
1398
|
-
* `business.id` a request reports — so every step of one turn must carry the
|
|
1399
|
-
* same run identity while the next subscriber prompt opens a new one. The
|
|
1400
|
-
* tracker owns that boundary.
|
|
1401
|
-
*/
|
|
1748
|
+
/** One tracker preserves agent-run identity across both model protocols. */
|
|
1402
1749
|
const qoderTurnTracker = new QoderTurnTracker();
|
|
1403
|
-
/**
|
|
1404
|
-
* Whether this request serves the subscriber's turn or the host's bookkeeping.
|
|
1405
|
-
*
|
|
1406
|
-
* The host marks an auxiliary call through `GenerateOptions.purpose`, and those
|
|
1407
|
-
* calls carry a message list of their own under the same session identity. They
|
|
1408
|
-
* must never move the open turn's boundary, or one subscriber prompt would
|
|
1409
|
-
* report two consumption records.
|
|
1410
|
-
*/
|
|
1411
1750
|
function callKind(options) {
|
|
1412
1751
|
return options.purpose === void 0 ? "conversation" : "auxiliary";
|
|
1413
1752
|
}
|
|
1414
|
-
/**
|
|
1415
|
-
* Reject an unusable request before any credential resolution or provider I/O.
|
|
1416
|
-
*
|
|
1417
|
-
* Image publication needs credentials, so message translation now runs after
|
|
1418
|
-
* authentication. This static pass preserves the guarantee that a request the
|
|
1419
|
-
* provider cannot serve never reaches the network.
|
|
1420
|
-
*/
|
|
1421
1753
|
function validateQoderRequestShape(options, model) {
|
|
1422
1754
|
if (options.reasoningEffort !== void 0) {
|
|
1423
1755
|
const effort = String(options.reasoningEffort);
|
|
@@ -1425,29 +1757,24 @@ function validateQoderRequestShape(options, model) {
|
|
|
1425
1757
|
}
|
|
1426
1758
|
if (options.messages.some((message) => contentHasImage(message.content)) && model?.supportsImages !== true) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise image input.`, "UNSUPPORTED_CONTENT");
|
|
1427
1759
|
}
|
|
1428
|
-
/** Translate a request whose shape has already been validated. */
|
|
1429
1760
|
function translateQoderMessages(options, attachments, pipeline) {
|
|
1430
1761
|
return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal, pipeline);
|
|
1431
1762
|
}
|
|
1432
|
-
async function
|
|
1433
|
-
validateQoderRequestShape(options, model);
|
|
1434
|
-
return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal);
|
|
1435
|
-
}
|
|
1436
|
-
async function buildQoderRequestBody(options, userId, translatedMessages, model, attachments, tracker) {
|
|
1763
|
+
async function prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker = qoderTurnTracker, identityScope = "") {
|
|
1437
1764
|
if (!userId) throw new QoderLlmError("Qoder request identity is missing.", "AUTH");
|
|
1765
|
+
if (translatedMessages === void 0) validateQoderRequestShape(options, model);
|
|
1766
|
+
const messages = translatedMessages ?? await translateQoderMessages(options, attachments);
|
|
1438
1767
|
const modelKey = options.model || "cmodel";
|
|
1439
|
-
const messages = translatedMessages ?? await validateQoderRequest(options, model, attachments);
|
|
1440
1768
|
const modelMaxTokens = model?.maxTokens ?? 32768;
|
|
1441
1769
|
const maxTokens = Math.min(options.maxTokens ?? modelMaxTokens, modelMaxTokens);
|
|
1442
1770
|
const isReasoning = options.reasoningEffort !== void 0 || (model?.isReasoning ?? false);
|
|
1443
|
-
const tools = translateTools(options.tools);
|
|
1444
1771
|
const selectedTier = model === void 0 ? void 0 : selectedContextTier(model);
|
|
1445
1772
|
const defaultContexts = Object.values(model?.contextOptions ?? {}).filter((option) => option.isDefault === true && typeof option.tokenCount === "number" && Number.isFinite(option.tokenCount) && option.tokenCount > 0);
|
|
1446
1773
|
const contextConfig = model?.contextOptions === void 0 || selectedTier === void 0 && defaultContexts.length !== 1 ? void 0 : Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, {
|
|
1447
1774
|
...value.tokenCount === void 0 ? {} : { token_count: value.tokenCount },
|
|
1448
1775
|
...selectedTier === void 0 ? value.isDefault === void 0 ? {} : { is_default: value.isDefault } : { is_default: key === selectedTier.key }
|
|
1449
1776
|
}]));
|
|
1450
|
-
const
|
|
1777
|
+
const contextLength = selectedTier?.tokenCount ?? model?.contextWindow;
|
|
1451
1778
|
let lastUserText = "";
|
|
1452
1779
|
for (let index = messages.length - 1; index >= 0; index--) if (messages[index].role === "user") {
|
|
1453
1780
|
lastUserText = wireMessageText(messages[index]);
|
|
@@ -1457,59 +1784,32 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
|
|
|
1457
1784
|
const sessionId = options.sessionId === void 0 ? `${stablePart}-${crypto.randomUUID()}` : `${stablePart}-${String(options.sessionId)}`;
|
|
1458
1785
|
const requestId = crypto.randomUUID();
|
|
1459
1786
|
const sessionIdParam = options.sessionId === void 0 ? void 0 : String(options.sessionId);
|
|
1460
|
-
const identity =
|
|
1787
|
+
const identity = tracker.resolveTurnIdentity(sessionIdParam === void 0 ? void 0 : JSON.stringify([
|
|
1788
|
+
identityScope,
|
|
1789
|
+
userId,
|
|
1790
|
+
sessionIdParam
|
|
1791
|
+
]), messages, callKind(options), requestId, modelKey);
|
|
1461
1792
|
return {
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
is_retry: false,
|
|
1470
|
-
source: 1,
|
|
1471
|
-
version: "3",
|
|
1472
|
-
session_type: "qodercli",
|
|
1473
|
-
agent_id: "agent_common",
|
|
1474
|
-
task_id: "common",
|
|
1475
|
-
code_language: "",
|
|
1476
|
-
chat_prompt: "",
|
|
1477
|
-
image_urls: null,
|
|
1478
|
-
aliyun_user_type: "",
|
|
1479
|
-
system: "",
|
|
1793
|
+
requestId,
|
|
1794
|
+
requestSetId: identity.requestSetId,
|
|
1795
|
+
chatRecordId: identity.chatRecordId,
|
|
1796
|
+
sessionId,
|
|
1797
|
+
taskId: "common",
|
|
1798
|
+
modelKey,
|
|
1799
|
+
modelSource: model?.source || "system",
|
|
1480
1800
|
messages,
|
|
1481
|
-
tools,
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
},
|
|
1487
|
-
|
|
1488
|
-
chatPrompt: "",
|
|
1489
|
-
imageUrls: null,
|
|
1490
|
-
extra: {
|
|
1491
|
-
context: [],
|
|
1492
|
-
modelConfig: {
|
|
1493
|
-
key: modelKey,
|
|
1494
|
-
is_reasoning: isReasoning
|
|
1495
|
-
},
|
|
1496
|
-
originalContent: lastUserText
|
|
1497
|
-
},
|
|
1498
|
-
features: [],
|
|
1499
|
-
text: lastUserText
|
|
1500
|
-
},
|
|
1501
|
-
model_config: {
|
|
1502
|
-
key: modelKey,
|
|
1503
|
-
is_reasoning: isReasoning,
|
|
1504
|
-
max_output_tokens: maxTokens,
|
|
1505
|
-
source: model?.source || "system",
|
|
1506
|
-
...contextConfig === void 0 ? {} : { context_config: contextConfig }
|
|
1507
|
-
},
|
|
1801
|
+
tools: translateTools(options.tools),
|
|
1802
|
+
maxTokens,
|
|
1803
|
+
...contextLength === void 0 ? {} : { contextLength },
|
|
1804
|
+
...options.reasoningEffort === void 0 ? {} : { reasoningEffort: String(options.reasoningEffort) },
|
|
1805
|
+
isReasoning,
|
|
1806
|
+
...contextConfig === void 0 ? {} : { contextConfig },
|
|
1807
|
+
lastUserText,
|
|
1508
1808
|
business: {
|
|
1509
1809
|
product: "cli",
|
|
1510
|
-
version:
|
|
1810
|
+
version: qoderIdeVersion,
|
|
1511
1811
|
type: "agent",
|
|
1512
|
-
stage:
|
|
1812
|
+
stage: identity.businessStage,
|
|
1513
1813
|
id: identity.businessId,
|
|
1514
1814
|
name: lastUserText.substring(0, 30),
|
|
1515
1815
|
begin_at: identity.beginAt
|
|
@@ -1716,7 +2016,7 @@ var QoderImageUploader = class {
|
|
|
1716
2016
|
reason: first.reason
|
|
1717
2017
|
});
|
|
1718
2018
|
try {
|
|
1719
|
-
refreshed = await this.refreshCredentials(signal);
|
|
2019
|
+
refreshed = await this.refreshCredentials(credentials, signal);
|
|
1720
2020
|
} catch (error) {
|
|
1721
2021
|
if (signal.aborted) throw aborted$2();
|
|
1722
2022
|
this.warnDegraded("credential refresh failed", error);
|
|
@@ -2027,15 +2327,18 @@ var QoderUsageReader = class {
|
|
|
2027
2327
|
}
|
|
2028
2328
|
async readAccount(pat, options) {
|
|
2029
2329
|
if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid.", "MISSING_CREDENTIAL");
|
|
2030
|
-
|
|
2330
|
+
return this.readWithCredentials(opaqueCredentialKey(pat), (signal) => this.authService.getCredentials(pat, signal), options);
|
|
2331
|
+
}
|
|
2332
|
+
async readWithCredentials(key, resolve, options) {
|
|
2333
|
+
const cacheKey = `${this.region}:${key}`;
|
|
2031
2334
|
if (!options?.force) {
|
|
2032
2335
|
const cached = this.cache.get(cacheKey);
|
|
2033
2336
|
if (cached && cached.expiresAt > Date.now()) return cached.info;
|
|
2034
2337
|
}
|
|
2035
|
-
return this.flights.run(cacheKey, options?.signal, (sharedSignal) => this.loadAccount(
|
|
2338
|
+
return this.flights.run(cacheKey, options?.signal, (sharedSignal) => this.loadAccount(resolve, sharedSignal, cacheKey), () => new QoderLlmError("Qoder account request was aborted.", "ABORTED"));
|
|
2036
2339
|
}
|
|
2037
|
-
async loadAccount(
|
|
2038
|
-
const creds = await
|
|
2340
|
+
async loadAccount(resolve, signal, cacheKey) {
|
|
2341
|
+
const creds = await resolve(signal);
|
|
2039
2342
|
const profile = {
|
|
2040
2343
|
id: creds.userID,
|
|
2041
2344
|
name: creds.name || "Qoder User",
|
|
@@ -2044,7 +2347,7 @@ var QoderUsageReader = class {
|
|
|
2044
2347
|
const [usage, plan, status] = await Promise.all([
|
|
2045
2348
|
retryMetadataRead(signal, () => this.fetchUsage(creds.authToken, signal)),
|
|
2046
2349
|
this.safeFetchPlan(creds.authToken, signal),
|
|
2047
|
-
this.safeFetchStatus(creds.authToken,
|
|
2350
|
+
this.safeFetchStatus(creds.authToken, signal)
|
|
2048
2351
|
]);
|
|
2049
2352
|
const accountInfo = {
|
|
2050
2353
|
profile,
|
|
@@ -2070,6 +2373,11 @@ var QoderUsageReader = class {
|
|
|
2070
2373
|
* `dedicatedResourcePackages`: those entries carry the campaign identifiers
|
|
2071
2374
|
* that the normalized list deliberately discards, and this payload crosses
|
|
2072
2375
|
* the seam into the browser.
|
|
2376
|
+
*
|
|
2377
|
+
* `addOnQuota` is the pool a plan holds separately from its own subscription
|
|
2378
|
+
* quota. A plan whose subscription quota is zero — Free in particular —
|
|
2379
|
+
* reports its whole balance there, so dropping the bucket left the card with
|
|
2380
|
+
* nothing to render.
|
|
2073
2381
|
*/
|
|
2074
2382
|
async fetchUsage(jobToken, signal) {
|
|
2075
2383
|
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
@@ -2087,6 +2395,7 @@ var QoderUsageReader = class {
|
|
|
2087
2395
|
return {
|
|
2088
2396
|
userQuota: normalizeQuota(data.userQuota),
|
|
2089
2397
|
orgResourcePackage: normalizeQuota(data.orgResourcePackage),
|
|
2398
|
+
addOnQuota: normalizeQuota(data.add_on_quota ?? data.addOnQuota),
|
|
2090
2399
|
...dedicatedResourcePackages !== void 0 ? { dedicatedResourcePackages } : {},
|
|
2091
2400
|
totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
|
|
2092
2401
|
isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
|
|
@@ -2114,11 +2423,10 @@ var QoderUsageReader = class {
|
|
|
2114
2423
|
return;
|
|
2115
2424
|
}
|
|
2116
2425
|
}
|
|
2117
|
-
async fetchStatus(jobToken,
|
|
2426
|
+
async fetchStatus(jobToken, signal) {
|
|
2118
2427
|
return normalizeStatus(await openApiJsonRequest(this.fetchImpl, {
|
|
2119
2428
|
url: getQoderUserStatusUrl(this.region),
|
|
2120
2429
|
token: jobToken,
|
|
2121
|
-
machineId,
|
|
2122
2430
|
signal,
|
|
2123
2431
|
timeoutMs: this.timeoutMs,
|
|
2124
2432
|
logger: this.logger,
|
|
@@ -2126,9 +2434,9 @@ var QoderUsageReader = class {
|
|
|
2126
2434
|
logCategory: "account.status"
|
|
2127
2435
|
}));
|
|
2128
2436
|
}
|
|
2129
|
-
async safeFetchStatus(jobToken,
|
|
2437
|
+
async safeFetchStatus(jobToken, signal) {
|
|
2130
2438
|
try {
|
|
2131
|
-
return await this.fetchStatus(jobToken,
|
|
2439
|
+
return await this.fetchStatus(jobToken, signal);
|
|
2132
2440
|
} catch (error) {
|
|
2133
2441
|
if (signal?.aborted) throw error;
|
|
2134
2442
|
this.logger?.warn?.("[Qoder Status] Failed to load user status (degraded)", error instanceof Error ? error.message : error);
|
|
@@ -2148,23 +2456,27 @@ var QoderUsageReader = class {
|
|
|
2148
2456
|
*/
|
|
2149
2457
|
const qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
|
|
2150
2458
|
const qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
2151
|
-
|
|
2459
|
+
const qoderEncodeTable = /* @__PURE__ */ new Uint8Array(128);
|
|
2460
|
+
for (let i = 0; i < 64; i++) qoderEncodeTable[qoderStdAlphabet.charCodeAt(i)] = qoderCustomAlphabet.charCodeAt(i);
|
|
2461
|
+
qoderEncodeTable["=".charCodeAt(0)] = "$".charCodeAt(0);
|
|
2462
|
+
/** Encode directly into wire bytes without building per-character string ropes. */
|
|
2463
|
+
function qoderEncodeBodyBytes(plaintext) {
|
|
2152
2464
|
const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
|
|
2153
2465
|
const n = std.length;
|
|
2154
2466
|
const a = Math.floor(n / 3);
|
|
2155
|
-
const
|
|
2156
|
-
let
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
else out += c;
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2467
|
+
const out = Buffer.allocUnsafe(n);
|
|
2468
|
+
let offset = 0;
|
|
2469
|
+
const writeRange = (start, end) => {
|
|
2470
|
+
for (let i = start; i < end; i++) out[offset++] = qoderEncodeTable[std.charCodeAt(i)];
|
|
2471
|
+
};
|
|
2472
|
+
writeRange(n - a, n);
|
|
2473
|
+
writeRange(a, n - a);
|
|
2474
|
+
writeRange(0, a);
|
|
2166
2475
|
return out;
|
|
2167
2476
|
}
|
|
2477
|
+
function qoderEncodeBody(plaintext) {
|
|
2478
|
+
return qoderEncodeBodyBytes(plaintext).toString("latin1");
|
|
2479
|
+
}
|
|
2168
2480
|
//#endregion
|
|
2169
2481
|
//#region src/qoder/transport/search.ts
|
|
2170
2482
|
/**
|
|
@@ -2212,7 +2524,7 @@ var QoderSearchClient = class {
|
|
|
2212
2524
|
reason: first.reason
|
|
2213
2525
|
});
|
|
2214
2526
|
try {
|
|
2215
|
-
credentials = await this.refreshCredentials(signal);
|
|
2527
|
+
credentials = await this.refreshCredentials(credentials, signal);
|
|
2216
2528
|
} catch (error) {
|
|
2217
2529
|
if (signal?.aborted) throw searchAborted(signal, error);
|
|
2218
2530
|
throw new WebError(`Qoder credential refresh failed: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
@@ -2335,224 +2647,624 @@ var QoderSearchClient = class {
|
|
|
2335
2647
|
}
|
|
2336
2648
|
};
|
|
2337
2649
|
//#endregion
|
|
2338
|
-
//#region src/qoder/transport/
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
const
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2650
|
+
//#region src/qoder/transport/request-compression.ts
|
|
2651
|
+
/** qodercli-compatible request compression and endpoint capability recovery. */
|
|
2652
|
+
const disabledEndpoints = /* @__PURE__ */ new Set();
|
|
2653
|
+
function endpointKey(protocol, url) {
|
|
2654
|
+
const { origin, pathname } = new URL(url);
|
|
2655
|
+
return `${protocol}:${origin}${pathname}`;
|
|
2656
|
+
}
|
|
2657
|
+
function vpcEndpoint(url) {
|
|
2658
|
+
const host = new URL(url).hostname.toLowerCase().replace(/\.$/u, "");
|
|
2659
|
+
return host === "vpc.qoder.com.cn" || host.endsWith(".vpc.qoder.com.cn");
|
|
2660
|
+
}
|
|
2661
|
+
function envInteger(value, fallback, max = Number.MAX_SAFE_INTEGER) {
|
|
2662
|
+
if (value === void 0 || !value.trim()) return fallback;
|
|
2663
|
+
const parsed = Number(value);
|
|
2664
|
+
return Number.isSafeInteger(parsed) && parsed >= 1 && parsed <= max ? parsed : fallback;
|
|
2665
|
+
}
|
|
2666
|
+
function compressionRejection(status, text, protocol) {
|
|
2667
|
+
if (status === 415 && /zstd|content[- ]encoding|compressed (?:request|body)/iu.test(text)) return "unsupported_encoding";
|
|
2668
|
+
if (status === 400 && /invalid.*json|invalid character|parse.*json|unexpected token|not valid json/iu.test(text) && /\\x28|\\u0028|0x28|(?:character|token)\s*['"]\(['"]/iu.test(text)) return "invalid_json_at_origin";
|
|
2669
|
+
if (protocol === "legacy" && status === 403) {
|
|
2670
|
+
let payload;
|
|
2671
|
+
try {
|
|
2672
|
+
payload = JSON.parse(text);
|
|
2673
|
+
} catch {
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
if (payload && typeof payload === "object") {
|
|
2677
|
+
const root = payload;
|
|
2678
|
+
const error = root.error && typeof root.error === "object" ? root.error : root;
|
|
2679
|
+
if (String(error.code) === "101" && /signature (?:is )?invalid/iu.test(text)) return "signature_invalid";
|
|
2354
2680
|
}
|
|
2355
2681
|
}
|
|
2356
|
-
return best;
|
|
2357
2682
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2683
|
+
/** Probe only rejection responses, without consuming the transport's response. */
|
|
2684
|
+
async function probe(response, protocol, signal) {
|
|
2685
|
+
if (![
|
|
2686
|
+
400,
|
|
2687
|
+
403,
|
|
2688
|
+
415
|
|
2689
|
+
].includes(response.status)) return void 0;
|
|
2690
|
+
const reader = response.clone().body?.getReader();
|
|
2691
|
+
if (!reader) return void 0;
|
|
2692
|
+
const deadline = AbortSignal.any([signal, AbortSignal.timeout(1e3)]);
|
|
2693
|
+
const decoder = new TextDecoder();
|
|
2694
|
+
let remaining = 8192;
|
|
2695
|
+
let text = "";
|
|
2696
|
+
let truncated = false;
|
|
2697
|
+
const interrupted = () => {
|
|
2698
|
+
reader.cancel().catch(() => void 0);
|
|
2699
|
+
};
|
|
2700
|
+
deadline.addEventListener("abort", interrupted, { once: true });
|
|
2701
|
+
try {
|
|
2702
|
+
if (deadline.aborted) return void 0;
|
|
2703
|
+
while (remaining > 0 && !deadline.aborted) {
|
|
2704
|
+
const { value, done } = await reader.read();
|
|
2705
|
+
if (done) break;
|
|
2706
|
+
if (value.byteLength >= remaining) truncated = true;
|
|
2707
|
+
text += decoder.decode(value.subarray(0, remaining), { stream: true });
|
|
2708
|
+
remaining -= Math.min(value.byteLength, remaining);
|
|
2709
|
+
try {
|
|
2710
|
+
JSON.parse(text);
|
|
2711
|
+
break;
|
|
2712
|
+
} catch {}
|
|
2713
|
+
}
|
|
2714
|
+
text += decoder.decode();
|
|
2715
|
+
return {
|
|
2716
|
+
status: response.status,
|
|
2717
|
+
contentType: response.headers.get("content-type"),
|
|
2718
|
+
serverRequestId: response.headers.get("x-request-id") ?? void 0,
|
|
2719
|
+
body: text,
|
|
2720
|
+
bodyTruncated: truncated,
|
|
2721
|
+
probeTimedOut: deadline.aborted,
|
|
2722
|
+
reason: deadline.aborted ? void 0 : compressionRejection(response.status, text, protocol)
|
|
2365
2723
|
};
|
|
2724
|
+
} catch {
|
|
2725
|
+
return;
|
|
2726
|
+
} finally {
|
|
2727
|
+
deadline.removeEventListener("abort", interrupted);
|
|
2728
|
+
reader.cancel().catch(() => void 0);
|
|
2366
2729
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2730
|
+
}
|
|
2731
|
+
/** One instance per prepared model request; endpoint decisions survive requests. */
|
|
2732
|
+
var QoderRequestCompression = class {
|
|
2733
|
+
fallbackAttempted = false;
|
|
2734
|
+
pendingConfirmation;
|
|
2735
|
+
compressedEndpoint;
|
|
2736
|
+
diagnostics;
|
|
2737
|
+
async send(options) {
|
|
2738
|
+
const { url, protocol, body, headers, signal, logger, requestId } = options;
|
|
2739
|
+
let identityReason;
|
|
2740
|
+
const dispatch = (wireBody, wireHeaders) => {
|
|
2741
|
+
this.diagnostics = {
|
|
2742
|
+
encoding: new Headers(wireHeaders).get("content-encoding") ?? "identity",
|
|
2743
|
+
originalBytes: Buffer.byteLength(body),
|
|
2744
|
+
sentBytes: typeof wireBody === "string" ? Buffer.byteLength(wireBody) : wireBody.byteLength,
|
|
2745
|
+
...identityReason === void 0 ? {} : { reason: identityReason }
|
|
2746
|
+
};
|
|
2747
|
+
logger?.debug?.("[Qoder Request] Dispatch", {
|
|
2748
|
+
requestId,
|
|
2749
|
+
protocol,
|
|
2750
|
+
...this.diagnostics
|
|
2751
|
+
});
|
|
2752
|
+
return options.dispatch(wireBody, wireHeaders);
|
|
2753
|
+
};
|
|
2754
|
+
signal.throwIfAborted();
|
|
2755
|
+
const enabled = options.enabled ?? false;
|
|
2756
|
+
const key = endpointKey(protocol, url);
|
|
2757
|
+
const minChars = envInteger(process.env.QODER_ZSTD_REQUEST_BODY_MIN_CHARS, 4096);
|
|
2758
|
+
const level = envInteger(process.env.QODER_ZSTD_REQUEST_BODY_LEVEL, 1, 22);
|
|
2759
|
+
const existingEncoding = new Headers(headers).get("content-encoding");
|
|
2760
|
+
identityReason = !enabled ? "disabled" : this.fallbackAttempted ? "fallback_already_attempted" : vpcEndpoint(url) ? "vpc_endpoint" : disabledEndpoints.has(key) ? "endpoint_disabled" : body.length < minChars ? "below_threshold" : typeof zlib.zstdCompress !== "function" || zlib.constants.ZSTD_c_compressionLevel === void 0 ? "native_unavailable" : existingEncoding && existingEncoding !== "identity" ? "existing_encoding" : void 0;
|
|
2761
|
+
if (identityReason) return dispatch(body, headers);
|
|
2762
|
+
let compressed;
|
|
2763
|
+
const original = Buffer.from(body, "utf8");
|
|
2764
|
+
try {
|
|
2765
|
+
compressed = await promisify(zlib.zstdCompress)(original, { params: { [zlib.constants.ZSTD_c_compressionLevel]: level } });
|
|
2766
|
+
} catch {
|
|
2767
|
+
signal.throwIfAborted();
|
|
2768
|
+
identityReason = "compression_error";
|
|
2769
|
+
logger?.warn?.("[Qoder Request] Zstd compression failed; sending identity", {
|
|
2770
|
+
requestId,
|
|
2771
|
+
protocol
|
|
2772
|
+
});
|
|
2773
|
+
return dispatch(body, headers);
|
|
2774
|
+
}
|
|
2775
|
+
signal.throwIfAborted();
|
|
2776
|
+
if (compressed.byteLength >= original.byteLength) {
|
|
2777
|
+
identityReason = "not_smaller";
|
|
2778
|
+
return dispatch(body, headers);
|
|
2779
|
+
}
|
|
2780
|
+
const identityHeaders = Object.fromEntries(Object.entries(headers).filter(([key]) => !["content-length", "content-encoding"].includes(key.toLowerCase())));
|
|
2781
|
+
logger?.debug?.("[Qoder Request] Sending zstd", {
|
|
2782
|
+
requestId,
|
|
2783
|
+
protocol,
|
|
2784
|
+
originalBytes: original.byteLength,
|
|
2785
|
+
compressedBytes: compressed.byteLength,
|
|
2786
|
+
level
|
|
2787
|
+
});
|
|
2788
|
+
this.compressedEndpoint = key;
|
|
2789
|
+
const response = await dispatch(new Uint8Array(compressed), {
|
|
2790
|
+
...identityHeaders,
|
|
2791
|
+
"Content-Encoding": "zstd"
|
|
2792
|
+
});
|
|
2793
|
+
const rejection = await probe(response, protocol, signal);
|
|
2794
|
+
signal.throwIfAborted();
|
|
2795
|
+
if (rejection && options.onRejection) try {
|
|
2796
|
+
await options.onRejection(rejection);
|
|
2797
|
+
} catch {
|
|
2798
|
+
logger?.warn?.("[Qoder Request] Compression rejection capture failed", {
|
|
2799
|
+
requestId,
|
|
2800
|
+
protocol,
|
|
2801
|
+
status: response.status
|
|
2802
|
+
});
|
|
2398
2803
|
}
|
|
2399
|
-
|
|
2804
|
+
signal.throwIfAborted();
|
|
2805
|
+
if (!rejection?.reason) return response;
|
|
2806
|
+
if (protocol === "legacy") {
|
|
2807
|
+
response.body?.cancel().catch(() => void 0);
|
|
2808
|
+
this.rejectLegacy(response.status, rejection.reason);
|
|
2809
|
+
}
|
|
2810
|
+
this.fallbackAttempted = true;
|
|
2811
|
+
identityReason = rejection.reason;
|
|
2812
|
+
logger?.debug?.("[Qoder Request] Retrying identity", {
|
|
2813
|
+
requestId,
|
|
2814
|
+
protocol,
|
|
2815
|
+
reason: rejection.reason,
|
|
2816
|
+
status: response.status
|
|
2817
|
+
});
|
|
2818
|
+
response.body?.cancel().catch(() => void 0);
|
|
2819
|
+
const identityResponse = await dispatch(body, identityHeaders);
|
|
2820
|
+
this.pendingConfirmation = key;
|
|
2821
|
+
return identityResponse;
|
|
2400
2822
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
this.
|
|
2404
|
-
|
|
2823
|
+
confirm() {
|
|
2824
|
+
if (this.pendingConfirmation) disabledEndpoints.add(this.pendingConfirmation);
|
|
2825
|
+
this.pendingConfirmation = void 0;
|
|
2826
|
+
this.compressedEndpoint = void 0;
|
|
2405
2827
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
return text ? [{
|
|
2415
|
-
type: "reasoning",
|
|
2416
|
-
text
|
|
2417
|
-
}] : [];
|
|
2828
|
+
/** Legacy may reject the encoding in its SSE envelope before model output. */
|
|
2829
|
+
rejectLegacyError(error) {
|
|
2830
|
+
if (!(error instanceof QoderLlmError) || !this.compressedEndpoint) return;
|
|
2831
|
+
const status = error.failure.status;
|
|
2832
|
+
const cause = error.cause;
|
|
2833
|
+
if (typeof status !== "number") return;
|
|
2834
|
+
const rejection = compressionRejection(status, typeof cause === "string" ? cause : JSON.stringify(cause) ?? "", "legacy");
|
|
2835
|
+
if (rejection) this.rejectLegacy(status, rejection);
|
|
2418
2836
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
}
|
|
2439
|
-
const held = trailingPrefixLength(this.buffer, [this.closingTag]);
|
|
2440
|
-
const safeLength = this.buffer.length - held;
|
|
2441
|
-
if (safeLength > 0) output.push({
|
|
2442
|
-
type: "reasoning",
|
|
2443
|
-
text: this.buffer.slice(0, safeLength)
|
|
2444
|
-
});
|
|
2445
|
-
this.buffer = this.buffer.slice(safeLength);
|
|
2446
|
-
break;
|
|
2447
|
-
}
|
|
2448
|
-
const opener = earliestTag(this.buffer, "open");
|
|
2449
|
-
const closer = earliestTag(this.buffer, "close");
|
|
2450
|
-
if (opener !== void 0 && (closer === void 0 || opener.position < closer.position)) {
|
|
2451
|
-
if (opener.position > 0) output.push({
|
|
2452
|
-
type: "text",
|
|
2453
|
-
text: this.buffer.slice(0, opener.position)
|
|
2454
|
-
});
|
|
2455
|
-
this.buffer = this.buffer.slice(opener.position + opener.tag.open.length);
|
|
2456
|
-
this.closingTag = opener.tag.close;
|
|
2457
|
-
this.mode = "reasoning";
|
|
2458
|
-
continue;
|
|
2459
|
-
}
|
|
2460
|
-
if (closer !== void 0) {
|
|
2461
|
-
if (closer.position > 0) output.push({
|
|
2462
|
-
type: "text",
|
|
2463
|
-
text: this.buffer.slice(0, closer.position)
|
|
2464
|
-
});
|
|
2465
|
-
this.buffer = trimSeparator(this.buffer.slice(closer.position + closer.tag.close.length));
|
|
2837
|
+
rejectLegacy(status, reason) {
|
|
2838
|
+
if (this.compressedEndpoint) disabledEndpoints.add(this.compressedEndpoint);
|
|
2839
|
+
throw new QoderLlmError(`Qoder rejected Zstd (${reason}); retry without compression.`, "TRANSPORT", { status });
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
//#endregion
|
|
2843
|
+
//#region src/qoder/transport/wire/queue.ts
|
|
2844
|
+
/** Decode queue fields from the provider's nested JSON envelopes, never from prose. */
|
|
2845
|
+
function records(value) {
|
|
2846
|
+
const pending = [value];
|
|
2847
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2848
|
+
const result = [];
|
|
2849
|
+
for (let count = 0; pending.length && count < 64; count++) {
|
|
2850
|
+
let next = pending.shift();
|
|
2851
|
+
if (typeof next === "string") {
|
|
2852
|
+
if (next.length > 65536) continue;
|
|
2853
|
+
try {
|
|
2854
|
+
next = JSON.parse(next);
|
|
2855
|
+
} catch {
|
|
2466
2856
|
continue;
|
|
2467
2857
|
}
|
|
2468
|
-
const held = trailingPrefixLength(this.buffer, allTags);
|
|
2469
|
-
const safeLength = this.buffer.length - held;
|
|
2470
|
-
if (safeLength > 0) output.push({
|
|
2471
|
-
type: "text",
|
|
2472
|
-
text: this.buffer.slice(0, safeLength)
|
|
2473
|
-
});
|
|
2474
|
-
this.buffer = this.buffer.slice(safeLength);
|
|
2475
|
-
break;
|
|
2476
2858
|
}
|
|
2477
|
-
|
|
2859
|
+
if (!next || typeof next !== "object" || Array.isArray(next) || seen.has(next)) continue;
|
|
2860
|
+
seen.add(next);
|
|
2861
|
+
const record = next;
|
|
2862
|
+
result.push(record);
|
|
2863
|
+
for (const key of [
|
|
2864
|
+
"error",
|
|
2865
|
+
"data",
|
|
2866
|
+
"result",
|
|
2867
|
+
"message",
|
|
2868
|
+
"body",
|
|
2869
|
+
"cause",
|
|
2870
|
+
"queue",
|
|
2871
|
+
"details",
|
|
2872
|
+
"qoderApiError",
|
|
2873
|
+
"providerError"
|
|
2874
|
+
]) if (record[key] !== void 0) pending.push(record[key]);
|
|
2478
2875
|
}
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2876
|
+
return result;
|
|
2877
|
+
}
|
|
2878
|
+
function boolean(value) {
|
|
2879
|
+
return value === true || value === "true" ? true : value === false || value === "false" ? false : void 0;
|
|
2880
|
+
}
|
|
2881
|
+
function number(value) {
|
|
2882
|
+
if (typeof value !== "number" && (typeof value !== "string" || !value.trim())) return void 0;
|
|
2883
|
+
const parsed = Number(value);
|
|
2884
|
+
return Number.isFinite(parsed) && parsed >= 0 && parsed <= Number.MAX_SAFE_INTEGER / 1e3 ? parsed : void 0;
|
|
2885
|
+
}
|
|
2886
|
+
function status(record) {
|
|
2887
|
+
const result = {};
|
|
2888
|
+
for (const key of ["isQueued", "serviceAvailable"]) {
|
|
2889
|
+
const value = boolean(record[key]);
|
|
2890
|
+
if (value !== void 0) result[key] = value;
|
|
2492
2891
|
}
|
|
2493
|
-
|
|
2892
|
+
for (const key of [
|
|
2893
|
+
"queueCount",
|
|
2894
|
+
"waitTime",
|
|
2895
|
+
"retryAfterSeconds"
|
|
2896
|
+
]) {
|
|
2897
|
+
const value = number(record[key]);
|
|
2898
|
+
if (value !== void 0) result[key] = value;
|
|
2899
|
+
}
|
|
2900
|
+
for (const key of ["modelKey", "queueType"]) {
|
|
2901
|
+
const value = record[key];
|
|
2902
|
+
if (typeof value === "string" && value.trim() && value.length <= 256) result[key] = value.trim();
|
|
2903
|
+
}
|
|
2904
|
+
return result;
|
|
2905
|
+
}
|
|
2906
|
+
function parseQueueStatus(value) {
|
|
2907
|
+
for (const record of records(value)) {
|
|
2908
|
+
const parsed = status(record);
|
|
2909
|
+
if (parsed.isQueued !== void 0 || parsed.serviceAvailable !== void 0) return parsed;
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
function queuedError(value) {
|
|
2913
|
+
const candidates = records(value);
|
|
2914
|
+
if (!candidates.some((record) => String(record.code) === "10605" || boolean(record.isQueued) === true)) return void 0;
|
|
2915
|
+
return new QoderModelQueuedError(candidates.map(status).find((candidate) => candidate.isQueued === true || candidate.waitTime !== void 0) ?? candidates.map(status).find((candidate) => Object.keys(candidate).length > 0) ?? {});
|
|
2916
|
+
}
|
|
2494
2917
|
//#endregion
|
|
2495
|
-
//#region src/qoder/transport/
|
|
2496
|
-
/**
|
|
2497
|
-
|
|
2498
|
-
const
|
|
2499
|
-
|
|
2500
|
-
return
|
|
2918
|
+
//#region src/qoder/transport/queue.ts
|
|
2919
|
+
/** Queue polling and lease cleanup, scoped to one logical model call. */
|
|
2920
|
+
function queueFailure(message, code = "QODER_QUEUE_FAILED") {
|
|
2921
|
+
const error = new QoderLlmError(message, code);
|
|
2922
|
+
error.upstreamCode = "10605";
|
|
2923
|
+
return error;
|
|
2501
2924
|
}
|
|
2502
|
-
function
|
|
2503
|
-
|
|
2504
|
-
if (!raw) return void 0;
|
|
2505
|
-
if (typeof raw !== "string") return JSON.stringify(raw);
|
|
2506
|
-
const trimmed = raw.trim();
|
|
2507
|
-
try {
|
|
2508
|
-
const parsed = JSON.parse(trimmed);
|
|
2509
|
-
if (parsed && typeof parsed === "object") {
|
|
2510
|
-
let detailMsg;
|
|
2511
|
-
if (typeof parsed.details === "string") try {
|
|
2512
|
-
const inner = JSON.parse(parsed.details);
|
|
2513
|
-
if (typeof inner?.error?.message === "string") detailMsg = inner.error.message.trim();
|
|
2514
|
-
else if (typeof inner?.message === "string") detailMsg = inner.message.trim();
|
|
2515
|
-
} catch {}
|
|
2516
|
-
const msg = typeof parsed.message === "string" ? parsed.message.trim() : void 0;
|
|
2517
|
-
const code = typeof parsed.code === "string" ? parsed.code.trim() : void 0;
|
|
2518
|
-
const err = typeof parsed.error === "string" ? parsed.error.trim() : void 0;
|
|
2519
|
-
const text = detailMsg || msg || err;
|
|
2520
|
-
if (code && text) return `${code}: ${text}`;
|
|
2521
|
-
return text || code || trimmed;
|
|
2522
|
-
}
|
|
2523
|
-
} catch {}
|
|
2524
|
-
return trimmed;
|
|
2925
|
+
function checkAbort(signal) {
|
|
2926
|
+
if (signal?.aborted) throw new QoderLlmError("Qoder request was aborted.", "ABORTED");
|
|
2525
2927
|
}
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2928
|
+
const queueClock = {
|
|
2929
|
+
now: () => performance.now(),
|
|
2930
|
+
sleep: (ms, signal) => new Promise((resolve, reject) => {
|
|
2931
|
+
if (signal?.aborted) {
|
|
2932
|
+
reject(new QoderLlmError("Qoder request was aborted.", "ABORTED"));
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
const abort = () => {
|
|
2936
|
+
clearTimeout(timer);
|
|
2937
|
+
reject(new QoderLlmError("Qoder request was aborted.", "ABORTED"));
|
|
2938
|
+
};
|
|
2939
|
+
const timer = setTimeout(() => {
|
|
2940
|
+
signal?.removeEventListener("abort", abort);
|
|
2941
|
+
resolve();
|
|
2942
|
+
}, ms);
|
|
2943
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2944
|
+
})
|
|
2945
|
+
};
|
|
2946
|
+
/** A cancellable deadline with no surviving timers, including failed operations. */
|
|
2947
|
+
async function bounded(ms, signal, run) {
|
|
2948
|
+
checkAbort(signal);
|
|
2949
|
+
const controller = new AbortController();
|
|
2950
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
2951
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2952
|
+
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("Qoder queue operation timed out.")), Math.max(1, Math.ceil(ms)));
|
|
2953
|
+
let stop = () => {};
|
|
2954
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
2955
|
+
stop = () => reject(signal?.aborted ? new QoderLlmError("Qoder request was aborted.", "ABORTED") : queueFailure("Qoder queue operation timed out."));
|
|
2956
|
+
controller.signal.addEventListener("abort", stop, { once: true });
|
|
2957
|
+
});
|
|
2958
|
+
try {
|
|
2959
|
+
return await Promise.race([run(controller.signal), interrupted]);
|
|
2960
|
+
} finally {
|
|
2961
|
+
clearTimeout(timer);
|
|
2962
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2963
|
+
controller.signal.removeEventListener("abort", stop);
|
|
2964
|
+
controller.abort();
|
|
2543
2965
|
}
|
|
2544
|
-
if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
|
|
2545
|
-
return envelope;
|
|
2546
2966
|
}
|
|
2547
|
-
function
|
|
2967
|
+
async function readBody(response, signal) {
|
|
2968
|
+
if (!response.body) return "";
|
|
2969
|
+
const reader = response.body.getReader();
|
|
2970
|
+
const cancel = () => {
|
|
2971
|
+
reader.cancel().catch(() => {});
|
|
2972
|
+
};
|
|
2973
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
2974
|
+
const decoder = new TextDecoder();
|
|
2975
|
+
let bytes = 0;
|
|
2976
|
+
let text = "";
|
|
2548
2977
|
try {
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2978
|
+
checkAbort(signal);
|
|
2979
|
+
while (true) {
|
|
2980
|
+
const { done, value } = await reader.read();
|
|
2981
|
+
checkAbort(signal);
|
|
2982
|
+
if (done) return text + decoder.decode();
|
|
2983
|
+
bytes += value.byteLength;
|
|
2984
|
+
if (bytes > 65536) throw queueFailure("Qoder queue response exceeded its size limit.");
|
|
2985
|
+
text += decoder.decode(value, { stream: true });
|
|
2986
|
+
}
|
|
2987
|
+
} finally {
|
|
2988
|
+
signal.removeEventListener("abort", cancel);
|
|
2989
|
+
await reader.cancel().catch(() => {});
|
|
2990
|
+
reader.releaseLock();
|
|
2554
2991
|
}
|
|
2555
2992
|
}
|
|
2993
|
+
function queueInterval(seconds) {
|
|
2994
|
+
return Math.min(3e4, Math.max(500, seconds !== void 0 && Number.isFinite(seconds) ? seconds * 1e3 : 3e4));
|
|
2995
|
+
}
|
|
2996
|
+
var ModelQueue = class {
|
|
2997
|
+
callId = randomUUID();
|
|
2998
|
+
options;
|
|
2999
|
+
clock;
|
|
3000
|
+
maxWaitMs;
|
|
3001
|
+
models = /* @__PURE__ */ new Set();
|
|
3002
|
+
deferred = /* @__PURE__ */ new Set();
|
|
3003
|
+
waitMs = 0;
|
|
3004
|
+
recoveries = 0;
|
|
3005
|
+
constructor(options) {
|
|
3006
|
+
this.options = options;
|
|
3007
|
+
this.clock = options.clock ?? queueClock;
|
|
3008
|
+
this.maxWaitMs = options.maxWaitMs ?? 36e5;
|
|
3009
|
+
if (!Number.isSafeInteger(this.maxWaitMs) || this.maxWaitMs <= 0 || this.maxWaitMs > 2147483647) throw new Error("modelQueueMaxWaitMs must be a positive integer no greater than 2147483647");
|
|
3010
|
+
}
|
|
3011
|
+
publish(update) {
|
|
3012
|
+
try {
|
|
3013
|
+
this.options.onUpdate?.(update);
|
|
3014
|
+
} catch {
|
|
3015
|
+
this.options.logger?.warn?.("[Qoder Queue] Status observer failed");
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
clear() {
|
|
3019
|
+
this.publish({
|
|
3020
|
+
type: "clear",
|
|
3021
|
+
callId: this.callId
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
/** Register even a queue signal rejected by the output guard, so cleanup still runs. */
|
|
3025
|
+
entered(status) {
|
|
3026
|
+
const model = status.modelKey ?? this.options.model;
|
|
3027
|
+
if (this.models.has(model)) return;
|
|
3028
|
+
this.models.add(model);
|
|
3029
|
+
if (this.options.dshSessionId && this.options.onLease) {
|
|
3030
|
+
const key = JSON.stringify([
|
|
3031
|
+
this.options.region,
|
|
3032
|
+
this.options.subscriberId,
|
|
3033
|
+
this.options.requestSetId,
|
|
3034
|
+
model
|
|
3035
|
+
]);
|
|
3036
|
+
if (this.options.onLease({
|
|
3037
|
+
key,
|
|
3038
|
+
sessionId: this.options.dshSessionId,
|
|
3039
|
+
release: () => this.release(model, Math.max(0, Date.now() - (this.options.runStartedAt ?? Date.now())))
|
|
3040
|
+
})) this.deferred.add(model);
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
async wait(initial, requestId) {
|
|
3044
|
+
this.entered(initial);
|
|
3045
|
+
if (this.recoveries >= 10) throw queueFailure("Qoder queue recovery limit reached.", "QODER_QUEUE_LIMIT");
|
|
3046
|
+
const started = this.clock.now();
|
|
3047
|
+
const remaining = () => this.maxWaitMs - this.waitMs - (this.clock.now() - started);
|
|
3048
|
+
const assertTime = () => {
|
|
3049
|
+
checkAbort(this.options.signal);
|
|
3050
|
+
if (remaining() <= 0) throw queueFailure("Qoder model queue wait timed out.", "QODER_QUEUE_TIMEOUT");
|
|
3051
|
+
};
|
|
3052
|
+
let status = initial;
|
|
3053
|
+
let failures = 0;
|
|
3054
|
+
let first = true;
|
|
3055
|
+
try {
|
|
3056
|
+
while (true) {
|
|
3057
|
+
assertTime();
|
|
3058
|
+
if (this.options.dshSessionId !== void 0) this.publish({
|
|
3059
|
+
type: "waiting",
|
|
3060
|
+
snapshot: {
|
|
3061
|
+
callId: this.callId,
|
|
3062
|
+
sessionId: this.options.dshSessionId,
|
|
3063
|
+
model: status.modelKey ?? this.options.model,
|
|
3064
|
+
...this.options.modelName ? { modelName: this.options.modelName } : {},
|
|
3065
|
+
...status.queueCount === void 0 ? {} : { queueCount: status.queueCount },
|
|
3066
|
+
...status.waitTime === void 0 ? {} : { waitTimeMs: status.waitTime * 1e3 },
|
|
3067
|
+
...status.serviceAvailable === void 0 ? {} : { serviceAvailable: status.serviceAvailable },
|
|
3068
|
+
waitElapsedMs: this.waitMs + Math.max(0, this.clock.now() - started),
|
|
3069
|
+
maxWaitMs: this.maxWaitMs,
|
|
3070
|
+
updatedAt: Date.now()
|
|
3071
|
+
}
|
|
3072
|
+
});
|
|
3073
|
+
if (!first) await this.clock.sleep(Math.min(queueInterval(status.retryAfterSeconds), remaining()), this.options.signal);
|
|
3074
|
+
first = false;
|
|
3075
|
+
assertTime();
|
|
3076
|
+
try {
|
|
3077
|
+
const next = await this.poll(status, requestId, remaining());
|
|
3078
|
+
status = {
|
|
3079
|
+
...next,
|
|
3080
|
+
modelKey: next.modelKey ?? status.modelKey,
|
|
3081
|
+
queueType: next.queueType ?? status.queueType
|
|
3082
|
+
};
|
|
3083
|
+
failures = 0;
|
|
3084
|
+
this.entered(status);
|
|
3085
|
+
if (status.isQueued === false) {
|
|
3086
|
+
if (status.retryAfterSeconds !== void 0 && status.retryAfterSeconds > 0) await this.clock.sleep(Math.min(queueInterval(status.retryAfterSeconds), remaining()), this.options.signal);
|
|
3087
|
+
assertTime();
|
|
3088
|
+
this.recoveries++;
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
if (status.isQueued !== true && status.serviceAvailable !== false) throw queueFailure("Qoder returned an invalid queue state.", "QODER_QUEUE_INVALID_STATE");
|
|
3092
|
+
} catch (error) {
|
|
3093
|
+
assertTime();
|
|
3094
|
+
if (error instanceof QoderLlmError && [
|
|
3095
|
+
"QODER_QUEUE_UNAVAILABLE",
|
|
3096
|
+
"QODER_QUEUE_INVALID_STATE",
|
|
3097
|
+
"QODER_QUEUE_IDENTITY"
|
|
3098
|
+
].includes(error.code)) throw error;
|
|
3099
|
+
if (++failures >= 3) throw queueFailure("Qoder queue status polling failed three consecutive times.");
|
|
3100
|
+
this.options.logger?.warn?.("[Qoder Queue] Status poll failed; will retry", { failures });
|
|
3101
|
+
status = {
|
|
3102
|
+
...status,
|
|
3103
|
+
retryAfterSeconds: void 0
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
} finally {
|
|
3108
|
+
this.waitMs += Math.max(0, this.clock.now() - started);
|
|
3109
|
+
this.clear();
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
async poll(status, requestId, remainingMs) {
|
|
3113
|
+
const url = getQoderQueueStatusUrl(this.options.region, {
|
|
3114
|
+
requestSetId: this.options.requestSetId,
|
|
3115
|
+
modelKey: status.modelKey ?? this.options.model,
|
|
3116
|
+
queueType: status.queueType
|
|
3117
|
+
});
|
|
3118
|
+
return bounded(remainingMs, this.options.signal, async (signal) => {
|
|
3119
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3120
|
+
const credentials = await this.options.credentials(attempt > 0, signal);
|
|
3121
|
+
checkAbort(signal);
|
|
3122
|
+
const outcome = await bounded(Math.min(3e4, remainingMs), signal, async (httpSignal) => {
|
|
3123
|
+
const response = await this.options.fetch(url, {
|
|
3124
|
+
method: "GET",
|
|
3125
|
+
headers: {
|
|
3126
|
+
...attributionHeaders(),
|
|
3127
|
+
...buildAuthHeaders(null, url, credentials),
|
|
3128
|
+
Accept: "application/json",
|
|
3129
|
+
"Accept-Encoding": "identity",
|
|
3130
|
+
"X-Request-Id": requestId,
|
|
3131
|
+
"X-Session-ID": this.options.sessionId
|
|
3132
|
+
},
|
|
3133
|
+
signal: httpSignal
|
|
3134
|
+
});
|
|
3135
|
+
return {
|
|
3136
|
+
status: response.status,
|
|
3137
|
+
ok: response.ok,
|
|
3138
|
+
body: await readBody(response, httpSignal)
|
|
3139
|
+
};
|
|
3140
|
+
});
|
|
3141
|
+
if ((outcome.status === 401 || outcome.status === 403) && attempt === 0) continue;
|
|
3142
|
+
if (outcome.status === 404) throw queueFailure("Qoder queue endpoint is unavailable.", "QODER_QUEUE_UNAVAILABLE");
|
|
3143
|
+
if (!outcome.ok) throw queueFailure(`Qoder queue status returned HTTP ${outcome.status}.`);
|
|
3144
|
+
const parsed = parseQueueStatus(outcome.body);
|
|
3145
|
+
if (!parsed) throw queueFailure("Qoder returned a malformed queue status.");
|
|
3146
|
+
return parsed;
|
|
3147
|
+
}
|
|
3148
|
+
throw queueFailure("Qoder queue authentication failed.");
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
/** Best-effort cleanup has an independent signal even after caller cancellation. */
|
|
3152
|
+
async finish(elapsedMs) {
|
|
3153
|
+
this.clear();
|
|
3154
|
+
const models = [...this.models].filter((model) => !this.deferred.has(model));
|
|
3155
|
+
this.models.clear();
|
|
3156
|
+
await Promise.allSettled(models.map((model) => this.release(model, elapsedMs)));
|
|
3157
|
+
}
|
|
3158
|
+
async release(model, elapsedMs) {
|
|
3159
|
+
try {
|
|
3160
|
+
await bounded(3e3, void 0, async (signal) => {
|
|
3161
|
+
const credentials = await this.options.credentials(false, signal);
|
|
3162
|
+
checkAbort(signal);
|
|
3163
|
+
const url = getQoderQueueFinishUrl(this.options.region);
|
|
3164
|
+
const body = JSON.stringify({
|
|
3165
|
+
payload: JSON.stringify({
|
|
3166
|
+
model_key: model,
|
|
3167
|
+
request_set_id: this.options.requestSetId,
|
|
3168
|
+
user_id: credentials.userID,
|
|
3169
|
+
time_consumed: Math.max(0, Math.round(elapsedMs))
|
|
3170
|
+
}),
|
|
3171
|
+
encodeVersion: "1"
|
|
3172
|
+
});
|
|
3173
|
+
const response = await this.options.fetch(url, {
|
|
3174
|
+
method: "POST",
|
|
3175
|
+
headers: {
|
|
3176
|
+
...attributionHeaders(),
|
|
3177
|
+
...buildAuthHeaders(body, url, credentials),
|
|
3178
|
+
"Content-Type": "application/json",
|
|
3179
|
+
Accept: "application/json",
|
|
3180
|
+
"X-Request-Id": randomUUID()
|
|
3181
|
+
},
|
|
3182
|
+
body,
|
|
3183
|
+
signal
|
|
3184
|
+
});
|
|
3185
|
+
await readBody(response, signal);
|
|
3186
|
+
if (!response.ok) throw queueFailure("Qoder lease release failed.");
|
|
3187
|
+
});
|
|
3188
|
+
} catch {
|
|
3189
|
+
this.options.logger?.warn?.("[Qoder Queue] Finish failed; server TTL will release the lease");
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
};
|
|
3193
|
+
//#endregion
|
|
3194
|
+
//#region src/qoder/transport/wire/serialize.ts
|
|
3195
|
+
async function buildQoderRequestBody(options, userId, translatedMessages, model, attachments, tracker, identityScope = "") {
|
|
3196
|
+
const prepared = await prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker, identityScope);
|
|
3197
|
+
return {
|
|
3198
|
+
request_id: prepared.requestId,
|
|
3199
|
+
request_set_id: prepared.requestSetId,
|
|
3200
|
+
chat_record_id: prepared.chatRecordId,
|
|
3201
|
+
session_id: prepared.sessionId,
|
|
3202
|
+
stream: true,
|
|
3203
|
+
chat_task: "FREE_INPUT",
|
|
3204
|
+
is_reply: true,
|
|
3205
|
+
is_retry: false,
|
|
3206
|
+
source: 1,
|
|
3207
|
+
version: "3",
|
|
3208
|
+
session_type: "qodercli",
|
|
3209
|
+
agent_id: "agent_common",
|
|
3210
|
+
task_id: prepared.taskId,
|
|
3211
|
+
code_language: "",
|
|
3212
|
+
chat_prompt: "",
|
|
3213
|
+
image_urls: null,
|
|
3214
|
+
aliyun_user_type: "",
|
|
3215
|
+
system: prepared.messages.filter((message) => message.role === "system").flatMap((message) => typeof message.content === "string" && message.content.length > 0 ? [{
|
|
3216
|
+
type: "text",
|
|
3217
|
+
text: message.content
|
|
3218
|
+
}] : []),
|
|
3219
|
+
messages: prepared.messages,
|
|
3220
|
+
tools: prepared.tools,
|
|
3221
|
+
parameters: {
|
|
3222
|
+
max_tokens: prepared.maxTokens,
|
|
3223
|
+
...prepared.contextLength === void 0 ? {} : { context_length: prepared.contextLength },
|
|
3224
|
+
...prepared.reasoningEffort === void 0 ? {} : { reasoning_effort: prepared.reasoningEffort }
|
|
3225
|
+
},
|
|
3226
|
+
chat_context: {
|
|
3227
|
+
chatPrompt: "",
|
|
3228
|
+
imageUrls: null,
|
|
3229
|
+
extra: {
|
|
3230
|
+
context: [],
|
|
3231
|
+
modelConfig: {
|
|
3232
|
+
key: prepared.modelKey,
|
|
3233
|
+
is_reasoning: prepared.isReasoning
|
|
3234
|
+
},
|
|
3235
|
+
originalContent: prepared.lastUserText
|
|
3236
|
+
},
|
|
3237
|
+
features: [],
|
|
3238
|
+
text: prepared.lastUserText
|
|
3239
|
+
},
|
|
3240
|
+
model_config: {
|
|
3241
|
+
key: prepared.modelKey,
|
|
3242
|
+
is_reasoning: prepared.isReasoning,
|
|
3243
|
+
max_output_tokens: prepared.maxTokens,
|
|
3244
|
+
source: prepared.modelSource,
|
|
3245
|
+
...prepared.contextConfig === void 0 ? {} : { context_config: prepared.contextConfig }
|
|
3246
|
+
},
|
|
3247
|
+
business: prepared.business
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
//#endregion
|
|
3251
|
+
//#region src/qoder/transport/wire/stream-chunks.ts
|
|
3252
|
+
/** Convert decoded Qoder model frames into DSH stream chunks. */
|
|
3253
|
+
const doneMarker = "[DONE]";
|
|
3254
|
+
const defaultMaxSseBufferChars = 2 * 1024 * 1024;
|
|
3255
|
+
function malformed$1(message) {
|
|
3256
|
+
return new QoderLlmError(message, "MALFORMED_RESPONSE");
|
|
3257
|
+
}
|
|
3258
|
+
/** Distinguish invalid JSON from a valid JSON value with an invalid model shape. */
|
|
3259
|
+
var QoderMalformedJsonFrameError = class extends QoderLlmError {
|
|
3260
|
+
incomplete;
|
|
3261
|
+
auxiliary;
|
|
3262
|
+
constructor(incomplete = false, auxiliary = false) {
|
|
3263
|
+
super("Malformed model-server SSE JSON payload.", "MALFORMED_RESPONSE");
|
|
3264
|
+
this.incomplete = incomplete;
|
|
3265
|
+
this.auxiliary = auxiliary;
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
2556
3268
|
function tokenUsage(innerChunk) {
|
|
2557
3269
|
if (!innerChunk.usage) return void 0;
|
|
2558
3270
|
const usage = innerChunk.usage;
|
|
@@ -2574,7 +3286,7 @@ function finishKind(value) {
|
|
|
2574
3286
|
if (value === "length") return "max-tokens";
|
|
2575
3287
|
if (value === "tool_calls" || value === "toolUse" || value === "function_call") return "tool-calls";
|
|
2576
3288
|
if (value === "content_filter") throw new QoderLlmError("Qoder blocked the response through its content filter.", "PROVIDER_ERROR");
|
|
2577
|
-
throw malformed(`Qoder returned unknown finish reason "${value}".`);
|
|
3289
|
+
throw malformed$1(`Qoder returned unknown finish reason "${value}".`);
|
|
2578
3290
|
}
|
|
2579
3291
|
/** Whether a choice delta streams content, reasoning, or tool-call data. Control-only frames do not. */
|
|
2580
3292
|
function carriesStreamDelta(delta) {
|
|
@@ -2584,12 +3296,15 @@ function carriesStreamDelta(delta) {
|
|
|
2584
3296
|
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) return true;
|
|
2585
3297
|
return false;
|
|
2586
3298
|
}
|
|
2587
|
-
async function*
|
|
3299
|
+
async function* parseQoderChunks(stream, decodeFrame, options = {}, behavior = {}) {
|
|
2588
3300
|
const reader = stream.getReader();
|
|
2589
3301
|
const decoder = new TextDecoder();
|
|
2590
|
-
const thinkingParser = new QoderThinkingParser();
|
|
2591
3302
|
const toolCalls = /* @__PURE__ */ new Map();
|
|
2592
3303
|
let buffer = "";
|
|
3304
|
+
let eventData = [];
|
|
3305
|
+
let continuedEventData = [];
|
|
3306
|
+
let eventDataChars = 0;
|
|
3307
|
+
let eventType;
|
|
2593
3308
|
let sourceEnded = false;
|
|
2594
3309
|
let sawDone = false;
|
|
2595
3310
|
let nextBlockIndex = 0;
|
|
@@ -2672,10 +3387,11 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2672
3387
|
sourceEnded = true;
|
|
2673
3388
|
buffer += decoder.decode();
|
|
2674
3389
|
if (buffer.length > 0 && !buffer.endsWith("\n")) buffer += "\n";
|
|
3390
|
+
buffer += "\n";
|
|
2675
3391
|
} else {
|
|
2676
3392
|
options.onActivity?.();
|
|
2677
3393
|
buffer += decoder.decode(value, { stream: true });
|
|
2678
|
-
if (buffer.length > maxBufferChars) throw malformed("Qoder SSE frame exceeded its size limit.");
|
|
3394
|
+
if (buffer.length > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
|
|
2679
3395
|
}
|
|
2680
3396
|
while (!sawDone) {
|
|
2681
3397
|
const lineEnd = buffer.indexOf("\n");
|
|
@@ -2683,22 +3399,86 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2683
3399
|
let line = buffer.substring(0, lineEnd);
|
|
2684
3400
|
buffer = buffer.substring(lineEnd + 1);
|
|
2685
3401
|
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
3402
|
+
if (line.startsWith("event:")) {
|
|
3403
|
+
eventType = line.substring(6).trim();
|
|
3404
|
+
continue;
|
|
3405
|
+
}
|
|
3406
|
+
if (line.startsWith("data:")) {
|
|
3407
|
+
const value = line.substring(5);
|
|
3408
|
+
const data = value.startsWith(" ") ? value.substring(1) : value;
|
|
3409
|
+
eventData.push(data);
|
|
3410
|
+
if (behavior.recoverUnprefixedContinuations) continuedEventData.push(data);
|
|
3411
|
+
eventDataChars += data.length + 1;
|
|
3412
|
+
if (eventDataChars > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
|
|
3413
|
+
continue;
|
|
3414
|
+
}
|
|
3415
|
+
if (line !== "") {
|
|
3416
|
+
const jsonPropertyContinuation = /^:\s*(?:-?\d|true\b|false\b|null\b|["{\[\\])/u.test(line);
|
|
3417
|
+
if (behavior.recoverUnprefixedContinuations && continuedEventData.length > 0 && (!line.startsWith(":") || jsonPropertyContinuation) && !/^[a-zA-Z][\w-]*(?::|$)/u.test(line)) {
|
|
3418
|
+
const index = continuedEventData.length - 1;
|
|
3419
|
+
continuedEventData[index] += line;
|
|
3420
|
+
eventDataChars += line.length;
|
|
3421
|
+
if (eventDataChars > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
|
|
3422
|
+
}
|
|
3423
|
+
continue;
|
|
3424
|
+
}
|
|
3425
|
+
const rawData = eventData.join("\n").trim();
|
|
3426
|
+
const continuedData = continuedEventData.join("\n").trim();
|
|
3427
|
+
const frameEventType = eventType;
|
|
3428
|
+
eventData = [];
|
|
3429
|
+
continuedEventData = [];
|
|
3430
|
+
eventDataChars = 0;
|
|
3431
|
+
eventType = void 0;
|
|
2689
3432
|
if (!rawData) continue;
|
|
2690
3433
|
if (rawData === doneMarker) {
|
|
3434
|
+
options.onControlFrame?.("done");
|
|
2691
3435
|
sawDone = true;
|
|
2692
3436
|
break;
|
|
2693
3437
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
3438
|
+
let innerChunk;
|
|
3439
|
+
let malformedDetails;
|
|
3440
|
+
try {
|
|
3441
|
+
const frameOptions = {
|
|
3442
|
+
...options,
|
|
3443
|
+
onMalformedFrame: (details) => {
|
|
3444
|
+
malformedDetails = {
|
|
3445
|
+
...details,
|
|
3446
|
+
atEof: sourceEnded,
|
|
3447
|
+
afterFinish: sawFinishReason
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
};
|
|
3451
|
+
try {
|
|
3452
|
+
innerChunk = decodeFrame(rawData, frameOptions, frameEventType);
|
|
3453
|
+
} catch (error) {
|
|
3454
|
+
if (!(error instanceof QoderMalformedJsonFrameError) || !continuedData || continuedData === rawData) throw error;
|
|
3455
|
+
const originalDetails = malformedDetails;
|
|
3456
|
+
innerChunk = decodeFrame(continuedData, frameOptions, frameEventType);
|
|
3457
|
+
if (originalDetails) options.onMalformedFrame?.({
|
|
3458
|
+
...originalDetails,
|
|
3459
|
+
disposition: "recovered-continuation"
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
} catch (error) {
|
|
3463
|
+
const malformedJson = error instanceof QoderMalformedJsonFrameError ? error : void 0;
|
|
3464
|
+
const skippedAfterFinish = behavior.ignoreMalformedAfterFinish && sawFinishReason && frameEventType !== "error" && malformedJson !== void 0;
|
|
3465
|
+
const skippedAuxiliary = !sourceEnded && behavior.allowIncompleteAuxiliaryFrames && malformedJson?.incomplete && malformedJson.auxiliary && frameEventType !== "error";
|
|
3466
|
+
if (malformedDetails !== void 0) options.onMalformedFrame?.({
|
|
3467
|
+
...malformedDetails,
|
|
3468
|
+
emptyChoicesPrefix: malformedJson?.auxiliary ?? false,
|
|
3469
|
+
disposition: skippedAfterFinish ? "skipped-after-finish" : skippedAuxiliary ? "skipped-auxiliary" : "rejected"
|
|
3470
|
+
});
|
|
3471
|
+
if (skippedAfterFinish) continue;
|
|
3472
|
+
if (sourceEnded && error instanceof QoderMalformedJsonFrameError && error.incomplete) throw new QoderLlmError("Qoder SSE connection ended with an incomplete JSON event before model completion.", "TRANSPORT");
|
|
3473
|
+
if (skippedAuxiliary) continue;
|
|
3474
|
+
throw error;
|
|
3475
|
+
}
|
|
3476
|
+
if (innerChunk === void 0) continue;
|
|
3477
|
+
if (innerChunk === doneMarker) {
|
|
3478
|
+
options.onControlFrame?.("done");
|
|
2698
3479
|
sawDone = true;
|
|
2699
3480
|
break;
|
|
2700
3481
|
}
|
|
2701
|
-
const innerChunk = parseInner(body);
|
|
2702
3482
|
pendingUsage = tokenUsage(innerChunk) ?? pendingUsage;
|
|
2703
3483
|
for (const choice of innerChunk.choices ?? []) {
|
|
2704
3484
|
const kind = finishKind(choice.finish_reason);
|
|
@@ -2708,14 +3488,20 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2708
3488
|
} else if (carriesStreamDelta(choice.delta)) sawFinishReason = false;
|
|
2709
3489
|
const delta = choice.delta;
|
|
2710
3490
|
if (!delta) continue;
|
|
2711
|
-
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) for (const
|
|
2712
|
-
|
|
3491
|
+
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) for (const chunk of appendSegment({
|
|
3492
|
+
type: "reasoning",
|
|
3493
|
+
text: delta.reasoning_content
|
|
3494
|
+
})) yield chunk;
|
|
3495
|
+
if (typeof delta.content === "string" && delta.content) for (const chunk of appendSegment({
|
|
3496
|
+
type: "text",
|
|
3497
|
+
text: delta.content
|
|
3498
|
+
})) yield chunk;
|
|
2713
3499
|
if (delta.tool_calls !== void 0) {
|
|
2714
|
-
if (!Array.isArray(delta.tool_calls)) throw malformed("Qoder tool-call delta is not an array.");
|
|
3500
|
+
if (!Array.isArray(delta.tool_calls)) throw malformed$1("Qoder tool-call delta is not an array.");
|
|
2715
3501
|
for (const rawCall of delta.tool_calls) {
|
|
2716
|
-
if (typeof rawCall !== "object" || rawCall === null || Array.isArray(rawCall)) throw malformed("Qoder tool-call delta is not an object.");
|
|
3502
|
+
if (typeof rawCall !== "object" || rawCall === null || Array.isArray(rawCall)) throw malformed$1("Qoder tool-call delta is not an object.");
|
|
2717
3503
|
const upstreamIndex = rawCall.index ?? 0;
|
|
2718
|
-
if (!Number.isInteger(upstreamIndex) || upstreamIndex < 0) throw malformed("Qoder tool-call delta has an invalid index.");
|
|
3504
|
+
if (!Number.isInteger(upstreamIndex) || upstreamIndex < 0) throw malformed$1("Qoder tool-call delta has an invalid index.");
|
|
2719
3505
|
let state = toolCalls.get(upstreamIndex);
|
|
2720
3506
|
if (state === void 0) {
|
|
2721
3507
|
state = {
|
|
@@ -2726,17 +3512,17 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2726
3512
|
};
|
|
2727
3513
|
toolCalls.set(upstreamIndex, state);
|
|
2728
3514
|
}
|
|
2729
|
-
if (rawCall.id !== void 0) if (rawCall.id === "" || rawCall.id === null) {} else if (typeof rawCall.id !== "string") throw malformed("Qoder tool call has an invalid id.");
|
|
3515
|
+
if (rawCall.id !== void 0) if (rawCall.id === "" || rawCall.id === null) {} else if (typeof rawCall.id !== "string") throw malformed$1("Qoder tool call has an invalid id.");
|
|
2730
3516
|
else {
|
|
2731
|
-
if (state.id && state.id !== rawCall.id) throw malformed("Qoder changed a streamed tool-call id.");
|
|
3517
|
+
if (state.id && state.id !== rawCall.id) throw malformed$1("Qoder changed a streamed tool-call id.");
|
|
2732
3518
|
state.id = rawCall.id;
|
|
2733
3519
|
}
|
|
2734
3520
|
let hasNewName = false;
|
|
2735
3521
|
if (rawCall.function?.name !== void 0) {
|
|
2736
3522
|
const name = rawCall.function.name;
|
|
2737
|
-
if (name === "" || name === null) {} else if (typeof name !== "string") throw malformed("Qoder tool call has an invalid name.");
|
|
3523
|
+
if (name === "" || name === null) {} else if (typeof name !== "string") throw malformed$1("Qoder tool call has an invalid name.");
|
|
2738
3524
|
else {
|
|
2739
|
-
if (state.name && state.name !== name) throw malformed("Qoder changed a streamed tool-call name.");
|
|
3525
|
+
if (state.name && state.name !== name) throw malformed$1("Qoder changed a streamed tool-call name.");
|
|
2740
3526
|
if (state.name !== name) {
|
|
2741
3527
|
state.name = name;
|
|
2742
3528
|
hasNewName = true;
|
|
@@ -2744,7 +3530,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2744
3530
|
}
|
|
2745
3531
|
}
|
|
2746
3532
|
if (rawCall.function?.arguments !== void 0) {
|
|
2747
|
-
if (typeof rawCall.function.arguments !== "string") throw malformed("Qoder tool-call arguments delta is not a string.");
|
|
3533
|
+
if (typeof rawCall.function.arguments !== "string") throw malformed$1("Qoder tool-call arguments delta is not a string.");
|
|
2748
3534
|
state.arguments += rawCall.function.arguments;
|
|
2749
3535
|
}
|
|
2750
3536
|
const wasOpen = state.blockIndex !== void 0;
|
|
@@ -2776,18 +3562,17 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2776
3562
|
reader.releaseLock();
|
|
2777
3563
|
}
|
|
2778
3564
|
if (!sawDone && !sawFinishReason) throw new QoderLlmError("SSE stream ended prematurely without [DONE].", "TRANSPORT");
|
|
2779
|
-
for (const segment of thinkingParser.finish()) for (const chunk of appendSegment(segment)) yield chunk;
|
|
2780
3565
|
for (const chunk of closeTextual()) yield chunk;
|
|
2781
3566
|
for (const [, state] of [...toolCalls.entries()].sort(([left], [right]) => left - right)) {
|
|
2782
|
-
if (!state.id || !state.name) throw malformed("Qoder completed an unidentifiable tool call.");
|
|
3567
|
+
if (!state.id || !state.name) throw malformed$1("Qoder completed an unidentifiable tool call.");
|
|
2783
3568
|
const normalizedArguments = state.arguments.trim() ? state.arguments : "{}";
|
|
2784
3569
|
try {
|
|
2785
3570
|
JSON.parse(normalizedArguments);
|
|
2786
3571
|
} catch {
|
|
2787
|
-
throw malformed(`Qoder completed tool call "${state.name}" with malformed JSON arguments.`);
|
|
3572
|
+
throw malformed$1(`Qoder completed tool call "${state.name}" with malformed JSON arguments.`);
|
|
2788
3573
|
}
|
|
2789
3574
|
for (const chunk of openToolCall(state)) yield chunk;
|
|
2790
|
-
if (state.blockIndex === void 0) throw malformed("Qoder failed to open a completed tool call.");
|
|
3575
|
+
if (state.blockIndex === void 0) throw malformed$1("Qoder failed to open a completed tool call.");
|
|
2791
3576
|
if (state.emittedArguments === 0 && normalizedArguments === "{}") yield {
|
|
2792
3577
|
type: "tool-call-delta",
|
|
2793
3578
|
index: state.blockIndex,
|
|
@@ -2829,15 +3614,128 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2829
3614
|
};
|
|
2830
3615
|
}
|
|
2831
3616
|
//#endregion
|
|
3617
|
+
//#region src/qoder/transport/wire/control-frames.ts
|
|
3618
|
+
function qoderControlFrameKind(data) {
|
|
3619
|
+
if (data === "[DONE]") return "done";
|
|
3620
|
+
if (data === "[NOT_EXCEED_QUOTA]") return "quota-available";
|
|
3621
|
+
if (data.startsWith("[EXCEED_QUOTA]")) return "quota-exceeded";
|
|
3622
|
+
if (data.startsWith("[NOTIFICATIONS]")) return "notifications";
|
|
3623
|
+
}
|
|
3624
|
+
//#endregion
|
|
3625
|
+
//#region src/qoder/transport/wire/sse.ts
|
|
3626
|
+
function malformed(message) {
|
|
3627
|
+
return new QoderLlmError(message, "MALFORMED_RESPONSE");
|
|
3628
|
+
}
|
|
3629
|
+
function decodeLegacyFrame(rawData, options) {
|
|
3630
|
+
const outerControlKind = qoderControlFrameKind(rawData);
|
|
3631
|
+
if (outerControlKind !== void 0) {
|
|
3632
|
+
options.onControlFrame?.(outerControlKind);
|
|
3633
|
+
return outerControlKind === "done" ? "[DONE]" : void 0;
|
|
3634
|
+
}
|
|
3635
|
+
let value;
|
|
3636
|
+
try {
|
|
3637
|
+
value = JSON.parse(rawData);
|
|
3638
|
+
} catch {
|
|
3639
|
+
throw malformed("Malformed outer SSE JSON payload received from Qoder.");
|
|
3640
|
+
}
|
|
3641
|
+
if (value === null || typeof value !== "object") throw malformed("Malformed outer SSE envelope received from Qoder.");
|
|
3642
|
+
const envelope = value;
|
|
3643
|
+
if (envelope.statusCodeValue !== void 0 && (typeof envelope.statusCodeValue !== "number" || !Number.isInteger(envelope.statusCodeValue) || envelope.statusCodeValue < 100 || envelope.statusCodeValue > 599)) throw malformed("Qoder SSE envelope has an invalid status code.");
|
|
3644
|
+
if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) {
|
|
3645
|
+
const queued = options.queueRecovery ? queuedError(envelope.body ?? envelope) : void 0;
|
|
3646
|
+
if (queued) throw queued;
|
|
3647
|
+
throw qoderModelError(`Qoder service returned upstream error status ${envelope.statusCodeValue}`, {
|
|
3648
|
+
status: envelope.statusCodeValue,
|
|
3649
|
+
cause: envelope.body ?? envelope.message,
|
|
3650
|
+
source: "sse",
|
|
3651
|
+
headers: options.headers,
|
|
3652
|
+
httpStatus: options.httpStatus
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
|
|
3656
|
+
const body = typeof envelope.body === "string" ? envelope.body.trim() : "";
|
|
3657
|
+
if (!body) return void 0;
|
|
3658
|
+
const innerControlKind = qoderControlFrameKind(body);
|
|
3659
|
+
if (innerControlKind !== void 0) {
|
|
3660
|
+
if (innerControlKind !== "done") options.onControlFrame?.(innerControlKind);
|
|
3661
|
+
return innerControlKind === "done" ? "[DONE]" : void 0;
|
|
3662
|
+
}
|
|
3663
|
+
try {
|
|
3664
|
+
const inner = JSON.parse(body);
|
|
3665
|
+
if (inner === null || typeof inner !== "object") throw new Error("not an object");
|
|
3666
|
+
return inner;
|
|
3667
|
+
} catch {
|
|
3668
|
+
throw malformed("Malformed inner model payload received from Qoder.");
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
function parseQoderSse(stream, options = {}) {
|
|
3672
|
+
return parseQoderChunks(stream, decodeLegacyFrame, options);
|
|
3673
|
+
}
|
|
3674
|
+
//#endregion
|
|
2832
3675
|
//#region src/qoder/transport/chat.ts
|
|
2833
|
-
/** Qoder chat request encoding, timeout lifecycle, and SSE streaming. */
|
|
3676
|
+
/** Qoder chat request encoding, queue recovery, timeout lifecycle, and SSE streaming. */
|
|
2834
3677
|
function aborted$1(message) {
|
|
2835
3678
|
return new QoderLlmError(message, "ABORTED");
|
|
2836
3679
|
}
|
|
2837
3680
|
async function* streamQoderChat(options, model, credentials, messages, dependencies) {
|
|
2838
|
-
const
|
|
2839
|
-
const
|
|
2840
|
-
|
|
3681
|
+
const identityScope = options.purpose === void 0 && dependencies.hostRunId !== void 0 ? JSON.stringify([dependencies.region, dependencies.hostRunId]) : dependencies.region;
|
|
3682
|
+
const request = await buildQoderRequestBody(options, credentials.userID, messages, model, void 0, void 0, identityScope);
|
|
3683
|
+
request.session_type = dependencies.region === "china" ? "qoderclicn" : "qodercli";
|
|
3684
|
+
const startedAt = performance.now();
|
|
3685
|
+
let activeCredentials = credentials;
|
|
3686
|
+
const resolveCredentials = async (refresh, signal) => {
|
|
3687
|
+
const next = dependencies.resolveCredentials ? await dependencies.resolveCredentials(refresh, signal) : refresh && dependencies.refreshCredentials ? await dependencies.refreshCredentials(activeCredentials, signal) : activeCredentials;
|
|
3688
|
+
if (next.userID !== credentials.userID) throw queueFailure("Qoder subscriber changed during queue recovery.", "QODER_QUEUE_IDENTITY");
|
|
3689
|
+
activeCredentials = next;
|
|
3690
|
+
return next;
|
|
3691
|
+
};
|
|
3692
|
+
const queue = new ModelQueue({
|
|
3693
|
+
region: dependencies.region,
|
|
3694
|
+
fetch: dependencies.fetch,
|
|
3695
|
+
subscriberId: credentials.userID,
|
|
3696
|
+
requestSetId: request.request_set_id,
|
|
3697
|
+
sessionId: request.session_id,
|
|
3698
|
+
dshSessionId: options.purpose === void 0 && options.sessionId !== void 0 ? String(options.sessionId) : void 0,
|
|
3699
|
+
runStartedAt: request.business.begin_at,
|
|
3700
|
+
model: request.model_config.key,
|
|
3701
|
+
modelName: model?.name,
|
|
3702
|
+
signal: options.signal,
|
|
3703
|
+
maxWaitMs: dependencies.modelQueueMaxWaitMs,
|
|
3704
|
+
credentials: resolveCredentials,
|
|
3705
|
+
onUpdate: dependencies.onQueueUpdate,
|
|
3706
|
+
onLease: dependencies.onQueueLease,
|
|
3707
|
+
logger: dependencies.logger,
|
|
3708
|
+
clock: dependencies.queueClock
|
|
3709
|
+
});
|
|
3710
|
+
let sawOutput = false;
|
|
3711
|
+
const clear = () => queue.clear();
|
|
3712
|
+
options.signal?.addEventListener("abort", clear, { once: true });
|
|
3713
|
+
try {
|
|
3714
|
+
for (;;) {
|
|
3715
|
+
if (options.signal?.aborted) throw aborted$1("Request was aborted.");
|
|
3716
|
+
try {
|
|
3717
|
+
for await (const chunk of streamAttempt(options, model, activeCredentials, request, dependencies)) {
|
|
3718
|
+
if (chunk.type !== "usage" && chunk.type !== "finish") sawOutput = true;
|
|
3719
|
+
yield chunk;
|
|
3720
|
+
}
|
|
3721
|
+
return;
|
|
3722
|
+
} catch (error) {
|
|
3723
|
+
if (!(error instanceof QoderModelQueuedError)) throw error;
|
|
3724
|
+
queue.entered(error.queue);
|
|
3725
|
+
if (sawOutput) throw queueFailure("Qoder queued a request after output began; replay was stopped.", "QODER_QUEUE_AFTER_OUTPUT");
|
|
3726
|
+
await queue.wait(error.queue, request.request_id);
|
|
3727
|
+
await resolveCredentials(false, options.signal ?? new AbortController().signal);
|
|
3728
|
+
request.request_id = randomUUID();
|
|
3729
|
+
request.chat_record_id = request.request_id;
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
} finally {
|
|
3733
|
+
options.signal?.removeEventListener("abort", clear);
|
|
3734
|
+
await queue.finish(performance.now() - startedAt);
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
async function* streamAttempt(options, model, credentials, request, dependencies) {
|
|
3738
|
+
const encodedBytes = qoderEncodeBodyBytes(JSON.stringify(request));
|
|
2841
3739
|
const chatUrl = getQoderChatUrl(dependencies.region);
|
|
2842
3740
|
const requestController = new AbortController();
|
|
2843
3741
|
let headerTimedOut = false;
|
|
@@ -2849,10 +3747,7 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2849
3747
|
let lastActivityAt = startedAt;
|
|
2850
3748
|
const onCallerAbort = () => requestController.abort(options.signal?.reason);
|
|
2851
3749
|
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
2852
|
-
|
|
2853
|
-
headerTimedOut = true;
|
|
2854
|
-
requestController.abort("response header timeout");
|
|
2855
|
-
}, dependencies.responseHeaderTimeoutMs);
|
|
3750
|
+
let headerTimer;
|
|
2856
3751
|
const resetIdleTimer = () => {
|
|
2857
3752
|
lastActivityAt = performance.now();
|
|
2858
3753
|
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
@@ -2872,69 +3767,118 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2872
3767
|
contextTier: markedTiers.length === 1 ? markedTiers[0][0] : null,
|
|
2873
3768
|
context_length: request.parameters.context_length ?? null
|
|
2874
3769
|
}));
|
|
2875
|
-
const
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3770
|
+
const compression = new QoderRequestCompression();
|
|
3771
|
+
let response;
|
|
3772
|
+
let activeCredentials = credentials;
|
|
3773
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3774
|
+
if (options.signal?.aborted) throw aborted$1("Request was aborted.");
|
|
3775
|
+
response = await compression.send({
|
|
3776
|
+
url: chatUrl,
|
|
3777
|
+
protocol: "legacy",
|
|
3778
|
+
body: encodedBytes.toString("latin1"),
|
|
3779
|
+
enabled: dependencies.requestCompression,
|
|
3780
|
+
signal: requestController.signal,
|
|
3781
|
+
logger: dependencies.logger,
|
|
3782
|
+
requestId: request.request_id,
|
|
3783
|
+
dispatch: async (wireBody, headers) => {
|
|
3784
|
+
headerTimer = setTimeout(() => {
|
|
3785
|
+
headerTimedOut = true;
|
|
3786
|
+
requestController.abort("response header timeout");
|
|
3787
|
+
}, dependencies.responseHeaderTimeoutMs);
|
|
3788
|
+
try {
|
|
3789
|
+
return await dependencies.fetch(chatUrl, {
|
|
3790
|
+
method: "POST",
|
|
3791
|
+
headers,
|
|
3792
|
+
body: wireBody,
|
|
3793
|
+
signal: requestController.signal
|
|
3794
|
+
});
|
|
3795
|
+
} finally {
|
|
3796
|
+
clearTimeout(headerTimer);
|
|
3797
|
+
}
|
|
3798
|
+
},
|
|
3799
|
+
headers: {
|
|
3800
|
+
"content-type": "application/json",
|
|
3801
|
+
"accept": "text/event-stream",
|
|
3802
|
+
"cache-control": "no-cache",
|
|
3803
|
+
"connection": "keep-alive",
|
|
3804
|
+
"accept-encoding": "identity",
|
|
3805
|
+
"x-model-key": options.model || "cmodel",
|
|
3806
|
+
"x-model-source": model?.source || "system",
|
|
3807
|
+
...attributionHeaders(),
|
|
3808
|
+
...buildAuthHeaders(encodedBytes, chatUrl, activeCredentials),
|
|
3809
|
+
"X-Request-Id": request.request_id,
|
|
3810
|
+
"X-Session-ID": request.session_id
|
|
3811
|
+
}
|
|
3812
|
+
});
|
|
3813
|
+
clearTimeout(headerTimer);
|
|
3814
|
+
reqId = qoderRequestId(response.headers);
|
|
3815
|
+
dependencies.logger?.debug?.("[Qoder Stream] Response headers received", {
|
|
3816
|
+
region: dependencies.region,
|
|
3817
|
+
status: response.status,
|
|
3818
|
+
attempt: attempt + 1,
|
|
3819
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
3820
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
3821
|
+
});
|
|
3822
|
+
resetIdleTimer();
|
|
3823
|
+
if (response.ok) break;
|
|
2900
3824
|
let bodyText = "";
|
|
2901
3825
|
try {
|
|
2902
|
-
bodyText = (await response
|
|
2903
|
-
} catch {
|
|
2904
|
-
|
|
2905
|
-
if (bodyText) try {
|
|
2906
|
-
const parsed = JSON.parse(bodyText);
|
|
2907
|
-
const msg = typeof parsed.message === "string" && parsed.message.trim() ? parsed.message.trim() : void 0;
|
|
2908
|
-
const code = typeof parsed.code === "string" && parsed.code.trim() ? parsed.code.trim() : void 0;
|
|
2909
|
-
if (code && msg) detail = `: ${code}: ${msg}`;
|
|
2910
|
-
else if (msg) detail = `: ${msg}`;
|
|
2911
|
-
else detail = `: ${bodyText.slice(0, 300)}`;
|
|
2912
|
-
} catch {
|
|
2913
|
-
detail = `: ${bodyText.slice(0, 300)}`;
|
|
3826
|
+
bodyText = (await readLimitedText(response, defaultMaxErrorBytes, "Qoder model error response")).trim();
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
if (requestController.signal.aborted) throw error;
|
|
2914
3829
|
}
|
|
2915
|
-
|
|
3830
|
+
const queued = queuedError(bodyText);
|
|
3831
|
+
if (queued) throw queued;
|
|
3832
|
+
const failure = qoderModelError(`Qoder upstream service returned HTTP ${response.status}`, {
|
|
2916
3833
|
status: response.status,
|
|
2917
3834
|
headers: response.headers,
|
|
2918
|
-
cause: bodyText || void 0
|
|
3835
|
+
cause: bodyText || void 0,
|
|
3836
|
+
source: "http"
|
|
2919
3837
|
});
|
|
3838
|
+
if (attempt === 0 && dependencies.refreshCredentials && canRefreshModelCredentials(failure)) {
|
|
3839
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
3840
|
+
activeCredentials = await dependencies.refreshCredentials(activeCredentials, options.signal);
|
|
3841
|
+
if (activeCredentials.userID !== credentials.userID) throw new QoderLlmError("Qoder subscriber identity changed during authentication recovery.", "AUTH");
|
|
3842
|
+
continue;
|
|
3843
|
+
}
|
|
3844
|
+
throw failure;
|
|
2920
3845
|
}
|
|
3846
|
+
if (!response) throw new QoderLlmError("Qoder response is missing.", "EMPTY_RESPONSE");
|
|
2921
3847
|
if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
|
|
2922
3848
|
let firstChunkDurationMs;
|
|
2923
3849
|
let tokenUsage;
|
|
2924
3850
|
let finishReason;
|
|
2925
3851
|
const streamStartedAt = performance.now();
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3852
|
+
try {
|
|
3853
|
+
for await (const chunk of parseQoderSse(response.body, {
|
|
3854
|
+
queueRecovery: true,
|
|
3855
|
+
onActivity: resetIdleTimer,
|
|
3856
|
+
headers: response.headers,
|
|
3857
|
+
httpStatus: response.status,
|
|
3858
|
+
onControlFrame: (kind) => {
|
|
3859
|
+
if (kind !== "done") dependencies.logger?.warn?.("[Qoder Stream] Control frame skipped", {
|
|
3860
|
+
transport: "legacy",
|
|
3861
|
+
kind,
|
|
3862
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3865
|
+
})) {
|
|
3866
|
+
compression.confirm();
|
|
3867
|
+
chunkCount++;
|
|
3868
|
+
if (firstChunkDurationMs === void 0) {
|
|
3869
|
+
firstChunkDurationMs = Math.round(performance.now() - startedAt);
|
|
3870
|
+
dependencies.logger?.debug?.("[Qoder Stream] First chunk received", {
|
|
3871
|
+
durationMs: firstChunkDurationMs,
|
|
3872
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
if (chunk.type === "usage") tokenUsage = chunk.usage;
|
|
3876
|
+
if (chunk.type === "finish") finishReason = chunk.reason.kind;
|
|
3877
|
+
yield chunk;
|
|
2934
3878
|
}
|
|
2935
|
-
|
|
2936
|
-
if (
|
|
2937
|
-
|
|
3879
|
+
} catch (error) {
|
|
3880
|
+
if (chunkCount === 0) compression.rejectLegacyError(error);
|
|
3881
|
+
throw error;
|
|
2938
3882
|
}
|
|
2939
3883
|
dependencies.logger?.debug?.("[Qoder Stream] Stream completed", {
|
|
2940
3884
|
durationMs: Math.round(performance.now() - startedAt),
|
|
@@ -2967,6 +3911,7 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2967
3911
|
});
|
|
2968
3912
|
throw failure;
|
|
2969
3913
|
}
|
|
3914
|
+
if (error instanceof QoderModelQueuedError) throw error;
|
|
2970
3915
|
if (error instanceof QoderLlmError) {
|
|
2971
3916
|
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(error), {
|
|
2972
3917
|
chunkCount,
|
|
@@ -2991,13 +3936,399 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2991
3936
|
}
|
|
2992
3937
|
}
|
|
2993
3938
|
//#endregion
|
|
3939
|
+
//#region src/qoder/transport/browser-authorization.ts
|
|
3940
|
+
/** qodercli 1.1.73 PKCE device flow. No website password or agent runtime is needed. */
|
|
3941
|
+
var QoderBrowserAuthorization = class {
|
|
3942
|
+
options;
|
|
3943
|
+
constructor(options) {
|
|
3944
|
+
this.options = options;
|
|
3945
|
+
}
|
|
3946
|
+
async begin(signal) {
|
|
3947
|
+
signal.throwIfAborted();
|
|
3948
|
+
const verifier = randomBytes(48).toString("base64url");
|
|
3949
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
3950
|
+
const nonce = randomUUID();
|
|
3951
|
+
const machineID = (this.options.resolveMachineId ?? (() => getMachineId(qoderMachineIdPaths(this.options.region))))();
|
|
3952
|
+
const query = new URLSearchParams({
|
|
3953
|
+
challenge,
|
|
3954
|
+
challenge_method: "S256",
|
|
3955
|
+
nonce,
|
|
3956
|
+
machine_id: machineID,
|
|
3957
|
+
client_id: "e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb"
|
|
3958
|
+
});
|
|
3959
|
+
const origin = this.options.region === "china" ? "https://qoder.cn" : "https://qoder.com";
|
|
3960
|
+
const expiresAt = Date.now() + (this.options.loginTimeoutMs ?? 3e5);
|
|
3961
|
+
const deadline = AbortSignal.timeout(Math.max(1, expiresAt - Date.now()));
|
|
3962
|
+
const combined = AbortSignal.any([signal, deadline]);
|
|
3963
|
+
return {
|
|
3964
|
+
url: `${origin}/device/selectAccounts?${query}`,
|
|
3965
|
+
expiresAt,
|
|
3966
|
+
complete: async () => {
|
|
3967
|
+
try {
|
|
3968
|
+
const poll = new URL(`${resolveQoderEndpoints(this.options.region).openApiUrl}/api/v1/deviceToken/poll`);
|
|
3969
|
+
poll.search = new URLSearchParams({
|
|
3970
|
+
nonce,
|
|
3971
|
+
verifier,
|
|
3972
|
+
challenge_method: "S256"
|
|
3973
|
+
}).toString();
|
|
3974
|
+
while (!combined.aborted) {
|
|
3975
|
+
let response;
|
|
3976
|
+
try {
|
|
3977
|
+
response = await (this.options.fetch ?? fetch)(poll, {
|
|
3978
|
+
headers: {
|
|
3979
|
+
accept: "application/json",
|
|
3980
|
+
"user-agent": defaultUserAgent
|
|
3981
|
+
},
|
|
3982
|
+
signal: AbortSignal.any([combined, AbortSignal.timeout(15e3)])
|
|
3983
|
+
});
|
|
3984
|
+
} catch {
|
|
3985
|
+
if (combined.aborted) break;
|
|
3986
|
+
await setTimeout$1(this.options.pollIntervalMs ?? 1e3, void 0, { signal: combined });
|
|
3987
|
+
continue;
|
|
3988
|
+
}
|
|
3989
|
+
if (response.status === 404 || response.status >= 500 || response.status === 429) {
|
|
3990
|
+
await response.body?.cancel();
|
|
3991
|
+
await setTimeout$1(this.options.pollIntervalMs ?? 1e3, void 0, { signal: combined });
|
|
3992
|
+
continue;
|
|
3993
|
+
}
|
|
3994
|
+
if (!response.ok) {
|
|
3995
|
+
await response.body?.cancel();
|
|
3996
|
+
throw new QoderLlmError("Qoder browser authorization was rejected.", "AUTH");
|
|
3997
|
+
}
|
|
3998
|
+
const raw = JSON.parse(await readLimitedText(response, defaultMaxJsonBytes, "Qoder browser authorization"));
|
|
3999
|
+
return await this.grant(raw, machineID, combined);
|
|
4000
|
+
}
|
|
4001
|
+
throw new QoderLlmError("Qoder browser login timed out.", "TIMEOUT");
|
|
4002
|
+
} catch (error) {
|
|
4003
|
+
if (signal.aborted) throw new QoderLlmError("Qoder browser login was cancelled.", "ABORTED");
|
|
4004
|
+
if (deadline.aborted) throw new QoderLlmError("Qoder browser login timed out.", "TIMEOUT");
|
|
4005
|
+
if (error instanceof QoderLlmError) throw error;
|
|
4006
|
+
throw new QoderLlmError("Qoder browser authorization returned an invalid response.", "MALFORMED_RESPONSE");
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
};
|
|
4010
|
+
}
|
|
4011
|
+
async refresh(grant, signal, rejectedToken) {
|
|
4012
|
+
if (grant.region !== this.options.region) throw new QoderLlmError("Qoder authorization region mismatch.", "AUTH");
|
|
4013
|
+
if (!(rejectedToken !== void 0 && rejectedToken === grant.credentials.authToken) && grant.expiresAt > Date.now() + 3e5) return grant;
|
|
4014
|
+
if (grant.refreshExpiresAt !== void 0 && grant.refreshExpiresAt <= Date.now()) throw new QoderLlmError("Qoder browser authorization expired. Sign in again.", "AUTH");
|
|
4015
|
+
const raw = await openApiJsonRequest(this.options.fetch ?? fetch, {
|
|
4016
|
+
url: `${resolveQoderEndpoints(this.options.region).openApiUrl}/api/v1/deviceToken/refresh`,
|
|
4017
|
+
body: {
|
|
4018
|
+
refresh_token: grant.refreshToken,
|
|
4019
|
+
machine_id: grant.credentials.machineID
|
|
4020
|
+
},
|
|
4021
|
+
signal,
|
|
4022
|
+
operation: "BrowserRefresh"
|
|
4023
|
+
});
|
|
4024
|
+
const token = raw.device_token ?? raw.token;
|
|
4025
|
+
if (!token || typeof token !== "string" || !raw.refresh_token || typeof raw.refresh_token !== "string") throw new QoderLlmError("Qoder refresh returned incomplete credentials.", "MALFORMED_RESPONSE");
|
|
4026
|
+
return {
|
|
4027
|
+
...grant,
|
|
4028
|
+
credentials: {
|
|
4029
|
+
...grant.credentials,
|
|
4030
|
+
authToken: token
|
|
4031
|
+
},
|
|
4032
|
+
refreshToken: raw.refresh_token,
|
|
4033
|
+
expiresAt: deviceExpiry(raw.expires_at, raw.expires_in) ?? Date.now() + 864e5,
|
|
4034
|
+
refreshExpiresAt: refreshExpiry(raw) ?? grant.refreshExpiresAt
|
|
4035
|
+
};
|
|
4036
|
+
}
|
|
4037
|
+
async grant(raw, machineID, signal) {
|
|
4038
|
+
if (!raw.token || typeof raw.token !== "string" || !raw.refresh_token || typeof raw.refresh_token !== "string") throw new QoderLlmError("Qoder browser authorization returned incomplete credentials.", "MALFORMED_RESPONSE");
|
|
4039
|
+
const credentials = await new QoderAuthManager({
|
|
4040
|
+
region: this.options.region,
|
|
4041
|
+
fetch: this.options.fetch
|
|
4042
|
+
}).credentialsForToken(raw.token, machineID, signal);
|
|
4043
|
+
return {
|
|
4044
|
+
id: randomUUID(),
|
|
4045
|
+
region: this.options.region,
|
|
4046
|
+
credentials,
|
|
4047
|
+
refreshToken: raw.refresh_token,
|
|
4048
|
+
expiresAt: deviceExpiry(raw.expires_at, raw.expires_in) ?? Date.now() + 864e5,
|
|
4049
|
+
refreshExpiresAt: refreshExpiry(raw)
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
};
|
|
4053
|
+
function refreshExpiry(raw) {
|
|
4054
|
+
return deviceExpiry(raw.refresh_token_expires_at, raw.refresh_token_expires_in);
|
|
4055
|
+
}
|
|
4056
|
+
/** Device-flow durations are seconds, including multi-day refresh-token lifetimes. */
|
|
4057
|
+
function deviceExpiry(absolute, seconds) {
|
|
4058
|
+
if (absolute !== void 0) {
|
|
4059
|
+
const parsed = typeof absolute === "string" ? Date.parse(absolute) : absolute > 0xe8d4a51000 ? absolute : absolute * 1e3;
|
|
4060
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
4061
|
+
}
|
|
4062
|
+
if (typeof seconds === "number" && Number.isFinite(seconds * 1e3) && seconds > 0) return Date.now() + seconds * 1e3;
|
|
4063
|
+
}
|
|
4064
|
+
//#endregion
|
|
4065
|
+
//#region src/qoder/transport/wire/model-server-sse.ts
|
|
4066
|
+
function malformedFrameDetails(rawData, eventType, error) {
|
|
4067
|
+
const marker = /^\[[A-Z][A-Z_]{0,63}\]/u.exec(rawData)?.[0];
|
|
4068
|
+
const safeEventType = eventType && /^[a-z][\w.-]{0,40}$/iu.test(eventType) ? eventType : void 0;
|
|
4069
|
+
const message = error instanceof SyntaxError ? error.message : "";
|
|
4070
|
+
const offset = /\bposition (\d+)\b/u.exec(message)?.[1];
|
|
4071
|
+
const first = rawData.charAt(0);
|
|
4072
|
+
const last = rawData.charAt(rawData.length - 1);
|
|
4073
|
+
return {
|
|
4074
|
+
...safeEventType === void 0 ? {} : { eventType: safeEventType },
|
|
4075
|
+
...marker === void 0 ? {} : { marker },
|
|
4076
|
+
chars: rawData.length,
|
|
4077
|
+
lines: rawData.split("\n").length,
|
|
4078
|
+
shape: rawData.startsWith("data:") ? "nested-data" : first === "{" ? "object" : first === "[" ? "array" : first === "\"" ? "string" : "other",
|
|
4079
|
+
endsWith: last === "}" ? "object" : last === "]" ? "array" : last === "\"" ? "string" : "other",
|
|
4080
|
+
syntax: /end of JSON input|Unterminated string/u.test(message) || offset === String(rawData.length) ? "truncated" : /non-whitespace character after JSON/u.test(message) ? "trailing-data" : /control character/u.test(message) ? "invalid-control" : "invalid-json",
|
|
4081
|
+
...offset === void 0 ? {} : { syntaxOffset: Number(offset) },
|
|
4082
|
+
...rawData.includes("\r") ? { hasCarriageReturn: true } : {},
|
|
4083
|
+
...rawData.includes("�") ? { hasReplacementCharacter: true } : {}
|
|
4084
|
+
};
|
|
4085
|
+
}
|
|
4086
|
+
function decodeModelServerFrame(rawData, options, eventType) {
|
|
4087
|
+
const controlKind = qoderControlFrameKind(rawData);
|
|
4088
|
+
if (controlKind !== void 0) {
|
|
4089
|
+
options.onControlFrame?.(controlKind);
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
let value;
|
|
4093
|
+
try {
|
|
4094
|
+
value = JSON.parse(rawData);
|
|
4095
|
+
} catch (error) {
|
|
4096
|
+
const details = malformedFrameDetails(rawData, eventType, error);
|
|
4097
|
+
options.onMalformedFrame?.(details);
|
|
4098
|
+
if (eventType === "error") throw qoderModelServerError(options.httpStatus ?? 200, { error: { message: "Qoder model-server emitted a non-JSON error SSE event." } }, "sse", options.headers);
|
|
4099
|
+
throw new QoderMalformedJsonFrameError(details.syntax === "truncated", /^\{\s*"choices"\s*:\s*\[\s*\]\s*[,}]/u.test(rawData));
|
|
4100
|
+
}
|
|
4101
|
+
if (eventType === "error") throw qoderModelServerError(options.httpStatus ?? 200, value, "sse", options.headers);
|
|
4102
|
+
if (value === null || typeof value !== "object") throw new QoderLlmError("Malformed model-server SSE frame.", "MALFORMED_RESPONSE");
|
|
4103
|
+
const payload = value;
|
|
4104
|
+
if (payload.error !== void 0) throw qoderModelServerError(options.httpStatus ?? 200, payload, "sse", options.headers);
|
|
4105
|
+
return payload;
|
|
4106
|
+
}
|
|
4107
|
+
function parseModelServerSse(stream, options = {}) {
|
|
4108
|
+
return parseQoderChunks(stream, decodeModelServerFrame, options, {
|
|
4109
|
+
ignoreMalformedAfterFinish: true,
|
|
4110
|
+
allowIncompleteAuxiliaryFrames: true,
|
|
4111
|
+
recoverUnprefixedContinuations: true
|
|
4112
|
+
});
|
|
4113
|
+
}
|
|
4114
|
+
//#endregion
|
|
4115
|
+
//#region src/qoder/transport/model-server.ts
|
|
4116
|
+
/** Bearer-authenticated model-server HTTP SSE transport. */
|
|
4117
|
+
/** Convert the shared Qoder turn into the model-server's OpenAI-shaped request. */
|
|
4118
|
+
function modelServerBody(request) {
|
|
4119
|
+
return {
|
|
4120
|
+
model: request.modelKey,
|
|
4121
|
+
messages: request.messages,
|
|
4122
|
+
stream: true,
|
|
4123
|
+
stream_options: { include_usage: true },
|
|
4124
|
+
metadata: {
|
|
4125
|
+
context: {
|
|
4126
|
+
request_id: request.requestId,
|
|
4127
|
+
request_set_id: request.requestSetId,
|
|
4128
|
+
session_id: request.sessionId,
|
|
4129
|
+
task_id: request.taskId,
|
|
4130
|
+
client_type: "5"
|
|
4131
|
+
},
|
|
4132
|
+
business: request.business
|
|
4133
|
+
},
|
|
4134
|
+
...request.tools.length > 0 ? { tools: request.tools } : {},
|
|
4135
|
+
max_tokens: request.maxTokens,
|
|
4136
|
+
...request.contextLength === void 0 ? {} : { context_length: request.contextLength },
|
|
4137
|
+
...request.reasoningEffort === void 0 ? {} : { reasoning_effort: request.reasoningEffort }
|
|
4138
|
+
};
|
|
4139
|
+
}
|
|
4140
|
+
async function* streamModelServerChat(options, model, credentials, messages, dependencies) {
|
|
4141
|
+
const identityScope = options.purpose === void 0 && dependencies.hostRunId !== void 0 ? JSON.stringify([dependencies.region, dependencies.hostRunId]) : dependencies.region;
|
|
4142
|
+
const request = await prepareQoderRequest(options, credentials.userID, messages, model, void 0, void 0, identityScope);
|
|
4143
|
+
const body = JSON.stringify(modelServerBody(request));
|
|
4144
|
+
const startedAt = performance.now();
|
|
4145
|
+
dependencies.logger?.debug?.("[Qoder Stream] Request context", {
|
|
4146
|
+
requestId: request.requestId,
|
|
4147
|
+
model: request.modelKey,
|
|
4148
|
+
region: dependencies.region,
|
|
4149
|
+
purpose: options.purpose ?? "chat",
|
|
4150
|
+
transport: "http"
|
|
4151
|
+
});
|
|
4152
|
+
const controller = new AbortController();
|
|
4153
|
+
const onAbort = () => controller.abort(options.signal?.reason);
|
|
4154
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
4155
|
+
if (options.signal?.aborted) onAbort();
|
|
4156
|
+
let headerTimedOut = false;
|
|
4157
|
+
let idleTimedOut = false;
|
|
4158
|
+
let headerTimer;
|
|
4159
|
+
let idleTimer;
|
|
4160
|
+
const clearTimers = () => {
|
|
4161
|
+
if (headerTimer) clearTimeout(headerTimer);
|
|
4162
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
4163
|
+
};
|
|
4164
|
+
const resetIdle = () => {
|
|
4165
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
4166
|
+
idleTimer = setTimeout(() => {
|
|
4167
|
+
idleTimedOut = true;
|
|
4168
|
+
controller.abort("stream idle timeout");
|
|
4169
|
+
}, dependencies.streamIdleTimeoutMs);
|
|
4170
|
+
};
|
|
4171
|
+
try {
|
|
4172
|
+
const compression = new QoderRequestCompression();
|
|
4173
|
+
let activeCredentials = credentials;
|
|
4174
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4175
|
+
if (controller.signal.aborted) throw new QoderLlmError("Qoder model request was aborted.", "ABORTED");
|
|
4176
|
+
let response;
|
|
4177
|
+
try {
|
|
4178
|
+
response = await compression.send({
|
|
4179
|
+
url: getQoderModelServerUrl(dependencies.region),
|
|
4180
|
+
protocol: "http",
|
|
4181
|
+
body,
|
|
4182
|
+
enabled: dependencies.requestCompression,
|
|
4183
|
+
signal: controller.signal,
|
|
4184
|
+
logger: dependencies.logger,
|
|
4185
|
+
requestId: request.requestId,
|
|
4186
|
+
onRejection: async (rejection) => {
|
|
4187
|
+
dependencies.logger?.warn?.("[Qoder Stream] Compression rejected", {
|
|
4188
|
+
requestId: request.requestId,
|
|
4189
|
+
status: rejection.status,
|
|
4190
|
+
contentType: rejection.contentType,
|
|
4191
|
+
serverRequestId: rejection.serverRequestId,
|
|
4192
|
+
bodyTruncated: rejection.bodyTruncated,
|
|
4193
|
+
reason: rejection.reason,
|
|
4194
|
+
request: compression.diagnostics
|
|
4195
|
+
});
|
|
4196
|
+
},
|
|
4197
|
+
dispatch: async (wireBody, headers) => {
|
|
4198
|
+
headerTimer = setTimeout(() => {
|
|
4199
|
+
headerTimedOut = true;
|
|
4200
|
+
controller.abort("response header timeout");
|
|
4201
|
+
}, dependencies.responseHeaderTimeoutMs);
|
|
4202
|
+
try {
|
|
4203
|
+
return await dependencies.fetch(getQoderModelServerUrl(dependencies.region), {
|
|
4204
|
+
method: "POST",
|
|
4205
|
+
headers,
|
|
4206
|
+
body: wireBody,
|
|
4207
|
+
signal: controller.signal
|
|
4208
|
+
});
|
|
4209
|
+
} finally {
|
|
4210
|
+
if (headerTimer) clearTimeout(headerTimer);
|
|
4211
|
+
}
|
|
4212
|
+
},
|
|
4213
|
+
headers: {
|
|
4214
|
+
...attributionHeaders(),
|
|
4215
|
+
...qoderModelClientHeaders(),
|
|
4216
|
+
"Accept": "text/event-stream",
|
|
4217
|
+
"Content-Type": "application/json",
|
|
4218
|
+
"Authorization": `Bearer ${activeCredentials.authToken}`,
|
|
4219
|
+
"X-Request-ID": request.requestId,
|
|
4220
|
+
"X-Session-ID": request.sessionId
|
|
4221
|
+
}
|
|
4222
|
+
});
|
|
4223
|
+
} finally {
|
|
4224
|
+
if (headerTimer) clearTimeout(headerTimer);
|
|
4225
|
+
}
|
|
4226
|
+
const responseRequestId = qoderRequestId(response.headers);
|
|
4227
|
+
dependencies.logger?.debug?.("[Qoder Stream] Response headers received", {
|
|
4228
|
+
region: dependencies.region,
|
|
4229
|
+
transport: "http",
|
|
4230
|
+
status: response.status,
|
|
4231
|
+
contentType: response.headers.get("content-type"),
|
|
4232
|
+
contentEncoding: response.headers.get("content-encoding"),
|
|
4233
|
+
attempt: attempt + 1,
|
|
4234
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
4235
|
+
...responseRequestId === void 0 ? {} : { requestId: responseRequestId }
|
|
4236
|
+
});
|
|
4237
|
+
if (!response.ok) {
|
|
4238
|
+
let cause;
|
|
4239
|
+
resetIdle();
|
|
4240
|
+
try {
|
|
4241
|
+
cause = await readLimitedText(response, defaultMaxErrorBytes, "Qoder model-server error");
|
|
4242
|
+
} catch {
|
|
4243
|
+
cause = void 0;
|
|
4244
|
+
}
|
|
4245
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
4246
|
+
if (controller.signal.aborted) throw new QoderLlmError("Qoder model-server error body was interrupted.", "ABORTED");
|
|
4247
|
+
dependencies.logger?.warn?.("[Qoder Stream] HTTP request rejected", {
|
|
4248
|
+
requestId: request.requestId,
|
|
4249
|
+
status: response.status,
|
|
4250
|
+
contentType: response.headers.get("content-type"),
|
|
4251
|
+
serverRequestId: responseRequestId,
|
|
4252
|
+
request: compression.diagnostics,
|
|
4253
|
+
bodyAvailable: typeof cause === "string"
|
|
4254
|
+
});
|
|
4255
|
+
const error = qoderModelServerError(response.status, cause, "http", response.headers);
|
|
4256
|
+
if (attempt === 0 && canRefreshModelCredentials(error)) {
|
|
4257
|
+
activeCredentials = await dependencies.refreshCredentials(activeCredentials, options.signal);
|
|
4258
|
+
if (activeCredentials.userID !== credentials.userID) throw new QoderLlmError("Qoder subscriber identity changed during authentication recovery.", "AUTH");
|
|
4259
|
+
continue;
|
|
4260
|
+
}
|
|
4261
|
+
throw error;
|
|
4262
|
+
}
|
|
4263
|
+
if (!response.body) throw new QoderLlmError("Qoder model-server response has no body.", "EMPTY_RESPONSE");
|
|
4264
|
+
resetIdle();
|
|
4265
|
+
const streamStartedAt = performance.now();
|
|
4266
|
+
let chunkCount = 0;
|
|
4267
|
+
let firstChunkReceived = false;
|
|
4268
|
+
let finishReason;
|
|
4269
|
+
for await (const chunk of parseModelServerSse(response.body, {
|
|
4270
|
+
headers: response.headers,
|
|
4271
|
+
httpStatus: response.status,
|
|
4272
|
+
onActivity: resetIdle,
|
|
4273
|
+
onControlFrame: (kind) => {
|
|
4274
|
+
if (kind !== "done") dependencies.logger?.warn?.("[Qoder Stream] Control frame skipped", {
|
|
4275
|
+
transport: "http",
|
|
4276
|
+
kind,
|
|
4277
|
+
...responseRequestId === void 0 ? {} : { requestId: responseRequestId }
|
|
4278
|
+
});
|
|
4279
|
+
},
|
|
4280
|
+
onMalformedFrame: (details) => dependencies.logger?.warn?.("[Qoder Stream] Malformed SSE frame", {
|
|
4281
|
+
transport: "http",
|
|
4282
|
+
...details,
|
|
4283
|
+
...responseRequestId === void 0 ? {} : { requestId: responseRequestId }
|
|
4284
|
+
})
|
|
4285
|
+
})) {
|
|
4286
|
+
compression.confirm();
|
|
4287
|
+
chunkCount++;
|
|
4288
|
+
if (!firstChunkReceived) {
|
|
4289
|
+
firstChunkReceived = true;
|
|
4290
|
+
dependencies.logger?.debug?.("[Qoder Stream] First chunk received", {
|
|
4291
|
+
transport: "http",
|
|
4292
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
4293
|
+
...responseRequestId === void 0 ? {} : { requestId: responseRequestId }
|
|
4294
|
+
});
|
|
4295
|
+
}
|
|
4296
|
+
if (chunk.type === "finish") finishReason = chunk.reason.kind;
|
|
4297
|
+
yield chunk;
|
|
4298
|
+
}
|
|
4299
|
+
dependencies.logger?.debug?.("[Qoder Stream] Stream completed", {
|
|
4300
|
+
transport: "http",
|
|
4301
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
4302
|
+
streamDurationMs: Math.round(performance.now() - streamStartedAt),
|
|
4303
|
+
chunkCount,
|
|
4304
|
+
...finishReason === void 0 ? {} : { finishReason },
|
|
4305
|
+
...responseRequestId === void 0 ? {} : { requestId: responseRequestId }
|
|
4306
|
+
});
|
|
4307
|
+
return;
|
|
4308
|
+
}
|
|
4309
|
+
} catch (error) {
|
|
4310
|
+
if (options.signal?.aborted) throw new QoderLlmError("Qoder model request was aborted.", "ABORTED");
|
|
4311
|
+
if (headerTimedOut) throw new QoderLlmError("Qoder model-server response header timeout.", "TIMEOUT");
|
|
4312
|
+
if (idleTimedOut) throw new QoderLlmError("Qoder model-server stream idle timeout.", "TIMEOUT");
|
|
4313
|
+
throw error;
|
|
4314
|
+
} finally {
|
|
4315
|
+
clearTimers();
|
|
4316
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
4317
|
+
controller.abort("stream finished");
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
//#endregion
|
|
2994
4321
|
//#region src/qoder/transport/default-transport.ts
|
|
2995
4322
|
const defaultStreamIdleTimeoutMs = 300 * 1e3;
|
|
2996
4323
|
function aborted(message) {
|
|
2997
4324
|
return new QoderLlmError(message, "ABORTED");
|
|
2998
4325
|
}
|
|
2999
4326
|
var DefaultQoderTransport = class {
|
|
4327
|
+
authorization;
|
|
4328
|
+
resolveCredential;
|
|
3000
4329
|
region;
|
|
4330
|
+
requestCompression;
|
|
4331
|
+
modelTransport;
|
|
3001
4332
|
resolvePat;
|
|
3002
4333
|
fetchImpl;
|
|
3003
4334
|
logger;
|
|
@@ -3009,19 +4340,34 @@ var DefaultQoderTransport = class {
|
|
|
3009
4340
|
modelFlights = new SingleFlight();
|
|
3010
4341
|
attachments;
|
|
3011
4342
|
imageUploader;
|
|
3012
|
-
|
|
4343
|
+
credentialLeases = /* @__PURE__ */ new WeakMap();
|
|
3013
4344
|
preserveThinking;
|
|
4345
|
+
modelQueueMaxWaitMs;
|
|
4346
|
+
onQueueUpdate;
|
|
4347
|
+
captureQueueLeaseOwner;
|
|
3014
4348
|
constructor(options) {
|
|
4349
|
+
this.modelQueueMaxWaitMs = options.modelQueueMaxWaitMs;
|
|
4350
|
+
this.onQueueUpdate = options.onQueueUpdate;
|
|
4351
|
+
this.captureQueueLeaseOwner = options.captureQueueLeaseOwner;
|
|
3015
4352
|
this.region = options.region;
|
|
4353
|
+
this.requestCompression = options.requestCompression ?? false;
|
|
4354
|
+
this.modelTransport = options.modelTransport ?? "legacy";
|
|
3016
4355
|
this.resolvePat = options.resolvePat;
|
|
4356
|
+
this.resolveCredential = options.resolveCredential;
|
|
4357
|
+
this.authorization = new QoderBrowserAuthorization(options);
|
|
3017
4358
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
3018
4359
|
this.logger = options.logger;
|
|
4360
|
+
this.logger?.debug?.("[Qoder Transport] Request compression configuration", {
|
|
4361
|
+
implementation: "zstd-v1",
|
|
4362
|
+
node: process.version,
|
|
4363
|
+
requestCompression: this.requestCompression
|
|
4364
|
+
});
|
|
3019
4365
|
this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 3e5;
|
|
3020
4366
|
this.responseHeaderTimeoutMs = options.responseHeaderTimeoutMs ?? 6e4;
|
|
3021
4367
|
this.metadataTimeoutMs = options.metadataTimeoutMs;
|
|
3022
4368
|
this.attachments = options.attachments;
|
|
3023
4369
|
this.preserveThinking = options.preserveThinking;
|
|
3024
|
-
this.auth = new
|
|
4370
|
+
this.auth = new QoderAuthManager({
|
|
3025
4371
|
fetch: this.fetchImpl,
|
|
3026
4372
|
logger: this.logger,
|
|
3027
4373
|
region: this.region,
|
|
@@ -3040,24 +4386,10 @@ var DefaultQoderTransport = class {
|
|
|
3040
4386
|
region: this.region,
|
|
3041
4387
|
...options.imageUploadTimeoutMs === void 0 ? {} : { timeoutMs: options.imageUploadTimeoutMs },
|
|
3042
4388
|
...options.imageUrlCacheTtlMs === void 0 ? {} : { cacheTtlMs: options.imageUrlCacheTtlMs },
|
|
3043
|
-
refreshCredentials: async (signal) => {
|
|
3044
|
-
const
|
|
3045
|
-
|
|
3046
|
-
return
|
|
3047
|
-
}
|
|
3048
|
-
});
|
|
3049
|
-
this.searchClient = new QoderSearchClient({
|
|
3050
|
-
fetch: this.fetchImpl,
|
|
3051
|
-
logger: this.logger,
|
|
3052
|
-
region: this.region,
|
|
3053
|
-
resolveCredentials: async (signal) => {
|
|
3054
|
-
const pat = await this.requirePat(signal);
|
|
3055
|
-
return this.auth.getCredentials(pat, signal);
|
|
3056
|
-
},
|
|
3057
|
-
refreshCredentials: async (signal) => {
|
|
3058
|
-
const pat = await this.requirePat(signal);
|
|
3059
|
-
this.auth.clear(pat);
|
|
3060
|
-
return this.auth.getCredentials(pat, signal);
|
|
4389
|
+
refreshCredentials: async (rejected, signal) => {
|
|
4390
|
+
const lease = rejected && this.credentialLeases.get(rejected);
|
|
4391
|
+
if (!lease) throw aborted("Qoder image credentials are no longer available.");
|
|
4392
|
+
return lease.resolve(signal, rejected.authToken);
|
|
3061
4393
|
}
|
|
3062
4394
|
});
|
|
3063
4395
|
}
|
|
@@ -3065,28 +4397,54 @@ var DefaultQoderTransport = class {
|
|
|
3065
4397
|
return this.generate(options, model);
|
|
3066
4398
|
}
|
|
3067
4399
|
async discoverModels(signal) {
|
|
3068
|
-
const
|
|
3069
|
-
const key =
|
|
4400
|
+
const lease = await this.lease(signal);
|
|
4401
|
+
const key = lease.key;
|
|
3070
4402
|
return this.modelFlights.run(key, signal, async (sharedSignal) => {
|
|
3071
|
-
const credentials = await
|
|
3072
|
-
|
|
4403
|
+
const credentials = await lease.resolve(sharedSignal);
|
|
4404
|
+
const read = (resolved) => retryMetadataRead(sharedSignal, () => fetchQoderModels(resolved, {
|
|
3073
4405
|
fetch: this.fetchImpl,
|
|
3074
4406
|
signal: sharedSignal,
|
|
3075
4407
|
logger: this.logger,
|
|
3076
4408
|
region: this.region,
|
|
3077
4409
|
timeoutMs: this.metadataTimeoutMs
|
|
3078
4410
|
}));
|
|
4411
|
+
try {
|
|
4412
|
+
return await read(credentials);
|
|
4413
|
+
} catch (error) {
|
|
4414
|
+
if (!(error instanceof QoderLlmError) || error.code !== "AUTH") throw error;
|
|
4415
|
+
return read(await lease.resolve(sharedSignal, credentials.authToken));
|
|
4416
|
+
}
|
|
3079
4417
|
}, () => aborted("Qoder model discovery was aborted."));
|
|
3080
4418
|
}
|
|
3081
4419
|
async readAccount(options) {
|
|
3082
|
-
const
|
|
3083
|
-
|
|
3084
|
-
|
|
4420
|
+
const lease = await this.lease(options?.signal);
|
|
4421
|
+
let used;
|
|
4422
|
+
const read = (force = options?.force) => this.usage.readWithCredentials(lease.key, async (signal) => {
|
|
4423
|
+
used = await lease.resolve(signal);
|
|
4424
|
+
return used;
|
|
4425
|
+
}, {
|
|
4426
|
+
force,
|
|
3085
4427
|
signal: options?.signal
|
|
3086
4428
|
});
|
|
4429
|
+
try {
|
|
4430
|
+
return await read();
|
|
4431
|
+
} catch (error) {
|
|
4432
|
+
if (!(error instanceof QoderLlmError) || error.code !== "AUTH" || !used) throw error;
|
|
4433
|
+
await lease.resolve(options?.signal, used.authToken);
|
|
4434
|
+
return read(true);
|
|
4435
|
+
}
|
|
3087
4436
|
}
|
|
3088
4437
|
async searchWeb(request, signal) {
|
|
3089
|
-
|
|
4438
|
+
const lease = await this.lease(signal);
|
|
4439
|
+
return new QoderSearchClient({
|
|
4440
|
+
fetch: this.fetchImpl,
|
|
4441
|
+
logger: this.logger,
|
|
4442
|
+
region: this.region,
|
|
4443
|
+
resolveCredentials: async (nextSignal) => {
|
|
4444
|
+
return lease.resolve(nextSignal);
|
|
4445
|
+
},
|
|
4446
|
+
refreshCredentials: (rejected, nextSignal) => lease.resolve(nextSignal, rejected.authToken)
|
|
4447
|
+
}).search(request, signal);
|
|
3090
4448
|
}
|
|
3091
4449
|
async requirePat(signal) {
|
|
3092
4450
|
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
@@ -3095,22 +4453,51 @@ var DefaultQoderTransport = class {
|
|
|
3095
4453
|
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
3096
4454
|
return pat;
|
|
3097
4455
|
}
|
|
4456
|
+
async lease(signal) {
|
|
4457
|
+
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
4458
|
+
const managed = await this.resolveCredential?.(signal);
|
|
4459
|
+
if (managed) return managed;
|
|
4460
|
+
const pat = await this.requirePat(signal);
|
|
4461
|
+
return {
|
|
4462
|
+
key: opaqueCredentialKey(pat),
|
|
4463
|
+
resolve: async (nextSignal, rejectedToken) => {
|
|
4464
|
+
const current = await this.auth.getCredentials(pat, nextSignal);
|
|
4465
|
+
if (rejectedToken !== void 0 && current.authToken === rejectedToken) return this.auth.refreshCredentials(pat, current, nextSignal);
|
|
4466
|
+
return current;
|
|
4467
|
+
}
|
|
4468
|
+
};
|
|
4469
|
+
}
|
|
3098
4470
|
async *generate(options, model) {
|
|
3099
4471
|
if (options.signal?.aborted) throw aborted("Request was aborted prior to generation.");
|
|
4472
|
+
const queueOwner = options.sessionId === void 0 ? void 0 : this.captureQueueLeaseOwner?.(String(options.sessionId));
|
|
3100
4473
|
validateQoderRequestShape(options, model);
|
|
3101
|
-
const
|
|
3102
|
-
const credentials = await
|
|
4474
|
+
const lease = await this.lease(options.signal);
|
|
4475
|
+
const credentials = await lease.resolve(options.signal);
|
|
4476
|
+
this.credentialLeases.set(credentials, lease);
|
|
3103
4477
|
const messages = await translateQoderMessages(options, this.attachments, {
|
|
3104
4478
|
uploader: this.imageUploader,
|
|
3105
4479
|
credentials,
|
|
3106
4480
|
preserveThinking: this.preserveThinking
|
|
3107
4481
|
});
|
|
3108
|
-
|
|
4482
|
+
const chatCredentials = await lease.resolve(options.signal);
|
|
4483
|
+
yield* (this.modelTransport === "legacy" ? streamQoderChat : streamModelServerChat)(options, model, chatCredentials, messages, {
|
|
3109
4484
|
fetch: this.fetchImpl,
|
|
3110
4485
|
logger: this.logger,
|
|
3111
4486
|
region: this.region,
|
|
4487
|
+
requestCompression: this.requestCompression,
|
|
3112
4488
|
responseHeaderTimeoutMs: this.responseHeaderTimeoutMs,
|
|
3113
|
-
streamIdleTimeoutMs: this.streamIdleTimeoutMs
|
|
4489
|
+
streamIdleTimeoutMs: this.streamIdleTimeoutMs,
|
|
4490
|
+
modelQueueMaxWaitMs: this.modelQueueMaxWaitMs,
|
|
4491
|
+
onQueueUpdate: this.onQueueUpdate,
|
|
4492
|
+
hostRunId: queueOwner?.runId,
|
|
4493
|
+
onQueueLease: queueOwner?.retain,
|
|
4494
|
+
resolveCredentials: async (refresh, signal) => {
|
|
4495
|
+
const current = await lease.resolve(signal);
|
|
4496
|
+
return refresh ? lease.resolve(signal, current.authToken) : current;
|
|
4497
|
+
},
|
|
4498
|
+
refreshCredentials: async (rejected, signal) => {
|
|
4499
|
+
return lease.resolve(signal, rejected.authToken);
|
|
4500
|
+
}
|
|
3114
4501
|
});
|
|
3115
4502
|
}
|
|
3116
4503
|
};
|
|
@@ -3150,6 +4537,7 @@ const catalogModel = z.object({
|
|
|
3150
4537
|
});
|
|
3151
4538
|
const modelsByRegionSchema = z.dict(z.array(catalogModel));
|
|
3152
4539
|
const Config = z.object({
|
|
4540
|
+
modelQueueMaxWaitMs: z.number().step(1).min(1).max(2147483647).default(defaultModelQueueMaxWaitMs).description("Maximum cumulative model queue wait in milliseconds; changes apply to new calls"),
|
|
3153
4541
|
region: z.union(["global", "china"]).default("global"),
|
|
3154
4542
|
modelsByRegion: modelsByRegionSchema.default({}),
|
|
3155
4543
|
streamIdleTimeoutMs: z.number().step(1).min(1).default(defaultStreamIdleTimeoutMs),
|
|
@@ -3159,7 +4547,9 @@ const Config = z.object({
|
|
|
3159
4547
|
"auto",
|
|
3160
4548
|
"always",
|
|
3161
4549
|
"disabled"
|
|
3162
|
-
]).default("auto")
|
|
4550
|
+
]).default("auto"),
|
|
4551
|
+
requestCompression: z.boolean().default(false),
|
|
4552
|
+
modelTransport: z.union(["http", "legacy"]).default("legacy")
|
|
3163
4553
|
});
|
|
3164
4554
|
const PluginConfig = typeof Config.volatile === "function" ? Config.volatile() : Config;
|
|
3165
4555
|
function readConfig(value) {
|
|
@@ -3180,6 +4570,237 @@ function modelsFor(config, region) {
|
|
|
3180
4570
|
return resolveModels(config.modelsByRegion?.[region]);
|
|
3181
4571
|
}
|
|
3182
4572
|
//#endregion
|
|
4573
|
+
//#region src/dsh/queue-leases.ts
|
|
4574
|
+
/** Correlate transport-owned release actions with committed DSH turn boundaries. */
|
|
4575
|
+
/** Bind committed boundaries and recover ownership when loaded during an open turn. */
|
|
4576
|
+
function bindQoderQueueLeases(ctx, leases) {
|
|
4577
|
+
ctx.on("session/event", (session, event) => {
|
|
4578
|
+
if (event.type === "turn/start") leases.start(session.id, event.data.turn);
|
|
4579
|
+
if (event.type === "turn/end") leases.end(session.id, event.data.turn);
|
|
4580
|
+
});
|
|
4581
|
+
ctx.on("agent/request", async ({ agent, turn }, next) => {
|
|
4582
|
+
leases.start(agent.session.id, turn);
|
|
4583
|
+
return next();
|
|
4584
|
+
});
|
|
4585
|
+
}
|
|
4586
|
+
var QoderQueueLeases = class {
|
|
4587
|
+
runs = /* @__PURE__ */ new Map();
|
|
4588
|
+
pending = /* @__PURE__ */ new Set();
|
|
4589
|
+
disposed = false;
|
|
4590
|
+
start(sessionId, turn) {
|
|
4591
|
+
if (this.disposed || this.runs.get(sessionId)?.turn === turn) return;
|
|
4592
|
+
this.end(sessionId);
|
|
4593
|
+
this.runs.set(sessionId, {
|
|
4594
|
+
turn,
|
|
4595
|
+
runId: randomUUID(),
|
|
4596
|
+
leases: /* @__PURE__ */ new Map()
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
retain = (lease) => {
|
|
4600
|
+
const run = this.runs.get(lease.sessionId);
|
|
4601
|
+
if (this.disposed || !run) return false;
|
|
4602
|
+
if (!run.leases.has(lease.key)) run.leases.set(lease.key, lease);
|
|
4603
|
+
return true;
|
|
4604
|
+
};
|
|
4605
|
+
/** Bind ownership before provider I/O; a late old call cannot join a newer turn. */
|
|
4606
|
+
capture = (sessionId) => {
|
|
4607
|
+
const owner = this.runs.get(sessionId);
|
|
4608
|
+
if (!owner || this.disposed) return void 0;
|
|
4609
|
+
return {
|
|
4610
|
+
runId: owner.runId,
|
|
4611
|
+
retain: (lease) => lease.sessionId === sessionId && this.runs.get(sessionId) === owner && this.retain(lease)
|
|
4612
|
+
};
|
|
4613
|
+
};
|
|
4614
|
+
async end(sessionId, turn) {
|
|
4615
|
+
const run = this.runs.get(sessionId);
|
|
4616
|
+
if (!run || turn !== void 0 && turn !== run.turn) return;
|
|
4617
|
+
this.runs.delete(sessionId);
|
|
4618
|
+
const cleanup = Promise.allSettled([...run.leases.values()].map((lease) => Promise.resolve().then(() => lease.release())));
|
|
4619
|
+
this.pending.add(cleanup);
|
|
4620
|
+
try {
|
|
4621
|
+
await cleanup;
|
|
4622
|
+
} finally {
|
|
4623
|
+
this.pending.delete(cleanup);
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
async dispose() {
|
|
4627
|
+
this.disposed = true;
|
|
4628
|
+
await Promise.allSettled([...this.runs.keys()].map((id) => this.end(id)));
|
|
4629
|
+
await Promise.allSettled([...this.pending]);
|
|
4630
|
+
}
|
|
4631
|
+
};
|
|
4632
|
+
//#endregion
|
|
4633
|
+
//#region src/dsh/queue-status.ts
|
|
4634
|
+
var QoderQueueStatusStore = class {
|
|
4635
|
+
calls = /* @__PURE__ */ new Map();
|
|
4636
|
+
active = true;
|
|
4637
|
+
update = (event) => {
|
|
4638
|
+
if (!this.active) return;
|
|
4639
|
+
if (event.type === "clear") this.calls.delete(event.callId);
|
|
4640
|
+
else this.calls.set(event.snapshot.callId, { ...event.snapshot });
|
|
4641
|
+
};
|
|
4642
|
+
read(sessionId) {
|
|
4643
|
+
return [...this.calls.values()].filter((value) => value.sessionId === sessionId).map((value) => ({ ...value }));
|
|
4644
|
+
}
|
|
4645
|
+
dispose() {
|
|
4646
|
+
this.active = false;
|
|
4647
|
+
this.calls.clear();
|
|
4648
|
+
}
|
|
4649
|
+
};
|
|
4650
|
+
//#endregion
|
|
4651
|
+
//#region src/dsh/catalog-lifecycle.ts
|
|
4652
|
+
/** Region-scoped Qoder model discovery, effective catalogs, and advisory persistence. */
|
|
4653
|
+
var QoderCatalogLifecycle = class {
|
|
4654
|
+
options;
|
|
4655
|
+
catalogs = {
|
|
4656
|
+
global: {
|
|
4657
|
+
models: [],
|
|
4658
|
+
generation: 0,
|
|
4659
|
+
expiresAt: 0,
|
|
4660
|
+
dirty: false,
|
|
4661
|
+
retryRequested: false
|
|
4662
|
+
},
|
|
4663
|
+
china: {
|
|
4664
|
+
models: [],
|
|
4665
|
+
generation: 0,
|
|
4666
|
+
expiresAt: 0,
|
|
4667
|
+
dirty: false,
|
|
4668
|
+
retryRequested: false
|
|
4669
|
+
}
|
|
4670
|
+
};
|
|
4671
|
+
settings;
|
|
4672
|
+
disposed = false;
|
|
4673
|
+
activeTransport;
|
|
4674
|
+
activeRegion;
|
|
4675
|
+
constructor(options) {
|
|
4676
|
+
this.options = options;
|
|
4677
|
+
}
|
|
4678
|
+
/** Observe every transport replacement, including switching away and back. */
|
|
4679
|
+
observeTransport() {
|
|
4680
|
+
const transport = this.options.resolveTransport();
|
|
4681
|
+
const region = this.options.region();
|
|
4682
|
+
if (transport === this.activeTransport && region === this.activeRegion) return;
|
|
4683
|
+
if (this.activeRegion !== void 0) {
|
|
4684
|
+
const previous = this.catalogs[this.activeRegion];
|
|
4685
|
+
previous.generation++;
|
|
4686
|
+
previous.inflight = void 0;
|
|
4687
|
+
}
|
|
4688
|
+
this.activeTransport = transport;
|
|
4689
|
+
this.activeRegion = region;
|
|
4690
|
+
const state = this.catalogs[region];
|
|
4691
|
+
state.generation++;
|
|
4692
|
+
state.expiresAt = 0;
|
|
4693
|
+
state.inflight = void 0;
|
|
4694
|
+
}
|
|
4695
|
+
models() {
|
|
4696
|
+
this.observeTransport();
|
|
4697
|
+
const region = this.options.region();
|
|
4698
|
+
return mergeQoderDiscoveryMetadata(this.options.configuredModels(region), this.catalogs[region].models);
|
|
4699
|
+
}
|
|
4700
|
+
/** Subscriber replacement must not retain another subscriber's discovered capabilities. */
|
|
4701
|
+
clearDiscovered() {
|
|
4702
|
+
for (const state of Object.values(this.catalogs)) {
|
|
4703
|
+
state.models = [];
|
|
4704
|
+
state.generation++;
|
|
4705
|
+
state.expiresAt = 0;
|
|
4706
|
+
state.inflight = void 0;
|
|
4707
|
+
state.dirty = false;
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
/** Automatic reads join the newest discovery and never wait for settings writes. */
|
|
4711
|
+
async refresh() {
|
|
4712
|
+
while (!this.disposed) {
|
|
4713
|
+
this.observeTransport();
|
|
4714
|
+
const region = this.options.region();
|
|
4715
|
+
const state = this.catalogs[region];
|
|
4716
|
+
const transport = this.activeTransport;
|
|
4717
|
+
const pending = state.inflight ?? (Date.now() >= state.expiresAt ? this.discover() : void 0);
|
|
4718
|
+
await pending?.catch(() => {});
|
|
4719
|
+
this.observeTransport();
|
|
4720
|
+
if (this.activeTransport !== transport || this.activeRegion !== region) continue;
|
|
4721
|
+
if (state.inflight && state.inflight !== pending) continue;
|
|
4722
|
+
return;
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
/** Explicit discovery supersedes older work as soon as it starts, even if it fails. */
|
|
4726
|
+
discover(signal, requestTransport) {
|
|
4727
|
+
if (this.disposed || signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model discovery was aborted.", "ABORTED"));
|
|
4728
|
+
this.observeTransport();
|
|
4729
|
+
const region = this.options.region();
|
|
4730
|
+
const state = this.catalogs[region];
|
|
4731
|
+
const transport = this.activeTransport;
|
|
4732
|
+
const generation = ++state.generation;
|
|
4733
|
+
const pending = Promise.resolve().then(() => (requestTransport ?? transport).discoverModels(signal)).then((models) => {
|
|
4734
|
+
this.observeTransport();
|
|
4735
|
+
if (!this.disposed && this.activeTransport === transport && this.activeRegion === region && state.generation === generation) {
|
|
4736
|
+
state.models = models.map(cloneCatalogModel);
|
|
4737
|
+
state.expiresAt = Date.now() + 300 * 1e3;
|
|
4738
|
+
state.dirty = true;
|
|
4739
|
+
this.schedulePersistence(region);
|
|
4740
|
+
try {
|
|
4741
|
+
Promise.resolve(this.options.onDiscovered?.(transport, state.models)).catch(() => {});
|
|
4742
|
+
} catch {}
|
|
4743
|
+
}
|
|
4744
|
+
return models;
|
|
4745
|
+
}).finally(() => {
|
|
4746
|
+
if (state.inflight === pending) state.inflight = void 0;
|
|
4747
|
+
});
|
|
4748
|
+
state.inflight = pending;
|
|
4749
|
+
return pending;
|
|
4750
|
+
}
|
|
4751
|
+
bindSettings(settings) {
|
|
4752
|
+
this.settings = settings;
|
|
4753
|
+
this.retryPersistence();
|
|
4754
|
+
}
|
|
4755
|
+
/** Called by settings changes/reattachment; no timer retries or fallback writes. */
|
|
4756
|
+
retryPersistence() {
|
|
4757
|
+
for (const region of ["global", "china"]) {
|
|
4758
|
+
const state = this.catalogs[region];
|
|
4759
|
+
if (state.models.length === 0) continue;
|
|
4760
|
+
state.dirty = true;
|
|
4761
|
+
this.schedulePersistence(region);
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
dispose() {
|
|
4765
|
+
this.disposed = true;
|
|
4766
|
+
this.settings = void 0;
|
|
4767
|
+
for (const state of Object.values(this.catalogs)) state.generation++;
|
|
4768
|
+
}
|
|
4769
|
+
schedulePersistence(region) {
|
|
4770
|
+
const state = this.catalogs[region];
|
|
4771
|
+
const settings = this.settings;
|
|
4772
|
+
if (this.disposed || !settings || !state.dirty) return;
|
|
4773
|
+
if (state.writing) {
|
|
4774
|
+
state.retryRequested = true;
|
|
4775
|
+
return;
|
|
4776
|
+
}
|
|
4777
|
+
state.retryRequested = false;
|
|
4778
|
+
const writing = Promise.resolve().then(() => this.persist(region, settings)).catch((error) => {
|
|
4779
|
+
this.options.onPersistenceError?.(error);
|
|
4780
|
+
}).finally(() => {
|
|
4781
|
+
if (state.writing === writing) state.writing = void 0;
|
|
4782
|
+
if (this.settings !== settings || state.retryRequested) this.schedulePersistence(region);
|
|
4783
|
+
});
|
|
4784
|
+
state.writing = writing;
|
|
4785
|
+
}
|
|
4786
|
+
async persist(region, settings) {
|
|
4787
|
+
const state = this.catalogs[region];
|
|
4788
|
+
while (!this.disposed && this.settings === settings && state.dirty) {
|
|
4789
|
+
const snapshot = settings.read(region);
|
|
4790
|
+
if (!snapshot) return;
|
|
4791
|
+
const enriched = modelsFor(Config({ modelsByRegion: { [region]: mergeQoderDiscoveryMetadata(snapshot.models, state.models) } }), region);
|
|
4792
|
+
state.dirty = false;
|
|
4793
|
+
if (hasSameQoderDiscoveryMetadata(snapshot.models, enriched)) continue;
|
|
4794
|
+
try {
|
|
4795
|
+
await settings.write(region, enriched, snapshot.revision);
|
|
4796
|
+
} catch (error) {
|
|
4797
|
+
state.dirty = true;
|
|
4798
|
+
if (!(error instanceof SettingsConflictError)) throw error;
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
};
|
|
4803
|
+
//#endregion
|
|
3183
4804
|
//#region src/dsh/provider.ts
|
|
3184
4805
|
/** DSH model provider route, matching the plugin package name. */
|
|
3185
4806
|
const QODER_PROVIDER_ID = "dsh-provider-qoder";
|
|
@@ -3201,12 +4822,12 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3201
4822
|
catalogModels;
|
|
3202
4823
|
providerId;
|
|
3203
4824
|
providerName;
|
|
3204
|
-
|
|
3205
|
-
discoveries = /* @__PURE__ */ new WeakMap();
|
|
4825
|
+
catalog;
|
|
3206
4826
|
historyRegion;
|
|
3207
4827
|
historyRegionChanged = false;
|
|
3208
4828
|
region;
|
|
3209
4829
|
sessionTiers = /* @__PURE__ */ new Map();
|
|
4830
|
+
tierListeners = /* @__PURE__ */ new Set();
|
|
3210
4831
|
sessions;
|
|
3211
4832
|
agents;
|
|
3212
4833
|
constructor(options) {
|
|
@@ -3215,8 +4836,13 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3215
4836
|
this.catalogModels = options.models && options.models.length > 0 ? options.models : defaultModels;
|
|
3216
4837
|
this.providerId = options.providerId ?? "dsh-provider-qoder";
|
|
3217
4838
|
this.providerName = options.providerName ?? "Qoder";
|
|
3218
|
-
this.onModelsDiscovered = options.onModelsDiscovered;
|
|
3219
4839
|
this.region = options.region ?? (() => "global");
|
|
4840
|
+
this.catalog = options.catalog ?? new QoderCatalogLifecycle({
|
|
4841
|
+
resolveTransport: this.resolveTransport,
|
|
4842
|
+
region: this.region,
|
|
4843
|
+
configuredModels: () => this.catalogModels,
|
|
4844
|
+
onDiscovered: options.onModelsDiscovered
|
|
4845
|
+
});
|
|
3220
4846
|
this.historyRegion = this.region();
|
|
3221
4847
|
this.sessions = options.sessions;
|
|
3222
4848
|
this.agents = options.agents;
|
|
@@ -3229,6 +4855,31 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3229
4855
|
sessionId,
|
|
3230
4856
|
modelId
|
|
3231
4857
|
]), tierKey);
|
|
4858
|
+
for (const listener of this.tierListeners) try {
|
|
4859
|
+
listener({
|
|
4860
|
+
region,
|
|
4861
|
+
sessionId,
|
|
4862
|
+
modelId
|
|
4863
|
+
});
|
|
4864
|
+
} catch {}
|
|
4865
|
+
}
|
|
4866
|
+
subscribeSessionTiers(listener) {
|
|
4867
|
+
this.tierListeners.add(listener);
|
|
4868
|
+
return () => {
|
|
4869
|
+
this.tierListeners.delete(listener);
|
|
4870
|
+
};
|
|
4871
|
+
}
|
|
4872
|
+
readSessionTier(scope) {
|
|
4873
|
+
const { region, sessionId, modelId } = scope;
|
|
4874
|
+
if (region !== this.region() || !sessionId || this.sessions && !this.sessions.get(sessionId)) throw new QoderLlmError("Invalid session context tier scope.", "INVALID_REQUEST");
|
|
4875
|
+
const model = this.resolveEffectiveModelForSession(modelId, sessionId);
|
|
4876
|
+
const tier = model && resolveContextTier(model, model.contextTier);
|
|
4877
|
+
if (!tier) throw new QoderLlmError("Model has no context tiers.", "INVALID_REQUEST");
|
|
4878
|
+
return {
|
|
4879
|
+
...scope,
|
|
4880
|
+
tierKey: tier.key,
|
|
4881
|
+
tokenCount: tier.tokenCount
|
|
4882
|
+
};
|
|
3232
4883
|
}
|
|
3233
4884
|
getSessionTier(sessionId, modelId) {
|
|
3234
4885
|
return this.sessionTiers.get(JSON.stringify([
|
|
@@ -3265,52 +4916,17 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3265
4916
|
name: this.providerName
|
|
3266
4917
|
};
|
|
3267
4918
|
}
|
|
3268
|
-
/** Refresh advertised metadata on catalog reads, sharing a five-minute transport-local cache. */
|
|
3269
4919
|
async listModels(provider) {
|
|
3270
|
-
|
|
3271
|
-
let cached = this.discoveries.get(transport);
|
|
3272
|
-
if (cached === void 0) {
|
|
3273
|
-
cached = { expiresAt: 0 };
|
|
3274
|
-
this.discoveries.set(transport, cached);
|
|
3275
|
-
}
|
|
3276
|
-
const entry = cached;
|
|
3277
|
-
if (entry.inflight === void 0 && Date.now() >= entry.expiresAt) entry.inflight = Promise.resolve().then(() => transport.discoverModels()).then((models) => {
|
|
3278
|
-
if (this.discoveries.get(transport) !== entry || this.resolveTransport() !== transport) return;
|
|
3279
|
-
entry.models = models;
|
|
3280
|
-
entry.expiresAt = Date.now() + 300 * 1e3;
|
|
3281
|
-
this.publishDiscoveredModels(transport, models);
|
|
3282
|
-
}).catch(() => {}).finally(() => {
|
|
3283
|
-
entry.inflight = void 0;
|
|
3284
|
-
});
|
|
3285
|
-
await entry.inflight;
|
|
3286
|
-
if (this.resolveTransport() !== transport) return this.listModels(provider);
|
|
4920
|
+
await this.catalog.refresh();
|
|
3287
4921
|
return this.effectiveModels().map((model) => modelInfo(provider, model));
|
|
3288
4922
|
}
|
|
3289
|
-
/**
|
|
3290
|
-
* Announce one accepted discovery without joining the catalog read.
|
|
3291
|
-
*
|
|
3292
|
-
* Persisting metadata can queue behind unrelated settings writes, so awaiting
|
|
3293
|
-
* it here would stall every `listModels` call on settings storage.
|
|
3294
|
-
*/
|
|
3295
|
-
publishDiscoveredModels(transport, models) {
|
|
3296
|
-
try {
|
|
3297
|
-
Promise.resolve(this.onModelsDiscovered?.(transport, models)).catch(() => {});
|
|
3298
|
-
} catch {}
|
|
3299
|
-
}
|
|
3300
4923
|
effectiveModels() {
|
|
3301
|
-
return
|
|
4924
|
+
return this.catalog.models();
|
|
3302
4925
|
}
|
|
3303
4926
|
replaceModels(models) {
|
|
3304
4927
|
this.observeRegion();
|
|
3305
4928
|
this.catalogModels = models;
|
|
3306
4929
|
}
|
|
3307
|
-
/** Publish an explicit discovery without letting older in-flight reads overwrite it. */
|
|
3308
|
-
updateDiscoveredModels(transport, models) {
|
|
3309
|
-
this.discoveries.set(transport, {
|
|
3310
|
-
models,
|
|
3311
|
-
expiresAt: Date.now() + 300 * 1e3
|
|
3312
|
-
});
|
|
3313
|
-
}
|
|
3314
4930
|
resolveModel(provider, modelId, signal) {
|
|
3315
4931
|
if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
|
|
3316
4932
|
const sessionId = this.agents?.currentInitiator()?.session?.id;
|
|
@@ -3469,7 +5085,14 @@ const qoderRpcApiPath = `/api/qoder-subscription`;
|
|
|
3469
5085
|
const qoderRpcEndpoints = [
|
|
3470
5086
|
"account",
|
|
3471
5087
|
"models",
|
|
3472
|
-
"
|
|
5088
|
+
"queue",
|
|
5089
|
+
"sessionTier",
|
|
5090
|
+
"readSessionTier",
|
|
5091
|
+
"sessionTierEvents",
|
|
5092
|
+
"authorization",
|
|
5093
|
+
"beginLogin",
|
|
5094
|
+
"cancelLogin",
|
|
5095
|
+
"selectAuthorization"
|
|
3473
5096
|
];
|
|
3474
5097
|
/** Absolute Fetch-route path of one endpoint. */
|
|
3475
5098
|
function qoderRpcPath(endpoint) {
|
|
@@ -3492,16 +5115,18 @@ function isQoderRpcEndpoint(value) {
|
|
|
3492
5115
|
* @returns disposer removing every registered route.
|
|
3493
5116
|
* @throws when Connection exposes no exact Fetch registry.
|
|
3494
5117
|
*/
|
|
3495
|
-
function registerQoderRpc(ctx, handler) {
|
|
5118
|
+
function registerQoderRpc(ctx, handler, subscribeTiers) {
|
|
3496
5119
|
const registry = ctx.connection?.fetch;
|
|
3497
5120
|
if (typeof registry?.register !== "function") throw new Error("provider-qoder: connection exposes no exact Fetch route registry");
|
|
5121
|
+
const streams = /* @__PURE__ */ new Set();
|
|
3498
5122
|
const disposers = qoderRpcEndpoints.map((endpoint) => registry.register({
|
|
3499
5123
|
path: qoderRpcPath(endpoint),
|
|
3500
5124
|
methods: ["POST"],
|
|
3501
5125
|
requestBody: "buffered",
|
|
3502
|
-
fetch: (request) => handleQoderRpcRequest(endpoint, handler, request)
|
|
5126
|
+
fetch: (request) => endpoint === "sessionTierEvents" && subscribeTiers ? Promise.resolve(sessionTierEvents(request, subscribeTiers, streams)) : handleQoderRpcRequest(endpoint, handler, request)
|
|
3503
5127
|
}));
|
|
3504
5128
|
return () => {
|
|
5129
|
+
for (const close of streams) close();
|
|
3505
5130
|
for (const dispose of disposers) dispose();
|
|
3506
5131
|
};
|
|
3507
5132
|
}
|
|
@@ -3541,6 +5166,333 @@ async function handleQoderRpcRequest(endpoint, handler, request) {
|
|
|
3541
5166
|
return new Response(`handler failure: ${String(error)}`, { status: 500 });
|
|
3542
5167
|
}
|
|
3543
5168
|
}
|
|
5169
|
+
/** Authenticated change stream; reconnecting clients reread on the initial ready item. */
|
|
5170
|
+
function sessionTierEvents(request, subscribe, streams) {
|
|
5171
|
+
let close = () => {};
|
|
5172
|
+
const encoder = new TextEncoder();
|
|
5173
|
+
const body = new ReadableStream({
|
|
5174
|
+
start(controller) {
|
|
5175
|
+
let ended = false;
|
|
5176
|
+
let unsubscribe = () => {};
|
|
5177
|
+
const send = (value) => {
|
|
5178
|
+
if (!ended) controller.enqueue(encoder.encode(JSON.stringify(value) + "\n"));
|
|
5179
|
+
};
|
|
5180
|
+
close = () => {
|
|
5181
|
+
if (ended) return;
|
|
5182
|
+
ended = true;
|
|
5183
|
+
unsubscribe();
|
|
5184
|
+
request.signal.removeEventListener("abort", close);
|
|
5185
|
+
streams.delete(close);
|
|
5186
|
+
try {
|
|
5187
|
+
controller.close();
|
|
5188
|
+
} catch {}
|
|
5189
|
+
};
|
|
5190
|
+
unsubscribe = subscribe((scope) => send(scope));
|
|
5191
|
+
streams.add(close);
|
|
5192
|
+
request.signal.addEventListener("abort", close, { once: true });
|
|
5193
|
+
if (request.signal.aborted) close();
|
|
5194
|
+
else send({ ready: true });
|
|
5195
|
+
},
|
|
5196
|
+
cancel() {
|
|
5197
|
+
close();
|
|
5198
|
+
}
|
|
5199
|
+
});
|
|
5200
|
+
return new Response(body, { headers: {
|
|
5201
|
+
"content-type": "application/x-ndjson",
|
|
5202
|
+
"cache-control": "no-store",
|
|
5203
|
+
"x-accel-buffering": "no"
|
|
5204
|
+
} });
|
|
5205
|
+
}
|
|
5206
|
+
//#endregion
|
|
5207
|
+
//#region src/dsh/browser-authorization.ts
|
|
5208
|
+
/** Instance-shared authorization lifecycle. Provider I/O is delegated to Qoder transport. */
|
|
5209
|
+
const browserAuthorizationKey = (region) => credentialKey("provider-qoder", region);
|
|
5210
|
+
function decode(record, region) {
|
|
5211
|
+
if (record === void 0) return {
|
|
5212
|
+
version: 1,
|
|
5213
|
+
method: "pat"
|
|
5214
|
+
};
|
|
5215
|
+
const value = record.kind === "grant" ? record.payload : null;
|
|
5216
|
+
if (!value || value.version !== 1 || ![
|
|
5217
|
+
"pat",
|
|
5218
|
+
"browser",
|
|
5219
|
+
"none"
|
|
5220
|
+
].includes(value.method ?? "")) throw new QoderLlmError("Saved Qoder authorization is invalid. Remove it and sign in again.", "AUTH");
|
|
5221
|
+
if (value.grant) {
|
|
5222
|
+
const grant = value.grant;
|
|
5223
|
+
if (grant.region !== region || typeof grant.id !== "string" || !grant.id || typeof grant.refreshToken !== "string" || !grant.refreshToken || !Number.isFinite(grant.expiresAt) || !grant.credentials || typeof grant.credentials.userID !== "string" || !grant.credentials.userID || typeof grant.credentials.authToken !== "string" || !grant.credentials.authToken) throw new QoderLlmError("Saved Qoder browser authorization is invalid.", "AUTH");
|
|
5224
|
+
}
|
|
5225
|
+
return value;
|
|
5226
|
+
}
|
|
5227
|
+
const record = (payload) => ({
|
|
5228
|
+
kind: "grant",
|
|
5229
|
+
payload: JSON.parse(JSON.stringify(payload))
|
|
5230
|
+
});
|
|
5231
|
+
const pending = (attempt) => attempt !== void 0 && [
|
|
5232
|
+
"waiting",
|
|
5233
|
+
"verifying",
|
|
5234
|
+
"saving"
|
|
5235
|
+
].includes(attempt.phase);
|
|
5236
|
+
const cancelled = () => new QoderLlmError("Qoder authorization changed or was cancelled.", "ABORTED");
|
|
5237
|
+
var QoderBrowserAuthorizationManager = class {
|
|
5238
|
+
credentials;
|
|
5239
|
+
transport;
|
|
5240
|
+
changed;
|
|
5241
|
+
attempts = /* @__PURE__ */ new Map();
|
|
5242
|
+
volatile = /* @__PURE__ */ new Map();
|
|
5243
|
+
invalid = /* @__PURE__ */ new Set();
|
|
5244
|
+
renewals = /* @__PURE__ */ new Set();
|
|
5245
|
+
lifecycle = new AbortController();
|
|
5246
|
+
activeRegion;
|
|
5247
|
+
regionGeneration = 0;
|
|
5248
|
+
disposed = false;
|
|
5249
|
+
constructor(options) {
|
|
5250
|
+
this.credentials = options.credentials;
|
|
5251
|
+
this.transport = options.transport;
|
|
5252
|
+
this.changed = options.changed;
|
|
5253
|
+
this.activeRegion = options.region;
|
|
5254
|
+
}
|
|
5255
|
+
activateRegion(region) {
|
|
5256
|
+
if (this.activeRegion === region) return;
|
|
5257
|
+
const previous = this.activeRegion;
|
|
5258
|
+
this.activeRegion = region;
|
|
5259
|
+
this.regionGeneration++;
|
|
5260
|
+
if (previous) this.cancel(previous).catch(() => {});
|
|
5261
|
+
}
|
|
5262
|
+
assertRegion(region, generation) {
|
|
5263
|
+
if (this.disposed || this.regionGeneration !== generation || this.activeRegion !== void 0 && this.activeRegion !== region) throw cancelled();
|
|
5264
|
+
}
|
|
5265
|
+
async read(region) {
|
|
5266
|
+
return decode(await this.credentials.readRecord?.(browserAuthorizationKey(region)), region);
|
|
5267
|
+
}
|
|
5268
|
+
async describe(region) {
|
|
5269
|
+
const writable = await this.credentials.describeRecord?.(browserAuthorizationKey(region));
|
|
5270
|
+
let saved;
|
|
5271
|
+
try {
|
|
5272
|
+
saved = await this.read(region);
|
|
5273
|
+
} catch (error) {
|
|
5274
|
+
if (!(error instanceof QoderLlmError) || error.code !== "AUTH") throw error;
|
|
5275
|
+
return {
|
|
5276
|
+
region,
|
|
5277
|
+
method: "none",
|
|
5278
|
+
configured: false,
|
|
5279
|
+
writable: writable?.writable ?? false,
|
|
5280
|
+
expired: true
|
|
5281
|
+
};
|
|
5282
|
+
}
|
|
5283
|
+
const live = this.volatile.get(region);
|
|
5284
|
+
const grant = live && live.grant.id === saved.grant?.id && live.baseToken === saved.grant?.credentials.authToken ? live.grant : saved.grant;
|
|
5285
|
+
const attempt = this.attempts.get(region);
|
|
5286
|
+
const expired = grant !== void 0 && (saved.expired === true || this.invalid.has(grant.id) || grant.refreshExpiresAt !== void 0 && grant.refreshExpiresAt <= Date.now());
|
|
5287
|
+
return {
|
|
5288
|
+
region,
|
|
5289
|
+
method: saved.method,
|
|
5290
|
+
writable: writable?.writable ?? false,
|
|
5291
|
+
configured: saved.method === "pat" ? Boolean(await resolveManagedQoderPat(this.credentials)) : saved.method === "browser" && grant !== void 0 && !expired,
|
|
5292
|
+
...grant ? {
|
|
5293
|
+
profile: {
|
|
5294
|
+
id: grant.credentials.userID,
|
|
5295
|
+
name: grant.credentials.name,
|
|
5296
|
+
email: grant.credentials.email
|
|
5297
|
+
},
|
|
5298
|
+
expired: saved.method === "browser" && expired,
|
|
5299
|
+
unsaved: live?.grant === grant
|
|
5300
|
+
} : {},
|
|
5301
|
+
...attempt ? { attempt: {
|
|
5302
|
+
id: attempt.id,
|
|
5303
|
+
phase: attempt.phase,
|
|
5304
|
+
expiresAt: attempt.expiresAt,
|
|
5305
|
+
...attempt.failure ? { failure: attempt.failure } : {},
|
|
5306
|
+
...pending(attempt) ? { url: attempt.url } : {}
|
|
5307
|
+
} } : {}
|
|
5308
|
+
};
|
|
5309
|
+
}
|
|
5310
|
+
async begin(region) {
|
|
5311
|
+
const generation = this.regionGeneration;
|
|
5312
|
+
await this.assertWritable(region);
|
|
5313
|
+
this.assertRegion(region, generation);
|
|
5314
|
+
if (pending(this.attempts.get(region))) return this.describe(region);
|
|
5315
|
+
const attempt = {
|
|
5316
|
+
id: randomUUID(),
|
|
5317
|
+
phase: "waiting",
|
|
5318
|
+
controller: new AbortController(),
|
|
5319
|
+
expiresAt: Date.now() + 3e5
|
|
5320
|
+
};
|
|
5321
|
+
this.attempts.set(region, attempt);
|
|
5322
|
+
attempt.done = this.login(region, attempt);
|
|
5323
|
+
await Promise.resolve();
|
|
5324
|
+
return this.describe(region);
|
|
5325
|
+
}
|
|
5326
|
+
async login(region, attempt) {
|
|
5327
|
+
let previous;
|
|
5328
|
+
let installed;
|
|
5329
|
+
try {
|
|
5330
|
+
const flow = await this.transport(region).begin(attempt.controller.signal);
|
|
5331
|
+
this.assertAttempt(region, attempt);
|
|
5332
|
+
attempt.url = flow.url;
|
|
5333
|
+
attempt.expiresAt = flow.expiresAt;
|
|
5334
|
+
const grant = await flow.complete();
|
|
5335
|
+
this.assertAttempt(region, attempt);
|
|
5336
|
+
attempt.phase = "saving";
|
|
5337
|
+
await this.credentials.modifyRecord(browserAuthorizationKey(region), async (current) => {
|
|
5338
|
+
this.assertAttempt(region, attempt);
|
|
5339
|
+
previous = current;
|
|
5340
|
+
installed = grant.id;
|
|
5341
|
+
return record({
|
|
5342
|
+
version: 1,
|
|
5343
|
+
method: "browser",
|
|
5344
|
+
grant
|
|
5345
|
+
});
|
|
5346
|
+
});
|
|
5347
|
+
this.assertAttempt(region, attempt);
|
|
5348
|
+
this.volatile.delete(region);
|
|
5349
|
+
attempt.phase = "complete";
|
|
5350
|
+
this.changed();
|
|
5351
|
+
} catch (error) {
|
|
5352
|
+
attempt.failure = attempt.phase === "saving" ? "storage" : error instanceof QoderLlmError && [
|
|
5353
|
+
"TRANSPORT",
|
|
5354
|
+
"SERVER",
|
|
5355
|
+
"TIMEOUT"
|
|
5356
|
+
].includes(error.code) ? "connection" : "authorization";
|
|
5357
|
+
if (installed && attempt.controller.signal.aborted) await this.credentials.modifyRecord(browserAuthorizationKey(region), async (current) => {
|
|
5358
|
+
if (decode(current, region).grant?.id !== installed) return void 0;
|
|
5359
|
+
return previous ?? record({
|
|
5360
|
+
version: 1,
|
|
5361
|
+
method: "pat"
|
|
5362
|
+
});
|
|
5363
|
+
}).catch(() => {
|
|
5364
|
+
attempt.cleanupFailed = true;
|
|
5365
|
+
});
|
|
5366
|
+
attempt.phase = attempt.cleanupFailed ? "failed" : attempt.controller.signal.aborted ? "cancelled" : error instanceof QoderLlmError && error.code === "TIMEOUT" && Date.now() >= attempt.expiresAt ? "timeout" : "failed";
|
|
5367
|
+
} finally {
|
|
5368
|
+
attempt.url = void 0;
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
assertAttempt(region, attempt) {
|
|
5372
|
+
if (this.disposed || attempt.controller.signal.aborted || this.attempts.get(region) !== attempt) throw cancelled();
|
|
5373
|
+
}
|
|
5374
|
+
async cancel(region) {
|
|
5375
|
+
const attempt = this.attempts.get(region);
|
|
5376
|
+
if (!pending(attempt)) return;
|
|
5377
|
+
attempt.controller.abort();
|
|
5378
|
+
await attempt.done;
|
|
5379
|
+
if (attempt.cleanupFailed) throw new QoderLlmError("Could not restore the previous Qoder authorization after cancellation. Reload account status.", "INVALID_REQUEST");
|
|
5380
|
+
}
|
|
5381
|
+
async select(region, method) {
|
|
5382
|
+
const generation = this.regionGeneration;
|
|
5383
|
+
await this.assertWritable(region);
|
|
5384
|
+
await this.cancel(region);
|
|
5385
|
+
await this.credentials.modifyRecord(browserAuthorizationKey(region), async (current) => {
|
|
5386
|
+
this.assertRegion(region, generation);
|
|
5387
|
+
const saved = method === "none" ? {
|
|
5388
|
+
version: 1,
|
|
5389
|
+
method
|
|
5390
|
+
} : decode(current, region);
|
|
5391
|
+
if (method === "browser" && !saved.grant) throw new QoderLlmError("Sign in to Qoder first.", "MISSING_CREDENTIAL");
|
|
5392
|
+
return record(method === "none" ? {
|
|
5393
|
+
version: 1,
|
|
5394
|
+
method
|
|
5395
|
+
} : {
|
|
5396
|
+
...saved,
|
|
5397
|
+
method
|
|
5398
|
+
});
|
|
5399
|
+
});
|
|
5400
|
+
if (method === "none") this.volatile.delete(region);
|
|
5401
|
+
this.attempts.delete(region);
|
|
5402
|
+
this.changed();
|
|
5403
|
+
return this.describe(region);
|
|
5404
|
+
}
|
|
5405
|
+
async lease(region, signal) {
|
|
5406
|
+
if (signal?.aborted || this.disposed) throw cancelled();
|
|
5407
|
+
const saved = await this.read(region);
|
|
5408
|
+
if (saved.method === "pat") return void 0;
|
|
5409
|
+
const grant = saved.grant;
|
|
5410
|
+
if (saved.method !== "browser" || !grant) throw new QoderLlmError("Sign in to Qoder or select a Managed Qoder PAT.", "MISSING_CREDENTIAL");
|
|
5411
|
+
if (saved.expired || this.invalid.has(grant.id)) throw new QoderLlmError("Qoder browser authorization expired. Sign in again.", "AUTH");
|
|
5412
|
+
let captured = grant;
|
|
5413
|
+
return {
|
|
5414
|
+
key: `browser:${grant.id}`,
|
|
5415
|
+
resolve: async (nextSignal, rejectedToken) => {
|
|
5416
|
+
if (nextSignal?.aborted || this.disposed) throw cancelled();
|
|
5417
|
+
const latest = await this.read(region);
|
|
5418
|
+
if (latest.method !== "browser" || latest.grant?.id !== grant.id) {
|
|
5419
|
+
if (rejectedToken !== void 0 || captured.expiresAt <= Date.now()) throw cancelled();
|
|
5420
|
+
return captured.credentials;
|
|
5421
|
+
}
|
|
5422
|
+
const combined = nextSignal ? AbortSignal.any([nextSignal, this.lifecycle.signal]) : this.lifecycle.signal;
|
|
5423
|
+
const renewal = this.renew(region, grant.id, combined, rejectedToken);
|
|
5424
|
+
this.renewals.add(renewal);
|
|
5425
|
+
try {
|
|
5426
|
+
captured = await renewal;
|
|
5427
|
+
} finally {
|
|
5428
|
+
this.renewals.delete(renewal);
|
|
5429
|
+
}
|
|
5430
|
+
if (combined.aborted) throw cancelled();
|
|
5431
|
+
return captured.credentials;
|
|
5432
|
+
}
|
|
5433
|
+
};
|
|
5434
|
+
}
|
|
5435
|
+
async renew(region, id, signal, rejectedToken) {
|
|
5436
|
+
if (this.invalid.has(id)) throw new QoderLlmError("Qoder browser authorization expired. Sign in again.", "AUTH");
|
|
5437
|
+
const snapshot = await this.read(region);
|
|
5438
|
+
const fresh = snapshot.method === "browser" && snapshot.grant?.id === id ? snapshot.grant : void 0;
|
|
5439
|
+
if (fresh && !snapshot.expired && !this.volatile.has(region) && fresh.expiresAt > Date.now() + 3e5 && rejectedToken !== fresh.credentials.authToken) return fresh;
|
|
5440
|
+
let rotated;
|
|
5441
|
+
let baseToken;
|
|
5442
|
+
try {
|
|
5443
|
+
await this.credentials.modifyRecord(browserAuthorizationKey(region), async (current) => {
|
|
5444
|
+
const saved = decode(current, region);
|
|
5445
|
+
if (this.disposed || signal?.aborted || saved.method !== "browser" || saved.grant?.id !== id) throw cancelled();
|
|
5446
|
+
if (saved.expired) throw new QoderLlmError("Qoder browser authorization expired. Sign in again.", "AUTH");
|
|
5447
|
+
const volatile = this.volatile.get(region);
|
|
5448
|
+
baseToken = saved.grant.credentials.authToken;
|
|
5449
|
+
const source = volatile?.grant.id === id && volatile.baseToken === baseToken ? volatile.grant : saved.grant;
|
|
5450
|
+
rotated = await this.transport(region).refresh(source, this.lifecycle.signal, rejectedToken);
|
|
5451
|
+
return rotated === saved.grant ? void 0 : record({
|
|
5452
|
+
...saved,
|
|
5453
|
+
grant: rotated
|
|
5454
|
+
});
|
|
5455
|
+
});
|
|
5456
|
+
this.volatile.delete(region);
|
|
5457
|
+
return rotated;
|
|
5458
|
+
} catch (error) {
|
|
5459
|
+
if (error instanceof QoderLlmError && error.code === "AUTH") {
|
|
5460
|
+
this.invalid.add(id);
|
|
5461
|
+
await this.credentials.modifyRecord(browserAuthorizationKey(region), async (current) => {
|
|
5462
|
+
const saved = decode(current, region);
|
|
5463
|
+
return saved.grant?.id === id ? record({
|
|
5464
|
+
...saved,
|
|
5465
|
+
expired: true
|
|
5466
|
+
}) : void 0;
|
|
5467
|
+
}).catch(() => {});
|
|
5468
|
+
}
|
|
5469
|
+
if (rotated && baseToken) {
|
|
5470
|
+
const saved = await this.read(region);
|
|
5471
|
+
if (saved.method === "browser" && saved.grant?.id === id && saved.grant.credentials.authToken === rotated.credentials.authToken) return saved.grant;
|
|
5472
|
+
if (saved.method === "browser" && saved.grant?.id === id && saved.grant.credentials.authToken === baseToken) {
|
|
5473
|
+
this.volatile.set(region, {
|
|
5474
|
+
baseToken,
|
|
5475
|
+
grant: rotated
|
|
5476
|
+
});
|
|
5477
|
+
return rotated;
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
throw error;
|
|
5481
|
+
}
|
|
5482
|
+
}
|
|
5483
|
+
async assertWritable(region) {
|
|
5484
|
+
if (!(await this.credentials.describeRecord?.(browserAuthorizationKey(region)))?.writable) throw new QoderLlmError("Qoder authorization storage is read-only or unsupported.", "INVALID_REQUEST");
|
|
5485
|
+
}
|
|
5486
|
+
async dispose() {
|
|
5487
|
+
this.disposed = true;
|
|
5488
|
+
this.lifecycle.abort();
|
|
5489
|
+
for (const attempt of this.attempts.values()) attempt.controller.abort();
|
|
5490
|
+
await Promise.all([...this.attempts.values()].map((attempt) => attempt.done));
|
|
5491
|
+
await Promise.allSettled(this.renewals);
|
|
5492
|
+
this.volatile.clear();
|
|
5493
|
+
this.invalid.clear();
|
|
5494
|
+
}
|
|
5495
|
+
};
|
|
3544
5496
|
//#endregion
|
|
3545
5497
|
//#region src/dsh/plugin.ts
|
|
3546
5498
|
const name = "provider-qoder";
|
|
@@ -3593,41 +5545,57 @@ async function executeRpc(operation, task, signal, logger) {
|
|
|
3593
5545
|
}
|
|
3594
5546
|
}
|
|
3595
5547
|
function apply(ctx, input = {}) {
|
|
5548
|
+
const leases = new QoderQueueLeases();
|
|
5549
|
+
const queueStatus = new QoderQueueStatusStore();
|
|
5550
|
+
ctx.effect(() => async () => {
|
|
5551
|
+
queueStatus.dispose();
|
|
5552
|
+
await leases.dispose();
|
|
5553
|
+
}, "provider-qoder: queue lifecycle");
|
|
5554
|
+
bindQoderQueueLeases(ctx, leases);
|
|
3596
5555
|
const config = readConfig(input);
|
|
3597
5556
|
const logger = ctx.logger;
|
|
5557
|
+
const initialRegion = config.region ?? "global";
|
|
3598
5558
|
const baseConfig = {
|
|
3599
|
-
region:
|
|
5559
|
+
region: initialRegion,
|
|
3600
5560
|
modelsByRegion: { ...config.modelsByRegion },
|
|
3601
5561
|
streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
|
|
3602
5562
|
responseHeaderTimeoutMs: config.responseHeaderTimeoutMs ?? 6e4,
|
|
5563
|
+
modelQueueMaxWaitMs: config.modelQueueMaxWaitMs ?? 36e5,
|
|
3603
5564
|
preserveThinking: config.preserveThinking ?? true,
|
|
3604
|
-
webSearchMode: config.webSearchMode ?? "auto"
|
|
5565
|
+
webSearchMode: config.webSearchMode ?? "auto",
|
|
5566
|
+
requestCompression: config.requestCompression ?? false,
|
|
5567
|
+
modelTransport: config.modelTransport ?? "legacy"
|
|
3605
5568
|
};
|
|
3606
5569
|
let current = () => baseConfig;
|
|
3607
|
-
let
|
|
3608
|
-
const
|
|
3609
|
-
global: [],
|
|
3610
|
-
china: []
|
|
3611
|
-
};
|
|
3612
|
-
const createTransport = (region, streamIdleTimeoutMs, responseHeaderTimeoutMs, preserveThinking, resolvePat = () => resolveManagedQoderPat(ctx.credentials)) => createQoderTransport({
|
|
5570
|
+
let authorization;
|
|
5571
|
+
const createTransport = (region, streamIdleTimeoutMs, responseHeaderTimeoutMs, preserveThinking, resolvePat) => createQoderTransport({
|
|
3613
5572
|
region,
|
|
3614
|
-
resolvePat,
|
|
5573
|
+
resolvePat: resolvePat ?? (() => resolveManagedQoderPat(ctx.credentials)),
|
|
5574
|
+
resolveCredential: resolvePat ? void 0 : (signal) => authorization.lease(region, signal),
|
|
5575
|
+
requestCompression: current().requestCompression ?? false,
|
|
5576
|
+
modelTransport: current().modelTransport ?? "legacy",
|
|
3615
5577
|
logger,
|
|
3616
5578
|
streamIdleTimeoutMs,
|
|
3617
5579
|
responseHeaderTimeoutMs,
|
|
3618
5580
|
attachments: ctx.attachments,
|
|
3619
|
-
preserveThinking
|
|
5581
|
+
preserveThinking,
|
|
5582
|
+
modelQueueMaxWaitMs: current().modelQueueMaxWaitMs ?? 36e5,
|
|
5583
|
+
onQueueUpdate: queueStatus.update,
|
|
5584
|
+
captureQueueLeaseOwner: leases.capture
|
|
3620
5585
|
});
|
|
3621
5586
|
const resolveConfig = () => {
|
|
3622
5587
|
const value = current();
|
|
3623
5588
|
const region = value.region ?? "global";
|
|
3624
5589
|
return {
|
|
3625
5590
|
region,
|
|
3626
|
-
models:
|
|
5591
|
+
models: modelsFor(value, region),
|
|
3627
5592
|
streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
|
|
3628
5593
|
responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
|
|
5594
|
+
modelQueueMaxWaitMs: value.modelQueueMaxWaitMs ?? 36e5,
|
|
3629
5595
|
preserveThinking: value.preserveThinking ?? true,
|
|
3630
|
-
webSearchMode: value.webSearchMode ?? "auto"
|
|
5596
|
+
webSearchMode: value.webSearchMode ?? "auto",
|
|
5597
|
+
requestCompression: value.requestCompression ?? false,
|
|
5598
|
+
modelTransport: value.modelTransport ?? "legacy"
|
|
3631
5599
|
};
|
|
3632
5600
|
};
|
|
3633
5601
|
const initial = resolveConfig();
|
|
@@ -3636,38 +5604,81 @@ function apply(ctx, input = {}) {
|
|
|
3636
5604
|
region: initial.region,
|
|
3637
5605
|
streamIdleTimeoutMs: initial.streamIdleTimeoutMs,
|
|
3638
5606
|
responseHeaderTimeoutMs: initial.responseHeaderTimeoutMs,
|
|
3639
|
-
|
|
5607
|
+
modelQueueMaxWaitMs: initial.modelQueueMaxWaitMs,
|
|
5608
|
+
preserveThinking: initial.preserveThinking,
|
|
5609
|
+
requestCompression: initial.requestCompression,
|
|
5610
|
+
modelTransport: initial.modelTransport
|
|
3640
5611
|
};
|
|
5612
|
+
const authorizationTransports = /* @__PURE__ */ new Map();
|
|
5613
|
+
authorization = new QoderBrowserAuthorizationManager({
|
|
5614
|
+
credentials: ctx.credentials,
|
|
5615
|
+
region: initialRegion,
|
|
5616
|
+
transport: (region) => {
|
|
5617
|
+
let transport = authorizationTransports.get(region);
|
|
5618
|
+
if (!transport) {
|
|
5619
|
+
transport = createQoderTransport({
|
|
5620
|
+
region,
|
|
5621
|
+
resolvePat: () => Promise.resolve("")
|
|
5622
|
+
}).authorization;
|
|
5623
|
+
authorizationTransports.set(region, transport);
|
|
5624
|
+
}
|
|
5625
|
+
return transport;
|
|
5626
|
+
},
|
|
5627
|
+
changed: () => {
|
|
5628
|
+
catalog.refresh();
|
|
5629
|
+
}
|
|
5630
|
+
});
|
|
5631
|
+
ctx.effect(() => () => authorization.dispose(), "provider-qoder: browser authorization lifecycle");
|
|
5632
|
+
const catalog = new QoderCatalogLifecycle({
|
|
5633
|
+
resolveTransport: () => activeTransport,
|
|
5634
|
+
region: () => activeTransportConfig.region,
|
|
5635
|
+
configuredModels: (region) => modelsFor(current(), region),
|
|
5636
|
+
onDiscovered: () => registration.replace([providerQoder]),
|
|
5637
|
+
onPersistenceError: (error) => logger?.error?.("[Qoder Settings] Failed to synchronize model catalog", logError(error))
|
|
5638
|
+
});
|
|
5639
|
+
ctx.effect(() => () => catalog.dispose(), "provider-qoder: catalog lifecycle");
|
|
3641
5640
|
const adapter = new QoderAdapter({
|
|
3642
5641
|
resolveTransport: () => activeTransport,
|
|
3643
|
-
|
|
5642
|
+
catalog,
|
|
3644
5643
|
region: () => activeTransportConfig.region,
|
|
3645
5644
|
providerId: providerQoder,
|
|
3646
5645
|
providerName: "Qoder",
|
|
3647
5646
|
sessions: ctx.get("sessions"),
|
|
3648
|
-
agents: ctx.get("agents")
|
|
3649
|
-
onModelsDiscovered: (transport, models) => {
|
|
3650
|
-
if (transport !== activeTransport || ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
3651
|
-
const region = activeTransportConfig.region;
|
|
3652
|
-
discoveredCatalogs[region] = models;
|
|
3653
|
-
refreshAdapter();
|
|
3654
|
-
persistDiscoveredModels(region).catch((error) => {
|
|
3655
|
-
logger?.error?.("[Qoder Settings] Failed to synchronize model catalog", logError(error));
|
|
3656
|
-
});
|
|
3657
|
-
}
|
|
5647
|
+
agents: ctx.get("agents")
|
|
3658
5648
|
});
|
|
3659
5649
|
const registration = ctx.llm.registerAdapter([providerQoder], adapter);
|
|
5650
|
+
const refreshAuthentication = () => {
|
|
5651
|
+
const value = resolveConfig();
|
|
5652
|
+
activeTransport = createTransport(value.region, value.streamIdleTimeoutMs, value.responseHeaderTimeoutMs, value.preserveThinking);
|
|
5653
|
+
catalog.clearDiscovered();
|
|
5654
|
+
catalog.observeTransport();
|
|
5655
|
+
registration.replace([providerQoder]);
|
|
5656
|
+
};
|
|
5657
|
+
ctx.on("credentials/record-updated", (key) => {
|
|
5658
|
+
if (key === browserAuthorizationKey(activeTransportConfig.region)) {
|
|
5659
|
+
refreshAuthentication();
|
|
5660
|
+
catalog.refresh();
|
|
5661
|
+
}
|
|
5662
|
+
});
|
|
5663
|
+
ctx.on("credentials/reference-updated", (ref) => {
|
|
5664
|
+
if (ref === "QODER_MANAGED_CREDENTIAL") refreshAuthentication();
|
|
5665
|
+
});
|
|
3660
5666
|
const refreshAdapter = () => {
|
|
3661
5667
|
const next = resolveConfig();
|
|
3662
|
-
if (next.region !== activeTransportConfig.region || next.streamIdleTimeoutMs !== activeTransportConfig.streamIdleTimeoutMs || next.responseHeaderTimeoutMs !== activeTransportConfig.responseHeaderTimeoutMs || next.preserveThinking !== activeTransportConfig.preserveThinking) {
|
|
5668
|
+
if (next.region !== activeTransportConfig.region || next.streamIdleTimeoutMs !== activeTransportConfig.streamIdleTimeoutMs || next.responseHeaderTimeoutMs !== activeTransportConfig.responseHeaderTimeoutMs || next.modelQueueMaxWaitMs !== activeTransportConfig.modelQueueMaxWaitMs || next.requestCompression !== activeTransportConfig.requestCompression || next.modelTransport !== activeTransportConfig.modelTransport || next.preserveThinking !== activeTransportConfig.preserveThinking) {
|
|
5669
|
+
authorization.activateRegion(next.region);
|
|
3663
5670
|
activeTransport = createTransport(next.region, next.streamIdleTimeoutMs, next.responseHeaderTimeoutMs, next.preserveThinking);
|
|
3664
5671
|
activeTransportConfig = {
|
|
3665
5672
|
region: next.region,
|
|
3666
5673
|
streamIdleTimeoutMs: next.streamIdleTimeoutMs,
|
|
3667
5674
|
responseHeaderTimeoutMs: next.responseHeaderTimeoutMs,
|
|
3668
|
-
|
|
5675
|
+
modelQueueMaxWaitMs: next.modelQueueMaxWaitMs,
|
|
5676
|
+
preserveThinking: next.preserveThinking,
|
|
5677
|
+
requestCompression: next.requestCompression,
|
|
5678
|
+
modelTransport: next.modelTransport
|
|
3669
5679
|
};
|
|
3670
5680
|
}
|
|
5681
|
+
catalog.observeTransport();
|
|
3671
5682
|
adapter.replaceModels(next.models);
|
|
3672
5683
|
registration.replace([providerQoder]);
|
|
3673
5684
|
};
|
|
@@ -3678,52 +5689,35 @@ function apply(ctx, input = {}) {
|
|
|
3678
5689
|
modelsFor(value, "china");
|
|
3679
5690
|
} });
|
|
3680
5691
|
current = () => scope.get();
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
if (!settingsCtx.settings.writable || discoveredCatalogs[region].length === 0) return;
|
|
5692
|
+
catalog.bindSettings({
|
|
5693
|
+
read: (region) => {
|
|
5694
|
+
if (!settingsCtx.settings.writable) return void 0;
|
|
3685
5695
|
const snapshot = settingsCtx.settings.describe().find((section) => section.ns === namespace);
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
} catch (error) {
|
|
3694
|
-
if (!(error instanceof SettingsConflictError)) throw error;
|
|
3695
|
-
}
|
|
3696
|
-
}
|
|
3697
|
-
};
|
|
3698
|
-
persistDiscoveredModels = persistCatalog;
|
|
5696
|
+
return snapshot === void 0 ? void 0 : {
|
|
5697
|
+
models: modelsFor(snapshot.value, region),
|
|
5698
|
+
revision: snapshot.revision
|
|
5699
|
+
};
|
|
5700
|
+
},
|
|
5701
|
+
write: (region, models, revision) => settingsCtx.settings.update(namespace, { modelsByRegion: { [region]: models } }, revision)
|
|
5702
|
+
});
|
|
3699
5703
|
refreshAdapter();
|
|
3700
|
-
scope.watch(
|
|
3701
|
-
if (
|
|
5704
|
+
scope.watch(() => {
|
|
5705
|
+
if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
3702
5706
|
refreshAdapter();
|
|
3703
|
-
|
|
5707
|
+
catalog.retryPersistence();
|
|
3704
5708
|
});
|
|
3705
|
-
for (const region of ["global", "china"]) {
|
|
3706
|
-
if (discoveredCatalogs[region].length === 0) continue;
|
|
3707
|
-
persistCatalog(region).catch((error) => logger?.error?.("[Qoder Settings] Failed to synchronize model catalog", logError(error)));
|
|
3708
|
-
}
|
|
3709
5709
|
settingsCtx.effect(() => () => {
|
|
3710
|
-
|
|
5710
|
+
catalog.bindSettings(void 0);
|
|
3711
5711
|
if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
3712
|
-
persistDiscoveredModels = async () => {};
|
|
3713
5712
|
current = () => baseConfig;
|
|
3714
5713
|
refreshAdapter();
|
|
3715
5714
|
});
|
|
3716
5715
|
});
|
|
3717
5716
|
const discoverModels = async (signal, suppliedPat) => {
|
|
3718
5717
|
const snapshot = resolveConfig();
|
|
3719
|
-
const snapshotTransport = activeTransport;
|
|
3720
5718
|
const normalizedPat = suppliedPat?.trim();
|
|
3721
|
-
const
|
|
3722
|
-
|
|
3723
|
-
discoveredCatalogs[snapshot.region] = models;
|
|
3724
|
-
refreshAdapter();
|
|
3725
|
-
await persistDiscoveredModels(snapshot.region);
|
|
3726
|
-
return models;
|
|
5719
|
+
const transport = normalizedPat ? createTransport(snapshot.region, snapshot.streamIdleTimeoutMs, snapshot.responseHeaderTimeoutMs, snapshot.preserveThinking, () => Promise.resolve(normalizedPat)) : activeTransport;
|
|
5720
|
+
return catalog.discover(signal, transport);
|
|
3727
5721
|
};
|
|
3728
5722
|
ctx.llm.registerModelDiscovery(settingsNamespace, async (request, signal) => {
|
|
3729
5723
|
if (request.provider !== void 0 && request.provider !== providerQoder) throw new QoderLlmError(`Qoder discovery does not own provider "${request.provider}".`, "INVALID_PROVIDER");
|
|
@@ -3737,14 +5731,54 @@ function apply(ctx, input = {}) {
|
|
|
3737
5731
|
const handler = async (endpoint, payload, signal) => {
|
|
3738
5732
|
if (!isQoderRpcEndpoint(endpoint)) return publicError("UNKNOWN_ENDPOINT", `Unknown endpoint: ${endpoint}`);
|
|
3739
5733
|
if (signal.aborted) return publicError("ABORTED", "Request aborted");
|
|
5734
|
+
if (endpoint === "authorization" || endpoint === "beginLogin" || endpoint === "cancelLogin" || endpoint === "selectAuthorization") {
|
|
5735
|
+
const data = payload;
|
|
5736
|
+
const region = data?.region;
|
|
5737
|
+
if (region !== "global" && region !== "china" || region !== resolveConfig().region) return publicError("INTERNAL", "The active Qoder service region is required");
|
|
5738
|
+
return executeRpc("manage Qoder authorization", async () => {
|
|
5739
|
+
if (endpoint === "beginLogin") return authorization.begin(region);
|
|
5740
|
+
if (endpoint === "cancelLogin") {
|
|
5741
|
+
await authorization.cancel(region);
|
|
5742
|
+
return authorization.describe(region);
|
|
5743
|
+
}
|
|
5744
|
+
if (endpoint === "selectAuthorization") {
|
|
5745
|
+
const method = data?.method;
|
|
5746
|
+
if (method !== "pat" && method !== "browser" && method !== "none") throw new QoderLlmError("Invalid authorization method.", "INVALID_REQUEST");
|
|
5747
|
+
return authorization.select(region, method);
|
|
5748
|
+
}
|
|
5749
|
+
return authorization.describe(region);
|
|
5750
|
+
}, signal, logger);
|
|
5751
|
+
}
|
|
5752
|
+
if (endpoint === "queue") {
|
|
5753
|
+
const sessionId = payload?.sessionId;
|
|
5754
|
+
if (typeof sessionId !== "string" || !sessionId.trim() || sessionId.length > 256) return publicError("INTERNAL", "A valid sessionId is required");
|
|
5755
|
+
return {
|
|
5756
|
+
ok: true,
|
|
5757
|
+
value: queueStatus.read(sessionId)
|
|
5758
|
+
};
|
|
5759
|
+
}
|
|
3740
5760
|
if (endpoint === "models") return await executeRpc("discover Qoder models", () => discoverModels(signal), signal, logger);
|
|
3741
|
-
if (endpoint === "sessionTier") {
|
|
5761
|
+
if (endpoint === "sessionTier" || endpoint === "readSessionTier") {
|
|
3742
5762
|
const data = payload;
|
|
3743
|
-
if (typeof data?.sessionId === "string" && typeof data?.modelId === "string" && typeof data?.tierKey === "string" && (data.region === "global" || data.region === "china")) {
|
|
5763
|
+
if (typeof data?.sessionId === "string" && typeof data?.modelId === "string" && (endpoint === "readSessionTier" || typeof data?.tierKey === "string") && (data.region === "global" || data.region === "china")) {
|
|
3744
5764
|
const { sessionId, modelId, tierKey, region } = data;
|
|
3745
|
-
return await executeRpc("select session context tier", async () => {
|
|
3746
|
-
|
|
3747
|
-
|
|
5765
|
+
return await executeRpc(endpoint === "sessionTier" ? "select session context tier" : "read session context tier", async () => {
|
|
5766
|
+
if (!sessionId.trim() || region !== resolveConfig().region) throw new QoderLlmError("Invalid session context tier scope.", "INVALID_REQUEST");
|
|
5767
|
+
const sessions = ctx.get("sessions");
|
|
5768
|
+
if (sessions && !sessions.get(sessionId)) {
|
|
5769
|
+
const controller = ctx.get("sessionController");
|
|
5770
|
+
if (controller) {
|
|
5771
|
+
const result = await controller.resolveAgent(sessionId);
|
|
5772
|
+
if ("error" in result) throw result.error;
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
signal.throwIfAborted();
|
|
5776
|
+
if (endpoint === "sessionTier") adapter.setSessionTier(sessionId, modelId, tierKey, region);
|
|
5777
|
+
return adapter.readSessionTier({
|
|
5778
|
+
sessionId,
|
|
5779
|
+
modelId,
|
|
5780
|
+
region
|
|
5781
|
+
});
|
|
3748
5782
|
}, signal, logger);
|
|
3749
5783
|
}
|
|
3750
5784
|
return publicError("INTERNAL", "Invalid sessionTier payload");
|
|
@@ -3759,7 +5793,7 @@ function apply(ctx, input = {}) {
|
|
|
3759
5793
|
return outcome;
|
|
3760
5794
|
};
|
|
3761
5795
|
try {
|
|
3762
|
-
ctx.effect(() => registerQoderRpc(ctx, handler), "provider-qoder: settings RPC routes");
|
|
5796
|
+
ctx.effect(() => registerQoderRpc(ctx, handler, (listener) => adapter.subscribeSessionTiers(listener)), "provider-qoder: settings RPC routes");
|
|
3763
5797
|
} catch (error) {
|
|
3764
5798
|
logger?.error?.("[Qoder RPC] Failed to register the settings RPC routes", logError(error));
|
|
3765
5799
|
}
|