dsh-qoder-connect 0.1.2 → 0.1.5

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.
@@ -876,6 +876,14 @@ function getQoderUserStatusUrl(region = "global") {
876
876
  const { openApiUrl } = resolveQoderEndpoints(region);
877
877
  return `${openApiUrl}/api/v3/user/status`;
878
878
  }
879
+ function getQoderCampaignsUrl(region = "global") {
880
+ const { openApiUrl } = resolveQoderEndpoints(region);
881
+ return `${openApiUrl}/sash/api/v1/me/campaigns`;
882
+ }
883
+ function getQoderClaimCampaignUrl(region = "global", campaignId) {
884
+ const { openApiUrl } = resolveQoderEndpoints(region);
885
+ return `${openApiUrl}/sash/api/v1/me/campaigns/${encodeURIComponent(campaignId)}/claim`;
886
+ }
879
887
  //#endregion
880
888
  //#region src/qoder/errors.ts
881
889
  /**
@@ -2408,6 +2416,131 @@ var QoderUsageReader = class {
2408
2416
  }
2409
2417
  };
2410
2418
  //#endregion
2419
+ //#region src/qoder/transport/checkin.ts
2420
+ const defaultCheckInTimeoutMs = 15e3;
2421
+ var QoderCheckInService = class {
2422
+ authService;
2423
+ fetchImpl;
2424
+ region;
2425
+ variantId;
2426
+ logger;
2427
+ timeoutMs;
2428
+ constructor(options) {
2429
+ this.authService = options.authService;
2430
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
2431
+ this.region = options.region ?? "china";
2432
+ this.variantId = options.variantId ?? "qoder";
2433
+ this.logger = options.logger;
2434
+ this.timeoutMs = options.timeoutMs ?? defaultCheckInTimeoutMs;
2435
+ }
2436
+ async fetchCampaigns(token, signal) {
2437
+ const url = getQoderCampaignsUrl(this.region);
2438
+ const data = await openApiJsonRequest(this.fetchImpl, {
2439
+ url,
2440
+ token,
2441
+ signal,
2442
+ timeoutMs: this.timeoutMs,
2443
+ logger: this.logger,
2444
+ operation: "Campaigns",
2445
+ logCategory: "campaigns.list"
2446
+ });
2447
+ return Array.isArray(data?.campaigns) ? data.campaigns : [];
2448
+ }
2449
+ async claimCampaign(token, campaignId, signal) {
2450
+ const url = getQoderClaimCampaignUrl(this.region, campaignId);
2451
+ const { openApiUrl } = resolveQoderEndpoints(this.region);
2452
+ return openApiJsonRequest(this.fetchImpl, {
2453
+ url,
2454
+ method: "POST",
2455
+ token,
2456
+ headers: { origin: openApiUrl },
2457
+ signal,
2458
+ timeoutMs: this.timeoutMs,
2459
+ logger: this.logger,
2460
+ operation: "ClaimCampaign",
2461
+ logCategory: "campaigns.claim"
2462
+ });
2463
+ }
2464
+ getTodayDateString() {
2465
+ const now = /* @__PURE__ */ new Date();
2466
+ const utc8Time = new Date(now.getTime() + (now.getTimezoneOffset() + 480) * 6e4);
2467
+ return `${utc8Time.getFullYear()}-${String(utc8Time.getMonth() + 1).padStart(2, "0")}-${String(utc8Time.getDate()).padStart(2, "0")}`;
2468
+ }
2469
+ async checkIn(pat, signal) {
2470
+ const today = this.getTodayDateString();
2471
+ const nowMs = Date.now();
2472
+ if (!pat || typeof pat !== "string" || pat.trim() === "") return {
2473
+ variantId: this.variantId,
2474
+ date: today,
2475
+ timestamp: nowMs,
2476
+ status: "error",
2477
+ message: "No PAT available"
2478
+ };
2479
+ const executeWithToken = async (authToken) => {
2480
+ const benefitCampaign = (await this.fetchCampaigns(authToken, signal)).find((c) => c.actionType === "CLAIM_BENEFIT");
2481
+ if (!benefitCampaign) return {
2482
+ variantId: this.variantId,
2483
+ date: today,
2484
+ timestamp: nowMs,
2485
+ status: "no-campaign",
2486
+ message: "No claimable benefit campaign found for this region"
2487
+ };
2488
+ if (benefitCampaign.claimStatus === "CLAIMED") return {
2489
+ variantId: this.variantId,
2490
+ date: today,
2491
+ timestamp: nowMs,
2492
+ status: "already-claimed",
2493
+ campaignKey: benefitCampaign.campaignKey,
2494
+ amount: benefitCampaign.benefit?.amount ?? 100,
2495
+ message: "Already claimed today"
2496
+ };
2497
+ const claimResult = await this.claimCampaign(authToken, benefitCampaign.campaignId, signal);
2498
+ const isClaimed = claimResult.status === "CLAIMED";
2499
+ const replayed = Boolean(claimResult.replayed);
2500
+ const amount = claimResult.benefit?.amount ?? benefitCampaign.benefit?.amount ?? 100;
2501
+ if (isClaimed) return {
2502
+ variantId: this.variantId,
2503
+ date: today,
2504
+ timestamp: nowMs,
2505
+ status: replayed ? "already-claimed" : "claimed",
2506
+ amount,
2507
+ campaignKey: benefitCampaign.campaignKey,
2508
+ message: replayed ? "Already claimed today" : `Successfully claimed ${amount} credits`
2509
+ };
2510
+ return {
2511
+ variantId: this.variantId,
2512
+ date: today,
2513
+ timestamp: nowMs,
2514
+ status: "error",
2515
+ campaignKey: benefitCampaign.campaignKey,
2516
+ message: `Claim returned status ${claimResult.status ?? "unknown"}`
2517
+ };
2518
+ };
2519
+ try {
2520
+ const creds = await this.authService.getCredentials(pat, signal);
2521
+ try {
2522
+ return await executeWithToken(creds.authToken);
2523
+ } catch (innerError) {
2524
+ if (isQoderAuthRejection(innerError)) {
2525
+ this.logger?.warn?.(`[Qoder CheckIn] Auth token rejected, retrying with fresh exchange`);
2526
+ return await executeWithToken((await this.authService.exchangeFresh(pat, signal)).authToken);
2527
+ }
2528
+ throw innerError;
2529
+ }
2530
+ } catch (error) {
2531
+ const message = error instanceof Error ? error.message : String(error);
2532
+ this.logger?.error?.(`[Qoder CheckIn] Check-in failed: ${message}`);
2533
+ return {
2534
+ variantId: this.variantId,
2535
+ date: today,
2536
+ timestamp: nowMs,
2537
+ status: "error",
2538
+ message
2539
+ };
2540
+ }
2541
+ }
2542
+ };
2543
+ //#endregion
2411
2544
  //#region src/qoder/transport/wire/encoding.ts
