dsh-qoder-connect 0.1.2 → 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-C0iZD8AD.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";
@@ -1290,16 +1290,42 @@ function emitJobTokenHint(at, text) {
1290
1290
  const agent = lastRunningAgent;
1291
1291
  if (agent?.session === void 0) return;
1292
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;
1293
1319
  const commandId = `cmd-qoder-hint-${Date.now().toString(36)}-${++hintSeq}`;
1294
1320
  try {
1295
- agent.session.append("command/run", {
1321
+ session.append("command/run", {
1296
1322
  commandId,
1297
1323
  name: JOB_TOKEN_HINT_NAME,
1298
1324
  source: { kind: "user" }
1299
1325
  });
1300
- agent.session.append("command/done", {
1326
+ session.append("command/done", {
1301
1327
  commandId,
1302
- kind: "success",
1328
+ kind,
1303
1329
  text
1304
1330
  });
1305
1331
  } catch {}
@@ -1475,6 +1501,17 @@ function fallbackFor(_variant) {
1475
1501
  function jobTokenHintText(at) {
1476
1502
  return `jobToken 已自动刷新(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})— 旧令牌被上游拒绝,已自动重换并恢复`;
1477
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
+ }
1478
1515
  /** Build one variant's stores, transport, and probe state. */
1479
1516
  function createVariantRuntime(ctx, config, variant, current, identityOf) {
1480
1517
  const store = new QoderCredentialStore({
@@ -1490,6 +1527,10 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
1490
1527
  jobTokenRefreshedAt = info.at;
1491
1528
  ctx.logger.warn(`dsh-qoder-connect: ${variant.displayName} job token was auto-refreshed after an upstream rejection`);
1492
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));
1493
1534
  }
1494
1535
  });
1495
1536
  const client = new QoderUpstreamClient({
@@ -2623,7 +2623,10 @@ function parseEnvelope(rawData) {
2623
2623
  const envelope = value;
2624
2624
  if (envelope.statusCodeValue !== void 0 && typeof envelope.statusCodeValue !== "number") throw malformed("Qoder SSE envelope has an invalid status code.");
2625
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.");
2626
- 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
+ }
2627
2630
  if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
2628
2631
  return envelope;
2629
2632
  }
@@ -3030,6 +3033,18 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
3030
3033
  requestController.abort("request complete");
3031
3034
  }
3032
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;
3033
3048
  function aborted(message) {
3034
3049
  return new QoderLlmError(message, "ABORTED");
3035
3050
  }
@@ -3048,6 +3063,7 @@ var DefaultQoderTransport = class {
3048
3063
  imageUploader;
3049
3064
  preserveThinking;
3050
3065
  onJobTokenRefreshed;
3066
+ onJobTokenRefreshFailed;
3051
3067
  /**
3052
3068
  * Set when the self-heal exchanged a fresh job token, cleared when a chat
3053
3069
  * afterwards succeeds. The notice reports "the token was rotated because the
@@ -3056,6 +3072,16 @@ var DefaultQoderTransport = class {
3056
3072
  * transient upstream timeout and be rescued by a later attempt.
3057
3073
  */
3058
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;
3059
3085
  constructor(options) {
3060
3086
  this.region = options.region;
3061
3087
  this.resolvePat = options.resolvePat;
@@ -3067,6 +3093,7 @@ var DefaultQoderTransport = class {
3067
3093
  this.attachments = options.attachments;
3068
3094
  this.preserveThinking = options.preserveThinking;
3069
3095
  this.onJobTokenRefreshed = options.onJobTokenRefreshed;
3096
+ this.onJobTokenRefreshFailed = options.onJobTokenRefreshFailed;
3070
3097
  this.auth = new QoderAuthService({
3071
3098
  fetch: this.fetchImpl,
3072
3099
  logger: this.logger,
@@ -3107,12 +3134,35 @@ var DefaultQoderTransport = class {
3107
3134
  if (this.pendingRefreshAt === void 0) return;
3108
3135
  const at = this.pendingRefreshAt;
3109
3136
  this.pendingRefreshAt = void 0;
3137
+ if (Date.now() - at > jobTokenNoticeMaxAgeMs) return;
3110
3138
  this.logger?.warn?.("[Qoder Stream] Job token was auto-refreshed after an upstream rejection; the chat has recovered");
3111
3139
  this.onJobTokenRefreshed?.({
3112
3140
  region: this.region,
3113
3141
  at
3114
3142
  });
3115
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
+ }
3116
3166
  async discoverModels(signal) {
3117
3167
  const pat = await this.requirePat(signal);
3118
3168
  const key = opaqueCredentialKey(pat);
@@ -3178,7 +3228,7 @@ var DefaultQoderTransport = class {
3178
3228
  const chatCredentials = await this.auth.getCredentials(pat, options.signal);
3179
3229
  try {
3180
3230
  yield* this.streamChat(options, model, chatCredentials, messages, () => {
3181
- this.flushPendingRefreshNotice();
3231
+ this.onChatAccepted();
3182
3232
  });
3183
3233
  return;
3184
3234
  } catch (error) {
@@ -3191,7 +3241,7 @@ var DefaultQoderTransport = class {
3191
3241
  this.pendingRefreshAt = Date.now();
3192
3242
  try {
3193
3243
  yield* this.streamChat(options, model, refreshed, messages, () => {
3194
- this.flushPendingRefreshNotice();
3244
+ this.onChatAccepted();
3195
3245
  });
3196
3246
  return;
3197
3247
  } catch (error) {
@@ -3212,6 +3262,16 @@ var DefaultQoderTransport = class {
3212
3262
  }
3213
3263
  }
3214
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
+ }
3215
3275
  throw lastRejection;
3216
3276
  }
3217
3277
  };
@@ -3618,14 +3678,23 @@ var ChunkEncoder = class {
3618
3678
  }] })];
3619
3679
  }
3620
3680
  case "usage": {
3621
- 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);
3622
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
+ };
3623
3690
  return [this.frame({
3624
3691
  choices: [],
3625
3692
  usage: {
3626
3693
  prompt_tokens: prompt,
3627
3694
  completion_tokens: completion,
3628
- 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 } }
3629
3698
  }
3630
3699
  })];
3631
3700
  }
@@ -4013,7 +4082,7 @@ const DEFAULT_MAX_TOKENS = 32768;
4013
4082
  const DEFAULT_CONTEXT_WINDOW = 18e4;
4014
4083
  //#endregion
4015
4084
  //#region src/version.ts
4016
- const QODER_CONNECT_VERSION = "0.1.2";
4085
+ const QODER_CONNECT_VERSION = "0.1.4";
4017
4086
  //#endregion
4018
4087
  //#region src/host-heartbeat.ts
4019
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.2",
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"