pi-provider-qoder-cn 0.2.19 → 0.2.21

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 (2) hide show
  1. package/dist/index.js +85 -28
  2. package/package.json +1 -1
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 CN \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
  }
@@ -2808,9 +2830,22 @@ function parseQueueNotice(envelope) {
2808
2830
  return {
2809
2831
  retryAfterMs: Math.min(retryMs, MAX_WAIT_MS),
2810
2832
  waitTimeSeconds: positiveNumber(payload.waitTime, 0),
2811
- queueCount: positiveNumber(payload.queueCount, 0)
2833
+ queueCount: positiveNumber(payload.queueCount, 0),
2834
+ // Only a real boolean is surfaced; a missing field must not be read as
2835
+ // "unavailable" (that would collapse the normal queue into a hard stop).
2836
+ serviceAvailable: typeof payload.serviceAvailable === "boolean" ? payload.serviceAvailable : void 0
2812
2837
  };
2813
2838
  }
2839
+ function formatQueueFigure(n) {
2840
+ if (!Number.isFinite(n) || n <= 0) return "0";
2841
+ if (n < 100) return String(Math.round(n));
2842
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2843
+ return `${(n / 1e3).toFixed(1)}k`;
2844
+ }
2845
+ function formatWaitMinutes(seconds) {
2846
+ if (!Number.isFinite(seconds) || seconds <= 0) return "<1";
2847
+ return String(Math.min(99, Math.max(1, Math.round(seconds / 60))));
2848
+ }
2814
2849
  function sleep(ms, signal) {
2815
2850
  return new Promise((resolve, reject) => {
2816
2851
  if (signal?.aborted) {
@@ -3188,10 +3223,12 @@ function stableHash(prefix, ...inputs) {
3188
3223
  }
3189
3224
  return hash.digest("hex").slice(0, 16);
3190
3225
  }
3191
- function stableChatRecordID(model, messages, tools, maxTokens) {
3226
+ function stableChatRecordID(model, messages, tools, maxTokens, salt) {
3192
3227
  const hash = crypto3.createHash("sha256");
3193
3228
  hash.update("qoder-record");
3194
3229
  hash.update("\0");
3230
+ hash.update(salt);
3231
+ hash.update("\0");
3195
3232
  hash.update(model);
3196
3233
  for (const msg of messages) {
3197
3234
  if (msg?.role) {
@@ -3372,7 +3409,13 @@ function streamQoder(model, context, options) {
3372
3409
  maxTokens = options.maxTokens;
3373
3410
  }
3374
3411
  const toolsRaw = resolved.tools.length > 0 ? transformTools(resolved.tools) : void 0;
3375
- const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
3412
+ const recordID = stableChatRecordID(
3413
+ qoderModel,
3414
+ normalizedMessages,
3415
+ toolsRaw,
3416
+ maxTokens,
3417
+ `${sessionID}:${userID}`
3418
+ );
3376
3419
  const requestedLevel = options?.reasoning;
3377
3420
  const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
3378
3421
  const reasoningLevel = clamped === "off" ? void 0 : clamped;
@@ -3387,7 +3430,9 @@ function streamQoder(model, context, options) {
3387
3430
  } else {
3388
3431
  parameters.enable_thinking = false;
3389
3432
  }
3390
- const reqBody = {
3433
+ const modelSource = modelConfig.source || "system";
3434
+ const chatURL = getQoderChatURL(providerMode);
3435
+ const buildRequestBody = (attempt) => ({
3391
3436
  request_id: crypto3.randomUUID(),
3392
3437
  request_set_id: recordID,
3393
3438
  chat_record_id: recordID,
@@ -3395,7 +3440,7 @@ function streamQoder(model, context, options) {
3395
3440
  stream: true,
3396
3441
  chat_task: "FREE_INPUT",
3397
3442
  is_reply: true,
3398
- is_retry: false,
3443
+ is_retry: attempt > 0,
3399
3444
  source: 1,
3400
3445
  version: "3",
3401
3446
  session_type: "qodercli",
@@ -3436,18 +3481,31 @@ function streamQoder(model, context, options) {
3436
3481
  name: lastUserText.substring(0, 30),
3437
3482
  begin_at: Date.now()
3438
3483
  }
3439
- };
3440
- const bodyBytes = Buffer.from(JSON.stringify(reqBody));
3441
- const encodedBytes = qoderEncodeBody(bodyBytes);
3442
- const chatURL = getQoderChatURL(providerMode);
3443
- const headers = buildAuthHeaders(encodedBytes, chatURL, {
3444
- userID,
3445
- authToken: accessToken,
3446
- name,
3447
- email,
3448
- machineID
3449
3484
  });
3450
- const modelSource = modelConfig.source || "system";
3485
+ const buildAttempt = (attempt) => {
3486
+ const encoded = qoderEncodeBody(Buffer.from(JSON.stringify(buildRequestBody(attempt))));
3487
+ return {
3488
+ encoded,
3489
+ headers: {
3490
+ "Content-Type": "application/json",
3491
+ Accept: "text/event-stream",
3492
+ "Cache-Control": "no-cache",
3493
+ "Accept-Encoding": "identity",
3494
+ "X-Model-Key": qoderModel,
3495
+ "X-Model-Source": modelSource,
3496
+ // Re-signed per attempt: the COSY payload carries its own requestId,
3497
+ // a second-granularity timestamp and a signature over the body, and
3498
+ // reusing any of them is what the gateway reads as a replay.
3499
+ ...buildAuthHeaders(encoded, chatURL, {
3500
+ userID,
3501
+ authToken: accessToken,
3502
+ name,
3503
+ email,
3504
+ machineID
3505
+ })
3506
+ }
3507
+ };
3508
+ };
3451
3509
  let queuedAttempt = 0;
3452
3510
  await reader?.cancel().catch(() => {
3453
3511
  });
@@ -3456,18 +3514,11 @@ function streamQoder(model, context, options) {
3456
3514
  let buffer = "";
3457
3515
  let bufferStart = 0;
3458
3516
  for (; ; ) {
3517
+ const current = buildAttempt(queuedAttempt);
3459
3518
  const response = await fetch(chatURL, {
3460
3519
  method: "POST",
3461
- headers: {
3462
- "Content-Type": "application/json",
3463
- Accept: "text/event-stream",
3464
- "Cache-Control": "no-cache",
3465
- "Accept-Encoding": "identity",
3466
- "X-Model-Key": qoderModel,
3467
- "X-Model-Source": modelSource,
3468
- ...headers
3469
- },
3470
- body: encodedBytes,
3520
+ headers: current.headers,
3521
+ body: current.encoded,
3471
3522
  signal: options?.signal
3472
3523
  });
3473
3524
  if (!response.ok) {
@@ -3484,9 +3535,15 @@ function streamQoder(model, context, options) {
3484
3535
  if (queue) {
3485
3536
  await reader.cancel().catch(() => {
3486
3537
  });
3487
- if (queuedAttempt >= MAX_QUEUE_RETRIES) {
3538
+ const stopEarly = queue.serviceAvailable === false;
3539
+ if (queuedAttempt >= (stopEarly ? 1 : MAX_QUEUE_RETRIES)) {
3540
+ if (stopEarly) {
3541
+ throw new Error(
3542
+ `Qoder \u4E0A\u6E38\u6682\u4E0D\u53EF\u670D\u52A1\uFF08\u7F51\u5173\u62A5 serviceAvailable:false\uFF1B\u961F\u5217\u4F4D\u7F6E ${formatQueueFigure(queue.queueCount)}\uFF0C\u9884\u8BA1 ${formatWaitMinutes(queue.waitTimeSeconds)} \u5206\u949F\u5185\u653E\u884C\uFF09\u3002\u5DF2\u6309 Retry-After \u7B49\u5F85\u5E76\u91CD\u8BD5 1 \u6B21\uFF0C\u4ECD\u672A\u653E\u884C\u3002\u8FD9\u4E0D\u662F\u51ED\u8BC1\u6216\u989D\u5EA6\u95EE\u9898\uFF0C\u8BF7\u7528 /model \u5207\u6362\u5176\u4ED6 Provider\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5\u3002`
3543
+ );
3544
+ }
3488
3545
  throw new Error(
3489
- `Qoder is at capacity: the request stayed queued after ${queuedAttempt + 1} attempts (~${queue.waitTimeSeconds}s wait reported). Try again shortly.`
3546
+ `Qoder is at capacity: the request stayed queued after ${queuedAttempt + 1} attempts (~${formatWaitMinutes(queue.waitTimeSeconds)} min wait reported). Try again shortly.`
3490
3547
  );
3491
3548
  }
3492
3549
  queuedAttempt++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-qoder-cn",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
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",