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/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
- ...tokenCount === void 0 ? {} : { tokenCount },
46
- ...isDefault === void 0 ? {} : { 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 = typeof raw.price_factor === "number" && Number.isFinite(raw.price_factor) && raw.price_factor >= 0 ? raw.price_factor : typeof raw.priceFactor === "number" && Number.isFinite(raw.priceFactor) && raw.priceFactor >= 0 ? raw.priceFactor : void 0;
100
- const rawOriginalPrice = typeof raw.original_price_factor === "number" && Number.isFinite(raw.original_price_factor) && raw.original_price_factor >= 0 ? raw.original_price_factor : typeof raw.originPriceFactor === "number" && Number.isFinite(raw.originPriceFactor) && raw.originPriceFactor >= 0 ? raw.originPriceFactor : void 0;
101
- const promotionPrice = typeof raw.promotion === "object" && raw.promotion !== null && raw.promotion.active === true && typeof raw.promotion.before_promotion_price_factor === "number" ? raw.promotion.before_promotion_price_factor : void 0;
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) delete merged[key];
200
- for (const key of discoveredMetadataKeys) if (advertised[key] !== void 0) {
201
- const val = advertised[key];
202
- Object.assign(merged, { [key]: key === "contextOptions" && typeof val === "object" && val !== null ? { ...val } : val });
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
- function defaultMachineIdPaths() {
371
- return [join(homedir(), ".qoder", ".auth", "machine_id"), join(homedir(), ".dsh", "qoder", "machine_id")];
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 = defaultMachineIdPaths()) {
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") return redactString(value);
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.60";
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
- name: creds.name || "",
503
- aid: "",
504
- email: creds.email || ""
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-Clientip": "127.0.0.1",
535
- "Cosy-Bodyhash": bodyHash,
536
- "Cosy-Bodylength": bodyLen,
537
- "Cosy-Sigpath": sigPath,
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": "1.0.1",
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.resolveMachineId = options.resolveMachineId ?? getMachineId;
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
- let jobToken;
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: { personal_token: pat },
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
- if (!data.token) throw new QoderLlmError("Qoder PAT exchange returned no job token.", "AUTH");
798
- jobToken = data.token;
799
- if (data.expires_at) {
800
- const parsed = Date.parse(data.expires_at);
801
- if (!Number.isNaN(parsed)) expiresAt = parsed;
802
- } else if (typeof data.expires_in === "number" && data.expires_in > 0) expiresAt = Date.now() + data.expires_in;
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: this.resolveMachineId()
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
- if (!info.id) throw new QoderLlmError("Qoder identity lookup returned no user id.", "AUTH");
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: info.id,
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
- function toolResultText(block) {
889
- let text = "";
890
- for (const nested of block.content) {
891
- if (nested.type === "image") continue;
892
- if (nested.type !== "text") throw unsupported(`Qoder tool results support text only; received nested ${String(nested.type)} content.`);
893
- text += nested.text;
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
- content: toolResultText(result)
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" && message.role !== "tool" && toolResults.length === 0) throw unsupported("Qoder image content is valid only in user messages.");
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
- * Text this transport synthesizes while translating a turn's own tool results.
1397
+ * Compatibility with image markers synthesized by older transport versions.
1156
1398
  *
1157
- * A tool result carrying images becomes a user-role message, so it must be
1158
- * recognized as part of the open record rather than as new subscriber input.
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: "1.0.60",
1746
+ version: qoderIdeVersion,
1496
1747
  type: "agent",
1497
- stage: "start",
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, creds.machineID, signal)
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, machineId, signal) {
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, machineId, signal) {
2370
+ async safeFetchStatus(jobToken, signal) {
2115
2371
  try {
2116
- return await this.fetchStatus(jobToken, machineId, signal);
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
- function qoderEncodeBody(plaintext) {
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 rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
2141
- let out = "";
2142
- for (let i = 0; i < n; i++) {
2143
- const c = rearranged[i];
2144
- if (c === "=") out += "$";
2145
- else {
2146
- const idx = qoderStdAlphabet.indexOf(c);
2147
- if (idx >= 0) out += qoderCustomAlphabet[idx];
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 extractErrorDetail(envelope) {
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
- const detail = extractErrorDetail(envelope);
2524
- throw qoderHttpError(detail ? `Qoder service returned upstream error status ${envelope.statusCodeValue}: ${detail}` : `Qoder service returned upstream error status ${envelope.statusCodeValue}.`, {
2525
- status: envelope.statusCodeValue,
2526
- cause: envelope.body
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 segment of thinkingParser.pushReasoning(delta.reasoning_content)) for (const chunk of appendSegment(segment)) yield chunk;
2697
- if (typeof delta.content === "string" && delta.content) for (const segment of thinkingParser.pushContent(delta.content)) for (const chunk of appendSegment(segment)) yield chunk;
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
- const encodedBody = qoderEncodeBody(JSON.stringify(request));
2825
- const encodedBytes = Buffer.from(encodedBody, "utf8");
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
- const headerTimer = setTimeout(() => {
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
- const response = await dependencies.fetch(chatUrl, {
2861
- method: "POST",
2862
- headers: {
2863
- "content-type": "application/json",
2864
- "accept": "text/event-stream",
2865
- "cache-control": "no-cache",
2866
- "accept-encoding": "identity",
2867
- "x-model-key": options.model || "cmodel",
2868
- "x-model-source": model?.source || "system",
2869
- ...attributionHeaders(),
2870
- ...buildAuthHeaders(encodedBytes, chatUrl, credentials)
2871
- },
2872
- body: encodedBytes,
2873
- signal: requestController.signal
2874
- });
2875
- clearTimeout(headerTimer);
2876
- reqId = qoderRequestId(response.headers);
2877
- dependencies.logger?.debug?.("[Qoder Stream] Response headers received", {
2878
- region: dependencies.region,
2879
- status: response.status,
2880
- durationMs: Math.round(performance.now() - startedAt),
2881
- ...reqId === void 0 ? {} : { requestId: reqId }
2882
- });
2883
- resetIdleTimer();
2884
- if (!response.ok) {
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.text()).trim();
2888
- } catch {}
2889
- let detail = "";
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
- throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}${detail}.`, {
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, { onActivity: resetIdleTimer })) {
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
- onModelsDiscovered;
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.sessionTiers.get(JSON.stringify([
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
- const transport = this.resolveTransport();
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 mergeQoderDiscoveryMetadata(this.catalogModels, this.discoveries.get(this.resolveTransport())?.models ?? []);
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 agent = (this.ctx.get("agents") ?? this.ctx.agents)?.currentInitiator?.();
3384
- const defaultModelService = this.ctx.get("agentDefaultModel");
3385
- const providerRoute = agent?.options?.provider ?? defaultModelService?.get?.()?.provider;
3386
- if (mode === "always" || providerRoute === "dsh-provider-qoder") {
3387
- if (this.resolveTransport !== void 0) return this.resolveTransport().searchWeb(request, signal);
3388
- if (this.searchClient !== void 0) return this.searchClient.search(request, signal);
3389
- throw new WebError("No Qoder transport configured for web search.", "WEB_PROVIDER_ERROR");
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: mergeQoderDiscoveryMetadata(modelsFor(value, region), discoveredCatalogs[region]),
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
- models: initial.models,
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
- let bindingActive = true;
3644
- const persistCatalog = async (region) => {
3645
- while (bindingActive && ctx.fiber.state !== fiberUnloading && ctx.fiber.state !== fiberDisposed) {
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
- if (snapshot === void 0) return;
3649
- const selected = modelsFor(snapshot.value, region);
3650
- const enriched = modelsFor(Config({ modelsByRegion: { [region]: mergeQoderDiscoveryMetadata(selected, discoveredCatalogs[region]) } }), region);
3651
- if (hasSameQoderDiscoveryMetadata(selected, enriched)) return;
3652
- try {
3653
- await settingsCtx.settings.update(namespace, { modelsByRegion: { [region]: enriched } }, snapshot.revision);
3654
- return;
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(async () => {
3663
- if (!bindingActive || ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
3955
+ scope.watch(() => {
3956
+ if (ctx.fiber.state === fiberUnloading || ctx.fiber.state === fiberDisposed) return;
3664
3957
  refreshAdapter();
3665
- await persistCatalog(scope.get().region ?? "global");
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
- bindingActive = false;
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 models = await (normalizedPat ? createTransport(snapshot.region, snapshot.streamIdleTimeoutMs, snapshot.responseHeaderTimeoutMs, snapshot.preserveThinking, () => Promise.resolve(normalizedPat)) : snapshotTransport).discoverModels(signal);
3684
- adapter.updateDiscoveredModels(snapshotTransport, models);
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
- const executeRpc = async (operation, task) => {
3703
- try {
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 { success: true };
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.bind(webCtx.web);
4023
+ const originalSearch = webCtx.web.search;
3757
4024
  webCtx.effect(() => {
3758
- webCtx.web.search = async (request, signal) => {
3759
- const mode = resolveConfig().webSearchMode ?? "auto";
3760
- if (mode === "disabled") return originalSearch(request, signal);
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
  }