2412
2545
  /**
2413
2546
  * Qoder WAF Body encoding algorithm.
@@ -2623,7 +2756,10 @@ function parseEnvelope(rawData) {
2623
2756
  const envelope = value;
2624
2757
  if (envelope.statusCodeValue !== void 0 && typeof envelope.statusCodeValue !== "number") throw malformed("Qoder SSE envelope has an invalid status code.");
2625
2758
  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 });
2759
+ if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) {
2760
+ const status = envelope.statusCodeValue;
2761
+ throw qoderHttpError(envelope.body ? `Qoder service returned upstream error status ${status}: ${envelope.body}` : `Qoder service returned upstream error status ${status}.`, { status });
2762
+ }
2627
2763
  if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
2628
2764
  return envelope;
2629
2765
  }
@@ -3030,6 +3166,18 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
3030
3166
  requestController.abort("request complete");
3031
3167
  }
3032
3168
  }
3169
+ /**
3170
+ * How long a rotated-token notice may wait for an accepting chat.
3171
+ *
3172
+ * The heal's own retry can lose a race with a transient fault and be rescued
3173
+ * by a later attempt, so the notice is deferred until some chat is accepted.
3174
+ * That deferral has to be bounded: an unbounded one let a rotation from
3175
+ * minutes earlier surface as a fresh-looking row whose timestamp named a
3176
+ * moment the user could not connect to the row appearing now. Five minutes
3177
+ * comfortably covers the host's own retry cycle (its backoff caps at 10s)
3178
+ * while keeping the row adjacent to the event it describes.
3179
+ */
3180
+ const jobTokenNoticeMaxAgeMs = 3e5;
3033
3181
  function aborted(message) {
3034
3182
  return new QoderLlmError(message, "ABORTED");
3035
3183
  }
