pi-provider-qoder-cn 0.2.18 → 0.2.20

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/README.md +8 -0
  2. package/dist/index.js +90 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -133,6 +133,14 @@ Notes:
133
133
  agree with the numbers on the same row. The API's own `percentage` field is a
134
134
  0-1 fraction (94% arrives as `0.94`) and is only used — scaled by 100 — when a
135
135
  bucket comes back without numbers.
136
+ - `Add-on expiry` is only printed when a real check-in grant exists. A pool that
137
+ is a whole stack of daily grant packs (never more packs than the validity
138
+ window holds) is laid out as `Rolling 30d · earliest of N daily 100-credit
139
+ packs ~DATE`. A pool that is not one (a purchased top-up, or a leftover
140
+ remainder) prints only the expiry the campaign actually reported, `DATE ·
141
+ latest grant`, and an account that has never claimed prints no expiry line at
142
+ all, because with no grant there is nothing to date from, and inventing one
143
+ would show dates the API never sent.
136
144
  - `Expires never` means the API reported the year-9999 sentinel, i.e. the
137
145
  allowance has no expiry date.
138
146
  - `Note plan quota is prorated` appears after a mid-cycle plan change.
package/dist/index.js CHANGED
@@ -2388,6 +2388,28 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "c
2388
2388
  `- Next step: run /login qoder. You can also refresh credentials by exporting QODER_PERSONAL_ACCESS_TOKEN (PAT).`
2389
2389
  ].join("\n");
2390
2390
  }
2391
+ if (code === "103" || /duplicate request/i.test(message)) {
2392
+ return isCn ? [
2393
+ `[Qoder \u91CD\u590D\u8BF7\u6C42] \u7F51\u5173\u628A\u8FD9\u6B21\u8BF7\u6C42\u5224\u5B9A\u4E3A\u91CD\u653E\u5E76\u62D2\u7EDD (\u9519\u8BEF\u7801 103 Duplicate request)`,
2394
+ `- \u539F\u56E0\uFF1A\u540C\u4E00\u4E2A\u5DF2\u7B7E\u540D\u7684\u8BF7\u6C42\u88AB\u518D\u6B21\u63D0\u4EA4\uFF08COSY Authorization/\u65F6\u95F4\u6233/\u7B7E\u540D \u672A\u5237\u65B0\uFF09\uFF0C\u5E76\u975E\u989D\u5EA6\u6216\u51ED\u8BC1\u95EE\u9898\u3002`,
2395
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u5BA2\u6237\u7AEF\u5E94\u4E3A\u6BCF\u6B21\u91CD\u8BD5\u91CD\u65B0\u7B7E\u540D\uFF1B\u82E5\u4ECD\u590D\u73B0\uFF0C\u8BF7\u7528 /model \u5207\u6362 Provider \u7A0D\u540E\u91CD\u8BD5\u3002`
2396
+ ].join("\n") : [
2397
+ `[Qoder duplicate request] The gateway rejected this request as a replay (code 103 Duplicate request)`,
2398
+ `- Reason: an already-signed request (same COSY Authorization/timestamp/signature) was submitted again. This is a transient server error, not a quota or credential problem.`,
2399
+ `- Next step: every retry must be re-signed; if it keeps repeating, switch provider with /model and try again shortly.`
2400
+ ].join("\n");
2401
+ }
2402
+ if (code === "10605" || /"isQueued"\s*:\s*true|isQueued/.test(message)) {
2403
+ return isCn ? [
2404
+ `[Qoder CN \u4E0A\u6E38\u6392\u961F] \u6A21\u578B\u7F51\u5173\u5F53\u524D\u5BB9\u91CF\u5DF2\u6EE1\uFF0C\u8BF7\u6C42\u8FDB\u5165\u7B49\u5F85\u961F\u5217 (\u9519\u8BEF\u7801 10605)`,
2405
+ `- \u8BF4\u660E\uFF1A\u51ED\u8BC1\u4E0E\u989D\u5EA6\u5747\u6B63\u5E38\uFF0C\u7EAF\u7CB9\u662F\u4E0A\u6E38\u6392\u961F\uFF1B\u5DF2\u6309\u7F51\u5173\u7ED9\u51FA\u7684 Retry-After \u91CD\u8BD5\u4ECD\u672A\u653E\u884C\u3002`,
2406
+ `- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 /model \u5207\u6362\u5230\u5176\u4ED6 Provider\uFF08\u5982\u5176\u4ED6\u6A21\u578B/\u6E20\u9053\uFF09\u3002`
2407
+ ].join("\n") : [
2408
+ `[Qoder upstream queue] The model gateway is at capacity and parked this request (code 10605 isQueued)`,
2409
+ `- Meaning: credentials and quota are fine \u2014 this is pure upstream queuing; the gateway's own Retry-After wait did not clear it.`,
2410
+ `- Next step: retry shortly, or switch provider with /model while the queue drains.`
2411
+ ].join("\n");
2412
+ }
2391
2413
  if (rawBody === void 0 || rawBody === null) {
2392
2414
  return `Upstream status ${statusCode}`;
2393
2415
  }
