dsh-provider-qoder 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -22
- package/README_CN.md +13 -25
- package/lib/client.js +549 -277
- package/lib/index.d.ts +8 -44
- package/lib/index.js +785 -522
- package/package.json +19 -16
package/lib/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, ToolCallId, attributionHeaders, contentHasImage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, ToolCallId, attributionHeaders, contentHasImage, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import crypto, { createHash } from "node:crypto";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { homedir, hostname } from "node:os";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
8
8
|
import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
|
|
9
9
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
@@ -32,6 +32,9 @@ const reasoningEffortOrder = /* @__PURE__ */ new Map([
|
|
|
32
32
|
function positiveNumber(value) {
|
|
33
33
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
34
34
|
}
|
|
35
|
+
function nonNegativeNumber(value) {
|
|
36
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
37
|
+
}
|
|
35
38
|
function contextOptionsOf(value) {
|
|
36
39
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
37
40
|
const options = {};
|
|
@@ -42,8 +45,8 @@ function contextOptionsOf(value) {
|
|
|
42
45
|
const isDefault = typeof entry.is_default === "boolean" ? entry.is_default : void 0;
|
|
43
46
|
if (tokenCount === void 0) continue;
|
|
44
47
|
options[key] = {
|
|
45
|
-
|
|
46
|
-
...isDefault
|
|
48
|
+
tokenCount,
|
|
49
|
+
...isDefault !== void 0 ? { isDefault } : {}
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
52
|
if (Object.keys(options).length === 0) return void 0;
|
|
@@ -96,9 +99,9 @@ function normalizeQoderModels(payload, onConflict) {
|
|
|
96
99
|
const contextWindow = (defaultOptions.length === 1 ? defaultOptions[0].tokenCount : void 0) ?? positiveNumber(raw.max_input_tokens) ?? 18e4;
|
|
97
100
|
const maxContextWindow = Math.max(positiveNumber(raw.max_input_tokens) ?? 0, contextWindow, ...Object.values(contextOptions ?? {}).map((option) => option.tokenCount ?? 0));
|
|
98
101
|
const isReasoning = thinkingDefault(raw.thinking_config, raw.is_reasoning === true, onConflict);
|
|
99
|
-
const priceFactor =
|
|
100
|
-
const rawOriginalPrice =
|
|
101
|
-
const promotionPrice = typeof raw.promotion === "object" && raw.promotion !== null && raw.promotion.active === true
|
|
102
|
+
const priceFactor = nonNegativeNumber(raw.price_factor) ?? nonNegativeNumber(raw.priceFactor);
|
|
103
|
+
const rawOriginalPrice = nonNegativeNumber(raw.original_price_factor) ?? nonNegativeNumber(raw.originPriceFactor);
|
|
104
|
+
const promotionPrice = typeof raw.promotion === "object" && raw.promotion !== null && raw.promotion.active === true ? nonNegativeNumber(raw.promotion.before_promotion_price_factor) : void 0;
|
|
102
105
|
const originalPriceFactor = rawOriginalPrice ?? promotionPrice;
|
|
103
106
|
const isFree = raw.is_free === true || raw.isFree === true || Array.isArray(raw.tags) && raw.tags.includes("limited_time_free") || priceFactor === 0;
|
|
104
107
|
const reasoning = reasoningEffortsOf(raw.thinking_config);
|
|
@@ -187,28 +190,40 @@ function selectedContextTier(model) {
|
|
|
187
190
|
function effectiveContextWindow(model) {
|
|
188
191
|
return selectedContextTier(model)?.tokenCount ?? model.contextWindow;
|
|
189
192
|
}
|
|
193
|
+
/** Clone a model catalog entry, detaching any contextOptions object. */
|
|
194
|
+
function cloneCatalogModel(model) {
|
|
195
|
+
return {
|
|
196
|
+
...model,
|
|
197
|
+
...model.contextOptions !== void 0 ? { contextOptions: { ...model.contextOptions } } : {}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** Carry subscriber context choices only while the refreshed catalog supports them. */
|
|
201
|
+
function mergeAdvertisedContext(remembered, advertised, updated) {
|
|
202
|
+
const merged = { ...updated };
|
|
203
|
+
delete merged.contextOptions;
|
|
204
|
+
delete merged.contextTier;
|
|
205
|
+
if (advertised.contextOptions !== void 0) merged.contextOptions = { ...advertised.contextOptions };
|
|
206
|
+
const tier = selectedContextTier({
|
|
207
|
+
contextTier: remembered.contextTier,
|
|
208
|
+
contextOptions: merged.contextOptions
|
|
209
|
+
});
|
|
210
|
+
if (tier !== void 0) {
|
|
211
|
+
merged.contextTier = tier.key;
|
|
212
|
+
merged.contextWindow = tier.tokenCount;
|
|
213
|
+
} else if (advertised.contextWindow !== void 0) merged.contextWindow = remembered.contextTier !== void 0 ? advertised.contextWindow : Math.min(remembered.contextWindow ?? advertised.contextWindow, advertised.contextWindow);
|
|
214
|
+
return merged;
|
|
215
|
+
}
|
|
190
216
|
function mergeQoderDiscoveryMetadata(configured, discovered) {
|
|
191
217
|
const catalog = new Map(discovered.map((model) => [model.id, model]));
|
|
192
218
|
return configured.map((model) => {
|
|
193
219
|
const advertised = catalog.get(model.id);
|
|
194
|
-
if (advertised === void 0) return
|
|
195
|
-
...model,
|
|
196
|
-
...model.contextOptions !== void 0 ? { contextOptions: { ...model.contextOptions } } : {}
|
|
197
|
-
};
|
|
220
|
+
if (advertised === void 0) return cloneCatalogModel(model);
|
|
198
221
|
const merged = { ...model };
|
|
199
|
-
for (const key of discoveredMetadataKeys)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const tier = selectedContextTier(merged);
|
|
205
|
-
if (tier !== void 0) {
|
|
206
|
-
merged.contextWindow = tier.tokenCount;
|
|
207
|
-
return merged;
|
|
208
|
-
}
|
|
209
|
-
delete merged.contextTier;
|
|
210
|
-
if (advertised.contextWindow !== void 0) merged.contextWindow = Math.min(model.contextWindow ?? advertised.contextWindow, advertised.contextWindow);
|
|
211
|
-
return merged;
|
|
222
|
+
for (const key of discoveredMetadataKeys) if (key !== "contextOptions") {
|
|
223
|
+
delete merged[key];
|
|
224
|
+
if (advertised[key] !== void 0) Object.assign(merged, { [key]: advertised[key] });
|
|
225
|
+
}
|
|
226
|
+
return mergeAdvertisedContext(model, advertised, merged);
|
|
212
227
|
});
|
|
213
228
|
}
|
|
214
229
|
function hasSameQoderDiscoveryMetadata(left, right) {
|
|
@@ -333,10 +348,177 @@ function getQoderWebSearchUrl(region = "global") {
|
|
|
333
348
|
* @module dsh-provider-qoder/qoder/errors
|
|
334
349
|
*/
|
|
335
350
|
var QoderLlmError = class extends LlmError {
|
|
351
|
+
upstreamCode;
|
|
352
|
+
source;
|
|
353
|
+
httpStatus;
|
|
336
354
|
constructor(message, code = "UNKNOWN_ERROR", options) {
|
|
337
355
|
super(message, code, options);
|
|
338
356
|
}
|
|
339
357
|
};
|
|
358
|
+
const modelBusinessErrors = {
|
|
359
|
+
"103": {
|
|
360
|
+
code: "PROVIDER_ERROR",
|
|
361
|
+
message: "Qoder rejected a duplicate model request."
|
|
362
|
+
},
|
|
363
|
+
"105": {
|
|
364
|
+
code: "AUTH",
|
|
365
|
+
message: "Qoder subscriber authentication has expired."
|
|
366
|
+
},
|
|
367
|
+
"110": {
|
|
368
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
369
|
+
message: "Qoder daily usage limit has been reached."
|
|
370
|
+
},
|
|
371
|
+
"112": {
|
|
372
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
373
|
+
message: "Qoder personal Credits are exhausted."
|
|
374
|
+
},
|
|
375
|
+
"114": {
|
|
376
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
377
|
+
message: "Qoder free trial account limit has been reached."
|
|
378
|
+
},
|
|
379
|
+
"115": {
|
|
380
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
381
|
+
message: "Qoder free subscriber quota limit has been reached."
|
|
382
|
+
},
|
|
383
|
+
"116": {
|
|
384
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
385
|
+
message: "Qoder Teams administrator Credits are exhausted."
|
|
386
|
+
},
|
|
387
|
+
"117": {
|
|
388
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
389
|
+
message: "Qoder Teams member Credits are exhausted."
|
|
390
|
+
},
|
|
391
|
+
"119": {
|
|
392
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
393
|
+
message: "Qoder Vela model free usage limit has been reached."
|
|
394
|
+
},
|
|
395
|
+
"122": {
|
|
396
|
+
code: QUOTA_EXCEEDED_CODE,
|
|
397
|
+
message: "Qoder billing group Credits limit has been reached."
|
|
398
|
+
},
|
|
399
|
+
"406": {
|
|
400
|
+
code: "PROVIDER_ERROR",
|
|
401
|
+
message: "Qoder rejected the request through its content safety policy."
|
|
402
|
+
},
|
|
403
|
+
"416": {
|
|
404
|
+
code: "INVALID_REQUEST",
|
|
405
|
+
message: "Qoder cannot satisfy the requested range."
|
|
406
|
+
},
|
|
407
|
+
"430": {
|
|
408
|
+
code: "INVALID_REQUEST",
|
|
409
|
+
message: "Qoder does not support this request; check client compatibility."
|
|
410
|
+
},
|
|
411
|
+
"10605": {
|
|
412
|
+
code: "RATE_LIMIT",
|
|
413
|
+
message: "Qoder model is queued."
|
|
414
|
+
},
|
|
415
|
+
"100400": {
|
|
416
|
+
code: "PROVIDER_ERROR",
|
|
417
|
+
message: "Qoder custom model service failed."
|
|
418
|
+
},
|
|
419
|
+
"100401": {
|
|
420
|
+
code: "AUTH",
|
|
421
|
+
message: "Qoder custom model authentication failed."
|
|
422
|
+
},
|
|
423
|
+
"100403": {
|
|
424
|
+
code: "PROVIDER_ERROR",
|
|
425
|
+
message: "Qoder custom model is unavailable."
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
/** Decode only model rejection diagnostics, with bounded depth, work and JSON size. */
|
|
429
|
+
function modelErrorDetails(raw, status) {
|
|
430
|
+
let upstreamCode;
|
|
431
|
+
let codeDepth = -1;
|
|
432
|
+
let detail;
|
|
433
|
+
let textCode;
|
|
434
|
+
let delayMs;
|
|
435
|
+
let remaining = 100;
|
|
436
|
+
const seen = /* @__PURE__ */ new Set();
|
|
437
|
+
function visit(value, depth) {
|
|
438
|
+
if (depth > 10 || --remaining < 0) return;
|
|
439
|
+
if (typeof value === "string") {
|
|
440
|
+
const text = value.trim();
|
|
441
|
+
if (!text) return;
|
|
442
|
+
if (text.length <= 16 * 1024) try {
|
|
443
|
+
visit(JSON.parse(text), depth + 1);
|
|
444
|
+
return;
|
|
445
|
+
} catch {}
|
|
446
|
+
const diagnostic = text.slice(0, 16 * 1024);
|
|
447
|
+
if (isContextWindowExceededError(diagnostic)) textCode = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
448
|
+
else if (textCode === void 0 && isQuotaExceededError(diagnostic)) textCode = QUOTA_EXCEEDED_CODE;
|
|
449
|
+
detail = text.slice(0, 300);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || seen.has(value)) return;
|
|
453
|
+
seen.add(value);
|
|
454
|
+
const obj = value;
|
|
455
|
+
for (const key of [
|
|
456
|
+
"code",
|
|
457
|
+
"error_code",
|
|
458
|
+
"errorCode",
|
|
459
|
+
"errCode"
|
|
460
|
+
]) {
|
|
461
|
+
const candidate = obj[key];
|
|
462
|
+
const code = (typeof candidate === "number" && Number.isFinite(candidate) ? String(candidate) : typeof candidate === "string" ? candidate.trim() : void 0)?.slice(0, 100);
|
|
463
|
+
if (!code || code === "0" || code === String(status) && !Object.hasOwn(modelBusinessErrors, code)) continue;
|
|
464
|
+
if (depth >= codeDepth) {
|
|
465
|
+
upstreamCode = code;
|
|
466
|
+
codeDepth = depth;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const seconds = obj.retryAfterSeconds;
|
|
470
|
+
if (typeof seconds === "number" && Number.isFinite(seconds * 1e3) && seconds > 0) delayMs = Math.max(delayMs ?? 0, seconds * 1e3);
|
|
471
|
+
for (const key of [
|
|
472
|
+
"message",
|
|
473
|
+
"body",
|
|
474
|
+
"details",
|
|
475
|
+
"error",
|
|
476
|
+
"cause",
|
|
477
|
+
"data",
|
|
478
|
+
"qoderApiError",
|
|
479
|
+
"providerError"
|
|
480
|
+
]) visit(obj[key], depth + 1);
|
|
481
|
+
}
|
|
482
|
+
visit(raw, 0);
|
|
483
|
+
return {
|
|
484
|
+
upstreamCode,
|
|
485
|
+
detail,
|
|
486
|
+
textCode,
|
|
487
|
+
delayMs
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
/** Model business semantics precede HTTP status; other transport capabilities keep their own protocols. */
|
|
491
|
+
function qoderModelError(message, response) {
|
|
492
|
+
const details = modelErrorDetails(response.cause, response.status);
|
|
493
|
+
const known = details.upstreamCode === void 0 ? void 0 : Object.hasOwn(modelBusinessErrors, details.upstreamCode) ? modelBusinessErrors[details.upstreamCode] : void 0;
|
|
494
|
+
const fallback = qoderHttpError(message, response);
|
|
495
|
+
let code = fallback.code;
|
|
496
|
+
let summary = details.detail ? `${message}: ${details.detail}` : message;
|
|
497
|
+
if (details.upstreamCode !== void 0) {
|
|
498
|
+
code = known?.code ?? "PROVIDER_ERROR";
|
|
499
|
+
summary = `${known?.message ?? summary} (Qoder code ${details.upstreamCode}).`;
|
|
500
|
+
} else if (details.textCode === CONTEXT_WINDOW_EXCEEDED_CODE) {
|
|
501
|
+
code = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
502
|
+
summary = "Qoder model context window has been exceeded.";
|
|
503
|
+
} else if (details.textCode === QUOTA_EXCEEDED_CODE) {
|
|
504
|
+
code = QUOTA_EXCEEDED_CODE;
|
|
505
|
+
summary = "Qoder subscriber quota is exhausted.";
|
|
506
|
+
}
|
|
507
|
+
const providerRetryAfterMs = Math.max(fallback.failure.providerRetryAfterMs ?? 0, details.delayMs ?? 0) || void 0;
|
|
508
|
+
const error = new QoderLlmError(summary, code, {
|
|
509
|
+
status: response.status,
|
|
510
|
+
...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
|
|
511
|
+
...fallback.failure.requestId === void 0 ? {} : { requestId: fallback.failure.requestId },
|
|
512
|
+
...response.cause === void 0 ? {} : { cause: response.cause }
|
|
513
|
+
});
|
|
514
|
+
error.upstreamCode = details.upstreamCode;
|
|
515
|
+
error.source = response.source;
|
|
516
|
+
error.httpStatus = response.httpStatus ?? (response.source === "http" ? response.status : void 0);
|
|
517
|
+
return error;
|
|
518
|
+
}
|
|
519
|
+
function canRefreshModelCredentials(error) {
|
|
520
|
+
return error.source === "http" && (error.failure.status === 401 || error.failure.status === 403) && error.code === "AUTH" && (error.upstreamCode === void 0 || error.upstreamCode === "105");
|
|
521
|
+
}
|
|
340
522
|
function qoderHttpError(message, response) {
|
|
341
523
|
const { status } = response;
|
|
342
524
|
const code = status === 401 || status === 403 ? "AUTH" : status === 408 ? "TIMEOUT" : status === 429 ? "RATE_LIMIT" : status >= 500 && status <= 599 ? "SERVER" : status >= 400 && status <= 499 ? "INVALID_REQUEST" : "PROVIDER_ERROR";
|
|
@@ -367,11 +549,16 @@ function retryAfterMs(value, nowMs = Date.now()) {
|
|
|
367
549
|
}
|
|
368
550
|
//#endregion
|
|
369
551
|
//#region src/qoder/transport/machine-id.ts
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
552
|
+
/** Match the native CLI's region-specific config directory overrides. */
|
|
553
|
+
function qoderMachineIdPaths(region = "global", env = process.env, home = homedir()) {
|
|
554
|
+
const prefix = region === "china" ? "QODERCN_" : "QODER_";
|
|
555
|
+
const cliHome = env[`${prefix}CLI_HOME`] ?? env.GEMINI_CLI_HOME ?? home;
|
|
556
|
+
const configDir = env[`${prefix}CONFIG_DIR`];
|
|
557
|
+
return [join(configDir ? resolve(configDir) : join(cliHome, region === "china" ? ".qoder-cn" : ".qoder"), ".auth", "machine_id"), join(home, ".dsh", "qoder", "machine_id")];
|
|
558
|
+
}
|
|
559
|
+
const ephemeralMachineIds = /* @__PURE__ */ new Map();
|
|
373
560
|
/** Read Qoder's machine id or create the DSH-owned fallback. */
|
|
374
|
-
function getMachineId(paths =
|
|
561
|
+
function getMachineId(paths = qoderMachineIdPaths()) {
|
|
375
562
|
for (const path of paths) {
|
|
376
563
|
if (!existsSync(path)) continue;
|
|
377
564
|
try {
|
|
@@ -379,8 +566,8 @@ function getMachineId(paths = defaultMachineIdPaths()) {
|
|
|
379
566
|
if (value) return value;
|
|
380
567
|
} catch {}
|
|
381
568
|
}
|
|
382
|
-
const machineId = crypto.randomUUID();
|
|
383
569
|
const savePath = paths.at(-1);
|
|
570
|
+
const machineId = (savePath === void 0 ? void 0 : ephemeralMachineIds.get(savePath)) ?? crypto.randomUUID();
|
|
384
571
|
if (savePath !== void 0) try {
|
|
385
572
|
mkdirSync(dirname(savePath), { recursive: true });
|
|
386
573
|
writeFileSync(savePath, machineId, {
|
|
@@ -393,6 +580,7 @@ function getMachineId(paths = defaultMachineIdPaths()) {
|
|
|
393
580
|
if (existing) return existing;
|
|
394
581
|
} catch {}
|
|
395
582
|
}
|
|
583
|
+
if (savePath !== void 0) ephemeralMachineIds.set(savePath, machineId);
|
|
396
584
|
return machineId;
|
|
397
585
|
}
|
|
398
586
|
//#endregion
|
|
@@ -419,7 +607,12 @@ function redactString(value) {
|
|
|
419
607
|
}
|
|
420
608
|
/** Redact credentials and bound arbitrary provider values before logging them. */
|
|
421
609
|
function redactLogValue(value, depth = 0) {
|
|
422
|
-
if (typeof value === "string")
|
|
610
|
+
if (typeof value === "string") {
|
|
611
|
+
if (depth < maxDepth && value.length <= 16 * 1024 && /^[\s]*[{"]/u.test(value)) try {
|
|
612
|
+
return redactString(JSON.stringify(redactLogValue(JSON.parse(value), depth + 1)));
|
|
613
|
+
} catch {}
|
|
614
|
+
return redactString(value);
|
|
615
|
+
}
|
|
423
616
|
if (value === null || typeof value !== "object") return value;
|
|
424
617
|
if (depth >= maxDepth) return "[TRUNCATED]";
|
|
425
618
|
if (value instanceof Error) return {
|
|
@@ -427,6 +620,10 @@ function redactLogValue(value, depth = 0) {
|
|
|
427
620
|
message: redactString(value.message),
|
|
428
621
|
..."code" in value ? { code: redactLogValue(value.code, depth + 1) } : {},
|
|
429
622
|
..."cause" in value ? { cause: redactLogValue(value.cause, depth + 1) } : {},
|
|
623
|
+
..."upstreamCode" in value ? { upstreamCode: redactLogValue(value.upstreamCode, depth + 1) } : {},
|
|
624
|
+
..."source" in value ? { source: value.source } : {},
|
|
625
|
+
..."httpStatus" in value ? { httpStatus: value.httpStatus } : {},
|
|
626
|
+
..."failure" in value ? { failure: redactLogValue(value.failure, depth + 1) } : {},
|
|
430
627
|
...value instanceof AggregateError ? { errors: redactLogValue(value.errors, depth + 1) } : {}
|
|
431
628
|
};
|
|
432
629
|
if (Array.isArray(value)) return value.slice(0, maxEntries).map((item) => redactLogValue(item, depth + 1));
|
|
@@ -468,12 +665,14 @@ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc
|
|
|
468
665
|
6HRkPJ7S236FZz73In/KVuLnwI8JJ2CbuJap8kvheCCZpmAWpb/cPx/3Vr/J6I17
|
|
469
666
|
XcW+ML9FoCI6AOvOzwIDAQAB
|
|
470
667
|
-----END PUBLIC KEY-----`;
|
|
471
|
-
const qoderIdeVersion = "1.1.
|
|
668
|
+
const qoderIdeVersion = "1.1.65";
|
|
472
669
|
const defaultUserAgent = `qoder/${qoderIdeVersion}`;
|
|
473
670
|
const qoderDataPolicy = "disagree";
|
|
474
671
|
const qoderLoginVersion = "v2";
|
|
475
|
-
const qoderMachineOs = process.platform === "win32" ? process.arch === "arm64" ? "aarch64_windows" : "x86_64_windows" : process.arch === "arm64" ? "aarch64_linux" : "x86_64_linux";
|
|
476
672
|
const qoderMachineTypeMagic = "5";
|
|
673
|
+
function qoderMachineOs(platform = process.platform, arch = process.arch) {
|
|
674
|
+
return `${arch === "arm64" ? "aarch64" : arch === "x64" ? "x86_64" : arch}_${platform}`;
|
|
675
|
+
}
|
|
477
676
|
function computeSigPath(urlStr) {
|
|
478
677
|
let sigPath = new URL(urlStr).pathname;
|
|
479
678
|
if (sigPath.startsWith("/algo")) sigPath = sigPath.substring(5);
|
|
@@ -499,9 +698,9 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
499
698
|
const userInfo = {
|
|
500
699
|
uid: creds.userID,
|
|
501
700
|
security_oauth_token: creds.authToken,
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
701
|
+
...creds.organizationId === void 0 ? {} : { organization_id: creds.organizationId },
|
|
702
|
+
...creds.organizationTags === void 0 ? {} : { organization_tags: creds.organizationTags },
|
|
703
|
+
...creds.dataPolicyAgreed === void 0 ? {} : { data_policy_agreed: creds.dataPolicyAgreed }
|
|
505
704
|
};
|
|
506
705
|
const infoB64 = aesEncryptCBCBase64(JSON.stringify(userInfo), aesKey);
|
|
507
706
|
const cosyKey = rsaEncryptBase64(aesKey);
|
|
@@ -517,9 +716,9 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
517
716
|
const sigPath = computeSigPath(requestURL);
|
|
518
717
|
const sigInput = `${payloadB64}\n${cosyKey}\n${timestamp}\n${body ? Buffer.isBuffer(body) ? body.toString("utf8") : body : ""}\n${sigPath}`;
|
|
519
718
|
const sig = crypto.createHash("md5").update(sigInput).digest("hex");
|
|
520
|
-
const bodyHash = crypto.createHash("md5").update(body || "").digest("hex");
|
|
521
|
-
const bodyLen = body ? (Buffer.isBuffer(body) ? body.length : Buffer.from(body).length).toString() : "0";
|
|
522
719
|
const machineID = creds.machineID || getMachineId();
|
|
720
|
+
const isModelRequest = sigPath === "/api/v2/service/pro/sse/agent_chat_generation";
|
|
721
|
+
const machineHostname = hostname().replace(/[^\x20-\x7e]/gu, "").trim();
|
|
523
722
|
return {
|
|
524
723
|
Authorization: `Bearer COSY.${payloadB64}.${sig}`,
|
|
525
724
|
"Cosy-Key": cosyKey,
|
|
@@ -529,17 +728,17 @@ function buildAuthHeaders(body, requestURL, creds) {
|
|
|
529
728
|
"Cosy-Machineid": machineID,
|
|
530
729
|
"Cosy-Machinetoken": machineID,
|
|
531
730
|
"Cosy-Machinetype": qoderMachineTypeMagic,
|
|
532
|
-
"Cosy-Machineos": qoderMachineOs,
|
|
731
|
+
"Cosy-Machineos": qoderMachineOs(),
|
|
732
|
+
...isModelRequest && machineHostname ? { "Cosy-MachineHostname": machineHostname } : {},
|
|
533
733
|
"Cosy-Clienttype": "5",
|
|
534
|
-
"Cosy-
|
|
535
|
-
"Cosy-
|
|
536
|
-
"Cosy-
|
|
537
|
-
"Cosy-
|
|
538
|
-
"Cosy-Data-Policy": qoderDataPolicy,
|
|
539
|
-
"Cosy-Organization-Id":
|
|
540
|
-
"Cosy-Organization-Tags": "",
|
|
541
|
-
"Login-Version": qoderLoginVersion
|
|
542
|
-
"X-Request-Id": crypto.randomUUID()
|
|
734
|
+
"Cosy-Business-Product": "cli",
|
|
735
|
+
"Cosy-Business-Type": "agent",
|
|
736
|
+
"Cosy-Scene": "assistant",
|
|
737
|
+
...isModelRequest ? {} : { "Cosy-Clientip": machineID },
|
|
738
|
+
"Cosy-Data-Policy": creds.dataPolicyAgreed === true ? "agree" : qoderDataPolicy,
|
|
739
|
+
...creds.organizationId ? { "Cosy-Organization-Id": creds.organizationId } : {},
|
|
740
|
+
...creds.organizationTags?.length ? { "Cosy-Organization-Tags": creds.organizationTags.join(",") } : {},
|
|
741
|
+
"Login-Version": qoderLoginVersion
|
|
543
742
|
};
|
|
544
743
|
}
|
|
545
744
|
const defaultResponseHeaderTimeoutMs = 6e4;
|
|
@@ -666,15 +865,12 @@ async function openApiJsonRequest(fetchImpl, options) {
|
|
|
666
865
|
const headers = {
|
|
667
866
|
accept: "application/json",
|
|
668
867
|
"user-agent": options.userAgent ?? defaultUserAgent,
|
|
669
|
-
"cosy-version":
|
|
868
|
+
"cosy-version": qoderIdeVersion,
|
|
869
|
+
"cosy-machineos": qoderMachineOs(),
|
|
670
870
|
"cosy-clienttype": "5",
|
|
671
871
|
...options.headers
|
|
672
872
|
};
|
|
673
873
|
if (options.token) headers.authorization = `Bearer ${options.token}`;
|
|
674
|
-
if (options.machineId) {
|
|
675
|
-
headers["Cosy-MachineToken"] = options.machineId;
|
|
676
|
-
headers["Cosy-MachineType"] = "host";
|
|
677
|
-
}
|
|
678
874
|
let bodyText;
|
|
679
875
|
if (options.body !== void 0) {
|
|
680
876
|
headers["content-type"] = "application/json";
|
|
@@ -718,16 +914,28 @@ async function openApiJsonRequest(fetchImpl, options) {
|
|
|
718
914
|
const expiryBufferMs = 300 * 1e3;
|
|
719
915
|
const defaultExpiryMs = 1440 * 60 * 1e3;
|
|
720
916
|
const defaultAuthTimeoutMs = 15e3;
|
|
917
|
+
const defaultOrganizationTagsTimeoutMs = 3e3;
|
|
918
|
+
/** Match qodercli's seconds/milliseconds compatibility, preferring absolute expiry. */
|
|
919
|
+
function jobTokenExpiry(data, now = Date.now()) {
|
|
920
|
+
const absolute = data.expires_at ?? data.expiresAt ?? data.expire_time ?? data.expireTime;
|
|
921
|
+
if (absolute !== void 0) {
|
|
922
|
+
const parsed = typeof absolute === "string" ? Date.parse(absolute) : absolute > 0xe8d4a51000 ? absolute : absolute * 1e3;
|
|
923
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
924
|
+
}
|
|
925
|
+
const relative = data.expires_in;
|
|
926
|
+
if (typeof relative === "number" && Number.isFinite(relative) && relative > 0) return now + (relative > 86400 ? relative : relative * 1e3);
|
|
927
|
+
return now + defaultExpiryMs;
|
|
928
|
+
}
|
|
721
929
|
function abortedError() {
|
|
722
930
|
return new QoderLlmError("Qoder authentication was aborted.", "ABORTED");
|
|
723
931
|
}
|
|
724
932
|
async function waitForFlight(promise, signal) {
|
|
725
933
|
if (signal === void 0) return promise;
|
|
726
|
-
if (signal.aborted) throw abortedError();
|
|
727
934
|
return new Promise((resolve, reject) => {
|
|
728
935
|
const onAbort = () => reject(abortedError());
|
|
729
936
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
730
937
|
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
938
|
+
if (signal.aborted) onAbort();
|
|
731
939
|
});
|
|
732
940
|
}
|
|
733
941
|
var QoderAuthService = class {
|
|
@@ -735,14 +943,16 @@ var QoderAuthService = class {
|
|
|
735
943
|
inFlight = /* @__PURE__ */ new Map();
|
|
736
944
|
fetchImpl;
|
|
737
945
|
timeoutMs;
|
|
946
|
+
organizationTagsTimeoutMs;
|
|
738
947
|
resolveMachineId;
|
|
739
948
|
region;
|
|
740
949
|
logger;
|
|
741
950
|
constructor(options = {}) {
|
|
742
951
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
743
952
|
this.timeoutMs = options.timeoutMs ?? defaultAuthTimeoutMs;
|
|
744
|
-
this.
|
|
953
|
+
this.organizationTagsTimeoutMs = options.organizationTagsTimeoutMs ?? defaultOrganizationTagsTimeoutMs;
|
|
745
954
|
this.region = options.region ?? "global";
|
|
955
|
+
this.resolveMachineId = options.resolveMachineId ?? (() => getMachineId(qoderMachineIdPaths(this.region)));
|
|
746
956
|
this.logger = options.logger;
|
|
747
957
|
}
|
|
748
958
|
clear(pat) {
|
|
@@ -763,7 +973,7 @@ var QoderAuthService = class {
|
|
|
763
973
|
created.waiters = 0;
|
|
764
974
|
created.settled = false;
|
|
765
975
|
created.timeout = setTimeout(() => controller.abort("authentication timeout"), this.timeoutMs);
|
|
766
|
-
created.promise = this.exchangeAndResolve(pat, controller.signal).finally(() => {
|
|
976
|
+
created.promise = this.exchangeAndResolve(pat, controller.signal, () => clearTimeout(created.timeout)).finally(() => {
|
|
767
977
|
created.settled = true;
|
|
768
978
|
clearTimeout(created.timeout);
|
|
769
979
|
if (this.inFlight.get(cacheKey) === created) this.inFlight.delete(cacheKey);
|
|
@@ -782,32 +992,45 @@ var QoderAuthService = class {
|
|
|
782
992
|
}
|
|
783
993
|
}
|
|
784
994
|
}
|
|
785
|
-
async exchangeAndResolve(pat, signal) {
|
|
786
|
-
|
|
787
|
-
let expiresAt = Date.now() + defaultExpiryMs;
|
|
995
|
+
async exchangeAndResolve(pat, signal, onAuthenticated) {
|
|
996
|
+
const machineID = this.resolveMachineId().trim();
|
|
788
997
|
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
789
998
|
url: getQoderExchangeUrl(this.region),
|
|
790
|
-
body: {
|
|
999
|
+
body: {
|
|
1000
|
+
personal_token: pat,
|
|
1001
|
+
...machineID && machineID !== "unknown" ? { machine_id: machineID } : {}
|
|
1002
|
+
},
|
|
791
1003
|
signal,
|
|
792
1004
|
timeoutMs: this.timeoutMs,
|
|
793
1005
|
logger: this.logger,
|
|
794
1006
|
operation: "Auth",
|
|
795
1007
|
logCategory: "auth.exchange"
|
|
796
1008
|
});
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1009
|
+
const jobToken = [
|
|
1010
|
+
data.token,
|
|
1011
|
+
data.device_token,
|
|
1012
|
+
data.access_token
|
|
1013
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
1014
|
+
if (!jobToken) throw new QoderLlmError("Qoder PAT exchange returned no job token.", "AUTH");
|
|
1015
|
+
const expiresAt = jobTokenExpiry(data);
|
|
803
1016
|
const userInfo = await retryMetadataRead(signal, () => this.fetchUserInfo(jobToken, signal));
|
|
1017
|
+
if (signal.aborted) throw abortedError();
|
|
1018
|
+
onAuthenticated();
|
|
804
1019
|
const creds = {
|
|
805
1020
|
userID: userInfo.userID,
|
|
806
1021
|
authToken: jobToken,
|
|
807
1022
|
name: userInfo.name || "Qoder User",
|
|
808
1023
|
email: userInfo.email,
|
|
809
|
-
machineID
|
|
1024
|
+
machineID,
|
|
1025
|
+
...userInfo.organizationId === void 0 ? {} : { organizationId: userInfo.organizationId },
|
|
1026
|
+
...userInfo.organizationTags === void 0 ? {} : { organizationTags: userInfo.organizationTags },
|
|
1027
|
+
...userInfo.dataPolicyAgreed === void 0 ? {} : { dataPolicyAgreed: userInfo.dataPolicyAgreed }
|
|
810
1028
|
};
|
|
1029
|
+
if (creds.organizationId && creds.organizationTags === void 0) {
|
|
1030
|
+
const tags = await this.fetchOrganizationTags(jobToken, creds.organizationId, signal);
|
|
1031
|
+
if (tags !== void 0) creds.organizationTags = tags;
|
|
1032
|
+
}
|
|
1033
|
+
if (signal.aborted) throw abortedError();
|
|
811
1034
|
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
812
1035
|
this.cache.set(cacheKey, {
|
|
813
1036
|
creds,
|
|
@@ -825,13 +1048,49 @@ var QoderAuthService = class {
|
|
|
825
1048
|
operation: "UserInfo",
|
|
826
1049
|
logCategory: "auth.user-info"
|
|
827
1050
|
});
|
|
828
|
-
|
|
1051
|
+
const userID = [
|
|
1052
|
+
info.id,
|
|
1053
|
+
info.user_id,
|
|
1054
|
+
info.uid
|
|
1055
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
1056
|
+
const organizationId = [
|
|
1057
|
+
info.orgId,
|
|
1058
|
+
info.organization_id,
|
|
1059
|
+
info.organizationId,
|
|
1060
|
+
info.organization?.org_id,
|
|
1061
|
+
info.organization?.orgId,
|
|
1062
|
+
info.organization?.id
|
|
1063
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
1064
|
+
if (!userID) throw new QoderLlmError("Qoder identity lookup returned no user id.", "AUTH");
|
|
1065
|
+
const organizationTags = Array.isArray(info.organization_tags) ? info.organization_tags.filter((tag) => typeof tag === "string") : void 0;
|
|
829
1066
|
return {
|
|
830
|
-
userID
|
|
1067
|
+
userID,
|
|
831
1068
|
email: info.email ?? "",
|
|
832
|
-
name: info.name ?? info.username ?? ""
|
|
1069
|
+
name: info.name ?? info.username ?? "",
|
|
1070
|
+
...organizationId ? { organizationId } : {},
|
|
1071
|
+
...organizationTags === void 0 ? {} : { organizationTags },
|
|
1072
|
+
...typeof info.data_policy_agreed === "boolean" ? { dataPolicyAgreed: info.data_policy_agreed } : {}
|
|
833
1073
|
};
|
|
834
1074
|
}
|
|
1075
|
+
async fetchOrganizationTags(jobToken, organizationId, signal) {
|
|
1076
|
+
const timeout = AbortSignal.timeout(this.organizationTagsTimeoutMs);
|
|
1077
|
+
const lookupSignal = AbortSignal.any([signal, timeout]);
|
|
1078
|
+
try {
|
|
1079
|
+
const { tags } = await retryMetadataRead(lookupSignal, () => openApiJsonRequest(this.fetchImpl, {
|
|
1080
|
+
url: `${resolveQoderEndpoints(this.region).openApiUrl}/api/v1/organizations/${encodeURIComponent(organizationId)}/tags`,
|
|
1081
|
+
token: jobToken,
|
|
1082
|
+
signal: lookupSignal,
|
|
1083
|
+
timeoutMs: this.organizationTagsTimeoutMs,
|
|
1084
|
+
logger: this.logger,
|
|
1085
|
+
operation: "OrganizationTags"
|
|
1086
|
+
}));
|
|
1087
|
+
return Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [];
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
if (signal.aborted) throw error;
|
|
1090
|
+
this.logger?.warn?.("[Qoder Auth] Organization tags unavailable; retaining subscriber organization.");
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
835
1094
|
};
|
|
836
1095
|
//#endregion
|
|
837
1096
|
//#region src/qoder/transport/catalog-reader.ts
|
|
@@ -885,14 +1144,19 @@ async function fetchQoderModels(credentials, options = {}) {
|
|
|
885
1144
|
function unsupported(message) {
|
|
886
1145
|
return new QoderLlmError(message, "UNSUPPORTED_CONTENT");
|
|
887
1146
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
if (
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
return text;
|
|
1147
|
+
/** Keep multimodal tool results attached to their call. */
|
|
1148
|
+
async function translateToolResultContent(blocks, context) {
|
|
1149
|
+
const supportedBlocks = blocks.map((block) => {
|
|
1150
|
+
if (block.type === "text" || block.type === "image") return block;
|
|
1151
|
+
throw unsupported(`Qoder tool results support text and images only; received ${String(block.type)} content.`);
|
|
1152
|
+
});
|
|
1153
|
+
const images = supportedBlocks.filter((block) => block.type === "image");
|
|
1154
|
+
if (images.length === 0) return { content: supportedBlocks.map((block) => block.type === "text" ? block.text : "").join("") };
|
|
1155
|
+
if (context.attachments !== void 0) enforceImageLimits(images, context.attachments);
|
|
1156
|
+
return { content: await Promise.all(supportedBlocks.map((block) => block.type === "image" ? resolveImagePart(block, context) : {
|
|
1157
|
+
type: "text",
|
|
1158
|
+
text: block.text
|
|
1159
|
+
})) };
|
|
896
1160
|
}
|
|
897
1161
|
/** Reject a batch that exceeds the deployment image policy before any upload work starts. */
|
|
898
1162
|
function enforceImageLimits(images, attachments) {
|
|
@@ -985,23 +1249,21 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
985
1249
|
output.push({
|
|
986
1250
|
role: "tool",
|
|
987
1251
|
tool_call_id: callId,
|
|
988
|
-
|
|
1252
|
+
...await translateToolResultContent(result.content, context)
|
|
989
1253
|
});
|
|
990
|
-
const images = result.content.filter((block) => block.type === "image");
|
|
991
|
-
if (images.length > 0) {
|
|
992
|
-
if (attachments !== void 0) enforceImageLimits(images, attachments);
|
|
993
|
-
output.push({
|
|
994
|
-
role: "user",
|
|
995
|
-
content: [{
|
|
996
|
-
type: "text",
|
|
997
|
-
text: `[${images.length} image${images.length === 1 ? "" : "s"} returned by the previous tool call]`
|
|
998
|
-
}, ...await Promise.all(images.map((image) => resolveImagePart(image, context)))]
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
1254
|
}
|
|
1002
1255
|
if (nonToolBlocks.length === 0) continue;
|
|
1003
1256
|
}
|
|
1004
1257
|
const contentBlocks = toolResults.length > 0 ? nonToolBlocks : message.content;
|
|
1258
|
+
if (message.role === "tool") {
|
|
1259
|
+
const callId = consumeToolCallId(message, void 0, lastAssistantToolCallIds, output.length);
|
|
1260
|
+
output.push({
|
|
1261
|
+
role: "tool",
|
|
1262
|
+
tool_call_id: callId,
|
|
1263
|
+
...await translateToolResultContent(contentBlocks, context)
|
|
1264
|
+
});
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1005
1267
|
let text = "";
|
|
1006
1268
|
let reasoningText = "";
|
|
1007
1269
|
const userContent = [];
|
|
@@ -1009,7 +1271,6 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1009
1271
|
let hasImage = false;
|
|
1010
1272
|
const toolCalls = [];
|
|
1011
1273
|
for (const block of contentBlocks) {
|
|
1012
|
-
if (message.role === "tool" && block.type !== "text" && block.type !== "image") throw unsupported(`Qoder tool results support text and images only; received ${block.type} content.`);
|
|
1013
1274
|
if (block.type === "text") {
|
|
1014
1275
|
text += block.text;
|
|
1015
1276
|
if (message.role === "user" || toolResults.length > 0) userContent.push({
|
|
@@ -1019,7 +1280,7 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1019
1280
|
continue;
|
|
1020
1281
|
}
|
|
1021
1282
|
if (block.type === "image") {
|
|
1022
|
-
if (message.role !== "user" &&
|
|
1283
|
+
if (message.role !== "user" && toolResults.length === 0) throw unsupported("Qoder image content is valid only in user messages.");
|
|
1023
1284
|
hasImage = true;
|
|
1024
1285
|
pendingImages.push({
|
|
1025
1286
|
slot: userContent.length,
|
|
@@ -1056,25 +1317,6 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
|
|
|
1056
1317
|
});
|
|
1057
1318
|
continue;
|
|
1058
1319
|
}
|
|
1059
|
-
if (message.role === "tool") {
|
|
1060
|
-
const callId = consumeToolCallId(message, void 0, lastAssistantToolCallIds, output.length);
|
|
1061
|
-
output.push({
|
|
1062
|
-
role: "tool",
|
|
1063
|
-
tool_call_id: callId,
|
|
1064
|
-
content: text
|
|
1065
|
-
});
|
|
1066
|
-
if (pendingImages.length > 0) {
|
|
1067
|
-
if (attachments !== void 0) enforceImageLimits(pendingImages.map((pending) => pending.block), attachments);
|
|
1068
|
-
output.push({
|
|
1069
|
-
role: "user",
|
|
1070
|
-
content: [{
|
|
1071
|
-
type: "text",
|
|
1072
|
-
text: `[${pendingImages.length} image${pendingImages.length === 1 ? "" : "s"} returned by the previous tool call]`
|
|
1073
|
-
}, ...await Promise.all(pendingImages.map((pending) => resolveImagePart(pending.block, context)))]
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
continue;
|
|
1077
|
-
}
|
|
1078
1320
|
if (pendingImages.length > 0) {
|
|
1079
1321
|
if (attachments !== void 0) enforceImageLimits(pendingImages.map((pending) => pending.block), attachments);
|
|
1080
1322
|
await Promise.all(pendingImages.map(async (pending) => {
|
|
@@ -1152,10 +1394,10 @@ const injectedContextOpenings = [
|
|
|
1152
1394
|
*/
|
|
1153
1395
|
const hostNotificationTemplate = /^(?:Agent|Background subagent) \S{3,} (?:sent a message:|finished\b)/;
|
|
1154
1396
|
/**
|
|
1155
|
-
*
|
|
1397
|
+
* Compatibility with image markers synthesized by older transport versions.
|
|
1156
1398
|
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1399
|
+
* Current tool images stay in tool-role messages. Historical user-role markers
|
|
1400
|
+
* still belong to the open record rather than to new subscriber input.
|
|
1159
1401
|
*/
|
|
1160
1402
|
const syntheticMarkerTemplate = /^\[\d+ images? returned by the previous tool call\]/;
|
|
1161
1403
|
/** Wire text of one user message, used for injection classification and text extraction. */
|
|
@@ -1230,7 +1472,8 @@ var QoderTurnTracker = class {
|
|
|
1230
1472
|
requestSetId: `qoder-request-${crypto.randomUUID()}`,
|
|
1231
1473
|
chatRecordId: requestId,
|
|
1232
1474
|
businessId: crypto.randomUUID(),
|
|
1233
|
-
beginAt: now
|
|
1475
|
+
beginAt: now,
|
|
1476
|
+
businessStage: "start"
|
|
1234
1477
|
};
|
|
1235
1478
|
if (kind === "auxiliary") {
|
|
1236
1479
|
if (this.sessions.has(sessionId)) this.sessions.get(sessionId).lastTouchedAt = now;
|
|
@@ -1238,7 +1481,8 @@ var QoderTurnTracker = class {
|
|
|
1238
1481
|
requestSetId: `qoder-request-${crypto.randomUUID()}`,
|
|
1239
1482
|
chatRecordId: requestId,
|
|
1240
1483
|
businessId: crypto.randomUUID(),
|
|
1241
|
-
beginAt: now
|
|
1484
|
+
beginAt: now,
|
|
1485
|
+
businessStage: "start"
|
|
1242
1486
|
};
|
|
1243
1487
|
}
|
|
1244
1488
|
let state = this.sessions.get(sessionId);
|
|
@@ -1286,12 +1530,15 @@ var QoderTurnTracker = class {
|
|
|
1286
1530
|
if (current === void 0) state.claimed.delete(fingerprint);
|
|
1287
1531
|
else if (current.count < claimed) state.claimed.set(fingerprint, current.count);
|
|
1288
1532
|
}
|
|
1533
|
+
let businessStage = "processing";
|
|
1289
1534
|
if (opening !== void 0 && unclaimed > 0 || modelChanged) {
|
|
1535
|
+
businessStage = "start";
|
|
1290
1536
|
state.turnCount += 1;
|
|
1291
1537
|
state.currentTurnId = turnIdFor(sessionId, state.turnCount, opening ?? state.currentModel ?? "");
|
|
1292
1538
|
state.currentBusinessId = crypto.randomUUID();
|
|
1293
1539
|
state.turnOpenedAt = now;
|
|
1294
1540
|
} else if (state.currentTurnId === "") {
|
|
1541
|
+
businessStage = "start";
|
|
1295
1542
|
state.turnCount += 1;
|
|
1296
1543
|
state.currentTurnId = turnIdFor(sessionId, state.turnCount, state.currentModel ?? "");
|
|
1297
1544
|
state.currentBusinessId = crypto.randomUUID();
|
|
@@ -1301,7 +1548,8 @@ var QoderTurnTracker = class {
|
|
|
1301
1548
|
requestSetId: state.currentTurnId,
|
|
1302
1549
|
chatRecordId: requestId,
|
|
1303
1550
|
businessId: state.currentBusinessId,
|
|
1304
|
-
beginAt: state.turnOpenedAt
|
|
1551
|
+
beginAt: state.turnOpenedAt,
|
|
1552
|
+
businessStage
|
|
1305
1553
|
};
|
|
1306
1554
|
}
|
|
1307
1555
|
/**
|
|
@@ -1461,7 +1709,10 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
|
|
|
1461
1709
|
chat_prompt: "",
|
|
1462
1710
|
image_urls: null,
|
|
1463
1711
|
aliyun_user_type: "",
|
|
1464
|
-
system: ""
|
|
1712
|
+
system: messages.filter((message) => message.role === "system").flatMap((message) => typeof message.content === "string" && message.content.length > 0 ? [{
|
|
1713
|
+
type: "text",
|
|
1714
|
+
text: message.content
|
|
1715
|
+
}] : []),
|
|
1465
1716
|
messages,
|
|
1466
1717
|
tools,
|
|
1467
1718
|
parameters: {
|
|
@@ -1492,9 +1743,9 @@ async function buildQoderRequestBody(options, userId, translatedMessages, model,
|
|
|
1492
1743
|
},
|
|
1493
1744
|
business: {
|
|
1494
1745
|
product: "cli",
|
|
1495
|
-
version:
|
|
1746
|
+
version: qoderIdeVersion,
|
|
1496
1747
|
type: "agent",
|
|
1497
|
-
stage:
|
|
1748
|
+
stage: identity.businessStage,
|
|
1498
1749
|
id: identity.businessId,
|
|
1499
1750
|
name: lastUserText.substring(0, 30),
|
|
1500
1751
|
begin_at: identity.beginAt
|
|
@@ -2029,7 +2280,7 @@ var QoderUsageReader = class {
|
|
|
2029
2280
|
const [usage, plan, status] = await Promise.all([
|
|
2030
2281
|
retryMetadataRead(signal, () => this.fetchUsage(creds.authToken, signal)),
|
|
2031
2282
|
this.safeFetchPlan(creds.authToken, signal),
|
|
2032
|
-
this.safeFetchStatus(creds.authToken,
|
|
2283
|
+
this.safeFetchStatus(creds.authToken, signal)
|
|
2033
2284
|
]);
|
|
2034
2285
|
const accountInfo = {
|
|
2035
2286
|
profile,
|
|
@@ -2055,6 +2306,11 @@ var QoderUsageReader = class {
|
|
|
2055
2306
|
* `dedicatedResourcePackages`: those entries carry the campaign identifiers
|
|
2056
2307
|
* that the normalized list deliberately discards, and this payload crosses
|
|
2057
2308
|
* the seam into the browser.
|
|
2309
|
+
*
|
|
2310
|
+
* `addOnQuota` is the pool a plan holds separately from its own subscription
|
|
2311
|
+
* quota. A plan whose subscription quota is zero — Free in particular —
|
|
2312
|
+
* reports its whole balance there, so dropping the bucket left the card with
|
|
2313
|
+
* nothing to render.
|
|
2058
2314
|
*/
|
|
2059
2315
|
async fetchUsage(jobToken, signal) {
|
|
2060
2316
|
const data = await openApiJsonRequest(this.fetchImpl, {
|
|
@@ -2072,6 +2328,7 @@ var QoderUsageReader = class {
|
|
|
2072
2328
|
return {
|
|
2073
2329
|
userQuota: normalizeQuota(data.userQuota),
|
|
2074
2330
|
orgResourcePackage: normalizeQuota(data.orgResourcePackage),
|
|
2331
|
+
addOnQuota: normalizeQuota(data.add_on_quota ?? data.addOnQuota),
|
|
2075
2332
|
...dedicatedResourcePackages !== void 0 ? { dedicatedResourcePackages } : {},
|
|
2076
2333
|
totalUsagePercentage: typeof data.totalUsagePercentage === "number" ? data.totalUsagePercentage : void 0,
|
|
2077
2334
|
isQuotaExceeded: typeof data.isQuotaExceeded === "boolean" ? data.isQuotaExceeded : false,
|
|
@@ -2099,11 +2356,10 @@ var QoderUsageReader = class {
|
|
|
2099
2356
|
return;
|
|
2100
2357
|
}
|
|
2101
2358
|
}
|
|
2102
|
-
async fetchStatus(jobToken,
|
|
2359
|
+
async fetchStatus(jobToken, signal) {
|
|
2103
2360
|
return normalizeStatus(await openApiJsonRequest(this.fetchImpl, {
|
|
2104
2361
|
url: getQoderUserStatusUrl(this.region),
|
|
2105
2362
|
token: jobToken,
|
|
2106
|
-
machineId,
|
|
2107
2363
|
signal,
|
|
2108
2364
|
timeoutMs: this.timeoutMs,
|
|
2109
2365
|
logger: this.logger,
|
|
@@ -2111,9 +2367,9 @@ var QoderUsageReader = class {
|
|
|
2111
2367
|
logCategory: "account.status"
|
|
2112
2368
|
}));
|
|
2113
2369
|
}
|
|
2114
|
-
async safeFetchStatus(jobToken,
|
|
2370
|
+
async safeFetchStatus(jobToken, signal) {
|
|
2115
2371
|
try {
|
|
2116
|
-
return await this.fetchStatus(jobToken,
|
|
2372
|
+
return await this.fetchStatus(jobToken, signal);
|
|
2117
2373
|
} catch (error) {
|
|
2118
2374
|
if (signal?.aborted) throw error;
|
|
2119
2375
|
this.logger?.warn?.("[Qoder Status] Failed to load user status (degraded)", error instanceof Error ? error.message : error);
|
|
@@ -2133,23 +2389,27 @@ var QoderUsageReader = class {
|
|
|
2133
2389
|
*/
|
|
2134
2390
|
const qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
|
|
2135
2391
|
const qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
2136
|
-
|
|
2392
|
+
const qoderEncodeTable = /* @__PURE__ */ new Uint8Array(128);
|
|
2393
|
+
for (let i = 0; i < 64; i++) qoderEncodeTable[qoderStdAlphabet.charCodeAt(i)] = qoderCustomAlphabet.charCodeAt(i);
|
|
2394
|
+
qoderEncodeTable["=".charCodeAt(0)] = "$".charCodeAt(0);
|
|
2395
|
+
/** Encode directly into wire bytes without building per-character string ropes. */
|
|
2396
|
+
function qoderEncodeBodyBytes(plaintext) {
|
|
2137
2397
|
const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
|
|
2138
2398
|
const n = std.length;
|
|
2139
2399
|
const a = Math.floor(n / 3);
|
|
2140
|
-
const
|
|
2141
|
-
let
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
else out += c;
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2400
|
+
const out = Buffer.allocUnsafe(n);
|
|
2401
|
+
let offset = 0;
|
|
2402
|
+
const writeRange = (start, end) => {
|
|
2403
|
+
for (let i = start; i < end; i++) out[offset++] = qoderEncodeTable[std.charCodeAt(i)];
|
|
2404
|
+
};
|
|
2405
|
+
writeRange(n - a, n);
|
|
2406
|
+
writeRange(a, n - a);
|
|
2407
|
+
writeRange(0, a);
|
|
2151
2408
|
return out;
|
|
2152
2409
|
}
|
|
2410
|
+
function qoderEncodeBody(plaintext) {
|
|
2411
|
+
return qoderEncodeBodyBytes(plaintext).toString("latin1");
|
|
2412
|
+
}
|
|
2153
2413
|
//#endregion
|
|
2154
2414
|
//#region src/qoder/transport/search.ts
|
|
2155
2415
|
/**
|
|
@@ -2320,163 +2580,6 @@ var QoderSearchClient = class {
|
|
|
2320
2580
|
}
|
|
2321
2581
|
};
|
|
2322
2582
|
//#endregion
|
|
2323
|
-
//#region src/qoder/transport/wire/thinking.ts
|
|
2324
|
-
const thinkingTags = [{
|
|
2325
|
-
open: "<thinking>",
|
|
2326
|
-
close: "</thinking>"
|
|
2327
|
-
}, {
|
|
2328
|
-
open: "<think>",
|
|
2329
|
-
close: "</think>"
|
|
2330
|
-
}];
|
|
2331
|
-
const allTags = thinkingTags.flatMap((tag) => [tag.open, tag.close]);
|
|
2332
|
-
function trailingPrefixLength(text, candidates) {
|
|
2333
|
-
let best = 0;
|
|
2334
|
-
for (const candidate of candidates) {
|
|
2335
|
-
const limit = Math.min(text.length, candidate.length - 1);
|
|
2336
|
-
for (let length = limit; length > best; length--) if (text.endsWith(candidate.slice(0, length))) {
|
|
2337
|
-
best = length;
|
|
2338
|
-
break;
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
return best;
|
|
2342
|
-
}
|
|
2343
|
-
function earliestTag(text, kind) {
|
|
2344
|
-
let found;
|
|
2345
|
-
for (const tag of thinkingTags) {
|
|
2346
|
-
const position = text.indexOf(tag[kind]);
|
|
2347
|
-
if (position !== -1 && (found === void 0 || position < found.position)) found = {
|
|
2348
|
-
position,
|
|
2349
|
-
tag
|
|
2350
|
-
};
|
|
2351
|
-
}
|
|
2352
|
-
return found;
|
|
2353
|
-
}
|
|
2354
|
-
function trimSeparator(text) {
|
|
2355
|
-
if (text.startsWith("\n\n")) return text.slice(2);
|
|
2356
|
-
if (text.startsWith("\n")) return text.slice(1);
|
|
2357
|
-
return text;
|
|
2358
|
-
}
|
|
2359
|
-
var NativeReasoningStripper = class {
|
|
2360
|
-
buffer = "";
|
|
2361
|
-
push(chunk) {
|
|
2362
|
-
this.buffer += chunk;
|
|
2363
|
-
let output = "";
|
|
2364
|
-
while (this.buffer) {
|
|
2365
|
-
let firstPosition = -1;
|
|
2366
|
-
let firstTag = "";
|
|
2367
|
-
for (const tag of allTags) {
|
|
2368
|
-
const position = this.buffer.indexOf(tag);
|
|
2369
|
-
if (position !== -1 && (firstPosition === -1 || position < firstPosition)) {
|
|
2370
|
-
firstPosition = position;
|
|
2371
|
-
firstTag = tag;
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
if (firstPosition !== -1) {
|
|
2375
|
-
output += this.buffer.slice(0, firstPosition);
|
|
2376
|
-
this.buffer = this.buffer.slice(firstPosition + firstTag.length);
|
|
2377
|
-
continue;
|
|
2378
|
-
}
|
|
2379
|
-
const held = trailingPrefixLength(this.buffer, allTags);
|
|
2380
|
-
output += this.buffer.slice(0, this.buffer.length - held);
|
|
2381
|
-
this.buffer = this.buffer.slice(this.buffer.length - held);
|
|
2382
|
-
break;
|
|
2383
|
-
}
|
|
2384
|
-
return output;
|
|
2385
|
-
}
|
|
2386
|
-
finish() {
|
|
2387
|
-
const output = allTags.some((tag) => tag.startsWith(this.buffer)) ? "" : this.buffer;
|
|
2388
|
-
this.buffer = "";
|
|
2389
|
-
return output;
|
|
2390
|
-
}
|
|
2391
|
-
};
|
|
2392
|
-
var QoderThinkingParser = class {
|
|
2393
|
-
buffer = "";
|
|
2394
|
-
mode = "text";
|
|
2395
|
-
closingTag = thinkingTags[0].close;
|
|
2396
|
-
native = new NativeReasoningStripper();
|
|
2397
|
-
pushReasoning(chunk) {
|
|
2398
|
-
const text = this.native.push(chunk);
|
|
2399
|
-
return text ? [{
|
|
2400
|
-
type: "reasoning",
|
|
2401
|
-
text
|
|
2402
|
-
}] : [];
|
|
2403
|
-
}
|
|
2404
|
-
pushContent(chunk) {
|
|
2405
|
-
const output = [];
|
|
2406
|
-
const pendingReasoning = this.native.finish();
|
|
2407
|
-
if (pendingReasoning) output.push({
|
|
2408
|
-
type: "reasoning",
|
|
2409
|
-
text: pendingReasoning
|
|
2410
|
-
});
|
|
2411
|
-
this.buffer += chunk;
|
|
2412
|
-
while (this.buffer) {
|
|
2413
|
-
if (this.mode === "reasoning") {
|
|
2414
|
-
const closeAt = this.buffer.indexOf(this.closingTag);
|
|
2415
|
-
if (closeAt !== -1) {
|
|
2416
|
-
if (closeAt > 0) output.push({
|
|
2417
|
-
type: "reasoning",
|
|
2418
|
-
text: this.buffer.slice(0, closeAt)
|
|
2419
|
-
});
|
|
2420
|
-
this.buffer = trimSeparator(this.buffer.slice(closeAt + this.closingTag.length));
|
|
2421
|
-
this.mode = "text";
|
|
2422
|
-
continue;
|
|
2423
|
-
}
|
|
2424
|
-
const held = trailingPrefixLength(this.buffer, [this.closingTag]);
|
|
2425
|
-
const safeLength = this.buffer.length - held;
|
|
2426
|
-
if (safeLength > 0) output.push({
|
|
2427
|
-
type: "reasoning",
|
|
2428
|
-
text: this.buffer.slice(0, safeLength)
|
|
2429
|
-
});
|
|
2430
|
-
this.buffer = this.buffer.slice(safeLength);
|
|
2431
|
-
break;
|
|
2432
|
-
}
|
|
2433
|
-
const opener = earliestTag(this.buffer, "open");
|
|
2434
|
-
const closer = earliestTag(this.buffer, "close");
|
|
2435
|
-
if (opener !== void 0 && (closer === void 0 || opener.position < closer.position)) {
|
|
2436
|
-
if (opener.position > 0) output.push({
|
|
2437
|
-
type: "text",
|
|
2438
|
-
text: this.buffer.slice(0, opener.position)
|
|
2439
|
-
});
|
|
2440
|
-
this.buffer = this.buffer.slice(opener.position + opener.tag.open.length);
|
|
2441
|
-
this.closingTag = opener.tag.close;
|
|
2442
|
-
this.mode = "reasoning";
|
|
2443
|
-
continue;
|
|
2444
|
-
}
|
|
2445
|
-
if (closer !== void 0) {
|
|
2446
|
-
if (closer.position > 0) output.push({
|
|
2447
|
-
type: "text",
|
|
2448
|
-
text: this.buffer.slice(0, closer.position)
|
|
2449
|
-
});
|
|
2450
|
-
this.buffer = trimSeparator(this.buffer.slice(closer.position + closer.tag.close.length));
|
|
2451
|
-
continue;
|
|
2452
|
-
}
|
|
2453
|
-
const held = trailingPrefixLength(this.buffer, allTags);
|
|
2454
|
-
const safeLength = this.buffer.length - held;
|
|
2455
|
-
if (safeLength > 0) output.push({
|
|
2456
|
-
type: "text",
|
|
2457
|
-
text: this.buffer.slice(0, safeLength)
|
|
2458
|
-
});
|
|
2459
|
-
this.buffer = this.buffer.slice(safeLength);
|
|
2460
|
-
break;
|
|
2461
|
-
}
|
|
2462
|
-
return output;
|
|
2463
|
-
}
|
|
2464
|
-
finish() {
|
|
2465
|
-
const output = [];
|
|
2466
|
-
const pendingReasoning = this.native.finish();
|
|
2467
|
-
if (pendingReasoning) output.push({
|
|
2468
|
-
type: "reasoning",
|
|
2469
|
-
text: pendingReasoning
|
|
2470
|
-
});
|
|
2471
|
-
if (this.buffer) output.push({
|
|
2472
|
-
type: this.mode,
|
|
2473
|
-
text: this.buffer
|
|
2474
|
-
});
|
|
2475
|
-
this.buffer = "";
|
|
2476
|
-
return output;
|
|
2477
|
-
}
|
|
2478
|
-
};
|
|
2479
|
-
//#endregion
|
|
2480
2583
|
//#region src/qoder/transport/wire/sse.ts
|
|
2481
2584
|
/** Decode Qoder's outer SSE envelope and inner model chunks. */
|
|
2482
2585
|
const doneMarker = "[DONE]";
|
|
@@ -2484,31 +2587,7 @@ const defaultMaxSseBufferChars = 2 * 1024 * 1024;
|
|
|
2484
2587
|
function malformed(message) {
|
|
2485
2588
|
return new QoderLlmError(message, "MALFORMED_RESPONSE");
|
|
2486
2589
|
}
|
|
2487
|
-
function
|
|
2488
|
-
const raw = envelope.body ?? envelope.message;
|
|
2489
|
-
if (!raw) return void 0;
|
|
2490
|
-
if (typeof raw !== "string") return JSON.stringify(raw);
|
|
2491
|
-
const trimmed = raw.trim();
|
|
2492
|
-
try {
|
|
2493
|
-
const parsed = JSON.parse(trimmed);
|
|
2494
|
-
if (parsed && typeof parsed === "object") {
|
|
2495
|
-
let detailMsg;
|
|
2496
|
-
if (typeof parsed.details === "string") try {
|
|
2497
|
-
const inner = JSON.parse(parsed.details);
|
|
2498
|
-
if (typeof inner?.error?.message === "string") detailMsg = inner.error.message.trim();
|
|
2499
|
-
else if (typeof inner?.message === "string") detailMsg = inner.message.trim();
|
|
2500
|
-
} catch {}
|
|
2501
|
-
const msg = typeof parsed.message === "string" ? parsed.message.trim() : void 0;
|
|
2502
|
-
const code = typeof parsed.code === "string" ? parsed.code.trim() : void 0;
|
|
2503
|
-
const err = typeof parsed.error === "string" ? parsed.error.trim() : void 0;
|
|
2504
|
-
const text = detailMsg || msg || err;
|
|
2505
|
-
if (code && text) return `${code}: ${text}`;
|
|
2506
|
-
return text || code || trimmed;
|
|
2507
|
-
}
|
|
2508
|
-
} catch {}
|
|
2509
|
-
return trimmed;
|
|
2510
|
-
}
|
|
2511
|
-
function parseEnvelope(rawData) {
|
|
2590
|
+
function parseEnvelope(rawData, options) {
|
|
2512
2591
|
let value;
|
|
2513
2592
|
try {
|
|
2514
2593
|
value = JSON.parse(rawData);
|
|
@@ -2519,13 +2598,13 @@ function parseEnvelope(rawData) {
|
|
|
2519
2598
|
const envelope = value;
|
|
2520
2599
|
if (envelope.statusCodeValue !== void 0 && typeof envelope.statusCodeValue !== "number") throw malformed("Qoder SSE envelope has an invalid status code.");
|
|
2521
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.");
|
|
2522
|
-
if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) {
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
}
|
|
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
|
+
});
|
|
2529
2608
|
if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
|
|
2530
2609
|
return envelope;
|
|
2531
2610
|
}
|
|
@@ -2572,7 +2651,6 @@ function carriesStreamDelta(delta) {
|
|
|
2572
2651
|
async function* parseQoderSse(stream, options = {}) {
|
|
2573
2652
|
const reader = stream.getReader();
|
|
2574
2653
|
const decoder = new TextDecoder();
|
|
2575
|
-
const thinkingParser = new QoderThinkingParser();
|
|
2576
2654
|
const toolCalls = /* @__PURE__ */ new Map();
|
|
2577
2655
|
let buffer = "";
|
|
2578
2656
|
let sourceEnded = false;
|
|
@@ -2676,7 +2754,7 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2676
2754
|
sawDone = true;
|
|
2677
2755
|
break;
|
|
2678
2756
|
}
|
|
2679
|
-
const envelope = parseEnvelope(rawData);
|
|
2757
|
+
const envelope = parseEnvelope(rawData, options);
|
|
2680
2758
|
const body = typeof envelope.body === "string" ? envelope.body.trim() : "";
|
|
2681
2759
|
if (!body) continue;
|
|
2682
2760
|
if (body === doneMarker) {
|
|
@@ -2693,8 +2771,14 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2693
2771
|
} else if (carriesStreamDelta(choice.delta)) sawFinishReason = false;
|
|
2694
2772
|
const delta = choice.delta;
|
|
2695
2773
|
if (!delta) continue;
|
|
2696
|
-
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) for (const
|
|
2697
|
-
|
|
2774
|
+
if (typeof delta.reasoning_content === "string" && delta.reasoning_content) for (const chunk of appendSegment({
|
|
2775
|
+
type: "reasoning",
|
|
2776
|
+
text: delta.reasoning_content
|
|
2777
|
+
})) yield chunk;
|
|
2778
|
+
if (typeof delta.content === "string" && delta.content) for (const chunk of appendSegment({
|
|
2779
|
+
type: "text",
|
|
2780
|
+
text: delta.content
|
|
2781
|
+
})) yield chunk;
|
|
2698
2782
|
if (delta.tool_calls !== void 0) {
|
|
2699
2783
|
if (!Array.isArray(delta.tool_calls)) throw malformed("Qoder tool-call delta is not an array.");
|
|
2700
2784
|
for (const rawCall of delta.tool_calls) {
|
|
@@ -2761,7 +2845,6 @@ async function* parseQoderSse(stream, options = {}) {
|
|
|
2761
2845
|
reader.releaseLock();
|
|
2762
2846
|
}
|
|
2763
2847
|
if (!sawDone && !sawFinishReason) throw new QoderLlmError("SSE stream ended prematurely without [DONE].", "TRANSPORT");
|
|
2764
|
-
for (const segment of thinkingParser.finish()) for (const chunk of appendSegment(segment)) yield chunk;
|
|
2765
2848
|
for (const chunk of closeTextual()) yield chunk;
|
|
2766
2849
|
for (const [, state] of [...toolCalls.entries()].sort(([left], [right]) => left - right)) {
|
|
2767
2850
|
if (!state.id || !state.name) throw malformed("Qoder completed an unidentifiable tool call.");
|
|
@@ -2821,8 +2904,8 @@ function aborted$1(message) {
|
|
|
2821
2904
|
}
|
|
2822
2905
|
async function* streamQoderChat(options, model, credentials, messages, dependencies) {
|
|
2823
2906
|
const request = await buildQoderRequestBody(options, credentials.userID, messages, model);
|
|
2824
|
-
|
|
2825
|
-
const encodedBytes =
|
|
2907
|
+
request.session_type = dependencies.region === "china" ? "qoderclicn" : "qodercli";
|
|
2908
|
+
const encodedBytes = qoderEncodeBodyBytes(JSON.stringify(request));
|
|
2826
2909
|
const chatUrl = getQoderChatUrl(dependencies.region);
|
|
2827
2910
|
const requestController = new AbortController();
|
|
2828
2911
|
let headerTimedOut = false;
|
|
@@ -2834,7 +2917,7 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2834
2917
|
let lastActivityAt = startedAt;
|
|
2835
2918
|
const onCallerAbort = () => requestController.abort(options.signal?.reason);
|
|
2836
2919
|
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
2837
|
-
|
|
2920
|
+
let headerTimer = setTimeout(() => {
|
|
2838
2921
|
headerTimedOut = true;
|
|
2839
2922
|
requestController.abort("response header timeout");
|
|
2840
2923
|
}, dependencies.responseHeaderTimeoutMs);
|
|
@@ -2857,58 +2940,72 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
2857
2940
|
contextTier: markedTiers.length === 1 ? markedTiers[0][0] : null,
|
|
2858
2941
|
context_length: request.parameters.context_length ?? null
|
|
2859
2942
|
}));
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
"
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2943
|
+
let response;
|
|
2944
|
+
let activeCredentials = credentials;
|
|
2945
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2946
|
+
if (options.signal?.aborted) throw aborted$1("Request was aborted.");
|
|
2947
|
+
if (attempt > 0) headerTimer = setTimeout(() => {
|
|
2948
|
+
headerTimedOut = true;
|
|
2949
|
+
requestController.abort("response header timeout");
|
|
2950
|
+
}, dependencies.responseHeaderTimeoutMs);
|
|
2951
|
+
response = await dependencies.fetch(chatUrl, {
|
|
2952
|
+
method: "POST",
|
|
2953
|
+
headers: {
|
|
2954
|
+
"content-type": "application/json",
|
|
2955
|
+
"accept": "text/event-stream",
|
|
2956
|
+
"cache-control": "no-cache",
|
|
2957
|
+
"connection": "keep-alive",
|
|
2958
|
+
"accept-encoding": "identity",
|
|
2959
|
+
"x-model-key": options.model || "cmodel",
|
|
2960
|
+
"x-model-source": model?.source || "system",
|
|
2961
|
+
...attributionHeaders(),
|
|
2962
|
+
...buildAuthHeaders(encodedBytes, chatUrl, activeCredentials)
|
|
2963
|
+
},
|
|
2964
|
+
body: encodedBytes,
|
|
2965
|
+
signal: requestController.signal
|
|
2966
|
+
});
|
|
2967
|
+
clearTimeout(headerTimer);
|
|
2968
|
+
reqId = qoderRequestId(response.headers);
|
|
2969
|
+
dependencies.logger?.debug?.("[Qoder Stream] Response headers received", {
|
|
2970
|
+
region: dependencies.region,
|
|
2971
|
+
status: response.status,
|
|
2972
|
+
attempt: attempt + 1,
|
|
2973
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
2974
|
+
...reqId === void 0 ? {} : { requestId: reqId }
|
|
2975
|
+
});
|
|
2976
|
+
resetIdleTimer();
|
|
2977
|
+
if (response.ok) break;
|
|
2885
2978
|
let bodyText = "";
|
|
2886
2979
|
try {
|
|
2887
|
-
bodyText = (await response
|
|
2888
|
-
} catch {
|
|
2889
|
-
|
|
2890
|
-
if (bodyText) try {
|
|
2891
|
-
const parsed = JSON.parse(bodyText);
|
|
2892
|
-
const msg = typeof parsed.message === "string" && parsed.message.trim() ? parsed.message.trim() : void 0;
|
|
2893
|
-
const code = typeof parsed.code === "string" && parsed.code.trim() ? parsed.code.trim() : void 0;
|
|
2894
|
-
if (code && msg) detail = `: ${code}: ${msg}`;
|
|
2895
|
-
else if (msg) detail = `: ${msg}`;
|
|
2896
|
-
else detail = `: ${bodyText.slice(0, 300)}`;
|
|
2897
|
-
} catch {
|
|
2898
|
-
detail = `: ${bodyText.slice(0, 300)}`;
|
|
2980
|
+
bodyText = (await readLimitedText(response, defaultMaxErrorBytes, "Qoder model error response")).trim();
|
|
2981
|
+
} catch (error) {
|
|
2982
|
+
if (requestController.signal.aborted) throw error;
|
|
2899
2983
|
}
|
|
2900
|
-
|
|
2984
|
+
const failure = qoderModelError(`Qoder upstream service returned HTTP ${response.status}`, {
|
|
2901
2985
|
status: response.status,
|
|
2902
2986
|
headers: response.headers,
|
|
2903
|
-
cause: bodyText || void 0
|
|
2987
|
+
cause: bodyText || void 0,
|
|
2988
|
+
source: "http"
|
|
2904
2989
|
});
|
|
2990
|
+
if (attempt === 0 && dependencies.refreshCredentials && canRefreshModelCredentials(failure)) {
|
|
2991
|
+
if (idleTimer !== void 0) clearTimeout(idleTimer);
|
|
2992
|
+
activeCredentials = await dependencies.refreshCredentials(activeCredentials, options.signal);
|
|
2993
|
+
if (activeCredentials.userID !== credentials.userID) throw new QoderLlmError("Qoder subscriber identity changed during authentication recovery.", "AUTH");
|
|
2994
|
+
continue;
|
|
2995
|
+
}
|
|
2996
|
+
throw failure;
|
|
2905
2997
|
}
|
|
2998
|
+
if (!response) throw new QoderLlmError("Qoder response is missing.", "EMPTY_RESPONSE");
|
|
2906
2999
|
if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
|
|
2907
3000
|
let firstChunkDurationMs;
|
|
2908
3001
|
let tokenUsage;
|
|
2909
3002
|
let finishReason;
|
|
2910
3003
|
const streamStartedAt = performance.now();
|
|
2911
|
-
for await (const chunk of parseQoderSse(response.body, {
|
|
3004
|
+
for await (const chunk of parseQoderSse(response.body, {
|
|
3005
|
+
onActivity: resetIdleTimer,
|
|
3006
|
+
headers: response.headers,
|
|
3007
|
+
httpStatus: response.status
|
|
3008
|
+
})) {
|
|
2912
3009
|
chunkCount++;
|
|
2913
3010
|
if (firstChunkDurationMs === void 0) {
|
|
2914
3011
|
firstChunkDurationMs = Math.round(performance.now() - startedAt);
|
|
@@ -3095,7 +3192,13 @@ var DefaultQoderTransport = class {
|
|
|
3095
3192
|
logger: this.logger,
|
|
3096
3193
|
region: this.region,
|
|
3097
3194
|
responseHeaderTimeoutMs: this.responseHeaderTimeoutMs,
|
|
3098
|
-
streamIdleTimeoutMs: this.streamIdleTimeoutMs
|
|
3195
|
+
streamIdleTimeoutMs: this.streamIdleTimeoutMs,
|
|
3196
|
+
refreshCredentials: async (rejected, signal) => {
|
|
3197
|
+
const current = await this.auth.getCredentials(pat, signal);
|
|
3198
|
+
if (current.authToken !== rejected.authToken) return current;
|
|
3199
|
+
this.auth.clear(pat);
|
|
3200
|
+
return this.auth.getCredentials(pat, signal);
|
|
3201
|
+
}
|
|
3099
3202
|
});
|
|
3100
3203
|
}
|
|
3101
3204
|
};
|
|
@@ -3165,6 +3268,149 @@ function modelsFor(config, region) {
|
|
|
3165
3268
|
return resolveModels(config.modelsByRegion?.[region]);
|
|
3166
3269
|
}
|
|
3167
3270
|
//#endregion
|
|
3271
|
+
//#region src/dsh/catalog-lifecycle.ts
|
|
3272
|
+
/** Region-scoped Qoder model discovery, effective catalogs, and advisory persistence. */
|
|
3273
|
+
var QoderCatalogLifecycle = class {
|
|
3274
|
+
options;
|
|
3275
|
+
catalogs = {
|
|
3276
|
+
global: {
|
|
3277
|
+
models: [],
|
|
3278
|
+
generation: 0,
|
|
3279
|
+
expiresAt: 0,
|
|
3280
|
+
dirty: false,
|
|
3281
|
+
retryRequested: false
|
|
3282
|
+
},
|
|
3283
|
+
china: {
|
|
3284
|
+
models: [],
|
|
3285
|
+
generation: 0,
|
|
3286
|
+
expiresAt: 0,
|
|
3287
|
+
dirty: false,
|
|
3288
|
+
retryRequested: false
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3291
|
+
settings;
|
|
3292
|
+
disposed = false;
|
|
3293
|
+
activeTransport;
|
|
3294
|
+
activeRegion;
|
|
3295
|
+
constructor(options) {
|
|
3296
|
+
this.options = options;
|
|
3297
|
+
}
|
|
3298
|
+
/** Observe every transport replacement, including switching away and back. */
|
|
3299
|
+
observeTransport() {
|
|
3300
|
+
const transport = this.options.resolveTransport();
|
|
3301
|
+
const region = this.options.region();
|
|
3302
|
+
if (transport === this.activeTransport && region === this.activeRegion) return;
|
|
3303
|
+
if (this.activeRegion !== void 0) {
|
|
3304
|
+
const previous = this.catalogs[this.activeRegion];
|
|
3305
|
+
previous.generation++;
|
|
3306
|
+
previous.inflight = void 0;
|
|
3307
|
+
}
|
|
3308
|
+
this.activeTransport = transport;
|
|
3309
|
+
this.activeRegion = region;
|
|
3310
|
+
const state = this.catalogs[region];
|
|
3311
|
+
state.generation++;
|
|
3312
|
+
state.expiresAt = 0;
|
|
3313
|
+
state.inflight = void 0;
|
|
3314
|
+
}
|
|
3315
|
+
models() {
|
|
3316
|
+
this.observeTransport();
|
|
3317
|
+
const region = this.options.region();
|
|
3318
|
+
return mergeQoderDiscoveryMetadata(this.options.configuredModels(region), this.catalogs[region].models);
|
|
3319
|
+
}
|
|
3320
|
+
/** Automatic reads join the newest discovery and never wait for settings writes. */
|
|
3321
|
+
async refresh() {
|
|
3322
|
+
while (!this.disposed) {
|
|
3323
|
+
this.observeTransport();
|
|
3324
|
+
const region = this.options.region();
|
|
3325
|
+
const state = this.catalogs[region];
|
|
3326
|
+
const transport = this.activeTransport;
|
|
3327
|
+
const pending = state.inflight ?? (Date.now() >= state.expiresAt ? this.discover() : void 0);
|
|
3328
|
+
await pending?.catch(() => {});
|
|
3329
|
+
this.observeTransport();
|
|
3330
|
+
if (this.activeTransport !== transport || this.activeRegion !== region) continue;
|
|
3331
|
+
if (state.inflight && state.inflight !== pending) continue;
|
|
3332
|
+
return;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
/** Explicit discovery supersedes older work as soon as it starts, even if it fails. */
|
|
3336
|
+
discover(signal, requestTransport) {
|
|
3337
|
+
if (this.disposed || signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model discovery was aborted.", "ABORTED"));
|
|
3338
|
+
this.observeTransport();
|
|
3339
|
+
const region = this.options.region();
|
|
3340
|
+
const state = this.catalogs[region];
|
|
3341
|
+
const transport = this.activeTransport;
|
|
3342
|
+
const generation = ++state.generation;
|
|
3343
|
+
const pending = Promise.resolve().then(() => (requestTransport ?? transport).discoverModels(signal)).then((models) => {
|
|
3344
|
+
this.observeTransport();
|
|
3345
|
+
if (!this.disposed && this.activeTransport === transport && this.activeRegion === region && state.generation === generation) {
|
|
3346
|
+
state.models = models.map(cloneCatalogModel);
|
|
3347
|
+
state.expiresAt = Date.now() + 300 * 1e3;
|
|
3348
|
+
state.dirty = true;
|
|
3349
|
+
this.schedulePersistence(region);
|
|
3350
|
+
try {
|
|
3351
|
+
Promise.resolve(this.options.onDiscovered?.(transport, state.models)).catch(() => {});
|
|
3352
|
+
} catch {}
|
|
3353
|
+
}
|
|
3354
|
+
return models;
|
|
3355
|
+
}).finally(() => {
|
|
3356
|
+
if (state.inflight === pending) state.inflight = void 0;
|
|
3357
|
+
});
|
|
3358
|
+
state.inflight = pending;
|
|
3359
|
+
return pending;
|
|
3360
|
+
}
|
|
3361
|
+
bindSettings(settings) {
|
|
3362
|
+
this.settings = settings;
|
|
3363
|
+
this.retryPersistence();
|
|
3364
|
+
}
|
|
3365
|
+
/** Called by settings changes/reattachment; no timer retries or fallback writes. */
|
|
3366
|
+
retryPersistence() {
|
|
3367
|
+
for (const region of ["global", "china"]) {
|
|
3368
|
+
const state = this.catalogs[region];
|
|
3369
|
+
if (state.models.length === 0) continue;
|
|
3370
|
+
state.dirty = true;
|
|
3371
|
+
this.schedulePersistence(region);
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
dispose() {
|
|
3375
|
+
this.disposed = true;
|
|
3376
|
+
this.settings = void 0;
|
|
3377
|
+
for (const state of Object.values(this.catalogs)) state.generation++;
|
|
3378
|
+
}
|
|
3379
|
+
schedulePersistence(region) {
|
|
3380
|
+
const state = this.catalogs[region];
|
|
3381
|
+
const settings = this.settings;
|
|
3382
|
+
if (this.disposed || !settings || !state.dirty) return;
|
|
3383
|
+
if (state.writing) {
|
|
3384
|
+
state.retryRequested = true;
|
|
3385
|
+
return;
|
|
3386
|
+
}
|
|
3387
|
+
state.retryRequested = false;
|
|
3388
|
+
const writing = Promise.resolve().then(() => this.persist(region, settings)).catch((error) => {
|
|
3389
|
+
this.options.onPersistenceError?.(error);
|
|
3390
|
+
}).finally(() => {
|
|
3391
|
+
if (state.writing === writing) state.writing = void 0;
|
|
3392
|
+
if (this.settings !== settings || state.retryRequested) this.schedulePersistence(region);
|
|
3393
|
+
});
|
|
3394
|
+
state.writing = writing;
|
|
3395
|
+
}
|
|
3396
|
+
async persist(region, settings) {
|
|
3397
|
+
const state = this.catalogs[region];
|
|
3398
|
+
while (!this.disposed && this.settings === settings && state.dirty) {
|
|
3399
|
+
const snapshot = settings.read(region);
|
|
3400
|
+
if (!snapshot) return;
|
|
3401
|
+
const enriched = modelsFor(Config({ modelsByRegion: { [region]: mergeQoderDiscoveryMetadata(snapshot.models, state.models) } }), region);
|
|
3402
|
+
state.dirty = false;
|
|
3403
|
+
if (hasSameQoderDiscoveryMetadata(snapshot.models, enriched)) continue;
|
|
3404
|
+
try {
|
|
3405
|
+
await settings.write(region, enriched, snapshot.revision);
|
|
3406
|
+
} catch (error) {
|
|
3407
|
+
state.dirty = true;
|
|
3408
|
+
if (!(error instanceof SettingsConflictError)) throw error;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
};
|
|
3413
|
+
//#endregion
|
|
3168
3414
|
//#region src/dsh/provider.ts
|
|
3169
3415
|
/** DSH model provider route, matching the plugin package name. */
|
|
3170
3416
|
const QODER_PROVIDER_ID = "dsh-provider-qoder";
|
|
@@ -3186,12 +3432,12 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3186
3432
|
catalogModels;
|
|
3187
3433
|
providerId;
|
|
3188
3434
|
providerName;
|
|
3189
|
-
|
|
3190
|
-
discoveries = /* @__PURE__ */ new WeakMap();
|
|
3435
|
+
catalog;
|
|
3191
3436
|
historyRegion;
|
|
3192
3437
|
historyRegionChanged = false;
|
|
3193
3438
|
region;
|
|
3194
3439
|
sessionTiers = /* @__PURE__ */ new Map();
|
|
3440
|
+
tierListeners = /* @__PURE__ */ new Set();
|
|
3195
3441
|
sessions;
|
|
3196
3442
|
agents;
|
|
3197
3443
|
constructor(options) {
|
|
@@ -3200,8 +3446,13 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3200
3446
|
this.catalogModels = options.models && options.models.length > 0 ? options.models : defaultModels;
|
|
3201
3447
|
this.providerId = options.providerId ?? "dsh-provider-qoder";
|
|
3202
3448
|
this.providerName = options.providerName ?? "Qoder";
|
|
3203
|
-
this.onModelsDiscovered = options.onModelsDiscovered;
|
|
3204
3449
|
this.region = options.region ?? (() => "global");
|
|
3450
|
+
this.catalog = options.catalog ?? new QoderCatalogLifecycle({
|
|
3451
|
+
resolveTransport: this.resolveTransport,
|
|
3452
|
+
region: this.region,
|
|
3453
|
+
configuredModels: () => this.catalogModels,
|
|
3454
|
+
onDiscovered: options.onModelsDiscovered
|
|
3455
|
+
});
|
|
3205
3456
|
this.historyRegion = this.region();
|
|
3206
3457
|
this.sessions = options.sessions;
|
|
3207
3458
|
this.agents = options.agents;
|
|
@@ -3214,6 +3465,31 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3214
3465
|
sessionId,
|
|
3215
3466
|
modelId
|
|
3216
3467
|
]), tierKey);
|
|
3468
|
+
for (const listener of this.tierListeners) try {
|
|
3469
|
+
listener({
|
|
3470
|
+
region,
|
|
3471
|
+
sessionId,
|
|
3472
|
+
modelId
|
|
3473
|
+
});
|
|
3474
|
+
} catch {}
|
|
3475
|
+
}
|
|
3476
|
+
subscribeSessionTiers(listener) {
|
|
3477
|
+
this.tierListeners.add(listener);
|
|
3478
|
+
return () => {
|
|
3479
|
+
this.tierListeners.delete(listener);
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
readSessionTier(scope) {
|
|
3483
|
+
const { region, sessionId, modelId } = scope;
|
|
3484
|
+
if (region !== this.region() || !sessionId || this.sessions && !this.sessions.get(sessionId)) throw new QoderLlmError("Invalid session context tier scope.", "INVALID_REQUEST");
|
|
3485
|
+
const model = this.resolveEffectiveModelForSession(modelId, sessionId);
|
|
3486
|
+
const tier = model && resolveContextTier(model, model.contextTier);
|
|
3487
|
+
if (!tier) throw new QoderLlmError("Model has no context tiers.", "INVALID_REQUEST");
|
|
3488
|
+
return {
|
|
3489
|
+
...scope,
|
|
3490
|
+
tierKey: tier.key,
|
|
3491
|
+
tokenCount: tier.tokenCount
|
|
3492
|
+
};
|
|
3217
3493
|
}
|
|
3218
3494
|
getSessionTier(sessionId, modelId) {
|
|
3219
3495
|
return this.sessionTiers.get(JSON.stringify([
|
|
@@ -3233,11 +3509,7 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3233
3509
|
const base = this.effectiveModels().find((candidate) => candidate.id === modelId);
|
|
3234
3510
|
if (base === void 0) return void 0;
|
|
3235
3511
|
if (sessionId) {
|
|
3236
|
-
const manualTierKey = this.
|
|
3237
|
-
this.region(),
|
|
3238
|
-
sessionId,
|
|
3239
|
-
modelId
|
|
3240
|
-
]));
|
|
3512
|
+
const manualTierKey = this.getSessionTier(sessionId, modelId);
|
|
3241
3513
|
const reqCtx = this.sessions?.get(sessionId)?.requestContext?.();
|
|
3242
3514
|
const tier = resolveContextTier(base, manualTierKey, !this.historyRegionChanged && reqCtx?.provider === this.providerId && reqCtx.model === modelId ? reqCtx.contextWindow : void 0);
|
|
3243
3515
|
if (tier) return {
|
|
@@ -3254,53 +3526,17 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3254
3526
|
name: this.providerName
|
|
3255
3527
|
};
|
|
3256
3528
|
}
|
|
3257
|
-
/** Refresh advertised metadata on catalog reads, sharing a five-minute transport-local cache. */
|
|
3258
3529
|
async listModels(provider) {
|
|
3259
|
-
|
|
3260
|
-
let cached = this.discoveries.get(transport);
|
|
3261
|
-
if (cached === void 0) {
|
|
3262
|
-
cached = { expiresAt: 0 };
|
|
3263
|
-
this.discoveries.set(transport, cached);
|
|
3264
|
-
}
|
|
3265
|
-
const entry = cached;
|
|
3266
|
-
if (entry.inflight === void 0 && Date.now() >= entry.expiresAt) entry.inflight = Promise.resolve().then(() => transport.discoverModels()).then((models) => {
|
|
3267
|
-
if (this.discoveries.get(transport) !== entry || this.resolveTransport() !== transport) return;
|
|
3268
|
-
entry.models = models;
|
|
3269
|
-
entry.expiresAt = Date.now() + 300 * 1e3;
|
|
3270
|
-
this.publishDiscoveredModels(transport, models);
|
|
3271
|
-
}).catch(() => {}).finally(() => {
|
|
3272
|
-
entry.inflight = void 0;
|
|
3273
|
-
});
|
|
3274
|
-
await entry.inflight;
|
|
3275
|
-
if (this.resolveTransport() !== transport) return this.listModels(provider);
|
|
3530
|
+
await this.catalog.refresh();
|
|
3276
3531
|
return this.effectiveModels().map((model) => modelInfo(provider, model));
|
|
3277
3532
|
}
|
|
3278
|
-
/**
|
|
3279
|
-
* Announce one accepted discovery without joining the catalog read.
|
|
3280
|
-
*
|
|
3281
|
-
* Persisting metadata can queue behind unrelated settings writes, so awaiting
|
|
3282
|
-
* it here would stall every `listModels` call on settings storage.
|
|
3283
|
-
*/
|
|
3284
|
-
publishDiscoveredModels(transport, models) {
|
|
3285
|
-
try {
|
|
3286
|
-
const pending = this.onModelsDiscovered?.(transport, models);
|
|
3287
|
-
if (pending !== void 0 && pending !== null && typeof pending.catch === "function") pending.catch(() => {});
|
|
3288
|
-
} catch {}
|
|
3289
|
-
}
|
|
3290
3533
|
effectiveModels() {
|
|
3291
|
-
return
|
|
3534
|
+
return this.catalog.models();
|
|
3292
3535
|
}
|
|
3293
3536
|
replaceModels(models) {
|
|
3294
3537
|
this.observeRegion();
|
|
3295
3538
|
this.catalogModels = models;
|
|
3296
3539
|
}
|
|
3297
|
-
/** Publish an explicit discovery without letting older in-flight reads overwrite it. */
|
|
3298
|
-
updateDiscoveredModels(transport, models) {
|
|
3299
|
-
this.discoveries.set(transport, {
|
|
3300
|
-
models,
|
|
3301
|
-
expiresAt: Date.now() + 300 * 1e3
|
|
3302
|
-
});
|
|
3303
|
-
}
|
|
3304
3540
|
resolveModel(provider, modelId, signal) {
|
|
3305
3541
|
if (signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model resolution was aborted.", "ABORTED"));
|
|
3306
3542
|
const sessionId = this.agents?.currentInitiator()?.session?.id;
|
|
@@ -3360,17 +3596,23 @@ var QoderAdapter = class extends LlmAdapter {
|
|
|
3360
3596
|
* @module dsh-provider-qoder/dsh/search-provider
|
|
3361
3597
|
*/
|
|
3362
3598
|
const QODER_SEARCH_PROVIDER_ID = "qoder";
|
|
3599
|
+
function initiatingModelProvider(ctx) {
|
|
3600
|
+
const agent = (ctx.get("agents") ?? ctx.agents)?.currentInitiator?.();
|
|
3601
|
+
const defaultModelService = ctx.get("agentDefaultModel");
|
|
3602
|
+
return agent?.options?.provider ?? defaultModelService?.get?.()?.provider;
|
|
3603
|
+
}
|
|
3604
|
+
function shouldUseQoderSearch(mode, provider) {
|
|
3605
|
+
return mode === "always" || mode === "auto" && provider === "dsh-provider-qoder";
|
|
3606
|
+
}
|
|
3363
3607
|
var QoderSearchProvider = class {
|
|
3364
3608
|
id = QODER_SEARCH_PROVIDER_ID;
|
|
3365
3609
|
ctx;
|
|
3366
3610
|
resolveTransport;
|
|
3367
|
-
searchClient;
|
|
3368
3611
|
getWebSearchMode;
|
|
3369
3612
|
fallbackProvider;
|
|
3370
3613
|
constructor(options) {
|
|
3371
3614
|
this.ctx = options.ctx;
|
|
3372
3615
|
this.resolveTransport = options.resolveTransport;
|
|
3373
|
-
this.searchClient = options.searchClient;
|
|
3374
3616
|
this.getWebSearchMode = options.getWebSearchMode;
|
|
3375
3617
|
this.fallbackProvider = options.fallbackProvider;
|
|
3376
3618
|
}
|
|
@@ -3380,13 +3622,15 @@ var QoderSearchProvider = class {
|
|
|
3380
3622
|
async search(request, signal) {
|
|
3381
3623
|
const mode = this.getWebSearchMode();
|
|
3382
3624
|
if (mode === "disabled") throw new WebError("Qoder web search is disabled in settings.", "WEB_PROVIDER_UNAVAILABLE");
|
|
3383
|
-
const
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3625
|
+
const providerRoute = initiatingModelProvider(this.ctx);
|
|
3626
|
+
if (shouldUseQoderSearch(mode, providerRoute)) {
|
|
3627
|
+
const result = await this.resolveTransport().searchWeb(request, signal);
|
|
3628
|
+
if (request.maxResults === void 0 || result.sources.length <= request.maxResults) return result;
|
|
3629
|
+
return {
|
|
3630
|
+
...result,
|
|
3631
|
+
sources: result.sources.slice(0, request.maxResults),
|
|
3632
|
+
truncated: true
|
|
3633
|
+
};
|
|
3390
3634
|
}
|
|
3391
3635
|
const fallback = this.resolveFallback();
|
|
3392
3636
|
if (fallback && fallback.available()) return fallback.search(request, signal);
|
|
@@ -3451,7 +3695,9 @@ const qoderRpcApiPath = `/api/qoder-subscription`;
|
|
|
3451
3695
|
const qoderRpcEndpoints = [
|
|
3452
3696
|
"account",
|
|
3453
3697
|
"models",
|
|
3454
|
-
"sessionTier"
|
|
3698
|
+
"sessionTier",
|
|
3699
|
+
"readSessionTier",
|
|
3700
|
+
"sessionTierEvents"
|
|
3455
3701
|
];
|
|
3456
3702
|
/** Absolute Fetch-route path of one endpoint. */
|
|
3457
3703
|
function qoderRpcPath(endpoint) {
|
|
@@ -3474,16 +3720,18 @@ function isQoderRpcEndpoint(value) {
|
|
|
3474
3720
|
* @returns disposer removing every registered route.
|
|
3475
3721
|
* @throws when Connection exposes no exact Fetch registry.
|
|
3476
3722
|
*/
|
|
3477
|
-
function registerQoderRpc(ctx, handler) {
|
|
3723
|
+
function registerQoderRpc(ctx, handler, subscribeTiers) {
|
|
3478
3724
|
const registry = ctx.connection?.fetch;
|
|
3479
3725
|
if (typeof registry?.register !== "function") throw new Error("provider-qoder: connection exposes no exact Fetch route registry");
|
|
3726
|
+
const streams = /* @__PURE__ */ new Set();
|
|
3480
3727
|
const disposers = qoderRpcEndpoints.map((endpoint) => registry.register({
|
|
3481
3728
|
path: qoderRpcPath(endpoint),
|
|
3482
3729
|
methods: ["POST"],
|
|
3483
3730
|
requestBody: "buffered",
|
|
3484
|
-
fetch: (request) => handleQoderRpcRequest(endpoint, handler, request)
|
|
3731
|
+
fetch: (request) => endpoint === "sessionTierEvents" && subscribeTiers ? Promise.resolve(sessionTierEvents(request, subscribeTiers, streams)) : handleQoderRpcRequest(endpoint, handler, request)
|
|
3485
3732
|
}));
|
|
3486
3733
|
return () => {
|
|
3734
|
+
for (const close of streams) close();
|
|
3487
3735
|
for (const dispose of disposers) dispose();
|
|
3488
3736
|
};
|
|
3489
3737
|
}
|
|
@@ -3523,6 +3771,43 @@ async function handleQoderRpcRequest(endpoint, handler, request) {
|
|
|
3523
3771
|
return new Response(`handler failure: ${String(error)}`, { status: 500 });
|
|
3524
3772
|
}
|
|
3525
3773
|
}
|
|
3774
|
+
/** Authenticated change stream; reconnecting clients reread on the initial ready item. */
|
|
3775
|
+
function sessionTierEvents(request, subscribe, streams) {
|
|
3776
|
+
let close = () => {};
|
|
3777
|
+
const encoder = new TextEncoder();
|
|
3778
|
+
const body = new ReadableStream({
|
|
3779
|
+
start(controller) {
|
|
3780
|
+
let ended = false;
|
|
3781
|
+
let unsubscribe = () => {};
|
|
3782
|
+
const send = (value) => {
|
|
3783
|
+
if (!ended) controller.enqueue(encoder.encode(JSON.stringify(value) + "\n"));
|
|
3784
|
+
};
|
|
3785
|
+
close = () => {
|
|
3786
|
+
if (ended) return;
|
|
3787
|
+
ended = true;
|
|
3788
|
+
unsubscribe();
|
|
3789
|
+
request.signal.removeEventListener("abort", close);
|
|
3790
|
+
streams.delete(close);
|
|
3791
|
+
try {
|
|
3792
|
+
controller.close();
|
|
3793
|
+
} catch {}
|
|
3794
|
+
};
|
|
3795
|
+
unsubscribe = subscribe((scope) => send(scope));
|
|
3796
|
+
streams.add(close);
|
|
3797
|
+
request.signal.addEventListener("abort", close, { once: true });
|
|
3798
|
+
if (request.signal.aborted) close();
|
|
3799
|
+
else send({ ready: true });
|
|
3800
|
+
},
|
|
3801
|
+
cancel() {
|
|
3802
|
+
close();
|
|
3803
|
+
}
|
|
3804
|
+
});
|
|
3805
|
+
return new Response(body, { headers: {
|
|
3806
|
+
"content-type": "application/x-ndjson",
|
|
3807
|
+
"cache-control": "no-store",
|
|
3808
|
+
"x-accel-buffering": "no"
|
|
3809
|
+
} });
|
|
3810
|
+
}
|
|
3526
3811
|
//#endregion
|
|
3527
3812
|
//#region src/dsh/plugin.ts
|
|
3528
3813
|
const name = "provider-qoder";
|
|
@@ -3554,6 +3839,26 @@ function publicError(code, message, details = { issues: [] }) {
|
|
|
3554
3839
|
}
|
|
3555
3840
|
};
|
|
3556
3841
|
}
|
|
3842
|
+
async function executeRpc(operation, task, signal, logger) {
|
|
3843
|
+
try {
|
|
3844
|
+
return {
|
|
3845
|
+
ok: true,
|
|
3846
|
+
value: await task()
|
|
3847
|
+
};
|
|
3848
|
+
} catch (error) {
|
|
3849
|
+
if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") {
|
|
3850
|
+
logger?.debug?.(`[Qoder RPC] ${operation} was aborted`);
|
|
3851
|
+
return publicError("ABORTED", "Request aborted");
|
|
3852
|
+
}
|
|
3853
|
+
let code = "INTERNAL";
|
|
3854
|
+
if (error instanceof QoderLlmError) if (error.code === "MISSING_CREDENTIAL" || error.code === "NO_CREDENTIALS") code = "NO_CREDENTIALS";
|
|
3855
|
+
else if (error.code === "AUTH") code = "UNAUTHENTICATED";
|
|
3856
|
+
else if (error.code === "TIMEOUT") code = "TIMEOUT";
|
|
3857
|
+
else code = "UPSTREAM_ERROR";
|
|
3858
|
+
logger?.error?.(`[Qoder RPC] Failed to ${operation}`, logError(error));
|
|
3859
|
+
return publicError(code, error instanceof Error ? error.message : `Failed to ${operation}`);
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3557
3862
|
function apply(ctx, input = {}) {
|
|
3558
3863
|
const config = readConfig(input);
|
|
3559
3864
|
const logger = ctx.logger;
|
|
@@ -3566,11 +3871,6 @@ function apply(ctx, input = {}) {
|
|
|
3566
3871
|
webSearchMode: config.webSearchMode ?? "auto"
|
|
3567
3872
|
};
|
|
3568
3873
|
let current = () => baseConfig;
|
|
3569
|
-
let persistDiscoveredModels = async (_region) => {};
|
|
3570
|
-
const discoveredCatalogs = {
|
|
3571
|
-
global: [],
|
|
3572
|
-
china: []
|
|
3573
|
-
};
|
|
3574
3874
|
const createTransport = (region, streamIdleTimeoutMs, responseHeaderTimeoutMs, preserveThinking, resolvePat = () => resolveManagedQoderPat(ctx.credentials)) => createQoderTransport({
|
|
3575
3875
|
region,
|
|
3576
3876
|
resolvePat,
|
|
@@ -3585,7 +3885,7 @@ function apply(ctx, input = {}) {
|
|
|
3585
3885
|
const region = value.region ?? "global";
|
|
3586
3886
|
return {
|
|
3587
3887
|
region,
|
|
3588
|
-
models:
|
|
3888
|
+
models: modelsFor(value, region),
|
|
3589
3889
|
streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
|
|
3590
3890
|
responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
|
|
3591
3891
|
preserveThinking: value.preserveThinking ?? true,
|
|
@@ -3600,23 +3900,22 @@ function apply(ctx, input = {}) {
|
|
|
3600
3900
|
responseHeaderTimeoutMs: initial.responseHeaderTimeoutMs,
|
|
3601
3901
|
preserveThinking: initial.preserveThinking
|
|
3602
3902
|
};
|
|
3903
|
+
const catalog = new QoderCatalogLifecycle({
|
|
3904
|
+
resolveTransport: () => activeTransport,
|
|
3905
|
+
region: () => activeTransportConfig.region,
|
|
3906
|
+
configuredModels: (region) => modelsFor(current(), region),
|
|
3907
|
+
onDiscovered: () => registration.replace([providerQoder]),
|
|
3908
|
+
onPersistenceError: (error) => logger?.error?.("[Qoder Settings] Failed to synchronize model catalog", logError(error))
|
|
3909
|
+
});
|
|
3910
|
+
ctx.effect(() => () => catalog.dispose(), "provider-qoder: catalog lifecycle");
|
|
3603
3911
|
const adapter = new QoderAdapter({
|
|
3604
3912
|
resolveTransport: () => activeTransport,
|
|
3605
|
-
|
|
3913
|
+
catalog,
|
|
3606
3914
|
region: () => activeTransportConfig.region,
|
|
3607
3915
|
providerId: providerQoder,
|
|
3608
3916
|
providerName: "Qoder",
|
|
3609
3917
|
sessions: ctx.get("sessions"),
|
|
3610
|
-
agents: ctx.get("agents")
|
|
3611
|
-
onModelsDiscovered: (transport, models) => {
|
|
3612
|
-
if (transport !== activeTransport || ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
3613
|
-
const region = activeTransportConfig.region;
|
|
3614
|
-
discoveredCatalogs[region] = models;
|
|
3615
|
-
refreshAdapter();
|
|
3616
|
-
persistDiscoveredModels(region).catch((error) => {
|
|
3617
|
-
logger?.error?.("[Qoder Settings] Failed to synchronize model catalog", logError(error));
|
|
3618
|
-
});
|
|
3619
|
-
}
|
|
3918
|
+
agents: ctx.get("agents")
|
|
3620
3919
|
});
|
|
3621
3920
|
const registration = ctx.llm.registerAdapter([providerQoder], adapter);
|
|
3622
3921
|
const refreshAdapter = () => {
|
|
@@ -3630,6 +3929,7 @@ function apply(ctx, input = {}) {
|
|
|
3630
3929
|
preserveThinking: next.preserveThinking
|
|
3631
3930
|
};
|
|
3632
3931
|
}
|
|
3932
|
+
catalog.observeTransport();
|
|
3633
3933
|
adapter.replaceModels(next.models);
|
|
3634
3934
|
registration.replace([providerQoder]);
|
|
3635
3935
|
};
|
|
@@ -3640,52 +3940,35 @@ function apply(ctx, input = {}) {
|
|
|
3640
3940
|
modelsFor(value, "china");
|
|
3641
3941
|
} });
|
|
3642
3942
|
current = () => scope.get();
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
if (!settingsCtx.settings.writable || discoveredCatalogs[region].length === 0) return;
|
|
3943
|
+
catalog.bindSettings({
|
|
3944
|
+
read: (region) => {
|
|
3945
|
+
if (!settingsCtx.settings.writable) return void 0;
|
|
3647
3946
|
const snapshot = settingsCtx.settings.describe().find((section) => section.ns === namespace);
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
} catch (error) {
|
|
3656
|
-
if (!(error instanceof SettingsConflictError)) throw error;
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
3659
|
-
};
|
|
3660
|
-
persistDiscoveredModels = persistCatalog;
|
|
3947
|
+
return snapshot === void 0 ? void 0 : {
|
|
3948
|
+
models: modelsFor(snapshot.value, region),
|
|
3949
|
+
revision: snapshot.revision
|
|
3950
|
+
};
|
|
3951
|
+
},
|
|
3952
|
+
write: (region, models, revision) => settingsCtx.settings.update(namespace, { modelsByRegion: { [region]: models } }, revision)
|
|
3953
|
+
});
|
|
3661
3954
|
refreshAdapter();
|
|
3662
|
-
scope.watch(
|
|
3663
|
-
if (
|
|
3955
|
+
scope.watch(() => {
|
|
3956
|
+
if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
3664
3957
|
refreshAdapter();
|
|
3665
|
-
|
|
3958
|
+
catalog.retryPersistence();
|
|
3666
3959
|
});
|
|
3667
|
-
for (const region of ["global", "china"]) {
|
|
3668
|
-
if (discoveredCatalogs[region].length === 0) continue;
|
|
3669
|
-
persistCatalog(region).catch((error) => logger?.error?.("[Qoder Settings] Failed to synchronize model catalog", logError(error)));
|
|
3670
|
-
}
|
|
3671
3960
|
settingsCtx.effect(() => () => {
|
|
3672
|
-
|
|
3961
|
+
catalog.bindSettings(void 0);
|
|
3673
3962
|
if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
|
|
3674
|
-
persistDiscoveredModels = async () => {};
|
|
3675
3963
|
current = () => baseConfig;
|
|
3676
3964
|
refreshAdapter();
|
|
3677
3965
|
});
|
|
3678
3966
|
});
|
|
3679
3967
|
const discoverModels = async (signal, suppliedPat) => {
|
|
3680
3968
|
const snapshot = resolveConfig();
|
|
3681
|
-
const snapshotTransport = activeTransport;
|
|
3682
3969
|
const normalizedPat = suppliedPat?.trim();
|
|
3683
|
-
const
|
|
3684
|
-
|
|
3685
|
-
discoveredCatalogs[snapshot.region] = models;
|
|
3686
|
-
refreshAdapter();
|
|
3687
|
-
await persistDiscoveredModels(snapshot.region);
|
|
3688
|
-
return models;
|
|
3970
|
+
const transport = normalizedPat ? createTransport(snapshot.region, snapshot.streamIdleTimeoutMs, snapshot.responseHeaderTimeoutMs, snapshot.preserveThinking, () => Promise.resolve(normalizedPat)) : activeTransport;
|
|
3971
|
+
return catalog.discover(signal, transport);
|
|
3689
3972
|
};
|
|
3690
3973
|
ctx.llm.registerModelDiscovery(settingsNamespace, async (request, signal) => {
|
|
3691
3974
|
if (request.provider !== void 0 && request.provider !== providerQoder) throw new QoderLlmError(`Qoder discovery does not own provider "${request.provider}".`, "INVALID_PROVIDER");
|
|
@@ -3699,35 +3982,19 @@ function apply(ctx, input = {}) {
|
|
|
3699
3982
|
const handler = async (endpoint, payload, signal) => {
|
|
3700
3983
|
if (!isQoderRpcEndpoint(endpoint)) return publicError("UNKNOWN_ENDPOINT", `Unknown endpoint: ${endpoint}`);
|
|
3701
3984
|
if (signal.aborted) return publicError("ABORTED", "Request aborted");
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
return {
|
|
3705
|
-
ok: true,
|
|
3706
|
-
value: await task()
|
|
3707
|
-
};
|
|
3708
|
-
} catch (error) {
|
|
3709
|
-
if (signal.aborted || error instanceof QoderLlmError && error.code === "ABORTED") {
|
|
3710
|
-
logger?.debug?.(`[Qoder RPC] ${operation} was aborted`);
|
|
3711
|
-
return publicError("ABORTED", "Request aborted");
|
|
3712
|
-
}
|
|
3713
|
-
let code = "INTERNAL";
|
|
3714
|
-
if (error instanceof QoderLlmError) if (error.code === "MISSING_CREDENTIAL" || error.code === "NO_CREDENTIALS") code = "NO_CREDENTIALS";
|
|
3715
|
-
else if (error.code === "AUTH") code = "UNAUTHENTICATED";
|
|
3716
|
-
else if (error.code === "TIMEOUT") code = "TIMEOUT";
|
|
3717
|
-
else code = "UPSTREAM_ERROR";
|
|
3718
|
-
logger?.error?.(`[Qoder RPC] Failed to ${operation}`, logError(error));
|
|
3719
|
-
return publicError(code, error instanceof Error ? error.message : `Failed to ${operation}`);
|
|
3720
|
-
}
|
|
3721
|
-
};
|
|
3722
|
-
if (endpoint === "models") return await executeRpc("discover Qoder models", () => discoverModels(signal));
|
|
3723
|
-
if (endpoint === "sessionTier") {
|
|
3985
|
+
if (endpoint === "models") return await executeRpc("discover Qoder models", () => discoverModels(signal), signal, logger);
|
|
3986
|
+
if (endpoint === "sessionTier" || endpoint === "readSessionTier") {
|
|
3724
3987
|
const data = payload;
|
|
3725
|
-
if (typeof data?.sessionId === "string" && typeof data?.modelId === "string" && typeof data?.tierKey === "string" && (data.region === "global" || data.region === "china")) {
|
|
3988
|
+
if (typeof data?.sessionId === "string" && typeof data?.modelId === "string" && (endpoint === "readSessionTier" || typeof data?.tierKey === "string") && (data.region === "global" || data.region === "china")) {
|
|
3726
3989
|
const { sessionId, modelId, tierKey, region } = data;
|
|
3727
|
-
return await executeRpc("select session context tier", async () => {
|
|
3728
|
-
adapter.setSessionTier(sessionId, modelId, tierKey, region);
|
|
3729
|
-
return {
|
|
3730
|
-
|
|
3990
|
+
return await executeRpc(endpoint === "sessionTier" ? "select session context tier" : "read session context tier", async () => {
|
|
3991
|
+
if (endpoint === "sessionTier") adapter.setSessionTier(sessionId, modelId, tierKey, region);
|
|
3992
|
+
return adapter.readSessionTier({
|
|
3993
|
+
sessionId,
|
|
3994
|
+
modelId,
|
|
3995
|
+
region
|
|
3996
|
+
});
|
|
3997
|
+
}, signal, logger);
|
|
3731
3998
|
}
|
|
3732
3999
|
return publicError("INTERNAL", "Invalid sessionTier payload");
|
|
3733
4000
|
}
|
|
@@ -3736,12 +4003,12 @@ function apply(ctx, input = {}) {
|
|
|
3736
4003
|
const outcome = await executeRpc("load Qoder account", () => activeTransport.readAccount({
|
|
3737
4004
|
force,
|
|
3738
4005
|
signal
|
|
3739
|
-
}));
|
|
4006
|
+
}), signal, logger);
|
|
3740
4007
|
if (outcome.ok) logger?.debug?.("[Qoder RPC] Subscriber account resolved");
|
|
3741
4008
|
return outcome;
|
|
3742
4009
|
};
|
|
3743
4010
|
try {
|
|
3744
|
-
ctx.effect(() => registerQoderRpc(ctx, handler), "provider-qoder: settings RPC routes");
|
|
4011
|
+
ctx.effect(() => registerQoderRpc(ctx, handler, (listener) => adapter.subscribeSessionTiers(listener)), "provider-qoder: settings RPC routes");
|
|
3745
4012
|
} catch (error) {
|
|
3746
4013
|
logger?.error?.("[Qoder RPC] Failed to register the settings RPC routes", logError(error));
|
|
3747
4014
|
}
|
|
@@ -3753,19 +4020,15 @@ function apply(ctx, input = {}) {
|
|
|
3753
4020
|
});
|
|
3754
4021
|
webCtx.web.registerSearchProvider(searchProvider);
|
|
3755
4022
|
if (typeof webCtx.web.search === "function") {
|
|
3756
|
-
const originalSearch = webCtx.web.search
|
|
4023
|
+
const originalSearch = webCtx.web.search;
|
|
3757
4024
|
webCtx.effect(() => {
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
const agent = (ctx.get("agents") ?? ctx.agents)?.currentInitiator?.();
|
|
3762
|
-
const defaultModelService = ctx.get("agentDefaultModel");
|
|
3763
|
-
const isQoderActive = (agent?.options?.provider ?? defaultModelService?.get?.()?.provider) === providerQoder;
|
|
3764
|
-
if (mode === "always" || isQoderActive) return searchProvider.search(request, signal);
|
|
3765
|
-
return originalSearch(request, signal);
|
|
4025
|
+
const routedSearch = async (request, signal) => {
|
|
4026
|
+
if (shouldUseQoderSearch(resolveConfig().webSearchMode ?? "auto", initiatingModelProvider(ctx))) return searchProvider.search(request, signal);
|
|
4027
|
+
return originalSearch.call(webCtx.web, request, signal);
|
|
3766
4028
|
};
|
|
4029
|
+
webCtx.web.search = routedSearch;
|
|
3767
4030
|
return () => {
|
|
3768
|
-
webCtx.web.search = originalSearch;
|
|
4031
|
+
if (webCtx.web.search === routedSearch) webCtx.web.search = originalSearch;
|
|
3769
4032
|
};
|
|
3770
4033
|
}, "provider-qoder: transparent web search router");
|
|
3771
4034
|
}
|