@@ -3043,11 +3191,13 @@ var DefaultQoderTransport = class {
3043
3191
  metadataTimeoutMs;
3044
3192
  auth;
3045
3193
  usage;
3194
+ checkInService;
3046
3195
  modelFlights = new SingleFlight();
3047
3196
  attachments;
3048
3197
  imageUploader;
3049
3198
  preserveThinking;
3050
3199
  onJobTokenRefreshed;
3200
+ onJobTokenRefreshFailed;
3051
3201
  /**
3052
3202
  * Set when the self-heal exchanged a fresh job token, cleared when a chat
3053
3203
  * afterwards succeeds. The notice reports "the token was rotated because the
@@ -3056,6 +3206,16 @@ var DefaultQoderTransport = class {
3056
3206
  * transient upstream timeout and be rescued by a later attempt.
3057
3207
  */
3058
3208
  pendingRefreshAt;
3209
+ /**
3210
+ * Whether the current unresolved heal failure has already been reported.
3211
+ *
3212
+ * One upstream rejection can outlive many host-level retries (an observed
3213
+ * storm ran 59 of them across 75 steps), and the transport re-heals inside
3214
+ * every one of them. Without this latch the failure notice would print once
3215
+ * per retry; with it, the user is told once per outage. Cleared as soon as
3216
+ * any chat is accepted, so the next outage announces itself again.
3217
+ */
3218
+ refreshFailureAnnounced = false;
3059
3219
  constructor(options) {
3060
3220
  this.region = options.region;
3061
3221
  this.resolvePat = options.resolvePat;
@@ -3067,6 +3227,7 @@ var DefaultQoderTransport = class {
3067
3227
  this.attachments = options.attachments;
3068
3228
  this.preserveThinking = options.preserveThinking;
3069
3229
  this.onJobTokenRefreshed = options.onJobTokenRefreshed;
3230
+ this.onJobTokenRefreshFailed = options.onJobTokenRefreshFailed;
3070
3231
  this.auth = new QoderAuthService({
3071
3232
  fetch: this.fetchImpl,
3072
3233
  logger: this.logger,
@@ -3092,6 +3253,13 @@ var DefaultQoderTransport = class {
3092
3253
  region: this.region,
3093
3254
  timeoutMs: this.metadataTimeoutMs
3094
3255
  });
3256
+ this.checkInService = new QoderCheckInService({
3257
+ authService: this.auth,
3258
+ fetch: this.fetchImpl,
3259
+ logger: this.logger,
3260
+ region: this.region,
3261
+ timeoutMs: this.metadataTimeoutMs
3262
+ });
3095
3263
  }
3096
3264
  stream(options, model) {
3097
3265
  return this.generate(options, model);
@@ -3107,12 +3275,35 @@ var DefaultQoderTransport = class {
3107
3275
  if (this.pendingRefreshAt === void 0) return;
3108
3276
  const at = this.pendingRefreshAt;
3109
3277
  this.pendingRefreshAt = void 0;
3278
+ if (Date.now() - at > jobTokenNoticeMaxAgeMs) return;
3110
3279
  this.logger?.warn?.("[Qoder Stream] Job token was auto-refreshed after an upstream rejection; the chat has recovered");
3111
3280
  this.onJobTokenRefreshed?.({
3112
3281
  region: this.region,
3113
3282
  at
3114
3283
  });
3115
3284
  }
3285
+ /**
3286
+ * Drop a pending rotation notice whose heal did not rescue anything.
3287
+ *
3288
+ * The heal's own retry was rejected too, so "the rotation fixed it" is not
3289
+ * what happened. Leaving the notice pending made a LATER, unrelated chat
3290
+ * acceptance flush it — printing a success row that contradicts the failure
3291
+ * row already shown, stamped with the old rotation time.
3292
+ */
3293
+ discardPendingRefreshNotice() {
3294
+ this.pendingRefreshAt = void 0;
3295
+ }
3296
+ /**
3297
+ * Note that the upstream accepted a chat, ending any unresolved heal failure.
3298
+ *
3299
+ * `streamChat` calls this the moment its first chunk arrives — the only point
3300
+ * at which "the credential was accepted" is actually known. A rejected chat
3301
+ * throws before that, so this never fires on a failing attempt.
3302
+ */
3303
+ onChatAccepted() {
3304
+ this.refreshFailureAnnounced = false;
3305
+ this.flushPendingRefreshNotice();
3306
+ }
3116
3307
  async discoverModels(signal) {
3117
3308
  const pat = await this.requirePat(signal);
3118
3309
  const key = opaqueCredentialKey(pat);
@@ -3134,6 +3325,10 @@ var DefaultQoderTransport = class {
3134
3325
  signal: options?.signal
3135
3326
  });
3136
3327
  }
3328
+ async checkIn(signal) {
3329
+ const pat = await this.requirePat(signal);
3330
+ return this.checkInService.checkIn(pat, signal);
3331
+ }
3137
3332
  async requirePat(signal) {
3138
3333
  if (signal?.aborted) throw aborted("Qoder request was aborted.");
3139
3334
  const pat = (await this.resolvePat()).trim();
@@ -3178,7 +3373,7 @@ var DefaultQoderTransport = class {
3178
3373
  const chatCredentials = await this.auth.getCredentials(pat, options.signal);
3179
3374
  try {
3180
3375
  yield* this.streamChat(options, model, chatCredentials, messages, () => {
3181
- this.flushPendingRefreshNotice();
3376
+ this.onChatAccepted();
3182
3377
  });
3183
3378
  return;
3184
3379
  } catch (error) {
@@ -3191,7 +3386,7 @@ var DefaultQoderTransport = class {
3191
3386
  this.pendingRefreshAt = Date.now();
3192
3387
  try {
3193
3388
  yield* this.streamChat(options, model, refreshed, messages, () => {
3194
- this.flushPendingRefreshNotice();
3389
+ this.onChatAccepted();
3195
3390
  });
3196
3391
  return;
3197
3392
  } catch (error) {
@@ -3212,6 +3407,16 @@ var DefaultQoderTransport = class {
3212
3407
  }
3213
3408
  }
3214
3409
  }
3410
+ this.discardPendingRefreshNotice();
3411
+ if (!this.refreshFailureAnnounced) {
3412
+ this.refreshFailureAnnounced = true;
3413
+ 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 });
3414
+ this.onJobTokenRefreshFailed?.({
3415
+ region: this.region,
3416
+ at: Date.now(),
3417
+ ...lastRejection instanceof LlmError && lastRejection.failure.status !== void 0 ? { status: lastRejection.failure.status } : {}
3418
+ });
3419
+ }
3215
3420
  throw lastRejection;
3216
3421
  }
3217
3422
  };
@@ -3618,14 +3823,23 @@ var ChunkEncoder = class {
3618
3823
  }] })];
