pi-provider-qoder-cn 0.2.20 → 0.2.22
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/README.md +80 -0
- package/dist/index.js +320 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,9 @@ pi install npm:pi-provider-qoder-cn
|
|
|
26
26
|
| Enterprise (VPC) selection during `/login qoder-cn` | ❌ | ✅ |
|
|
27
27
|
| Current CN model catalog (`qwen3.8-max` / `qwen3.8-flash`, …) | ❌ | ✅ |
|
|
28
28
|
| Stable CN model ids for `enabledModels` | ❌ | ✅ |
|
|
29
|
+
| Capacity-queue detection (`isQueued` / `RESOURCE_LIMIT` / lane) | ❌ | ✅ (recognised, honoured, adjustable) |
|
|
30
|
+
| Per-turn wait budget + lane cooldown knobs | ❌ | ✅ |
|
|
31
|
+
| Cache ownership tag (foreign catalogs discarded, not half-read) | ❌ | ✅ |
|
|
29
32
|
|
|
30
33
|
Both `qoder` (Global) and `qoder-cn` (Domestic) providers are registered.
|
|
31
34
|
- To use Global: run `/login qoder` and use `/model qoder/<id>`. Commands: `/qoder.usage`, `/qoder.claim`.
|
|
@@ -259,6 +262,83 @@ the offline fallback and the source of model ids.
|
|
|
259
262
|
the whitespace-stripped display name, so `enabledModels` entries such as
|
|
260
263
|
`qoder-cn/qwen3.8-flash` keep working across upgrades.
|
|
261
264
|
|
|
265
|
+
**Thinking switch.** A model whose catalog advertises no `disabled` thinking
|
|
266
|
+
option (`thinkingLevelMap.off === null`, e.g. GLM-5.3 / GLM-5.3-Flash on CN)
|
|
267
|
+
is always-thinking: sending `enable_thinking=false` is answered HTTP 400 code
|
|
268
|
+
1210 `该模型始终思考`, so no switch is sent at all. Note the TUI's thinking-off
|
|
269
|
+
toggle does not reach that path — pi-ai's `clampThinkingLevel()` promotes `off`
|
|
270
|
+
to the model's lowest advertised effort; the switch is omitted for requests that
|
|
271
|
+
carry no thinking level.
|
|
272
|
+
|
|
273
|
+
## Capacity queue and wait policy
|
|
274
|
+
|
|
275
|
+
When Qoder's gateway has no capacity it does not reject the request. It answers
|
|
276
|
+
HTTP 200 and puts the verdict in the first SSE frame, three JSON layers deep:
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{ "isQueued": true, "modelKey": "auto", "queueCount": 0, "queueType": "p3",
|
|
280
|
+
"retryAfterSeconds": 30, "serviceAvailable": false, "subCode": "RESOURCE_LIMIT", "waitTime": 30 }
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Two verdicts look alike and mean opposite things, so they get different handling
|
|
284
|
+
and different wording:
|
|
285
|
+
|
|
286
|
+
| Gateway says | Meaning | Default handling |
|
|
287
|
+
|---|---|---|
|
|
288
|
+
| `serviceAvailable: true` | in line; the line drains | wait per `Retry-After`, up to 3 retries |
|
|
289
|
+
| `serviceAvailable: false` | this lane has nothing to serve | honour `Retry-After` **once**, then stop and name the lane |
|
|
290
|
+
|
|
291
|
+
Measured against `api3.qoder.sh` on an account whose plan quota is 0
|
|
292
|
+
(`personal_standard`, add-on credits only), 2026-10-17/18:
|
|
293
|
+
|
|
294
|
+
- The refusal is **account-lane, not model**: `auto`, `qmodel_38max`, `efficient`
|
|
295
|
+
and `ultimate` all answered `queueType:"p3"` + `RESOURCE_LIMIT`. Switching
|
|
296
|
+
model inside one provider does not dodge it; switching provider does
|
|
297
|
+
(`qoder-cn` answered in 2–5 s at the same moment).
|
|
298
|
+
- `queueCount` is `0` — there is no line to report a position in.
|
|
299
|
+
- **Queued attempts are not billed**: 19 chat requests refused this way left
|
|
300
|
+
`addOnQuota.used` unchanged. Waiting costs time, never credits.
|
|
301
|
+
- The release rate is correlated with the time window, not per-attempt random:
|
|
302
|
+
6 of 21 attempts served in one window, 0 of 19 in another. That is why extra
|
|
303
|
+
patience is opt-in instead of a default.
|
|
304
|
+
|
|
305
|
+
### Knobs
|
|
306
|
+
|
|
307
|
+
All defaults reproduce the pre-0.2.22 behaviour; each knob is read per request.
|
|
308
|
+
|
|
309
|
+
| Env var | Default | Effect |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| `QODER_QUEUE_MAX_ATTEMPTS` | `3` | Retries for an ordinary queue (`serviceAvailable: true`). |
|
|
312
|
+
| `QODER_QUEUE_UNAVAILABLE_ATTEMPTS` | `1` | Retries after `serviceAvailable: false`. Raise it only when you would rather wait than switch. |
|
|
313
|
+
| `QODER_QUEUE_MAX_TOTAL_WAIT_MS` | `90000` | Wall-clock ceiling for queue waits **in one turn** (it is not restarted by the 105 credential self-heal). `0` means never wait. |
|
|
314
|
+
| `QODER_QUEUE_SINGLE_WAIT_MS` | `120000` | Ceiling for one sleep, clamped further by the total budget. |
|
|
315
|
+
| `QODER_QUEUE_JITTER_MS` | `2000` | Random spread added to each wait so parallel sessions do not retry in lockstep. |
|
|
316
|
+
| `QODER_QUEUE_LANE_COOLDOWN_MS` | `0` (off) | After a hard refusal, skip later requests on that lane for N ms instead of spending another ~31 s. The skip says so explicitly — the request was never sent. |
|
|
317
|
+
| `QODER_DEBUG` | off | Logs one line per queue notice (`lane`, `subCode`, attempt, waited ms) and when a foreign model cache is discarded. |
|
|
318
|
+
|
|
319
|
+
Verified live on the built bundle: default = refusal in ~31 s after 2 requests;
|
|
320
|
+
`QODER_QUEUE_LANE_COOLDOWN_MS=20000` = first request 2 requests/32 s, then
|
|
321
|
+
0 network calls and 0 ms; `QODER_QUEUE_MAX_TOTAL_WAIT_MS=0` = refusal after a
|
|
322
|
+
single request in 0.6 s.
|
|
323
|
+
|
|
324
|
+
Garbage values fall back to the default, out-of-range values are clamped, and a
|
|
325
|
+
negative value falls back rather than silently meaning "zero".
|
|
326
|
+
|
|
327
|
+
The error text is deliberately worded so pi-ai's `isRetryableAssistantError()`
|
|
328
|
+
does not classify it as transient: every number passes through
|
|
329
|
+
`formatQueueFigure`, which keeps at most two contiguous digits (a real
|
|
330
|
+
`queueCount` of 5204 renders as `5.2k`, because bare `520`/`503` would re-run the
|
|
331
|
+
whole turn against a gateway that already refused).
|
|
332
|
+
|
|
333
|
+
### Model cache ownership
|
|
334
|
+
|
|
335
|
+
The catalog is cached at `~/.pi/agent/qoder-models-cache.json` (and
|
|
336
|
+
`qoder-cn-models-cache.json`), a path other Qoder providers also use. Each write
|
|
337
|
+
is now tagged `"writer": "pi-provider-qoder-cn"`. A cache tagged by a different
|
|
338
|
+
implementation is discarded and re-fetched instead of being half-believed — its
|
|
339
|
+
model ids are not the ones this package registers. A cache with **no** tag stays
|
|
340
|
+
readable, because every release up to 0.2.21 wrote that shape.
|
|
341
|
+
|
|
262
342
|
## Development
|
|
263
343
|
|
|
264
344
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -464,6 +464,13 @@ function readParsedModelCache(mode) {
|
|
|
464
464
|
}
|
|
465
465
|
try {
|
|
466
466
|
const data = JSON.parse(readFileSync3(cachePath, "utf8"));
|
|
467
|
+
if (data.writer !== void 0 && data.writer !== QODER_CACHE_WRITER) {
|
|
468
|
+
if (process.env.QODER_DEBUG) {
|
|
469
|
+
console.error(`[qoder] model cache ${cachePath} was written by "${data.writer}"; ignoring and refetching`);
|
|
470
|
+
}
|
|
471
|
+
modelCacheMem.set(cachePath, null);
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
467
474
|
modelCacheMem.set(cachePath, data);
|
|
468
475
|
return data;
|
|
469
476
|
} catch {
|
|
@@ -474,8 +481,8 @@ function readParsedModelCache(mode) {
|
|
|
474
481
|
function writeParsedModelCache(mode, data) {
|
|
475
482
|
const cachePath = getQoderCachePath(mode);
|
|
476
483
|
mkdirSync3(dirname2(cachePath), { recursive: true });
|
|
477
|
-
writeFileSync3(cachePath, JSON.stringify(data, null, 2), "utf-8");
|
|
478
|
-
modelCacheMem.set(cachePath, data);
|
|
484
|
+
writeFileSync3(cachePath, JSON.stringify({ ...data, writer: QODER_CACHE_WRITER }, null, 2), "utf-8");
|
|
485
|
+
modelCacheMem.set(cachePath, { ...data, writer: QODER_CACHE_WRITER });
|
|
479
486
|
}
|
|
480
487
|
function toQoderModelId(displayName) {
|
|
481
488
|
return (displayName || "QoderModel").replace(/\s+/g, "");
|
|
@@ -667,7 +674,7 @@ async function updateQoderModelsCache(authToken, userID, name, email, mode) {
|
|
|
667
674
|
} catch {
|
|
668
675
|
}
|
|
669
676
|
}
|
|
670
|
-
var ZERO_COST, MAX_OUTPUT_TOKENS, DEFAULT_CONTEXT_WINDOW, modelCacheMem, staticModels, staticCnModels, PI_THINKING_LEVELS;
|
|
677
|
+
var ZERO_COST, MAX_OUTPUT_TOKENS, DEFAULT_CONTEXT_WINDOW, QODER_CACHE_WRITER, modelCacheMem, staticModels, staticCnModels, PI_THINKING_LEVELS;
|
|
671
678
|
var init_catalog = __esm({
|
|
672
679
|
"src/catalog.ts"() {
|
|
673
680
|
"use strict";
|
|
@@ -677,6 +684,7 @@ var init_catalog = __esm({
|
|
|
677
684
|
ZERO_COST = Object.freeze({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
678
685
|
MAX_OUTPUT_TOKENS = 131072;
|
|
679
686
|
DEFAULT_CONTEXT_WINDOW = 1e6;
|
|
687
|
+
QODER_CACHE_WRITER = "pi-provider-qoder-cn";
|
|
680
688
|
modelCacheMem = /* @__PURE__ */ new Map();
|
|
681
689
|
staticModels = [
|
|
682
690
|
{
|
|
@@ -2243,7 +2251,149 @@ async function runClaimCommand(mode, args, ctx) {
|
|
|
2243
2251
|
}
|
|
2244
2252
|
|
|
2245
2253
|
// src/protocol/errors.ts
|
|
2254
|
+
init_region();
|
|
2246
2255
|
init_vpc();
|
|
2256
|
+
|
|
2257
|
+
// src/protocol/queue.ts
|
|
2258
|
+
var MAX_WAIT_MS = 12e4;
|
|
2259
|
+
var DEFAULT_RETRY_SECONDS = 30;
|
|
2260
|
+
function digForQueue(node, depth) {
|
|
2261
|
+
if (depth > 6) return null;
|
|
2262
|
+
if (typeof node === "string") {
|
|
2263
|
+
const trimmed = node.trim();
|
|
2264
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
|
|
2265
|
+
try {
|
|
2266
|
+
return digForQueue(JSON.parse(trimmed), depth + 1);
|
|
2267
|
+
} catch {
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
if (node === null || typeof node !== "object") return null;
|
|
2272
|
+
if (Array.isArray(node)) {
|
|
2273
|
+
for (const item of node) {
|
|
2274
|
+
const found = digForQueue(item, depth + 1);
|
|
2275
|
+
if (found) return found;
|
|
2276
|
+
}
|
|
2277
|
+
return null;
|
|
2278
|
+
}
|
|
2279
|
+
const record = node;
|
|
2280
|
+
if (record.isQueued === true) return record;
|
|
2281
|
+
for (const value of Object.values(record)) {
|
|
2282
|
+
const found = digForQueue(value, depth + 1);
|
|
2283
|
+
if (found) return found;
|
|
2284
|
+
}
|
|
2285
|
+
return null;
|
|
2286
|
+
}
|
|
2287
|
+
function positiveNumber(value, fallback) {
|
|
2288
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
2289
|
+
}
|
|
2290
|
+
function textOf(value) {
|
|
2291
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
2292
|
+
}
|
|
2293
|
+
function parseQueueNotice(envelope) {
|
|
2294
|
+
const payload = digForQueue(envelope, 0);
|
|
2295
|
+
if (!payload) return null;
|
|
2296
|
+
const retryMs = positiveNumber(payload.retry_after_ms, 0) || positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3;
|
|
2297
|
+
return {
|
|
2298
|
+
retryAfterMs: Math.min(retryMs, MAX_WAIT_MS),
|
|
2299
|
+
waitTimeSeconds: positiveNumber(payload.waitTime, 0),
|
|
2300
|
+
queueCount: positiveNumber(payload.queueCount, 0),
|
|
2301
|
+
// Only a real boolean is surfaced; a missing field must not be read as
|
|
2302
|
+
// "unavailable" (that would collapse the normal queue into a hard stop).
|
|
2303
|
+
serviceAvailable: typeof payload.serviceAvailable === "boolean" ? payload.serviceAvailable : void 0,
|
|
2304
|
+
// `subCode` sits inside the same record as `isQueued` in the payloads seen
|
|
2305
|
+
// live, so one source is enough; the wrapper levels carry no extra names.
|
|
2306
|
+
subCode: textOf(payload.subCode),
|
|
2307
|
+
queueType: textOf(payload.queueType),
|
|
2308
|
+
modelKey: textOf(payload.modelKey)
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
function formatWaitSecondsShort(ms) {
|
|
2312
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0";
|
|
2313
|
+
const seconds = Math.round(ms / 1e3);
|
|
2314
|
+
if (seconds < 100) return String(seconds);
|
|
2315
|
+
return `${formatWaitMinutes(seconds)}\u5206\u949F`;
|
|
2316
|
+
}
|
|
2317
|
+
function formatQueueFailureMessage(notice, ctx) {
|
|
2318
|
+
const isCn = ctx.mode === "cn";
|
|
2319
|
+
const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
|
|
2320
|
+
const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
|
|
2321
|
+
const modelKey = notice.modelKey ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
|
|
2322
|
+
const hard = notice.serviceAvailable === false;
|
|
2323
|
+
const waited = formatWaitSecondsShort(ctx.waitedMs ?? 0);
|
|
2324
|
+
const queueLabel = formatQueueFigure(notice.queueCount ?? 0);
|
|
2325
|
+
const tried = ctx.attempts === void 0 ? void 0 : isCn ? `\u5DF2\u5C1D\u8BD5 ${formatQueueFigure(ctx.attempts)} \u6B21\uFF08\u7D2F\u8BA1\u7B49\u5F85 ${waited} \u79D2\uFF09\uFF0C\u7ED3\u8BBA\u76F8\u540C\u3002` : `${formatQueueFigure(ctx.attempts)} attempts (about ${waited} seconds of waiting) ended in the same verdict. `;
|
|
2326
|
+
if (hard) {
|
|
2327
|
+
return isCn ? [
|
|
2328
|
+
`[Qoder CN \u65E0\u53EF\u7528\u5BB9\u91CF] \u7F51\u5173\u56DE serviceAvailable:false\uFF08\u5B50\u7801 ${sub}\uFF0C\u961F\u5217\u957F\u5EA6 ${queueLabel}\uFF0C\u4F18\u5148\u7EA7\u901A\u9053 ${lane}\uFF0C\u6A21\u578B ${modelKey}\uFF09`,
|
|
2329
|
+
`- \u8BF4\u660E\uFF1A${tried ?? ""}\u8FD9\u662F\u8D26\u53F7\u4F18\u5148\u7EA7\u901A\u9053\u7684\u5BB9\u91CF\u9650\u5236\uFF0C\u4E0D\u662F\u51ED\u8BC1\u95EE\u9898\u3002\u6392\u961F\u5C1D\u8BD5\u4E0D\u4EA7\u751F\u8F93\u51FA\uFF0C\u5B9E\u6D4B\u4E5F\u4E0D\u6263 credits\uFF08\u4EE3\u4EF7\u53EA\u6709\u65F6\u5EF6\uFF09\u3002`,
|
|
2330
|
+
`- \u6362\u6A21\u578B\u65E0\u6548\uFF1A\u540C\u4E00\u8D26\u53F7\u5728 ${lane} \u901A\u9053\u4E0B\u5404\u6A21\u578B\u8FD4\u56DE\u540C\u4E00\u7ED3\u8BBA\uFF08\u5B9E\u6D4B auto / qmodel_38max / efficient / ultimate \u4E00\u81F4\uFF09\uFF0C\u8BF7\u52FF\u5728\u540C\u4E00 Provider \u5185\u53CD\u590D\u6311\u6A21\u578B\u3002`,
|
|
2331
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u6216\u5176\u4ED6 Provider\uFF0C\u6216\u7A0D\u540E\u91CD\u8BD5\u3002\u989D\u5EA6\u4E0E\u8BA1\u5212\uFF1A${ctx.manageUrl}`
|
|
2332
|
+
].join("\n") : [
|
|
2333
|
+
`[Qoder international has no spare capacity] The gateway answered serviceAvailable:false (sub code ${sub}, queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
|
|
2334
|
+
`- Meaning: ${tried ?? ""}This is the account's priority-lane capacity, not a credential problem. Queued attempts produce no output and are not billed (measured: 19 queued requests, credits unchanged) \u2014 the only cost is latency.`,
|
|
2335
|
+
`- Switching model will not help: every model of this account answers on the same ${lane} lane (measured across auto / qmodel_38max / efficient / ultimate).`,
|
|
2336
|
+
`- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry later. Plan and quota: ${ctx.manageUrl}`
|
|
2337
|
+
].join("\n");
|
|
2338
|
+
}
|
|
2339
|
+
return isCn ? [
|
|
2340
|
+
`[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\uFF08\u961F\u5217\u957F\u5EA6 ${queueLabel}\uFF0C\u4F18\u5148\u7EA7\u901A\u9053 ${lane}\uFF0C\u6A21\u578B ${modelKey}\uFF09`,
|
|
2341
|
+
`- \u8BF4\u660E\uFF1A${tried ?? ""}\u961F\u5217\u5C1A\u672A\u653E\u884C\u3002\u51ED\u8BC1\u4E0E\u989D\u5EA6\u6B63\u5E38\uFF0C\u7EAF\u7CB9\u662F\u4E0A\u6E38\u6392\u961F\uFF1B\u6392\u961F\u5C1D\u8BD5\u4E0D\u6D88\u8017 credits\u3002`,
|
|
2342
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7A0D\u540E\u91CD\u8BD5\uFF0C\u6216\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u7B49\u5176\u4ED6 Provider\u3002\u989D\u5EA6\u4E0E\u8BA1\u5212\uFF1A${ctx.manageUrl}`
|
|
2343
|
+
].join("\n") : [
|
|
2344
|
+
// "is at capacity" is the established wording for the drainable case and
|
|
2345
|
+
// the only queue text allowed to carry it; the hard refusal above must
|
|
2346
|
+
// never read as "wait it out".
|
|
2347
|
+
`[Qoder upstream queue] The model gateway is at capacity and parked this request (queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
|
|
2348
|
+
`- Meaning: ${tried ?? ""}the queue has not cleared yet. Credentials and quota are fine; queued attempts are not billed.`,
|
|
2349
|
+
`- Next step: retry shortly, or switch provider with /model to ${ctx.otherProviderID} while the queue drains. Plan and quota: ${ctx.manageUrl}`
|
|
2350
|
+
].join("\n");
|
|
2351
|
+
}
|
|
2352
|
+
function formatLaneCooldownMessage(notice, remainingMs, ctx) {
|
|
2353
|
+
const isCn = ctx.mode === "cn";
|
|
2354
|
+
const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
|
|
2355
|
+
const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
|
|
2356
|
+
const left = Math.min(99, Math.max(1, Math.ceil(remainingMs / 1e3)));
|
|
2357
|
+
return isCn ? [
|
|
2358
|
+
`[Qoder CN \u5DF2\u8DF3\u8FC7\u672C\u6B21\u8BF7\u6C42] \u4F18\u5148\u7EA7\u901A\u9053 ${lane} \u521A\u521A\u56DE\u8FC7 serviceAvailable:false\uFF08\u5B50\u7801 ${sub}\uFF09\uFF0C${left} \u79D2\u5185\u4E0D\u518D\u91CD\u590D\u7B49\u5F85\u3002`,
|
|
2359
|
+
`- \u8BF4\u660E\uFF1A\u8FD9\u6B21\u8BF7\u6C42\u5C1A\u672A\u53D1\u51FA\u3002\u4E0A\u4E00\u8F6E\u540C\u901A\u9053\u8BF7\u6C42\u6309 Retry-After \u7B49\u5F85\u540E\u4ECD\u672A\u653E\u884C\uFF0C\u6392\u961F\u53EA\u4F1A\u518D\u6D88\u8017\u540C\u6837\u7684\u65F6\u5EF6\u3002`,
|
|
2360
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u6216\u5176\u4ED6 Provider\uFF0C\u6216 ${left} \u79D2\u540E\u91CD\u8BD5\u3002`
|
|
2361
|
+
].join("\n") : [
|
|
2362
|
+
`[Qoder request skipped] Priority lane ${lane} just refused service (sub code ${sub}, serviceAvailable:false), so this request was not sent; repeats are skipped for ${left} more seconds.`,
|
|
2363
|
+
`- Meaning: the previous request on this lane already waited through Retry-After and was still refused, so waiting again buys the same verdict.`,
|
|
2364
|
+
`- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry in ${left} seconds.`
|
|
2365
|
+
].join("\n");
|
|
2366
|
+
}
|
|
2367
|
+
function formatQueueFigure(n) {
|
|
2368
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
2369
|
+
if (n < 100) return String(Math.round(n));
|
|
2370
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2371
|
+
return `${(n / 1e3).toFixed(1)}k`;
|
|
2372
|
+
}
|
|
2373
|
+
function formatWaitMinutes(seconds) {
|
|
2374
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return "<1";
|
|
2375
|
+
return String(Math.min(99, Math.max(1, Math.round(seconds / 60))));
|
|
2376
|
+
}
|
|
2377
|
+
function sleep(ms, signal) {
|
|
2378
|
+
return new Promise((resolve, reject) => {
|
|
2379
|
+
if (signal?.aborted) {
|
|
2380
|
+
reject(new Error("Aborted while waiting for the Qoder queue."));
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
let timer;
|
|
2384
|
+
const onAbort = () => {
|
|
2385
|
+
clearTimeout(timer);
|
|
2386
|
+
reject(new Error("Aborted while waiting for the Qoder queue."));
|
|
2387
|
+
};
|
|
2388
|
+
timer = setTimeout(() => {
|
|
2389
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2390
|
+
resolve();
|
|
2391
|
+
}, ms);
|
|
2392
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// src/protocol/errors.ts
|
|
2247
2397
|
function parseQoderErrorPayload(payload) {
|
|
2248
2398
|
if (!payload) return null;
|
|
2249
2399
|
let parsed = payload;
|
|
@@ -2302,6 +2452,7 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "c
|
|
|
2302
2452
|
const parsed = parseQoderErrorPayload(rawBody);
|
|
2303
2453
|
const code = parsed?.code;
|
|
2304
2454
|
const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
|
|
2455
|
+
const bodyForMatch = `${message} ${typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody ?? "")}`;
|
|
2305
2456
|
const countdown = getBeijingDailyResetCountdown(now);
|
|
2306
2457
|
const isCn = mode !== "global";
|
|
2307
2458
|
if (code === "110" || /billing daily count exceeded/i.test(message) || /daily usage limit reached/i.test(message)) {
|
|
@@ -2390,7 +2541,7 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "c
|
|
|
2390
2541
|
}
|
|
2391
2542
|
if (code === "103" || /duplicate request/i.test(message)) {
|
|
2392
2543
|
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)`,
|
|
2544
|
+
`[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
2545
|
`- \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
2546
|
`- \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
2547
|
].join("\n") : [
|
|
@@ -2399,7 +2550,17 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "c
|
|
|
2399
2550
|
`- Next step: every retry must be re-signed; if it keeps repeating, switch provider with /model and try again shortly.`
|
|
2400
2551
|
].join("\n");
|
|
2401
2552
|
}
|
|
2402
|
-
if (code === "10605" || /"isQueued"\s*:\s*true
|
|
2553
|
+
if (code === "10605" || /"isQueued"\s*:\s*true/.test(message)) {
|
|
2554
|
+
const notice = parseQueueNotice(rawBody) ?? parseQueueNotice(message);
|
|
2555
|
+
if (notice) {
|
|
2556
|
+
return formatQueueFailureMessage(notice, {
|
|
2557
|
+
mode,
|
|
2558
|
+
// This call does not own the retry loop, so it claims no attempt count.
|
|
2559
|
+
waitedMs: 0,
|
|
2560
|
+
manageUrl: getQoderRegionConfig(mode).manageUrl,
|
|
2561
|
+
otherProviderID: mode === "cn" ? "qoder" : "qoder-cn"
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2403
2564
|
return isCn ? [
|
|
2404
2565
|
`[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
2566
|
`- \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`,
|
|
@@ -2410,6 +2571,17 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "c
|
|
|
2410
2571
|
`- Next step: retry shortly, or switch provider with /model while the queue drains.`
|
|
2411
2572
|
].join("\n");
|
|
2412
2573
|
}
|
|
2574
|
+
if (code === "1210" || /不支持关闭思考|cannot disable thinking|always.{0,12}think/i.test(bodyForMatch)) {
|
|
2575
|
+
return isCn ? [
|
|
2576
|
+
`[Qoder CN \u4E0D\u652F\u6301\u5173\u95ED\u601D\u8003] \u8BE5\u6A21\u578B\u59CB\u7EC8\u601D\u8003 (\u9519\u8BEF\u7801 1210)`,
|
|
2577
|
+
`- \u8BF4\u660E\uFF1A\u8FD9\u4E2A\u6A21\u578B\u6CA1\u6709\u201C\u5173\u95ED\u601D\u8003\u201D\u9009\u9879\uFF0C\u8BF7\u6C42\u91CC\u4E0D\u80FD\u5E26 enable_thinking=false\u3002`,
|
|
2578
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 thinking \u5F00\u5173\u9009 low / medium / high / max \u4EFB\u4E00\u6863\u4F4D\uFF1B\u5BA2\u6237\u7AEF\u5E94\u4E3A\u8BE5\u6A21\u578B\u7701\u7565\u601D\u8003\u5F00\u5173\u3002`
|
|
2579
|
+
].join("\n") : [
|
|
2580
|
+
`[Qoder model always thinks] This model cannot disable thinking (code 1210)`,
|
|
2581
|
+
`- Meaning: the model has no "off" switch, so a request carrying enable_thinking=false is refused. This will not change on retry.`,
|
|
2582
|
+
`- Next step: pick a thinking level (low / medium / high / max) instead of turning thinking off.`
|
|
2583
|
+
].join("\n");
|
|
2584
|
+
}
|
|
2413
2585
|
if (rawBody === void 0 || rawBody === null) {
|
|
2414
2586
|
return `Upstream status ${statusCode}`;
|
|
2415
2587
|
}
|
|
@@ -2789,67 +2961,112 @@ function qoderEncodeBody(plaintext) {
|
|
|
2789
2961
|
return out;
|
|
2790
2962
|
}
|
|
2791
2963
|
|
|
2792
|
-
// src/protocol/queue.ts
|
|
2793
|
-
var
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
}
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
return
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
function parseQueueNotice(envelope) {
|
|
2827
|
-
const payload = digForQueue(envelope, 0);
|
|
2828
|
-
if (!payload) return null;
|
|
2829
|
-
const retryMs = positiveNumber(payload.retry_after_ms, 0) || positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3;
|
|
2964
|
+
// src/protocol/queue-policy.ts
|
|
2965
|
+
var DEFAULT_QUEUE_POLICY = Object.freeze({
|
|
2966
|
+
unavailableAttempts: 1,
|
|
2967
|
+
maxAttempts: 3,
|
|
2968
|
+
totalWaitBudgetMs: 9e4,
|
|
2969
|
+
singleWaitCapMs: 12e4,
|
|
2970
|
+
jitterMs: 2e3,
|
|
2971
|
+
/**
|
|
2972
|
+
* OFF by default. A skipped request is a request the user expected to be sent,
|
|
2973
|
+
* and "the lane refused a moment ago" is an inference, not a gateway answer:
|
|
2974
|
+
* it holds on the international service (every model of the account answered
|
|
2975
|
+
* `p3`/`RESOURCE_LIMIT`), but it would also suppress a request that was about
|
|
2976
|
+
* to be served. Opt in with `QODER_QUEUE_LANE_COOLDOWN_MS=20000` when a whole
|
|
2977
|
+
* agent turn is otherwise spending 31s per call on a dead lane.
|
|
2978
|
+
*/
|
|
2979
|
+
laneCooldownMs: 0
|
|
2980
|
+
});
|
|
2981
|
+
var LIMITS = {
|
|
2982
|
+
attempts: { min: 0, max: 10 },
|
|
2983
|
+
budgetMs: { min: 0, max: 15 * 6e4 },
|
|
2984
|
+
singleMs: { min: 1e3, max: 5 * 6e4 },
|
|
2985
|
+
jitterMs: { min: 0, max: 3e4 },
|
|
2986
|
+
cooldownMs: { min: 0, max: 5 * 6e4 }
|
|
2987
|
+
};
|
|
2988
|
+
function clampInt(raw, min, max, fallback) {
|
|
2989
|
+
if (raw === void 0) return fallback;
|
|
2990
|
+
const trimmed = raw.trim();
|
|
2991
|
+
if (!trimmed) return fallback;
|
|
2992
|
+
const value = Number(trimmed);
|
|
2993
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) return fallback;
|
|
2994
|
+
if (value < 0) return fallback;
|
|
2995
|
+
return Math.min(max, Math.max(min, value));
|
|
2996
|
+
}
|
|
2997
|
+
function readQueuePolicy(env = process.env) {
|
|
2830
2998
|
return {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2999
|
+
unavailableAttempts: clampInt(
|
|
3000
|
+
env.QODER_QUEUE_UNAVAILABLE_ATTEMPTS,
|
|
3001
|
+
LIMITS.attempts.min,
|
|
3002
|
+
LIMITS.attempts.max,
|
|
3003
|
+
DEFAULT_QUEUE_POLICY.unavailableAttempts
|
|
3004
|
+
),
|
|
3005
|
+
maxAttempts: clampInt(
|
|
3006
|
+
env.QODER_QUEUE_MAX_ATTEMPTS,
|
|
3007
|
+
LIMITS.attempts.min,
|
|
3008
|
+
LIMITS.attempts.max,
|
|
3009
|
+
DEFAULT_QUEUE_POLICY.maxAttempts
|
|
3010
|
+
),
|
|
3011
|
+
totalWaitBudgetMs: clampInt(
|
|
3012
|
+
env.QODER_QUEUE_MAX_TOTAL_WAIT_MS,
|
|
3013
|
+
LIMITS.budgetMs.min,
|
|
3014
|
+
LIMITS.budgetMs.max,
|
|
3015
|
+
DEFAULT_QUEUE_POLICY.totalWaitBudgetMs
|
|
3016
|
+
),
|
|
3017
|
+
singleWaitCapMs: clampInt(
|
|
3018
|
+
env.QODER_QUEUE_SINGLE_WAIT_MS,
|
|
3019
|
+
LIMITS.singleMs.min,
|
|
3020
|
+
LIMITS.singleMs.max,
|
|
3021
|
+
DEFAULT_QUEUE_POLICY.singleWaitCapMs
|
|
3022
|
+
),
|
|
3023
|
+
jitterMs: clampInt(
|
|
3024
|
+
env.QODER_QUEUE_JITTER_MS,
|
|
3025
|
+
LIMITS.jitterMs.min,
|
|
3026
|
+
LIMITS.jitterMs.max,
|
|
3027
|
+
DEFAULT_QUEUE_POLICY.jitterMs
|
|
3028
|
+
),
|
|
3029
|
+
laneCooldownMs: clampInt(
|
|
3030
|
+
env.QODER_QUEUE_LANE_COOLDOWN_MS,
|
|
3031
|
+
LIMITS.cooldownMs.min,
|
|
3032
|
+
LIMITS.cooldownMs.max,
|
|
3033
|
+
DEFAULT_QUEUE_POLICY.laneCooldownMs
|
|
3034
|
+
)
|
|
2834
3035
|
};
|
|
2835
3036
|
}
|
|
2836
|
-
function
|
|
2837
|
-
return
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
3037
|
+
function allowedAttemptsFor(policy, notice) {
|
|
3038
|
+
return notice.serviceAvailable === false ? policy.unavailableAttempts : policy.maxAttempts;
|
|
3039
|
+
}
|
|
3040
|
+
function nextSleepMs(policy, notice, waitedMs, random = Math.random) {
|
|
3041
|
+
if (!Number.isFinite(notice.retryAfterMs) || notice.retryAfterMs <= 0) return 0;
|
|
3042
|
+
const remaining = Math.max(0, policy.totalWaitBudgetMs - waitedMs);
|
|
3043
|
+
if (remaining === 0) return 0;
|
|
3044
|
+
const base = Math.min(notice.retryAfterMs, policy.singleWaitCapMs, remaining);
|
|
3045
|
+
const jitter = policy.jitterMs > 0 ? random() * policy.jitterMs : 0;
|
|
3046
|
+
return Math.max(0, Math.min(policy.singleWaitCapMs, remaining, base + jitter));
|
|
3047
|
+
}
|
|
3048
|
+
function isHardUnavailable(notice) {
|
|
3049
|
+
return notice.serviceAvailable === false;
|
|
3050
|
+
}
|
|
3051
|
+
function laneKey(mode, notice) {
|
|
3052
|
+
return `${mode}:${notice.queueType ?? "unknown"}:${notice.subCode ?? "queued"}`;
|
|
3053
|
+
}
|
|
3054
|
+
var armedLanes = /* @__PURE__ */ new Map();
|
|
3055
|
+
function armLaneCooldown(key, ttlMs, notice, now = Date.now()) {
|
|
3056
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
3057
|
+
armedLanes.delete(key);
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
armedLanes.set(key, { until: now + ttlMs, notice });
|
|
3061
|
+
}
|
|
3062
|
+
function pendingLaneCooldown(mode, now = Date.now()) {
|
|
3063
|
+
for (const [key, entry] of armedLanes) {
|
|
3064
|
+
if (!key.startsWith(`${mode}:`)) continue;
|
|
3065
|
+
const remaining = entry.until - now;
|
|
3066
|
+
if (remaining > 0) return { key, remainingMs: remaining, notice: entry.notice };
|
|
3067
|
+
armedLanes.delete(key);
|
|
3068
|
+
}
|
|
3069
|
+
return null;
|
|
2853
3070
|
}
|
|
2854
3071
|
|
|
2855
3072
|
// src/protocol/thinking.ts
|
|
@@ -3359,6 +3576,8 @@ function streamQoder(model, context, options) {
|
|
|
3359
3576
|
const providerMode = model.provider === "qoder-cn" ? "cn" : "global";
|
|
3360
3577
|
let reader;
|
|
3361
3578
|
let started = false;
|
|
3579
|
+
const queuePolicy = readQueuePolicy();
|
|
3580
|
+
let turnWaitedMs = 0;
|
|
3362
3581
|
const region = getQoderRegionConfig(providerMode);
|
|
3363
3582
|
const runAttempt = async (apiKey) => {
|
|
3364
3583
|
const accessToken = apiKey ?? options?.apiKey;
|
|
@@ -3406,6 +3625,7 @@ function streamQoder(model, context, options) {
|
|
|
3406
3625
|
const requestedLevel = options?.reasoning;
|
|
3407
3626
|
const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
|
|
3408
3627
|
const reasoningLevel = clamped === "off" ? void 0 : clamped;
|
|
3628
|
+
const cannotDisableThinking = model.thinkingLevelMap?.off === null;
|
|
3409
3629
|
const parameters = { max_tokens: maxTokens };
|
|
3410
3630
|
if (reasoningLevel) {
|
|
3411
3631
|
parameters.enable_thinking = true;
|
|
@@ -3414,6 +3634,7 @@ function streamQoder(model, context, options) {
|
|
|
3414
3634
|
if (modelConfig?.thinking_config?.enabled?.efforts && typeof effort === "string") {
|
|
3415
3635
|
parameters.reasoning_effort = effort;
|
|
3416
3636
|
}
|
|
3637
|
+
} else if (cannotDisableThinking) {
|
|
3417
3638
|
} else {
|
|
3418
3639
|
parameters.enable_thinking = false;
|
|
3419
3640
|
}
|
|
@@ -3494,6 +3715,16 @@ function streamQoder(model, context, options) {
|
|
|
3494
3715
|
};
|
|
3495
3716
|
};
|
|
3496
3717
|
let queuedAttempt = 0;
|
|
3718
|
+
const cooled = pendingLaneCooldown(providerMode);
|
|
3719
|
+
if (cooled) {
|
|
3720
|
+
throw new Error(
|
|
3721
|
+
formatLaneCooldownMessage(cooled.notice, cooled.remainingMs, {
|
|
3722
|
+
mode: providerMode,
|
|
3723
|
+
manageUrl: region.manageUrl,
|
|
3724
|
+
otherProviderID: providerMode === "cn" ? "qoder" : "qoder-cn"
|
|
3725
|
+
})
|
|
3726
|
+
);
|
|
3727
|
+
}
|
|
3497
3728
|
await reader?.cancel().catch(() => {
|
|
3498
3729
|
});
|
|
3499
3730
|
reader = void 0;
|
|
@@ -3522,13 +3753,35 @@ function streamQoder(model, context, options) {
|
|
|
3522
3753
|
if (queue) {
|
|
3523
3754
|
await reader.cancel().catch(() => {
|
|
3524
3755
|
});
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3756
|
+
const queueCtx = {
|
|
3757
|
+
mode: providerMode,
|
|
3758
|
+
attempts: queuedAttempt + 1,
|
|
3759
|
+
waitedMs: turnWaitedMs,
|
|
3760
|
+
manageUrl: region.manageUrl,
|
|
3761
|
+
otherProviderID: providerMode === "cn" ? "qoder" : "qoder-cn"
|
|
3762
|
+
};
|
|
3763
|
+
if (process.env.QODER_DEBUG) {
|
|
3764
|
+
console.error(
|
|
3765
|
+
`[qoder] queued lane=${queue.queueType ?? "?"} sub=${queue.subCode ?? "?"} model=${queue.modelKey ?? "?"} serviceAvailable=${String(queue.serviceAvailable ?? "?")} attempt=${queuedAttempt + 1} waited=${turnWaitedMs}ms`
|
|
3528
3766
|
);
|
|
3529
3767
|
}
|
|
3768
|
+
const maxAttempts = allowedAttemptsFor(queuePolicy, queue);
|
|
3769
|
+
if (queuedAttempt >= maxAttempts) {
|
|
3770
|
+
if (isHardUnavailable(queue)) {
|
|
3771
|
+
armLaneCooldown(laneKey(providerMode, queue), queuePolicy.laneCooldownMs, queue);
|
|
3772
|
+
}
|
|
3773
|
+
throw new Error(formatQueueFailureMessage(queue, queueCtx));
|
|
3774
|
+
}
|
|
3775
|
+
const waitMs = nextSleepMs(queuePolicy, queue, turnWaitedMs);
|
|
3776
|
+
if (waitMs <= 0) {
|
|
3777
|
+
if (isHardUnavailable(queue)) {
|
|
3778
|
+
armLaneCooldown(laneKey(providerMode, queue), queuePolicy.laneCooldownMs, queue);
|
|
3779
|
+
}
|
|
3780
|
+
throw new Error(formatQueueFailureMessage(queue, queueCtx));
|
|
3781
|
+
}
|
|
3530
3782
|
queuedAttempt++;
|
|
3531
|
-
|
|
3783
|
+
turnWaitedMs += waitMs;
|
|
3784
|
+
await sleep(waitMs, options?.signal);
|
|
3532
3785
|
continue;
|
|
3533
3786
|
}
|
|
3534
3787
|
if (await peekCredentialExpired(reader, decoder)) {
|
package/package.json
CHANGED