dsh-provider-qoder 0.4.3 → 0.5.1
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 +888 -220
- package/lib/index.d.ts +63 -24
- package/lib/index.js +2278 -399
- package/package.json +3 -1
package/lib/index.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, ToolCallId, attributionHeaders,
|
|
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, 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
5
|
import { homedir, hostname } from "node:os";
|
|
6
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`;
|
|
@@ -355,6 +376,15 @@ var QoderLlmError = class extends LlmError {
|
|
|
355
376
|
super(message, code, options);
|
|
356
377
|
}
|
|
357
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
|
+
};
|
|
358
388
|
const modelBusinessErrors = {
|
|
359
389
|
"103": {
|
|
360
390
|
code: "PROVIDER_ERROR",
|
|
@@ -519,6 +549,42 @@ function qoderModelError(message, response) {
|
|
|
519
549
|
function canRefreshModelCredentials(error) {
|
|
520
550
|
return error.source === "http" && (error.failure.status === 401 || error.failure.status === 403) && error.code === "AUTH" && (error.upstreamCode === void 0 || error.upstreamCode === "105");
|
|
521
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
|
+
}
|
|
522
588
|
function qoderHttpError(message, response) {
|
|
523
589
|
const { status } = response;
|
|
524
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";
|
|
@@ -650,6 +716,24 @@ function logParsedResponse(logger, operation, result) {
|
|
|
650
716
|
}));
|
|
651
717
|
}
|
|
652
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
|
|
653
737
|
//#region src/qoder/transport/wire/cosy.ts
|
|
654
738
|
/**
|
|
655
739
|
* COSY authentication headers and signature algorithm for Qoder services.
|
|
@@ -665,14 +749,9 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
|
665
749
|
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
|
|
666
750
|
XcW+ML9FoCI6AOvOzwIDAQAB
|
|
667
751
|
-----END PUBLIC KEY-----`;
|
|
668
|
-
const qoderIdeVersion = "1.1.65";
|
|
669
|
-
const defaultUserAgent = `qoder/${qoderIdeVersion}`;
|
|
670
752
|
const qoderDataPolicy = "disagree";
|
|
671
753
|
const qoderLoginVersion = "v2";
|
|
672
754
|
const qoderMachineTypeMagic = "5";
|
|
673
|
-
function qoderMachineOs(platform = process.platform, arch = process.arch) {
|
|
674
|
-
return `${arch === "arm64" ? "aarch64" : arch === "x64" ? "x86_64" : arch}_${platform}`;
|
|
675
|
-
}
|
|
676
755
|
function computeSigPath(urlStr) {
|
|
677
756
|
let sigPath = new URL(urlStr).pathname;
|
|
678
757
|
if (sigPath.startsWith("/algo")) sigPath = sigPath.substring(5);
|
|
@@ -938,9 +1017,11 @@ async function waitForFlight(promise, signal) {
|
|
|
938
1017
|
if (signal.aborted) onAbort();
|
|
939
1018
|
});
|
|
940
1019
|
}
|
|
941
|
-
var
|
|
1020
|
+
var QoderAuthManager = class {
|
|
942
1021
|
cache = /* @__PURE__ */ new Map();
|
|
943
1022
|
inFlight = /* @__PURE__ */ new Map();
|
|
1023
|
+
epochs = /* @__PURE__ */ new Map();
|
|
1024
|
+
nextVersion = 0;
|
|
944
1025
|
fetchImpl;
|
|
945
1026
|
timeoutMs;
|
|
946
1027
|
organizationTagsTimeoutMs;
|
|
@@ -956,15 +1037,39 @@ var QoderAuthService = class {
|
|
|
956
1037
|
this.logger = options.logger;
|
|
957
1038
|
}
|
|
958
1039
|
clear(pat) {
|
|
959
|
-
if (pat) this.
|
|
960
|
-
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);
|
|
961
1062
|
}
|
|
962
1063
|
async getCredentials(pat, signal) {
|
|
963
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");
|
|
964
1065
|
if (signal?.aborted) throw abortedError();
|
|
965
1066
|
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
966
1067
|
const cached = this.cache.get(cacheKey);
|
|
967
|
-
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
|
+
}
|
|
968
1073
|
let entry = this.inFlight.get(cacheKey);
|
|
969
1074
|
if (entry === void 0 || entry.controller.signal.aborted) {
|
|
970
1075
|
const controller = new AbortController();
|
|
@@ -973,7 +1078,12 @@ var QoderAuthService = class {
|
|
|
973
1078
|
created.waiters = 0;
|
|
974
1079
|
created.settled = false;
|
|
975
1080
|
created.timeout = setTimeout(() => controller.abort("authentication timeout"), this.timeoutMs);
|
|
976
|
-
|
|
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(() => {
|
|
977
1087
|
created.settled = true;
|
|
978
1088
|
clearTimeout(created.timeout);
|
|
979
1089
|
if (this.inFlight.get(cacheKey) === created) this.inFlight.delete(cacheKey);
|
|
@@ -992,7 +1102,7 @@ var QoderAuthService = class {
|
|
|
992
1102
|
}
|
|
993
1103
|
}
|
|
994
1104
|
}
|
|
995
|
-
async exchangeAndResolve(pat, signal, onAuthenticated) {
|
|
1105
|
+
async exchangeAndResolve(pat, signal, onAuthenticated, epoch) {
|
|
996
1106
|
const machineID = this.resolveMachineId().trim();
|
|
997
1107
|
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
998
1108
|
url: getQoderExchangeUrl(this.region),
|
|
@@ -1013,6 +1123,22 @@ var QoderAuthService = class {
|
|
|
1013
1123
|
].find((value) => typeof value === "string" && value.length > 0);
|
|
1014
1124
|
if (!jobToken) throw new QoderLlmError("Qoder PAT exchange returned no job token.", "AUTH");
|
|
1015
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 = () => {}) {
|
|
1016
1142
|
const userInfo = await retryMetadataRead(signal, () => this.fetchUserInfo(jobToken, signal));
|
|
1017
1143
|
if (signal.aborted) throw abortedError();
|
|
1018
1144
|
onAuthenticated();
|
|
@@ -1031,11 +1157,6 @@ var QoderAuthService = class {
|
|
|
1031
1157
|
if (tags !== void 0) creds.organizationTags = tags;
|
|
1032
1158
|
}
|
|
1033
1159
|
if (signal.aborted) throw abortedError();
|
|
1034
|
-
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
1035
|
-
this.cache.set(cacheKey, {
|
|
1036
|
-
creds,
|
|
1037
|
-
expiresAt
|
|
1038
|
-
});
|
|
1039
1160
|
return creds;
|
|
1040
1161
|
}
|
|
1041
1162
|
async fetchUserInfo(jobToken, signal) {
|
|
@@ -1144,6 +1265,9 @@ async function fetchQoderModels(credentials, options = {}) {
|
|
|
1144
1265
|
function unsupported(message) {
|
|
1145
1266
|
return new QoderLlmError(message, "UNSUPPORTED_CONTENT");
|
|
1146
1267
|
}
|
|
1268
|
+
function validateText(block) {
|
|
1269
|
+
if ((block.type === "text" || block.type === "reasoning") && typeof block.text !== "string") throw unsupported("Qoder text and reasoning content must contain a string text value.");
|
|
1270
|
+
}
|
|
1147
1271
|
/** Keep multimodal tool results attached to their call. */
|
|
1148
1272
|
async function translateToolResultContent(blocks, context) {
|
|
1149
1273
|
const supportedBlocks = blocks.map((block) => {
|
|
@@ -1226,7 +1350,49 @@ function consumeToolCallId(message, result, pendingIds, fallbackIndex) {
|
|
|
1226
1350
|
}
|
|
1227
1351
|
return pendingIds.shift() ?? `call_unknown_${fallbackIndex}`;
|
|
1228
1352
|
}
|
|
1353
|
+
/** Validate the entire batch without reading attachments or resolving credentials. */
|
|
1354
|
+
function validateMessageShapes(messages, attachments) {
|
|
1355
|
+
let hasImages = false;
|
|
1356
|
+
const validateImages = (blocks) => {
|
|
1357
|
+
const images = blocks.filter((block) => block.type === "image");
|
|
1358
|
+
hasImages ||= images.length > 0;
|
|
1359
|
+
if (attachments) enforceImageLimits(images, attachments);
|
|
1360
|
+
};
|
|
1361
|
+
for (const message of messages) {
|
|
1362
|
+
if (![
|
|
1363
|
+
"system",
|
|
1364
|
+
"user",
|
|
1365
|
+
"assistant",
|
|
1366
|
+
"tool"
|
|
1367
|
+
].includes(message.role) || !Array.isArray(message.content)) throw unsupported("Qoder received an unsupported message shape.");
|
|
1368
|
+
const results = message.content.filter((block) => block.type === "tool-result");
|
|
1369
|
+
for (const result of results) {
|
|
1370
|
+
if (!Array.isArray(result.content) || result.content.some((block) => block.type !== "text" && block.type !== "image")) throw unsupported("Qoder tool results support text and images only.");
|
|
1371
|
+
for (const block of result.content) validateText(block);
|
|
1372
|
+
validateImages(result.content);
|
|
1373
|
+
}
|
|
1374
|
+
const blocks = message.content.filter((block) => block.type !== "tool-result");
|
|
1375
|
+
for (const block of blocks) {
|
|
1376
|
+
validateText(block);
|
|
1377
|
+
if (block.type === "tool-call") {
|
|
1378
|
+
if (message.role !== "assistant") throw unsupported("Qoder tool-call content is valid only in assistant messages.");
|
|
1379
|
+
if (asCallId(block.id) === void 0 || typeof block.name !== "string" || block.name.trim().length === 0 || typeof block.arguments !== "string") throw unsupported("Qoder tool-call content requires a non-empty string id and name, and string arguments.");
|
|
1380
|
+
}
|
|
1381
|
+
if (message.role === "tool" && block.type !== "text" && block.type !== "image") throw unsupported("Qoder tool results support text and images only.");
|
|
1382
|
+
if (block.type === "image" && message.role !== "user" && message.role !== "tool") throw unsupported("Qoder image content is valid only in user and tool messages.");
|
|
1383
|
+
if (![
|
|
1384
|
+
"text",
|
|
1385
|
+
"image",
|
|
1386
|
+
"reasoning",
|
|
1387
|
+
"tool-call"
|
|
1388
|
+
].includes(block.type)) throw unsupported(`Qoder does not support ${String(block.type)} content.`);
|
|
1389
|
+
}
|
|
1390
|
+
validateImages(blocks);
|
|
1391
|
+
}
|
|
1392
|
+
return hasImages;
|
|
1393
|
+
}
|
|
1229
1394
|
async function validateAndTranslateMessages(messages, systemPrompt, attachments, signal, pipeline) {
|
|
1395
|
+
validateMessageShapes(messages, attachments);
|
|
1230
1396
|
const context = {
|
|
1231
1397
|
attachments,
|
|
1232
1398
|
signal,
|
|
@@ -1280,7 +1446,7 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1280
1446
|
continue;
|
|
1281
1447
|
}
|
|
1282
1448
|
if (block.type === "image") {
|
|
1283
|
-
if (message.role !== "user"
|
|
1449
|
+
if (message.role !== "user") throw unsupported("Qoder image content is valid only in user and tool messages.");
|
|
1284
1450
|
hasImage = true;
|
|
1285
1451
|
pendingImages.push({
|
|
1286
1452
|
slot: userContent.length,
|
|
@@ -1343,20 +1509,15 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1343
1509
|
* request reports — so every step of one turn must carry the same run identity
|
|
1344
1510
|
* while the next subscriber prompt opens a new one.
|
|
1345
1511
|
*
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1348
|
-
*
|
|
1349
|
-
*
|
|
1350
|
-
*
|
|
1351
|
-
* transport-synthesized markers, and anything an auxiliary call carries continue
|
|
1352
|
-
* the open record.
|
|
1512
|
+
* Hosted calls use a captured host run as their authoritative boundary. The
|
|
1513
|
+
* weakly owned identity survives history projections and standalone-cache eviction.
|
|
1514
|
+
* Standalone calls infer boundaries from the multiset of user-role messages:
|
|
1515
|
+
* unseen subscriber input opens a run; recognized host context continues it.
|
|
1516
|
+
* Auxiliary calls always receive independent identities.
|
|
1353
1517
|
*
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
1356
|
-
*
|
|
1357
|
-
* the open record instead of opening a new one. Carrying the host's message
|
|
1358
|
-
* identity through translation is the fix, and needs the host to expose it on
|
|
1359
|
-
* the request the adapter receives.
|
|
1518
|
+
* Standalone limit: text identifies a message because wire messages lack host
|
|
1519
|
+
* message IDs. Identical input replacing an earlier occurrence during compaction
|
|
1520
|
+
* can share the open record. Hosted calls use real boundaries instead.
|
|
1360
1521
|
*
|
|
1361
1522
|
* @module dsh-provider-qoder/qoder/transport/wire/turn-identity
|
|
1362
1523
|
*/
|
|
@@ -1445,14 +1606,14 @@ function messageFingerprint(message) {
|
|
|
1445
1606
|
/**
|
|
1446
1607
|
* Resolves the record identity of the turn one request belongs to.
|
|
1447
1608
|
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1450
|
-
* `maxSessions`.
|
|
1609
|
+
* Standalone session state is bounded by `maxSessions`. Hosted state is weakly
|
|
1610
|
+
* owned by its run handle, which the host and outstanding requests keep alive.
|
|
1451
1611
|
*/
|
|
1452
1612
|
var QoderTurnTracker = class {
|
|
1453
1613
|
maxSessions;
|
|
1454
1614
|
idleSessionTtlMs;
|
|
1455
1615
|
now;
|
|
1616
|
+
hosted = /* @__PURE__ */ new WeakMap();
|
|
1456
1617
|
sessions = /* @__PURE__ */ new Map();
|
|
1457
1618
|
constructor(options = {}) {
|
|
1458
1619
|
this.maxSessions = options.maxSessions ?? defaultMaxSessions;
|
|
@@ -1466,7 +1627,7 @@ var QoderTurnTracker = class {
|
|
|
1466
1627
|
* that requests within one subscriber turn share a single agent run and
|
|
1467
1628
|
* consumption record.
|
|
1468
1629
|
*/
|
|
1469
|
-
resolveTurnIdentity(sessionId, messages, kind = "conversation", requestId = crypto.randomUUID(), modelKey) {
|
|
1630
|
+
resolveTurnIdentity(sessionId, messages, kind = "conversation", requestId = crypto.randomUUID(), modelKey, hostRun) {
|
|
1470
1631
|
const now = this.now();
|
|
1471
1632
|
if (sessionId === void 0 || sessionId.length === 0) return {
|
|
1472
1633
|
requestSetId: `qoder-request-${crypto.randomUUID()}`,
|
|
@@ -1485,6 +1646,7 @@ var QoderTurnTracker = class {
|
|
|
1485
1646
|
businessStage: "start"
|
|
1486
1647
|
};
|
|
1487
1648
|
}
|
|
1649
|
+
if (hostRun !== void 0) return this.resolveHosted(hostRun, sessionId, requestId, modelKey, now);
|
|
1488
1650
|
let state = this.sessions.get(sessionId);
|
|
1489
1651
|
if (state === void 0) {
|
|
1490
1652
|
state = {
|
|
@@ -1552,6 +1714,36 @@ var QoderTurnTracker = class {
|
|
|
1552
1714
|
businessStage
|
|
1553
1715
|
};
|
|
1554
1716
|
}
|
|
1717
|
+
/** Host boundaries are authoritative; message projections cannot open a new run. */
|
|
1718
|
+
resolveHosted(run, scope, requestId, modelKey, now) {
|
|
1719
|
+
let scopes = this.hosted.get(run);
|
|
1720
|
+
if (scopes === void 0) {
|
|
1721
|
+
scopes = /* @__PURE__ */ new Map();
|
|
1722
|
+
this.hosted.set(run, scopes);
|
|
1723
|
+
}
|
|
1724
|
+
const previous = scopes.get(scope);
|
|
1725
|
+
const modelChanged = previous?.modelKey !== void 0 && modelKey !== void 0 && previous.modelKey !== modelKey;
|
|
1726
|
+
if (previous !== void 0 && !modelChanged) {
|
|
1727
|
+
if (modelKey !== void 0) previous.modelKey = modelKey;
|
|
1728
|
+
return {
|
|
1729
|
+
...previous.identity,
|
|
1730
|
+
chatRecordId: requestId,
|
|
1731
|
+
businessStage: "processing"
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
const identity = {
|
|
1735
|
+
requestSetId: `qoder-request-${crypto.randomUUID()}`,
|
|
1736
|
+
chatRecordId: requestId,
|
|
1737
|
+
businessId: crypto.randomUUID(),
|
|
1738
|
+
beginAt: now,
|
|
1739
|
+
businessStage: "start"
|
|
1740
|
+
};
|
|
1741
|
+
scopes.set(scope, {
|
|
1742
|
+
modelKey,
|
|
1743
|
+
identity
|
|
1744
|
+
});
|
|
1745
|
+
return identity;
|
|
1746
|
+
}
|
|
1555
1747
|
/**
|
|
1556
1748
|
* Record id shared by every step of `sessionId`'s current turn.
|
|
1557
1749
|
*
|
|
@@ -1576,13 +1768,14 @@ var QoderTurnTracker = class {
|
|
|
1576
1768
|
beginAt: identity.beginAt
|
|
1577
1769
|
};
|
|
1578
1770
|
}
|
|
1579
|
-
/** Forget one session
|
|
1771
|
+
/** Forget one standalone session. Hosted state follows its run handle lifetime. */
|
|
1580
1772
|
forget(sessionId) {
|
|
1581
1773
|
this.sessions.delete(sessionId);
|
|
1582
1774
|
}
|
|
1583
1775
|
/** Forget every session. */
|
|
1584
1776
|
clear() {
|
|
1585
1777
|
this.sessions.clear();
|
|
1778
|
+
this.hosted = /* @__PURE__ */ new WeakMap();
|
|
1586
1779
|
}
|
|
1587
1780
|
claimSession(sessionId, state) {
|
|
1588
1781
|
this.sessions.set(sessionId, state);
|
|
@@ -1613,8 +1806,7 @@ var QoderTurnTracker = class {
|
|
|
1613
1806
|
}
|
|
1614
1807
|
};
|
|
1615
1808
|
//#endregion
|
|
1616
|
-
//#region src/qoder/transport/wire/
|
|
1617
|
-
/** Build the minimum qodercli request envelope for a validated DSH request. */
|
|
1809
|
+
//#region src/qoder/transport/wire/prepare.ts
|
|
1618
1810
|
function stableHash(prefix, ...inputs) {
|
|
1619
1811
|
const hash = crypto.createHash("sha256");
|
|
1620
1812
|
hash.update(prefix);
|
|
@@ -1624,63 +1816,37 @@ function stableHash(prefix, ...inputs) {
|
|
|
1624
1816
|
}
|
|
1625
1817
|
return hash.digest("hex").slice(0, 16);
|
|
1626
1818
|
}
|
|
1627
|
-
/**
|
|
1628
|
-
* Turn identity shared by every request the process serves.
|
|
1629
|
-
*
|
|
1630
|
-
* Qoder's Credits panel aggregates consumption per agent run — the
|
|
1631
|
-
* `business.id` a request reports — so every step of one turn must carry the
|
|
1632
|
-
* same run identity while the next subscriber prompt opens a new one. The
|
|
1633
|
-
* tracker owns that boundary.
|
|
1634
|
-
*/
|
|
1819
|
+
/** One tracker preserves agent-run identity across both model protocols. */
|
|
1635
1820
|
const qoderTurnTracker = new QoderTurnTracker();
|
|
1636
|
-
/**
|
|
1637
|
-
* Whether this request serves the subscriber's turn or the host's bookkeeping.
|
|
1638
|
-
*
|
|
1639
|
-
* The host marks an auxiliary call through `GenerateOptions.purpose`, and those
|
|
1640
|
-
* calls carry a message list of their own under the same session identity. They
|
|
1641
|
-
* must never move the open turn's boundary, or one subscriber prompt would
|
|
1642
|
-
* report two consumption records.
|
|
1643
|
-
*/
|
|
1644
1821
|
function callKind(options) {
|
|
1645
1822
|
return options.purpose === void 0 ? "conversation" : "auxiliary";
|
|
1646
1823
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
*
|
|
1650
|
-
* Image publication needs credentials, so message translation now runs after
|
|
1651
|
-
* authentication. This static pass preserves the guarantee that a request the
|
|
1652
|
-
* provider cannot serve never reaches the network.
|
|
1653
|
-
*/
|
|
1654
|
-
function validateQoderRequestShape(options, model) {
|
|
1824
|
+
function validateQoderRequestShape(options, model, attachments) {
|
|
1825
|
+
const hasImages = validateMessageShapes(options.messages, attachments);
|
|
1655
1826
|
if (options.reasoningEffort !== void 0) {
|
|
1656
1827
|
const effort = String(options.reasoningEffort);
|
|
1657
1828
|
if (!model?.reasoningEfforts?.some((candidate) => candidate.id === effort)) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise reasoning effort "${effort}".`, "UNSUPPORTED_REASONING_EFFORT");
|
|
1658
1829
|
}
|
|
1659
|
-
if (
|
|
1830
|
+
if (hasImages && model?.supportsImages !== true) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise image input.`, "UNSUPPORTED_CONTENT");
|
|
1660
1831
|
}
|
|
1661
|
-
/** Translate a request whose shape has already been validated. */
|
|
1662
1832
|
function translateQoderMessages(options, attachments, pipeline) {
|
|
1663
1833
|
return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal, pipeline);
|
|
1664
1834
|
}
|
|
1665
|
-
async function
|
|
1666
|
-
validateQoderRequestShape(options, model);
|
|
1667
|
-
return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal);
|
|
1668
|
-
}
|
|
1669
|
-
async function buildQoderRequestBody(options, userId, translatedMessages, model, attachments, tracker) {
|
|
1835
|
+
async function prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker = qoderTurnTracker, identityScope = "", hostRun) {
|
|
1670
1836
|
if (!userId) throw new QoderLlmError("Qoder request identity is missing.", "AUTH");
|
|
1837
|
+
if (translatedMessages === void 0) validateQoderRequestShape(options, model);
|
|
1838
|
+
const messages = translatedMessages ?? await translateQoderMessages(options, attachments);
|
|
1671
1839
|
const modelKey = options.model || "cmodel";
|
|
1672
|
-
const messages = translatedMessages ?? await validateQoderRequest(options, model, attachments);
|
|
1673
1840
|
const modelMaxTokens = model?.maxTokens ?? 32768;
|
|
1674
1841
|
const maxTokens = Math.min(options.maxTokens ?? modelMaxTokens, modelMaxTokens);
|
|
1675
1842
|
const isReasoning = options.reasoningEffort !== void 0 || (model?.isReasoning ?? false);
|
|
1676
|
-
const tools = translateTools(options.tools);
|
|
1677
1843
|
const selectedTier = model === void 0 ? void 0 : selectedContextTier(model);
|
|
1678
1844
|
const defaultContexts = Object.values(model?.contextOptions ?? {}).filter((option) => option.isDefault === true && typeof option.tokenCount === "number" && Number.isFinite(option.tokenCount) && option.tokenCount > 0);
|
|
1679
1845
|
const contextConfig = model?.contextOptions === void 0 || selectedTier === void 0 && defaultContexts.length !== 1 ? void 0 : Object.fromEntries(Object.entries(model.contextOptions).map(([key, value]) => [key, {
|
|
1680
1846
|
...value.tokenCount === void 0 ? {} : { token_count: value.tokenCount },
|
|
1681
1847
|
...selectedTier === void 0 ? value.isDefault === void 0 ? {} : { is_default: value.isDefault } : { is_default: key === selectedTier.key }
|
|
1682
1848
|
}]));
|
|
1683
|
-
const
|
|
1849
|
+
const contextLength = selectedTier?.tokenCount ?? model?.contextWindow;
|
|
1684
1850
|
let lastUserText = "";
|
|
1685
1851
|
for (let index = messages.length - 1; index >= 0; index--) if (messages[index].role === "user") {
|
|
1686
1852
|
lastUserText = wireMessageText(messages[index]);
|
|
@@ -1690,57 +1856,27 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
|
|
|
1690
1856
|
const sessionId = options.sessionId === void 0 ? `${stablePart}-${crypto.randomUUID()}` : `${stablePart}-${String(options.sessionId)}`;
|
|
1691
1857
|
const requestId = crypto.randomUUID();
|
|
1692
1858
|
const sessionIdParam = options.sessionId === void 0 ? void 0 : String(options.sessionId);
|
|
1693
|
-
const identity =
|
|
1859
|
+
const identity = tracker.resolveTurnIdentity(sessionIdParam === void 0 ? void 0 : JSON.stringify([
|
|
1860
|
+
identityScope,
|
|
1861
|
+
userId,
|
|
1862
|
+
sessionIdParam
|
|
1863
|
+
]), messages, callKind(options), requestId, modelKey, hostRun);
|
|
1694
1864
|
return {
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
is_retry: false,
|
|
1703
|
-
source: 1,
|
|
1704
|
-
version: "3",
|
|
1705
|
-
session_type: "qodercli",
|
|
1706
|
-
agent_id: "agent_common",
|
|
1707
|
-
task_id: "common",
|
|
1708
|
-
code_language: "",
|
|
1709
|
-
chat_prompt: "",
|
|
1710
|
-
image_urls: null,
|
|
1711
|
-
aliyun_user_type: "",
|
|
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
|
-
}] : []),
|
|
1865
|
+
requestId,
|
|
1866
|
+
requestSetId: identity.requestSetId,
|
|
1867
|
+
chatRecordId: identity.chatRecordId,
|
|
1868
|
+
sessionId,
|
|
1869
|
+
taskId: "common",
|
|
1870
|
+
modelKey,
|
|
1871
|
+
modelSource: model?.source || "system",
|
|
1716
1872
|
messages,
|
|
1717
|
-
tools,
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
},
|
|
1723
|
-
|
|
1724
|
-
chatPrompt: "",
|
|
1725
|
-
imageUrls: null,
|
|
1726
|
-
extra: {
|
|
1727
|
-
context: [],
|
|
1728
|
-
modelConfig: {
|
|
1729
|
-
key: modelKey,
|
|
1730
|
-
is_reasoning: isReasoning
|
|
1731
|
-
},
|
|
1732
|
-
originalContent: lastUserText
|
|
1733
|
-
},
|
|
1734
|
-
features: [],
|
|
1735
|
-
text: lastUserText
|
|
1736
|
-
},
|
|
1737
|
-
model_config: {
|
|
1738
|
-
key: modelKey,
|
|
1739
|
-
is_reasoning: isReasoning,
|
|
1740
|
-
max_output_tokens: maxTokens,
|
|
1741
|
-
source: model?.source || "system",
|
|
1742
|
-
...contextConfig === void 0 ? {} : { context_config: contextConfig }
|
|
1743
|
-
},
|
|
1873
|
+
tools: translateTools(options.tools),
|
|
1874
|
+
maxTokens,
|
|
1875
|
+
...contextLength === void 0 ? {} : { contextLength },
|
|
1876
|
+
...options.reasoningEffort === void 0 ? {} : { reasoningEffort: String(options.reasoningEffort) },
|
|
1877
|
+
isReasoning,
|
|
1878
|
+
...contextConfig === void 0 ? {} : { contextConfig },
|
|
1879
|
+
lastUserText,
|
|
1744
1880
|
business: {
|
|
1745
1881
|
product: "cli",
|
|
1746
1882
|
version: qoderIdeVersion,
|
|
@@ -1952,7 +2088,7 @@ var QoderImageUploader = class {
|
|
|
1952
2088
|
reason: first.reason
|
|
1953
2089
|
});
|
|
1954
2090
|
try {
|
|
1955
|
-
refreshed = await this.refreshCredentials(signal);
|
|
2091
|
+
refreshed = await this.refreshCredentials(credentials, signal);
|
|
1956
2092
|
} catch (error) {
|
|
1957
2093
|
if (signal.aborted) throw aborted$2();
|
|
1958
2094
|
this.warnDegraded("credential refresh failed", error);
|
|
@@ -2263,15 +2399,30 @@ var QoderUsageReader = class {
|
|
|
2263
2399
|
}
|
|
2264
2400
|
async readAccount(pat, options) {
|
|
2265
2401
|
if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid.", "MISSING_CREDENTIAL");
|
|
2266
|
-
|
|
2402
|
+
return this.readWithCredentials(opaqueCredentialKey(pat), (signal) => this.authService.getCredentials(pat, signal), options);
|
|
2403
|
+
}
|
|
2404
|
+
async readWithCredentials(key, resolve, options) {
|
|
2405
|
+
const cacheKey = `${this.region}:${key}`;
|
|
2267
2406
|
if (!options?.force) {
|
|
2268
2407
|
const cached = this.cache.get(cacheKey);
|
|
2269
2408
|
if (cached && cached.expiresAt > Date.now()) return cached.info;
|
|
2270
2409
|
}
|
|
2271
|
-
return this.flights.run(cacheKey, options?.signal,
|
|
2410
|
+
return this.flights.run(cacheKey, options?.signal, async (sharedSignal) => {
|
|
2411
|
+
let credentials;
|
|
2412
|
+
try {
|
|
2413
|
+
return await this.loadAccount(async (signal) => {
|
|
2414
|
+
credentials = await resolve(signal);
|
|
2415
|
+
return credentials;
|
|
2416
|
+
}, sharedSignal, cacheKey);
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
if (!(error instanceof QoderLlmError) || error.code !== "AUTH" || !credentials || !options?.refresh) throw error;
|
|
2419
|
+
const refreshed = await options.refresh(credentials, sharedSignal);
|
|
2420
|
+
return this.loadAccount(async () => refreshed, sharedSignal, cacheKey);
|
|
2421
|
+
}
|
|
2422
|
+
}, () => new QoderLlmError("Qoder account request was aborted.", "ABORTED"));
|
|
2272
2423
|
}
|
|
2273
|
-
async loadAccount(
|
|
2274
|
-
const creds = await
|
|
2424
|
+
async loadAccount(resolve, signal, cacheKey) {
|
|
2425
|
+
const creds = await resolve(signal);
|
|
2275
2426
|
const profile = {
|
|
2276
2427
|
id: creds.userID,
|
|
2277
2428
|
name: creds.name || "Qoder User",
|
|
@@ -2457,7 +2608,7 @@ var QoderSearchClient = class {
|
|
|
2457
2608
|
reason: first.reason
|
|
2458
2609
|
});
|
|
2459
2610
|
try {
|
|
2460
|
-
credentials = await this.refreshCredentials(signal);
|
|
2611
|
+
credentials = await this.refreshCredentials(credentials, signal);
|
|
2461
2612
|
} catch (error) {
|
|
2462
2613
|
if (signal?.aborted) throw searchAborted(signal, error);
|
|
2463
2614
|
throw new WebError(`Qoder credential refresh failed: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
@@ -2580,147 +2731,732 @@ var QoderSearchClient = class {
|
|
|
2580
2731
|
}
|
|
2581
2732
|
};
|
|
2582
2733
|
//#endregion
|
|
2583
|
-
//#region src/qoder/transport/
|
|
2584
|
-
/**
|
|
2585
|
-
const
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
return
|
|
2734
|
+
//#region src/qoder/transport/request-compression.ts
|
|
2735
|
+
/** qodercli-compatible request compression and endpoint capability recovery. */
|
|
2736
|
+
const disabledEndpoints = /* @__PURE__ */ new Set();
|
|
2737
|
+
function endpointKey(protocol, url) {
|
|
2738
|
+
const { origin, pathname } = new URL(url);
|
|
2739
|
+
return `${protocol}:${origin}${pathname}`;
|
|
2589
2740
|
}
|
|
2590
|
-
function
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2741
|
+
function vpcEndpoint(url) {
|
|
2742
|
+
const host = new URL(url).hostname.toLowerCase().replace(/\.$/u, "");
|
|
2743
|
+
return host === "vpc.qoder.com.cn" || host.endsWith(".vpc.qoder.com.cn");
|
|
2744
|
+
}
|
|
2745
|
+
function envInteger(value, fallback, max = Number.MAX_SAFE_INTEGER) {
|
|
2746
|
+
if (value === void 0 || !value.trim()) return fallback;
|
|
2747
|
+
const parsed = Number(value);
|
|
2748
|
+
return Number.isSafeInteger(parsed) && parsed >= 1 && parsed <= max ? parsed : fallback;
|
|
2749
|
+
}
|
|
2750
|
+
function compressionRejection(status, text, protocol) {
|
|
2751
|
+
if (status === 415 && /zstd|content[- ]encoding|compressed (?:request|body)/iu.test(text)) return "unsupported_encoding";
|
|
2752
|
+
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";
|
|
2753
|
+
if (protocol === "legacy" && status === 403) {
|
|
2754
|
+
let payload;
|
|
2755
|
+
try {
|
|
2756
|
+
payload = JSON.parse(text);
|
|
2757
|
+
} catch {
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
if (payload && typeof payload === "object") {
|
|
2761
|
+
const root = payload;
|
|
2762
|
+
const error = root.error && typeof root.error === "object" ? root.error : root;
|
|
2763
|
+
if (String(error.code) === "101" && /signature (?:is )?invalid/iu.test(text)) return "signature_invalid";
|
|
2764
|
+
}
|
|
2596
2765
|
}
|
|
2597
|
-
if (value === null || typeof value !== "object") throw malformed("Malformed outer SSE envelope received from Qoder.");
|
|
2598
|
-
const envelope = value;
|
|
2599
|
-
if (envelope.statusCodeValue !== void 0 && typeof envelope.statusCodeValue !== "number") throw malformed("Qoder SSE envelope has an invalid status code.");
|
|
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.");
|
|
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
|
-
});
|
|
2608
|
-
if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
|
|
2609
|
-
return envelope;
|
|
2610
2766
|
}
|
|
2611
|
-
|
|
2767
|
+
/** Probe only rejection responses, without consuming the transport's response. */
|
|
2768
|
+
async function probe(response, protocol, signal) {
|
|
2769
|
+
if (![
|
|
2770
|
+
400,
|
|
2771
|
+
403,
|
|
2772
|
+
415
|
|
2773
|
+
].includes(response.status)) return void 0;
|
|
2774
|
+
const reader = response.clone().body?.getReader();
|
|
2775
|
+
if (!reader) return void 0;
|
|
2776
|
+
const deadline = AbortSignal.any([signal, AbortSignal.timeout(1e3)]);
|
|
2777
|
+
const decoder = new TextDecoder();
|
|
2778
|
+
let remaining = 8192;
|
|
2779
|
+
let text = "";
|
|
2780
|
+
let truncated = false;
|
|
2781
|
+
const interrupted = () => {
|
|
2782
|
+
reader.cancel().catch(() => void 0);
|
|
2783
|
+
};
|
|
2784
|
+
deadline.addEventListener("abort", interrupted, { once: true });
|
|
2612
2785
|
try {
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2786
|
+
if (deadline.aborted) return void 0;
|
|
2787
|
+
while (remaining > 0 && !deadline.aborted) {
|
|
2788
|
+
const { value, done } = await reader.read();
|
|
2789
|
+
if (done) break;
|
|
2790
|
+
if (value.byteLength >= remaining) truncated = true;
|
|
2791
|
+
text += decoder.decode(value.subarray(0, remaining), { stream: true });
|
|
2792
|
+
remaining -= Math.min(value.byteLength, remaining);
|
|
2793
|
+
try {
|
|
2794
|
+
JSON.parse(text);
|
|
2795
|
+
break;
|
|
2796
|
+
} catch {}
|
|
2797
|
+
}
|
|
2798
|
+
text += decoder.decode();
|
|
2799
|
+
return {
|
|
2800
|
+
status: response.status,
|
|
2801
|
+
contentType: response.headers.get("content-type"),
|
|
2802
|
+
serverRequestId: response.headers.get("x-request-id") ?? void 0,
|
|
2803
|
+
body: text,
|
|
2804
|
+
bodyTruncated: truncated,
|
|
2805
|
+
probeTimedOut: deadline.aborted,
|
|
2806
|
+
reason: deadline.aborted ? void 0 : compressionRejection(response.status, text, protocol)
|
|
2807
|
+
};
|
|
2616
2808
|
} catch {
|
|
2617
|
-
|
|
2809
|
+
return;
|
|
2810
|
+
} finally {
|
|
2811
|
+
deadline.removeEventListener("abort", interrupted);
|
|
2812
|
+
reader.cancel().catch(() => void 0);
|
|
2618
2813
|
}
|
|
2619
2814
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
}
|
|
2635
|
-
function finishKind(value) {
|
|
2636
|
-
if (value === void 0 || value === null || value === "") return void 0;
|
|
2637
|
-
if (value === "stop") return "stop";
|
|
2638
|
-
if (value === "length") return "max-tokens";
|
|
2639
|
-
if (value === "tool_calls" || value === "toolUse" || value === "function_call") return "tool-calls";
|
|
2640
|
-
if (value === "content_filter") throw new QoderLlmError("Qoder blocked the response through its content filter.", "PROVIDER_ERROR");
|
|
2641
|
-
throw malformed(`Qoder returned unknown finish reason "${value}".`);
|
|
2642
|
-
}
|
|
2643
|
-
/** Whether a choice delta streams content, reasoning, or tool-call data. Control-only frames do not. */
|
|
2644
|
-
function carriesStreamDelta(delta) {
|
|
2645
|
-
if (delta == null) return false;
|
|
2646
|
-
if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) return true;
|
|
2647
|
-
if (typeof delta.content === "string" && delta.content) return true;
|
|
2648
|
-
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) return true;
|
|
2649
|
-
return false;
|
|
2650
|
-
}
|
|
2651
|
-
async function* parseQoderSse(stream, options = {}) {
|
|
2652
|
-
const reader = stream.getReader();
|
|
2653
|
-
const decoder = new TextDecoder();
|
|
2654
|
-
const toolCalls = /* @__PURE__ */ new Map();
|
|
2655
|
-
let buffer = "";
|
|
2656
|
-
let sourceEnded = false;
|
|
2657
|
-
let sawDone = false;
|
|
2658
|
-
let nextBlockIndex = 0;
|
|
2659
|
-
let activeTextual;
|
|
2660
|
-
let pendingUsage;
|
|
2661
|
-
let terminalKind = "stop";
|
|
2662
|
-
let sawFinishReason = false;
|
|
2663
|
-
let sawContent = false;
|
|
2664
|
-
const maxBufferChars = options.maxBufferChars ?? defaultMaxSseBufferChars;
|
|
2665
|
-
const closeTextual = () => {
|
|
2666
|
-
if (activeTextual === void 0) return [];
|
|
2667
|
-
const state = activeTextual;
|
|
2668
|
-
activeTextual = void 0;
|
|
2669
|
-
return [{
|
|
2670
|
-
type: "block-end",
|
|
2671
|
-
index: state.index,
|
|
2672
|
-
block: state.type === "text" ? {
|
|
2673
|
-
type: "text",
|
|
2674
|
-
text: state.text
|
|
2675
|
-
} : {
|
|
2676
|
-
type: "reasoning",
|
|
2677
|
-
text: state.text
|
|
2678
|
-
}
|
|
2679
|
-
}];
|
|
2680
|
-
};
|
|
2681
|
-
const appendSegment = (segment) => {
|
|
2682
|
-
if (!segment.text) return [];
|
|
2683
|
-
const chunks = [];
|
|
2684
|
-
sawContent = true;
|
|
2685
|
-
if (activeTextual?.type !== segment.type) {
|
|
2686
|
-
chunks.push(...closeTextual());
|
|
2687
|
-
activeTextual = {
|
|
2688
|
-
type: segment.type,
|
|
2689
|
-
index: nextBlockIndex++,
|
|
2690
|
-
text: ""
|
|
2815
|
+
/** One instance per prepared model request; endpoint decisions survive requests. */
|
|
2816
|
+
var QoderRequestCompression = class {
|
|
2817
|
+
fallbackAttempted = false;
|
|
2818
|
+
pendingConfirmation;
|
|
2819
|
+
compressedEndpoint;
|
|
2820
|
+
diagnostics;
|
|
2821
|
+
async send(options) {
|
|
2822
|
+
const { url, protocol, body, headers, signal, logger, requestId } = options;
|
|
2823
|
+
let identityReason;
|
|
2824
|
+
const dispatch = (wireBody, wireHeaders) => {
|
|
2825
|
+
this.diagnostics = {
|
|
2826
|
+
encoding: new Headers(wireHeaders).get("content-encoding") ?? "identity",
|
|
2827
|
+
originalBytes: Buffer.byteLength(body),
|
|
2828
|
+
sentBytes: typeof wireBody === "string" ? Buffer.byteLength(wireBody) : wireBody.byteLength,
|
|
2829
|
+
...identityReason === void 0 ? {} : { reason: identityReason }
|
|
2691
2830
|
};
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2831
|
+
logger?.debug?.("[Qoder Request] Dispatch", {
|
|
2832
|
+
requestId,
|
|
2833
|
+
protocol,
|
|
2834
|
+
...this.diagnostics
|
|
2835
|
+
});
|
|
2836
|
+
return options.dispatch(wireBody, wireHeaders);
|
|
2837
|
+
};
|
|
2838
|
+
signal.throwIfAborted();
|
|
2839
|
+
const enabled = options.enabled ?? false;
|
|
2840
|
+
const key = endpointKey(protocol, url);
|
|
2841
|
+
const minChars = envInteger(process.env.QODER_ZSTD_REQUEST_BODY_MIN_CHARS, 4096);
|
|
2842
|
+
const level = envInteger(process.env.QODER_ZSTD_REQUEST_BODY_LEVEL, 1, 22);
|
|
2843
|
+
const existingEncoding = new Headers(headers).get("content-encoding");
|
|
2844
|
+
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;
|
|
2845
|
+
if (identityReason) return dispatch(body, headers);
|
|
2846
|
+
let compressed;
|
|
2847
|
+
const original = Buffer.from(body, "utf8");
|
|
2848
|
+
try {
|
|
2849
|
+
compressed = await promisify(zlib.zstdCompress)(original, { params: { [zlib.constants.ZSTD_c_compressionLevel]: level } });
|
|
2850
|
+
} catch {
|
|
2851
|
+
signal.throwIfAborted();
|
|
2852
|
+
identityReason = "compression_error";
|
|
2853
|
+
logger?.warn?.("[Qoder Request] Zstd compression failed; sending identity", {
|
|
2854
|
+
requestId,
|
|
2855
|
+
protocol
|
|
2696
2856
|
});
|
|
2857
|
+
return dispatch(body, headers);
|
|
2697
2858
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2859
|
+
signal.throwIfAborted();
|
|
2860
|
+
if (compressed.byteLength >= original.byteLength) {
|
|
2861
|
+
identityReason = "not_smaller";
|
|
2862
|
+
return dispatch(body, headers);
|
|
2863
|
+
}
|
|
2864
|
+
const identityHeaders = Object.fromEntries(Object.entries(headers).filter(([key]) => !["content-length", "content-encoding"].includes(key.toLowerCase())));
|
|
2865
|
+
logger?.debug?.("[Qoder Request] Sending zstd", {
|
|
2866
|
+
requestId,
|
|
2867
|
+
protocol,
|
|
2868
|
+
originalBytes: original.byteLength,
|
|
2869
|
+
compressedBytes: compressed.byteLength,
|
|
2870
|
+
level
|
|
2707
2871
|
});
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
const chunks = closeTextual();
|
|
2713
|
-
state.blockIndex = nextBlockIndex++;
|
|
2714
|
-
sawContent = true;
|
|
2715
|
-
chunks.push({
|
|
2716
|
-
type: "block-start",
|
|
2717
|
-
index: state.blockIndex,
|
|
2718
|
-
blockType: "tool-call"
|
|
2872
|
+
this.compressedEndpoint = key;
|
|
2873
|
+
const response = await dispatch(new Uint8Array(compressed), {
|
|
2874
|
+
...identityHeaders,
|
|
2875
|
+
"Content-Encoding": "zstd"
|
|
2719
2876
|
});
|
|
2720
|
-
const
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2877
|
+
const rejection = await probe(response, protocol, signal);
|
|
2878
|
+
signal.throwIfAborted();
|
|
2879
|
+
if (rejection && options.onRejection) try {
|
|
2880
|
+
await options.onRejection(rejection);
|
|
2881
|
+
} catch {
|
|
2882
|
+
logger?.warn?.("[Qoder Request] Compression rejection capture failed", {
|
|
2883
|
+
requestId,
|
|
2884
|
+
protocol,
|
|
2885
|
+
status: response.status
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
signal.throwIfAborted();
|
|
2889
|
+
if (!rejection?.reason) return response;
|
|
2890
|
+
if (protocol === "legacy") {
|
|
2891
|
+
response.body?.cancel().catch(() => void 0);
|
|
2892
|
+
this.rejectLegacy(response.status, rejection.reason);
|
|
2893
|
+
}
|
|
2894
|
+
this.fallbackAttempted = true;
|
|
2895
|
+
identityReason = rejection.reason;
|
|
2896
|
+
logger?.debug?.("[Qoder Request] Retrying identity", {
|
|
2897
|
+
requestId,
|
|
2898
|
+
protocol,
|
|
2899
|
+
reason: rejection.reason,
|
|
2900
|
+
status: response.status
|
|
2901
|
+
});
|
|
2902
|
+
response.body?.cancel().catch(() => void 0);
|
|
2903
|
+
const identityResponse = await dispatch(body, identityHeaders);
|
|
2904
|
+
this.pendingConfirmation = key;
|
|
2905
|
+
return identityResponse;
|
|
2906
|
+
}
|
|
2907
|
+
confirm() {
|
|
2908
|
+
if (this.pendingConfirmation) disabledEndpoints.add(this.pendingConfirmation);
|
|
2909
|
+
this.pendingConfirmation = void 0;
|
|
2910
|
+
this.compressedEndpoint = void 0;
|
|
2911
|
+
}
|
|
2912
|
+
/** Legacy may reject the encoding in its SSE envelope before model output. */
|
|
2913
|
+
rejectLegacyError(error) {
|
|
2914
|
+
if (!(error instanceof QoderLlmError) || !this.compressedEndpoint) return;
|
|
2915
|
+
const status = error.failure.status;
|
|
2916
|
+
const cause = error.cause;
|
|
2917
|
+
if (typeof status !== "number") return;
|
|
2918
|
+
const rejection = compressionRejection(status, typeof cause === "string" ? cause : JSON.stringify(cause) ?? "", "legacy");
|
|
2919
|
+
if (rejection) this.rejectLegacy(status, rejection);
|
|
2920
|
+
}
|
|
2921
|
+
rejectLegacy(status, reason) {
|
|
2922
|
+
if (this.compressedEndpoint) disabledEndpoints.add(this.compressedEndpoint);
|
|
2923
|
+
throw new QoderLlmError(`Qoder rejected Zstd (${reason}); retry without compression.`, "TRANSPORT", { status });
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
//#endregion
|
|
2927
|
+
//#region src/qoder/transport/wire/queue.ts
|
|
2928
|
+
/** Decode queue fields from the provider's nested JSON envelopes, never from prose. */
|
|
2929
|
+
function records(value) {
|
|
2930
|
+
const pending = [value];
|
|
2931
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2932
|
+
const result = [];
|
|
2933
|
+
for (let count = 0; pending.length && count < 64; count++) {
|
|
2934
|
+
let next = pending.shift();
|
|
2935
|
+
if (typeof next === "string") {
|
|
2936
|
+
if (next.length > 65536) continue;
|
|
2937
|
+
try {
|
|
2938
|
+
next = JSON.parse(next);
|
|
2939
|
+
} catch {
|
|
2940
|
+
continue;
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
if (!next || typeof next !== "object" || Array.isArray(next) || seen.has(next)) continue;
|
|
2944
|
+
seen.add(next);
|
|
2945
|
+
const record = next;
|
|
2946
|
+
result.push(record);
|
|
2947
|
+
for (const key of [
|
|
2948
|
+
"error",
|
|
2949
|
+
"data",
|
|
2950
|
+
"result",
|
|
2951
|
+
"message",
|
|
2952
|
+
"body",
|
|
2953
|
+
"cause",
|
|
2954
|
+
"queue",
|
|
2955
|
+
"details",
|
|
2956
|
+
"qoderApiError",
|
|
2957
|
+
"providerError"
|
|
2958
|
+
]) if (record[key] !== void 0) pending.push(record[key]);
|
|
2959
|
+
}
|
|
2960
|
+
return result;
|
|
2961
|
+
}
|
|
2962
|
+
function boolean(value) {
|
|
2963
|
+
return value === true || value === "true" ? true : value === false || value === "false" ? false : void 0;
|
|
2964
|
+
}
|
|
2965
|
+
function number(value) {
|
|
2966
|
+
if (typeof value !== "number" && (typeof value !== "string" || !value.trim())) return void 0;
|
|
2967
|
+
const parsed = Number(value);
|
|
2968
|
+
return Number.isFinite(parsed) && parsed >= 0 && parsed <= Number.MAX_SAFE_INTEGER / 1e3 ? parsed : void 0;
|
|
2969
|
+
}
|
|
2970
|
+
function status(record) {
|
|
2971
|
+
const result = {};
|
|
2972
|
+
for (const key of ["isQueued", "serviceAvailable"]) {
|
|
2973
|
+
const value = boolean(record[key]);
|
|
2974
|
+
if (value !== void 0) result[key] = value;
|
|
2975
|
+
}
|
|
2976
|
+
for (const key of [
|
|
2977
|
+
"queueCount",
|
|
2978
|
+
"waitTime",
|
|
2979
|
+
"retryAfterSeconds"
|
|
2980
|
+
]) {
|
|
2981
|
+
const value = number(record[key]);
|
|
2982
|
+
if (value !== void 0) result[key] = value;
|
|
2983
|
+
}
|
|
2984
|
+
for (const key of ["modelKey", "queueType"]) {
|
|
2985
|
+
const value = record[key];
|
|
2986
|
+
if (typeof value === "string" && value.trim() && value.length <= 256) result[key] = value.trim();
|
|
2987
|
+
}
|
|
2988
|
+
return result;
|
|
2989
|
+
}
|
|
2990
|
+
function parseQueueStatus(value) {
|
|
2991
|
+
for (const record of records(value)) {
|
|
2992
|
+
const parsed = status(record);
|
|
2993
|
+
if (parsed.isQueued !== void 0 || parsed.serviceAvailable !== void 0) return parsed;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
function queuedError(value) {
|
|
2997
|
+
const candidates = records(value);
|
|
2998
|
+
if (!candidates.some((record) => String(record.code) === "10605" || boolean(record.isQueued) === true)) return void 0;
|
|
2999
|
+
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) ?? {});
|
|
3000
|
+
}
|
|
3001
|
+
//#endregion
|
|
3002
|
+
//#region src/qoder/transport/queue.ts
|
|
3003
|
+
/** Queue polling and lease cleanup, scoped to one logical model call. */
|
|
3004
|
+
function queueFailure(message, code = "QODER_QUEUE_FAILED") {
|
|
3005
|
+
const error = new QoderLlmError(message, code);
|
|
3006
|
+
error.upstreamCode = "10605";
|
|
3007
|
+
return error;
|
|
3008
|
+
}
|
|
3009
|
+
function checkAbort(signal) {
|
|
3010
|
+
if (signal?.aborted) throw new QoderLlmError("Qoder request was aborted.", "ABORTED");
|
|
3011
|
+
}
|
|
3012
|
+
const queueClock = {
|
|
3013
|
+
now: () => performance.now(),
|
|
3014
|
+
sleep: (ms, signal) => new Promise((resolve, reject) => {
|
|
3015
|
+
if (signal?.aborted) {
|
|
3016
|
+
reject(new QoderLlmError("Qoder request was aborted.", "ABORTED"));
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
const abort = () => {
|
|
3020
|
+
clearTimeout(timer);
|
|
3021
|
+
reject(new QoderLlmError("Qoder request was aborted.", "ABORTED"));
|
|
3022
|
+
};
|
|
3023
|
+
const timer = setTimeout(() => {
|
|
3024
|
+
signal?.removeEventListener("abort", abort);
|
|
3025
|
+
resolve();
|
|
3026
|
+
}, ms);
|
|
3027
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
3028
|
+
})
|
|
3029
|
+
};
|
|
3030
|
+
/** A cancellable deadline with no surviving timers, including failed operations. */
|
|
3031
|
+
async function bounded(ms, signal, run) {
|
|
3032
|
+
checkAbort(signal);
|
|
3033
|
+
const controller = new AbortController();
|
|
3034
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
3035
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3036
|
+
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("Qoder queue operation timed out.")), Math.max(1, Math.ceil(ms)));
|
|
3037
|
+
let stop = () => {};
|
|
3038
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
3039
|
+
stop = () => reject(signal?.aborted ? new QoderLlmError("Qoder request was aborted.", "ABORTED") : queueFailure("Qoder queue operation timed out."));
|
|
3040
|
+
controller.signal.addEventListener("abort", stop, { once: true });
|
|
3041
|
+
});
|
|
3042
|
+
try {
|
|
3043
|
+
return await Promise.race([run(controller.signal), interrupted]);
|
|
3044
|
+
} finally {
|
|
3045
|
+
clearTimeout(timer);
|
|
3046
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3047
|
+
controller.signal.removeEventListener("abort", stop);
|
|
3048
|
+
controller.abort();
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
async function readBody(response, signal) {
|
|
3052
|
+
if (!response.body) return "";
|
|
3053
|
+
const reader = response.body.getReader();
|
|
3054
|
+
const cancel = () => {
|
|
3055
|
+
reader.cancel().catch(() => {});
|
|
3056
|
+
};
|
|
3057
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
3058
|
+
const decoder = new TextDecoder();
|
|
3059
|
+
let bytes = 0;
|
|
3060
|
+
let text = "";
|
|
3061
|
+
try {
|
|
3062
|
+
checkAbort(signal);
|
|
3063
|
+
while (true) {
|
|
3064
|
+
const { done, value } = await reader.read();
|
|
3065
|
+
checkAbort(signal);
|
|
3066
|
+
if (done) return text + decoder.decode();
|
|
3067
|
+
bytes += value.byteLength;
|
|
3068
|
+
if (bytes > 65536) throw queueFailure("Qoder queue response exceeded its size limit.");
|
|
3069
|
+
text += decoder.decode(value, { stream: true });
|
|
3070
|
+
}
|
|
3071
|
+
} finally {
|
|
3072
|
+
signal.removeEventListener("abort", cancel);
|
|
3073
|
+
await reader.cancel().catch(() => {});
|
|
3074
|
+
reader.releaseLock();
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
function queueInterval(seconds) {
|
|
3078
|
+
return Math.min(3e4, Math.max(500, seconds !== void 0 && Number.isFinite(seconds) ? seconds * 1e3 : 3e4));
|
|
3079
|
+
}
|
|
3080
|
+
var ModelQueue = class {
|
|
3081
|
+
callId = randomUUID();
|
|
3082
|
+
options;
|
|
3083
|
+
clock;
|
|
3084
|
+
maxWaitMs;
|
|
3085
|
+
models = /* @__PURE__ */ new Set();
|
|
3086
|
+
deferred = /* @__PURE__ */ new Set();
|
|
3087
|
+
waitMs = 0;
|
|
3088
|
+
recoveries = 0;
|
|
3089
|
+
constructor(options) {
|
|
3090
|
+
this.options = options;
|
|
3091
|
+
this.clock = options.clock ?? queueClock;
|
|
3092
|
+
this.maxWaitMs = options.maxWaitMs ?? 36e5;
|
|
3093
|
+
if (!Number.isSafeInteger(this.maxWaitMs) || this.maxWaitMs <= 0 || this.maxWaitMs > 2147483647) throw new Error("modelQueueMaxWaitMs must be a positive integer no greater than 2147483647");
|
|
3094
|
+
}
|
|
3095
|
+
publish(update) {
|
|
3096
|
+
try {
|
|
3097
|
+
this.options.onUpdate?.(update);
|
|
3098
|
+
} catch {
|
|
3099
|
+
this.options.logger?.warn?.("[Qoder Queue] Status observer failed");
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
clear() {
|
|
3103
|
+
this.publish({
|
|
3104
|
+
type: "clear",
|
|
3105
|
+
callId: this.callId
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
/** Register even a queue signal rejected by the output guard, so cleanup still runs. */
|
|
3109
|
+
entered(status) {
|
|
3110
|
+
const model = status.modelKey ?? this.options.model;
|
|
3111
|
+
if (this.models.has(model)) return;
|
|
3112
|
+
this.models.add(model);
|
|
3113
|
+
if (this.options.dshSessionId && this.options.onLease) {
|
|
3114
|
+
const key = JSON.stringify([
|
|
3115
|
+
this.options.region,
|
|
3116
|
+
this.options.subscriberId,
|
|
3117
|
+
this.options.requestSetId,
|
|
3118
|
+
model
|
|
3119
|
+
]);
|
|
3120
|
+
if (this.options.onLease({
|
|
3121
|
+
key,
|
|
3122
|
+
sessionId: this.options.dshSessionId,
|
|
3123
|
+
release: () => this.release(model, Math.max(0, Date.now() - (this.options.runStartedAt ?? Date.now())))
|
|
3124
|
+
})) this.deferred.add(model);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
async wait(initial, requestId) {
|
|
3128
|
+
this.entered(initial);
|
|
3129
|
+
if (this.recoveries >= 10) throw queueFailure("Qoder queue recovery limit reached.", "QODER_QUEUE_LIMIT");
|
|
3130
|
+
const started = this.clock.now();
|
|
3131
|
+
const remaining = () => this.maxWaitMs - this.waitMs - (this.clock.now() - started);
|
|
3132
|
+
const assertTime = () => {
|
|
3133
|
+
checkAbort(this.options.signal);
|
|
3134
|
+
if (remaining() <= 0) throw queueFailure("Qoder model queue wait timed out.", "QODER_QUEUE_TIMEOUT");
|
|
3135
|
+
};
|
|
3136
|
+
let status = initial;
|
|
3137
|
+
let failures = 0;
|
|
3138
|
+
let first = true;
|
|
3139
|
+
try {
|
|
3140
|
+
while (true) {
|
|
3141
|
+
assertTime();
|
|
3142
|
+
if (this.options.dshSessionId !== void 0) this.publish({
|
|
3143
|
+
type: "waiting",
|
|
3144
|
+
snapshot: {
|
|
3145
|
+
callId: this.callId,
|
|
3146
|
+
sessionId: this.options.dshSessionId,
|
|
3147
|
+
model: status.modelKey ?? this.options.model,
|
|
3148
|
+
...this.options.modelName ? { modelName: this.options.modelName } : {},
|
|
3149
|
+
...status.queueCount === void 0 ? {} : { queueCount: status.queueCount },
|
|
3150
|
+
...status.waitTime === void 0 ? {} : { waitTimeMs: status.waitTime * 1e3 },
|
|
3151
|
+
...status.serviceAvailable === void 0 ? {} : { serviceAvailable: status.serviceAvailable },
|
|
3152
|
+
waitElapsedMs: this.waitMs + Math.max(0, this.clock.now() - started),
|
|
3153
|
+
maxWaitMs: this.maxWaitMs,
|
|
3154
|
+
updatedAt: Date.now()
|
|
3155
|
+
}
|
|
3156
|
+
});
|
|
3157
|
+
if (!first) await this.clock.sleep(Math.min(queueInterval(status.retryAfterSeconds), remaining()), this.options.signal);
|
|
3158
|
+
first = false;
|
|
3159
|
+
assertTime();
|
|
3160
|
+
try {
|
|
3161
|
+
const next = await this.poll(status, requestId, remaining());
|
|
3162
|
+
status = {
|
|
3163
|
+
...next,
|
|
3164
|
+
modelKey: next.modelKey ?? status.modelKey,
|
|
3165
|
+
queueType: next.queueType ?? status.queueType
|
|
3166
|
+
};
|
|
3167
|
+
failures = 0;
|
|
3168
|
+
this.entered(status);
|
|
3169
|
+
if (status.isQueued === false) {
|
|
3170
|
+
if (status.retryAfterSeconds !== void 0 && status.retryAfterSeconds > 0) await this.clock.sleep(Math.min(queueInterval(status.retryAfterSeconds), remaining()), this.options.signal);
|
|
3171
|
+
assertTime();
|
|
3172
|
+
this.recoveries++;
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
if (status.isQueued !== true && status.serviceAvailable !== false) throw queueFailure("Qoder returned an invalid queue state.", "QODER_QUEUE_INVALID_STATE");
|
|
3176
|
+
} catch (error) {
|
|
3177
|
+
assertTime();
|
|
3178
|
+
if (error instanceof QoderLlmError && [
|
|
3179
|
+
"QODER_QUEUE_UNAVAILABLE",
|
|
3180
|
+
"QODER_QUEUE_INVALID_STATE",
|
|
3181
|
+
"QODER_QUEUE_IDENTITY"
|
|
3182
|
+
].includes(error.code)) throw error;
|
|
3183
|
+
if (++failures >= 3) throw queueFailure("Qoder queue status polling failed three consecutive times.");
|
|
3184
|
+
this.options.logger?.warn?.("[Qoder Queue] Status poll failed; will retry", { failures });
|
|
3185
|
+
status = {
|
|
3186
|
+
...status,
|
|
3187
|
+
retryAfterSeconds: void 0
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
} finally {
|
|
3192
|
+
this.waitMs += Math.max(0, this.clock.now() - started);
|
|
3193
|
+
this.clear();
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
async poll(status, requestId, remainingMs) {
|
|
3197
|
+
const url = getQoderQueueStatusUrl(this.options.region, {
|
|
3198
|
+
requestSetId: this.options.requestSetId,
|
|
3199
|
+
modelKey: status.modelKey ?? this.options.model,
|
|
3200
|
+
queueType: status.queueType
|
|
3201
|
+
});
|
|
3202
|
+
return bounded(remainingMs, this.options.signal, async (signal) => {
|
|
3203
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3204
|
+
const credentials = await this.options.credentials(attempt > 0, signal);
|
|
3205
|
+
checkAbort(signal);
|
|
3206
|
+
const outcome = await bounded(Math.min(3e4, remainingMs), signal, async (httpSignal) => {
|
|
3207
|
+
const response = await this.options.fetch(url, {
|
|
3208
|
+
method: "GET",
|
|
3209
|
+
headers: {
|
|
3210
|
+
...attributionHeaders(),
|
|
3211
|
+
...buildAuthHeaders(null, url, credentials),
|
|
3212
|
+
Accept: "application/json",
|
|
3213
|
+
"Accept-Encoding": "identity",
|
|
3214
|
+
"X-Request-Id": requestId,
|
|
3215
|
+
"X-Session-ID": this.options.sessionId
|
|
3216
|
+
},
|
|
3217
|
+
signal: httpSignal
|
|
3218
|
+
});
|
|
3219
|
+
return {
|
|
3220
|
+
status: response.status,
|
|
3221
|
+
ok: response.ok,
|
|
3222
|
+
body: await readBody(response, httpSignal)
|
|
3223
|
+
};
|
|
3224
|
+
});
|
|
3225
|
+
if ((outcome.status === 401 || outcome.status === 403) && attempt === 0) continue;
|
|
3226
|
+
if (outcome.status === 404) throw queueFailure("Qoder queue endpoint is unavailable.", "QODER_QUEUE_UNAVAILABLE");
|
|
3227
|
+
if (!outcome.ok) throw queueFailure(`Qoder queue status returned HTTP ${outcome.status}.`);
|
|
3228
|
+
const parsed = parseQueueStatus(outcome.body);
|
|
3229
|
+
if (!parsed) throw queueFailure("Qoder returned a malformed queue status.");
|
|
3230
|
+
return parsed;
|
|
3231
|
+
}
|
|
3232
|
+
throw queueFailure("Qoder queue authentication failed.");
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
/** Best-effort cleanup has an independent signal even after caller cancellation. */
|
|
3236
|
+
async finish(elapsedMs) {
|
|
3237
|
+
this.clear();
|
|
3238
|
+
const models = [...this.models].filter((model) => !this.deferred.has(model));
|
|
3239
|
+
this.models.clear();
|
|
3240
|
+
await Promise.allSettled(models.map((model) => this.release(model, elapsedMs)));
|
|
3241
|
+
}
|
|
3242
|
+
async release(model, elapsedMs) {
|
|
3243
|
+
try {
|
|
3244
|
+
await bounded(3e3, void 0, async (signal) => {
|
|
3245
|
+
const credentials = await this.options.credentials(false, signal);
|
|
3246
|
+
checkAbort(signal);
|
|
3247
|
+
const url = getQoderQueueFinishUrl(this.options.region);
|
|
3248
|
+
const body = JSON.stringify({
|
|
3249
|
+
payload: JSON.stringify({
|
|
3250
|
+
model_key: model,
|
|
3251
|
+
request_set_id: this.options.requestSetId,
|
|
3252
|
+
user_id: credentials.userID,
|
|
3253
|
+
time_consumed: Math.max(0, Math.round(elapsedMs))
|
|
3254
|
+
}),
|
|
3255
|
+
encodeVersion: "1"
|
|
3256
|
+
});
|
|
3257
|
+
const response = await this.options.fetch(url, {
|
|
3258
|
+
method: "POST",
|
|
3259
|
+
headers: {
|
|
3260
|
+
...attributionHeaders(),
|
|
3261
|
+
...buildAuthHeaders(body, url, credentials),
|
|
3262
|
+
"Content-Type": "application/json",
|
|
3263
|
+
Accept: "application/json",
|
|
3264
|
+
"X-Request-Id": randomUUID()
|
|
3265
|
+
},
|
|
3266
|
+
body,
|
|
3267
|
+
signal
|
|
3268
|
+
});
|
|
3269
|
+
await readBody(response, signal);
|
|
3270
|
+
if (!response.ok) throw queueFailure("Qoder lease release failed.");
|
|
3271
|
+
});
|
|
3272
|
+
} catch {
|
|
3273
|
+
this.options.logger?.warn?.("[Qoder Queue] Finish failed; server TTL will release the lease");
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
};
|
|
3277
|
+
//#endregion
|
|
3278
|
+
//#region src/qoder/transport/wire/serialize.ts
|
|
3279
|
+
async function buildQoderRequestBody(options, userId, translatedMessages, model, attachments, tracker, identityScope = "", hostRun) {
|
|
3280
|
+
const prepared = await prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker, identityScope, hostRun);
|
|
3281
|
+
return {
|
|
3282
|
+
request_id: prepared.requestId,
|
|
3283
|
+
request_set_id: prepared.requestSetId,
|
|
3284
|
+
chat_record_id: prepared.chatRecordId,
|
|
3285
|
+
session_id: prepared.sessionId,
|
|
3286
|
+
stream: true,
|
|
3287
|
+
chat_task: "FREE_INPUT",
|
|
3288
|
+
is_reply: true,
|
|
3289
|
+
is_retry: false,
|
|
3290
|
+
source: 1,
|
|
3291
|
+
version: "3",
|
|
3292
|
+
session_type: "qodercli",
|
|
3293
|
+
agent_id: "agent_common",
|
|
3294
|
+
task_id: prepared.taskId,
|
|
3295
|
+
code_language: "",
|
|
3296
|
+
chat_prompt: "",
|
|
3297
|
+
image_urls: null,
|
|
3298
|
+
aliyun_user_type: "",
|
|
3299
|
+
system: prepared.messages.filter((message) => message.role === "system").flatMap((message) => typeof message.content === "string" && message.content.length > 0 ? [{
|
|
3300
|
+
type: "text",
|
|
3301
|
+
text: message.content
|
|
3302
|
+
}] : []),
|
|
3303
|
+
messages: prepared.messages,
|
|
3304
|
+
tools: prepared.tools,
|
|
3305
|
+
parameters: {
|
|
3306
|
+
max_tokens: prepared.maxTokens,
|
|
3307
|
+
...prepared.contextLength === void 0 ? {} : { context_length: prepared.contextLength },
|
|
3308
|
+
...prepared.reasoningEffort === void 0 ? {} : { reasoning_effort: prepared.reasoningEffort }
|
|
3309
|
+
},
|
|
3310
|
+
chat_context: {
|
|
3311
|
+
chatPrompt: "",
|
|
3312
|
+
imageUrls: null,
|
|
3313
|
+
extra: {
|
|
3314
|
+
context: [],
|
|
3315
|
+
modelConfig: {
|
|
3316
|
+
key: prepared.modelKey,
|
|
3317
|
+
is_reasoning: prepared.isReasoning
|
|
3318
|
+
},
|
|
3319
|
+
originalContent: prepared.lastUserText
|
|
3320
|
+
},
|
|
3321
|
+
features: [],
|
|
3322
|
+
text: prepared.lastUserText
|
|
3323
|
+
},
|
|
3324
|
+
model_config: {
|
|
3325
|
+
key: prepared.modelKey,
|
|
3326
|
+
is_reasoning: prepared.isReasoning,
|
|
3327
|
+
max_output_tokens: prepared.maxTokens,
|
|
3328
|
+
source: prepared.modelSource,
|
|
3329
|
+
...prepared.contextConfig === void 0 ? {} : { context_config: prepared.contextConfig }
|
|
3330
|
+
},
|
|
3331
|
+
business: prepared.business
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
//#endregion
|
|
3335
|
+
//#region src/qoder/transport/wire/stream-chunks.ts
|
|
3336
|
+
/** Convert decoded Qoder model frames into DSH stream chunks. */
|
|
3337
|
+
const doneMarker = "[DONE]";
|
|
3338
|
+
const defaultMaxSseBufferChars = 2 * 1024 * 1024;
|
|
3339
|
+
function malformed$1(message) {
|
|
3340
|
+
return new QoderLlmError(message, "MALFORMED_RESPONSE");
|
|
3341
|
+
}
|
|
3342
|
+
/** Distinguish invalid JSON from a valid JSON value with an invalid model shape. */
|
|
3343
|
+
var QoderMalformedJsonFrameError = class extends QoderLlmError {
|
|
3344
|
+
incomplete;
|
|
3345
|
+
auxiliary;
|
|
3346
|
+
constructor(incomplete = false, auxiliary = false) {
|
|
3347
|
+
super("Malformed model-server SSE JSON payload.", "MALFORMED_RESPONSE");
|
|
3348
|
+
this.incomplete = incomplete;
|
|
3349
|
+
this.auxiliary = auxiliary;
|
|
3350
|
+
}
|
|
3351
|
+
};
|
|
3352
|
+
function tokenUsage(innerChunk) {
|
|
3353
|
+
if (!innerChunk.usage) return void 0;
|
|
3354
|
+
const usage = innerChunk.usage;
|
|
3355
|
+
const promptTokens = usage.prompt_tokens ?? 0;
|
|
3356
|
+
const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
3357
|
+
const cacheWriteTokens = usage.prompt_tokens_details?.cache_write_tokens ?? 0;
|
|
3358
|
+
const reasoningTokens = usage.completion_tokens_details?.reasoning_tokens;
|
|
3359
|
+
return {
|
|
3360
|
+
inputTokens: Math.max(0, promptTokens - cacheReadTokens - cacheWriteTokens),
|
|
3361
|
+
outputTokens: usage.completion_tokens ?? 0,
|
|
3362
|
+
...cacheReadTokens > 0 ? { cacheReadTokens } : {},
|
|
3363
|
+
...cacheWriteTokens > 0 ? { cacheWriteTokens } : {},
|
|
3364
|
+
...reasoningTokens === void 0 ? {} : { reasoningTokens }
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
function finishKind(value) {
|
|
3368
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
3369
|
+
if (value === "stop") return "stop";
|
|
3370
|
+
if (value === "length") return "max-tokens";
|
|
3371
|
+
if (value === "tool_calls" || value === "toolUse" || value === "function_call") return "tool-calls";
|
|
3372
|
+
if (value === "content_filter") throw new QoderLlmError("Qoder blocked the response through its content filter.", "PROVIDER_ERROR");
|
|
3373
|
+
throw malformed$1(`Qoder returned unknown finish reason "${value}".`);
|
|
3374
|
+
}
|
|
3375
|
+
/** Whether a choice delta streams content, reasoning, or tool-call data. Control-only frames do not. */
|
|
3376
|
+
function carriesStreamDelta(delta) {
|
|
3377
|
+
if (delta == null) return false;
|
|
3378
|
+
if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) return true;
|
|
3379
|
+
if (typeof delta.content === "string" && delta.content) return true;
|
|
3380
|
+
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) return true;
|
|
3381
|
+
return false;
|
|
3382
|
+
}
|
|
3383
|
+
async function* parseQoderChunks(stream, decodeFrame, options = {}, behavior = {}) {
|
|
3384
|
+
const reader = stream.getReader();
|
|
3385
|
+
const decoder = new TextDecoder();
|
|
3386
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
3387
|
+
let buffer = "";
|
|
3388
|
+
let eventData = [];
|
|
3389
|
+
let continuedEventData = [];
|
|
3390
|
+
let eventDataChars = 0;
|
|
3391
|
+
let eventType;
|
|
3392
|
+
let sourceEnded = false;
|
|
3393
|
+
let sawDone = false;
|
|
3394
|
+
let nextBlockIndex = 0;
|
|
3395
|
+
let activeTextual;
|
|
3396
|
+
let pendingUsage;
|
|
3397
|
+
let terminalKind = "stop";
|
|
3398
|
+
let sawFinishReason = false;
|
|
3399
|
+
let sawContent = false;
|
|
3400
|
+
const maxBufferChars = options.maxBufferChars ?? defaultMaxSseBufferChars;
|
|
3401
|
+
const closeTextual = () => {
|
|
3402
|
+
if (activeTextual === void 0) return [];
|
|
3403
|
+
const state = activeTextual;
|
|
3404
|
+
activeTextual = void 0;
|
|
3405
|
+
return [{
|
|
3406
|
+
type: "block-end",
|
|
3407
|
+
index: state.index,
|
|
3408
|
+
block: state.type === "text" ? {
|
|
3409
|
+
type: "text",
|
|
3410
|
+
text: state.text
|
|
3411
|
+
} : {
|
|
3412
|
+
type: "reasoning",
|
|
3413
|
+
text: state.text
|
|
3414
|
+
}
|
|
3415
|
+
}];
|
|
3416
|
+
};
|
|
3417
|
+
const appendSegment = (segment) => {
|
|
3418
|
+
if (!segment.text) return [];
|
|
3419
|
+
const chunks = [];
|
|
3420
|
+
sawContent = true;
|
|
3421
|
+
if (activeTextual?.type !== segment.type) {
|
|
3422
|
+
chunks.push(...closeTextual());
|
|
3423
|
+
activeTextual = {
|
|
3424
|
+
type: segment.type,
|
|
3425
|
+
index: nextBlockIndex++,
|
|
3426
|
+
text: ""
|
|
3427
|
+
};
|
|
3428
|
+
chunks.push({
|
|
3429
|
+
type: "block-start",
|
|
3430
|
+
index: activeTextual.index,
|
|
3431
|
+
blockType: segment.type
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
activeTextual.text += segment.text;
|
|
3435
|
+
chunks.push(segment.type === "text" ? {
|
|
3436
|
+
type: "text-delta",
|
|
3437
|
+
index: activeTextual.index,
|
|
3438
|
+
text: segment.text
|
|
3439
|
+
} : {
|
|
3440
|
+
type: "reasoning-delta",
|
|
3441
|
+
index: activeTextual.index,
|
|
3442
|
+
text: segment.text
|
|
3443
|
+
});
|
|
3444
|
+
return chunks;
|
|
3445
|
+
};
|
|
3446
|
+
const openToolCall = (state) => {
|
|
3447
|
+
if (!state.id || state.blockIndex !== void 0) return [];
|
|
3448
|
+
const chunks = closeTextual();
|
|
3449
|
+
state.blockIndex = nextBlockIndex++;
|
|
3450
|
+
sawContent = true;
|
|
3451
|
+
chunks.push({
|
|
3452
|
+
type: "block-start",
|
|
3453
|
+
index: state.blockIndex,
|
|
3454
|
+
blockType: "tool-call"
|
|
3455
|
+
});
|
|
3456
|
+
const argumentsDelta = state.arguments.slice(state.emittedArguments);
|
|
3457
|
+
state.emittedArguments = state.arguments.length;
|
|
3458
|
+
chunks.push({
|
|
3459
|
+
type: "tool-call-delta",
|
|
2724
3460
|
index: state.blockIndex,
|
|
2725
3461
|
id: ToolCallId(state.id),
|
|
2726
3462
|
...state.name ? { name: state.name } : {},
|
|
@@ -2735,33 +3471,102 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2735
3471
|
sourceEnded = true;
|
|
2736
3472
|
buffer += decoder.decode();
|
|
2737
3473
|
if (buffer.length > 0 && !buffer.endsWith("\n")) buffer += "\n";
|
|
3474
|
+
buffer += "\n";
|
|
2738
3475
|
} else {
|
|
2739
3476
|
options.onActivity?.();
|
|
2740
3477
|
buffer += decoder.decode(value, { stream: true });
|
|
2741
|
-
if (buffer.length > maxBufferChars) throw malformed("Qoder SSE frame exceeded its size limit.");
|
|
2742
3478
|
}
|
|
2743
3479
|
while (!sawDone) {
|
|
2744
3480
|
const lineEnd = buffer.indexOf("\n");
|
|
2745
|
-
if (lineEnd === -1)
|
|
3481
|
+
if (lineEnd === -1) {
|
|
3482
|
+
const pending = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
|
|
3483
|
+
if ((pending.startsWith("data:") ? eventDataChars + pending.slice(pending.startsWith("data: ") ? 6 : 5).length + 1 : pending.length) > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
|
|
3484
|
+
break;
|
|
3485
|
+
}
|
|
2746
3486
|
let line = buffer.substring(0, lineEnd);
|
|
2747
3487
|
buffer = buffer.substring(lineEnd + 1);
|
|
2748
3488
|
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
2749
|
-
line
|
|
2750
|
-
if (
|
|
2751
|
-
|
|
3489
|
+
if (!line.startsWith("data:") && line.length > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
|
|
3490
|
+
if (line.startsWith("event:")) {
|
|
3491
|
+
eventType = line.substring(6).trim();
|
|
3492
|
+
continue;
|
|
3493
|
+
}
|
|
3494
|
+
if (line.startsWith("data:")) {
|
|
3495
|
+
const value = line.substring(5);
|
|
3496
|
+
const data = value.startsWith(" ") ? value.substring(1) : value;
|
|
3497
|
+
eventData.push(data);
|
|
3498
|
+
if (behavior.recoverUnprefixedContinuations) continuedEventData.push(data);
|
|
3499
|
+
eventDataChars += data.length + 1;
|
|
3500
|
+
if (eventDataChars > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
|
|
3501
|
+
continue;
|
|
3502
|
+
}
|
|
3503
|
+
if (line !== "") {
|
|
3504
|
+
const jsonPropertyContinuation = /^:\s*(?:-?\d|true\b|false\b|null\b|["{\[\\])/u.test(line);
|
|
3505
|
+
if (behavior.recoverUnprefixedContinuations && continuedEventData.length > 0 && (!line.startsWith(":") || jsonPropertyContinuation) && !/^[a-zA-Z][\w-]*(?::|$)/u.test(line)) {
|
|
3506
|
+
const index = continuedEventData.length - 1;
|
|
3507
|
+
continuedEventData[index] += line;
|
|
3508
|
+
eventDataChars += line.length;
|
|
3509
|
+
if (eventDataChars > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
|
|
3510
|
+
}
|
|
3511
|
+
continue;
|
|
3512
|
+
}
|
|
3513
|
+
const rawData = eventData.join("\n").trim();
|
|
3514
|
+
const continuedData = continuedEventData.join("\n").trim();
|
|
3515
|
+
const frameEventType = eventType;
|
|
3516
|
+
eventData = [];
|
|
3517
|
+
continuedEventData = [];
|
|
3518
|
+
eventDataChars = 0;
|
|
3519
|
+
eventType = void 0;
|
|
2752
3520
|
if (!rawData) continue;
|
|
2753
3521
|
if (rawData === doneMarker) {
|
|
3522
|
+
options.onControlFrame?.("done");
|
|
2754
3523
|
sawDone = true;
|
|
2755
3524
|
break;
|
|
2756
3525
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
3526
|
+
let innerChunk;
|
|
3527
|
+
let malformedDetails;
|
|
3528
|
+
try {
|
|
3529
|
+
const frameOptions = {
|
|
3530
|
+
...options,
|
|
3531
|
+
onMalformedFrame: (details) => {
|
|
3532
|
+
malformedDetails = {
|
|
3533
|
+
...details,
|
|
3534
|
+
atEof: sourceEnded,
|
|
3535
|
+
afterFinish: sawFinishReason
|
|
3536
|
+
};
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
try {
|
|
3540
|
+
innerChunk = decodeFrame(rawData, frameOptions, frameEventType);
|
|
3541
|
+
} catch (error) {
|
|
3542
|
+
if (!(error instanceof QoderMalformedJsonFrameError) || !continuedData || continuedData === rawData) throw error;
|
|
3543
|
+
const originalDetails = malformedDetails;
|
|
3544
|
+
innerChunk = decodeFrame(continuedData, frameOptions, frameEventType);
|
|
3545
|
+
if (originalDetails) options.onMalformedFrame?.({
|
|
3546
|
+
...originalDetails,
|
|
3547
|
+
disposition: "recovered-continuation"
|
|
3548
|
+
});
|
|
3549
|
+
}
|
|
3550
|
+
} catch (error) {
|
|
3551
|
+
const malformedJson = error instanceof QoderMalformedJsonFrameError ? error : void 0;
|
|
3552
|
+
const skippedAfterFinish = behavior.ignoreMalformedAfterFinish && sawFinishReason && frameEventType !== "error" && malformedJson !== void 0;
|
|
3553
|
+
const skippedAuxiliary = !sourceEnded && behavior.allowIncompleteAuxiliaryFrames && malformedJson?.incomplete && malformedJson.auxiliary && frameEventType !== "error";
|
|
3554
|
+
if (malformedDetails !== void 0) options.onMalformedFrame?.({
|
|
3555
|
+
...malformedDetails,
|
|
3556
|
+
emptyChoicesPrefix: malformedJson?.auxiliary ?? false,
|
|
3557
|
+
disposition: skippedAfterFinish ? "skipped-after-finish" : skippedAuxiliary ? "skipped-auxiliary" : "rejected"
|
|
3558
|
+
});
|
|
3559
|
+
if (skippedAfterFinish) continue;
|
|
3560
|
+
if (sourceEnded && error instanceof QoderMalformedJsonFrameError && error.incomplete) throw new QoderLlmError("Qoder SSE connection ended with an incomplete JSON event before model completion.", "TRANSPORT");
|
|
3561
|
+
if (skippedAuxiliary) continue;
|
|
3562
|
+
throw error;
|
|
3563
|
+
}
|
|
3564
|
+
if (innerChunk === void 0) continue;
|
|
3565
|
+
if (innerChunk === doneMarker) {
|
|
3566
|
+
options.onControlFrame?.("done");
|
|
2761
3567
|
sawDone = true;
|
|
2762
3568
|
break;
|
|
2763
3569
|
}
|
|
2764
|
-
const innerChunk = parseInner(body);
|
|
2765
3570
|
pendingUsage = tokenUsage(innerChunk) ?? pendingUsage;
|
|
2766
3571
|
for (const choice of innerChunk.choices ?? []) {
|
|
2767
3572
|
const kind = finishKind(choice.finish_reason);
|
|
@@ -2780,11 +3585,11 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2780
3585
|
text: delta.content
|
|
2781
3586
|
})) yield chunk;
|
|
2782
3587
|
if (delta.tool_calls !== void 0) {
|
|
2783
|
-
if (!Array.isArray(delta.tool_calls)) throw malformed("Qoder tool-call delta is not an array.");
|
|
3588
|
+
if (!Array.isArray(delta.tool_calls)) throw malformed$1("Qoder tool-call delta is not an array.");
|
|
2784
3589
|
for (const rawCall of delta.tool_calls) {
|
|
2785
|
-
if (typeof rawCall !== "object" || rawCall === null || Array.isArray(rawCall)) throw malformed("Qoder tool-call delta is not an object.");
|
|
3590
|
+
if (typeof rawCall !== "object" || rawCall === null || Array.isArray(rawCall)) throw malformed$1("Qoder tool-call delta is not an object.");
|
|
2786
3591
|
const upstreamIndex = rawCall.index ?? 0;
|
|
2787
|
-
if (!Number.isInteger(upstreamIndex) || upstreamIndex < 0) throw malformed("Qoder tool-call delta has an invalid index.");
|
|
3592
|
+
if (!Number.isInteger(upstreamIndex) || upstreamIndex < 0) throw malformed$1("Qoder tool-call delta has an invalid index.");
|
|
2788
3593
|
let state = toolCalls.get(upstreamIndex);
|
|
2789
3594
|
if (state === void 0) {
|
|
2790
3595
|
state = {
|
|
@@ -2795,17 +3600,17 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2795
3600
|
};
|
|
2796
3601
|
toolCalls.set(upstreamIndex, state);
|
|
2797
3602
|
}
|
|
2798
|
-
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.");
|
|
3603
|
+
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.");
|
|
2799
3604
|
else {
|
|
2800
|
-
if (state.id && state.id !== rawCall.id) throw malformed("Qoder changed a streamed tool-call id.");
|
|
3605
|
+
if (state.id && state.id !== rawCall.id) throw malformed$1("Qoder changed a streamed tool-call id.");
|
|
2801
3606
|
state.id = rawCall.id;
|
|
2802
3607
|
}
|
|
2803
3608
|
let hasNewName = false;
|
|
2804
3609
|
if (rawCall.function?.name !== void 0) {
|
|
2805
3610
|
const name = rawCall.function.name;
|
|
2806
|
-
if (name === "" || name === null) {} else if (typeof name !== "string") throw malformed("Qoder tool call has an invalid name.");
|
|
3611
|
+
if (name === "" || name === null) {} else if (typeof name !== "string") throw malformed$1("Qoder tool call has an invalid name.");
|
|
2807
3612
|
else {
|
|
2808
|
-
if (state.name && state.name !== name) throw malformed("Qoder changed a streamed tool-call name.");
|
|
3613
|
+
if (state.name && state.name !== name) throw malformed$1("Qoder changed a streamed tool-call name.");
|
|
2809
3614
|
if (state.name !== name) {
|
|
2810
3615
|
state.name = name;
|
|
2811
3616
|
hasNewName = true;
|
|
@@ -2813,7 +3618,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2813
3618
|
}
|
|
2814
3619
|
}
|
|
2815
3620
|
if (rawCall.function?.arguments !== void 0) {
|
|
2816
|
-
if (typeof rawCall.function.arguments !== "string") throw malformed("Qoder tool-call arguments delta is not a string.");
|
|
3621
|
+
if (typeof rawCall.function.arguments !== "string") throw malformed$1("Qoder tool-call arguments delta is not a string.");
|
|
2817
3622
|
state.arguments += rawCall.function.arguments;
|
|
2818
3623
|
}
|
|
2819
3624
|
const wasOpen = state.blockIndex !== void 0;
|
|
@@ -2847,15 +3652,15 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2847
3652
|
if (!sawDone && !sawFinishReason) throw new QoderLlmError("SSE stream ended prematurely without [DONE].", "TRANSPORT");
|
|
2848
3653
|
for (const chunk of closeTextual()) yield chunk;
|
|
2849
3654
|
for (const [, state] of [...toolCalls.entries()].sort(([left], [right]) => left - right)) {
|
|
2850
|
-
if (!state.id || !state.name) throw malformed("Qoder completed an unidentifiable tool call.");
|
|
3655
|
+
if (!state.id || !state.name) throw malformed$1("Qoder completed an unidentifiable tool call.");
|
|
2851
3656
|
const normalizedArguments = state.arguments.trim() ? state.arguments : "{}";
|
|
2852
3657
|
try {
|
|
2853
3658
|
JSON.parse(normalizedArguments);
|
|
2854
3659
|
} catch {
|
|
2855
|
-
throw malformed(`Qoder completed tool call "${state.name}" with malformed JSON arguments.`);
|
|
3660
|
+
throw malformed$1(`Qoder completed tool call "${state.name}" with malformed JSON arguments.`);
|
|
2856
3661
|
}
|
|
2857
3662
|
for (const chunk of openToolCall(state)) yield chunk;
|
|
2858
|
-
if (state.blockIndex === void 0) throw malformed("Qoder failed to open a completed tool call.");
|
|
3663
|
+
if (state.blockIndex === void 0) throw malformed$1("Qoder failed to open a completed tool call.");
|
|
2859
3664
|
if (state.emittedArguments === 0 && normalizedArguments === "{}") yield {
|
|
2860
3665
|
type: "tool-call-delta",
|
|
2861
3666
|
index: state.blockIndex,
|
|
@@ -2897,14 +3702,125 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2897
3702
|
};
|
|
2898
3703
|
}
|
|
2899
3704
|
//#endregion
|
|
3705
|
+
//#region src/qoder/transport/wire/control-frames.ts
|
|
3706
|
+
function qoderControlFrameKind(data) {
|
|
3707
|
+
if (data === "[DONE]") return "done";
|
|
3708
|
+
if (data === "[NOT_EXCEED_QUOTA]") return "quota-available";
|
|
3709
|
+
if (data.startsWith("[EXCEED_QUOTA]")) return "quota-exceeded";
|
|
3710
|
+
if (data.startsWith("[NOTIFICATIONS]")) return "notifications";
|
|
3711
|
+
}
|
|
3712
|
+
//#endregion
|
|
3713
|
+
//#region src/qoder/transport/wire/sse.ts
|
|
3714
|
+
function malformed(message) {
|
|
3715
|
+
return new QoderLlmError(message, "MALFORMED_RESPONSE");
|
|
3716
|
+
}
|
|
3717
|
+
function decodeLegacyFrame(rawData, options) {
|
|
3718
|
+
const outerControlKind = qoderControlFrameKind(rawData);
|
|
3719
|
+
if (outerControlKind !== void 0) {
|
|
3720
|
+
options.onControlFrame?.(outerControlKind);
|
|
3721
|
+
return outerControlKind === "done" ? "[DONE]" : void 0;
|
|
3722
|
+
}
|
|
3723
|
+
let value;
|
|
3724
|
+
try {
|
|
3725
|
+
value = JSON.parse(rawData);
|
|
3726
|
+
} catch {
|
|
3727
|
+
throw malformed("Malformed outer SSE JSON payload received from Qoder.");
|
|
3728
|
+
}
|
|
3729
|
+
if (value === null || typeof value !== "object") throw malformed("Malformed outer SSE envelope received from Qoder.");
|
|
3730
|
+
const envelope = value;
|
|
3731
|
+
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.");
|
|
3732
|
+
if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) {
|
|
3733
|
+
const queued = options.queueRecovery ? queuedError(envelope.body ?? envelope) : void 0;
|
|
3734
|
+
if (queued) throw queued;
|
|
3735
|
+
throw qoderModelError(`Qoder service returned upstream error status ${envelope.statusCodeValue}`, {
|
|
3736
|
+
status: envelope.statusCodeValue,
|
|
3737
|
+
cause: envelope.body ?? envelope.message,
|
|
3738
|
+
source: "sse",
|
|
3739
|
+
headers: options.headers,
|
|
3740
|
+
httpStatus: options.httpStatus
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
|
|
3744
|
+
const body = typeof envelope.body === "string" ? envelope.body.trim() : "";
|
|
3745
|
+
if (!body) return void 0;
|
|
3746
|
+
const innerControlKind = qoderControlFrameKind(body);
|
|
3747
|
+
if (innerControlKind !== void 0) {
|
|
3748
|
+
if (innerControlKind !== "done") options.onControlFrame?.(innerControlKind);
|
|
3749
|
+
return innerControlKind === "done" ? "[DONE]" : void 0;
|
|
3750
|
+
}
|
|
3751
|
+
try {
|
|
3752
|
+
const inner = JSON.parse(body);
|
|
3753
|
+
if (inner === null || typeof inner !== "object") throw new Error("not an object");
|
|
3754
|
+
return inner;
|
|
3755
|
+
} catch {
|
|
3756
|
+
throw malformed("Malformed inner model payload received from Qoder.");
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
function parseQoderSse(stream, options = {}) {
|
|
3760
|
+
return parseQoderChunks(stream, decodeLegacyFrame, options);
|
|
3761
|
+
}
|
|
3762
|
+
//#endregion
|
|
2900
3763
|
//#region src/qoder/transport/chat.ts
|
|
2901
|
-
/** Qoder chat request encoding, timeout lifecycle, and SSE streaming. */
|
|
2902
3764
|
function aborted$1(message) {
|
|
2903
3765
|
return new QoderLlmError(message, "ABORTED");
|
|
2904
3766
|
}
|
|
2905
3767
|
async function* streamQoderChat(options, model, credentials, messages, dependencies) {
|
|
2906
|
-
const request = await buildQoderRequestBody(options, credentials.userID, messages, model);
|
|
3768
|
+
const request = await buildQoderRequestBody(options, credentials.userID, messages, model, void 0, void 0, dependencies.region, dependencies.hostRun);
|
|
2907
3769
|
request.session_type = dependencies.region === "china" ? "qoderclicn" : "qodercli";
|
|
3770
|
+
const startedAt = performance.now();
|
|
3771
|
+
let activeCredentials = credentials;
|
|
3772
|
+
const resolveCredentials = async (refresh, signal) => {
|
|
3773
|
+
const next = dependencies.resolveCredentials ? await dependencies.resolveCredentials(refresh, signal) : refresh && dependencies.refreshCredentials ? await dependencies.refreshCredentials(activeCredentials, signal) : activeCredentials;
|
|
3774
|
+
if (next.userID !== credentials.userID) throw queueFailure("Qoder subscriber changed during queue recovery.", "QODER_QUEUE_IDENTITY");
|
|
3775
|
+
activeCredentials = next;
|
|
3776
|
+
return next;
|
|
3777
|
+
};
|
|
3778
|
+
const queue = new ModelQueue({
|
|
3779
|
+
region: dependencies.region,
|
|
3780
|
+
fetch: dependencies.fetch,
|
|
3781
|
+
subscriberId: credentials.userID,
|
|
3782
|
+
requestSetId: request.request_set_id,
|
|
3783
|
+
sessionId: request.session_id,
|
|
3784
|
+
dshSessionId: options.purpose === void 0 && options.sessionId !== void 0 ? String(options.sessionId) : void 0,
|
|
3785
|
+
runStartedAt: request.business.begin_at,
|
|
3786
|
+
model: request.model_config.key,
|
|
3787
|
+
modelName: model?.name,
|
|
3788
|
+
signal: options.signal,
|
|
3789
|
+
maxWaitMs: dependencies.modelQueueMaxWaitMs,
|
|
3790
|
+
credentials: resolveCredentials,
|
|
3791
|
+
onUpdate: dependencies.onQueueUpdate,
|
|
3792
|
+
onLease: dependencies.onQueueLease,
|
|
3793
|
+
logger: dependencies.logger,
|
|
3794
|
+
clock: dependencies.queueClock
|
|
3795
|
+
});
|
|
3796
|
+
let sawOutput = false;
|
|
3797
|
+
const clear = () => queue.clear();
|
|
3798
|
+
options.signal?.addEventListener("abort", clear, { once: true });
|
|
3799
|
+
try {
|
|
3800
|
+
for (;;) {
|
|
3801
|
+
if (options.signal?.aborted) throw aborted$1("Request was aborted.");
|
|
3802
|
+
try {
|
|
3803
|
+
for await (const chunk of streamAttempt(options, model, activeCredentials, request, dependencies)) {
|
|
3804
|
+
if (chunk.type !== "usage" && chunk.type !== "finish") sawOutput = true;
|
|
3805
|
+
yield chunk;
|
|
3806
|
+
}
|
|
3807
|
+
return;
|
|
3808
|
+
} catch (error) {
|
|
3809
|
+
if (!(error instanceof QoderModelQueuedError)) throw error;
|
|
3810
|
+
queue.entered(error.queue);
|
|
3811
|
+
if (sawOutput) throw queueFailure("Qoder queued a request after output began; replay was stopped.", "QODER_QUEUE_AFTER_OUTPUT");
|
|
3812
|
+
await queue.wait(error.queue, request.request_id);
|
|
3813
|
+
await resolveCredentials(false, options.signal ?? new AbortController().signal);
|
|
3814
|
+
request.request_id = randomUUID();
|
|
3815
|
+
request.chat_record_id = request.request_id;
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
} finally {
|
|
3819
|
+
options.signal?.removeEventListener("abort", clear);
|
|
3820
|
+
await queue.finish(performance.now() - startedAt);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
async function* streamAttempt(options, model, credentials, request, dependencies) {
|
|
2908
3824
|
const encodedBytes = qoderEncodeBodyBytes(JSON.stringify(request));
|
|
2909
3825
|
const chatUrl = getQoderChatUrl(dependencies.region);
|
|
2910
3826
|
const requestController = new AbortController();
|
|
@@ -2917,10 +3833,7 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2917
3833
|
let lastActivityAt = startedAt;
|
|
2918
3834
|
const onCallerAbort = () => requestController.abort(options.signal?.reason);
|
|
2919
3835
|
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
2920
|
-
let headerTimer
|
|
2921
|
-
headerTimedOut = true;
|
|
2922
|
-
requestController.abort("response header timeout");
|
|
2923
|
-
}, dependencies.responseHeaderTimeoutMs);
|
|
3836
|
+
let headerTimer;
|
|
2924
3837
|
const resetIdleTimer = () => {
|
|
2925
3838
|
lastActivityAt = performance.now();
|
|
2926
3839
|
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
@@ -2940,16 +3853,35 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2940
3853
|
contextTier: markedTiers.length === 1 ? markedTiers[0][0] : null,
|
|
2941
3854
|
context_length: request.parameters.context_length ?? null
|
|
2942
3855
|
}));
|
|
3856
|
+
const compression = new QoderRequestCompression();
|
|
2943
3857
|
let response;
|
|
2944
3858
|
let activeCredentials = credentials;
|
|
2945
3859
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2946
3860
|
if (options.signal?.aborted) throw aborted$1("Request was aborted.");
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
3861
|
+
response = await compression.send({
|
|
3862
|
+
url: chatUrl,
|
|
3863
|
+
protocol: "legacy",
|
|
3864
|
+
body: encodedBytes.toString("latin1"),
|
|
3865
|
+
enabled: dependencies.requestCompression,
|
|
3866
|
+
signal: requestController.signal,
|
|
3867
|
+
logger: dependencies.logger,
|
|
3868
|
+
requestId: request.request_id,
|
|
3869
|
+
dispatch: async (wireBody, headers) => {
|
|
3870
|
+
headerTimer = setTimeout(() => {
|
|
3871
|
+
headerTimedOut = true;
|
|
3872
|
+
requestController.abort("response header timeout");
|
|
3873
|
+
}, dependencies.responseHeaderTimeoutMs);
|
|
3874
|
+
try {
|
|
3875
|
+
return await dependencies.fetch(chatUrl, {
|
|
3876
|
+
method: "POST",
|
|
3877
|
+
headers,
|
|
3878
|
+
body: wireBody,
|
|
3879
|
+
signal: requestController.signal
|
|
3880
|
+
});
|
|
3881
|
+
} finally {
|
|
3882
|
+
clearTimeout(headerTimer);
|
|
3883
|
+
}
|
|
3884
|
+
},
|
|
2953
3885
|
headers: {
|
|
2954
3886
|
"content-type": "application/json",
|
|
2955
3887
|
"accept": "text/event-stream",
|
|
@@ -2959,10 +3891,10 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2959
3891
|
"x-model-key": options.model || "cmodel",
|
|
2960
3892
|
"x-model-source": model?.source || "system",
|
|
2961
3893
|
...attributionHeaders(),
|
|
2962
|
-
...buildAuthHeaders(encodedBytes, chatUrl, activeCredentials)
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3894
|
+
...buildAuthHeaders(encodedBytes, chatUrl, activeCredentials),
|
|
3895
|
+
"X-Request-Id": request.request_id,
|
|
3896
|
+
"X-Session-ID": request.session_id
|
|
3897
|
+
}
|
|
2966
3898
|
});
|
|
2967
3899
|
clearTimeout(headerTimer);
|
|
2968
3900
|
reqId = qoderRequestId(response.headers);
|
|
@@ -2981,6 +3913,8 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2981
3913
|
} catch (error) {
|
|
2982
3914
|
if (requestController.signal.aborted) throw error;
|
|
2983
3915
|
}
|
|
3916
|
+
const queued = queuedError(bodyText);
|
|
3917
|
+
if (queued) throw queued;
|
|
2984
3918
|
const failure = qoderModelError(`Qoder upstream service returned HTTP ${response.status}`, {
|
|
2985
3919
|
status: response.status,
|
|
2986
3920
|
headers: response.headers,
|
|
@@ -3001,22 +3935,36 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
3001
3935
|
let tokenUsage;
|
|
3002
3936
|
let finishReason;
|
|
3003
3937
|
const streamStartedAt = performance.now();
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3938
|
+
try {
|
|
3939
|
+
for await (const chunk of parseQoderSse(response.body, {
|
|
3940
|
+
queueRecovery: true,
|
|
3941
|
+
onActivity: resetIdleTimer,
|
|
3942
|
+
headers: response.headers,
|
|
3943
|
+
httpStatus: response.status,
|
|
3944
|
+
onControlFrame: (kind) => {
|
|
3945
|
+
if (kind !== "done") dependencies.logger?.warn?.("[Qoder Stream] Control frame skipped", {
|
|
3946
|
+
transport: "legacy",
|
|
3947
|
+
kind,
|
|
3948
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
3949
|
+
});
|
|
3950
|
+
}
|
|
3951
|
+
})) {
|
|
3952
|
+
compression.confirm();
|
|
3953
|
+
chunkCount++;
|
|
3954
|
+
if (firstChunkDurationMs === void 0) {
|
|
3955
|
+
firstChunkDurationMs = Math.round(performance.now() - startedAt);
|
|
3956
|
+
dependencies.logger?.debug?.("[Qoder Stream] First chunk received", {
|
|
3957
|
+
durationMs: firstChunkDurationMs,
|
|
3958
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3961
|
+
if (chunk.type === "usage") tokenUsage = chunk.usage;
|
|
3962
|
+
if (chunk.type === "finish") finishReason = chunk.reason.kind;
|
|
3963
|
+
yield chunk;
|
|
3016
3964
|
}
|
|
3017
|
-
|
|
3018
|
-
if (
|
|
3019
|
-
|
|
3965
|
+
} catch (error) {
|
|
3966
|
+
if (chunkCount === 0) compression.rejectLegacyError(error);
|
|
3967
|
+
throw error;
|
|
3020
3968
|
}
|
|
3021
3969
|
dependencies.logger?.debug?.("[Qoder Stream] Stream completed", {
|
|
3022
3970
|
durationMs: Math.round(performance.now() - startedAt),
|
|
@@ -3049,6 +3997,7 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
3049
3997
|
});
|
|
3050
3998
|
throw failure;
|
|
3051
3999
|
}
|
|
4000
|
+
if (error instanceof QoderModelQueuedError) throw error;
|
|
3052
4001
|
if (error instanceof QoderLlmError) {
|
|
3053
4002
|
dependencies.logger?.error?.("[Qoder Stream] Request failed", redactLogValue(error), {
|
|
3054
4003
|
chunkCount,
|
|
@@ -3073,13 +4022,397 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
3073
4022
|
}
|
|
3074
4023
|
}
|
|
3075
4024
|
//#endregion
|
|
4025
|
+
//#region src/qoder/transport/browser-authorization.ts
|
|
4026
|
+
/** qodercli 1.1.73 PKCE device flow. No website password or agent runtime is needed. */
|
|
4027
|
+
var QoderBrowserAuthorization = class {
|
|
4028
|
+
options;
|
|
4029
|
+
constructor(options) {
|
|
4030
|
+
this.options = options;
|
|
4031
|
+
}
|
|
4032
|
+
async begin(signal) {
|
|
4033
|
+
signal.throwIfAborted();
|
|
4034
|
+
const verifier = randomBytes(48).toString("base64url");
|
|
4035
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
4036
|
+
const nonce = randomUUID();
|
|
4037
|
+
const machineID = (this.options.resolveMachineId ?? (() => getMachineId(qoderMachineIdPaths(this.options.region))))();
|
|
4038
|
+
const query = new URLSearchParams({
|
|
4039
|
+
challenge,
|
|
4040
|
+
challenge_method: "S256",
|
|
4041
|
+
nonce,
|
|
4042
|
+
machine_id: machineID,
|
|
4043
|
+
client_id: "e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb"
|
|
4044
|
+
});
|
|
4045
|
+
const origin = this.options.region === "china" ? "https://qoder.cn" : "https://qoder.com";
|
|
4046
|
+
const expiresAt = Date.now() + (this.options.loginTimeoutMs ?? 3e5);
|
|
4047
|
+
const deadline = AbortSignal.timeout(Math.max(1, expiresAt - Date.now()));
|
|
4048
|
+
const combined = AbortSignal.any([signal, deadline]);
|
|
4049
|
+
return {
|
|
4050
|
+
url: `${origin}/device/selectAccounts?${query}`,
|
|
4051
|
+
expiresAt,
|
|
4052
|
+
complete: async () => {
|
|
4053
|
+
try {
|
|
4054
|
+
const poll = new URL(`${resolveQoderEndpoints(this.options.region).openApiUrl}/api/v1/deviceToken/poll`);
|
|
4055
|
+
poll.search = new URLSearchParams({
|
|
4056
|
+
nonce,
|
|
4057
|
+
verifier,
|
|
4058
|
+
challenge_method: "S256"
|
|
4059
|
+
}).toString();
|
|
4060
|
+
while (!combined.aborted) {
|
|
4061
|
+
let response;
|
|
4062
|
+
try {
|
|
4063
|
+
response = await (this.options.fetch ?? fetch)(poll, {
|
|
4064
|
+
headers: {
|
|
4065
|
+
accept: "application/json",
|
|
4066
|
+
"user-agent": defaultUserAgent
|
|
4067
|
+
},
|
|
4068
|
+
signal: AbortSignal.any([combined, AbortSignal.timeout(15e3)])
|
|
4069
|
+
});
|
|
4070
|
+
} catch {
|
|
4071
|
+
if (combined.aborted) break;
|
|
4072
|
+
await setTimeout$1(this.options.pollIntervalMs ?? 1e3, void 0, { signal: combined });
|
|
4073
|
+
continue;
|
|
4074
|
+
}
|
|
4075
|
+
if (response.status === 404 || response.status >= 500 || response.status === 429) {
|
|
4076
|
+
await response.body?.cancel();
|
|
4077
|
+
await setTimeout$1(this.options.pollIntervalMs ?? 1e3, void 0, { signal: combined });
|
|
4078
|
+
continue;
|
|
4079
|
+
}
|
|
4080
|
+
if (!response.ok) {
|
|
4081
|
+
await response.body?.cancel();
|
|
4082
|
+
throw new QoderLlmError("Qoder browser authorization was rejected.", "AUTH");
|
|
4083
|
+
}
|
|
4084
|
+
const raw = JSON.parse(await readLimitedText(response, defaultMaxJsonBytes, "Qoder browser authorization"));
|
|
4085
|
+
return await this.grant(raw, machineID, combined);
|
|
4086
|
+
}
|
|
4087
|
+
throw new QoderLlmError("Qoder browser login timed out.", "TIMEOUT");
|
|
4088
|
+
} catch (error) {
|
|
4089
|
+
if (signal.aborted) throw new QoderLlmError("Qoder browser login was cancelled.", "ABORTED");
|
|
4090
|
+
if (deadline.aborted) throw new QoderLlmError("Qoder browser login timed out.", "TIMEOUT");
|
|
4091
|
+
if (error instanceof QoderLlmError) throw error;
|
|
4092
|
+
throw new QoderLlmError("Qoder browser authorization returned an invalid response.", "MALFORMED_RESPONSE");
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
};
|
|
4096
|
+
}
|
|
4097
|
+
async refresh(grant, signal, rejectedToken) {
|
|
4098
|
+
if (grant.region !== this.options.region) throw new QoderLlmError("Qoder authorization region mismatch.", "AUTH");
|
|
4099
|
+
if (!(rejectedToken !== void 0 && rejectedToken === grant.credentials.authToken) && grant.expiresAt > Date.now() + 3e5) return grant;
|
|
4100
|
+
if (grant.refreshExpiresAt !== void 0 && grant.refreshExpiresAt <= Date.now()) throw new QoderLlmError("Qoder browser authorization expired. Sign in again.", "AUTH");
|
|
4101
|
+
const raw = await openApiJsonRequest(this.options.fetch ?? fetch, {
|
|
4102
|
+
url: `${resolveQoderEndpoints(this.options.region).openApiUrl}/api/v1/deviceToken/refresh`,
|
|
4103
|
+
body: {
|
|
4104
|
+
refresh_token: grant.refreshToken,
|
|
4105
|
+
machine_id: grant.credentials.machineID
|
|
4106
|
+
},
|
|
4107
|
+
signal,
|
|
4108
|
+
operation: "BrowserRefresh"
|
|
4109
|
+
});
|
|
4110
|
+
const token = raw.device_token ?? raw.token;
|
|
4111
|
+
if (!token || typeof token !== "string" || !raw.refresh_token || typeof raw.refresh_token !== "string") throw new QoderLlmError("Qoder refresh returned incomplete credentials.", "MALFORMED_RESPONSE");
|
|
4112
|
+
return {
|
|
4113
|
+
...grant,
|
|
4114
|
+
credentials: {
|
|
4115
|
+
...grant.credentials,
|
|
4116
|
+
authToken: token
|
|
4117
|
+
},
|
|
4118
|
+
refreshToken: raw.refresh_token,
|
|
4119
|
+
expiresAt: deviceExpiry(raw.expires_at, raw.expires_in) ?? Date.now() + 864e5,
|
|
4120
|
+
refreshExpiresAt: refreshExpiry(raw) ?? grant.refreshExpiresAt
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
async grant(raw, machineID, signal) {
|
|
4124
|
+
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");
|
|
4125
|
+
const credentials = await new QoderAuthManager({
|
|
4126
|
+
region: this.options.region,
|
|
4127
|
+
fetch: this.options.fetch
|
|
4128
|
+
}).credentialsForToken(raw.token, machineID, signal);
|
|
4129
|
+
return {
|
|
4130
|
+
id: randomUUID(),
|
|
4131
|
+
region: this.options.region,
|
|
4132
|
+
credentials,
|
|
4133
|
+
refreshToken: raw.refresh_token,
|
|
4134
|
+
expiresAt: deviceExpiry(raw.expires_at, raw.expires_in) ?? Date.now() + 864e5,
|
|
4135
|
+
refreshExpiresAt: refreshExpiry(raw)
|
|
4136
|
+
};
|
|
4137
|
+
}
|
|
4138
|
+
};
|
|
4139
|
+
function refreshExpiry(raw) {
|
|
4140
|
+
return deviceExpiry(raw.refresh_token_expires_at, raw.refresh_token_expires_in);
|
|
4141
|
+
}
|
|
4142
|
+
/** Device-flow durations are seconds, including multi-day refresh-token lifetimes. */
|
|
4143
|
+
function deviceExpiry(absolute, seconds) {
|
|
4144
|
+
if (absolute !== void 0) {
|
|
4145
|
+
const parsed = typeof absolute === "string" ? Date.parse(absolute) : absolute > 0xe8d4a51000 ? absolute : absolute * 1e3;
|
|
4146
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
4147
|
+
}
|
|
4148
|
+
if (typeof seconds === "number" && Number.isFinite(seconds * 1e3) && seconds > 0) return Date.now() + seconds * 1e3;
|
|
4149
|
+
}
|
|
4150
|
+
//#endregion
|
|
4151
|
+
//#region src/qoder/transport/wire/model-server-sse.ts
|
|
4152
|
+
function malformedFrameDetails(rawData, eventType, error) {
|
|
4153
|
+
const marker = /^\[[A-Z][A-Z_]{0,63}\]/u.exec(rawData)?.[0];
|
|
4154
|
+
const safeEventType = eventType && /^[a-z][\w.-]{0,40}$/iu.test(eventType) ? eventType : void 0;
|
|
4155
|
+
const message = error instanceof SyntaxError ? error.message : "";
|
|
4156
|
+
const offset = /\bposition (\d+)\b/u.exec(message)?.[1];
|
|
4157
|
+
const first = rawData.charAt(0);
|
|
4158
|
+
const last = rawData.charAt(rawData.length - 1);
|
|
4159
|
+
return {
|
|
4160
|
+
...safeEventType === void 0 ? {} : { eventType: safeEventType },
|
|
4161
|
+
...marker === void 0 ? {} : { marker },
|
|
4162
|
+
chars: rawData.length,
|
|
4163
|
+
lines: rawData.split("\n").length,
|
|
4164
|
+
shape: rawData.startsWith("data:") ? "nested-data" : first === "{" ? "object" : first === "[" ? "array" : first === "\"" ? "string" : "other",
|
|
4165
|
+
endsWith: last === "}" ? "object" : last === "]" ? "array" : last === "\"" ? "string" : "other",
|
|
4166
|
+
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",
|
|
4167
|
+
...offset === void 0 ? {} : { syntaxOffset: Number(offset) },
|
|
4168
|
+
...rawData.includes("\r") ? { hasCarriageReturn: true } : {},
|
|
4169
|
+
...rawData.includes("�") ? { hasReplacementCharacter: true } : {}
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
function decodeModelServerFrame(rawData, options, eventType) {
|
|
4173
|
+
const controlKind = qoderControlFrameKind(rawData);
|
|
4174
|
+
if (controlKind !== void 0) {
|
|
4175
|
+
options.onControlFrame?.(controlKind);
|
|
4176
|
+
return;
|
|
4177
|
+
}
|
|
4178
|
+
let value;
|
|
4179
|
+
try {
|
|
4180
|
+
value = JSON.parse(rawData);
|
|
4181
|
+
} catch (error) {
|
|
4182
|
+
const details = malformedFrameDetails(rawData, eventType, error);
|
|
4183
|
+
options.onMalformedFrame?.(details);
|
|
4184
|
+
if (eventType === "error") throw qoderModelServerError(options.httpStatus ?? 200, { error: { message: "Qoder model-server emitted a non-JSON error SSE event." } }, "sse", options.headers);
|
|
4185
|
+
throw new QoderMalformedJsonFrameError(details.syntax === "truncated", /^\{\s*"choices"\s*:\s*\[\s*\]\s*[,}]/u.test(rawData));
|
|
4186
|
+
}
|
|
4187
|
+
if (eventType === "error") throw qoderModelServerError(options.httpStatus ?? 200, value, "sse", options.headers);
|
|
4188
|
+
if (value === null || typeof value !== "object") throw new QoderLlmError("Malformed model-server SSE frame.", "MALFORMED_RESPONSE");
|
|
4189
|
+
const payload = value;
|
|
4190
|
+
if (payload.error !== void 0) throw qoderModelServerError(options.httpStatus ?? 200, payload, "sse", options.headers);
|
|
4191
|
+
return payload;
|
|
4192
|
+
}
|
|
4193
|
+
function parseModelServerSse(stream, options = {}) {
|
|
4194
|
+
return parseQoderChunks(stream, decodeModelServerFrame, options, {
|
|
4195
|
+
ignoreMalformedAfterFinish: true,
|
|
4196
|
+
allowIncompleteAuxiliaryFrames: true,
|
|
4197
|
+
recoverUnprefixedContinuations: true
|
|
4198
|
+
});
|
|
4199
|
+
}
|
|
4200
|
+
//#endregion
|
|
4201
|
+
//#region src/qoder/transport/model-server.ts
|
|
4202
|
+
/** Convert the shared Qoder turn into the model-server's OpenAI-shaped request. */
|
|
4203
|
+
function modelServerBody(request) {
|
|
4204
|
+
return {
|
|
4205
|
+
model: request.modelKey,
|
|
4206
|
+
messages: request.messages,
|
|
4207
|
+
stream: true,
|
|
4208
|
+
stream_options: { include_usage: true },
|
|
4209
|
+
metadata: {
|
|
4210
|
+
context: {
|
|
4211
|
+
request_id: request.requestId,
|
|
4212
|
+
request_set_id: request.requestSetId,
|
|
4213
|
+
session_id: request.sessionId,
|
|
4214
|
+
task_id: request.taskId,
|
|
4215
|
+
client_type: "5"
|
|
4216
|
+
},
|
|
4217
|
+
business: request.business
|
|
4218
|
+
},
|
|
4219
|
+
...request.tools.length > 0 ? { tools: request.tools } : {},
|
|
4220
|
+
max_tokens: request.maxTokens,
|
|
4221
|
+
...request.contextLength === void 0 ? {} : { context_length: request.contextLength },
|
|
4222
|
+
...request.reasoningEffort === void 0 ? {} : { reasoning_effort: request.reasoningEffort }
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
async function* streamModelServerChat(options, model, credentials, messages, dependencies) {
|
|
4226
|
+
const request = await prepareQoderRequest(options, credentials.userID, messages, model, void 0, void 0, dependencies.region, dependencies.hostRun);
|
|
4227
|
+
const body = JSON.stringify(modelServerBody(request));
|
|
4228
|
+
const startedAt = performance.now();
|
|
4229
|
+
dependencies.logger?.debug?.("[Qoder Stream] Request context", {
|
|
4230
|
+
requestId: request.requestId,
|
|
4231
|
+
model: request.modelKey,
|
|
4232
|
+
region: dependencies.region,
|
|
4233
|
+
purpose: options.purpose ?? "chat",
|
|
4234
|
+
transport: "http"
|
|
4235
|
+
});
|
|
4236
|
+
const controller = new AbortController();
|
|
4237
|
+
const onAbort = () => controller.abort(options.signal?.reason);
|
|
4238
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
4239
|
+
if (options.signal?.aborted) onAbort();
|
|
4240
|
+
let headerTimedOut = false;
|
|
4241
|
+
let idleTimedOut = false;
|
|
4242
|
+
let headerTimer;
|
|
4243
|
+
let idleTimer;
|
|
4244
|
+
const clearTimers = () => {
|
|
4245
|
+
if (headerTimer) clearTimeout(headerTimer);
|
|
4246
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
4247
|
+
};
|
|
4248
|
+
const resetIdle = () => {
|
|
4249
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
4250
|
+
idleTimer = setTimeout(() => {
|
|
4251
|
+
idleTimedOut = true;
|
|
4252
|
+
controller.abort("stream idle timeout");
|
|
4253
|
+
}, dependencies.streamIdleTimeoutMs);
|
|
4254
|
+
};
|
|
4255
|
+
try {
|
|
4256
|
+
const compression = new QoderRequestCompression();
|
|
4257
|
+
let activeCredentials = credentials;
|
|
4258
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4259
|
+
if (controller.signal.aborted) throw new QoderLlmError("Qoder model request was aborted.", "ABORTED");
|
|
4260
|
+
let response;
|
|
4261
|
+
try {
|
|
4262
|
+
response = await compression.send({
|
|
4263
|
+
url: getQoderModelServerUrl(dependencies.region),
|
|
4264
|
+
protocol: "http",
|
|
4265
|
+
body,
|
|
4266
|
+
enabled: dependencies.requestCompression,
|
|
4267
|
+
signal: controller.signal,
|
|
4268
|
+
logger: dependencies.logger,
|
|
4269
|
+
requestId: request.requestId,
|
|
4270
|
+
onRejection: async (rejection) => {
|
|
4271
|
+
dependencies.logger?.warn?.("[Qoder Stream] Compression rejected", {
|
|
4272
|
+
requestId: request.requestId,
|
|
4273
|
+
status: rejection.status,
|
|
4274
|
+
contentType: rejection.contentType,
|
|
4275
|
+
serverRequestId: rejection.serverRequestId,
|
|
4276
|
+
bodyTruncated: rejection.bodyTruncated,
|
|
4277
|
+
reason: rejection.reason,
|
|
4278
|
+
request: compression.diagnostics
|
|
4279
|
+
});
|
|
4280
|
+
},
|
|
4281
|
+
dispatch: async (wireBody, headers) => {
|
|
4282
|
+
headerTimer = setTimeout(() => {
|
|
4283
|
+
headerTimedOut = true;
|
|
4284
|
+
controller.abort("response header timeout");
|
|
4285
|
+
}, dependencies.responseHeaderTimeoutMs);
|
|
4286
|
+
try {
|
|
4287
|
+
return await dependencies.fetch(getQoderModelServerUrl(dependencies.region), {
|
|
4288
|
+
method: "POST",
|
|
4289
|
+
headers,
|
|
4290
|
+
body: wireBody,
|
|
4291
|
+
signal: controller.signal
|
|
4292
|
+
});
|
|
4293
|
+
} finally {
|
|
4294
|
+
if (headerTimer) clearTimeout(headerTimer);
|
|
4295
|
+
}
|
|
4296
|
+
},
|
|
4297
|
+
headers: {
|
|
4298
|
+
...attributionHeaders(),
|
|
4299
|
+
...qoderModelClientHeaders(),
|
|
4300
|
+
"Accept": "text/event-stream",
|
|
4301
|
+
"Content-Type": "application/json",
|
|
4302
|
+
"Authorization": `Bearer ${activeCredentials.authToken}`,
|
|
4303
|
+
"X-Request-ID": request.requestId,
|
|
4304
|
+
"X-Session-ID": request.sessionId
|
|
4305
|
+
}
|
|
4306
|
+
});
|
|
4307
|
+
} finally {
|
|
4308
|
+
if (headerTimer) clearTimeout(headerTimer);
|
|
4309
|
+
}
|
|
4310
|
+
const responseRequestId = qoderRequestId(response.headers);
|
|
4311
|
+
dependencies.logger?.debug?.("[Qoder Stream] Response headers received", {
|
|
4312
|
+
region: dependencies.region,
|
|
4313
|
+
transport: "http",
|
|
4314
|
+
status: response.status,
|
|
4315
|
+
contentType: response.headers.get("content-type"),
|
|
4316
|
+
contentEncoding: response.headers.get("content-encoding"),
|
|
4317
|
+
attempt: attempt + 1,
|
|
4318
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
4319
|
+
...responseRequestId === void 0 ? {} : { requestId: responseRequestId }
|
|
4320
|
+
});
|
|
4321
|
+
if (!response.ok) {
|
|
4322
|
+
let cause;
|
|
4323
|
+
resetIdle();
|
|
4324
|
+
try {
|
|
4325
|
+
cause = await readLimitedText(response, defaultMaxErrorBytes, "Qoder model-server error");
|
|
4326
|
+
} catch {
|
|
4327
|
+
cause = void 0;
|
|
4328
|
+
}
|
|
4329
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
4330
|
+
if (controller.signal.aborted) throw new QoderLlmError("Qoder model-server error body was interrupted.", "ABORTED");
|
|
4331
|
+
dependencies.logger?.warn?.("[Qoder Stream] HTTP request rejected", {
|
|
4332
|
+
requestId: request.requestId,
|
|
4333
|
+
status: response.status,
|
|
4334
|
+
contentType: response.headers.get("content-type"),
|
|
4335
|
+
serverRequestId: responseRequestId,
|
|
4336
|
+
request: compression.diagnostics,
|
|
4337
|
+
bodyAvailable: typeof cause === "string"
|
|
4338
|
+
});
|
|
4339
|
+
const error = qoderModelServerError(response.status, cause, "http", response.headers);
|
|
4340
|
+
if (attempt === 0 && canRefreshModelCredentials(error)) {
|
|
4341
|
+
activeCredentials = await dependencies.refreshCredentials(activeCredentials, options.signal);
|
|
4342
|
+
if (activeCredentials.userID !== credentials.userID) throw new QoderLlmError("Qoder subscriber identity changed during authentication recovery.", "AUTH");
|
|
4343
|
+
continue;
|
|
4344
|
+
}
|
|
4345
|
+
throw error;
|
|
4346
|
+
}
|
|
4347
|
+
if (!response.body) throw new QoderLlmError("Qoder model-server response has no body.", "EMPTY_RESPONSE");
|
|
4348
|
+
resetIdle();
|
|
4349
|
+
const streamStartedAt = performance.now();
|
|
4350
|
+
let chunkCount = 0;
|
|
4351
|
+
let firstChunkReceived = false;
|
|
4352
|
+
let finishReason;
|
|
4353
|
+
for await (const chunk of parseModelServerSse(response.body, {
|
|
4354
|
+
headers: response.headers,
|
|
4355
|
+
httpStatus: response.status,
|
|
4356
|
+
onActivity: resetIdle,
|
|
4357
|
+
onControlFrame: (kind) => {
|
|
4358
|
+
if (kind !== "done") dependencies.logger?.warn?.("[Qoder Stream] Control frame skipped", {
|
|
4359
|
+
transport: "http",
|
|
4360
|
+
kind,
|
|
4361
|
+
...responseRequestId === void 0 ? {} : { requestId: responseRequestId }
|
|
4362
|
+
});
|
|
4363
|
+
},
|
|
4364
|
+
onMalformedFrame: (details) => dependencies.logger?.warn?.("[Qoder Stream] Malformed SSE frame", {
|
|
4365
|
+
transport: "http",
|
|
4366
|
+
...details,
|
|
4367
|
+
...responseRequestId === void 0 ? {} : { requestId: responseRequestId }
|
|
4368
|
+
})
|
|
4369
|
+
})) {
|
|
4370
|
+
compression.confirm();
|
|
4371
|
+
chunkCount++;
|
|
4372
|
+
if (!firstChunkReceived) {
|
|
4373
|
+
firstChunkReceived = true;
|
|
4374
|
+
dependencies.logger?.debug?.("[Qoder Stream] First chunk received", {
|
|
4375
|
+
transport: "http",
|
|
4376
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
4377
|
+
...responseRequestId === void 0 ? {} : { requestId: responseRequestId }
|
|
4378
|
+
});
|
|
4379
|
+
}
|
|
4380
|
+
if (chunk.type === "finish") finishReason = chunk.reason.kind;
|
|
4381
|
+
yield chunk;
|
|
4382
|
+
}
|
|
4383
|
+
dependencies.logger?.debug?.("[Qoder Stream] Stream completed", {
|
|
4384
|
+
transport: "http",
|
|
4385
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
4386
|
+
streamDurationMs: Math.round(performance.now() - streamStartedAt),
|
|
4387
|
+
chunkCount,
|
|
4388
|
+
...finishReason === void 0 ? {} : { finishReason },
|
|
4389
|
+
...responseRequestId === void 0 ? {} : { requestId: responseRequestId }
|
|
4390
|
+
});
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4393
|
+
} catch (error) {
|
|
4394
|
+
if (options.signal?.aborted) throw new QoderLlmError("Qoder model request was aborted.", "ABORTED");
|
|
4395
|
+
if (headerTimedOut) throw new QoderLlmError("Qoder model-server response header timeout.", "TIMEOUT");
|
|
4396
|
+
if (idleTimedOut) throw new QoderLlmError("Qoder model-server stream idle timeout.", "TIMEOUT");
|
|
4397
|
+
throw error;
|
|
4398
|
+
} finally {
|
|
4399
|
+
clearTimers();
|
|
4400
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
4401
|
+
controller.abort("stream finished");
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
//#endregion
|
|
3076
4405
|
//#region src/qoder/transport/default-transport.ts
|
|
3077
4406
|
const defaultStreamIdleTimeoutMs = 300 * 1e3;
|
|
3078
4407
|
function aborted(message) {
|
|
3079
4408
|
return new QoderLlmError(message, "ABORTED");
|
|
3080
4409
|
}
|
|
3081
4410
|
var DefaultQoderTransport = class {
|
|
4411
|
+
authorization;
|
|
4412
|
+
resolveCredential;
|
|
3082
4413
|
region;
|
|
4414
|
+
requestCompression;
|
|
4415
|
+
modelTransport;
|
|
3083
4416
|
resolvePat;
|
|
3084
4417
|
fetchImpl;
|
|
3085
4418
|
logger;
|
|
@@ -3091,19 +4424,34 @@ var DefaultQoderTransport = class {
|
|
|
3091
4424
|
modelFlights = new SingleFlight();
|
|
3092
4425
|
attachments;
|
|
3093
4426
|
imageUploader;
|
|
3094
|
-
|
|
4427
|
+
credentialLeases = /* @__PURE__ */ new WeakMap();
|
|
3095
4428
|
preserveThinking;
|
|
4429
|
+
modelQueueMaxWaitMs;
|
|
4430
|
+
onQueueUpdate;
|
|
4431
|
+
captureQueueLeaseOwner;
|
|
3096
4432
|
constructor(options) {
|
|
4433
|
+
this.modelQueueMaxWaitMs = options.modelQueueMaxWaitMs;
|
|
4434
|
+
this.onQueueUpdate = options.onQueueUpdate;
|
|
4435
|
+
this.captureQueueLeaseOwner = options.captureQueueLeaseOwner;
|
|
3097
4436
|
this.region = options.region;
|
|
4437
|
+
this.requestCompression = options.requestCompression ?? false;
|
|
4438
|
+
this.modelTransport = options.modelTransport ?? "legacy";
|
|
3098
4439
|
this.resolvePat = options.resolvePat;
|
|
4440
|
+
this.resolveCredential = options.resolveCredential;
|
|
4441
|
+
this.authorization = new QoderBrowserAuthorization(options);
|
|
3099
4442
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
3100
4443
|
this.logger = options.logger;
|
|
4444
|
+
this.logger?.debug?.("[Qoder Transport] Request compression configuration", {
|
|
4445
|
+
implementation: "zstd-v1",
|
|
4446
|
+
node: process.version,
|
|
4447
|
+
requestCompression: this.requestCompression
|
|
4448
|
+
});
|
|
3101
4449
|
this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 3e5;
|
|
3102
4450
|
this.responseHeaderTimeoutMs = options.responseHeaderTimeoutMs ?? 6e4;
|
|
3103
4451
|
this.metadataTimeoutMs = options.metadataTimeoutMs;
|
|
3104
4452
|
this.attachments = options.attachments;
|
|
3105
4453
|
this.preserveThinking = options.preserveThinking;
|
|
3106
|
-
this.auth = new
|
|
4454
|
+
this.auth = new QoderAuthManager({
|
|
3107
4455
|
fetch: this.fetchImpl,
|
|
3108
4456
|
logger: this.logger,
|
|
3109
4457
|
region: this.region,
|
|
@@ -3122,24 +4470,10 @@ var DefaultQoderTransport = class {
|
|
|
3122
4470
|
region: this.region,
|
|
3123
4471
|
...options.imageUploadTimeoutMs === void 0 ? {} : { timeoutMs: options.imageUploadTimeoutMs },
|
|
3124
4472
|
...options.imageUrlCacheTtlMs === void 0 ? {} : { cacheTtlMs: options.imageUrlCacheTtlMs },
|
|
3125
|
-
refreshCredentials: async (signal) => {
|
|
3126
|
-
const
|
|
3127
|
-
|
|
3128
|
-
return
|
|
3129
|
-
}
|
|
3130
|
-
});
|
|
3131
|
-
this.searchClient = new QoderSearchClient({
|
|
3132
|
-
fetch: this.fetchImpl,
|
|
3133
|
-
logger: this.logger,
|
|
3134
|
-
region: this.region,
|
|
3135
|
-
resolveCredentials: async (signal) => {
|
|
3136
|
-
const pat = await this.requirePat(signal);
|
|
3137
|
-
return this.auth.getCredentials(pat, signal);
|
|
3138
|
-
},
|
|
3139
|
-
refreshCredentials: async (signal) => {
|
|
3140
|
-
const pat = await this.requirePat(signal);
|
|
3141
|
-
this.auth.clear(pat);
|
|
3142
|
-
return this.auth.getCredentials(pat, signal);
|
|
4473
|
+
refreshCredentials: async (rejected, signal) => {
|
|
4474
|
+
const lease = rejected && this.credentialLeases.get(rejected);
|
|
4475
|
+
if (!lease) throw aborted("Qoder image credentials are no longer available.");
|
|
4476
|
+
return lease.resolve(signal, rejected.authToken);
|
|
3143
4477
|
}
|
|
3144
4478
|
});
|
|
3145
4479
|
}
|
|
@@ -3147,28 +4481,43 @@ var DefaultQoderTransport = class {
|
|
|
3147
4481
|
return this.generate(options, model);
|
|
3148
4482
|
}
|
|
3149
4483
|
async discoverModels(signal) {
|
|
3150
|
-
const
|
|
3151
|
-
const key =
|
|
4484
|
+
const lease = await this.lease(signal);
|
|
4485
|
+
const key = lease.key;
|
|
3152
4486
|
return this.modelFlights.run(key, signal, async (sharedSignal) => {
|
|
3153
|
-
const credentials = await
|
|
3154
|
-
|
|
4487
|
+
const credentials = await lease.resolve(sharedSignal);
|
|
4488
|
+
const read = (resolved) => retryMetadataRead(sharedSignal, () => fetchQoderModels(resolved, {
|
|
3155
4489
|
fetch: this.fetchImpl,
|
|
3156
4490
|
signal: sharedSignal,
|
|
3157
4491
|
logger: this.logger,
|
|
3158
4492
|
region: this.region,
|
|
3159
4493
|
timeoutMs: this.metadataTimeoutMs
|
|
3160
4494
|
}));
|
|
4495
|
+
try {
|
|
4496
|
+
return await read(credentials);
|
|
4497
|
+
} catch (error) {
|
|
4498
|
+
if (!(error instanceof QoderLlmError) || error.code !== "AUTH") throw error;
|
|
4499
|
+
return read(await lease.resolve(sharedSignal, credentials.authToken));
|
|
4500
|
+
}
|
|
3161
4501
|
}, () => aborted("Qoder model discovery was aborted."));
|
|
3162
4502
|
}
|
|
3163
4503
|
async readAccount(options) {
|
|
3164
|
-
const
|
|
3165
|
-
return this.usage.
|
|
3166
|
-
|
|
3167
|
-
|
|
4504
|
+
const lease = await this.lease(options?.signal);
|
|
4505
|
+
return this.usage.readWithCredentials(lease.key, (signal) => lease.resolve(signal), {
|
|
4506
|
+
...options,
|
|
4507
|
+
refresh: (rejected, signal) => lease.resolve(signal, rejected.authToken)
|
|
3168
4508
|
});
|
|
3169
4509
|
}
|
|
3170
4510
|
async searchWeb(request, signal) {
|
|
3171
|
-
|
|
4511
|
+
const lease = await this.lease(signal);
|
|
4512
|
+
return new QoderSearchClient({
|
|
4513
|
+
fetch: this.fetchImpl,
|
|
4514
|
+
logger: this.logger,
|
|
4515
|
+
region: this.region,
|
|
4516
|
+
resolveCredentials: async (nextSignal) => {
|
|
4517
|
+
return lease.resolve(nextSignal);
|
|
4518
|
+
},
|
|
4519
|
+
refreshCredentials: (rejected, nextSignal) => lease.resolve(nextSignal, rejected.authToken)
|
|
4520
|
+
}).search(request, signal);
|
|
3172
4521
|
}
|
|
3173
4522
|
async requirePat(signal) {
|
|
3174
4523
|
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
@@ -3177,27 +4526,50 @@ var DefaultQoderTransport = class {
|
|
|
3177
4526
|
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
3178
4527
|
return pat;
|
|
3179
4528
|
}
|
|
4529
|
+
async lease(signal) {
|
|
4530
|
+
if (signal?.aborted) throw aborted("Qoder request was aborted.");
|
|
4531
|
+
const managed = await this.resolveCredential?.(signal);
|
|
4532
|
+
if (managed) return managed;
|
|
4533
|
+
const pat = await this.requirePat(signal);
|
|
4534
|
+
return {
|
|
4535
|
+
key: opaqueCredentialKey(pat),
|
|
4536
|
+
resolve: async (nextSignal, rejectedToken) => {
|
|
4537
|
+
const current = await this.auth.getCredentials(pat, nextSignal);
|
|
4538
|
+
if (rejectedToken !== void 0 && current.authToken === rejectedToken) return this.auth.refreshCredentials(pat, current, nextSignal);
|
|
4539
|
+
return current;
|
|
4540
|
+
}
|
|
4541
|
+
};
|
|
4542
|
+
}
|
|
3180
4543
|
async *generate(options, model) {
|
|
3181
4544
|
if (options.signal?.aborted) throw aborted("Request was aborted prior to generation.");
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
const
|
|
4545
|
+
const queueOwner = options.sessionId === void 0 ? void 0 : this.captureQueueLeaseOwner?.(String(options.sessionId));
|
|
4546
|
+
validateQoderRequestShape(options, model, this.attachments);
|
|
4547
|
+
const lease = await this.lease(options.signal);
|
|
4548
|
+
const credentials = await lease.resolve(options.signal);
|
|
4549
|
+
this.credentialLeases.set(credentials, lease);
|
|
3185
4550
|
const messages = await translateQoderMessages(options, this.attachments, {
|
|
3186
4551
|
uploader: this.imageUploader,
|
|
3187
4552
|
credentials,
|
|
3188
4553
|
preserveThinking: this.preserveThinking
|
|
3189
4554
|
});
|
|
3190
|
-
|
|
4555
|
+
const chatCredentials = await lease.resolve(options.signal);
|
|
4556
|
+
yield* (this.modelTransport === "legacy" ? streamQoderChat : streamModelServerChat)(options, model, chatCredentials, messages, {
|
|
3191
4557
|
fetch: this.fetchImpl,
|
|
3192
4558
|
logger: this.logger,
|
|
3193
4559
|
region: this.region,
|
|
4560
|
+
requestCompression: this.requestCompression,
|
|
3194
4561
|
responseHeaderTimeoutMs: this.responseHeaderTimeoutMs,
|
|
3195
4562
|
streamIdleTimeoutMs: this.streamIdleTimeoutMs,
|
|
4563
|
+
modelQueueMaxWaitMs: this.modelQueueMaxWaitMs,
|
|
4564
|
+
onQueueUpdate: this.onQueueUpdate,
|
|
4565
|
+
hostRun: queueOwner?.run,
|
|
4566
|
+
onQueueLease: queueOwner?.retain,
|
|
4567
|
+
resolveCredentials: async (refresh, signal) => {
|
|
4568
|
+
const current = await lease.resolve(signal);
|
|
4569
|
+
return refresh ? lease.resolve(signal, current.authToken) : current;
|
|
4570
|
+
},
|
|
3196
4571
|
refreshCredentials: async (rejected, signal) => {
|
|
3197
|
-
|
|
3198
|
-
if (current.authToken !== rejected.authToken) return current;
|
|
3199
|
-
this.auth.clear(pat);
|
|
3200
|
-
return this.auth.getCredentials(pat, signal);
|
|
4572
|
+
return lease.resolve(signal, rejected.authToken);
|
|
3201
4573
|
}
|
|
3202
4574
|
});
|
|
3203
4575
|
}
|
|
@@ -3238,6 +4610,7 @@ const catalogModel = z.object({
|
|
|
3238
4610
|
});
|
|
3239
4611
|
const modelsByRegionSchema = z.dict(z.array(catalogModel));
|
|
3240
4612
|
const Config = z.object({
|
|
4613
|
+
modelQueueMaxWaitMs: z.number().step(1).min(1).max(2147483647).default(defaultModelQueueMaxWaitMs).description("Maximum cumulative model queue wait in milliseconds; changes apply to new calls"),
|
|
3241
4614
|
region: z.union(["global", "china"]).default("global"),
|
|
3242
4615
|
modelsByRegion: modelsByRegionSchema.default({}),
|
|
3243
4616
|
streamIdleTimeoutMs: z.number().step(1).min(1).default(defaultStreamIdleTimeoutMs),
|
|
@@ -3247,7 +4620,9 @@ const Config = z.object({
|
|
|
3247
4620
|
"auto",
|
|
3248
4621
|
"always",
|
|
3249
4622
|
"disabled"
|
|
3250
|
-
]).default("auto")
|
|
4623
|
+
]).default("auto"),
|
|
4624
|
+
requestCompression: z.boolean().default(false),
|
|
4625
|
+
modelTransport: z.union(["http", "legacy"]).default("legacy")
|
|
3251
4626
|
});
|
|
3252
4627
|
const PluginConfig = typeof Config.volatile === "function" ? Config.volatile() : Config;
|
|
3253
4628
|
function readConfig(value) {
|
|
@@ -3268,6 +4643,125 @@ function modelsFor(config, region) {
|
|
|
3268
4643
|
return resolveModels(config.modelsByRegion?.[region]);
|
|
3269
4644
|
}
|
|
3270
4645
|
//#endregion
|
|
4646
|
+
//#region src/dsh/queue-leases.ts
|
|
4647
|
+
/** Correlate transport-owned release actions with committed DSH turn boundaries. */
|
|
4648
|
+
/** Bind committed boundaries and recover ownership when loaded during an open turn. */
|
|
4649
|
+
function bindQoderQueueLeases(ctx, leases) {
|
|
4650
|
+
ctx.on("session/event", (session, event) => {
|
|
4651
|
+
if (event.type === "turn/start") leases.start(session.id, event.data.turn);
|
|
4652
|
+
if (event.type === "turn/end") leases.end(session.id, event.data.turn);
|
|
4653
|
+
});
|
|
4654
|
+
ctx.on("agent/request", async ({ agent, turn }, next) => {
|
|
4655
|
+
leases.start(agent.session.id, turn);
|
|
4656
|
+
return next();
|
|
4657
|
+
});
|
|
4658
|
+
}
|
|
4659
|
+
var QoderQueueLeases = class {
|
|
4660
|
+
runs = /* @__PURE__ */ new Map();
|
|
4661
|
+
pending = /* @__PURE__ */ new Set();
|
|
4662
|
+
disposed = false;
|
|
4663
|
+
start(sessionId, turn) {
|
|
4664
|
+
if (this.disposed || this.runs.get(sessionId)?.turn === turn) return;
|
|
4665
|
+
this.end(sessionId);
|
|
4666
|
+
this.runs.set(sessionId, {
|
|
4667
|
+
turn,
|
|
4668
|
+
run: Object.freeze({ id: randomUUID() }),
|
|
4669
|
+
leases: /* @__PURE__ */ new Map()
|
|
4670
|
+
});
|
|
4671
|
+
}
|
|
4672
|
+
retain = (lease) => {
|
|
4673
|
+
const run = this.runs.get(lease.sessionId);
|
|
4674
|
+
if (this.disposed || !run) return false;
|
|
4675
|
+
if (!run.leases.has(lease.key)) run.leases.set(lease.key, lease);
|
|
4676
|
+
return true;
|
|
4677
|
+
};
|
|
4678
|
+
/** Bind ownership before provider I/O; a late old call cannot join a newer turn. */
|
|
4679
|
+
capture = (sessionId) => {
|
|
4680
|
+
const owner = this.runs.get(sessionId);
|
|
4681
|
+
if (!owner || this.disposed) return void 0;
|
|
4682
|
+
return {
|
|
4683
|
+
run: owner.run,
|
|
4684
|
+
retain: (lease) => lease.sessionId === sessionId && this.runs.get(sessionId) === owner && this.retain(lease)
|
|
4685
|
+
};
|
|
4686
|
+
};
|
|
4687
|
+
async end(sessionId, turn) {
|
|
4688
|
+
const run = this.runs.get(sessionId);
|
|
4689
|
+
if (!run || turn !== void 0 && turn !== run.turn) return;
|
|
4690
|
+
this.runs.delete(sessionId);
|
|
4691
|
+
const cleanup = Promise.allSettled([...run.leases.values()].map((lease) => Promise.resolve().then(() => lease.release())));
|
|
4692
|
+
this.pending.add(cleanup);
|
|
4693
|
+
try {
|
|
4694
|
+
await cleanup;
|
|
4695
|
+
} finally {
|
|
4696
|
+
this.pending.delete(cleanup);
|
|
4697
|
+
}
|
|
4698
|
+
}
|
|
4699
|
+
async dispose() {
|
|
4700
|
+
this.disposed = true;
|
|
4701
|
+
await Promise.allSettled([...this.runs.keys()].map((id) => this.end(id)));
|
|
4702
|
+
await Promise.allSettled([...this.pending]);
|
|
4703
|
+
}
|
|
4704
|
+
};
|
|
4705
|
+
//#endregion
|
|
4706
|
+
//#region src/dsh/queue-status.ts
|
|
4707
|
+
var QoderQueueStatusStore = class {
|
|
4708
|
+
calls = /* @__PURE__ */ new Map();
|
|
4709
|
+
active = true;
|
|
4710
|
+
update = (event) => {
|
|
4711
|
+
if (!this.active) return;
|
|
4712
|
+
if (event.type === "clear") this.calls.delete(event.callId);
|
|
4713
|
+
else this.calls.set(event.snapshot.callId, { ...event.snapshot });
|
|
4714
|
+
};
|
|
4715
|
+
read(sessionId) {
|
|
4716
|
+
return [...this.calls.values()].filter((value) => value.sessionId === sessionId).map((value) => ({ ...value }));
|
|
4717
|
+
}
|
|
4718
|
+
dispose() {
|
|
4719
|
+
this.active = false;
|
|
4720
|
+
this.calls.clear();
|
|
4721
|
+
}
|
|
4722
|
+
};
|
|
4723
|
+
//#endregion
|
|
4724
|
+
//#region src/dsh/transport-runtime.ts
|
|
4725
|
+
function transportConfig(value) {
|
|
4726
|
+
return {
|
|
4727
|
+
region: value.region ?? "global",
|
|
4728
|
+
streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
|
|
4729
|
+
responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
|
|
4730
|
+
modelQueueMaxWaitMs: value.modelQueueMaxWaitMs ?? 36e5,
|
|
4731
|
+
preserveThinking: value.preserveThinking ?? true,
|
|
4732
|
+
requestCompression: value.requestCompression ?? false,
|
|
4733
|
+
modelTransport: value.modelTransport ?? "legacy"
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
var QoderTransportRuntime = class {
|
|
4737
|
+
snapshot;
|
|
4738
|
+
constructor(value, factory) {
|
|
4739
|
+
this.create = factory;
|
|
4740
|
+
const config = Object.freeze(transportConfig(value));
|
|
4741
|
+
this.snapshot = {
|
|
4742
|
+
config,
|
|
4743
|
+
transport: this.create(config)
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
create;
|
|
4747
|
+
get config() {
|
|
4748
|
+
return this.snapshot.config;
|
|
4749
|
+
}
|
|
4750
|
+
get transport() {
|
|
4751
|
+
return this.snapshot.transport;
|
|
4752
|
+
}
|
|
4753
|
+
update(value, force = false) {
|
|
4754
|
+
const config = Object.freeze(transportConfig(value));
|
|
4755
|
+
if (!force && Object.keys(config).every((key) => config[key] === this.config[key])) return false;
|
|
4756
|
+
const transport = this.create(config);
|
|
4757
|
+
this.snapshot = {
|
|
4758
|
+
config,
|
|
4759
|
+
transport
|
|
4760
|
+
};
|
|
4761
|
+
return true;
|
|
4762
|
+
}
|
|
4763
|
+
};
|
|
4764
|
+
//#endregion
|
|
3271
4765
|
//#region src/dsh/catalog-lifecycle.ts
|
|
3272
4766
|
/** Region-scoped Qoder model discovery, effective catalogs, and advisory persistence. */
|
|
3273
4767
|
var QoderCatalogLifecycle = class {
|
|
@@ -3317,6 +4811,16 @@ var QoderCatalogLifecycle = class {
|
|
|
3317
4811
|
const region = this.options.region();
|
|
3318
4812
|
return mergeQoderDiscoveryMetadata(this.options.configuredModels(region), this.catalogs[region].models);
|
|
3319
4813
|
}
|
|
4814
|
+
/** Subscriber replacement must not retain another subscriber's discovered capabilities. */
|
|
4815
|
+
clearDiscovered() {
|
|
4816
|
+
for (const state of Object.values(this.catalogs)) {
|
|
4817
|
+
state.models = [];
|
|
4818
|
+
state.generation++;
|
|
4819
|
+
state.expiresAt = 0;
|
|
4820
|
+
state.inflight = void 0;
|
|
4821
|
+
state.dirty = false;
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
3320
4824
|
/** Automatic reads join the newest discovery and never wait for settings writes. */
|
|
3321
4825
|
async refresh() {
|
|
3322
4826
|
while (!this.disposed) {
|
|
@@ -3332,10 +4836,11 @@ var QoderCatalogLifecycle = class {
|
|
|
3332
4836
|
return;
|
|
3333
4837
|
}
|
|
3334
4838
|
}
|
|
3335
|
-
/**
|
|
4839
|
+
/** Active discovery supersedes older work; a foreign transport is an isolated preview. */
|
|
3336
4840
|
discover(signal, requestTransport) {
|
|
3337
4841
|
if (this.disposed || signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model discovery was aborted.", "ABORTED"));
|
|
3338
4842
|
this.observeTransport();
|
|
4843
|
+
if (requestTransport && requestTransport !== this.activeTransport) return requestTransport.discoverModels(signal);
|
|
3339
4844
|
const region = this.options.region();
|
|
3340
4845
|
const state = this.catalogs[region];
|
|
3341
4846
|
const transport = this.activeTransport;
|
|
@@ -3473,6 +4978,9 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3473
4978
|
});
|
|
3474
4979
|
} catch {}
|
|
3475
4980
|
}
|
|
4981
|
+
forgetSession(sessionId) {
|
|
4982
|
+
for (const key of this.sessionTiers.keys()) if (JSON.parse(key)[1] === sessionId) this.sessionTiers.delete(key);
|
|
4983
|
+
}
|
|
3476
4984
|
subscribeSessionTiers(listener) {
|
|
3477
4985
|
this.tierListeners.add(listener);
|
|
3478
4986
|
return () => {
|
|
@@ -3695,9 +5203,14 @@ const qoderRpcApiPath = `/api/qoder-subscription`;
|
|
|
3695
5203
|
const qoderRpcEndpoints = [
|
|
3696
5204
|
"account",
|
|
3697
5205
|
"models",
|
|
5206
|
+
"queue",
|
|
3698
5207
|
"sessionTier",
|
|
3699
5208
|
"readSessionTier",
|
|
3700
|
-
"sessionTierEvents"
|
|
5209
|
+
"sessionTierEvents",
|
|
5210
|
+
"authorization",
|
|
5211
|
+
"beginLogin",
|
|
5212
|
+
"cancelLogin",
|
|
5213
|
+
"selectAuthorization"
|
|
3701
5214
|
];
|
|
3702
5215
|
/** Absolute Fetch-route path of one endpoint. */
|
|
3703
5216
|
function qoderRpcPath(endpoint) {
|
|
@@ -3724,12 +5237,21 @@ function registerQoderRpc(ctx, handler, subscribeTiers) {
|
|
|
3724
5237
|
const registry = ctx.connection?.fetch;
|
|
3725
5238
|
if (typeof registry?.register !== "function") throw new Error("provider-qoder: connection exposes no exact Fetch route registry");
|
|
3726
5239
|
const streams = /* @__PURE__ */ new Set();
|
|
3727
|
-
const disposers =
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
5240
|
+
const disposers = [];
|
|
5241
|
+
try {
|
|
5242
|
+
for (const endpoint of qoderRpcEndpoints) disposers.push(registry.register({
|
|
5243
|
+
path: qoderRpcPath(endpoint),
|
|
5244
|
+
methods: ["POST"],
|
|
5245
|
+
requestBody: "buffered",
|
|
5246
|
+
fetch: (request) => endpoint === "sessionTierEvents" && subscribeTiers ? Promise.resolve(sessionTierEvents(request, subscribeTiers, streams)) : handleQoderRpcRequest(endpoint, handler, request)
|
|
5247
|
+
}));
|
|
5248
|
+
} catch (error) {
|
|
5249
|
+
for (const close of streams) close();
|
|
5250
|
+
for (const dispose of disposers.reverse()) try {
|
|
5251
|
+
Promise.resolve(dispose()).catch(() => {});
|
|
5252
|
+
} catch {}
|
|
5253
|
+
throw error;
|
|
5254
|
+
}
|
|
3733
5255
|
return () => {
|
|
3734
5256
|
for (const close of streams) close();
|
|
3735
5257
|
for (const dispose of disposers) dispose();
|
|
@@ -3809,6 +5331,296 @@ function sessionTierEvents(request, subscribe, streams) {
|
|
|
3809
5331
|
} });
|
|
3810
5332
|
}
|
|
3811
5333
|
//#endregion
|
|
5334
|
+
//#region src/dsh/browser-authorization.ts
|
|
5335
|
+
/** Instance-shared authorization lifecycle. Provider I/O is delegated to Qoder transport. */
|
|
5336
|
+
const browserAuthorizationKey = (region) => credentialKey("provider-qoder", region);
|
|
5337
|
+
function decode(record, region) {
|
|
5338
|
+
if (record === void 0) return {
|
|
5339
|
+
version: 1,
|
|
5340
|
+
method: "pat"
|
|
5341
|
+
};
|
|
5342
|
+
const value = record.kind === "grant" ? record.payload : null;
|
|
5343
|
+
if (!value || value.version !== 1 || ![
|
|
5344
|
+
"pat",
|
|
5345
|
+
"browser",
|
|
5346
|
+
"none"
|
|
5347
|
+
].includes(value.method ?? "")) throw new QoderLlmError("Saved Qoder authorization is invalid. Remove it and sign in again.", "AUTH");
|
|
5348
|
+
if (value.grant) {
|
|
5349
|
+
const grant = value.grant;
|
|
5350
|
+
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");
|
|
5351
|
+
}
|
|
5352
|
+
return value;
|
|
5353
|
+
}
|
|
5354
|
+
const record = (payload) => ({
|
|
5355
|
+
kind: "grant",
|
|
5356
|
+
payload: JSON.parse(JSON.stringify(payload))
|
|
5357
|
+
});
|
|
5358
|
+
const pending = (attempt) => attempt !== void 0 && [
|
|
5359
|
+
"waiting",
|
|
5360
|
+
"verifying",
|
|
5361
|
+
"saving"
|
|
5362
|
+
].includes(attempt.phase);
|
|
5363
|
+
const cancelled = () => new QoderLlmError("Qoder authorization changed or was cancelled.", "ABORTED");
|
|
5364
|
+
var QoderBrowserAuthorizationManager = class {
|
|
5365
|
+
credentials;
|
|
5366
|
+
transport;
|
|
5367
|
+
changed;
|
|
5368
|
+
attempts = /* @__PURE__ */ new Map();
|
|
5369
|
+
volatile = /* @__PURE__ */ new Map();
|
|
5370
|
+
invalid = /* @__PURE__ */ new Set();
|
|
5371
|
+
renewals = /* @__PURE__ */ new Set();
|
|
5372
|
+
lifecycle = new AbortController();
|
|
5373
|
+
activeRegion;
|
|
5374
|
+
regionGeneration = 0;
|
|
5375
|
+
disposed = false;
|
|
5376
|
+
constructor(options) {
|
|
5377
|
+
this.credentials = options.credentials;
|
|
5378
|
+
this.transport = options.transport;
|
|
5379
|
+
this.changed = options.changed;
|
|
5380
|
+
this.activeRegion = options.region;
|
|
5381
|
+
}
|
|
5382
|
+
activateRegion(region) {
|
|
5383
|
+
if (this.activeRegion === region) return;
|
|
5384
|
+
const previous = this.activeRegion;
|
|
5385
|
+
this.activeRegion = region;
|
|
5386
|
+
this.regionGeneration++;
|
|
5387
|
+
if (previous) this.cancel(previous).catch(() => {});
|
|
5388
|
+
}
|
|
5389
|
+
assertRegion(region, generation) {
|
|
5390
|
+
if (this.disposed || this.regionGeneration !== generation || this.activeRegion !== void 0 && this.activeRegion !== region) throw cancelled();
|
|
5391
|
+
}
|
|
5392
|
+
async read(region) {
|
|
5393
|
+
return decode(await this.credentials.readRecord?.(browserAuthorizationKey(region)), region);
|
|
5394
|
+
}
|
|
5395
|
+
async describe(region) {
|
|
5396
|
+
const writable = await this.credentials.describeRecord?.(browserAuthorizationKey(region));
|
|
5397
|
+
let saved;
|
|
5398
|
+
try {
|
|
5399
|
+
saved = await this.read(region);
|
|
5400
|
+
} catch (error) {
|
|
5401
|
+
if (!(error instanceof QoderLlmError) || error.code !== "AUTH") throw error;
|
|
5402
|
+
return {
|
|
5403
|
+
region,
|
|
5404
|
+
method: "none",
|
|
5405
|
+
configured: false,
|
|
5406
|
+
writable: writable?.writable ?? false,
|
|
5407
|
+
expired: true
|
|
5408
|
+
};
|
|
5409
|
+
}
|
|
5410
|
+
const live = this.volatile.get(region);
|
|
5411
|
+
const grant = live && live.grant.id === saved.grant?.id && live.baseToken === saved.grant?.credentials.authToken ? live.grant : saved.grant;
|
|
5412
|
+
const attempt = this.attempts.get(region);
|
|
5413
|
+
const expired = grant !== void 0 && (saved.expired === true || this.invalid.has(grant.id) || grant.refreshExpiresAt !== void 0 && grant.refreshExpiresAt <= Date.now());
|
|
5414
|
+
return {
|
|
5415
|
+
region,
|
|
5416
|
+
method: saved.method,
|
|
5417
|
+
writable: writable?.writable ?? false,
|
|
5418
|
+
configured: saved.method === "pat" ? Boolean(await resolveManagedQoderPat(this.credentials)) : saved.method === "browser" && grant !== void 0 && !expired,
|
|
5419
|
+
...grant ? {
|
|
5420
|
+
profile: {
|
|
5421
|
+
id: grant.credentials.userID,
|
|
5422
|
+
name: grant.credentials.name,
|
|
5423
|
+
email: grant.credentials.email
|
|
5424
|
+
},
|
|
5425
|
+
expired: saved.method === "browser" && expired,
|
|
5426
|
+
unsaved: live?.grant === grant
|
|
5427
|
+
} : {},
|
|
5428
|
+
...attempt ? { attempt: {
|
|
5429
|
+
id: attempt.id,
|
|
5430
|
+
phase: attempt.phase,
|
|
5431
|
+
expiresAt: attempt.expiresAt,
|
|
5432
|
+
...attempt.failure ? { failure: attempt.failure } : {},
|
|
5433
|
+
...pending(attempt) ? { url: attempt.url } : {}
|
|
5434
|
+
} } : {}
|
|
5435
|
+
};
|
|
5436
|
+
}
|
|
5437
|
+
async begin(region) {
|
|
5438
|
+
const generation = this.regionGeneration;
|
|
5439
|
+
await this.assertWritable(region);
|
|
5440
|
+
this.assertRegion(region, generation);
|
|
5441
|
+
if (pending(this.attempts.get(region))) return this.describe(region);
|
|
5442
|
+
const attempt = {
|
|
5443
|
+
id: randomUUID(),
|
|
5444
|
+
phase: "waiting",
|
|
5445
|
+
controller: new AbortController(),
|
|
5446
|
+
expiresAt: Date.now() + 3e5
|
|
5447
|
+
};
|
|
5448
|
+
this.attempts.set(region, attempt);
|
|
5449
|
+
attempt.done = this.login(region, attempt);
|
|
5450
|
+
await Promise.resolve();
|
|
5451
|
+
return this.describe(region);
|
|
5452
|
+
}
|
|
5453
|
+
async login(region, attempt) {
|
|
5454
|
+
let previous;
|
|
5455
|
+
let installed;
|
|
5456
|
+
try {
|
|
5457
|
+
const flow = await this.transport(region).begin(attempt.controller.signal);
|
|
5458
|
+
this.assertAttempt(region, attempt);
|
|
5459
|
+
attempt.url = flow.url;
|
|
5460
|
+
attempt.expiresAt = flow.expiresAt;
|
|
5461
|
+
const grant = await flow.complete();
|
|
5462
|
+
this.assertAttempt(region, attempt);
|
|
5463
|
+
attempt.phase = "saving";
|
|
5464
|
+
await this.credentials.modifyRecord(browserAuthorizationKey(region), async (current) => {
|
|
5465
|
+
this.assertAttempt(region, attempt);
|
|
5466
|
+
previous = current;
|
|
5467
|
+
installed = grant.id;
|
|
5468
|
+
return record({
|
|
5469
|
+
version: 1,
|
|
5470
|
+
method: "browser",
|
|
5471
|
+
grant
|
|
5472
|
+
});
|
|
5473
|
+
});
|
|
5474
|
+
this.assertAttempt(region, attempt);
|
|
5475
|
+
this.volatile.delete(region);
|
|
5476
|
+
attempt.phase = "complete";
|
|
5477
|
+
this.changed();
|
|
5478
|
+
} catch (error) {
|
|
5479
|
+
attempt.failure = attempt.phase === "saving" ? "storage" : error instanceof QoderLlmError && [
|
|
5480
|
+
"TRANSPORT",
|
|
5481
|
+
"SERVER",
|
|
5482
|
+
"TIMEOUT"
|
|
5483
|
+
].includes(error.code) ? "connection" : "authorization";
|
|
5484
|
+
if (installed && attempt.controller.signal.aborted) await this.credentials.modifyRecord(browserAuthorizationKey(region), async (current) => {
|
|
5485
|
+
if (decode(current, region).grant?.id !== installed) return void 0;
|
|
5486
|
+
return previous ?? record({
|
|
5487
|
+
version: 1,
|
|
5488
|
+
method: "pat"
|
|
5489
|
+
});
|
|
5490
|
+
}).catch(() => {
|
|
5491
|
+
attempt.cleanupFailed = true;
|
|
5492
|
+
});
|
|
5493
|
+
attempt.phase = attempt.cleanupFailed ? "failed" : attempt.controller.signal.aborted ? "cancelled" : error instanceof QoderLlmError && error.code === "TIMEOUT" && Date.now() >= attempt.expiresAt ? "timeout" : "failed";
|
|
5494
|
+
} finally {
|
|
5495
|
+
attempt.url = void 0;
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
assertAttempt(region, attempt) {
|
|
5499
|
+
if (this.disposed || attempt.controller.signal.aborted || this.attempts.get(region) !== attempt) throw cancelled();
|
|
5500
|
+
}
|
|
5501
|
+
async cancel(region) {
|
|
5502
|
+
const attempt = this.attempts.get(region);
|
|
5503
|
+
if (!pending(attempt)) return;
|
|
5504
|
+
attempt.controller.abort();
|
|
5505
|
+
await attempt.done;
|
|
5506
|
+
if (attempt.cleanupFailed) throw new QoderLlmError("Could not restore the previous Qoder authorization after cancellation. Reload account status.", "INVALID_REQUEST");
|
|
5507
|
+
}
|
|
5508
|
+
async select(region, method) {
|
|
5509
|
+
const generation = this.regionGeneration;
|
|
5510
|
+
await this.assertWritable(region);
|
|
5511
|
+
await this.cancel(region);
|
|
5512
|
+
await this.credentials.modifyRecord(browserAuthorizationKey(region), async (current) => {
|
|
5513
|
+
this.assertRegion(region, generation);
|
|
5514
|
+
const saved = method === "none" ? {
|
|
5515
|
+
version: 1,
|
|
5516
|
+
method
|
|
5517
|
+
} : decode(current, region);
|
|
5518
|
+
if (method === "browser" && !saved.grant) throw new QoderLlmError("Sign in to Qoder first.", "MISSING_CREDENTIAL");
|
|
5519
|
+
return record(method === "none" ? {
|
|
5520
|
+
version: 1,
|
|
5521
|
+
method
|
|
5522
|
+
} : {
|
|
5523
|
+
...saved,
|
|
5524
|
+
method
|
|
5525
|
+
});
|
|
5526
|
+
});
|
|
5527
|
+
if (method === "none") this.volatile.delete(region);
|
|
5528
|
+
this.attempts.delete(region);
|
|
5529
|
+
this.changed();
|
|
5530
|
+
return this.describe(region);
|
|
5531
|
+
}
|
|
5532
|
+
async lease(region, signal) {
|
|
5533
|
+
if (signal?.aborted || this.disposed) throw cancelled();
|
|
5534
|
+
const saved = await this.read(region);
|
|
5535
|
+
if (saved.method === "pat") return void 0;
|
|
5536
|
+
const grant = saved.grant;
|
|
5537
|
+
if (saved.method !== "browser" || !grant) throw new QoderLlmError("Sign in to Qoder or select a Managed Qoder PAT.", "MISSING_CREDENTIAL");
|
|
5538
|
+
if (saved.expired || this.invalid.has(grant.id)) throw new QoderLlmError("Qoder browser authorization expired. Sign in again.", "AUTH");
|
|
5539
|
+
let captured = grant;
|
|
5540
|
+
return {
|
|
5541
|
+
key: `browser:${grant.id}`,
|
|
5542
|
+
resolve: async (nextSignal, rejectedToken) => {
|
|
5543
|
+
if (nextSignal?.aborted || this.disposed) throw cancelled();
|
|
5544
|
+
const latest = await this.read(region);
|
|
5545
|
+
if (latest.method !== "browser" || latest.grant?.id !== grant.id) {
|
|
5546
|
+
if (rejectedToken !== void 0 || captured.expiresAt <= Date.now()) throw cancelled();
|
|
5547
|
+
return captured.credentials;
|
|
5548
|
+
}
|
|
5549
|
+
const combined = nextSignal ? AbortSignal.any([nextSignal, this.lifecycle.signal]) : this.lifecycle.signal;
|
|
5550
|
+
const renewal = this.renew(region, grant.id, combined, rejectedToken);
|
|
5551
|
+
this.renewals.add(renewal);
|
|
5552
|
+
try {
|
|
5553
|
+
captured = await renewal;
|
|
5554
|
+
} finally {
|
|
5555
|
+
this.renewals.delete(renewal);
|
|
5556
|
+
}
|
|
5557
|
+
if (combined.aborted) throw cancelled();
|
|
5558
|
+
return captured.credentials;
|
|
5559
|
+
}
|
|
5560
|
+
};
|
|
5561
|
+
}
|
|
5562
|
+
async renew(region, id, signal, rejectedToken) {
|
|
5563
|
+
if (this.invalid.has(id)) throw new QoderLlmError("Qoder browser authorization expired. Sign in again.", "AUTH");
|
|
5564
|
+
const snapshot = await this.read(region);
|
|
5565
|
+
const fresh = snapshot.method === "browser" && snapshot.grant?.id === id ? snapshot.grant : void 0;
|
|
5566
|
+
if (fresh && !snapshot.expired && !this.volatile.has(region) && fresh.expiresAt > Date.now() + 3e5 && rejectedToken !== fresh.credentials.authToken) return fresh;
|
|
5567
|
+
let rotated;
|
|
5568
|
+
let baseToken;
|
|
5569
|
+
try {
|
|
5570
|
+
await this.credentials.modifyRecord(browserAuthorizationKey(region), async (current) => {
|
|
5571
|
+
const saved = decode(current, region);
|
|
5572
|
+
if (this.disposed || signal?.aborted || saved.method !== "browser" || saved.grant?.id !== id) throw cancelled();
|
|
5573
|
+
if (saved.expired) throw new QoderLlmError("Qoder browser authorization expired. Sign in again.", "AUTH");
|
|
5574
|
+
const volatile = this.volatile.get(region);
|
|
5575
|
+
baseToken = saved.grant.credentials.authToken;
|
|
5576
|
+
const source = volatile?.grant.id === id && volatile.baseToken === baseToken ? volatile.grant : saved.grant;
|
|
5577
|
+
rotated = await this.transport(region).refresh(source, this.lifecycle.signal, rejectedToken);
|
|
5578
|
+
return rotated === saved.grant ? void 0 : record({
|
|
5579
|
+
...saved,
|
|
5580
|
+
grant: rotated
|
|
5581
|
+
});
|
|
5582
|
+
});
|
|
5583
|
+
this.volatile.delete(region);
|
|
5584
|
+
return rotated;
|
|
5585
|
+
} catch (error) {
|
|
5586
|
+
if (error instanceof QoderLlmError && error.code === "AUTH") {
|
|
5587
|
+
this.invalid.add(id);
|
|
5588
|
+
await this.credentials.modifyRecord(browserAuthorizationKey(region), async (current) => {
|
|
5589
|
+
const saved = decode(current, region);
|
|
5590
|
+
return saved.grant?.id === id ? record({
|
|
5591
|
+
...saved,
|
|
5592
|
+
expired: true
|
|
5593
|
+
}) : void 0;
|
|
5594
|
+
}).catch(() => {});
|
|
5595
|
+
}
|
|
5596
|
+
if (rotated && baseToken) {
|
|
5597
|
+
const saved = await this.read(region);
|
|
5598
|
+
if (saved.method === "browser" && saved.grant?.id === id && saved.grant.credentials.authToken === rotated.credentials.authToken) return saved.grant;
|
|
5599
|
+
if (saved.method === "browser" && saved.grant?.id === id && saved.grant.credentials.authToken === baseToken) {
|
|
5600
|
+
this.volatile.set(region, {
|
|
5601
|
+
baseToken,
|
|
5602
|
+
grant: rotated
|
|
5603
|
+
});
|
|
5604
|
+
return rotated;
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
throw error;
|
|
5608
|
+
}
|
|
5609
|
+
}
|
|
5610
|
+
async assertWritable(region) {
|
|
5611
|
+
if (!(await this.credentials.describeRecord?.(browserAuthorizationKey(region)))?.writable) throw new QoderLlmError("Qoder authorization storage is read-only or unsupported.", "INVALID_REQUEST");
|
|
5612
|
+
}
|
|
5613
|
+
async dispose() {
|
|
5614
|
+
this.disposed = true;
|
|
5615
|
+
this.lifecycle.abort();
|
|
5616
|
+
for (const attempt of this.attempts.values()) attempt.controller.abort();
|
|
5617
|
+
await Promise.all([...this.attempts.values()].map((attempt) => attempt.done));
|
|
5618
|
+
await Promise.allSettled(this.renewals);
|
|
5619
|
+
this.volatile.clear();
|
|
5620
|
+
this.invalid.clear();
|
|
5621
|
+
}
|
|
5622
|
+
};
|
|
5623
|
+
//#endregion
|
|
3812
5624
|
//#region src/dsh/plugin.ts
|
|
3813
5625
|
const name = "provider-qoder";
|
|
3814
5626
|
const inject = [
|
|
@@ -3860,75 +5672,101 @@ async function executeRpc(operation, task, signal, logger) {
|
|
|
3860
5672
|
}
|
|
3861
5673
|
}
|
|
3862
5674
|
function apply(ctx, input = {}) {
|
|
5675
|
+
const leases = new QoderQueueLeases();
|
|
5676
|
+
const queueStatus = new QoderQueueStatusStore();
|
|
5677
|
+
ctx.effect(() => async () => {
|
|
5678
|
+
queueStatus.dispose();
|
|
5679
|
+
await leases.dispose();
|
|
5680
|
+
}, "provider-qoder: queue lifecycle");
|
|
5681
|
+
bindQoderQueueLeases(ctx, leases);
|
|
3863
5682
|
const config = readConfig(input);
|
|
3864
5683
|
const logger = ctx.logger;
|
|
5684
|
+
const initialRegion = config.region ?? "global";
|
|
3865
5685
|
const baseConfig = {
|
|
3866
|
-
|
|
5686
|
+
...transportConfig(config),
|
|
3867
5687
|
modelsByRegion: { ...config.modelsByRegion },
|
|
3868
|
-
streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
|
|
3869
|
-
responseHeaderTimeoutMs: config.responseHeaderTimeoutMs ?? 6e4,
|
|
3870
|
-
preserveThinking: config.preserveThinking ?? true,
|
|
3871
5688
|
webSearchMode: config.webSearchMode ?? "auto"
|
|
3872
5689
|
};
|
|
3873
5690
|
let current = () => baseConfig;
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
5691
|
+
let authorization;
|
|
5692
|
+
const createTransport = (value, resolvePat) => createQoderTransport({
|
|
5693
|
+
...value,
|
|
5694
|
+
resolvePat: resolvePat ?? (() => resolveManagedQoderPat(ctx.credentials)),
|
|
5695
|
+
resolveCredential: resolvePat ? void 0 : (signal) => authorization.lease(value.region, signal),
|
|
3877
5696
|
logger,
|
|
3878
|
-
streamIdleTimeoutMs,
|
|
3879
|
-
responseHeaderTimeoutMs,
|
|
3880
5697
|
attachments: ctx.attachments,
|
|
3881
|
-
|
|
5698
|
+
onQueueUpdate: queueStatus.update,
|
|
5699
|
+
captureQueueLeaseOwner: leases.capture
|
|
3882
5700
|
});
|
|
3883
5701
|
const resolveConfig = () => {
|
|
3884
5702
|
const value = current();
|
|
3885
|
-
const
|
|
5703
|
+
const transport = transportConfig(value);
|
|
3886
5704
|
return {
|
|
3887
|
-
|
|
3888
|
-
models: modelsFor(value, region),
|
|
3889
|
-
streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
|
|
3890
|
-
responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
|
|
3891
|
-
preserveThinking: value.preserveThinking ?? true,
|
|
5705
|
+
...transport,
|
|
5706
|
+
models: modelsFor(value, transport.region),
|
|
3892
5707
|
webSearchMode: value.webSearchMode ?? "auto"
|
|
3893
5708
|
};
|
|
3894
5709
|
};
|
|
3895
|
-
const
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
5710
|
+
const runtime = new QoderTransportRuntime(current(), createTransport);
|
|
5711
|
+
const authorizationTransports = /* @__PURE__ */ new Map();
|
|
5712
|
+
authorization = new QoderBrowserAuthorizationManager({
|
|
5713
|
+
credentials: ctx.credentials,
|
|
5714
|
+
region: initialRegion,
|
|
5715
|
+
transport: (region) => {
|
|
5716
|
+
let transport = authorizationTransports.get(region);
|
|
5717
|
+
if (!transport) {
|
|
5718
|
+
transport = createQoderTransport({
|
|
5719
|
+
region,
|
|
5720
|
+
resolvePat: () => Promise.resolve("")
|
|
5721
|
+
}).authorization;
|
|
5722
|
+
authorizationTransports.set(region, transport);
|
|
5723
|
+
}
|
|
5724
|
+
return transport;
|
|
5725
|
+
},
|
|
5726
|
+
changed: () => {
|
|
5727
|
+
catalog.refresh();
|
|
5728
|
+
}
|
|
5729
|
+
});
|
|
5730
|
+
ctx.effect(() => () => authorization.dispose(), "provider-qoder: browser authorization lifecycle");
|
|
3903
5731
|
const catalog = new QoderCatalogLifecycle({
|
|
3904
|
-
resolveTransport: () =>
|
|
3905
|
-
region: () =>
|
|
5732
|
+
resolveTransport: () => runtime.transport,
|
|
5733
|
+
region: () => runtime.config.region,
|
|
3906
5734
|
configuredModels: (region) => modelsFor(current(), region),
|
|
3907
5735
|
onDiscovered: () => registration.replace([providerQoder]),
|
|
3908
5736
|
onPersistenceError: (error) => logger?.error?.("[Qoder Settings] Failed to synchronize model catalog", logError(error))
|
|
3909
5737
|
});
|
|
3910
5738
|
ctx.effect(() => () => catalog.dispose(), "provider-qoder: catalog lifecycle");
|
|
3911
5739
|
const adapter = new QoderAdapter({
|
|
3912
|
-
resolveTransport: () =>
|
|
5740
|
+
resolveTransport: () => runtime.transport,
|
|
3913
5741
|
catalog,
|
|
3914
|
-
region: () =>
|
|
5742
|
+
region: () => runtime.config.region,
|
|
3915
5743
|
providerId: providerQoder,
|
|
3916
5744
|
providerName: "Qoder",
|
|
3917
5745
|
sessions: ctx.get("sessions"),
|
|
3918
5746
|
agents: ctx.get("agents")
|
|
3919
5747
|
});
|
|
5748
|
+
ctx.on("session/disposed", (session) => adapter.forgetSession(session.id));
|
|
3920
5749
|
const registration = ctx.llm.registerAdapter([providerQoder], adapter);
|
|
5750
|
+
const refreshAuthentication = () => {
|
|
5751
|
+
const value = resolveConfig();
|
|
5752
|
+
runtime.update(value, true);
|
|
5753
|
+
authorization.activateRegion(runtime.config.region);
|
|
5754
|
+
catalog.clearDiscovered();
|
|
5755
|
+
catalog.observeTransport();
|
|
5756
|
+
registration.replace([providerQoder]);
|
|
5757
|
+
};
|
|
5758
|
+
ctx.on("credentials/record-updated", (key) => {
|
|
5759
|
+
if (key === browserAuthorizationKey(runtime.config.region)) {
|
|
5760
|
+
refreshAuthentication();
|
|
5761
|
+
catalog.refresh();
|
|
5762
|
+
}
|
|
5763
|
+
});
|
|
5764
|
+
ctx.on("credentials/reference-updated", (ref) => {
|
|
5765
|
+
if (ref === "QODER_MANAGED_CREDENTIAL") refreshAuthentication();
|
|
5766
|
+
});
|
|
3921
5767
|
const refreshAdapter = () => {
|
|
3922
5768
|
const next = resolveConfig();
|
|
3923
|
-
if (
|
|
3924
|
-
activeTransport = createTransport(next.region, next.streamIdleTimeoutMs, next.responseHeaderTimeoutMs, next.preserveThinking);
|
|
3925
|
-
activeTransportConfig = {
|
|
3926
|
-
region: next.region,
|
|
3927
|
-
streamIdleTimeoutMs: next.streamIdleTimeoutMs,
|
|
3928
|
-
responseHeaderTimeoutMs: next.responseHeaderTimeoutMs,
|
|
3929
|
-
preserveThinking: next.preserveThinking
|
|
3930
|
-
};
|
|
3931
|
-
}
|
|
5769
|
+
if (runtime.update(next)) authorization.activateRegion(runtime.config.region);
|
|
3932
5770
|
catalog.observeTransport();
|
|
3933
5771
|
adapter.replaceModels(next.models);
|
|
3934
5772
|
registration.replace([providerQoder]);
|
|
@@ -3967,7 +5805,12 @@ function apply(ctx, input = {}) {
|
|
|
3967
5805
|
const discoverModels = async (signal, suppliedPat) => {
|
|
3968
5806
|
const snapshot = resolveConfig();
|
|
3969
5807
|
const normalizedPat = suppliedPat?.trim();
|
|
3970
|
-
const transport =
|
|
5808
|
+
const transport = runtime.transport;
|
|
5809
|
+
if (normalizedPat) {
|
|
5810
|
+
const view = await authorization.describe(snapshot.region);
|
|
5811
|
+
const managedPat = await resolveManagedQoderPat(ctx.credentials);
|
|
5812
|
+
if (view.method !== "pat" || normalizedPat !== managedPat.trim() || transport !== runtime.transport) return createTransport(transportConfig(snapshot), () => Promise.resolve(normalizedPat)).discoverModels(signal);
|
|
5813
|
+
}
|
|
3971
5814
|
return catalog.discover(signal, transport);
|
|
3972
5815
|
};
|
|
3973
5816
|
ctx.llm.registerModelDiscovery(settingsNamespace, async (request, signal) => {
|
|
@@ -3982,12 +5825,48 @@ function apply(ctx, input = {}) {
|
|
|
3982
5825
|
const handler = async (endpoint, payload, signal) => {
|
|
3983
5826
|
if (!isQoderRpcEndpoint(endpoint)) return publicError("UNKNOWN_ENDPOINT", `Unknown endpoint: ${endpoint}`);
|
|
3984
5827
|
if (signal.aborted) return publicError("ABORTED", "Request aborted");
|
|
5828
|
+
if (endpoint === "authorization" || endpoint === "beginLogin" || endpoint === "cancelLogin" || endpoint === "selectAuthorization") {
|
|
5829
|
+
const data = payload;
|
|
5830
|
+
const region = data?.region;
|
|
5831
|
+
if (region !== "global" && region !== "china" || region !== resolveConfig().region) return publicError("INTERNAL", "The active Qoder service region is required");
|
|
5832
|
+
return executeRpc("manage Qoder authorization", async () => {
|
|
5833
|
+
if (endpoint === "beginLogin") return authorization.begin(region);
|
|
5834
|
+
if (endpoint === "cancelLogin") {
|
|
5835
|
+
await authorization.cancel(region);
|
|
5836
|
+
return authorization.describe(region);
|
|
5837
|
+
}
|
|
5838
|
+
if (endpoint === "selectAuthorization") {
|
|
5839
|
+
const method = data?.method;
|
|
5840
|
+
if (method !== "pat" && method !== "browser" && method !== "none") throw new QoderLlmError("Invalid authorization method.", "INVALID_REQUEST");
|
|
5841
|
+
return authorization.select(region, method);
|
|
5842
|
+
}
|
|
5843
|
+
return authorization.describe(region);
|
|
5844
|
+
}, signal, logger);
|
|
5845
|
+
}
|
|
5846
|
+
if (endpoint === "queue") {
|
|
5847
|
+
const sessionId = payload?.sessionId;
|
|
5848
|
+
if (typeof sessionId !== "string" || !sessionId.trim() || sessionId.length > 256) return publicError("INTERNAL", "A valid sessionId is required");
|
|
5849
|
+
return {
|
|
5850
|
+
ok: true,
|
|
5851
|
+
value: queueStatus.read(sessionId)
|
|
5852
|
+
};
|
|
5853
|
+
}
|
|
3985
5854
|
if (endpoint === "models") return await executeRpc("discover Qoder models", () => discoverModels(signal), signal, logger);
|
|
3986
5855
|
if (endpoint === "sessionTier" || endpoint === "readSessionTier") {
|
|
3987
5856
|
const data = payload;
|
|
3988
5857
|
if (typeof data?.sessionId === "string" && typeof data?.modelId === "string" && (endpoint === "readSessionTier" || typeof data?.tierKey === "string") && (data.region === "global" || data.region === "china")) {
|
|
3989
5858
|
const { sessionId, modelId, tierKey, region } = data;
|
|
3990
5859
|
return await executeRpc(endpoint === "sessionTier" ? "select session context tier" : "read session context tier", async () => {
|
|
5860
|
+
if (!sessionId.trim() || region !== resolveConfig().region) throw new QoderLlmError("Invalid session context tier scope.", "INVALID_REQUEST");
|
|
5861
|
+
const sessions = ctx.get("sessions");
|
|
5862
|
+
if (sessions && !sessions.get(sessionId)) {
|
|
5863
|
+
const controller = ctx.get("sessionController");
|
|
5864
|
+
if (controller) {
|
|
5865
|
+
const result = await controller.resolveAgent(sessionId);
|
|
5866
|
+
if ("error" in result) throw result.error;
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
signal.throwIfAborted();
|
|
3991
5870
|
if (endpoint === "sessionTier") adapter.setSessionTier(sessionId, modelId, tierKey, region);
|
|
3992
5871
|
return adapter.readSessionTier({
|
|
3993
5872
|
sessionId,
|
|
@@ -4000,7 +5879,7 @@ function apply(ctx, input = {}) {
|
|
|
4000
5879
|
}
|
|
4001
5880
|
const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
|
|
4002
5881
|
logger?.debug?.("[Qoder RPC] Reading subscriber account", { force });
|
|
4003
|
-
const outcome = await executeRpc("load Qoder account", () =>
|
|
5882
|
+
const outcome = await executeRpc("load Qoder account", () => runtime.transport.readAccount({
|
|
4004
5883
|
force,
|
|
4005
5884
|
signal
|
|
4006
5885
|
}), signal, logger);
|
|
@@ -4015,7 +5894,7 @@ function apply(ctx, input = {}) {
|
|
|
4015
5894
|
ctx.inject(["web"], (webCtx) => {
|
|
4016
5895
|
const searchProvider = new QoderSearchProvider({
|
|
4017
5896
|
ctx,
|
|
4018
|
-
resolveTransport: () =>
|
|
5897
|
+
resolveTransport: () => runtime.transport,
|
|
4019
5898
|
getWebSearchMode: () => resolveConfig().webSearchMode ?? "auto"
|
|
4020
5899
|
});
|
|
4021
5900
|
webCtx.web.registerSearchProvider(searchProvider);
|