3619
3824
  }
3620
3825
  case "usage": {
3621
- const prompt = chunk.usage.inputTokens + (chunk.usage.cacheReadTokens ?? 0) + (chunk.usage.cacheWriteTokens ?? 0);
3826
+ const cacheRead = chunk.usage.cacheReadTokens;
3827
+ const cacheWrite = chunk.usage.cacheWriteTokens;
3828
+ const prompt = chunk.usage.inputTokens + (cacheRead ?? 0) + (cacheWrite ?? 0);
3622
3829
  const completion = chunk.usage.outputTokens;
3830
+ const reasoning = chunk.usage.reasoningTokens;
3831
+ const details = cacheRead === void 0 && cacheWrite === void 0 ? void 0 : {
3832
+ ...cacheRead === void 0 ? {} : { cached_tokens: cacheRead },
3833
+ ...cacheWrite === void 0 ? {} : { cache_write_tokens: cacheWrite }
3834
+ };
3623
3835
  return [this.frame({
3624
3836
  choices: [],
3625
3837
  usage: {
3626
3838
  prompt_tokens: prompt,
3627
3839
  completion_tokens: completion,
3628
- total_tokens: chunk.usage.totalTokens ?? prompt + completion
3840
+ total_tokens: chunk.usage.totalTokens ?? prompt + completion,
3841
+ ...details === void 0 ? {} : { prompt_tokens_details: details },
3842
+ ...reasoning === void 0 ? {} : { completion_tokens_details: { reasoning_tokens: reasoning } }
3629
3843
  }
3630
3844
  })];
3631
3845
  }
@@ -3779,6 +3993,12 @@ var QoderUpstreamClient = class {
3779
3993
  };
3780
3994
  }
3781
3995
  /**
3996
+ * Run daily benefit check-in for this client's variant.
3997
+ */
3998
+ async checkIn(signal) {
3999
+ return this.transport.checkIn(signal);
4000
+ }
4001
+ /**
3782
4002
  * Stream one translated chat-completions request.
3783
4003
  *
3784
4004
  * The first transport chunk is pulled before the response is constructed:
@@ -4013,7 +4233,7 @@ const DEFAULT_MAX_TOKENS = 32768;
4013
4233
  const DEFAULT_CONTEXT_WINDOW = 18e4;
4014
4234
  //#endregion
4015
4235
  //#region src/version.ts
4016
- const QODER_CONNECT_VERSION = "0.1.2";
4236
+ const QODER_CONNECT_VERSION = "0.1.5";
4017
4237
  //#endregion
4018
4238
  //#region src/host-heartbeat.ts
4019
4239
  /**
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.5",
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"