@@ -2402,30 +2424,39 @@ function isCredentialExpiredError(error) {
2402
2424
 
2403
2425
  // src/commands/usage.ts
2404
2426
  init_region();
2405
- function estimateRollingAddOnExpiry(addOn, latestExpiryMs, now = Date.now()) {
2427
+ var DEFAULT_GRANT_CREDITS = 100;
2428
+ var DEFAULT_GRANT_VALIDITY_DAYS = 30;
2429
+ function positiveOr(value, fallback) {
2430
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
2431
+ }
2432
+ function estimateRollingAddOnExpiry(addOn, grant, now = Date.now()) {
2406
2433
  if (!addOn || !addOn.total || addOn.total <= 0 || !addOn.remaining || addOn.remaining <= 0) {
2407
2434
  return null;
2408
2435
  }
2409
- const packSize = 100;
2410
- const totalPacks = Math.max(1, Math.round(addOn.total / packSize));
2411
- if (totalPacks > 50) return null;
2436
+ const shape = typeof grant === "number" ? { expiryMs: grant } : grant ?? {};
2437
+ const packSize = positiveOr(shape.amount, DEFAULT_GRANT_CREDITS);
2438
+ const validityDays = positiveOr(shape.validityDays, DEFAULT_GRANT_VALIDITY_DAYS);
2439
+ const anchorMs = shape.expiryMs;
2440
+ if (typeof anchorMs !== "number" || !Number.isFinite(anchorMs) || anchorMs <= now) return null;
2441
+ if (addOn.total % packSize !== 0) return null;
2442
+ const totalPacks = Math.round(addOn.total / packSize);
2443
+ if (totalPacks < 1 || totalPacks > validityDays) return null;
2412
2444
  const used = Math.max(0, addOn.used ?? 0);
2413
2445
  const consumedPacks = Math.min(totalPacks - 1, Math.floor(used / packSize));
2414
- const earliestActiveIndex = consumedPacks;
2415
- const daysAgoClaimed = totalPacks - 1 - earliestActiveIndex;
2416
- const anchorExpiryMs = latestExpiryMs && Number.isFinite(latestExpiryMs) ? latestExpiryMs : now + 30 * 864e5;
2417
- const earliestMs = anchorExpiryMs - daysAgoClaimed * 864e5;
2446
+ const daysAgoClaimed = totalPacks - 1 - consumedPacks;
2447
+ const earliestMs = anchorMs - daysAgoClaimed * 864e5;
2418
2448
  const daysRemaining = Math.max(0, Math.round((earliestMs - now) / 864e5));
2419
2449
  const earliestRemaining = Math.min(addOn.remaining, packSize - used % packSize);
2420
2450
  const d = new Date(earliestMs);
2421
- const earliestDate = d.toISOString().slice(0, 10);
2422
2451
  return {
2423
- earliestDate,
2452
+ earliestDate: d.toISOString().slice(0, 10),
2424
2453
  earliestMs,
2425
2454
  earliestRemaining,
2426
2455
  totalPacks,
2427
2456
  consumedPacks,
2428
- daysRemaining
2457
+ daysRemaining,
2458
+ packSize,
2459
+ validityDays
2429
2460
  };
2430
2461
  }
2431
2462
  var ANSI2 = {
@@ -2607,14 +2638,17 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
2607
2638
  }
2608
2639
  const addOn = pickBucket2(raw, "addOnQuota");
2609
2640
  if (showAddOnQuota && (addOn?.remaining ?? 0) > 0 && checkin) {
2610
- const latestExpiryMs = checkin.expiresAt ? Date.parse(checkin.expiresAt) : void 0;
2611
- const estimate = estimateRollingAddOnExpiry(addOn, latestExpiryMs, now);
2641
+ const expiryMs = checkin.expiresAt ? Date.parse(checkin.expiresAt) : void 0;
2642
+ const grant = { expiryMs, amount: checkin.amount, validityDays: checkin.validityDays };
2643
+ const estimate = estimateRollingAddOnExpiry(addOn, grant, now);
2612
2644
  if (estimate) {
2613
- const earliestText = estimate.totalPacks > 1 ? `earliest active pack ~${estimate.earliestDate} \xB7 in ~${estimate.daysRemaining}d (~${formatAmount(estimate.earliestRemaining)} credits)` : `${estimate.earliestDate} (in ${estimate.daysRemaining}d)`;
2645
+ const expiryText = estimate.totalPacks > 1 ? `earliest of ${estimate.totalPacks} daily ${estimate.packSize}-credit packs ~${estimate.earliestDate} (in ~${estimate.daysRemaining}d, ~${formatAmount(estimate.earliestRemaining)} credits)` : `${estimate.earliestDate} (in ${estimate.daysRemaining}d)`;
2614
2646
  notes.push({
2615
2647
  label: "Add-on expiry",
2616
- value: `Rolling 30d (${earliestText})`
2648
+ value: `Rolling ${estimate.validityDays}d \xB7 ${expiryText}`
2617
2649
  });
2650
+ } else if (typeof expiryMs === "number" && Number.isFinite(expiryMs) && expiryMs > now) {
2651
+ notes.push({ label: "Add-on expiry", value: `${formatResetTime(expiryMs, now)} \xB7 latest grant` });
2618
2652
  }
2619
2653
  }
2620
2654
  notes.push({ label: "Expires", value: formatResetTime(raw?.expiresAt, now) });
@@ -3176,10 +3210,12 @@ function stableHash(prefix, ...inputs) {
3176
3210
  }
3177
3211
  return hash.digest("hex").slice(0, 16);
3178
3212
  }
3179
- function stableChatRecordID(model, messages, tools, maxTokens) {
3213
+ function stableChatRecordID(model, messages, tools, maxTokens, salt) {
3180
3214
  const hash = crypto3.createHash("sha256");
3181
3215
  hash.update("qoder-record");
3182
3216
  hash.update("\0");
3217
+ hash.update(salt);
3218
+ hash.update("\0");
3183
3219
  hash.update(model);
3184
3220
  for (const msg of messages) {
3185
3221
  if (msg?.role) {
@@ -3360,7 +3396,13 @@ function streamQoder(model, context, options) {
3360
3396
  maxTokens = options.maxTokens;
3361
3397
  }
3362
3398
  const toolsRaw = resolved.tools.length > 0 ? transformTools(resolved.tools) : void 0;
3363
- const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
3399
+ const recordID = stableChatRecordID(
3400
+ qoderModel,
3401
+ normalizedMessages,
3402
+ toolsRaw,
3403
+ maxTokens,
3404
+ `${sessionID}:${userID}`
3405
+ );
3364
3406
  const requestedLevel = options?.reasoning;
3365
3407
  const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
3366
3408
  const reasoningLevel = clamped === "off" ? void 0 : clamped;
@@ -3375,7 +3417,9 @@ function streamQoder(model, context, options) {
3375
3417
  } else {
3376
3418
  parameters.enable_thinking = false;
3377
3419
  }
3378
- const reqBody = {
3420
+ const modelSource = modelConfig.source || "system";
3421
+ const chatURL = getQoderChatURL(providerMode);
3422
+ const buildRequestBody = (attempt) => ({
3379
3423
  request_id: crypto3.randomUUID(),
3380
3424
  request_set_id: recordID,
3381
3425
  chat_record_id: recordID,
@@ -3383,7 +3427,7 @@ function streamQoder(model, context, options) {
3383
3427
  stream: true,
3384
3428
  chat_task: "FREE_INPUT",
3385
3429
  is_reply: true,
3386
- is_retry: false,
3430
+ is_retry: attempt > 0,
3387
3431
  source: 1,
3388
3432
  version: "3",
3389
3433
  session_type: "qodercli",
@@ -3424,18 +3468,31 @@ function streamQoder(model, context, options) {
3424
3468
  name: lastUserText.substring(0, 30),
3425
3469
  begin_at: Date.now()
3426
3470
  }
3427
- };
3428
- const bodyBytes = Buffer.from(JSON.stringify(reqBody));
3429
- const encodedBytes = qoderEncodeBody(bodyBytes);
3430
- const chatURL = getQoderChatURL(providerMode);
3431
- const headers = buildAuthHeaders(encodedBytes, chatURL, {
3432
- userID,
3433
- authToken: accessToken,
3434
- name,
3435
- email,
3436
- machineID
3437
3471
  });
3438
- const modelSource = modelConfig.source || "system";
3472
+ const buildAttempt = (attempt) => {
3473
+ const encoded = qoderEncodeBody(Buffer.from(JSON.stringify(buildRequestBody(attempt))));
3474
+ return {
3475
+ encoded,
3476
+ headers: {
3477
+ "Content-Type": "application/json",
3478
+ Accept: "text/event-stream",
3479
+ "Cache-Control": "no-cache",
3480
+ "Accept-Encoding": "identity",
3481
+ "X-Model-Key": qoderModel,
3482
+ "X-Model-Source": modelSource,
3483
+ // Re-signed per attempt: the COSY payload carries its own requestId,
3484
+ // a second-granularity timestamp and a signature over the body, and
3485
+ // reusing any of them is what the gateway reads as a replay.
3486
+ ...buildAuthHeaders(encoded, chatURL, {
3487
+ userID,
3488
+ authToken: accessToken,
3489
+ name,
3490
+ email,
3491
+ machineID
3492
+ })
3493
+ }
3494
+ };
3495
+ };
3439
3496
  let queuedAttempt = 0;
3440
3497
  await reader?.cancel().catch(() => {
3441
3498
  });
@@ -3444,18 +3501,11 @@ function streamQoder(model, context, options) {
3444
3501
  let buffer = "";
3445
3502
  let bufferStart = 0;
3446
3503
  for (; ; ) {
3504
+ const current = buildAttempt(queuedAttempt);
3447
3505
  const response = await fetch(chatURL, {
3448
3506
  method: "POST",
3449
- headers: {
3450
- "Content-Type": "application/json",
3451
- Accept: "text/event-stream",
3452
- "Cache-Control": "no-cache",
3453
- "Accept-Encoding": "identity",
3454
- "X-Model-Key": qoderModel,
3455
- "X-Model-Source": modelSource,
3456
- ...headers
3457
- },
3458
- body: encodedBytes,
3507
+ headers: current.headers,
3508
+ body: current.encoded,
3459
3509
  signal: options?.signal
3460
3510
  });
3461
3511
  if (!response.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-qoder-cn",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "description": "Pi extension for Qoder CN — PAT/OAuth auth, /qoder-cn.usage quota command, and enterprise VPC endpoint support",
5
5
  "type": "module",
6
6
  "license": "MIT",