pi-provider-qoder-cn 0.2.21 → 0.2.23
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 +84 -0
- package/dist/index.js +353 -117
- 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`.
|
|
@@ -141,6 +144,10 @@ Notes:
|
|
|
141
144
|
latest grant`, and an account that has never claimed prints no expiry line at
|
|
142
145
|
all, because with no grant there is nothing to date from, and inventing one
|
|
143
146
|
would show dates the API never sent.
|
|
147
|
+
- `Enterprise` is the org-wide pool. It names its allowance `cap` and sends no
|
|
148
|
+
`total`, so the row is keyed on any of `cap`, `used` or `remaining` carrying a
|
|
149
|
+
value: a freshly refreshed cycle reports `used: 0`, and that used to drop the
|
|
150
|
+
whole row and leave the view saying `Buckets none returned`.
|
|
144
151
|
- `Expires never` means the API reported the year-9999 sentinel, i.e. the
|
|
145
152
|
allowance has no expiry date.
|
|
146
153
|
- `Note plan quota is prorated` appears after a mid-cycle plan change.
|
|
@@ -259,6 +266,83 @@ the offline fallback and the source of model ids.
|
|
|
259
266
|
the whitespace-stripped display name, so `enabledModels` entries such as
|
|
260
267
|
`qoder-cn/qwen3.8-flash` keep working across upgrades.
|
|
261
268
|
|
|
269
|
+
**Thinking switch.** A model whose catalog advertises no `disabled` thinking
|
|
270
|
+
option (`thinkingLevelMap.off === null`, e.g. GLM-5.3 / GLM-5.3-Flash on CN)
|
|
271
|
+
is always-thinking: sending `enable_thinking=false` is answered HTTP 400 code
|
|
272
|
+
1210 `该模型始终思考`, so no switch is sent at all. Note the TUI's thinking-off
|
|
273
|
+
toggle does not reach that path — pi-ai's `clampThinkingLevel()` promotes `off`
|
|
274
|
+
to the model's lowest advertised effort; the switch is omitted for requests that
|
|
275
|
+
carry no thinking level.
|
|
276
|
+
|
|
277
|
+
## Capacity queue and wait policy
|
|
278
|
+
|
|
279
|
+
When Qoder's gateway has no capacity it does not reject the request. It answers
|
|
280
|
+
HTTP 200 and puts the verdict in the first SSE frame, three JSON layers deep:
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{ "isQueued": true, "modelKey": "auto", "queueCount": 0, "queueType": "p3",
|
|
284
|
+
"retryAfterSeconds": 30, "serviceAvailable": false, "subCode": "RESOURCE_LIMIT", "waitTime": 30 }
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Two verdicts look alike and mean opposite things, so they get different handling
|
|
288
|
+
and different wording:
|
|
289
|
+
|
|
290
|
+
| Gateway says | Meaning | Default handling |
|
|
291
|
+
|---|---|---|
|
|
292
|
+
| `serviceAvailable: true` | in line; the line drains | wait per `Retry-After`, up to 3 retries |
|
|
293
|
+
| `serviceAvailable: false` | this lane has nothing to serve | honour `Retry-After` **once**, then stop and name the lane |
|
|
294
|
+
|
|
295
|
+
Measured against `api3.qoder.sh` on an account whose plan quota is 0
|
|
296
|
+
(`personal_standard`, add-on credits only), 2026-10-17/18:
|
|
297
|
+
|
|
298
|
+
- The refusal is **account-lane, not model**: `auto`, `qmodel_38max`, `efficient`
|
|
299
|
+
and `ultimate` all answered `queueType:"p3"` + `RESOURCE_LIMIT`. Switching
|
|
300
|
+
model inside one provider does not dodge it; switching provider does
|
|
301
|
+
(`qoder-cn` answered in 2–5 s at the same moment).
|
|
302
|
+
- `queueCount` is `0` — there is no line to report a position in.
|
|
303
|
+
- **Queued attempts are not billed**: 19 chat requests refused this way left
|
|
304
|
+
`addOnQuota.used` unchanged. Waiting costs time, never credits.
|
|
305
|
+
- The release rate is correlated with the time window, not per-attempt random:
|
|
306
|
+
6 of 21 attempts served in one window, 0 of 19 in another. That is why extra
|
|
307
|
+
patience is opt-in instead of a default.
|
|
308
|
+
|
|
309
|
+
### Knobs
|
|
310
|
+
|
|
311
|
+
All defaults reproduce the pre-0.2.22 behaviour; each knob is read per request.
|
|
312
|
+
|
|
313
|
+
| Env var | Default | Effect |
|
|
314
|
+
|---|---|---|
|
|
315
|
+
| `QODER_QUEUE_MAX_ATTEMPTS` | `3` | Retries for an ordinary queue (`serviceAvailable: true`). |
|
|
316
|
+
| `QODER_QUEUE_UNAVAILABLE_ATTEMPTS` | `1` | Retries after `serviceAvailable: false`. Raise it only when you would rather wait than switch. |
|
|
317
|
+
| `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. |
|
|
318
|
+
| `QODER_QUEUE_SINGLE_WAIT_MS` | `120000` | Ceiling for one sleep, clamped further by the total budget. |
|
|
319
|
+
| `QODER_QUEUE_JITTER_MS` | `2000` | Random spread added to each wait so parallel sessions do not retry in lockstep. |
|
|
320
|
+
| `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. |
|
|
321
|
+
| `QODER_DEBUG` | off | Logs one line per queue notice (`lane`, `subCode`, attempt, waited ms) and when a foreign model cache is discarded. |
|
|
322
|
+
|
|
323
|
+
Verified live on the built bundle: default = refusal in ~31 s after 2 requests;
|
|
324
|
+
`QODER_QUEUE_LANE_COOLDOWN_MS=20000` = first request 2 requests/32 s, then
|
|
325
|
+
0 network calls and 0 ms; `QODER_QUEUE_MAX_TOTAL_WAIT_MS=0` = refusal after a
|
|
326
|
+
single request in 0.6 s.
|
|
327
|
+
|
|
328
|
+
Garbage values fall back to the default, out-of-range values are clamped, and a
|
|
329
|
+
negative value falls back rather than silently meaning "zero".
|
|
330
|
+
|
|
331
|
+
The error text is deliberately worded so pi-ai's `isRetryableAssistantError()`
|
|
332
|
+
does not classify it as transient: every number passes through
|
|
333
|
+
`formatQueueFigure`, which keeps at most two contiguous digits (a real
|
|
334
|
+
`queueCount` of 5204 renders as `5.2k`, because bare `520`/`503` would re-run the
|
|
335
|
+
whole turn against a gateway that already refused).
|
|
336
|
+
|
|
337
|
+
### Model cache ownership
|
|
338
|
+
|
|
339
|
+
The catalog is cached at `~/.pi/agent/qoder-models-cache.json` (and
|
|
340
|
+
`qoder-cn-models-cache.json`), a path other Qoder providers also use. Each write
|
|
341
|
+
is now tagged `"writer": "pi-provider-qoder-cn"`. A cache tagged by a different
|
|
342
|
+
implementation is discarded and re-fetched instead of being half-believed — its
|
|
343
|
+
model ids are not the ones this package registers. A cache with **no** tag stays
|
|
344
|
+
readable, because every release up to 0.2.21 wrote that shape.
|
|
345
|
+
|
|
262
346
|
## Development
|
|
263
347
|
|
|
264
348
|
```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
|
{
|
|
@@ -1783,29 +1791,36 @@ var init_oauth = __esm({
|
|
|
1783
1791
|
// src/index.ts
|
|
1784
1792
|
init_oauth();
|
|
1785
1793
|
|
|
1786
|
-
// src/
|
|
1787
|
-
|
|
1788
|
-
function num(value) {
|
|
1794
|
+
// src/quota.ts
|
|
1795
|
+
function quotaNumber(value) {
|
|
1789
1796
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1790
1797
|
}
|
|
1791
|
-
function
|
|
1792
|
-
return
|
|
1798
|
+
function bucketLimit(bucket) {
|
|
1799
|
+
return quotaNumber(bucket.total ?? bucket.cap);
|
|
1793
1800
|
}
|
|
1794
1801
|
function hasAllowance(bucket) {
|
|
1795
1802
|
if (!bucket) return false;
|
|
1796
|
-
return
|
|
1803
|
+
return bucketLimit(bucket) > 0 || quotaNumber(bucket.used) > 0 || quotaNumber(bucket.remaining) > 0;
|
|
1804
|
+
}
|
|
1805
|
+
function pickBucket(raw, camel) {
|
|
1806
|
+
if (!raw) return void 0;
|
|
1807
|
+
const source = raw;
|
|
1808
|
+
const snake = camel.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
1809
|
+
return source[snake] ?? source[camel];
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// src/auth/usage.ts
|
|
1813
|
+
init_region();
|
|
1814
|
+
function round2(value) {
|
|
1815
|
+
return Math.round(value * 100) / 100;
|
|
1797
1816
|
}
|
|
1798
1817
|
function resetTimestamp(expiresAt) {
|
|
1799
|
-
const ms =
|
|
1818
|
+
const ms = quotaNumber(expiresAt);
|
|
1800
1819
|
if (ms <= 0) return void 0;
|
|
1801
1820
|
const timestamp = ms < 1e12 ? ms * 1e3 : ms;
|
|
1802
1821
|
if (new Date(timestamp).getUTCFullYear() >= 9999) return void 0;
|
|
1803
1822
|
return new Date(timestamp).toISOString();
|
|
1804
1823
|
}
|
|
1805
|
-
function pickBucket(raw, camel) {
|
|
1806
|
-
const snake = camel.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
1807
|
-
return raw[snake] ?? raw[camel];
|
|
1808
|
-
}
|
|
1809
1824
|
async function fetchQoderUsageForMode(credentials, mode) {
|
|
1810
1825
|
const region = getQoderRegionConfig(mode);
|
|
1811
1826
|
const response = await fetch(getQoderUsageURL(mode), {
|
|
@@ -1830,12 +1845,12 @@ async function fetchQoderUsageForMode(credentials, mode) {
|
|
|
1830
1845
|
const usageBuckets = buckets.map(({ id, label, quota }) => ({
|
|
1831
1846
|
id,
|
|
1832
1847
|
label,
|
|
1833
|
-
usedDisplay: round2(
|
|
1834
|
-
limitDisplay: round2(
|
|
1848
|
+
usedDisplay: round2(quotaNumber(quota?.used)).toFixed(2),
|
|
1849
|
+
limitDisplay: round2(quotaNumber(quota?.total ?? quota?.cap)).toFixed(2),
|
|
1835
1850
|
unit: quota?.unit || "credits",
|
|
1836
1851
|
resetAt
|
|
1837
1852
|
}));
|
|
1838
|
-
const remaining = buckets.reduce((sum, { quota }) => sum + Math.max(0,
|
|
1853
|
+
const remaining = buckets.reduce((sum, { quota }) => sum + Math.max(0, quotaNumber(quota?.remaining)), 0);
|
|
1839
1854
|
const unit = buckets[0]?.quota?.unit || "credits";
|
|
1840
1855
|
return {
|
|
1841
1856
|
summary: buckets.length > 0 ? `${round2(remaining)} ${unit} remaining` : "",
|
|
@@ -2243,7 +2258,149 @@ async function runClaimCommand(mode, args, ctx) {
|
|
|
2243
2258
|
}
|
|
2244
2259
|
|
|
2245
2260
|
// src/protocol/errors.ts
|
|
2261
|
+
init_region();
|
|
2246
2262
|
init_vpc();
|
|
2263
|
+
|
|
2264
|
+
// src/protocol/queue.ts
|
|
2265
|
+
var MAX_WAIT_MS = 12e4;
|
|
2266
|
+
var DEFAULT_RETRY_SECONDS = 30;
|
|
2267
|
+
function digForQueue(node, depth) {
|
|
2268
|
+
if (depth > 6) return null;
|
|
2269
|
+
if (typeof node === "string") {
|
|
2270
|
+
const trimmed = node.trim();
|
|
2271
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
|
|
2272
|
+
try {
|
|
2273
|
+
return digForQueue(JSON.parse(trimmed), depth + 1);
|
|
2274
|
+
} catch {
|
|
2275
|
+
return null;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
if (node === null || typeof node !== "object") return null;
|
|
2279
|
+
if (Array.isArray(node)) {
|
|
2280
|
+
for (const item of node) {
|
|
2281
|
+
const found = digForQueue(item, depth + 1);
|
|
2282
|
+
if (found) return found;
|
|
2283
|
+
}
|
|
2284
|
+
return null;
|
|
2285
|
+
}
|
|
2286
|
+
const record = node;
|
|
2287
|
+
if (record.isQueued === true) return record;
|
|
2288
|
+
for (const value of Object.values(record)) {
|
|
2289
|
+
const found = digForQueue(value, depth + 1);
|
|
2290
|
+
if (found) return found;
|
|
2291
|
+
}
|
|
2292
|
+
return null;
|
|
2293
|
+
}
|
|
2294
|
+
function positiveNumber(value, fallback) {
|
|
2295
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
2296
|
+
}
|
|
2297
|
+
function textOf(value) {
|
|
2298
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
2299
|
+
}
|
|
2300
|
+
function parseQueueNotice(envelope) {
|
|
2301
|
+
const payload = digForQueue(envelope, 0);
|
|
2302
|
+
if (!payload) return null;
|
|
2303
|
+
const retryMs = positiveNumber(payload.retry_after_ms, 0) || positiveNumber(payload.retryAfterSeconds, DEFAULT_RETRY_SECONDS) * 1e3;
|
|
2304
|
+
return {
|
|
2305
|
+
retryAfterMs: Math.min(retryMs, MAX_WAIT_MS),
|
|
2306
|
+
waitTimeSeconds: positiveNumber(payload.waitTime, 0),
|
|
2307
|
+
queueCount: positiveNumber(payload.queueCount, 0),
|
|
2308
|
+
// Only a real boolean is surfaced; a missing field must not be read as
|
|
2309
|
+
// "unavailable" (that would collapse the normal queue into a hard stop).
|
|
2310
|
+
serviceAvailable: typeof payload.serviceAvailable === "boolean" ? payload.serviceAvailable : void 0,
|
|
2311
|
+
// `subCode` sits inside the same record as `isQueued` in the payloads seen
|
|
2312
|
+
// live, so one source is enough; the wrapper levels carry no extra names.
|
|
2313
|
+
subCode: textOf(payload.subCode),
|
|
2314
|
+
queueType: textOf(payload.queueType),
|
|
2315
|
+
modelKey: textOf(payload.modelKey)
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
function formatWaitSecondsShort(ms) {
|
|
2319
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0";
|
|
2320
|
+
const seconds = Math.round(ms / 1e3);
|
|
2321
|
+
if (seconds < 100) return String(seconds);
|
|
2322
|
+
return `${formatWaitMinutes(seconds)}\u5206\u949F`;
|
|
2323
|
+
}
|
|
2324
|
+
function formatQueueFailureMessage(notice, ctx) {
|
|
2325
|
+
const isCn = ctx.mode === "cn";
|
|
2326
|
+
const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
|
|
2327
|
+
const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
|
|
2328
|
+
const modelKey = notice.modelKey ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
|
|
2329
|
+
const hard = notice.serviceAvailable === false;
|
|
2330
|
+
const waited = formatWaitSecondsShort(ctx.waitedMs ?? 0);
|
|
2331
|
+
const queueLabel = formatQueueFigure(notice.queueCount ?? 0);
|
|
2332
|
+
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. `;
|
|
2333
|
+
if (hard) {
|
|
2334
|
+
return isCn ? [
|
|
2335
|
+
`[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`,
|
|
2336
|
+
`- \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`,
|
|
2337
|
+
`- \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`,
|
|
2338
|
+
`- \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}`
|
|
2339
|
+
].join("\n") : [
|
|
2340
|
+
`[Qoder international has no spare capacity] The gateway answered serviceAvailable:false (sub code ${sub}, queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
|
|
2341
|
+
`- 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.`,
|
|
2342
|
+
`- Switching model will not help: every model of this account answers on the same ${lane} lane (measured across auto / qmodel_38max / efficient / ultimate).`,
|
|
2343
|
+
`- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry later. Plan and quota: ${ctx.manageUrl}`
|
|
2344
|
+
].join("\n");
|
|
2345
|
+
}
|
|
2346
|
+
return isCn ? [
|
|
2347
|
+
`[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`,
|
|
2348
|
+
`- \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`,
|
|
2349
|
+
`- \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}`
|
|
2350
|
+
].join("\n") : [
|
|
2351
|
+
// "is at capacity" is the established wording for the drainable case and
|
|
2352
|
+
// the only queue text allowed to carry it; the hard refusal above must
|
|
2353
|
+
// never read as "wait it out".
|
|
2354
|
+
`[Qoder upstream queue] The model gateway is at capacity and parked this request (queue length ${queueLabel}, priority lane ${lane}, model ${modelKey})`,
|
|
2355
|
+
`- Meaning: ${tried ?? ""}the queue has not cleared yet. Credentials and quota are fine; queued attempts are not billed.`,
|
|
2356
|
+
`- Next step: retry shortly, or switch provider with /model to ${ctx.otherProviderID} while the queue drains. Plan and quota: ${ctx.manageUrl}`
|
|
2357
|
+
].join("\n");
|
|
2358
|
+
}
|
|
2359
|
+
function formatLaneCooldownMessage(notice, remainingMs, ctx) {
|
|
2360
|
+
const isCn = ctx.mode === "cn";
|
|
2361
|
+
const lane = notice.queueType ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
|
|
2362
|
+
const sub = notice.subCode ?? (isCn ? "\u672A\u63D0\u4F9B" : "unset");
|
|
2363
|
+
const left = Math.min(99, Math.max(1, Math.ceil(remainingMs / 1e3)));
|
|
2364
|
+
return isCn ? [
|
|
2365
|
+
`[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`,
|
|
2366
|
+
`- \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`,
|
|
2367
|
+
`- \u89E3\u51B3\u5EFA\u8BAE\uFF1A\u7528 /model \u5207\u6362\u5230 ${ctx.otherProviderID} \u6216\u5176\u4ED6 Provider\uFF0C\u6216 ${left} \u79D2\u540E\u91CD\u8BD5\u3002`
|
|
2368
|
+
].join("\n") : [
|
|
2369
|
+
`[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.`,
|
|
2370
|
+
`- Meaning: the previous request on this lane already waited through Retry-After and was still refused, so waiting again buys the same verdict.`,
|
|
2371
|
+
`- Next step: use /model to switch to ${ctx.otherProviderID} or another provider, or retry in ${left} seconds.`
|
|
2372
|
+
].join("\n");
|
|
2373
|
+
}
|
|
2374
|
+
function formatQueueFigure(n) {
|
|
2375
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
2376
|
+
if (n < 100) return String(Math.round(n));
|
|
2377
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2378
|
+
return `${(n / 1e3).toFixed(1)}k`;
|
|
2379
|
+
}
|
|
2380
|
+
function formatWaitMinutes(seconds) {
|
|
2381
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return "<1";
|
|
2382
|
+
return String(Math.min(99, Math.max(1, Math.round(seconds / 60))));
|
|
2383
|
+
}
|
|
2384
|
+
function sleep(ms, signal) {
|
|
2385
|
+
return new Promise((resolve, reject) => {
|
|
2386
|
+
if (signal?.aborted) {
|
|
2387
|
+
reject(new Error("Aborted while waiting for the Qoder queue."));
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
let timer;
|
|
2391
|
+
const onAbort = () => {
|
|
2392
|
+
clearTimeout(timer);
|
|
2393
|
+
reject(new Error("Aborted while waiting for the Qoder queue."));
|
|
2394
|
+
};
|
|
2395
|
+
timer = setTimeout(() => {
|
|
2396
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2397
|
+
resolve();
|
|
2398
|
+
}, ms);
|
|
2399
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// src/protocol/errors.ts
|
|
2247
2404
|
function parseQoderErrorPayload(payload) {
|
|
2248
2405
|
if (!payload) return null;
|
|
2249
2406
|
let parsed = payload;
|
|
@@ -2302,6 +2459,7 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "c
|
|
|
2302
2459
|
const parsed = parseQoderErrorPayload(rawBody);
|
|
2303
2460
|
const code = parsed?.code;
|
|
2304
2461
|
const message = parsed?.message || (typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody));
|
|
2462
|
+
const bodyForMatch = `${message} ${typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody ?? "")}`;
|
|
2305
2463
|
const countdown = getBeijingDailyResetCountdown(now);
|
|
2306
2464
|
const isCn = mode !== "global";
|
|
2307
2465
|
if (code === "110" || /billing daily count exceeded/i.test(message) || /daily usage limit reached/i.test(message)) {
|
|
@@ -2399,7 +2557,17 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "c
|
|
|
2399
2557
|
`- Next step: every retry must be re-signed; if it keeps repeating, switch provider with /model and try again shortly.`
|
|
2400
2558
|
].join("\n");
|
|
2401
2559
|
}
|
|
2402
|
-
if (code === "10605" || /"isQueued"\s*:\s*true
|
|
2560
|
+
if (code === "10605" || /"isQueued"\s*:\s*true/.test(message)) {
|
|
2561
|
+
const notice = parseQueueNotice(rawBody) ?? parseQueueNotice(message);
|
|
2562
|
+
if (notice) {
|
|
2563
|
+
return formatQueueFailureMessage(notice, {
|
|
2564
|
+
mode,
|
|
2565
|
+
// This call does not own the retry loop, so it claims no attempt count.
|
|
2566
|
+
waitedMs: 0,
|
|
2567
|
+
manageUrl: getQoderRegionConfig(mode).manageUrl,
|
|
2568
|
+
otherProviderID: mode === "cn" ? "qoder" : "qoder-cn"
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2403
2571
|
return isCn ? [
|
|
2404
2572
|
`[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
2573
|
`- \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 +2578,17 @@ function formatQoderStreamError(statusCode, rawBody, now = Date.now(), mode = "c
|
|
|
2410
2578
|
`- Next step: retry shortly, or switch provider with /model while the queue drains.`
|
|
2411
2579
|
].join("\n");
|
|
2412
2580
|
}
|
|
2581
|
+
if (code === "1210" || /不支持关闭思考|cannot disable thinking|always.{0,12}think/i.test(bodyForMatch)) {
|
|
2582
|
+
return isCn ? [
|
|
2583
|
+
`[Qoder CN \u4E0D\u652F\u6301\u5173\u95ED\u601D\u8003] \u8BE5\u6A21\u578B\u59CB\u7EC8\u601D\u8003 (\u9519\u8BEF\u7801 1210)`,
|
|
2584
|
+
`- \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`,
|
|
2585
|
+
`- \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`
|
|
2586
|
+
].join("\n") : [
|
|
2587
|
+
`[Qoder model always thinks] This model cannot disable thinking (code 1210)`,
|
|
2588
|
+
`- Meaning: the model has no "off" switch, so a request carrying enable_thinking=false is refused. This will not change on retry.`,
|
|
2589
|
+
`- Next step: pick a thinking level (low / medium / high / max) instead of turning thinking off.`
|
|
2590
|
+
].join("\n");
|
|
2591
|
+
}
|
|
2413
2592
|
if (rawBody === void 0 || rawBody === null) {
|
|
2414
2593
|
return `Upstream status ${statusCode}`;
|
|
2415
2594
|
}
|
|
@@ -2572,33 +2751,28 @@ function bucketRow(label, bucket, danger) {
|
|
|
2572
2751
|
danger
|
|
2573
2752
|
};
|
|
2574
2753
|
}
|
|
2575
|
-
function hasBucket(bucket) {
|
|
2576
|
-
if (!bucket) return false;
|
|
2577
|
-
return (bucket.total ?? 0) > 0 || (bucket.used ?? 0) > 0;
|
|
2578
|
-
}
|
|
2579
|
-
function pickBucket2(raw, camel) {
|
|
2580
|
-
if (!raw) return void 0;
|
|
2581
|
-
const snake = camel.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
2582
|
-
return raw[snake] ?? raw[camel];
|
|
2583
|
-
}
|
|
2584
2754
|
function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
|
|
2585
2755
|
const color = options.color ?? false;
|
|
2586
2756
|
const region = getQoderRegionConfig(mode);
|
|
2587
2757
|
const danger = Boolean(raw?.isQuotaExceeded);
|
|
2588
2758
|
const userType = raw?.userType ? ` (${raw.userType})` : "";
|
|
2589
|
-
const showUserQuota =
|
|
2590
|
-
const showAddOnQuota =
|
|
2591
|
-
const org =
|
|
2592
|
-
const showOrg =
|
|
2593
|
-
const
|
|
2759
|
+
const showUserQuota = hasAllowance(pickBucket(raw, "userQuota"));
|
|
2760
|
+
const showAddOnQuota = hasAllowance(pickBucket(raw, "addOnQuota"));
|
|
2761
|
+
const org = pickBucket(raw, "orgResourcePackage") ?? pickBucket(raw, "sharedQuota");
|
|
2762
|
+
const showOrg = hasAllowance(org);
|
|
2763
|
+
const orgLimit = org ? bucketLimit(org) : 0;
|
|
2764
|
+
const orgPercent = org && orgLimit > 0 ? formatPercent(quotaNumber(org.used) / orgLimit * 100) : void 0;
|
|
2594
2765
|
const overallPercent = formatPercent(
|
|
2595
|
-
showOrg ? normalizePercent(raw?.totalUsagePercentage)
|
|
2766
|
+
showOrg ? orgPercent ?? normalizePercent(raw?.totalUsagePercentage) : normalizePercent(raw?.totalUsagePercentage)
|
|
2596
2767
|
);
|
|
2597
2768
|
const rows = [{ label: "Overall", percent: overallPercent, tail: "used", danger }];
|
|
2598
|
-
if (showUserQuota) rows.push(bucketRow("Plan quota",
|
|
2599
|
-
if (showAddOnQuota) rows.push(bucketRow("Add-on",
|
|
2600
|
-
if (showOrg && org)
|
|
2601
|
-
rows.push(
|
|
2769
|
+
if (showUserQuota) rows.push(bucketRow("Plan quota", pickBucket(raw, "userQuota"), danger));
|
|
2770
|
+
if (showAddOnQuota) rows.push(bucketRow("Add-on", pickBucket(raw, "addOnQuota"), danger));
|
|
2771
|
+
if (showOrg && org) {
|
|
2772
|
+
rows.push(
|
|
2773
|
+
bucketRow("Enterprise", { ...org, percentage: void 0, total: orgLimit, used: quotaNumber(org.used) }, danger)
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2602
2776
|
const isPersonalStandard = raw?.userType === "personal_standard" || raw?.userType === "free";
|
|
2603
2777
|
const notes = [];
|
|
2604
2778
|
if (danger) {
|
|
@@ -2636,7 +2810,7 @@ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
|
|
|
2636
2810
|
});
|
|
2637
2811
|
}
|
|
2638
2812
|
}
|
|
2639
|
-
const addOn =
|
|
2813
|
+
const addOn = pickBucket(raw, "addOnQuota");
|
|
2640
2814
|
if (showAddOnQuota && (addOn?.remaining ?? 0) > 0 && checkin) {
|
|
2641
2815
|
const expiryMs = checkin.expiresAt ? Date.parse(checkin.expiresAt) : void 0;
|
|
2642
2816
|
const grant = { expiryMs, amount: checkin.amount, validityDays: checkin.validityDays };
|
|
@@ -2789,80 +2963,112 @@ function qoderEncodeBody(plaintext) {
|
|
|
2789
2963
|
return out;
|
|
2790
2964
|
}
|
|
2791
2965
|
|
|
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;
|
|
2966
|
+
// src/protocol/queue-policy.ts
|
|
2967
|
+
var DEFAULT_QUEUE_POLICY = Object.freeze({
|
|
2968
|
+
unavailableAttempts: 1,
|
|
2969
|
+
maxAttempts: 3,
|
|
2970
|
+
totalWaitBudgetMs: 9e4,
|
|
2971
|
+
singleWaitCapMs: 12e4,
|
|
2972
|
+
jitterMs: 2e3,
|
|
2973
|
+
/**
|
|
2974
|
+
* OFF by default. A skipped request is a request the user expected to be sent,
|
|
2975
|
+
* and "the lane refused a moment ago" is an inference, not a gateway answer:
|
|
2976
|
+
* it holds on the international service (every model of the account answered
|
|
2977
|
+
* `p3`/`RESOURCE_LIMIT`), but it would also suppress a request that was about
|
|
2978
|
+
* to be served. Opt in with `QODER_QUEUE_LANE_COOLDOWN_MS=20000` when a whole
|
|
2979
|
+
* agent turn is otherwise spending 31s per call on a dead lane.
|
|
2980
|
+
*/
|
|
2981
|
+
laneCooldownMs: 0
|
|
2982
|
+
});
|
|
2983
|
+
var LIMITS = {
|
|
2984
|
+
attempts: { min: 0, max: 10 },
|
|
2985
|
+
budgetMs: { min: 0, max: 15 * 6e4 },
|
|
2986
|
+
singleMs: { min: 1e3, max: 5 * 6e4 },
|
|
2987
|
+
jitterMs: { min: 0, max: 3e4 },
|
|
2988
|
+
cooldownMs: { min: 0, max: 5 * 6e4 }
|
|
2989
|
+
};
|
|
2990
|
+
function clampInt(raw, min, max, fallback) {
|
|
2991
|
+
if (raw === void 0) return fallback;
|
|
2992
|
+
const trimmed = raw.trim();
|
|
2993
|
+
if (!trimmed) return fallback;
|
|
2994
|
+
const value = Number(trimmed);
|
|
2995
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) return fallback;
|
|
2996
|
+
if (value < 0) return fallback;
|
|
2997
|
+
return Math.min(max, Math.max(min, value));
|
|
2998
|
+
}
|
|
2999
|
+
function readQueuePolicy(env = process.env) {
|
|
2830
3000
|
return {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3001
|
+
unavailableAttempts: clampInt(
|
|
3002
|
+
env.QODER_QUEUE_UNAVAILABLE_ATTEMPTS,
|
|
3003
|
+
LIMITS.attempts.min,
|
|
3004
|
+
LIMITS.attempts.max,
|
|
3005
|
+
DEFAULT_QUEUE_POLICY.unavailableAttempts
|
|
3006
|
+
),
|
|
3007
|
+
maxAttempts: clampInt(
|
|
3008
|
+
env.QODER_QUEUE_MAX_ATTEMPTS,
|
|
3009
|
+
LIMITS.attempts.min,
|
|
3010
|
+
LIMITS.attempts.max,
|
|
3011
|
+
DEFAULT_QUEUE_POLICY.maxAttempts
|
|
3012
|
+
),
|
|
3013
|
+
totalWaitBudgetMs: clampInt(
|
|
3014
|
+
env.QODER_QUEUE_MAX_TOTAL_WAIT_MS,
|
|
3015
|
+
LIMITS.budgetMs.min,
|
|
3016
|
+
LIMITS.budgetMs.max,
|
|
3017
|
+
DEFAULT_QUEUE_POLICY.totalWaitBudgetMs
|
|
3018
|
+
),
|
|
3019
|
+
singleWaitCapMs: clampInt(
|
|
3020
|
+
env.QODER_QUEUE_SINGLE_WAIT_MS,
|
|
3021
|
+
LIMITS.singleMs.min,
|
|
3022
|
+
LIMITS.singleMs.max,
|
|
3023
|
+
DEFAULT_QUEUE_POLICY.singleWaitCapMs
|
|
3024
|
+
),
|
|
3025
|
+
jitterMs: clampInt(
|
|
3026
|
+
env.QODER_QUEUE_JITTER_MS,
|
|
3027
|
+
LIMITS.jitterMs.min,
|
|
3028
|
+
LIMITS.jitterMs.max,
|
|
3029
|
+
DEFAULT_QUEUE_POLICY.jitterMs
|
|
3030
|
+
),
|
|
3031
|
+
laneCooldownMs: clampInt(
|
|
3032
|
+
env.QODER_QUEUE_LANE_COOLDOWN_MS,
|
|
3033
|
+
LIMITS.cooldownMs.min,
|
|
3034
|
+
LIMITS.cooldownMs.max,
|
|
3035
|
+
DEFAULT_QUEUE_POLICY.laneCooldownMs
|
|
3036
|
+
)
|
|
2837
3037
|
};
|
|
2838
3038
|
}
|
|
2839
|
-
function
|
|
2840
|
-
|
|
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`;
|
|
3039
|
+
function allowedAttemptsFor(policy, notice) {
|
|
3040
|
+
return notice.serviceAvailable === false ? policy.unavailableAttempts : policy.maxAttempts;
|
|
2844
3041
|
}
|
|
2845
|
-
function
|
|
2846
|
-
if (!Number.isFinite(
|
|
2847
|
-
|
|
3042
|
+
function nextSleepMs(policy, notice, waitedMs, random = Math.random) {
|
|
3043
|
+
if (!Number.isFinite(notice.retryAfterMs) || notice.retryAfterMs <= 0) return 0;
|
|
3044
|
+
const remaining = Math.max(0, policy.totalWaitBudgetMs - waitedMs);
|
|
3045
|
+
if (remaining === 0) return 0;
|
|
3046
|
+
const base = Math.min(notice.retryAfterMs, policy.singleWaitCapMs, remaining);
|
|
3047
|
+
const jitter = policy.jitterMs > 0 ? random() * policy.jitterMs : 0;
|
|
3048
|
+
return Math.max(0, Math.min(policy.singleWaitCapMs, remaining, base + jitter));
|
|
2848
3049
|
}
|
|
2849
|
-
function
|
|
2850
|
-
return
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
3050
|
+
function isHardUnavailable(notice) {
|
|
3051
|
+
return notice.serviceAvailable === false;
|
|
3052
|
+
}
|
|
3053
|
+
function laneKey(mode, notice) {
|
|
3054
|
+
return `${mode}:${notice.queueType ?? "unknown"}:${notice.subCode ?? "queued"}`;
|
|
3055
|
+
}
|
|
3056
|
+
var armedLanes = /* @__PURE__ */ new Map();
|
|
3057
|
+
function armLaneCooldown(key, ttlMs, notice, now = Date.now()) {
|
|
3058
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
3059
|
+
armedLanes.delete(key);
|
|
3060
|
+
return;
|
|
3061
|
+
}
|
|
3062
|
+
armedLanes.set(key, { until: now + ttlMs, notice });
|
|
3063
|
+
}
|
|
3064
|
+
function pendingLaneCooldown(mode, now = Date.now()) {
|
|
3065
|
+
for (const [key, entry] of armedLanes) {
|
|
3066
|
+
if (!key.startsWith(`${mode}:`)) continue;
|
|
3067
|
+
const remaining = entry.until - now;
|
|
3068
|
+
if (remaining > 0) return { key, remainingMs: remaining, notice: entry.notice };
|
|
3069
|
+
armedLanes.delete(key);
|
|
3070
|
+
}
|
|
3071
|
+
return null;
|
|
2866
3072
|
}
|
|
2867
3073
|
|
|
2868
3074
|
// src/protocol/thinking.ts
|
|
@@ -3372,6 +3578,8 @@ function streamQoder(model, context, options) {
|
|
|
3372
3578
|
const providerMode = model.provider === "qoder-cn" ? "cn" : "global";
|
|
3373
3579
|
let reader;
|
|
3374
3580
|
let started = false;
|
|
3581
|
+
const queuePolicy = readQueuePolicy();
|
|
3582
|
+
let turnWaitedMs = 0;
|
|
3375
3583
|
const region = getQoderRegionConfig(providerMode);
|
|
3376
3584
|
const runAttempt = async (apiKey) => {
|
|
3377
3585
|
const accessToken = apiKey ?? options?.apiKey;
|
|
@@ -3419,6 +3627,7 @@ function streamQoder(model, context, options) {
|
|
|
3419
3627
|
const requestedLevel = options?.reasoning;
|
|
3420
3628
|
const clamped = requestedLevel ? clampThinkingLevel(model, requestedLevel) : void 0;
|
|
3421
3629
|
const reasoningLevel = clamped === "off" ? void 0 : clamped;
|
|
3630
|
+
const cannotDisableThinking = model.thinkingLevelMap?.off === null;
|
|
3422
3631
|
const parameters = { max_tokens: maxTokens };
|
|
3423
3632
|
if (reasoningLevel) {
|
|
3424
3633
|
parameters.enable_thinking = true;
|
|
@@ -3427,6 +3636,7 @@ function streamQoder(model, context, options) {
|
|
|
3427
3636
|
if (modelConfig?.thinking_config?.enabled?.efforts && typeof effort === "string") {
|
|
3428
3637
|
parameters.reasoning_effort = effort;
|
|
3429
3638
|
}
|
|
3639
|
+
} else if (cannotDisableThinking) {
|
|
3430
3640
|
} else {
|
|
3431
3641
|
parameters.enable_thinking = false;
|
|
3432
3642
|
}
|
|
@@ -3507,6 +3717,16 @@ function streamQoder(model, context, options) {
|
|
|
3507
3717
|
};
|
|
3508
3718
|
};
|
|
3509
3719
|
let queuedAttempt = 0;
|
|
3720
|
+
const cooled = pendingLaneCooldown(providerMode);
|
|
3721
|
+
if (cooled) {
|
|
3722
|
+
throw new Error(
|
|
3723
|
+
formatLaneCooldownMessage(cooled.notice, cooled.remainingMs, {
|
|
3724
|
+
mode: providerMode,
|
|
3725
|
+
manageUrl: region.manageUrl,
|
|
3726
|
+
otherProviderID: providerMode === "cn" ? "qoder" : "qoder-cn"
|
|
3727
|
+
})
|
|
3728
|
+
);
|
|
3729
|
+
}
|
|
3510
3730
|
await reader?.cancel().catch(() => {
|
|
3511
3731
|
});
|
|
3512
3732
|
reader = void 0;
|
|
@@ -3535,19 +3755,35 @@ function streamQoder(model, context, options) {
|
|
|
3535
3755
|
if (queue) {
|
|
3536
3756
|
await reader.cancel().catch(() => {
|
|
3537
3757
|
});
|
|
3538
|
-
const
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3758
|
+
const queueCtx = {
|
|
3759
|
+
mode: providerMode,
|
|
3760
|
+
attempts: queuedAttempt + 1,
|
|
3761
|
+
waitedMs: turnWaitedMs,
|
|
3762
|
+
manageUrl: region.manageUrl,
|
|
3763
|
+
otherProviderID: providerMode === "cn" ? "qoder" : "qoder-cn"
|
|
3764
|
+
};
|
|
3765
|
+
if (process.env.QODER_DEBUG) {
|
|
3766
|
+
console.error(
|
|
3767
|
+
`[qoder] queued lane=${queue.queueType ?? "?"} sub=${queue.subCode ?? "?"} model=${queue.modelKey ?? "?"} serviceAvailable=${String(queue.serviceAvailable ?? "?")} attempt=${queuedAttempt + 1} waited=${turnWaitedMs}ms`
|
|
3547
3768
|
);
|
|
3548
3769
|
}
|
|
3770
|
+
const maxAttempts = allowedAttemptsFor(queuePolicy, queue);
|
|
3771
|
+
if (queuedAttempt >= maxAttempts) {
|
|
3772
|
+
if (isHardUnavailable(queue)) {
|
|
3773
|
+
armLaneCooldown(laneKey(providerMode, queue), queuePolicy.laneCooldownMs, queue);
|
|
3774
|
+
}
|
|
3775
|
+
throw new Error(formatQueueFailureMessage(queue, queueCtx));
|
|
3776
|
+
}
|
|
3777
|
+
const waitMs = nextSleepMs(queuePolicy, queue, turnWaitedMs);
|
|
3778
|
+
if (waitMs <= 0) {
|
|
3779
|
+
if (isHardUnavailable(queue)) {
|
|
3780
|
+
armLaneCooldown(laneKey(providerMode, queue), queuePolicy.laneCooldownMs, queue);
|
|
3781
|
+
}
|
|
3782
|
+
throw new Error(formatQueueFailureMessage(queue, queueCtx));
|
|
3783
|
+
}
|
|
3549
3784
|
queuedAttempt++;
|
|
3550
|
-
|
|
3785
|
+
turnWaitedMs += waitMs;
|
|
3786
|
+
await sleep(waitMs, options?.signal);
|
|
3551
3787
|
continue;
|
|
3552
3788
|
}
|
|
3553
3789
|
if (await peekCredentialExpired(reader, decoder)) {
|
package/package.json
CHANGED