dsh-qoder-connect 0.1.1 → 0.1.4

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,4 +1,4 @@
1
- import { A as QODER_GLOBAL_AUTH_PATH, B as qoderCredentialIdentity, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_AUTH_FILENAME, G as qoderPluginDataDir, H as QODER_DATA_DIR_ENV, I as QODER_PAT_ENV_CN, K as qoderStateDir, L as QODER_PAT_ENV_GLOBAL, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_STATUS_PATH, R as QoderCredentialStore, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_NAME, V as qoderOwnAuthPath, W as qoderMachineIdPath, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_GLOBAL_PROBE_PATH, k as QODER_AUTH_PATH, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as randomSentinel, x as getMachineId, y as validateApiKey, z as patTail } from "./variants-CQg-9veJ.js";
1
+ import { A as QODER_GLOBAL_AUTH_PATH, B as qoderCredentialIdentity, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_AUTH_FILENAME, G as qoderPluginDataDir, H as QODER_DATA_DIR_ENV, I as QODER_PAT_ENV_CN, K as qoderStateDir, L as QODER_PAT_ENV_GLOBAL, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_STATUS_PATH, R as QoderCredentialStore, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_NAME, V as qoderOwnAuthPath, W as qoderMachineIdPath, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_GLOBAL_PROBE_PATH, k as QODER_AUTH_PATH, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as randomSentinel, x as getMachineId, y as validateApiKey, z as patTail } from "./variants-CSeJqJpR.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
4
4
  import { dirname, join, resolve } from "node:path";
@@ -539,7 +539,13 @@ function createQoderShim(options) {
539
539
  return timingSafeEqual(a, b);
540
540
  }
541
541
  const server = createServer((req, res) => {
542
- handle(req, res);
542
+ handle(req, res).catch((error) => {
543
+ logger?.warn("dsh-qoder-connect: loopback request failed", error);
544
+ try {
545
+ if (!res.headersSent) writeOpenAIError(res, 500, "internal", "loopback request failed");
546
+ else res.end();
547
+ } catch {}
548
+ });
543
549
  });
544
550
  const ready = new Promise((resolve, reject) => {
545
551
  server.once("listening", () => resolve());
@@ -1284,16 +1290,42 @@ function emitJobTokenHint(at, text) {
1284
1290
  const agent = lastRunningAgent;
1285
1291
  if (agent?.session === void 0) return;
1286
1292
  lastRefreshAt.set(String(agent.id), at);
1293
+ appendHintRow(agent, "success", text);
1294
+ }
1295
+ /**
1296
+ * Print the failed-heal row into the conversation whose request triggered it.
1297
+ *
1298
+ * The mirror of {@link emitJobTokenHint}: the self-heal ran, exchanged a fresh
1299
+ * job token, retried, and the upstream still refused. Without this row that
1300
+ * outcome was silent — the user saw only the resulting authorization failure
1301
+ * and could not tell that recovery had already been attempted and lost.
1302
+ *
1303
+ * Rate limited by the transport (once per unresolved outage), not here: this
1304
+ * module stays a pure printer so the dedupe policy lives with the state that
1305
+ * knows when an outage ends.
1306
+ *
1307
+ * @param at - When the failed heal was recorded.
1308
+ * @param text - The row's summary line.
1309
+ */
1310
+ function emitJobTokenRefreshFailedHint(at, text) {
1311
+ const agent = lastRunningAgent;
1312
+ if (agent?.session === void 0) return;
1313
+ appendHintRow(agent, "error", text);
1314
+ }
1315
+ /** Append one log-only command pair carrying `text` as its outcome. */
1316
+ function appendHintRow(agent, kind, text) {
1317
+ const session = agent.session;
1318
+ if (session === void 0) return;
1287
1319
  const commandId = `cmd-qoder-hint-${Date.now().toString(36)}-${++hintSeq}`;
1288
1320
  try {
1289
- agent.session.append("command/run", {
1321
+ session.append("command/run", {
1290
1322
  commandId,
1291
1323
  name: JOB_TOKEN_HINT_NAME,
1292
1324
  source: { kind: "user" }
1293
1325
  });
1294
- agent.session.append("command/done", {
1326
+ session.append("command/done", {
1295
1327
  commandId,
1296
- kind: "success",
1328
+ kind,
1297
1329
  text
1298
1330
  });
1299
1331
  } catch {}
@@ -1469,6 +1501,17 @@ function fallbackFor(_variant) {
1469
1501
  function jobTokenHintText(at) {
1470
1502
  return `jobToken 已自动刷新(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})— 旧令牌被上游拒绝,已自动重换并恢复`;
1471
1503
  }
1504
+ /**
1505
+ * The failed-heal row's summary line.
1506
+ *
1507
+ * Deliberately does not name a cause: the upstream rejection that survived a
1508
+ * fresh token is not necessarily an authorization problem at all, and the real
1509
+ * reason travels in the failure message itself (see the SSE envelope body).
1510
+ * Claiming "quota" or "revoked" here would be a guess presented as a finding.
1511
+ */
1512
+ function jobTokenRefreshFailedHintText(at) {
1513
+ return `jobToken 已重换但仍被上游拒绝(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})— 自愈未恢复,请查看上方错误详情`;
1514
+ }
1472
1515
  /** Build one variant's stores, transport, and probe state. */
1473
1516
  function createVariantRuntime(ctx, config, variant, current, identityOf) {
1474
1517
  const store = new QoderCredentialStore({
@@ -1484,6 +1527,10 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
1484
1527
  jobTokenRefreshedAt = info.at;
1485
1528
  ctx.logger.warn(`dsh-qoder-connect: ${variant.displayName} job token was auto-refreshed after an upstream rejection`);
1486
1529
  emitJobTokenHint(info.at, jobTokenHintText(info.at));
1530
+ },
1531
+ onJobTokenRefreshFailed: (info) => {
1532
+ ctx.logger.warn(`dsh-qoder-connect: ${variant.displayName} job token refresh did not recover the chat (upstream status ${info.status ?? "unknown"})`);
1533
+ emitJobTokenRefreshFailedHint(info.at, jobTokenRefreshFailedHintText(info.at));
1487
1534
  }
1488
1535
  });
1489
1536
  const client = new QoderUpstreamClient({
@@ -1582,6 +1629,21 @@ function probeSection(runtime, consent) {
1582
1629
  };
1583
1630
  }
1584
1631
  /**
1632
+ * Run a detached promise without letting its failure kill the host.
1633
+ *
1634
+ * Node terminates the whole process on an unhandled rejection (exit code 1),
1635
+ * which the desktop shell reports as the backend having "exited unexpectedly"
1636
+ * — so a failing heartbeat write, loopback close, or background sweep in this
1637
+ * plugin would take the entire Harness down with it. Every fire-and-forget
1638
+ * call therefore carries a handler; a failure is a diagnostic, never a reason
1639
+ * for the host to die.
1640
+ */
1641
+ function detach(ctx, work, what) {
1642
+ work.catch((error) => {
1643
+ ctx.logger.warn(`dsh-qoder-connect: ${what} failed`, error);
1644
+ });
1645
+ }
1646
+ /**
1585
1647
  * Start one variant: its loopback endpoint, provider registration, and
1586
1648
  * configuration-card wiring.
1587
1649
  *
@@ -1641,18 +1703,18 @@ async function startVariant(ctx, runtime) {
1641
1703
  ctx.effect(() => () => {
1642
1704
  releaseAdapter?.();
1643
1705
  releaseDirectory?.();
1644
- shim.close();
1706
+ detach(ctx, shim.close(), "loopback endpoint close");
1645
1707
  });
1646
1708
  } catch {
1647
1709
  releaseAdapter?.();
1648
1710
  releaseDirectory?.();
1649
- shim.close();
1711
+ detach(ctx, shim.close(), "loopback endpoint close");
1650
1712
  }
1651
1713
  runtime.registered = true;
1652
1714
  return true;
1653
1715
  } catch (error) {
1654
1716
  ctx.logger.error(`dsh-qoder-connect: ${variant.displayName} provider registration failed`, error);
1655
- shim.close();
1717
+ detach(ctx, shim.close(), "loopback endpoint close");
1656
1718
  return false;
1657
1719
  }
1658
1720
  }
@@ -1906,7 +1968,7 @@ function apply(ctx, config) {
1906
1968
  stopped = true;
1907
1969
  for (const timer of timers) clearInterval(timer);
1908
1970
  timers.length = 0;
1909
- clearHostHeartbeat();
1971
+ detach(ctx, clearHostHeartbeat(), "host heartbeat cleanup");
1910
1972
  });
1911
1973
  /**
1912
1974
  * Fetch one variant's catalog for the current credential.
@@ -1967,11 +2029,15 @@ function apply(ctx, config) {
1967
2029
  runtime.catalogSource = "live";
1968
2030
  runtime.catalogFetchedAtMs = runtime.client.lastCatalog?.fetchedAtMs ?? Date.now();
1969
2031
  runtime.catalogError = void 0;
1970
- if (lastIdentities.get(runtime.variant.id) === identity) runtime.savedCatalogs.set(identity, {
1971
- source: runtime.client.lastCatalog?.source ?? "unknown",
1972
- fetchedAtMs: runtime.client.lastCatalog?.fetchedAtMs ?? Date.now(),
1973
- models: [...models]
1974
- });
2032
+ if (lastIdentities.get(runtime.variant.id) === identity) try {
2033
+ runtime.savedCatalogs.set(identity, {
2034
+ source: runtime.client.lastCatalog?.source ?? "unknown",
2035
+ fetchedAtMs: runtime.client.lastCatalog?.fetchedAtMs ?? Date.now(),
2036
+ models: [...models]
2037
+ });
2038
+ } catch (error) {
2039
+ ctx.logger.warn(`dsh-qoder-connect: ${runtime.variant.displayName} catalog could not be saved for this account`, error);
2040
+ }
1975
2041
  runtime.invalidate();
1976
2042
  })().finally(() => {
1977
2043
  if (runtime.inflightFetch?.promise === run) runtime.inflightFetch = void 0;
@@ -2022,16 +2088,23 @@ function apply(ctx, config) {
2022
2088
  const syncAll = async () => {
2023
2089
  for (const runtime of runtimes) await syncVariant(runtime);
2024
2090
  };
2025
- Promise.all(runtimes.map(async (runtime) => startVariant(ctx, runtime))).then(() => {
2091
+ /**
2092
+ * Start both variants, then begin the credential sweep.
2093
+ *
2094
+ * The chain carries its own failure handler: without one, a rejection here
2095
+ * would be an unhandled rejection — which Node turns into process
2096
+ * termination, taking the whole Harness down over one plugin's startup.
2097
+ */
2098
+ detach(ctx, Promise.all(runtimes.map(async (runtime) => startVariant(ctx, runtime))).then(() => {
2026
2099
  if (stopped) return;
2027
- if (runtimes.some((runtime) => runtime.registered)) writeHostHeartbeat();
2028
- syncAll();
2100
+ if (runtimes.some((runtime) => runtime.registered)) detach(ctx, writeHostHeartbeat(), "host heartbeat write");
2101
+ detach(ctx, syncAll(), "credential sweep");
2029
2102
  const timer = setInterval(() => {
2030
- syncAll();
2103
+ detach(ctx, syncAll(), "credential sweep");
2031
2104
  }, credentialPollMs());
2032
2105
  timer.unref?.();
2033
2106
  timers.push(timer);
2034
- });
2107
+ }), "variant startup");
2035
2108
  }
2036
2109
  //#endregion
2037
2110
  export { CHINA_VARIANT, Config, FALLBACK_QODER_MODELS, GLOBAL_VARIANT, KIND_STATUS, PROBE_EFFORT_CANDIDATES, QODER_AUTH_FILENAME, QODER_AUTH_PATH, QODER_DATA_DIR_ENV, QODER_DATA_DIR_NAME, QODER_GLOBAL_AUTH_PATH, QODER_GLOBAL_PROBE_PATH, QODER_GLOBAL_SETTINGS_NS, QODER_GLOBAL_STATUS_PATH, QODER_HOST_HEARTBEAT_FILENAME, QODER_PAT_ENV_CN, QODER_PAT_ENV_GLOBAL, QODER_PROBE_FILENAME, QODER_PROBE_PATH, QODER_PROVIDER, QODER_QUOTA_SETTINGS_NS, QODER_SETTINGS_NS, QODER_STATUS_PATH, QODER_STREAM_IDLE_TIMEOUT_MS, QODER_VARIANTS, QUOTA_POLL_DEFAULT_MS, QUOTA_POLL_MIN_MS, QoderCatalog, QoderCatalogStore, QoderCredentialStore, QoderProbeService, QoderProbeStore, QoderUpstreamClient, apply, classifyUpstreamError, clearHostHeartbeat, createAuthKey, createQoderAdapter, createQoderShim, fingerprintModel, inject, isHeartbeatProcessAlive, kindFromQoderFailure, modelInfoOf, name, normalizeCredits, patTail, probeModel, processStartTimeMs, qoderAuthHandler, qoderCatalogPath, qoderCredentialIdentity, qoderHostHeartbeatPath, qoderOwnAuthPath, qoderPluginDataDir, qoderProbePath, randomSentinel, readHostHeartbeat, registerQoderAuthRoute, variantFor };
@@ -2415,27 +2415,38 @@ var QoderUsageReader = class {
2415
2415
  * Re-arranges standard Base64 chunks and translates characters against
2416
2416
  * a custom alphabet to satisfy Qoder upstream WAF requirements.
2417
2417
  *
2418
+ * The translation runs through a byte table applied to a Buffer instead of a
2419
+ * per-character string loop. The loop form (`out += char` plus
2420
+ * `alphabet.indexOf(char)`) costs ~800 ms on a 1 MB body and runs on the main
2421
+ * thread once per model request, which saturated the Harness event loop as
2422
+ * soon as a few large-context sessions were in flight. The table form is
2423
+ * byte-for-byte identical (the input is always Base64, i.e. ASCII) and about
2424
+ * 24x faster.
2425
+ *
2418
2426
  * @module dsh-provider-qoder/qoder/transport/wire/encoding
2419
2427
  */
2420
2428
  const qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
2421
2429
  const qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2430
+ /**
2431
+ * Byte translation table: every byte maps to itself unless it is a Base64
2432
+ * alphabet character (→ the custom alphabet) or the `=` pad (→ `$`).
2433
+ */
2434
+ const encodeTable = (() => {
2435
+ const table = /* @__PURE__ */ new Uint8Array(256);
2436
+ for (let index = 0; index < 256; index++) table[index] = index;
2437
+ for (let index = 0; index < 64; index++) table[qoderStdAlphabet.charCodeAt(index)] = qoderCustomAlphabet.charCodeAt(index);
2438
+ table["=".charCodeAt(0)] = "$".charCodeAt(0);
2439
+ return table;
2440
+ })();
2422
2441
  function qoderEncodeBody(plaintext) {
2423
2442
  const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
2424
2443
  const n = std.length;
2425
2444
  const a = Math.floor(n / 3);
2426
2445
  const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
2427
- let out = "";
2428
- for (let i = 0; i < n; i++) {
2429
- const c = rearranged[i];
2430
- if (c === void 0) continue;
2431
- if (c === "=") out += "$";
2432
- else {
2433
- const idx = qoderStdAlphabet.indexOf(c);
2434
- if (idx >= 0) out += qoderCustomAlphabet[idx];
2435
- else out += c;
2436
- }
2437
- }
2438
- return out;
2446
+ const source = Buffer.from(rearranged, "latin1");
2447
+ const target = Buffer.allocUnsafe(n);
2448
+ for (let index = 0; index < n; index++) target[index] = encodeTable[source[index] ?? 0] ?? 0;
2449
+ return target.toString("latin1");
2439
2450
  }
2440
2451
  //#endregion
2441
2452
  //#region src/qoder/transport/wire/thinking.ts
@@ -2612,7 +2623,10 @@ function parseEnvelope(rawData) {
2612
2623
  const envelope = value;
2613
2624
  if (envelope.statusCodeValue !== void 0 && typeof envelope.statusCodeValue !== "number") throw malformed("Qoder SSE envelope has an invalid status code.");
2614
2625
  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.");
2615
- if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) throw qoderHttpError(`Qoder service returned upstream error status ${envelope.statusCodeValue}.`, { status: envelope.statusCodeValue });
2626
+ if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) {
2627
+ const status = envelope.statusCodeValue;
2628
+ throw qoderHttpError(envelope.body ? `Qoder service returned upstream error status ${status}: ${envelope.body}` : `Qoder service returned upstream error status ${status}.`, { status });
2629
+ }
2616
2630
  if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
2617
2631
  return envelope;
2618
2632
  }
@@ -3019,6 +3033,18 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
3019
3033
  requestController.abort("request complete");
3020
3034
  }
3021
3035
  }
3036
+ /**
3037
+ * How long a rotated-token notice may wait for an accepting chat.
3038
+ *
3039
+ * The heal's own retry can lose a race with a transient fault and be rescued
3040
+ * by a later attempt, so the notice is deferred until some chat is accepted.
3041
+ * That deferral has to be bounded: an unbounded one let a rotation from
3042
+ * minutes earlier surface as a fresh-looking row whose timestamp named a
3043
+ * moment the user could not connect to the row appearing now. Five minutes
3044
+ * comfortably covers the host's own retry cycle (its backoff caps at 10s)
3045
+ * while keeping the row adjacent to the event it describes.
3046
+ */
3047
+ const jobTokenNoticeMaxAgeMs = 3e5;
3022
3048
  function aborted(message) {
3023
3049
  return new QoderLlmError(message, "ABORTED");
3024
3050
  }
@@ -3037,6 +3063,7 @@ var DefaultQoderTransport = class {
3037
3063
  imageUploader;
3038
3064
  preserveThinking;
3039
3065
  onJobTokenRefreshed;
3066
+ onJobTokenRefreshFailed;
3040
3067
  /**
3041
3068
  * Set when the self-heal exchanged a fresh job token, cleared when a chat
3042
3069
  * afterwards succeeds. The notice reports "the token was rotated because the
@@ -3045,6 +3072,16 @@ var DefaultQoderTransport = class {
3045
3072
  * transient upstream timeout and be rescued by a later attempt.
3046
3073
  */
3047
3074
  pendingRefreshAt;
3075
+ /**
3076
+ * Whether the current unresolved heal failure has already been reported.
3077
+ *
3078
+ * One upstream rejection can outlive many host-level retries (an observed
3079
+ * storm ran 59 of them across 75 steps), and the transport re-heals inside
3080
+ * every one of them. Without this latch the failure notice would print once
3081
+ * per retry; with it, the user is told once per outage. Cleared as soon as
3082
+ * any chat is accepted, so the next outage announces itself again.
3083
+ */
3084
+ refreshFailureAnnounced = false;
3048
3085
  constructor(options) {
3049
3086
  this.region = options.region;
3050
3087
  this.resolvePat = options.resolvePat;
@@ -3056,6 +3093,7 @@ var DefaultQoderTransport = class {
3056
3093
  this.attachments = options.attachments;
3057
3094
  this.preserveThinking = options.preserveThinking;
3058
3095
  this.onJobTokenRefreshed = options.onJobTokenRefreshed;
3096
+ this.onJobTokenRefreshFailed = options.onJobTokenRefreshFailed;
3059
3097
  this.auth = new QoderAuthService({
3060
3098
  fetch: this.fetchImpl,
3061
3099
  logger: this.logger,
@@ -3096,12 +3134,35 @@ var DefaultQoderTransport = class {
3096
3134
  if (this.pendingRefreshAt === void 0) return;
3097
3135
  const at = this.pendingRefreshAt;
3098
3136
  this.pendingRefreshAt = void 0;
3137
+ if (Date.now() - at > jobTokenNoticeMaxAgeMs) return;
3099
3138
  this.logger?.warn?.("[Qoder Stream] Job token was auto-refreshed after an upstream rejection; the chat has recovered");
3100
3139
  this.onJobTokenRefreshed?.({
3101
3140
  region: this.region,
3102
3141
  at
3103
3142
  });
3104
3143
  }
3144
+ /**
3145
+ * Drop a pending rotation notice whose heal did not rescue anything.
3146
+ *
3147
+ * The heal's own retry was rejected too, so "the rotation fixed it" is not
3148
+ * what happened. Leaving the notice pending made a LATER, unrelated chat
3149
+ * acceptance flush it — printing a success row that contradicts the failure
3150
+ * row already shown, stamped with the old rotation time.
3151
+ */
3152
+ discardPendingRefreshNotice() {
3153
+ this.pendingRefreshAt = void 0;
3154
+ }
3155
+ /**
3156
+ * Note that the upstream accepted a chat, ending any unresolved heal failure.
3157
+ *
3158
+ * `streamChat` calls this the moment its first chunk arrives — the only point
3159
+ * at which "the credential was accepted" is actually known. A rejected chat
3160
+ * throws before that, so this never fires on a failing attempt.
3161
+ */
3162
+ onChatAccepted() {
3163
+ this.refreshFailureAnnounced = false;
3164
+ this.flushPendingRefreshNotice();
3165
+ }
3105
3166
  async discoverModels(signal) {
3106
3167
  const pat = await this.requirePat(signal);
3107
3168
  const key = opaqueCredentialKey(pat);
@@ -3167,7 +3228,7 @@ var DefaultQoderTransport = class {
3167
3228
  const chatCredentials = await this.auth.getCredentials(pat, options.signal);
3168
3229
  try {
3169
3230
  yield* this.streamChat(options, model, chatCredentials, messages, () => {
3170
- this.flushPendingRefreshNotice();
3231
+ this.onChatAccepted();
3171
3232
  });
3172
3233
  return;
3173
3234
  } catch (error) {
@@ -3180,7 +3241,7 @@ var DefaultQoderTransport = class {
3180
3241
  this.pendingRefreshAt = Date.now();
3181
3242
  try {
3182
3243
  yield* this.streamChat(options, model, refreshed, messages, () => {
3183
- this.flushPendingRefreshNotice();
3244
+ this.onChatAccepted();
3184
3245
  });
3185
3246
  return;
3186
3247
  } catch (error) {
@@ -3201,6 +3262,16 @@ var DefaultQoderTransport = class {
3201
3262
  }
3202
3263
  }
3203
3264
  }
3265
+ this.discardPendingRefreshNotice();
3266
+ if (!this.refreshFailureAnnounced) {
3267
+ this.refreshFailureAnnounced = true;
3268
+ this.logger?.warn?.("[Qoder Stream] Job token refresh did not recover the chat; the upstream still rejects it", { status: lastRejection instanceof LlmError ? lastRejection.failure.status : void 0 });
3269
+ this.onJobTokenRefreshFailed?.({
3270
+ region: this.region,
3271
+ at: Date.now(),
3272
+ ...lastRejection instanceof LlmError && lastRejection.failure.status !== void 0 ? { status: lastRejection.failure.status } : {}
3273
+ });
3274
+ }
3204
3275
  throw lastRejection;
3205
3276
  }
3206
3277
  };
@@ -3607,14 +3678,23 @@ var ChunkEncoder = class {
3607
3678
  }] })];
3608
3679
  }
3609
3680
  case "usage": {
3610
- const prompt = chunk.usage.inputTokens + (chunk.usage.cacheReadTokens ?? 0) + (chunk.usage.cacheWriteTokens ?? 0);
3681
+ const cacheRead = chunk.usage.cacheReadTokens;
3682
+ const cacheWrite = chunk.usage.cacheWriteTokens;
3683
+ const prompt = chunk.usage.inputTokens + (cacheRead ?? 0) + (cacheWrite ?? 0);
3611
3684
  const completion = chunk.usage.outputTokens;
3685
+ const reasoning = chunk.usage.reasoningTokens;
3686
+ const details = cacheRead === void 0 && cacheWrite === void 0 ? void 0 : {
3687
+ ...cacheRead === void 0 ? {} : { cached_tokens: cacheRead },
3688
+ ...cacheWrite === void 0 ? {} : { cache_write_tokens: cacheWrite }
3689
+ };
3612
3690
  return [this.frame({
3613
3691
  choices: [],
3614
3692
  usage: {
3615
3693
  prompt_tokens: prompt,
3616
3694
  completion_tokens: completion,
3617
- total_tokens: chunk.usage.totalTokens ?? prompt + completion
3695
+ total_tokens: chunk.usage.totalTokens ?? prompt + completion,
3696
+ ...details === void 0 ? {} : { prompt_tokens_details: details },
3697
+ ...reasoning === void 0 ? {} : { completion_tokens_details: { reasoning_tokens: reasoning } }
3618
3698
  }
3619
3699
  })];
3620
3700
  }
@@ -4002,7 +4082,7 @@ const DEFAULT_MAX_TOKENS = 32768;
4002
4082
  const DEFAULT_CONTEXT_WINDOW = 18e4;
4003
4083
  //#endregion
4004
4084
  //#region src/version.ts
4005
- const QODER_CONNECT_VERSION = "0.1.1";
4085
+ const QODER_CONNECT_VERSION = "0.1.4";
4006
4086
  //#endregion
4007
4087
  //#region src/host-heartbeat.ts
4008
4088
  /**
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-qoder-connect",
3
3
  "displayName": "DSH Qoder Connect",
4
4
  "description": "将 Qoder(国内版 / 国际版)的模型接入 DeepSeek Harness — bring Qoder (CN & international) models into DeepSeek Harness.",
5
- "version": "0.1.1",
5
+ "version": "0.1.4",
6
6
  "author": "corrinehu",
7
7
  "keywords": [
8
8
  "dsh-plugin",
@@ -23,7 +23,7 @@
23
23
  "scripts": {
24
24
  "prepack": "npm run build",
25
25
  "build": "tsdown",
26
- "test": "vitest run",
26
+ "test": "node -r ./tests/sandbox-shim.cjs ./node_modules/vitest/vitest.mjs run",
27
27
  "test:qoder": "node --test --test-isolation=none --experimental-strip-types tests/qoder/*.test.ts",
28
28
  "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
29
29
  "check": "pnpm run typecheck && pnpm run test && pnpm run test:qoder && pnpm run build"