dsh-provider-qoder 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/client.js +6 -2
  2. package/lib/index.js +215 -121
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -672,10 +672,13 @@ window.__ModuleLoader__.load({
672
672
  const followsConfig = this.snapshot.catalog === this.confirmed;
673
673
  this.confirmed = models.map(cloneCatalogModel);
674
674
  const selected = new Map(this.confirmed.map((model) => [model.id, model]));
675
+ const catalog = new Map(this.snapshot.catalog.map((model) => [model.id, selected.get(model.id) ?? model]));
676
+ for (const model of this.confirmed) catalog.set(model.id, model);
675
677
  this.publish({
676
678
  ...this.snapshot,
677
679
  models: this.confirmed,
678
- catalog: followsConfig ? this.confirmed : this.snapshot.catalog.map((model) => selected.get(model.id) ?? model)
680
+ catalog: followsConfig ? this.confirmed : [...catalog.values()],
681
+ unavailableIds: new Set([...this.snapshot.unavailableIds].filter((id) => !selected.has(id)))
679
682
  });
680
683
  }
681
684
  activate() {
@@ -2500,7 +2503,8 @@ window.__ModuleLoader__.load({
2500
2503
  if (response.ok) {
2501
2504
  const region = regionOf(readModelSnapshot().value);
2502
2505
  const current = await authorizationOperations.read(region);
2503
- if (current.ok && current.value.method !== "pat") return (await authorizationOperations.select(region, "pat")).ok;
2506
+ if (!current.ok) return false;
2507
+ if (current.value.method !== "pat") return (await authorizationOperations.select(region, "pat")).ok;
2504
2508
  }
2505
2509
  return response.ok;
2506
2510
  } catch {
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, ToolCallId, attributionHeaders, contentHasImage, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
2
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, ReasoningEffortId, ToolCallId, attributionHeaders, isContextWindowExceededError, isQuotaExceededError } from "@deepseek-ai/dsh-llm";
3
3
  import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { homedir, hostname } from "node:os";
@@ -1265,6 +1265,9 @@ async function fetchQoderModels(credentials, options = {}) {
1265
1265
  function unsupported(message) {
1266
1266
  return new QoderLlmError(message, "UNSUPPORTED_CONTENT");
1267
1267
  }
1268
+ function validateText(block) {
1269
+ if ((block.type === "text" || block.type === "reasoning") && typeof block.text !== "string") throw unsupported("Qoder text and reasoning content must contain a string text value.");
1270
+ }
1268
1271
  /** Keep multimodal tool results attached to their call. */
1269
1272
  async function translateToolResultContent(blocks, context) {
1270
1273
  const supportedBlocks = blocks.map((block) => {
@@ -1347,7 +1350,49 @@ function consumeToolCallId(message, result, pendingIds, fallbackIndex) {
1347
1350
  }
1348
1351
  return pendingIds.shift() ?? `call_unknown_${fallbackIndex}`;
1349
1352
  }
1353
+ /** Validate the entire batch without reading attachments or resolving credentials. */
1354
+ function validateMessageShapes(messages, attachments) {
1355
+ let hasImages = false;
1356
+ const validateImages = (blocks) => {
1357
+ const images = blocks.filter((block) => block.type === "image");
1358
+ hasImages ||= images.length > 0;
1359
+ if (attachments) enforceImageLimits(images, attachments);
1360
+ };
1361
+ for (const message of messages) {
1362
+ if (![
1363
+ "system",
1364
+ "user",
1365
+ "assistant",
1366
+ "tool"
1367
+ ].includes(message.role) || !Array.isArray(message.content)) throw unsupported("Qoder received an unsupported message shape.");
1368
+ const results = message.content.filter((block) => block.type === "tool-result");
1369
+ for (const result of results) {
1370
+ if (!Array.isArray(result.content) || result.content.some((block) => block.type !== "text" && block.type !== "image")) throw unsupported("Qoder tool results support text and images only.");
1371
+ for (const block of result.content) validateText(block);
1372
+ validateImages(result.content);
1373
+ }
1374
+ const blocks = message.content.filter((block) => block.type !== "tool-result");
1375
+ for (const block of blocks) {
1376
+ validateText(block);
1377
+ if (block.type === "tool-call") {
1378
+ if (message.role !== "assistant") throw unsupported("Qoder tool-call content is valid only in assistant messages.");
1379
+ if (asCallId(block.id) === void 0 || typeof block.name !== "string" || block.name.trim().length === 0 || typeof block.arguments !== "string") throw unsupported("Qoder tool-call content requires a non-empty string id and name, and string arguments.");
1380
+ }
1381
+ if (message.role === "tool" && block.type !== "text" && block.type !== "image") throw unsupported("Qoder tool results support text and images only.");
1382
+ if (block.type === "image" && message.role !== "user" && message.role !== "tool") throw unsupported("Qoder image content is valid only in user and tool messages.");
1383
+ if (![
1384
+ "text",
1385
+ "image",
1386
+ "reasoning",
1387
+ "tool-call"
1388
+ ].includes(block.type)) throw unsupported(`Qoder does not support ${String(block.type)} content.`);
1389
+ }
1390
+ validateImages(blocks);
1391
+ }
1392
+ return hasImages;
1393
+ }
1350
1394
  async function validateAndTranslateMessages(messages, systemPrompt, attachments, signal, pipeline) {
1395
+ validateMessageShapes(messages, attachments);
1351
1396
  const context = {
1352
1397
  attachments,
1353
1398
  signal,
@@ -1401,7 +1446,7 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
1401
1446
  continue;
1402
1447
  }
1403
1448
  if (block.type === "image") {
1404
- if (message.role !== "user" && toolResults.length === 0) throw unsupported("Qoder image content is valid only in user messages.");
1449
+ if (message.role !== "user") throw unsupported("Qoder image content is valid only in user and tool messages.");
1405
1450
  hasImage = true;
1406
1451
  pendingImages.push({
1407
1452
  slot: userContent.length,
@@ -1464,20 +1509,15 @@ async function validateAndTranslateMessages(messages, systemPrompt, attachments,
1464
1509
  * request reports — so every step of one turn must carry the same run identity
1465
1510
  * while the next subscriber prompt opens a new one.
1466
1511
  *
1467
- * The adapter never sees a turn number, and the host appends messages to a
1468
- * turn's history while it runs — its own context, notifications, delegated
1469
- * agents' messages — so the boundary is derived from the history here. A turn is
1470
- * the multiset of user-role messages it has accounted for: a subscriber message
1471
- * the run has not seen opens the next turn, while host-appended context,
1472
- * transport-synthesized markers, and anything an auxiliary call carries continue
1473
- * the open record.
1512
+ * Hosted calls use a captured host run as their authoritative boundary. The
1513
+ * weakly owned identity survives history projections and standalone-cache eviction.
1514
+ * Standalone calls infer boundaries from the multiset of user-role messages:
1515
+ * unseen subscriber input opens a run; recognized host context continues it.
1516
+ * Auxiliary calls always receive independent identities.
1474
1517
  *
1475
- * Known limit: text identifies a message because the host's message ids are not
1476
- * part of the wire shape. A subscriber who sends the same words twice with the
1477
- * first copy already gone from history — compaction replaced that span — shares
1478
- * the open record instead of opening a new one. Carrying the host's message
1479
- * identity through translation is the fix, and needs the host to expose it on
1480
- * the request the adapter receives.
1518
+ * Standalone limit: text identifies a message because wire messages lack host
1519
+ * message IDs. Identical input replacing an earlier occurrence during compaction
1520
+ * can share the open record. Hosted calls use real boundaries instead.
1481
1521
  *
1482
1522
  * @module dsh-provider-qoder/qoder/transport/wire/turn-identity
1483
1523
  */
@@ -1566,14 +1606,14 @@ function messageFingerprint(message) {
1566
1606
  /**
1567
1607
  * Resolves the record identity of the turn one request belongs to.
1568
1608
  *
1569
- * One tracker instance owns its sessions, so tests and transports stay
1570
- * independent. State is created lazily per session and bounded by
1571
- * `maxSessions`.
1609
+ * Standalone session state is bounded by `maxSessions`. Hosted state is weakly
1610
+ * owned by its run handle, which the host and outstanding requests keep alive.
1572
1611
  */
1573
1612
  var QoderTurnTracker = class {
1574
1613
  maxSessions;
1575
1614
  idleSessionTtlMs;
1576
1615
  now;
1616
+ hosted = /* @__PURE__ */ new WeakMap();
1577
1617
  sessions = /* @__PURE__ */ new Map();
1578
1618
  constructor(options = {}) {
1579
1619
  this.maxSessions = options.maxSessions ?? defaultMaxSessions;
@@ -1587,7 +1627,7 @@ var QoderTurnTracker = class {
1587
1627
  * that requests within one subscriber turn share a single agent run and
1588
1628
  * consumption record.
1589
1629
  */
1590
- resolveTurnIdentity(sessionId, messages, kind = "conversation", requestId = crypto.randomUUID(), modelKey) {
1630
+ resolveTurnIdentity(sessionId, messages, kind = "conversation", requestId = crypto.randomUUID(), modelKey, hostRun) {
1591
1631
  const now = this.now();
1592
1632
  if (sessionId === void 0 || sessionId.length === 0) return {
1593
1633
  requestSetId: `qoder-request-${crypto.randomUUID()}`,
@@ -1606,6 +1646,7 @@ var QoderTurnTracker = class {
1606
1646
  businessStage: "start"
1607
1647
  };
1608
1648
  }
1649
+ if (hostRun !== void 0) return this.resolveHosted(hostRun, sessionId, requestId, modelKey, now);
1609
1650
  let state = this.sessions.get(sessionId);
1610
1651
  if (state === void 0) {
1611
1652
  state = {
@@ -1673,6 +1714,36 @@ var QoderTurnTracker = class {
1673
1714
  businessStage
1674
1715
  };
1675
1716
  }
1717
+ /** Host boundaries are authoritative; message projections cannot open a new run. */
1718
+ resolveHosted(run, scope, requestId, modelKey, now) {
1719
+ let scopes = this.hosted.get(run);
1720
+ if (scopes === void 0) {
1721
+ scopes = /* @__PURE__ */ new Map();
1722
+ this.hosted.set(run, scopes);
1723
+ }
1724
+ const previous = scopes.get(scope);
1725
+ const modelChanged = previous?.modelKey !== void 0 && modelKey !== void 0 && previous.modelKey !== modelKey;
1726
+ if (previous !== void 0 && !modelChanged) {
1727
+ if (modelKey !== void 0) previous.modelKey = modelKey;
1728
+ return {
1729
+ ...previous.identity,
1730
+ chatRecordId: requestId,
1731
+ businessStage: "processing"
1732
+ };
1733
+ }
1734
+ const identity = {
1735
+ requestSetId: `qoder-request-${crypto.randomUUID()}`,
1736
+ chatRecordId: requestId,
1737
+ businessId: crypto.randomUUID(),
1738
+ beginAt: now,
1739
+ businessStage: "start"
1740
+ };
1741
+ scopes.set(scope, {
1742
+ modelKey,
1743
+ identity
1744
+ });
1745
+ return identity;
1746
+ }
1676
1747
  /**
1677
1748
  * Record id shared by every step of `sessionId`'s current turn.
1678
1749
  *
@@ -1697,13 +1768,14 @@ var QoderTurnTracker = class {
1697
1768
  beginAt: identity.beginAt
1698
1769
  };
1699
1770
  }
1700
- /** Forget one session, e.g. when its transport is disposed. */
1771
+ /** Forget one standalone session. Hosted state follows its run handle lifetime. */
1701
1772
  forget(sessionId) {
1702
1773
  this.sessions.delete(sessionId);
1703
1774
  }
1704
1775
  /** Forget every session. */
1705
1776
  clear() {
1706
1777
  this.sessions.clear();
1778
+ this.hosted = /* @__PURE__ */ new WeakMap();
1707
1779
  }
1708
1780
  claimSession(sessionId, state) {
1709
1781
  this.sessions.set(sessionId, state);
@@ -1735,7 +1807,6 @@ var QoderTurnTracker = class {
1735
1807
  };
1736
1808
  //#endregion
1737
1809
  //#region src/qoder/transport/wire/prepare.ts
1738
- /** Shared model request preparation, before selecting a wire protocol. */
1739
1810
  function stableHash(prefix, ...inputs) {
1740
1811
  const hash = crypto.createHash("sha256");
1741
1812
  hash.update(prefix);
@@ -1750,17 +1821,18 @@ const qoderTurnTracker = new QoderTurnTracker();
1750
1821
  function callKind(options) {
1751
1822
  return options.purpose === void 0 ? "conversation" : "auxiliary";
1752
1823
  }
1753
- function validateQoderRequestShape(options, model) {
1824
+ function validateQoderRequestShape(options, model, attachments) {
1825
+ const hasImages = validateMessageShapes(options.messages, attachments);
1754
1826
  if (options.reasoningEffort !== void 0) {
1755
1827
  const effort = String(options.reasoningEffort);
1756
1828
  if (!model?.reasoningEfforts?.some((candidate) => candidate.id === effort)) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise reasoning effort "${effort}".`, "UNSUPPORTED_REASONING_EFFORT");
1757
1829
  }
1758
- if (options.messages.some((message) => contentHasImage(message.content)) && model?.supportsImages !== true) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise image input.`, "UNSUPPORTED_CONTENT");
1830
+ if (hasImages && model?.supportsImages !== true) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise image input.`, "UNSUPPORTED_CONTENT");
1759
1831
  }
1760
1832
  function translateQoderMessages(options, attachments, pipeline) {
1761
1833
  return validateAndTranslateMessages(options.messages, options.system, attachments, options.signal, pipeline);
1762
1834
  }
1763
- async function prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker = qoderTurnTracker, identityScope = "") {
1835
+ async function prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker = qoderTurnTracker, identityScope = "", hostRun) {
1764
1836
  if (!userId) throw new QoderLlmError("Qoder request identity is missing.", "AUTH");
1765
1837
  if (translatedMessages === void 0) validateQoderRequestShape(options, model);
1766
1838
  const messages = translatedMessages ?? await translateQoderMessages(options, attachments);
@@ -1788,7 +1860,7 @@ async function prepareQoderRequest(options, userId, translatedMessages, model, a
1788
1860
  identityScope,
1789
1861
  userId,
1790
1862
  sessionIdParam
1791
- ]), messages, callKind(options), requestId, modelKey);
1863
+ ]), messages, callKind(options), requestId, modelKey, hostRun);
1792
1864
  return {
1793
1865
  requestId,
1794
1866
  requestSetId: identity.requestSetId,
@@ -2335,7 +2407,19 @@ var QoderUsageReader = class {
2335
2407
  const cached = this.cache.get(cacheKey);
2336
2408
  if (cached && cached.expiresAt > Date.now()) return cached.info;
2337
2409
  }
2338
- return this.flights.run(cacheKey, options?.signal, (sharedSignal) => this.loadAccount(resolve, sharedSignal, cacheKey), () => new QoderLlmError("Qoder account request was aborted.", "ABORTED"));
2410
+ return this.flights.run(cacheKey, options?.signal, async (sharedSignal) => {
2411
+ let credentials;
2412
+ try {
2413
+ return await this.loadAccount(async (signal) => {
2414
+ credentials = await resolve(signal);
2415
+ return credentials;
2416
+ }, sharedSignal, cacheKey);
2417
+ } catch (error) {
2418
+ if (!(error instanceof QoderLlmError) || error.code !== "AUTH" || !credentials || !options?.refresh) throw error;
2419
+ const refreshed = await options.refresh(credentials, sharedSignal);
2420
+ return this.loadAccount(async () => refreshed, sharedSignal, cacheKey);
2421
+ }
2422
+ }, () => new QoderLlmError("Qoder account request was aborted.", "ABORTED"));
2339
2423
  }
2340
2424
  async loadAccount(resolve, signal, cacheKey) {
2341
2425
  const creds = await resolve(signal);
@@ -3192,8 +3276,8 @@ var ModelQueue = class {
3192
3276
  };
3193
3277
  //#endregion
3194
3278
  //#region src/qoder/transport/wire/serialize.ts
3195
- async function buildQoderRequestBody(options, userId, translatedMessages, model, attachments, tracker, identityScope = "") {
3196
- const prepared = await prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker, identityScope);
3279
+ async function buildQoderRequestBody(options, userId, translatedMessages, model, attachments, tracker, identityScope = "", hostRun) {
3280
+ const prepared = await prepareQoderRequest(options, userId, translatedMessages, model, attachments, tracker, identityScope, hostRun);
3197
3281
  return {
3198
3282
  request_id: prepared.requestId,
3199
3283
  request_set_id: prepared.requestSetId,
@@ -3391,14 +3475,18 @@ async function* parseQoderChunks(stream, decodeFrame, options = {}, behavior = {
3391
3475
  } else {
3392
3476
  options.onActivity?.();
3393
3477
  buffer += decoder.decode(value, { stream: true });
3394
- if (buffer.length > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
3395
3478
  }
3396
3479
  while (!sawDone) {
3397
3480
  const lineEnd = buffer.indexOf("\n");
3398
- if (lineEnd === -1) break;
3481
+ if (lineEnd === -1) {
3482
+ const pending = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
3483
+ if ((pending.startsWith("data:") ? eventDataChars + pending.slice(pending.startsWith("data: ") ? 6 : 5).length + 1 : pending.length) > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
3484
+ break;
3485
+ }
3399
3486
  let line = buffer.substring(0, lineEnd);
3400
3487
  buffer = buffer.substring(lineEnd + 1);
3401
3488
  if (line.endsWith("\r")) line = line.slice(0, -1);
3489
+ if (!line.startsWith("data:") && line.length > maxBufferChars) throw malformed$1("Qoder SSE frame exceeded its size limit.");
3402
3490
  if (line.startsWith("event:")) {
3403
3491
  eventType = line.substring(6).trim();
3404
3492
  continue;
@@ -3673,13 +3761,11 @@ function parseQoderSse(stream, options = {}) {
3673
3761
  }
3674
3762
  //#endregion
3675
3763
  //#region src/qoder/transport/chat.ts
3676
- /** Qoder chat request encoding, queue recovery, timeout lifecycle, and SSE streaming. */
3677
3764
  function aborted$1(message) {
3678
3765
  return new QoderLlmError(message, "ABORTED");
3679
3766
  }
3680
3767
  async function* streamQoderChat(options, model, credentials, messages, dependencies) {
3681
- const identityScope = options.purpose === void 0 && dependencies.hostRunId !== void 0 ? JSON.stringify([dependencies.region, dependencies.hostRunId]) : dependencies.region;
3682
- const request = await buildQoderRequestBody(options, credentials.userID, messages, model, void 0, void 0, identityScope);
3768
+ const request = await buildQoderRequestBody(options, credentials.userID, messages, model, void 0, void 0, dependencies.region, dependencies.hostRun);
3683
3769
  request.session_type = dependencies.region === "china" ? "qoderclicn" : "qodercli";
3684
3770
  const startedAt = performance.now();
3685
3771
  let activeCredentials = credentials;
@@ -4113,7 +4199,6 @@ function parseModelServerSse(stream, options = {}) {
4113
4199
  }
4114
4200
  //#endregion
4115
4201
  //#region src/qoder/transport/model-server.ts
4116
- /** Bearer-authenticated model-server HTTP SSE transport. */
4117
4202
  /** Convert the shared Qoder turn into the model-server's OpenAI-shaped request. */
4118
4203
  function modelServerBody(request) {
4119
4204
  return {
@@ -4138,8 +4223,7 @@ function modelServerBody(request) {
4138
4223
  };
4139
4224
  }
4140
4225
  async function* streamModelServerChat(options, model, credentials, messages, dependencies) {
4141
- const identityScope = options.purpose === void 0 && dependencies.hostRunId !== void 0 ? JSON.stringify([dependencies.region, dependencies.hostRunId]) : dependencies.region;
4142
- const request = await prepareQoderRequest(options, credentials.userID, messages, model, void 0, void 0, identityScope);
4226
+ const request = await prepareQoderRequest(options, credentials.userID, messages, model, void 0, void 0, dependencies.region, dependencies.hostRun);
4143
4227
  const body = JSON.stringify(modelServerBody(request));
4144
4228
  const startedAt = performance.now();
4145
4229
  dependencies.logger?.debug?.("[Qoder Stream] Request context", {
@@ -4418,21 +4502,10 @@ var DefaultQoderTransport = class {
4418
4502
  }
4419
4503
  async readAccount(options) {
4420
4504
  const lease = await this.lease(options?.signal);
4421
- let used;
4422
- const read = (force = options?.force) => this.usage.readWithCredentials(lease.key, async (signal) => {
4423
- used = await lease.resolve(signal);
4424
- return used;
4425
- }, {
4426
- force,
4427
- signal: options?.signal
4505
+ return this.usage.readWithCredentials(lease.key, (signal) => lease.resolve(signal), {
4506
+ ...options,
4507
+ refresh: (rejected, signal) => lease.resolve(signal, rejected.authToken)
4428
4508
  });
4429
- try {
4430
- return await read();
4431
- } catch (error) {
4432
- if (!(error instanceof QoderLlmError) || error.code !== "AUTH" || !used) throw error;
4433
- await lease.resolve(options?.signal, used.authToken);
4434
- return read(true);
4435
- }
4436
4509
  }
4437
4510
  async searchWeb(request, signal) {
4438
4511
  const lease = await this.lease(signal);
@@ -4470,7 +4543,7 @@ var DefaultQoderTransport = class {
4470
4543
  async *generate(options, model) {
4471
4544
  if (options.signal?.aborted) throw aborted("Request was aborted prior to generation.");
4472
4545
  const queueOwner = options.sessionId === void 0 ? void 0 : this.captureQueueLeaseOwner?.(String(options.sessionId));
4473
- validateQoderRequestShape(options, model);
4546
+ validateQoderRequestShape(options, model, this.attachments);
4474
4547
  const lease = await this.lease(options.signal);
4475
4548
  const credentials = await lease.resolve(options.signal);
4476
4549
  this.credentialLeases.set(credentials, lease);
@@ -4489,7 +4562,7 @@ var DefaultQoderTransport = class {
4489
4562
  streamIdleTimeoutMs: this.streamIdleTimeoutMs,
4490
4563
  modelQueueMaxWaitMs: this.modelQueueMaxWaitMs,
4491
4564
  onQueueUpdate: this.onQueueUpdate,
4492
- hostRunId: queueOwner?.runId,
4565
+ hostRun: queueOwner?.run,
4493
4566
  onQueueLease: queueOwner?.retain,
4494
4567
  resolveCredentials: async (refresh, signal) => {
4495
4568
  const current = await lease.resolve(signal);
@@ -4592,7 +4665,7 @@ var QoderQueueLeases = class {
4592
4665
  this.end(sessionId);
4593
4666
  this.runs.set(sessionId, {
4594
4667
  turn,
4595
- runId: randomUUID(),
4668
+ run: Object.freeze({ id: randomUUID() }),
4596
4669
  leases: /* @__PURE__ */ new Map()
4597
4670
  });
4598
4671
  }
@@ -4607,7 +4680,7 @@ var QoderQueueLeases = class {
4607
4680
  const owner = this.runs.get(sessionId);
4608
4681
  if (!owner || this.disposed) return void 0;
4609
4682
  return {
4610
- runId: owner.runId,
4683
+ run: owner.run,
4611
4684
  retain: (lease) => lease.sessionId === sessionId && this.runs.get(sessionId) === owner && this.retain(lease)
4612
4685
  };
4613
4686
  };
@@ -4648,6 +4721,47 @@ var QoderQueueStatusStore = class {
4648
4721
  }
4649
4722
  };
4650
4723
  //#endregion
4724
+ //#region src/dsh/transport-runtime.ts
4725
+ function transportConfig(value) {
4726
+ return {
4727
+ region: value.region ?? "global",
4728
+ streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
4729
+ responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
4730
+ modelQueueMaxWaitMs: value.modelQueueMaxWaitMs ?? 36e5,
4731
+ preserveThinking: value.preserveThinking ?? true,
4732
+ requestCompression: value.requestCompression ?? false,
4733
+ modelTransport: value.modelTransport ?? "legacy"
4734
+ };
4735
+ }
4736
+ var QoderTransportRuntime = class {
4737
+ snapshot;
4738
+ constructor(value, factory) {
4739
+ this.create = factory;
4740
+ const config = Object.freeze(transportConfig(value));
4741
+ this.snapshot = {
4742
+ config,
4743
+ transport: this.create(config)
4744
+ };
4745
+ }
4746
+ create;
4747
+ get config() {
4748
+ return this.snapshot.config;
4749
+ }
4750
+ get transport() {
4751
+ return this.snapshot.transport;
4752
+ }
4753
+ update(value, force = false) {
4754
+ const config = Object.freeze(transportConfig(value));
4755
+ if (!force && Object.keys(config).every((key) => config[key] === this.config[key])) return false;
4756
+ const transport = this.create(config);
4757
+ this.snapshot = {
4758
+ config,
4759
+ transport
4760
+ };
4761
+ return true;
4762
+ }
4763
+ };
4764
+ //#endregion
4651
4765
  //#region src/dsh/catalog-lifecycle.ts
4652
4766
  /** Region-scoped Qoder model discovery, effective catalogs, and advisory persistence. */
4653
4767
  var QoderCatalogLifecycle = class {
@@ -4722,10 +4836,11 @@ var QoderCatalogLifecycle = class {
4722
4836
  return;
4723
4837
  }
4724
4838
  }
4725
- /** Explicit discovery supersedes older work as soon as it starts, even if it fails. */
4839
+ /** Active discovery supersedes older work; a foreign transport is an isolated preview. */
4726
4840
  discover(signal, requestTransport) {
4727
4841
  if (this.disposed || signal?.aborted) return Promise.reject(new QoderLlmError("Qoder model discovery was aborted.", "ABORTED"));
4728
4842
  this.observeTransport();
4843
+ if (requestTransport && requestTransport !== this.activeTransport) return requestTransport.discoverModels(signal);
4729
4844
  const region = this.options.region();
4730
4845
  const state = this.catalogs[region];
4731
4846
  const transport = this.activeTransport;
@@ -4863,6 +4978,9 @@ var QoderAdapter = class extends LlmAdapter {
4863
4978
  });
4864
4979
  } catch {}
4865
4980
  }
4981
+ forgetSession(sessionId) {
4982
+ for (const key of this.sessionTiers.keys()) if (JSON.parse(key)[1] === sessionId) this.sessionTiers.delete(key);
4983
+ }
4866
4984
  subscribeSessionTiers(listener) {
4867
4985
  this.tierListeners.add(listener);
4868
4986
  return () => {
@@ -5119,12 +5237,21 @@ function registerQoderRpc(ctx, handler, subscribeTiers) {
5119
5237
  const registry = ctx.connection?.fetch;
5120
5238
  if (typeof registry?.register !== "function") throw new Error("provider-qoder: connection exposes no exact Fetch route registry");
5121
5239
  const streams = /* @__PURE__ */ new Set();
5122
- const disposers = qoderRpcEndpoints.map((endpoint) => registry.register({
5123
- path: qoderRpcPath(endpoint),
5124
- methods: ["POST"],
5125
- requestBody: "buffered",
5126
- fetch: (request) => endpoint === "sessionTierEvents" && subscribeTiers ? Promise.resolve(sessionTierEvents(request, subscribeTiers, streams)) : handleQoderRpcRequest(endpoint, handler, request)
5127
- }));
5240
+ const disposers = [];
5241
+ try {
5242
+ for (const endpoint of qoderRpcEndpoints) disposers.push(registry.register({
5243
+ path: qoderRpcPath(endpoint),
5244
+ methods: ["POST"],
5245
+ requestBody: "buffered",
5246
+ fetch: (request) => endpoint === "sessionTierEvents" && subscribeTiers ? Promise.resolve(sessionTierEvents(request, subscribeTiers, streams)) : handleQoderRpcRequest(endpoint, handler, request)
5247
+ }));
5248
+ } catch (error) {
5249
+ for (const close of streams) close();
5250
+ for (const dispose of disposers.reverse()) try {
5251
+ Promise.resolve(dispose()).catch(() => {});
5252
+ } catch {}
5253
+ throw error;
5254
+ }
5128
5255
  return () => {
5129
5256
  for (const close of streams) close();
5130
5257
  for (const dispose of disposers) dispose();
@@ -5556,59 +5683,31 @@ function apply(ctx, input = {}) {
5556
5683
  const logger = ctx.logger;
5557
5684
  const initialRegion = config.region ?? "global";
5558
5685
  const baseConfig = {
5559
- region: initialRegion,
5686
+ ...transportConfig(config),
5560
5687
  modelsByRegion: { ...config.modelsByRegion },
5561
- streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
5562
- responseHeaderTimeoutMs: config.responseHeaderTimeoutMs ?? 6e4,
5563
- modelQueueMaxWaitMs: config.modelQueueMaxWaitMs ?? 36e5,
5564
- preserveThinking: config.preserveThinking ?? true,
5565
- webSearchMode: config.webSearchMode ?? "auto",
5566
- requestCompression: config.requestCompression ?? false,
5567
- modelTransport: config.modelTransport ?? "legacy"
5688
+ webSearchMode: config.webSearchMode ?? "auto"
5568
5689
  };
5569
5690
  let current = () => baseConfig;
5570
5691
  let authorization;
5571
- const createTransport = (region, streamIdleTimeoutMs, responseHeaderTimeoutMs, preserveThinking, resolvePat) => createQoderTransport({
5572
- region,
5692
+ const createTransport = (value, resolvePat) => createQoderTransport({
5693
+ ...value,
5573
5694
  resolvePat: resolvePat ?? (() => resolveManagedQoderPat(ctx.credentials)),
5574
- resolveCredential: resolvePat ? void 0 : (signal) => authorization.lease(region, signal),
5575
- requestCompression: current().requestCompression ?? false,
5576
- modelTransport: current().modelTransport ?? "legacy",
5695
+ resolveCredential: resolvePat ? void 0 : (signal) => authorization.lease(value.region, signal),
5577
5696
  logger,
5578
- streamIdleTimeoutMs,
5579
- responseHeaderTimeoutMs,
5580
5697
  attachments: ctx.attachments,
5581
- preserveThinking,
5582
- modelQueueMaxWaitMs: current().modelQueueMaxWaitMs ?? 36e5,
5583
5698
  onQueueUpdate: queueStatus.update,
5584
5699
  captureQueueLeaseOwner: leases.capture
5585
5700
  });
5586
5701
  const resolveConfig = () => {
5587
5702
  const value = current();
5588
- const region = value.region ?? "global";
5703
+ const transport = transportConfig(value);
5589
5704
  return {
5590
- region,
5591
- models: modelsFor(value, region),
5592
- streamIdleTimeoutMs: value.streamIdleTimeoutMs ?? 3e5,
5593
- responseHeaderTimeoutMs: value.responseHeaderTimeoutMs ?? 6e4,
5594
- modelQueueMaxWaitMs: value.modelQueueMaxWaitMs ?? 36e5,
5595
- preserveThinking: value.preserveThinking ?? true,
5596
- webSearchMode: value.webSearchMode ?? "auto",
5597
- requestCompression: value.requestCompression ?? false,
5598
- modelTransport: value.modelTransport ?? "legacy"
5705
+ ...transport,
5706
+ models: modelsFor(value, transport.region),
5707
+ webSearchMode: value.webSearchMode ?? "auto"
5599
5708
  };
5600
5709
  };
5601
- const initial = resolveConfig();
5602
- let activeTransport = createTransport(initial.region, initial.streamIdleTimeoutMs, initial.responseHeaderTimeoutMs, initial.preserveThinking);
5603
- let activeTransportConfig = {
5604
- region: initial.region,
5605
- streamIdleTimeoutMs: initial.streamIdleTimeoutMs,
5606
- responseHeaderTimeoutMs: initial.responseHeaderTimeoutMs,
5607
- modelQueueMaxWaitMs: initial.modelQueueMaxWaitMs,
5608
- preserveThinking: initial.preserveThinking,
5609
- requestCompression: initial.requestCompression,
5610
- modelTransport: initial.modelTransport
5611
- };
5710
+ const runtime = new QoderTransportRuntime(current(), createTransport);
5612
5711
  const authorizationTransports = /* @__PURE__ */ new Map();
5613
5712
  authorization = new QoderBrowserAuthorizationManager({
5614
5713
  credentials: ctx.credentials,
@@ -5630,32 +5729,34 @@ function apply(ctx, input = {}) {
5630
5729
  });
5631
5730
  ctx.effect(() => () => authorization.dispose(), "provider-qoder: browser authorization lifecycle");
5632
5731
  const catalog = new QoderCatalogLifecycle({
5633
- resolveTransport: () => activeTransport,
5634
- region: () => activeTransportConfig.region,
5732
+ resolveTransport: () => runtime.transport,
5733
+ region: () => runtime.config.region,
5635
5734
  configuredModels: (region) => modelsFor(current(), region),
5636
5735
  onDiscovered: () => registration.replace([providerQoder]),
5637
5736
  onPersistenceError: (error) => logger?.error?.("[Qoder Settings] Failed to synchronize model catalog", logError(error))
5638
5737
  });
5639
5738
  ctx.effect(() => () => catalog.dispose(), "provider-qoder: catalog lifecycle");
5640
5739
  const adapter = new QoderAdapter({
5641
- resolveTransport: () => activeTransport,
5740
+ resolveTransport: () => runtime.transport,
5642
5741
  catalog,
5643
- region: () => activeTransportConfig.region,
5742
+ region: () => runtime.config.region,
5644
5743
  providerId: providerQoder,
5645
5744
  providerName: "Qoder",
5646
5745
  sessions: ctx.get("sessions"),
5647
5746
  agents: ctx.get("agents")
5648
5747
  });
5748
+ ctx.on("session/disposed", (session) => adapter.forgetSession(session.id));
5649
5749
  const registration = ctx.llm.registerAdapter([providerQoder], adapter);
5650
5750
  const refreshAuthentication = () => {
5651
5751
  const value = resolveConfig();
5652
- activeTransport = createTransport(value.region, value.streamIdleTimeoutMs, value.responseHeaderTimeoutMs, value.preserveThinking);
5752
+ runtime.update(value, true);
5753
+ authorization.activateRegion(runtime.config.region);
5653
5754
  catalog.clearDiscovered();
5654
5755
  catalog.observeTransport();
5655
5756
  registration.replace([providerQoder]);
5656
5757
  };
5657
5758
  ctx.on("credentials/record-updated", (key) => {
5658
- if (key === browserAuthorizationKey(activeTransportConfig.region)) {
5759
+ if (key === browserAuthorizationKey(runtime.config.region)) {
5659
5760
  refreshAuthentication();
5660
5761
  catalog.refresh();
5661
5762
  }
@@ -5665,19 +5766,7 @@ function apply(ctx, input = {}) {
5665
5766
  });
5666
5767
  const refreshAdapter = () => {
5667
5768
  const next = resolveConfig();
5668
- if (next.region !== activeTransportConfig.region || next.streamIdleTimeoutMs !== activeTransportConfig.streamIdleTimeoutMs || next.responseHeaderTimeoutMs !== activeTransportConfig.responseHeaderTimeoutMs || next.modelQueueMaxWaitMs !== activeTransportConfig.modelQueueMaxWaitMs || next.requestCompression !== activeTransportConfig.requestCompression || next.modelTransport !== activeTransportConfig.modelTransport || next.preserveThinking !== activeTransportConfig.preserveThinking) {
5669
- authorization.activateRegion(next.region);
5670
- activeTransport = createTransport(next.region, next.streamIdleTimeoutMs, next.responseHeaderTimeoutMs, next.preserveThinking);
5671
- activeTransportConfig = {
5672
- region: next.region,
5673
- streamIdleTimeoutMs: next.streamIdleTimeoutMs,
5674
- responseHeaderTimeoutMs: next.responseHeaderTimeoutMs,
5675
- modelQueueMaxWaitMs: next.modelQueueMaxWaitMs,
5676
- preserveThinking: next.preserveThinking,
5677
- requestCompression: next.requestCompression,
5678
- modelTransport: next.modelTransport
5679
- };
5680
- }
5769
+ if (runtime.update(next)) authorization.activateRegion(runtime.config.region);
5681
5770
  catalog.observeTransport();
5682
5771
  adapter.replaceModels(next.models);
5683
5772
  registration.replace([providerQoder]);
@@ -5716,7 +5805,12 @@ function apply(ctx, input = {}) {
5716
5805
  const discoverModels = async (signal, suppliedPat) => {
5717
5806
  const snapshot = resolveConfig();
5718
5807
  const normalizedPat = suppliedPat?.trim();
5719
- const transport = normalizedPat ? createTransport(snapshot.region, snapshot.streamIdleTimeoutMs, snapshot.responseHeaderTimeoutMs, snapshot.preserveThinking, () => Promise.resolve(normalizedPat)) : activeTransport;
5808
+ const transport = runtime.transport;
5809
+ if (normalizedPat) {
5810
+ const view = await authorization.describe(snapshot.region);
5811
+ const managedPat = await resolveManagedQoderPat(ctx.credentials);
5812
+ if (view.method !== "pat" || normalizedPat !== managedPat.trim() || transport !== runtime.transport) return createTransport(transportConfig(snapshot), () => Promise.resolve(normalizedPat)).discoverModels(signal);
5813
+ }
5720
5814
  return catalog.discover(signal, transport);
5721
5815
  };
5722
5816
  ctx.llm.registerModelDiscovery(settingsNamespace, async (request, signal) => {
@@ -5785,7 +5879,7 @@ function apply(ctx, input = {}) {
5785
5879
  }
5786
5880
  const force = typeof payload === "object" && payload !== null && "force" in payload ? payload.force === true : false;
5787
5881
  logger?.debug?.("[Qoder RPC] Reading subscriber account", { force });
5788
- const outcome = await executeRpc("load Qoder account", () => activeTransport.readAccount({
5882
+ const outcome = await executeRpc("load Qoder account", () => runtime.transport.readAccount({
5789
5883
  force,
5790
5884
  signal
5791
5885
  }), signal, logger);
@@ -5800,7 +5894,7 @@ function apply(ctx, input = {}) {
5800
5894
  ctx.inject(["web"], (webCtx) => {
5801
5895
  const searchProvider = new QoderSearchProvider({
5802
5896
  ctx,
5803
- resolveTransport: () => activeTransport,
5897
+ resolveTransport: () => runtime.transport,
5804
5898
  getWebSearchMode: () => resolveConfig().webSearchMode ?? "auto"
5805
5899
  });
5806
5900
  webCtx.web.registerSearchProvider(searchProvider);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-provider-qoder",
3
3
  "description": "Global Qoder subscription adapter for the DeepSeek Harness LLM seam",
4
- "version": "0.5.0",
4
+ "version": "0.5.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },