dsh-provider-qoder 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +476 -169
- package/lib/index.d.ts +6 -0
- package/lib/index.js +701 -452
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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";
|
|
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
3
|
import crypto, { createHash } 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
8
|
import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
|
|
9
9
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
@@ -348,10 +348,177 @@ function getQoderWebSearchUrl(region = "global") {
|
|
|
348
348
|
* @module dsh-provider-qoder/qoder/errors
|
|
349
349
|
*/
|
|
350
350
|
var QoderLlmError = class extends LlmError {
|
|
351
|
+
upstreamCode;
|
|
352
|
+
source;
|
|
353
|
+
httpStatus;
|
|
351
354
|
constructor(message, code = "UNKNOWN_ERROR", options) {
|
|
352
355
|
super(message, code, options);
|
|
353
356
|
}
|
|
354
357
|
};
|
|
358
|
+
const modelBusinessErrors = {
|
|
359
|
+
"103": {
|
|
360
|
+
code: "PROVIDER_ERROR",
|
|
361
|
+
message: "Qoder rejected a duplicate model request."
|
|
362
|
+
},
|
|
363
|
+
"105": {
|
|
364
|
+
code: "AUTH",
|
|
365
|
+
message: "Qoder subscriber authentication has expired."
|
|
366
|
+
},
|
|
367
|
+
"110": {
|
|
368
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
369
|
+
message: "Qoder daily usage limit has been reached."
|
|
370
|
+
},
|
|
371
|
+
"112": {
|
|
372
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
373
|
+
message: "Qoder personal Credits are exhausted."
|
|
374
|
+
},
|
|
375
|
+
"114": {
|
|
376
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
377
|
+
message: "Qoder free trial account limit has been reached."
|
|
378
|
+
},
|
|
379
|
+
"115": {
|
|
380
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
381
|
+
message: "Qoder free subscriber quota limit has been reached."
|
|
382
|
+
},
|
|
383
|
+
"116": {
|
|
384
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
385
|
+
message: "Qoder Teams administrator Credits are exhausted."
|
|
386
|
+
},
|
|
387
|
+
"117": {
|
|
388
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
389
|
+
message: "Qoder Teams member Credits are exhausted."
|
|
390
|
+
},
|
|
391
|
+
"119": {
|
|
392
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
393
|
+
message: "Qoder Vela model free usage limit has been reached."
|
|
394
|
+
},
|
|
395
|
+
"122": {
|
|
396
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
397
|
+
message: "Qoder billing group Credits limit has been reached."
|
|
398
|
+
},
|
|
399
|
+
"406": {
|
|
400
|
+
code: "PROVIDER_ERROR",
|
|
401
|
+
message: "Qoder rejected the request through its content safety policy."
|
|
402
|
+
},
|
|
403
|
+
"416": {
|
|
404
|
+
code: "INVALID_REQUEST",
|
|
405
|
+
message: "Qoder cannot satisfy the requested range."
|
|
406
|
+
},
|
|
407
|
+
"430": {
|
|
408
|
+
code: "INVALID_REQUEST",
|
|
409
|
+
message: "Qoder does not support this request; check client compatibility."
|
|
410
|
+
},
|
|
411
|
+
"10605": {
|
|
412
|
+
code: "RATE_LIMIT",
|
|
413
|
+
message: "Qoder model is queued."
|
|
414
|
+
},
|
|
415
|
+
"100400": {
|
|
416
|
+
code: "PROVIDER_ERROR",
|
|
417
|
+
message: "Qoder custom model service failed."
|
|
418
|
+
},
|
|
419
|
+
"100401": {
|
|
420
|
+
code: "AUTH",
|
|
421
|
+
message: "Qoder custom model authentication failed."
|
|
422
|
+
},
|
|
423
|
+
"100403": {
|
|
424
|
+
code: "PROVIDER_ERROR",
|
|
425
|
+
message: "Qoder custom model is unavailable."
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
/** Decode only model rejection diagnostics, with bounded depth, work and JSON size. */
|
|
429
|
+
function modelErrorDetails(raw, status) {
|
|
430
|
+
let upstreamCode;
|
|
431
|
+
let codeDepth = -1;
|
|
432
|
+
let detail;
|
|
433
|
+
let textCode;
|
|
434
|
+
let delayMs;
|
|
435
|
+
let remaining = 100;
|
|
436
|
+
const seen = /* @__PURE__ */ new Set();
|
|
437
|
+
function visit(value, depth) {
|
|
438
|
+
if (depth > 10 || --remaining < 0) return;
|
|
439
|
+
if (typeof value === "string") {
|
|
440
|
+
const text = value.trim();
|
|
441
|
+
if (!text) return;
|
|
442
|
+
if (text.length <= 16 * 1024) try {
|
|
443
|
+
visit(JSON.parse(text), depth + 1);
|
|
444
|
+
return;
|
|
445
|
+
} catch {}
|
|
446
|
+
const diagnostic = text.slice(0, 16 * 1024);
|
|
447
|
+
if (isContextWindowExceededError(diagnostic)) textCode = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
448
|
+
else if (textCode === void 0 && isQuotaExceededError(diagnostic)) textCode = QUOTA_EXCEEDED_CODE;
|
|
449
|
+
detail = text.slice(0, 300);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || seen.has(value)) return;
|
|
453
|
+
seen.add(value);
|
|
454
|
+
const obj = value;
|
|
455
|
+
for (const key of [
|
|
456
|
+
"code",
|
|
457
|
+
"error_code",
|
|
458
|
+
"errorCode",
|
|
459
|
+
"errCode"
|
|
460
|
+
]) {
|
|
461
|
+
const candidate = obj[key];
|
|
462
|
+
const code = (typeof candidate === "number" && Number.isFinite(candidate) ? String(candidate) : typeof candidate === "string" ? candidate.trim() : void 0)?.slice(0, 100);
|
|
463
|
+
if (!code || code === "0" || code === String(status) && !Object.hasOwn(modelBusinessErrors, code)) continue;
|
|
464
|
+
if (depth >= codeDepth) {
|
|
465
|
+
upstreamCode = code;
|
|
466
|
+
codeDepth = depth;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const seconds = obj.retryAfterSeconds;
|
|
470
|
+
if (typeof seconds === "number" && Number.isFinite(seconds * 1e3) && seconds > 0) delayMs = Math.max(delayMs ?? 0, seconds * 1e3);
|
|
471
|
+
for (const key of [
|
|
472
|
+
"message",
|
|
473
|
+
"body",
|
|
474
|
+
"details",
|
|
475
|
+
"error",
|
|
476
|
+
"cause",
|
|
477
|
+
"data",
|
|
478
|
+
"qoderApiError",
|
|
479
|
+
"providerError"
|
|
480
|
+
]) visit(obj[key], depth + 1);
|
|
481
|
+
}
|
|
482
|
+
visit(raw, 0);
|
|
483
|
+
return {
|
|
484
|
+
upstreamCode,
|
|
485
|
+
detail,
|
|
486
|
+
textCode,
|
|
487
|
+
delayMs
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
/** Model business semantics precede HTTP status; other transport capabilities keep their own protocols. */
|
|
491
|
+
function qoderModelError(message, response) {
|
|
492
|
+
const details = modelErrorDetails(response.cause, response.status);
|
|
493
|
+
const known = details.upstreamCode === void 0 ? void 0 : Object.hasOwn(modelBusinessErrors, details.upstreamCode) ? modelBusinessErrors[details.upstreamCode] : void 0;
|
|
494
|
+
const fallback = qoderHttpError(message, response);
|
|
495
|
+
let code = fallback.code;
|
|
496
|
+
let summary = details.detail ? `${message}: ${details.detail}` : message;
|
|
497
|
+
if (details.upstreamCode !== void 0) {
|
|
498
|
+
code = known?.code ?? "PROVIDER_ERROR";
|
|
499
|
+
summary = `${known?.message ?? summary} (Qoder code ${details.upstreamCode}).`;
|
|
500
|
+
} else if (details.textCode === CONTEXT_WINDOW_EXCEEDED_CODE) {
|
|
501
|
+
code = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
502
|
+
summary = "Qoder model context window has been exceeded.";
|
|
503
|
+
} else if (details.textCode === QUOTA_EXCEEDED_CODE) {
|
|
504
|
+
code = QUOTA_EXCEEDED_CODE;
|
|
505
|
+
summary = "Qoder subscriber quota is exhausted.";
|
|
506
|
+
}
|
|
507
|
+
const providerRetryAfterMs = Math.max(fallback.failure.providerRetryAfterMs ?? 0, details.delayMs ?? 0) || void 0;
|
|
508
|
+
const error = new QoderLlmError(summary, code, {
|
|
509
|
+
status: response.status,
|
|
510
|
+
...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
|
|
511
|
+
...fallback.failure.requestId === void 0 ? {} : { requestId: fallback.failure.requestId },
|
|
512
|
+
...response.cause === void 0 ? {} : { cause: response.cause }
|
|
513
|
+
});
|
|
514
|
+
error.upstreamCode = details.upstreamCode;
|
|
515
|
+
error.source = response.source;
|
|
516
|
+
error.httpStatus = response.httpStatus ?? (response.source === "http" ? response.status : void 0);
|
|
517
|
+
return error;
|
|
518
|
+
}
|
|
519
|
+
function canRefreshModelCredentials(error) {
|
|
520
|
+
return error.source === "http" && (error.failure.status === 401 || error.failure.status === 403) && error.code === "AUTH" && (error.upstreamCode === void 0 || error.upstreamCode === "105");
|
|
521
|
+
}
|
|
355
522
|
function qoderHttpError(message, response) {
|
|
356
523
|
const { status } = response;
|
|
357
524
|
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 +549,16 @@ function retryAfterMs(value, nowMs = Date.now()) {
|
|
|
382
549
|
}
|
|
383
550
|
//#endregion
|
|
384
551
|
//#region src/qoder/transport/machine-id.ts
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
552
|
+
/** Match the native CLI's region-specific config directory overrides. */
|
|
553
|
+
function qoderMachineIdPaths(region = "global", env = process.env, home = homedir()) {
|
|
554
|
+
const prefix = region === "china" ? "QODERCN_" : "QODER_";
|
|
555
|
+
const cliHome = env[`${prefix}CLI_HOME`] ?? env.GEMINI_CLI_HOME ?? home;
|
|
556
|
+
const configDir = env[`${prefix}CONFIG_DIR`];
|
|
557
|
+
return [join(configDir ? resolve(configDir) : join(cliHome, region === "china" ? ".qoder-cn" : ".qoder"), ".auth", "machine_id"), join(home, ".dsh", "qoder", "machine_id")];
|
|
558
|
+
}
|
|
559
|
+
const ephemeralMachineIds = /* @__PURE__ */ new Map();
|
|
388
560
|
/** Read Qoder's machine id or create the DSH-owned fallback. */
|
|
389
|
-
function getMachineId(paths =
|
|
561
|
+
function getMachineId(paths = qoderMachineIdPaths()) {
|
|
390
562
|
for (const path of paths) {
|
|
391
563
|
if (!existsSync(path)) continue;
|
|
392
564
|
try {
|
|
@@ -394,8 +566,8 @@ function getMachineId(paths = defaultMachineIdPaths()) {
|
|
|
394
566
|
if (value) return value;
|
|
395
567
|
} catch {}
|
|
396
568
|
}
|
|
397
|
-
const machineId = crypto.randomUUID();
|
|
398
569
|
const savePath = paths.at(-1);
|
|
570
|
+
const machineId = (savePath === void 0 ? void 0 : ephemeralMachineIds.get(savePath)) ?? crypto.randomUUID();
|
|
399
571
|
if (savePath !== void 0) try {
|
|
400
572
|
mkdirSync(dirname(savePath), { recursive: true });
|
|
401
573
|
writeFileSync(savePath, machineId, {
|
|
@@ -408,6 +580,7 @@ function getMachineId(paths = defaultMachineIdPaths()) {
|
|
|
408
580
|
if (existing) return existing;
|
|
409
581
|
} catch {}
|
|
410
582
|
}
|
|
583
|
+
if (savePath !== void 0) ephemeralMachineIds.set(savePath, machineId);
|
|
411
584
|
return machineId;
|
|
412
585
|
}
|
|
413
586
|
//#endregion
|
|
@@ -434,7 +607,12 @@ function redactString(value) {
|
|
|
434
607
|
}
|
|
435
608
|
/** Redact credentials and bound arbitrary provider values before logging them. */
|
|
436
609
|
function redactLogValue(value, depth = 0) {
|
|
437
|
-
if (typeof value === "string")
|
|
610
|
+
if (typeof value === "string") {
|
|
611
|
+
if (depth < maxDepth && value.length <= 16 * 1024 && /^[\s]*[{"]/u.test(value)) try {
|
|
612
|
+
return redactString(JSON.stringify(redactLogValue(JSON.parse(value), depth + 1)));
|
|
613
|
+
} catch {}
|
|
614
|
+
return redactString(value);
|
|
615
|
+
}
|
|
438
616
|
if (value === null || typeof value !== "object") return value;
|
|
439
617
|
if (depth >= maxDepth) return "[TRUNCATED]";
|
|
440
618
|
if (value instanceof Error) return {
|
|
@@ -442,6 +620,10 @@ function redactLogValue(value, depth = 0) {
|
|
|
442
620
|
message: redactString(value.message),
|
|
443
621
|
..."code" in value ? { code: redactLogValue(value.code, depth + 1) } : {},
|
|
444
622
|
..."cause" in value ? { cause: redactLogValue(value.cause, depth + 1) } : {},
|
|
623
|
+
..."upstreamCode" in value ? { upstreamCode: redactLogValue(value.upstreamCode, depth + 1) } : {},
|
|
624
|
+
..."source" in value ? { source: value.source } : {},
|
|
625
|
+
..."httpStatus" in value ? { httpStatus: value.httpStatus } : {},
|
|
626
|
+
..."failure" in value ? { failure: redactLogValue(value.failure, depth + 1) } : {},
|
|
445
627
|
...value instanceof AggregateError ? { errors: redactLogValue(value.errors, depth + 1) } : {}
|
|
446
628
|
};
|
|
447
629
|
if (Array.isArray(value)) return value.slice(0, maxEntries).map((item) => redactLogValue(item, depth + 1));
|
|
@@ -483,12 +665,14 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
|
483
665
|
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
|
|
484
666
|
XcW+ML9FoCI6AOvOzwIDAQAB
|
|
485
667
|
-----END PUBLIC KEY-----`;
|
|
486
|
-
const qoderIdeVersion = "1.1.
|
|
668
|
+
const qoderIdeVersion = "1.1.65";
|
|
487
669
|
const defaultUserAgent = `qoder/${qoderIdeVersion}`;
|
|
488
670
|
const qoderDataPolicy = "disagree";
|
|
489
671
|
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
672
|
const qoderMachineTypeMagic = "5";
|
|
673
|
+
function qoderMachineOs(platform = process.platform, arch = process.arch) {
|
|
674
|
+
return `${arch === "arm64" ? "aarch64" : arch === "x64" ? "x86_64" : arch}_${platform}`;
|
|
675
|
+
}
|
|
492
676
|
function computeSigPath(urlStr) {
|
|
493
677
|
let sigPath = new URL(urlStr).pathname;
|
|
494
678
|
if (sigPath.startsWith("/algo")) sigPath = sigPath.substring(5);
|
|
@@ -514,9 +698,9 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
514
698
|
const userInfo = {
|
|
515
699
|
uid: creds.userID,
|
|
516
700
|
security_oauth_token: creds.authToken,
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
701
|
+
...creds.organizationId === void 0 ? {} : { organization_id: creds.organizationId },
|
|
702
|
+
...creds.organizationTags === void 0 ? {} : { organization_tags: creds.organizationTags },
|
|
703
|
+
...creds.dataPolicyAgreed === void 0 ? {} : { data_policy_agreed: creds.dataPolicyAgreed }
|
|
520
704
|
};
|
|
521
705
|
const infoB64 = aesEncryptCBCBase64(JSON.stringify(userInfo), aesKey);
|
|
522
706
|
const cosyKey = rsaEncryptBase64(aesKey);
|
|
@@ -532,9 +716,9 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
532
716
|
const sigPath = computeSigPath(requestURL);
|
|
533
717
|
const sigInput = `${payloadB64}\n${cosyKey}\n${timestamp}\n${body ? Buffer.isBuffer(body) ? body.toString("utf8") : body : ""}\n${sigPath}`;
|
|
534
718
|
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
719
|
const machineID = creds.machineID || getMachineId();
|
|
720
|
+
const isModelRequest = sigPath === "/api/v2/service/pro/sse/agent_chat_generation";
|
|
721
|
+
const machineHostname = hostname().replace(/[^\x20-\x7e]/gu, "").trim();
|
|
538
722
|
return {
|
|
539
723
|
Authorization: `Bearer COSY.${payloadB64}.${sig}`,
|
|
540
724
|
"Cosy-Key": cosyKey,
|
|
@@ -544,17 +728,17 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
544
728
|
"Cosy-Machineid": machineID,
|
|
545
729
|
"Cosy-Machinetoken": machineID,
|
|
546
730
|
"Cosy-Machinetype": qoderMachineTypeMagic,
|
|
547
|
-
"Cosy-Machineos": qoderMachineOs,
|
|
731
|
+
"Cosy-Machineos": qoderMachineOs(),
|
|
732
|
+
...isModelRequest && machineHostname ? { "Cosy-MachineHostname": machineHostname } : {},
|
|
548
733
|
"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()
|
|
734
|
+
"Cosy-Business-Product": "cli",
|
|
735
|
+
"Cosy-Business-Type": "agent",
|
|
736
|
+
"Cosy-Scene": "assistant",
|
|
737
|
+
...isModelRequest ? {} : { "Cosy-Clientip": machineID },
|
|
738
|
+
"Cosy-Data-Policy": creds.dataPolicyAgreed === true ? "agree" : qoderDataPolicy,
|
|
739
|
+
...creds.organizationId ? { "Cosy-Organization-Id": creds.organizationId } : {},
|
|
740
|
+
...creds.organizationTags?.length ? { "Cosy-Organization-Tags": creds.organizationTags.join(",") } : {},
|
|
741
|
+
"Login-Version": qoderLoginVersion
|
|
558
742
|
};
|
|
559
743
|
}
|
|
560
744
|
const defaultResponseHeaderTimeoutMs = 6e4;
|
|
@@ -681,15 +865,12 @@ async function openApiJsonRequest(fetchImpl, options) {
|
|
|
681
865
|
const headers = {
|
|
682
866
|
accept: "application/json",
|
|
683
867
|
"user-agent": options.userAgent ?? defaultUserAgent,
|
|
684
|
-
"cosy-version":
|
|
868
|
+
"cosy-version": qoderIdeVersion,
|
|
869
|
+
"cosy-machineos": qoderMachineOs(),
|
|
685
870
|
"cosy-clienttype": "5",
|
|
686
871
|
...options.headers
|
|
687
872
|
};
|
|
688
873
|
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
874
|
let bodyText;
|
|
694
875
|
if (options.body !== void 0) {
|
|
695
876
|
headers["content-type"] = "application/json";
|
|
@@ -733,16 +914,28 @@ async function openApiJsonRequest(fetchImpl, options) {
|
|
|
733
914
|
const expiryBufferMs = 300 * 1e3;
|
|
734
915
|
const defaultExpiryMs = 1440 * 60 * 1e3;
|
|
735
916
|
const defaultAuthTimeoutMs = 15e3;
|
|
917
|
+
const defaultOrganizationTagsTimeoutMs = 3e3;
|
|
918
|
+
/** Match qodercli's seconds/milliseconds compatibility, preferring absolute expiry. */
|
|
919
|
+
function jobTokenExpiry(data, now = Date.now()) {
|
|
920
|
+
const absolute = data.expires_at ?? data.expiresAt ?? data.expire_time ?? data.expireTime;
|
|
921
|
+
if (absolute !== void 0) {
|
|
922
|
+
const parsed = typeof absolute === "string" ? Date.parse(absolute) : absolute > 0xe8d4a51000 ? absolute : absolute * 1e3;
|
|
923
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
924
|
+
}
|
|
925
|
+
const relative = data.expires_in;
|
|
926
|
+
if (typeof relative === "number" && Number.isFinite(relative) && relative > 0) return now + (relative > 86400 ? relative : relative * 1e3);
|
|
927
|
+
return now + defaultExpiryMs;
|
|
928
|
+
}
|
|
736
929
|
function abortedError() {
|
|
737
930
|
return new QoderLlmError("Qoder authentication was aborted.", "ABORTED");
|
|
738
931
|
}
|
|
739
932
|
async function waitForFlight(promise, signal) {
|
|
740
933
|
if (signal === void 0) return promise;
|
|
741
|
-
if (signal.aborted) throw abortedError();
|
|
742
934
|
return new Promise((resolve, reject) => {
|
|
743
935
|
const onAbort = () => reject(abortedError());
|
|
744
936
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
745
937
|
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
938
|
+
if (signal.aborted) onAbort();
|
|
746
939
|
});
|
|
747
940
|
}
|
|
748
941
|
var QoderAuthService = class {
|
|
@@ -750,14 +943,16 @@ var QoderAuthService = class {
|
|
|
750
943
|
inFlight = /* @__PURE__ */ new Map();
|
|
751
944
|
fetchImpl;
|
|
752
945
|
timeoutMs;
|
|
946
|
+
organizationTagsTimeoutMs;
|
|
753
947
|
resolveMachineId;
|
|
754
948
|
region;
|
|
755
949
|
logger;
|
|
756
950
|
constructor(options = {}) {
|
|
757
951
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
758
952
|
this.timeoutMs = options.timeoutMs ?? defaultAuthTimeoutMs;
|
|
759
|
-
this.
|
|
953
|
+
this.organizationTagsTimeoutMs = options.organizationTagsTimeoutMs ?? defaultOrganizationTagsTimeoutMs;
|
|
760
954
|
this.region = options.region ?? "global";
|
|
955
|
+
this.resolveMachineId = options.resolveMachineId ?? (() => getMachineId(qoderMachineIdPaths(this.region)));
|
|
761
956
|
this.logger = options.logger;
|
|
762
957
|
}
|
|
763
958
|
clear(pat) {
|
|
@@ -778,7 +973,7 @@ var QoderAuthService = class {
|
|
|
778
973
|
created.waiters = 0;
|
|
779
974
|
created.settled = false;
|
|
780
975
|
created.timeout = setTimeout(() => controller.abort("authentication timeout"), this.timeoutMs);
|
|
781
|
-
created.promise = this.exchangeAndResolve(pat, controller.signal).finally(() => {
|
|
976
|
+
created.promise = this.exchangeAndResolve(pat, controller.signal, () => clearTimeout(created.timeout)).finally(() => {
|
|
782
977
|
created.settled = true;
|
|
783
978
|
clearTimeout(created.timeout);
|
|
784
979
|
if (this.inFlight.get(cacheKey) === created) this.inFlight.delete(cacheKey);
|
|
@@ -797,32 +992,45 @@ var QoderAuthService = class {
|
|
|
797
992
|
}
|
|
798
993
|
}
|
|
799
994
|
}
|
|
800
|
-
async exchangeAndResolve(pat, signal) {
|
|
801
|
-
|
|
802
|
-
let expiresAt = Date.now() + defaultExpiryMs;
|
|
995
|
+
async exchangeAndResolve(pat, signal, onAuthenticated) {
|
|
996
|
+
const machineID = this.resolveMachineId().trim();
|
|
803
997
|
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
804
998
|
url: getQoderExchangeUrl(this.region),
|
|
805
|
-
body: {
|
|
999
|
+
body: {
|
|
1000
|
+
personal_token: pat,
|
|
1001
|
+
...machineID && machineID !== "unknown" ? { machine_id: machineID } : {}
|
|
1002
|
+
},
|
|
806
1003
|
signal,
|
|
807
1004
|
timeoutMs: this.timeoutMs,
|
|
808
1005
|
logger: this.logger,
|
|
809
1006
|
operation: "Auth",
|
|
810
1007
|
logCategory: "auth.exchange"
|
|
811
1008
|
});
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1009
|
+
const jobToken = [
|
|
1010
|
+
data.token,
|
|
1011
|
+
data.device_token,
|
|
1012
|
+
data.access_token
|
|
1013
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
1014
|
+
if (!jobToken) throw new QoderLlmError("Qoder PAT exchange returned no job token.", "AUTH");
|
|
1015
|
+
const expiresAt = jobTokenExpiry(data);
|
|
818
1016
|
const userInfo = await retryMetadataRead(signal, () => this.fetchUserInfo(jobToken, signal));
|
|
1017
|
+
if (signal.aborted) throw abortedError();
|
|
1018
|
+
onAuthenticated();
|
|
819
1019
|
const creds = {
|
|
820
1020
|
userID: userInfo.userID,
|
|
821
1021
|
authToken: jobToken,
|
|
822
1022
|
name: userInfo.name || "Qoder User",
|
|
823
1023
|
email: userInfo.email,
|
|
824
|
-
machineID
|
|
1024
|
+
machineID,
|
|
1025
|
+
...userInfo.organizationId === void 0 ? {} : { organizationId: userInfo.organizationId },
|
|
1026
|
+
...userInfo.organizationTags === void 0 ? {} : { organizationTags: userInfo.organizationTags },
|
|
1027
|
+
...userInfo.dataPolicyAgreed === void 0 ? {} : { dataPolicyAgreed: userInfo.dataPolicyAgreed }
|
|
825
1028
|
};
|
|
1029
|
+
if (creds.organizationId && creds.organizationTags === void 0) {
|
|
1030
|
+
const tags = await this.fetchOrganizationTags(jobToken, creds.organizationId, signal);
|
|
1031
|
+
if (tags !== void 0) creds.organizationTags = tags;
|
|
1032
|
+
}
|
|
1033
|
+
if (signal.aborted) throw abortedError();
|
|
826
1034
|
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
827
1035
|
this.cache.set(cacheKey, {
|
|
828
1036
|
creds,
|
|
@@ -840,13 +1048,49 @@ var QoderAuthService = class {
|
|
|
840
1048
|
operation: "UserInfo",
|
|
841
1049
|
logCategory: "auth.user-info"
|
|
842
1050
|
});
|
|
843
|
-
|
|
1051
|
+
const userID = [
|
|
1052
|
+
info.id,
|
|
1053
|
+
info.user_id,
|
|
1054
|
+
info.uid
|
|
1055
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
1056
|
+
const organizationId = [
|
|
1057
|
+
info.orgId,
|
|
1058
|
+
info.organization_id,
|
|
1059
|
+
info.organizationId,
|
|
1060
|
+
info.organization?.org_id,
|
|
1061
|
+
info.organization?.orgId,
|
|
1062
|
+
info.organization?.id
|
|
1063
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
1064
|
+
if (!userID) throw new QoderLlmError("Qoder identity lookup returned no user id.", "AUTH");
|
|
1065
|
+
const organizationTags = Array.isArray(info.organization_tags) ? info.organization_tags.filter((tag) => typeof tag === "string") : void 0;
|
|
844
1066
|
return {
|
|
845
|
-
userID
|
|
1067
|
+
userID,
|
|
846
1068
|
email: info.email ?? "",
|
|
847
|
-
name: info.name ?? info.username ?? ""
|
|
1069
|
+
name: info.name ?? info.username ?? "",
|
|
1070
|
+
...organizationId ? { organizationId } : {},
|
|
1071
|
+
...organizationTags === void 0 ? {} : { organizationTags },
|
|
1072
|
+
...typeof info.data_policy_agreed === "boolean" ? { dataPolicyAgreed: info.data_policy_agreed } : {}
|
|
848
1073
|
};
|
|
849
1074
|
}
|
|
1075
|
+
async fetchOrganizationTags(jobToken, organizationId, signal) {
|
|
1076
|
+
const timeout = AbortSignal.timeout(this.organizationTagsTimeoutMs);
|
|
1077
|
+
const lookupSignal = AbortSignal.any([signal, timeout]);
|
|
1078
|
+
try {
|
|
1079
|
+
const { tags } = await retryMetadataRead(lookupSignal, () => openApiJsonRequest(this.fetchImpl, {
|
|
1080
|
+
url: `${resolveQoderEndpoints(this.region).openApiUrl}/api/v1/organizations/${encodeURIComponent(organizationId)}/tags`,
|
|
1081
|
+
token: jobToken,
|
|
1082
|
+
signal: lookupSignal,
|
|
1083
|
+
timeoutMs: this.organizationTagsTimeoutMs,
|
|
1084
|
+
logger: this.logger,
|
|
1085
|
+
operation: "OrganizationTags"
|
|
1086
|
+
}));
|
|
1087
|
+
return Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [];
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
if (signal.aborted) throw error;
|
|
1090
|
+
this.logger?.warn?.("[Qoder Auth] Organization tags unavailable; retaining subscriber organization.");
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
850
1094
|
};
|
|
851
1095
|
//#endregion
|
|
852
1096
|
//#region src/qoder/transport/catalog-reader.ts
|
|
@@ -900,14 +1144,19 @@ async function fetchQoderModels(credentials, options = {}) {
|
|
|
900
1144
|
function unsupported(message) {
|
|
901
1145
|
return new QoderLlmError(message, "UNSUPPORTED_CONTENT");
|
|
902
1146
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
return text;
|
|
1147
|
+
/** Keep multimodal tool results attached to their call. */
|
|
1148
|
+
async function translateToolResultContent(blocks, context) {
|
|
1149
|
+
const supportedBlocks = blocks.map((block) => {
|
|
1150
|
+
if (block.type === "text" || block.type === "image") return block;
|
|
1151
|
+
throw unsupported(`Qoder tool results support text and images only; received ${String(block.type)} content.`);
|
|
1152
|
+
});
|
|
1153
|
+
const images = supportedBlocks.filter((block) => block.type === "image");
|
|
1154
|
+
if (images.length === 0) return { content: supportedBlocks.map((block) => block.type === "text" ? block.text : "").join("") };
|
|
1155
|
+
if (context.attachments !== void 0) enforceImageLimits(images, context.attachments);
|
|
1156
|
+
return { content: await Promise.all(supportedBlocks.map((block) => block.type === "image" ? resolveImagePart(block, context) : {
|
|
1157
|
+
type: "text",
|
|
1158
|
+
text: block.text
|
|
1159
|
+
})) };
|
|
911
1160
|
}
|
|
912
1161
|
/** Reject a batch that exceeds the deployment image policy before any upload work starts. */
|
|
913
1162
|
function enforceImageLimits(images, attachments) {
|
|
@@ -1000,23 +1249,21 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1000
1249
|
output.push({
|
|
1001
1250
|
role: "tool",
|
|
1002
1251
|
tool_call_id: callId,
|
|
1003
|
-
|
|
1252
|
+
...await translateToolResultContent(result.content, context)
|
|
1004
1253
|
});
|
|
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
1254
|
}
|
|
1017
1255
|
if (nonToolBlocks.length === 0) continue;
|
|
1018
1256
|
}
|
|
1019
1257
|
const contentBlocks = toolResults.length > 0 ? nonToolBlocks : message.content;
|
|
1258
|
+
if (message.role === "tool") {
|
|
1259
|
+
const callId = consumeToolCallId(message, void 0, lastAssistantToolCallIds, output.length);
|
|
1260
|
+
output.push({
|
|
1261
|
+
role: "tool",
|
|
1262
|
+
tool_call_id: callId,
|
|
1263
|
+
...await translateToolResultContent(contentBlocks, context)
|
|
1264
|
+
});
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1020
1267
|
let text = "";
|
|
1021
1268
|
let reasoningText = "";
|
|
1022
1269
|
const userContent = [];
|
|
@@ -1024,7 +1271,6 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1024
1271
|
let hasImage = false;
|
|
1025
1272
|
const toolCalls = [];
|
|
1026
1273
|
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
1274
|
if (block.type === "text") {
|
|
1029
1275
|
text += block.text;
|
|
1030
1276
|
if (message.role === "user" || toolResults.length > 0) userContent.push({
|
|
@@ -1034,7 +1280,7 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1034
1280
|
continue;
|
|
1035
1281
|
}
|
|
1036
1282
|
if (block.type === "image") {
|
|
1037
|
-
if (message.role !== "user" &&
|
|
1283
|
+
if (message.role !== "user" && toolResults.length === 0) throw unsupported("Qoder image content is valid only in user messages.");
|
|
1038
1284
|
hasImage = true;
|
|
1039
1285
|
pendingImages.push({
|
|
1040
1286
|
slot: userContent.length,
|
|
@@ -1071,25 +1317,6 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1071
1317
|
});
|
|
1072
1318
|
continue;
|
|
1073
1319
|
}
|
|
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
1320
|
if (pendingImages.length > 0) {
|
|
1094
1321
|
if (attachments !== void 0) enforceImageLimits(pendingImages.map((pending) => pending.block), attachments);
|
|
1095
1322
|
await Promise.all(pendingImages.map(async (pending) => {
|
|
@@ -1167,10 +1394,10 @@ const injectedContextOpenings = [
|
|
|
1167
1394
|
*/
|
|
1168
1395
|
const hostNotificationTemplate = /^(?:Agent|Background subagent) \S{3,} (?:sent a message:|finished\b)/;
|
|
1169
1396
|
/**
|
|
1170
|
-
*
|
|
1397
|
+
* Compatibility with image markers synthesized by older transport versions.
|
|
1171
1398
|
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1399
|
+
* Current tool images stay in tool-role messages. Historical user-role markers
|
|
1400
|
+
* still belong to the open record rather than to new subscriber input.
|
|
1174
1401
|
*/
|
|
1175
1402
|
const syntheticMarkerTemplate = /^\[\d+ images? returned by the previous tool call\]/;
|
|
1176
1403
|
/** Wire text of one user message, used for injection classification and text extraction. */
|
|
@@ -1245,7 +1472,8 @@ var QoderTurnTracker = class {
|
|
|
1245
1472
|
requestSetId: `qoder-request-${crypto.randomUUID()}`,
|
|
1246
1473
|
chatRecordId: requestId,
|
|
1247
1474
|
businessId: crypto.randomUUID(),
|
|
1248
|
-
beginAt: now
|
|
1475
|
+
beginAt: now,
|
|
1476
|
+
businessStage: "start"
|
|
1249
1477
|
};
|
|
1250
1478
|
if (kind === "auxiliary") {
|
|
1251
1479
|
if (this.sessions.has(sessionId)) this.sessions.get(sessionId).lastTouchedAt = now;
|
|
@@ -1253,7 +1481,8 @@ var QoderTurnTracker = class {
|
|
|
1253
1481
|
requestSetId: `qoder-request-${crypto.randomUUID()}`,
|
|
1254
1482
|
chatRecordId: requestId,
|
|
1255
1483
|
businessId: crypto.randomUUID(),
|
|
1256
|
-
beginAt: now
|
|
1484
|
+
beginAt: now,
|
|
1485
|
+
businessStage: "start"
|
|
1257
1486
|
};
|
|
1258
1487
|
}
|
|
1259
1488
|
let state = this.sessions.get(sessionId);
|
|
@@ -1301,12 +1530,15 @@ var QoderTurnTracker = class {
|
|
|
1301
1530
|
if (current === void 0) state.claimed.delete(fingerprint);
|
|
1302
1531
|
else if (current.count < claimed) state.claimed.set(fingerprint, current.count);
|
|
1303
1532
|
}
|
|
1533
|
+
let businessStage = "processing";
|
|
1304
1534
|
if (opening !== void 0 && unclaimed > 0 || modelChanged) {
|
|
1535
|
+
businessStage = "start";
|
|
1305
1536
|
state.turnCount += 1;
|
|
1306
1537
|
state.currentTurnId = turnIdFor(sessionId, state.turnCount, opening ?? state.currentModel ?? "");
|
|
1307
1538
|
state.currentBusinessId = crypto.randomUUID();
|
|
1308
1539
|
state.turnOpenedAt = now;
|
|
1309
1540
|
} else if (state.currentTurnId === "") {
|
|
1541
|
+
businessStage = "start";
|
|
1310
1542
|
state.turnCount += 1;
|
|
1311
1543
|
state.currentTurnId = turnIdFor(sessionId, state.turnCount, state.currentModel ?? "");
|
|
1312
1544
|
state.currentBusinessId = crypto.randomUUID();
|
|
@@ -1316,7 +1548,8 @@ var QoderTurnTracker = class {
|
|
|
1316
1548
|
requestSetId: state.currentTurnId,
|
|
1317
1549
|
chatRecordId: requestId,
|
|
1318
1550
|
businessId: state.currentBusinessId,
|
|
1319
|
-
beginAt: state.turnOpenedAt
|
|
1551
|
+
beginAt: state.turnOpenedAt,
|
|
1552
|
+
businessStage
|
|
1320
1553
|
};
|
|
1321
1554
|
}
|
|
1322
1555
|
/**
|
|
@@ -1476,7 +1709,10 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
|
|
|
1476
1709
|
chat_prompt: "",
|
|
1477
1710
|
image_urls: null,
|
|
1478
1711
|
aliyun_user_type: "",
|
|
1479
|
-
system: ""
|
|
1712
|
+
system: messages.filter((message) => message.role === "system").flatMap((message) => typeof message.content === "string" && message.content.length > 0 ? [{
|
|
1713
|
+
type: "text",
|
|
1714
|
+
text: message.content
|
|
1715
|
+
}] : []),
|
|
1480
1716
|
messages,
|
|
1481
1717
|
tools,
|
|
1482
1718
|
parameters: {
|
|
@@ -1507,9 +1743,9 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
|
|
|
1507
1743
|
},
|
|
1508
1744
|
business: {
|
|
1509
1745
|
product: "cli",
|
|
1510
|
-
version:
|
|
1746
|
+
version: qoderIdeVersion,
|
|
1511
1747
|
type: "agent",
|
|
1512
|
-
stage:
|
|
1748
|
+
stage: identity.businessStage,
|
|
1513
1749
|
id: identity.businessId,
|
|
1514
1750
|
name: lastUserText.substring(0, 30),
|
|
1515
1751
|
begin_at: identity.beginAt
|
|
@@ -2044,7 +2280,7 @@ var QoderUsageReader = class {
|
|
|
2044
2280
|
const [usage, plan, status] = await Promise.all([
|
|
2045
2281
|
retryMetadataRead(signal, () => this.fetchUsage(creds.authToken, signal)),
|
|
2046
2282
|
this.safeFetchPlan(creds.authToken, signal),
|
|
2047
|
-
this.safeFetchStatus(creds.authToken,
|
|
2283
|
+
this.safeFetchStatus(creds.authToken, signal)
|
|
2048
2284
|
]);
|
|
2049
2285
|
const accountInfo = {
|
|
2050
2286
|
profile,
|
|
@@ -2070,6 +2306,11 @@ var QoderUsageReader = class {
|
|
|
2070
2306
|
* `dedicatedResourcePackages`: those entries carry the campaign identifiers
|
|
2071
2307
|
* that the normalized list deliberately discards, and this payload crosses
|
|
2072
2308
|
* the seam into the browser.
|
|
2309
|
+
*
|
|
2310
|
+
* `addOnQuota` is the pool a plan holds separately from its own subscription
|
|
2311
|
+
* quota. A plan whose subscription quota is zero — Free in particular —
|
|
2312
|
+
* reports its whole balance there, so dropping the bucket left the card with
|
|
2313
|
+
* nothing to render.
|
|
2073
2314
|
*/
|
|
2074
2315
|
async fetchUsage(jobToken, signal) {
|
|
2075
2316
|
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
@@ -2087,6 +2328,7 @@ var QoderUsageReader = class {
|
|
|
2087
2328
|
return {
|
|
2088
2329
|
userQuota: normalizeQuota(data.userQuota),
|
|
2089
2330
|
orgResourcePackage: normalizeQuota(data.orgResourcePackage),
|
|
2331
|
+
addOnQuota: normalizeQuota(data.add_on_quota ?? data.addOnQuota),
|
|
2090
2332
|
...dedicatedResourcePackages !== void 0 ? { dedicatedResourcePackages } : {},
|
|
2091
2333
|
totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
|
|
2092
2334
|
isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
|
|
@@ -2114,11 +2356,10 @@ var QoderUsageReader = class {
|
|
|
2114
2356
|
return;
|
|
2115
2357
|
}
|
|
2116
2358
|
}
|
|
2117
|
-
async fetchStatus(jobToken,
|
|
2359
|
+
async fetchStatus(jobToken, signal) {
|
|
2118
2360
|
return normalizeStatus(await openApiJsonRequest(this.fetchImpl, {
|
|
2119
2361
|
url: getQoderUserStatusUrl(this.region),
|
|
2120
2362
|
token: jobToken,
|
|
2121
|
-
machineId,
|
|
2122
2363
|
signal,
|
|
2123
2364
|
timeoutMs: this.timeoutMs,
|
|
2124
2365
|
logger: this.logger,
|
|
@@ -2126,9 +2367,9 @@ var QoderUsageReader = class {
|
|
|
2126
2367
|
logCategory: "account.status"
|
|
2127
2368
|
}));
|
|
2128
2369
|
}
|
|
2129
|
-
async safeFetchStatus(jobToken,
|
|
2370
|
+
async safeFetchStatus(jobToken, signal) {
|
|
2130
2371
|
try {
|
|
2131
|
-
return await this.fetchStatus(jobToken,
|
|
2372
|
+
return await this.fetchStatus(jobToken, signal);
|
|
2132
2373
|
} catch (error) {
|
|
2133
2374
|
if (signal?.aborted) throw error;
|
|
2134
2375
|
this.logger?.warn?.("[Qoder Status] Failed to load user status (degraded)", error instanceof Error ? error.message : error);
|
|
@@ -2148,23 +2389,27 @@ var QoderUsageReader = class {
|
|
|
2148
2389
|
*/
|
|
2149
2390
|
const qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
|
|
2150
2391
|
const qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
2151
|
-
|
|
2392
|
+
const qoderEncodeTable = /* @__PURE__ */ new Uint8Array(128);
|
|
2393
|
+
for (let i = 0; i < 64; i++) qoderEncodeTable[qoderStdAlphabet.charCodeAt(i)] = qoderCustomAlphabet.charCodeAt(i);
|
|
2394
|
+
qoderEncodeTable["=".charCodeAt(0)] = "$".charCodeAt(0);
|
|
2395
|
+
/** Encode directly into wire bytes without building per-character string ropes. */
|
|
2396
|
+
function qoderEncodeBodyBytes(plaintext) {
|
|
2152
2397
|
const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
|
|
2153
2398
|
const n = std.length;
|
|
2154
2399
|
const a = Math.floor(n / 3);
|
|
2155
|
-
const
|
|
2156
|
-
let
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
else out += c;
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2400
|
+
const out = Buffer.allocUnsafe(n);
|
|
2401
|
+
let offset = 0;
|
|
2402
|
+
const writeRange = (start, end) => {
|
|
2403
|
+
for (let i = start; i < end; i++) out[offset++] = qoderEncodeTable[std.charCodeAt(i)];
|
|
2404
|
+
};
|
|
2405
|
+
writeRange(n - a, n);
|
|
2406
|
+
writeRange(a, n - a);
|
|
2407
|
+
writeRange(0, a);
|
|
2166
2408
|
return out;
|
|
2167
2409
|
}
|
|
2410
|
+
function qoderEncodeBody(plaintext) {
|
|
2411
|
+
return qoderEncodeBodyBytes(plaintext).toString("latin1");
|
|
2412
|
+
}
|
|
2168
2413
|
//#endregion
|
|
2169
2414
|
//#region src/qoder/transport/search.ts
|
|
2170
2415
|
/**
|
|
@@ -2335,163 +2580,6 @@ var QoderSearchClient = class {
|
|
|
2335
2580
|
}
|
|
2336
2581
|
};
|
|
2337
2582
|
//#endregion
|
|
2338
|
-
//#region src/qoder/transport/wire/thinking.ts
|
|
2339
|
-
const thinkingTags = [{
|
|
2340
|
-
open: "<thinking>",
|
|
2341
|
-
close: "</thinking>"
|
|
2342
|
-
}, {
|
|
2343
|
-
open: "<think>",
|
|
2344
|
-
close: "</think>"
|
|
2345
|
-
}];
|
|
2346
|
-
const allTags = thinkingTags.flatMap((tag) => [tag.open, tag.close]);
|
|
2347
|
-
function trailingPrefixLength(text, candidates) {
|
|
2348
|
-
let best = 0;
|
|
2349
|
-
for (const candidate of candidates) {
|
|
2350
|
-
const limit = Math.min(text.length, candidate.length - 1);
|
|
2351
|
-
for (let length = limit; length > best; length--) if (text.endsWith(candidate.slice(0, length))) {
|
|
2352
|
-
best = length;
|
|
2353
|
-
break;
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
return best;
|
|
2357
|
-
}
|
|
2358
|
-
function earliestTag(text, kind) {
|
|
2359
|
-
let found;
|
|
2360
|
-
for (const tag of thinkingTags) {
|
|
2361
|
-
const position = text.indexOf(tag[kind]);
|
|
2362
|
-
if (position !== -1 && (found === void 0 || position < found.position)) found = {
|
|
2363
|
-
position,
|
|
2364
|
-
tag
|
|
2365
|
-
};
|
|
2366
|
-
}
|
|
2367
|
-
return found;
|
|
2368
|
-
}
|
|
2369
|
-
function trimSeparator(text) {
|
|
2370
|
-
if (text.startsWith("\n\n")) return text.slice(2);
|
|
2371
|
-
if (text.startsWith("\n")) return text.slice(1);
|
|
2372
|
-
return text;
|
|
2373
|
-
}
|
|
2374
|
-
var NativeReasoningStripper = class {
|
|
2375
|
-
buffer = "";
|
|
2376
|
-
push(chunk) {
|
|
2377
|
-
this.buffer += chunk;
|
|
2378
|
-
let output = "";
|
|
2379
|
-
while (this.buffer) {
|
|
2380
|
-
let firstPosition = -1;
|
|
2381
|
-
let firstTag = "";
|
|
2382
|
-
for (const tag of allTags) {
|
|
2383
|
-
const position = this.buffer.indexOf(tag);
|
|
2384
|
-
if (position !== -1 && (firstPosition === -1 || position < firstPosition)) {
|
|
2385
|
-
firstPosition = position;
|
|
2386
|
-
firstTag = tag;
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
if (firstPosition !== -1) {
|
|
2390
|
-
output += this.buffer.slice(0, firstPosition);
|
|
2391
|
-
this.buffer = this.buffer.slice(firstPosition + firstTag.length);
|
|
2392
|
-
continue;
|
|
2393
|
-
}
|
|
2394
|
-
const held = trailingPrefixLength(this.buffer, allTags);
|
|
2395
|
-
output += this.buffer.slice(0, this.buffer.length - held);
|
|
2396
|
-
this.buffer = this.buffer.slice(this.buffer.length - held);
|
|
2397
|
-
break;
|
|
2398
|
-
}
|
|
2399
|
-
return output;
|
|
2400
|
-
}
|
|
2401
|
-
finish() {
|
|
2402
|
-
const output = allTags.some((tag) => tag.startsWith(this.buffer)) ? "" : this.buffer;
|
|
2403
|
-
this.buffer = "";
|
|
2404
|
-
return output;
|
|
2405
|
-
}
|
|
2406
|
-
};
|
|
2407
|
-
var QoderThinkingParser = class {
|
|
2408
|
-
buffer = "";
|
|
2409
|
-
mode = "text";
|
|
2410
|
-
closingTag = thinkingTags[0].close;
|
|
2411
|
-
native = new NativeReasoningStripper();
|
|
2412
|
-
pushReasoning(chunk) {
|
|
2413
|
-
const text = this.native.push(chunk);
|
|
2414
|
-
return text ? [{
|
|
2415
|
-
type: "reasoning",
|
|
2416
|
-
text
|
|
2417
|
-
}] : [];
|
|
2418
|
-
}
|
|
2419
|
-
pushContent(chunk) {
|
|
2420
|
-
const output = [];
|
|
2421
|
-
const pendingReasoning = this.native.finish();
|
|
2422
|
-
if (pendingReasoning) output.push({
|
|
2423
|
-
type: "reasoning",
|
|
2424
|
-
text: pendingReasoning
|
|
2425
|
-
});
|
|
2426
|
-
this.buffer += chunk;
|
|
2427
|
-
while (this.buffer) {
|
|
2428
|
-
if (this.mode === "reasoning") {
|
|
2429
|
-
const closeAt = this.buffer.indexOf(this.closingTag);
|
|
2430
|
-
if (closeAt !== -1) {
|
|
2431
|
-
if (closeAt > 0) output.push({
|
|
2432
|
-
type: "reasoning",
|
|
2433
|
-
text: this.buffer.slice(0, closeAt)
|
|
2434
|
-
});
|
|
2435
|
-
this.buffer = trimSeparator(this.buffer.slice(closeAt + this.closingTag.length));
|
|
2436
|
-
this.mode = "text";
|
|
2437
|
-
continue;
|
|
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));
|
|
2466
|
-
continue;
|
|
2467
|
-
}
|
|
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
|
-
}
|
|
2477
|
-
return output;
|
|
2478
|
-
}
|
|
2479
|
-
finish() {
|
|
2480
|
-
const output = [];
|
|
2481
|
-
const pendingReasoning = this.native.finish();
|
|
2482
|
-
if (pendingReasoning) output.push({
|
|
2483
|
-
type: "reasoning",
|
|
2484
|
-
text: pendingReasoning
|
|
2485
|
-
});
|
|
2486
|
-
if (this.buffer) output.push({
|
|
2487
|
-
type: this.mode,
|
|
2488
|
-
text: this.buffer
|
|
2489
|
-
});
|
|
2490
|
-
this.buffer = "";
|
|
2491
|
-
return output;
|
|
2492
|
-
}
|
|
2493
|
-
};
|
|
2494
|
-
//#endregion
|
|
2495
2583
|
//#region src/qoder/transport/wire/sse.ts
|
|
2496
2584
|
/** Decode Qoder's outer SSE envelope and inner model chunks. */
|
|
2497
2585
|
const doneMarker = "[DONE]";
|
|
@@ -2499,31 +2587,7 @@ const defaultMaxSseBufferChars = 2 * 1024 * 1024;
|
|
|
2499
2587
|
function malformed(message) {
|
|
2500
2588
|
return new QoderLlmError(message, "MALFORMED_RESPONSE");
|
|
2501
2589
|
}
|
|
2502
|
-
function
|
|
2503
|
-
const raw = envelope.body ?? envelope.message;
|
|
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;
|
|
2525
|
-
}
|
|
2526
|
-
function parseEnvelope(rawData) {
|
|
2590
|
+
function parseEnvelope(rawData, options) {
|
|
2527
2591
|
let value;
|
|
2528
2592
|
try {
|
|
2529
2593
|
value = JSON.parse(rawData);
|
|
@@ -2534,13 +2598,13 @@ function parseEnvelope(rawData) {
|
|
|
2534
2598
|
const envelope = value;
|
|
2535
2599
|
if (envelope.statusCodeValue !== void 0 && typeof envelope.statusCodeValue !== "number") throw malformed("Qoder SSE envelope has an invalid status code.");
|
|
2536
2600
|
if (envelope.statusCodeValue !== void 0 && (!Number.isInteger(envelope.statusCodeValue) || envelope.statusCodeValue < 100 || envelope.statusCodeValue > 599)) throw malformed("Qoder SSE envelope has an invalid status code.");
|
|
2537
|
-
if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) {
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
}
|
|
2601
|
+
if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) throw qoderModelError(`Qoder service returned upstream error status ${envelope.statusCodeValue}`, {
|
|
2602
|
+
status: envelope.statusCodeValue,
|
|
2603
|
+
cause: envelope.body ?? envelope.message,
|
|
2604
|
+
source: "sse",
|
|
2605
|
+
headers: options.headers,
|
|
2606
|
+
httpStatus: options.httpStatus
|
|
2607
|
+
});
|
|
2544
2608
|
if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
|
|
2545
2609
|
return envelope;
|
|
2546
2610
|
}
|
|
@@ -2587,7 +2651,6 @@ function carriesStreamDelta(delta) {
|
|
|
2587
2651
|
async function* parseQoderSse(stream, options = {}) {
|
|
2588
2652
|
const reader = stream.getReader();
|
|
2589
2653
|
const decoder = new TextDecoder();
|
|
2590
|
-
const thinkingParser = new QoderThinkingParser();
|
|
2591
2654
|
const toolCalls = /* @__PURE__ */ new Map();
|
|
2592
2655
|
let buffer = "";
|
|
2593
2656
|
let sourceEnded = false;
|
|
@@ -2691,7 +2754,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2691
2754
|
sawDone = true;
|
|
2692
2755
|
break;
|
|
2693
2756
|
}
|
|
2694
|
-
const envelope = parseEnvelope(rawData);
|
|
2757
|
+
const envelope = parseEnvelope(rawData, options);
|
|
2695
2758
|
const body = typeof envelope.body === "string" ? envelope.body.trim() : "";
|
|
2696
2759
|
if (!body) continue;
|
|
2697
2760
|
if (body === doneMarker) {
|
|
@@ -2708,8 +2771,14 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2708
2771
|
} else if (carriesStreamDelta(choice.delta)) sawFinishReason = false;
|
|
2709
2772
|
const delta = choice.delta;
|
|
2710
2773
|
if (!delta) continue;
|
|
2711
|
-
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) for (const
|
|
2712
|
-
|
|
2774
|
+
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) for (const chunk of appendSegment({
|
|
2775
|
+
type: "reasoning",
|
|
2776
|
+
text: delta.reasoning_content
|
|
2777
|
+
})) yield chunk;
|
|
2778
|
+
if (typeof delta.content === "string" && delta.content) for (const chunk of appendSegment({
|
|
2779
|
+
type: "text",
|
|
2780
|
+
text: delta.content
|
|
2781
|
+
})) yield chunk;
|
|
2713
2782
|
if (delta.tool_calls !== void 0) {
|
|
2714
2783
|
if (!Array.isArray(delta.tool_calls)) throw malformed("Qoder tool-call delta is not an array.");
|
|
2715
2784
|
for (const rawCall of delta.tool_calls) {
|
|
@@ -2776,7 +2845,6 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2776
2845
|
reader.releaseLock();
|
|
2777
2846
|
}
|
|
2778
2847
|
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
2848
|
for (const chunk of closeTextual()) yield chunk;
|
|
2781
2849
|
for (const [, state] of [...toolCalls.entries()].sort(([left], [right]) => left - right)) {
|
|
2782
2850
|
if (!state.id || !state.name) throw malformed("Qoder completed an unidentifiable tool call.");
|
|
@@ -2836,8 +2904,8 @@ function aborted$1(message) {
|
|
|
2836
2904
|
}
|
|
2837
2905
|
async function* streamQoderChat(options, model, credentials, messages, dependencies) {
|
|
2838
2906
|
const request = await buildQoderRequestBody(options, credentials.userID, messages, model);
|
|
2839
|
-
|
|
2840
|
-
const encodedBytes =
|
|
2907
|
+
request.session_type = dependencies.region === "china" ? "qoderclicn" : "qodercli";
|
|
2908
|
+
const encodedBytes = qoderEncodeBodyBytes(JSON.stringify(request));
|
|
2841
2909
|
const chatUrl = getQoderChatUrl(dependencies.region);
|
|
2842
2910
|
const requestController = new AbortController();
|
|
2843
2911
|
let headerTimedOut = false;
|
|
@@ -2849,7 +2917,7 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2849
2917
|
let lastActivityAt = startedAt;
|
|
2850
2918
|
const onCallerAbort = () => requestController.abort(options.signal?.reason);
|
|
2851
2919
|
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
2852
|
-
|
|
2920
|
+
let headerTimer = setTimeout(() => {
|
|
2853
2921
|
headerTimedOut = true;
|
|
2854
2922
|
requestController.abort("response header timeout");
|
|
2855
2923
|
}, dependencies.responseHeaderTimeoutMs);
|
|
@@ -2872,58 +2940,72 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2872
2940
|
contextTier: markedTiers.length === 1 ? markedTiers[0][0] : null,
|
|
2873
2941
|
context_length: request.parameters.context_length ?? null
|
|
2874
2942
|
}));
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
"
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2943
|
+
let response;
|
|
2944
|
+
let activeCredentials = credentials;
|
|
2945
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2946
|
+
if (options.signal?.aborted) throw aborted$1("Request was aborted.");
|
|
2947
|
+
if (attempt > 0) headerTimer = setTimeout(() => {
|
|
2948
|
+
headerTimedOut = true;
|
|
2949
|
+
requestController.abort("response header timeout");
|
|
2950
|
+
}, dependencies.responseHeaderTimeoutMs);
|
|
2951
|
+
response = await dependencies.fetch(chatUrl, {
|
|
2952
|
+
method: "POST",
|
|
2953
|
+
headers: {
|
|
2954
|
+
"content-type": "application/json",
|
|
2955
|
+
"accept": "text/event-stream",
|
|
2956
|
+
"cache-control": "no-cache",
|
|
2957
|
+
"connection": "keep-alive",
|
|
2958
|
+
"accept-encoding": "identity",
|
|
2959
|
+
"x-model-key": options.model || "cmodel",
|
|
2960
|
+
"x-model-source": model?.source || "system",
|
|
2961
|
+
...attributionHeaders(),
|
|
2962
|
+
...buildAuthHeaders(encodedBytes, chatUrl, activeCredentials)
|
|
2963
|
+
},
|
|
2964
|
+
body: encodedBytes,
|
|
2965
|
+
signal: requestController.signal
|
|
2966
|
+
});
|
|
2967
|
+
clearTimeout(headerTimer);
|
|
2968
|
+
reqId = qoderRequestId(response.headers);
|
|
2969
|
+
dependencies.logger?.debug?.("[Qoder Stream] Response headers received", {
|
|
2970
|
+
region: dependencies.region,
|
|
2971
|
+
status: response.status,
|
|
2972
|
+
attempt: attempt + 1,
|
|
2973
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2974
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
2975
|
+
});
|
|
2976
|
+
resetIdleTimer();
|
|
2977
|
+
if (response.ok) break;
|
|
2900
2978
|
let bodyText = "";
|
|
2901
2979
|
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)}`;
|
|
2980
|
+
bodyText = (await readLimitedText(response, defaultMaxErrorBytes, "Qoder model error response")).trim();
|
|
2981
|
+
} catch (error) {
|
|
2982
|
+
if (requestController.signal.aborted) throw error;
|
|
2914
2983
|
}
|
|
2915
|
-
|
|
2984
|
+
const failure = qoderModelError(`Qoder upstream service returned HTTP ${response.status}`, {
|
|
2916
2985
|
status: response.status,
|
|
2917
2986
|
headers: response.headers,
|
|
2918
|
-
cause: bodyText || void 0
|
|
2987
|
+
cause: bodyText || void 0,
|
|
2988
|
+
source: "http"
|
|
2919
2989
|
});
|
|
2990
|
+
if (attempt === 0 && dependencies.refreshCredentials && canRefreshModelCredentials(failure)) {
|
|
2991
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
2992
|
+
activeCredentials = await dependencies.refreshCredentials(activeCredentials, options.signal);
|
|
2993
|
+
if (activeCredentials.userID !== credentials.userID) throw new QoderLlmError("Qoder subscriber identity changed during authentication recovery.", "AUTH");
|
|
2994
|
+
continue;
|
|
2995
|
+
}
|
|
2996
|
+
throw failure;
|
|
2920
2997
|
}
|
|
2998
|
+
if (!response) throw new QoderLlmError("Qoder response is missing.", "EMPTY_RESPONSE");
|
|
2921
2999
|
if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
|
|
2922
3000
|
let firstChunkDurationMs;
|
|
2923
3001
|
let tokenUsage;
|
|
2924
3002
|
let finishReason;
|
|
2925
3003
|
const streamStartedAt = performance.now();
|
|
2926
|
-
for await (const chunk of parseQoderSse(response.body, {
|
|
3004
|
+
for await (const chunk of parseQoderSse(response.body, {
|
|
3005
|
+
onActivity: resetIdleTimer,
|
|
3006
|
+
headers: response.headers,
|
|
3007
|
+
httpStatus: response.status
|
|
3008
|
+
})) {
|
|
2927
3009
|
chunkCount++;
|
|
2928
3010
|
if (firstChunkDurationMs === void 0) {
|
|
2929
3011
|
firstChunkDurationMs = Math.round(performance.now() - startedAt);
|
|
@@ -3110,7 +3192,13 @@ var DefaultQoderTransport = class {
|
|
|
3110
3192
|
logger: this.logger,
|
|
3111
3193
|
region: this.region,
|
|
3112
3194
|
responseHeaderTimeoutMs: this.responseHeaderTimeoutMs,
|
|
3113
|
-
streamIdleTimeoutMs: this.streamIdleTimeoutMs
|
|
3195
|
+
streamIdleTimeoutMs: this.streamIdleTimeoutMs,
|
|
3196
|
+
refreshCredentials: async (rejected, signal) => {
|
|
3197
|
+
const current = await this.auth.getCredentials(pat, signal);
|
|
3198
|
+
if (current.authToken !== rejected.authToken) return current;
|
|
3199
|
+
this.auth.clear(pat);
|
|
3200
|
+
return this.auth.getCredentials(pat, signal);
|
|
3201
|
+
}
|
|
3114
3202
|
});
|
|
3115
3203
|
}
|
|
3116
3204
|
};
|
|
@@ -3180,6 +3268,149 @@ function modelsFor(config, region) {
|
|
|
3180
3268
|
return resolveModels(config.modelsByRegion?.[region]);
|
|
3181
3269
|
}
|
|
3182
3270
|
//#endregion
|
|
3271
|
+
//#region src/dsh/catalog-lifecycle.ts
|
|
3272
|
+
/** Region-scoped Qoder model discovery, effective catalogs, and advisory persistence. */
|
|
3273
|
+
var QoderCatalogLifecycle = class {
|
|
3274
|
+
options;
|
|
3275
|
+
catalogs = {
|
|
3276
|
+
global: {
|
|
3277
|
+
models: [],
|
|
3278
|
+
generation: 0,
|
|
3279
|
+
expiresAt: 0,
|
|
3280
|
+
dirty: false,
|
|
3281
|
+
retryRequested: false
|
|
3282
|
+
},
|
|
3283
|
+
china: {
|
|
3284
|
+
models: [],
|
|
3285
|
+
generation: 0,
|
|
3286
|
+
expiresAt: 0,
|
|
3287
|
+
dirty: false,
|
|
3288
|
+
retryRequested: false
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3291
|
+
settings;
|
|
3292
|
+
disposed = false;
|
|
3293
|
+
activeTransport;
|
|
3294
|
+
activeRegion;
|
|
3295
|
+
constructor(options) {
|
|
3296
|
+
this.options = options;
|
|
3297
|
+
}
|
|
3298
|
+
/** Observe every transport replacement, including switching away and back. */
|
|
3299
|
+
observeTransport() {
|
|
3300
|
+
const transport = this.options.resolveTransport();
|
|
3301
|
+
const region = this.options.region();
|
|
3302
|
+
if (transport === this.activeTransport && region === this.activeRegion) return;
|
|
3303
|
+
if (this.activeRegion !== void 0) {
|
|
3304
|
+
const previous = this.catalogs[this.activeRegion];
|
|
3305
|
+
previous.generation++;
|
|
3306
|
+
previous.inflight = void 0;
|
|
3307
|
+
}
|
|
3308
|
+
this.activeTransport = transport;
|
|
3309
|
+
this.activeRegion = region;
|
|
3310
|
+
const state = this.catalogs[region];
|
|
3311
|
+
state.generation++;
|
|
3312
|
+
state.expiresAt = 0;
|
|
3313
|
+
state.inflight = void 0;
|
|
3314
|
+
}
|
|
3315
|
+
models() {
|
|
3316
|
+
this.observeTransport();
|
|
3317
|
+
const region = this.options.region();
|
|
3318
|
+
return mergeQoderDiscoveryMetadata(this.options.configuredModels(region), this.catalogs[region].models);
|
|
3319
|
+
}
|
|
3320
|
+
/** Automatic reads join the newest discovery and never wait for settings writes. */
|
|
3321
|
+
async refresh() {
|
|
3322
|
+
while (!this.disposed) {
|
|
3323
|
+
this.observeTransport();
|
|
3324
|
+
const region = this.options.region();
|
|
3325
|
+
const state = this.catalogs[region];
|
|
3326
|
+
const transport = this.activeTransport;
|
|
3327
|
+
const pending = state.inflight ?? (Date.now() >= state.expiresAt ? this.discover() : void 0);
|
|
3328
|
+
await pending?.catch(() => {});
|
|
3329
|
+
this.observeTransport();
|
|
3330
|
+
if (this.activeTransport !== transport || this.activeRegion !== region) continue;
|
|
3331
|
+
if (state.inflight && state.inflight !== pending) continue;
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
/** Explicit discovery supersedes older work as soon as it starts, even if it fails. */
|
|
3336
|
+
discover(signal, requestTransport) {
|
|
3337
|
+
if (this.disposed || signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model discovery was aborted.", "ABORTED"));
|
|
3338
|
+
this.observeTransport();
|
|
3339
|
+
const region = this.options.region();
|
|
3340
|
+
const state = this.catalogs[region];
|
|
3341
|
+
const transport = this.activeTransport;
|
|
3342
|
+
const generation = ++state.generation;
|
|
3343
|
+
const pending = Promise.resolve().then(() => (requestTransport ?? transport).discoverModels(signal)).then((models) => {
|
|
3344
|
+
this.observeTransport();
|
|
3345
|
+
if (!this.disposed && this.activeTransport === transport && this.activeRegion === region && state.generation === generation) {
|
|
3346
|
+
state.models = models.map(cloneCatalogModel);
|
|
3347
|
+
state.expiresAt = Date.now() + 300 * 1e3;
|
|
3348
|
+
state.dirty = true;
|
|
3349
|
+
this.schedulePersistence(region);
|
|
3350
|
+
try {
|
|
3351
|
+
Promise.resolve(this.options.onDiscovered?.(transport, state.models)).catch(() => {});
|
|
3352
|
+
} catch {}
|
|
3353
|
+
}
|
|
3354
|
+
return models;
|
|
3355
|
+
}).finally(() => {
|
|
3356
|
+
if (state.inflight === pending) state.inflight = void 0;
|
|
3357
|
+
});
|
|
3358
|
+
state.inflight = pending;
|
|
3359
|
+
return pending;
|
|
3360
|
+
}
|
|
3361
|
+
bindSettings(settings) {
|
|
3362
|
+
this.settings = settings;
|
|
3363
|
+
this.retryPersistence();
|
|
3364
|
+
}
|
|
3365
|
+
/** Called by settings changes/reattachment; no timer retries or fallback writes. */
|
|
3366
|
+
retryPersistence() {
|
|
3367
|
+
for (const region of ["global", "china"]) {
|
|
3368
|
+
const state = this.catalogs[region];
|
|
3369
|
+
if (state.models.length === 0) continue;
|
|
3370
|
+
state.dirty = true;
|
|
3371
|
+
this.schedulePersistence(region);
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
dispose() {
|
|
3375
|
+
this.disposed = true;
|
|
3376
|
+
this.settings = void 0;
|
|
3377
|
+
for (const state of Object.values(this.catalogs)) state.generation++;
|
|
3378
|
+
}
|
|
3379
|
+
schedulePersistence(region) {
|
|
3380
|
+
const state = this.catalogs[region];
|
|
3381
|
+
const settings = this.settings;
|
|
3382
|
+
if (this.disposed || !settings || !state.dirty) return;
|
|
3383
|
+
if (state.writing) {
|
|
3384
|
+
state.retryRequested = true;
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
state.retryRequested = false;
|
|
3388
|
+
const writing = Promise.resolve().then(() => this.persist(region, settings)).catch((error) => {
|
|
3389
|
+
this.options.onPersistenceError?.(error);
|
|
3390
|
+
}).finally(() => {
|
|
3391
|
+
if (state.writing === writing) state.writing = void 0;
|
|
3392
|
+
if (this.settings !== settings || state.retryRequested) this.schedulePersistence(region);
|
|
3393
|
+
});
|
|
3394
|
+
state.writing = writing;
|
|
3395
|
+
}
|
|
3396
|
+
async persist(region, settings) {
|
|
3397
|
+
const state = this.catalogs[region];
|
|
3398
|
+
while (!this.disposed && this.settings === settings && state.dirty) {
|
|
3399
|
+
const snapshot = settings.read(region);
|
|
3400
|
+
if (!snapshot) return;
|
|
3401
|
+
const enriched = modelsFor(Config({ modelsByRegion: { [region]: mergeQoderDiscoveryMetadata(snapshot.models, state.models) } }), region);
|
|
3402
|
+
state.dirty = false;
|
|
3403
|
+
if (hasSameQoderDiscoveryMetadata(snapshot.models, enriched)) continue;
|
|
3404
|
+
try {
|
|
3405
|
+
await settings.write(region, enriched, snapshot.revision);
|
|
3406
|
+
} catch (error) {
|
|
3407
|
+
state.dirty = true;
|
|
3408
|
+
if (!(error instanceof SettingsConflictError)) throw error;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
};
|
|
3413
|
+
//#endregion
|
|
3183
3414
|
//#region src/dsh/provider.ts
|
|
3184
3415
|
/** DSH model provider route, matching the plugin package name. */
|
|
3185
3416
|
const QODER_PROVIDER_ID = "dsh-provider-qoder";
|
|
@@ -3201,12 +3432,12 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3201
3432
|
catalogModels;
|
|
3202
3433
|
providerId;
|
|
3203
3434
|
providerName;
|
|
3204
|
-
|
|
3205
|
-
discoveries = /* @__PURE__ */ new WeakMap();
|
|
3435
|
+
catalog;
|
|
3206
3436
|
historyRegion;
|
|
3207
3437
|
historyRegionChanged = false;
|
|
3208
3438
|
region;
|
|
3209
3439
|
sessionTiers = /* @__PURE__ */ new Map();
|
|
3440
|
+
tierListeners = /* @__PURE__ */ new Set();
|
|
3210
3441
|
sessions;
|
|
3211
3442
|
agents;
|
|
3212
3443
|
constructor(options) {
|
|
@@ -3215,8 +3446,13 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3215
3446
|
this.catalogModels = options.models && options.models.length > 0 ? options.models : defaultModels;
|
|
3216
3447
|
this.providerId = options.providerId ?? "dsh-provider-qoder";
|
|
3217
3448
|
this.providerName = options.providerName ?? "Qoder";
|
|
3218
|
-
this.onModelsDiscovered = options.onModelsDiscovered;
|
|
3219
3449
|
this.region = options.region ?? (() => "global");
|
|
3450
|
+
this.catalog = options.catalog ?? new QoderCatalogLifecycle({
|
|
3451
|
+
resolveTransport: this.resolveTransport,
|
|
3452
|
+
region: this.region,
|
|
3453
|
+
configuredModels: () => this.catalogModels,
|
|
3454
|
+
onDiscovered: options.onModelsDiscovered
|
|
3455
|
+
});
|
|
3220
3456
|
this.historyRegion = this.region();
|
|
3221
3457
|
this.sessions = options.sessions;
|
|
3222
3458
|
this.agents = options.agents;
|
|
@@ -3229,6 +3465,31 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3229
3465
|
sessionId,
|
|
3230
3466
|
modelId
|
|
3231
3467
|
]), tierKey);
|
|
3468
|
+
for (const listener of this.tierListeners) try {
|
|
3469
|
+
listener({
|
|
3470
|
+
region,
|
|
3471
|
+
sessionId,
|
|
3472
|
+
modelId
|
|
3473
|
+
});
|
|
3474
|
+
} catch {}
|
|
3475
|
+
}
|
|
3476
|
+
subscribeSessionTiers(listener) {
|
|
3477
|
+
this.tierListeners.add(listener);
|
|
3478
|
+
return () => {
|
|
3479
|
+
this.tierListeners.delete(listener);
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
readSessionTier(scope) {
|
|
3483
|
+
const { region, sessionId, modelId } = scope;
|
|
3484
|
+
if (region !== this.region() || !sessionId || this.sessions && !this.sessions.get(sessionId)) throw new QoderLlmError("Invalid session context tier scope.", "INVALID_REQUEST");
|
|
3485
|
+
const model = this.resolveEffectiveModelForSession(modelId, sessionId);
|
|
3486
|
+
const tier = model && resolveContextTier(model, model.contextTier);
|
|
3487
|
+
if (!tier) throw new QoderLlmError("Model has no context tiers.", "INVALID_REQUEST");
|
|
3488
|
+
return {
|
|
3489
|
+
...scope,
|
|
3490
|
+
tierKey: tier.key,
|
|
3491
|
+
tokenCount: tier.tokenCount
|
|
3492
|
+
};
|
|
3232
3493
|
}
|
|
3233
3494
|
getSessionTier(sessionId, modelId) {
|
|
3234
3495
|
return this.sessionTiers.get(JSON.stringify([
|
|
@@ -3265,52 +3526,17 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3265
3526
|
name: this.providerName
|
|
3266
3527
|
};
|
|
3267
3528
|
}
|
|
3268
|
-
/** Refresh advertised metadata on catalog reads, sharing a five-minute transport-local cache. */
|
|
3269
3529
|
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);
|
|
3530
|
+
await this.catalog.refresh();
|
|
3287
3531
|
return this.effectiveModels().map((model) => modelInfo(provider, model));
|
|
3288
3532
|
}
|
|
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
3533
|
effectiveModels() {
|
|
3301
|
-
return
|
|
3534
|
+
return this.catalog.models();
|
|
3302
3535
|
}
|
|
3303
3536
|
replaceModels(models) {
|
|
3304
3537
|
this.observeRegion();
|
|
3305
3538
|
this.catalogModels = models;
|
|
3306
3539
|
}
|
|
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
3540
|
resolveModel(provider, modelId, signal) {
|
|
3315
3541
|
if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
|
|
3316
3542
|
const sessionId = this.agents?.currentInitiator()?.session?.id;
|
|
@@ -3469,7 +3695,9 @@ const qoderRpcApiPath = `/api/qoder-subscription`;
|
|
|
3469
3695
|
const qoderRpcEndpoints = [
|
|
3470
3696
|
"account",
|
|
3471
3697
|
"models",
|
|
3472
|
-
"sessionTier"
|
|
3698
|
+
"sessionTier",
|
|
3699
|
+
"readSessionTier",
|
|
3700
|
+
"sessionTierEvents"
|
|
3473
3701
|
];
|
|
3474
3702
|
/** Absolute Fetch-route path of one endpoint. */
|
|
3475
3703
|
function qoderRpcPath(endpoint) {
|
|
@@ -3492,16 +3720,18 @@ function isQoderRpcEndpoint(value) {
|
|
|
3492
3720
|
* @returns disposer removing every registered route.
|
|
3493
3721
|
* @throws when Connection exposes no exact Fetch registry.
|
|
3494
3722
|
*/
|
|
3495
|
-
function registerQoderRpc(ctx, handler) {
|
|
3723
|
+
function registerQoderRpc(ctx, handler, subscribeTiers) {
|
|
3496
3724
|
const registry = ctx.connection?.fetch;
|
|
3497
3725
|
if (typeof registry?.register !== "function") throw new Error("provider-qoder: connection exposes no exact Fetch route registry");
|
|
3726
|
+
const streams = /* @__PURE__ */ new Set();
|
|
3498
3727
|
const disposers = qoderRpcEndpoints.map((endpoint) => registry.register({
|
|
3499
3728
|
path: qoderRpcPath(endpoint),
|
|
3500
3729
|
methods: ["POST"],
|
|
3501
3730
|
requestBody: "buffered",
|
|
3502
|
-
fetch: (request) => handleQoderRpcRequest(endpoint, handler, request)
|
|
3731
|
+
fetch: (request) => endpoint === "sessionTierEvents" && subscribeTiers ? Promise.resolve(sessionTierEvents(request, subscribeTiers, streams)) : handleQoderRpcRequest(endpoint, handler, request)
|
|
3503
3732
|
}));
|
|
3504
3733
|
return () => {
|
|
3734
|
+
for (const close of streams) close();
|
|
3505
3735
|
for (const dispose of disposers) dispose();
|
|
3506
3736
|
};
|
|
3507
3737
|
}
|
|
@@ -3541,6 +3771,43 @@ async function handleQoderRpcRequest(endpoint, handler, request) {
|
|
|
3541
3771
|
return new Response(`handler failure: ${String(error)}`, { status: 500 });
|
|
3542
3772
|
}
|
|
3543
3773
|
}
|
|
3774
|
+
/** Authenticated change stream; reconnecting clients reread on the initial ready item. */
|
|
3775
|
+
function sessionTierEvents(request, subscribe, streams) {
|
|
3776
|
+
let close = () => {};
|
|
3777
|
+
const encoder = new TextEncoder();
|
|
3778
|
+
const body = new ReadableStream({
|
|
3779
|
+
start(controller) {
|
|
3780
|
+
let ended = false;
|
|
3781
|
+
let unsubscribe = () => {};
|
|
3782
|
+
const send = (value) => {
|
|
3783
|
+
if (!ended) controller.enqueue(encoder.encode(JSON.stringify(value) + "\n"));
|
|
3784
|
+
};
|
|
3785
|
+
close = () => {
|
|
3786
|
+
if (ended) return;
|
|
3787
|
+
ended = true;
|
|
3788
|
+
unsubscribe();
|
|
3789
|
+
request.signal.removeEventListener("abort", close);
|
|
3790
|
+
streams.delete(close);
|
|
3791
|
+
try {
|
|
3792
|
+
controller.close();
|
|
3793
|
+
} catch {}
|
|
3794
|
+
};
|
|
3795
|
+
unsubscribe = subscribe((scope) => send(scope));
|
|
3796
|
+
streams.add(close);
|
|
3797
|
+
request.signal.addEventListener("abort", close, { once: true });
|
|
3798
|
+
if (request.signal.aborted) close();
|
|
3799
|
+
else send({ ready: true });
|
|
3800
|
+
},
|
|
3801
|
+
cancel() {
|
|
3802
|
+
close();
|
|
3803
|
+
}
|
|
3804
|
+
});
|
|
3805
|
+
return new Response(body, { headers: {
|
|
3806
|
+
"content-type": "application/x-ndjson",
|
|
3807
|
+
"cache-control": "no-store",
|
|
3808
|
+
"x-accel-buffering": "no"
|
|
3809
|
+
} });
|
|
3810
|
+
}
|
|
3544
3811
|
//#endregion
|
|
3545
3812
|
//#region src/dsh/plugin.ts
|
|
3546
3813
|
const name = "provider-qoder";
|
|
@@ -3604,11 +3871,6 @@ function apply(ctx, input = {}) {
|
|
|
3604
3871
|
webSearchMode: config.webSearchMode ?? "auto"
|
|
3605
3872
|
};
|
|
3606
3873
|
let current = () => baseConfig;
|
|
3607
|
-
let persistDiscoveredModels = async (_region) => {};
|
|
3608
|
-
const discoveredCatalogs = {
|
|
3609
|
-
global: [],
|
|
3610
|
-
china: []
|
|
3611
|
-
};
|
|
3612
3874
|
const createTransport = (region, streamIdleTimeoutMs, responseHeaderTimeoutMs, preserveThinking, resolvePat = () => resolveManagedQoderPat(ctx.credentials)) => createQoderTransport({
|
|
3613
3875
|
region,
|
|
3614
3876
|
resolvePat,
|
|
@@ -3623,7 +3885,7 @@ function apply(ctx, input = {}) {
|
|
|
3623
3885
|
const region = value.region ?? "global";
|
|
3624
3886
|
return {
|
|
3625
3887
|
region,
|
|
3626
|
-
models:
|
|
3888
|
+
models: modelsFor(value, region),
|
|
3627
3889
|
streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
|
|
3628
3890
|
responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
|
|
3629
3891
|
preserveThinking: value.preserveThinking ?? true,
|
|
@@ -3638,23 +3900,22 @@ function apply(ctx, input = {}) {
|
|
|
3638
3900
|
responseHeaderTimeoutMs: initial.responseHeaderTimeoutMs,
|
|
3639
3901
|
preserveThinking: initial.preserveThinking
|
|
3640
3902
|
};
|
|
3903
|
+
const catalog = new QoderCatalogLifecycle({
|
|
3904
|
+
resolveTransport: () => activeTransport,
|
|
3905
|
+
region: () => activeTransportConfig.region,
|
|
3906
|
+
configuredModels: (region) => modelsFor(current(), region),
|
|
3907
|
+
onDiscovered: () => registration.replace([providerQoder]),
|
|
3908
|
+
onPersistenceError: (error) => logger?.error?.("[Qoder Settings] Failed to synchronize model catalog", logError(error))
|
|
3909
|
+
});
|
|
3910
|
+
ctx.effect(() => () => catalog.dispose(), "provider-qoder: catalog lifecycle");
|
|
3641
3911
|
const adapter = new QoderAdapter({
|
|
3642
3912
|
resolveTransport: () => activeTransport,
|
|
3643
|
-
|
|
3913
|
+
catalog,
|
|
3644
3914
|
region: () => activeTransportConfig.region,
|
|
3645
3915
|
providerId: providerQoder,
|
|
3646
3916
|
providerName: "Qoder",
|
|
3647
3917
|
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
|
-
}
|
|
3918
|
+
agents: ctx.get("agents")
|
|
3658
3919
|
});
|
|
3659
3920
|
const registration = ctx.llm.registerAdapter([providerQoder], adapter);
|
|
3660
3921
|
const refreshAdapter = () => {
|
|
@@ -3668,6 +3929,7 @@ function apply(ctx, input = {}) {
|
|
|
3668
3929
|
preserveThinking: next.preserveThinking
|
|
3669
3930
|
};
|
|
3670
3931
|
}
|
|
3932
|
+
catalog.observeTransport();
|
|
3671
3933
|
adapter.replaceModels(next.models);
|
|
3672
3934
|
registration.replace([providerQoder]);
|
|
3673
3935
|
};
|
|
@@ -3678,52 +3940,35 @@ function apply(ctx, input = {}) {
|
|
|
3678
3940
|
modelsFor(value, "china");
|
|
3679
3941
|
} });
|
|
3680
3942
|
current = () => scope.get();
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
if (!settingsCtx.settings.writable || discoveredCatalogs[region].length === 0) return;
|
|
3943
|
+
catalog.bindSettings({
|
|
3944
|
+
read: (region) => {
|
|
3945
|
+
if (!settingsCtx.settings.writable) return void 0;
|
|
3685
3946
|
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;
|
|
3947
|
+
return snapshot === void 0 ? void 0 : {
|
|
3948
|
+
models: modelsFor(snapshot.value, region),
|
|
3949
|
+
revision: snapshot.revision
|
|
3950
|
+
};
|
|
3951
|
+
},
|
|
3952
|
+
write: (region, models, revision) => settingsCtx.settings.update(namespace, { modelsByRegion: { [region]: models } }, revision)
|
|
3953
|
+
});
|
|
3699
3954
|
refreshAdapter();
|
|
3700
|
-
scope.watch(
|
|
3701
|
-
if (
|
|
3955
|
+
scope.watch(() => {
|
|
3956
|
+
if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
3702
3957
|
refreshAdapter();
|
|
3703
|
-
|
|
3958
|
+
catalog.retryPersistence();
|
|
3704
3959
|
});
|
|
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
3960
|
settingsCtx.effect(() => () => {
|
|
3710
|
-
|
|
3961
|
+
catalog.bindSettings(void 0);
|
|
3711
3962
|
if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
3712
|
-
persistDiscoveredModels = async () => {};
|
|
3713
3963
|
current = () => baseConfig;
|
|
3714
3964
|
refreshAdapter();
|
|
3715
3965
|
});
|
|
3716
3966
|
});
|
|
3717
3967
|
const discoverModels = async (signal, suppliedPat) => {
|
|
3718
3968
|
const snapshot = resolveConfig();
|
|
3719
|
-
const snapshotTransport = activeTransport;
|
|
3720
3969
|
const normalizedPat = suppliedPat?.trim();
|
|
3721
|
-
const
|
|
3722
|
-
|
|
3723
|
-
discoveredCatalogs[snapshot.region] = models;
|
|
3724
|
-
refreshAdapter();
|
|
3725
|
-
await persistDiscoveredModels(snapshot.region);
|
|
3726
|
-
return models;
|
|
3970
|
+
const transport = normalizedPat ? createTransport(snapshot.region, snapshot.streamIdleTimeoutMs, snapshot.responseHeaderTimeoutMs, snapshot.preserveThinking, () => Promise.resolve(normalizedPat)) : activeTransport;
|
|
3971
|
+
return catalog.discover(signal, transport);
|
|
3727
3972
|
};
|
|
3728
3973
|
ctx.llm.registerModelDiscovery(settingsNamespace, async (request, signal) => {
|
|
3729
3974
|
if (request.provider !== void 0 && request.provider !== providerQoder) throw new QoderLlmError(`Qoder discovery does not own provider "${request.provider}".`, "INVALID_PROVIDER");
|
|
@@ -3738,13 +3983,17 @@ function apply(ctx, input = {}) {
|
|
|
3738
3983
|
if (!isQoderRpcEndpoint(endpoint)) return publicError("UNKNOWN_ENDPOINT", `Unknown endpoint: ${endpoint}`);
|
|
3739
3984
|
if (signal.aborted) return publicError("ABORTED", "Request aborted");
|
|
3740
3985
|
if (endpoint === "models") return await executeRpc("discover Qoder models", () => discoverModels(signal), signal, logger);
|
|
3741
|
-
if (endpoint === "sessionTier") {
|
|
3986
|
+
if (endpoint === "sessionTier" || endpoint === "readSessionTier") {
|
|
3742
3987
|
const data = payload;
|
|
3743
|
-
if (typeof data?.sessionId === "string" && typeof data?.modelId === "string" && typeof data?.tierKey === "string" && (data.region === "global" || data.region === "china")) {
|
|
3988
|
+
if (typeof data?.sessionId === "string" && typeof data?.modelId === "string" && (endpoint === "readSessionTier" || typeof data?.tierKey === "string") && (data.region === "global" || data.region === "china")) {
|
|
3744
3989
|
const { sessionId, modelId, tierKey, region } = data;
|
|
3745
|
-
return await executeRpc("select session context tier", async () => {
|
|
3746
|
-
adapter.setSessionTier(sessionId, modelId, tierKey, region);
|
|
3747
|
-
return {
|
|
3990
|
+
return await executeRpc(endpoint === "sessionTier" ? "select session context tier" : "read session context tier", async () => {
|
|
3991
|
+
if (endpoint === "sessionTier") adapter.setSessionTier(sessionId, modelId, tierKey, region);
|
|
3992
|
+
return adapter.readSessionTier({
|
|
3993
|
+
sessionId,
|
|
3994
|
+
modelId,
|
|
3995
|
+
region
|
|
3996
|
+
});
|
|
3748
3997
|
}, signal, logger);
|
|
3749
3998
|
}
|
|
3750
3999
|
return publicError("INTERNAL", "Invalid sessionTier payload");
|
|
@@ -3759,7 +4008,7 @@ function apply(ctx, input = {}) {
|
|
|
3759
4008
|
return outcome;
|
|
3760
4009
|
};
|
|
3761
4010
|
try {
|
|
3762
|
-
ctx.effect(() => registerQoderRpc(ctx, handler), "provider-qoder: settings RPC routes");
|
|
4011
|
+
ctx.effect(() => registerQoderRpc(ctx, handler, (listener) => adapter.subscribeSessionTiers(listener)), "provider-qoder: settings RPC routes");
|
|
3763
4012
|
} catch (error) {
|
|
3764
4013
|
logger?.error?.("[Qoder RPC] Failed to register the settings RPC routes", logError(error));
|
|
3765
4014
|
}
|