pi-provider-qoder-cn 0.2.19 → 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 (2) hide show
  1. package/dist/index.js +63 -25
  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 \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
  }
@@ -3188,10 +3210,12 @@ function stableHash(prefix, ...inputs) {
3188
3210
  }
3189
3211
  return hash.digest("hex").slice(0, 16);
3190
3212
  }
3191
- function stableChatRecordID(model, messages, tools, maxTokens) {
3213
+ function stableChatRecordID(model, messages, tools, maxTokens, salt) {
3192
3214
  const hash = crypto3.createHash("sha256");
3193
3215
  hash.update("qoder-record");
3194
3216
  hash.update("\0");
3217
+ hash.update(salt);
3218
+ hash.update("\0");
3195
3219
  hash.update(model);
3196
3220
  for (const msg of messages) {
3197
3221
  if (msg?.role) {
@@ -3372,7 +3396,13 @@ function streamQoder(model, context, options) {
3372
3396
  maxTokens = options.maxTokens;
3373
3397
  }
3374
3398
  const toolsRaw = resolved.tools.length > 0 ? transformTools(resolved.tools) : void 0;
3375
- const recordID = stableChatRecordID(qoderModel, normalizedMessages, toolsRaw, maxTokens);
3399
+ const recordID = stableChatRecordID(
3400
+ qoderModel,
3401
+ normalizedMessages,
3402
+ toolsRaw,
3403
+ maxTokens,
3404
+ `${sessionID}:${userID}`
3405
+ );
3376
3406
  const requestedLevel = options?.reasoning;
3377
3407
  const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
3378
3408
  const reasoningLevel = clamped === "off" ? void 0 : clamped;
@@ -3387,7 +3417,9 @@ function streamQoder(model, context, options) {
3387
3417
  } else {
3388
3418
  parameters.enable_thinking = false;
3389
3419
  }
3390
- const reqBody = {
3420
+ const modelSource = modelConfig.source || "system";
3421
+ const chatURL = getQoderChatURL(providerMode);
3422
+ const buildRequestBody = (attempt) => ({
3391
3423
  request_id: crypto3.randomUUID(),
3392
3424
  request_set_id: recordID,
3393
3425
  chat_record_id: recordID,
@@ -3395,7 +3427,7 @@ function streamQoder(model, context, options) {
3395
3427
  stream: true,
3396
3428
  chat_task: "FREE_INPUT",
3397
3429
  is_reply: true,
3398
- is_retry: false,
3430
+ is_retry: attempt > 0,
3399
3431
  source: 1,
3400
3432
  version: "3",
3401
3433
  session_type: "qodercli",
@@ -3436,18 +3468,31 @@ function streamQoder(model, context, options) {
3436
3468
  name: lastUserText.substring(0, 30),
3437
3469
  begin_at: Date.now()
3438
3470
  }
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
3471
  });
3450
- 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
+ };
3451
3496
  let queuedAttempt = 0;
3452
3497
  await reader?.cancel().catch(() => {
3453
3498
  });
@@ -3456,18 +3501,11 @@ function streamQoder(model, context, options) {
3456
3501
  let buffer = "";
3457
3502
  let bufferStart = 0;
3458
3503
  for (; ; ) {
3504
+ const current = buildAttempt(queuedAttempt);
3459
3505
  const response = await fetch(chatURL, {
3460
3506
  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,
3507
+ headers: current.headers,
3508
+ body: current.encoded,
3471
3509
  signal: options?.signal
3472
3510
  });
3473
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.19",
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",