@mars-sea/dsh-commandcode-provider 0.11.5 → 0.11.7
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/CHANGELOG.md +24 -0
- package/lib/client.js +1 -1
- package/lib/index.d.ts +204 -38
- package/lib/index.js +501 -79
- package/lib/index.js.map +1 -1
- package/package.json +13 -1
package/lib/index.js
CHANGED
|
@@ -56,6 +56,11 @@ const KNOWN_EFFORTS = {
|
|
|
56
56
|
"medium",
|
|
57
57
|
"xhigh"
|
|
58
58
|
],
|
|
59
|
+
"Qwen/Qwen3.8-Omni-Flash": [
|
|
60
|
+
"low",
|
|
61
|
+
"medium",
|
|
62
|
+
"xhigh"
|
|
63
|
+
],
|
|
59
64
|
"claude-fable-5-1": [
|
|
60
65
|
"low",
|
|
61
66
|
"medium",
|
|
@@ -219,6 +224,11 @@ const KNOWN_EFFORTS = {
|
|
|
219
224
|
"high",
|
|
220
225
|
"max"
|
|
221
226
|
],
|
|
227
|
+
"z-ai/glm-5.3-flashx": [
|
|
228
|
+
"low",
|
|
229
|
+
"high",
|
|
230
|
+
"max"
|
|
231
|
+
],
|
|
222
232
|
"zai-org/GLM-5.2": ["high", "max"],
|
|
223
233
|
"zai-org/GLM-5.3": [
|
|
224
234
|
"low",
|
|
@@ -293,6 +303,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
293
303
|
"Qwen/Qwen3.8-Flash",
|
|
294
304
|
"Qwen/Qwen3.8-Max",
|
|
295
305
|
"Qwen/Qwen3.8-Max-0902",
|
|
306
|
+
"Qwen/Qwen3.8-Omni-Flash",
|
|
296
307
|
"claude-fable-5-1",
|
|
297
308
|
"claude-fable-5",
|
|
298
309
|
"claude-haiku-4-5-20251001",
|
|
@@ -334,7 +345,8 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
334
345
|
"xai/grok-4.5",
|
|
335
346
|
"xai/grok-4.6",
|
|
336
347
|
"xiaomi/mimo-v2.5",
|
|
337
|
-
"z-ai/glm-5.3-flash"
|
|
348
|
+
"z-ai/glm-5.3-flash",
|
|
349
|
+
"z-ai/glm-5.3-flashx"
|
|
338
350
|
]);
|
|
339
351
|
/**
|
|
340
352
|
* Models the official CLI's model table (command-code@1.53.0) marks
|
|
@@ -355,8 +367,9 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
355
367
|
* `['low', 'medium', 'high']` efforts in command-code@1.38.0 and moved to
|
|
356
368
|
* `KNOWN_EFFORTS`. `moonshotai/Kimi-K3` followed the same path in
|
|
357
369
|
* command-code@1.39.3 — it gained `['low', 'high', 'max']` efforts and moved
|
|
358
|
-
* to `KNOWN_EFFORTS`. command-code@1.42.0 added
|
|
359
|
-
*
|
|
370
|
+
* to `KNOWN_EFFORTS`. command-code@1.42.0 added LongCat 2.0 (then
|
|
371
|
+
* `meituan/LongCat-2.0:free`, now the paid `meituan/LongCat-2.0`; reasoning:!0,
|
|
372
|
+
* no efforts). command-code@1.45.0 gave the Muse Spark family
|
|
360
373
|
* (1.1, 1.2, 1.2-contributor, 1.3, 1.3-contributor) selectable
|
|
361
374
|
* `['low', 'medium', 'high', 'xhigh']` efforts — they moved to `KNOWN_EFFORTS`.
|
|
362
375
|
* command-code@1.51.3 gave `MiniMaxAI/MiniMax-M3` selectable
|
|
@@ -382,7 +395,7 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
|
|
|
382
395
|
"thinkingmachines/inkling",
|
|
383
396
|
"thinkingmachines/inkling-small",
|
|
384
397
|
"poolside/laguna-s-2.1-free",
|
|
385
|
-
"meituan/LongCat-2.0
|
|
398
|
+
"meituan/LongCat-2.0",
|
|
386
399
|
"inclusionai/ling-3.0-flash-sante:free"
|
|
387
400
|
]);
|
|
388
401
|
/**
|
|
@@ -438,11 +451,14 @@ function requiresMessagesEndpoint(modelId) {
|
|
|
438
451
|
* the official plan/pricing pages grants individual-provider/max/ultra and
|
|
439
452
|
* teams-pro only, and the CLI's plan-access map blocks it on Go/GOAT/Pro.
|
|
440
453
|
* command-code@1.41.0 added `Qwen/Qwen3.8-Max-0902` (Go) and 1.42.0 added
|
|
441
|
-
*
|
|
454
|
+
* LongCat 2.0 (Go — a free promo until 2026-09-19, when the backend renamed
|
|
455
|
+
* `meituan/LongCat-2.0:free` to the paid `meituan/LongCat-2.0`); 1.43.0 added
|
|
442
456
|
* `google/gemini-3.8-flash` (GOAT) and 1.44.0 added `meta/muse-spark-1.3`
|
|
443
457
|
* (GOAT) plus its Contributor sibling (Go); command-code@1.52.0 added the
|
|
444
458
|
* free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
|
|
445
|
-
* added `deepseek/deepseek-v4.1-flash` (Go).
|
|
459
|
+
* added `deepseek/deepseek-v4.1-flash` (Go); command-code@1.56.0 added
|
|
460
|
+
* `Qwen/Qwen3.8-Omni-Flash` (Go); command-code@1.57.0 added
|
|
461
|
+
* `z-ai/glm-5.3-flashx` (Go).
|
|
446
462
|
*
|
|
447
463
|
* The Provider API exposes no plan metadata, so this snapshot is the source of
|
|
448
464
|
* truth for the picker's plan annotation — it answers "which plan do I need to
|
|
@@ -465,13 +481,14 @@ const KNOWN_PLANS = {
|
|
|
465
481
|
"Qwen/Qwen3.8-Flash": "go",
|
|
466
482
|
"Qwen/Qwen3.8-Max": "go",
|
|
467
483
|
"Qwen/Qwen3.8-Max-0902": "go",
|
|
484
|
+
"Qwen/Qwen3.8-Omni-Flash": "go",
|
|
468
485
|
"deepseek/deepseek-v4-flash-fast": "go",
|
|
469
486
|
"deepseek/deepseek-v4.1-flash": "go",
|
|
470
487
|
"deepseek/deepseek-v4-flash": "go",
|
|
471
488
|
"deepseek/deepseek-v4-flash-vision-exp": "go",
|
|
472
489
|
"deepseek/deepseek-v4-pro": "go",
|
|
473
490
|
"gpt-5.6-luna": "go",
|
|
474
|
-
"meituan/LongCat-2.0
|
|
491
|
+
"meituan/LongCat-2.0": "go",
|
|
475
492
|
"inclusionai/ling-3.0-flash-sante:free": "go",
|
|
476
493
|
"meta/muse-spark-1.2-contributor": "go",
|
|
477
494
|
"meta/muse-spark-1.3-contributor": "go",
|
|
@@ -492,6 +509,7 @@ const KNOWN_PLANS = {
|
|
|
492
509
|
"xiaomi/mimo-v2.5": "go",
|
|
493
510
|
"xiaomi/mimo-v2.5-pro": "go",
|
|
494
511
|
"z-ai/glm-5.3-flash": "go",
|
|
512
|
+
"z-ai/glm-5.3-flashx": "go",
|
|
495
513
|
"zai-org/GLM-5": "go",
|
|
496
514
|
"zai-org/GLM-5.1": "go",
|
|
497
515
|
"zai-org/GLM-5.2": "go",
|
|
@@ -652,6 +670,23 @@ function modelVisibleInPlan(modelId, access) {
|
|
|
652
670
|
if (weight === void 0) return true;
|
|
653
671
|
return weight <= access.tierWeight;
|
|
654
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* Whether the picker lists `modelId` for a whole POOL of accounts: true when
|
|
675
|
+
* at least one of them includes it. The picker's plan filter is asked this
|
|
676
|
+
* rather than {@link modelVisibleInPlan} for a single account because the pool
|
|
677
|
+
* — not any one account — is what serves a request: keying the list on
|
|
678
|
+
* whichever account rotation happened to reach made models appear and vanish
|
|
679
|
+
* as accounts rotated, and hid models the user's other accounts could run.
|
|
680
|
+
*
|
|
681
|
+
* An account whose facts are unknown counts as "may include it" (the same
|
|
682
|
+
* fail-open rule `modelVisibleInPlan` applies one level down), and an empty
|
|
683
|
+
* pool is "show everything": hiding a model somebody can run is the one
|
|
684
|
+
* failure this filter must never cause.
|
|
685
|
+
*/
|
|
686
|
+
function modelVisibleForAnyAccount(modelId, accounts) {
|
|
687
|
+
if (accounts === void 0 || accounts.length === 0) return true;
|
|
688
|
+
return accounts.some((access) => modelVisibleInPlan(modelId, access));
|
|
689
|
+
}
|
|
655
690
|
const KNOWN_DEALS = {
|
|
656
691
|
"MiniMaxAI/MiniMax-M3": { label: "50% off" },
|
|
657
692
|
"xiaomi/mimo-v2.5-pro": { label: "99% off" },
|
|
@@ -660,10 +695,6 @@ const KNOWN_DEALS = {
|
|
|
660
695
|
label: "FREE",
|
|
661
696
|
free: true
|
|
662
697
|
},
|
|
663
|
-
"meituan/LongCat-2.0:free": {
|
|
664
|
-
label: "FREE",
|
|
665
|
-
free: true
|
|
666
|
-
},
|
|
667
698
|
"inclusionai/ling-3.0-flash-sante:free": {
|
|
668
699
|
label: "FREE",
|
|
669
700
|
free: true
|
|
@@ -1078,6 +1109,14 @@ const MODEL_PRICE_ROWS = [
|
|
|
1078
1109
|
.03
|
|
1079
1110
|
]
|
|
1080
1111
|
},
|
|
1112
|
+
{
|
|
1113
|
+
id: "glm-5.3-flashx",
|
|
1114
|
+
rates: [
|
|
1115
|
+
.37,
|
|
1116
|
+
1.25,
|
|
1117
|
+
.075
|
|
1118
|
+
]
|
|
1119
|
+
},
|
|
1081
1120
|
{
|
|
1082
1121
|
id: "gpt-5.3-codex",
|
|
1083
1122
|
rates: [
|
|
@@ -1290,6 +1329,14 @@ const MODEL_PRICE_ROWS = [
|
|
|
1290
1329
|
.3
|
|
1291
1330
|
]
|
|
1292
1331
|
},
|
|
1332
|
+
{
|
|
1333
|
+
id: "longcat-2.0",
|
|
1334
|
+
rates: [
|
|
1335
|
+
.3,
|
|
1336
|
+
1.2,
|
|
1337
|
+
.006
|
|
1338
|
+
]
|
|
1339
|
+
},
|
|
1293
1340
|
{
|
|
1294
1341
|
id: "mimo-v2.5",
|
|
1295
1342
|
rates: [
|
|
@@ -1507,6 +1554,14 @@ const MODEL_PRICE_ROWS = [
|
|
|
1507
1554
|
.25
|
|
1508
1555
|
]
|
|
1509
1556
|
},
|
|
1557
|
+
{
|
|
1558
|
+
id: "qwen-3.8-omni-flash",
|
|
1559
|
+
rates: [
|
|
1560
|
+
.15,
|
|
1561
|
+
.47,
|
|
1562
|
+
.016
|
|
1563
|
+
]
|
|
1564
|
+
},
|
|
1510
1565
|
{
|
|
1511
1566
|
id: "step-3.5-flash",
|
|
1512
1567
|
rates: [
|
|
@@ -1869,15 +1924,20 @@ function installCostProjection(ctx) {
|
|
|
1869
1924
|
* usable, the routed account — resolving each slot's key lazily (literal
|
|
1870
1925
|
* config key → credential seam → launch environment → the official CLI
|
|
1871
1926
|
* auth file for the default slot only).
|
|
1872
|
-
* - {@link CommandCodeAccountPool.markRejected} records a 429
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
1875
|
-
*
|
|
1927
|
+
* - {@link CommandCodeAccountPool.markRejected} records a 429 or 401 against
|
|
1928
|
+
* the exact API key, so several slots sharing one key share one state. A
|
|
1929
|
+
* 429 that NAMED a usage window marks the window exhausted; a 429 that
|
|
1930
|
+
* named none is recorded as a plain throttle, which rotates the same way
|
|
1931
|
+
* but is never reported as an exhausted window (issue #54).
|
|
1876
1932
|
* - When every account is marked, the pool probes each key's
|
|
1877
1933
|
* `/alpha/billing/credits` window limits (through the injected
|
|
1878
1934
|
* {@link CommandCodeAccountPoolDeps.probeWindow}): an account whose window
|
|
1879
1935
|
* no longer reports `exceeded` is revived, otherwise the pool throws a
|
|
1880
1936
|
* `RATE_LIMIT` error naming the earliest reset time.
|
|
1937
|
+
* - An account the user explicitly asked for — the manual pin
|
|
1938
|
+
* ({@link CommandCodeAccountPoolDeps.preferredId}) or a model rule's
|
|
1939
|
+
* routed account — is probed the same way when a mark would demote it, so
|
|
1940
|
+
* a fallback is never permanent (issue #51).
|
|
1881
1941
|
*
|
|
1882
1942
|
* The pool is deliberately cordis-free (like the adapter): every host fact
|
|
1883
1943
|
* arrives through injected thunks, so node tests can drive it directly.
|
|
@@ -1894,6 +1954,18 @@ function installCostProjection(ctx) {
|
|
|
1894
1954
|
* window opens" into "fail now".
|
|
1895
1955
|
*/
|
|
1896
1956
|
const RETRY_MAX_DELAY_MS = 9e5;
|
|
1957
|
+
/**
|
|
1958
|
+
* How long an explicitly selected account's rate-limit mark may stand before
|
|
1959
|
+
* its window is probed again. A 429 whose reset was never learned marks the
|
|
1960
|
+
* key `unknown`, which {@link accountUsable} refuses forever — deliberately,
|
|
1961
|
+
* because the all-marked revival pass is the only thing meant to clear it and
|
|
1962
|
+
* it costs one billing call per key. That is fine for a key nobody asked for,
|
|
1963
|
+
* and wrong for the one the user pinned: a single 429 would otherwise demote
|
|
1964
|
+
* the user's own selection until the process restarts (issue #51). The probe
|
|
1965
|
+
* is therefore allowed once per interval per key, which bounds an endpoint
|
|
1966
|
+
* that keeps failing while a successful probe resolves the mark for good.
|
|
1967
|
+
*/
|
|
1968
|
+
const EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS = 6e4;
|
|
1897
1969
|
/** A labeled, human-readable clock reading for error messages. */
|
|
1898
1970
|
function clockLabel(ms) {
|
|
1899
1971
|
return new Date(ms).toLocaleString();
|
|
@@ -1951,6 +2023,8 @@ var CommandCodeAccountPool = class {
|
|
|
1951
2023
|
deps;
|
|
1952
2024
|
/** Rotation state by API key. */
|
|
1953
2025
|
states = /* @__PURE__ */ new Map();
|
|
2026
|
+
/** Last explicit-revival probe attempt by API key (throttles a failing probe). */
|
|
2027
|
+
explicitProbes = /* @__PURE__ */ new Map();
|
|
1954
2028
|
constructor(deps) {
|
|
1955
2029
|
this.deps = deps;
|
|
1956
2030
|
}
|
|
@@ -2005,22 +2079,40 @@ var CommandCodeAccountPool = class {
|
|
|
2005
2079
|
* `options.model` is the request's model id; routing rules re-read per
|
|
2006
2080
|
* resolution, so a settings change applies live.
|
|
2007
2081
|
*
|
|
2008
|
-
* `options.
|
|
2009
|
-
*
|
|
2010
|
-
*
|
|
2011
|
-
*
|
|
2082
|
+
* `options.tried` lists the keys this request has already used — the
|
|
2083
|
+
* just-rejected one included. They are removed from the resolution entirely,
|
|
2084
|
+
* which is what lets one request walk a four-account pool: an account-scoped
|
|
2085
|
+
* rejection that does not mark the key (no credits, a model outside the
|
|
2086
|
+
* account's plan) would otherwise be offered again on every attempt, and the
|
|
2087
|
+
* accounts behind it would never be reached.
|
|
2088
|
+
*
|
|
2089
|
+
* An explicit selection (the pin or a model rule) that a rate-limit mark
|
|
2090
|
+
* would demote is probed before the fallback serves, so "falls back while
|
|
2091
|
+
* exhausted" never becomes "stays demoted until the process restarts".
|
|
2012
2092
|
*/
|
|
2013
2093
|
async resolveKey(options) {
|
|
2014
2094
|
const accounts = await this.resolvedAccounts();
|
|
2015
2095
|
if (accounts.length === 0) return;
|
|
2016
|
-
const
|
|
2017
|
-
|
|
2096
|
+
const tried = options?.tried;
|
|
2097
|
+
const available = tried === void 0 || tried.length === 0 ? accounts : accounts.filter((account) => !tried.includes(account.key));
|
|
2018
2098
|
const preferred = this.deps.preferredId?.();
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2099
|
+
if (available.length === 0) {
|
|
2100
|
+
if (selectActiveAccount(accounts, preferred) !== void 0) return void 0;
|
|
2101
|
+
throw allAccountsUnusable(accounts);
|
|
2102
|
+
}
|
|
2103
|
+
const routed = selectAccountForModel(available, options?.model ?? "", this.deps.modelAccountRules?.());
|
|
2104
|
+
if (routed !== void 0) return this.pick(routed);
|
|
2105
|
+
const chosen = selectActiveAccount(available, preferred);
|
|
2106
|
+
if (chosen !== void 0) {
|
|
2107
|
+
const explicit = this.explicitAccount(available, options?.model ?? "", preferred);
|
|
2108
|
+
if (explicit !== void 0 && explicit.key !== chosen.key && this.canProbeExplicit(explicit)) {
|
|
2109
|
+
const revived = await this.probeExplicit(explicit);
|
|
2110
|
+
if (revived !== void 0) return this.pick(revived);
|
|
2111
|
+
}
|
|
2112
|
+
return this.pick(chosen);
|
|
2113
|
+
}
|
|
2114
|
+
await Promise.all(available.map(async (account) => {
|
|
2022
2115
|
if (account.state?.kind === "disabled") return;
|
|
2023
|
-
if (options?.exclude !== void 0 && account.key === options.exclude) return;
|
|
2024
2116
|
let probe;
|
|
2025
2117
|
try {
|
|
2026
2118
|
probe = await this.deps.probeWindow(account.key);
|
|
@@ -2028,39 +2120,71 @@ var CommandCodeAccountPool = class {
|
|
|
2028
2120
|
return;
|
|
2029
2121
|
}
|
|
2030
2122
|
if (probe === void 0) return;
|
|
2031
|
-
if (!probe.exceeded)
|
|
2032
|
-
|
|
2123
|
+
if (!probe.exceeded) {
|
|
2124
|
+
this.states.delete(account.key);
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
if (probe.resetAt > 0) this.states.set(account.key, {
|
|
2033
2128
|
kind: "cooldown",
|
|
2034
|
-
|
|
2129
|
+
cause: "window",
|
|
2130
|
+
reason: account.state?.reason ?? "usage window exhausted (429)",
|
|
2035
2131
|
until: probe.resetAt
|
|
2036
2132
|
});
|
|
2133
|
+
else if (account.state?.kind !== "cooldown") this.states.set(account.key, {
|
|
2134
|
+
kind: "unknown",
|
|
2135
|
+
cause: "window",
|
|
2136
|
+
reason: account.state?.reason ?? "usage window exhausted (429)",
|
|
2137
|
+
until: 0
|
|
2138
|
+
});
|
|
2037
2139
|
}));
|
|
2038
|
-
const
|
|
2039
|
-
const revived = selectActiveAccount(
|
|
2140
|
+
const latestAccounts = await this.resolvedAccounts();
|
|
2141
|
+
const revived = selectActiveAccount(tried === void 0 || tried.length === 0 ? latestAccounts : latestAccounts.filter((account) => !tried.includes(account.key)), preferred);
|
|
2040
2142
|
if (revived !== void 0) return this.pick(revived);
|
|
2041
|
-
if (
|
|
2042
|
-
|
|
2043
|
-
const earliest = resets.length > 0 ? Math.min(...resets) : 0;
|
|
2044
|
-
const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
|
|
2045
|
-
throw new LlmError(`llm-commandcode: all ${latest.length} Command Code account(s) have exhausted their usage window` + (earliest > 0 ? `; the earliest window resets at ${clockLabel(earliest)}` : "") + ` — requests will succeed again after the reset (or add another account);已用尽全部 ${latest.length} 个 Command Code 账户的用量窗口` + (earliest > 0 ? `,最早的重置时间为 ${clockLabel(earliest)}` : "") + "——窗口重置后请求会自动恢复(也可以添加更多账户)", "RATE_LIMIT", wait > 0 && wait <= 9e5 ? { providerRetryAfterMs: wait } : void 0);
|
|
2143
|
+
if (selectActiveAccount(latestAccounts, preferred) !== void 0) return void 0;
|
|
2144
|
+
throw allAccountsUnusable(latestAccounts.length > 0 ? latestAccounts : accounts);
|
|
2046
2145
|
}
|
|
2047
2146
|
/**
|
|
2048
|
-
* Record a rejection against one key. `rate-limit`
|
|
2049
|
-
* exhausted
|
|
2050
|
-
*
|
|
2051
|
-
*
|
|
2147
|
+
* Record a rejection against one key. `rate-limit` marks the key's usage
|
|
2148
|
+
* WINDOW exhausted — as a `cooldown` until `resetAtMs` when the rejection
|
|
2149
|
+
* body named the provider's own reset time, otherwise as an `unknown` mark
|
|
2150
|
+
* whose window is probed lazily. `throttled` (the plain 429 that named no
|
|
2151
|
+
* window) marks the key with the `throttle` cause instead: it leaves rotation
|
|
2152
|
+
* exactly like a window mark does, but the pool's own diagnosis keeps saying
|
|
2153
|
+
* "rate limited" rather than inventing an exhausted window. A 401
|
|
2154
|
+
* (`invalid-credential`) disables the key until the stored credential
|
|
2155
|
+
* changes.
|
|
2156
|
+
*
|
|
2157
|
+
* `resetAtMs` is seconds-to-millis converted by the adapter from the
|
|
2158
|
+
* provider's `error.rateLimit.reset`: knowing the real reset immediately is
|
|
2159
|
+
* what keeps an exhausted account out of rotation for exactly as long as the
|
|
2160
|
+
* provider said. It only applies to a window mark; a plain throttle keeps the
|
|
2161
|
+
* window unknown on purpose, so a probe can still find out.
|
|
2052
2162
|
*/
|
|
2053
|
-
markRejected(apiKey, rejection) {
|
|
2163
|
+
markRejected(apiKey, rejection, resetAtMs) {
|
|
2054
2164
|
if (rejection === "invalid-credential") this.states.set(apiKey, {
|
|
2055
2165
|
kind: "disabled",
|
|
2166
|
+
cause: "auth",
|
|
2056
2167
|
reason: "invalid API key (401)",
|
|
2057
2168
|
until: 0
|
|
2058
2169
|
});
|
|
2059
|
-
else this.states.set(apiKey, {
|
|
2170
|
+
else if (rejection === "throttled") this.states.set(apiKey, {
|
|
2060
2171
|
kind: "unknown",
|
|
2172
|
+
cause: "throttle",
|
|
2061
2173
|
reason: "rate limited (429)",
|
|
2062
2174
|
until: 0
|
|
2063
2175
|
});
|
|
2176
|
+
else if (resetAtMs !== void 0 && resetAtMs > Date.now()) this.states.set(apiKey, {
|
|
2177
|
+
kind: "cooldown",
|
|
2178
|
+
cause: "window",
|
|
2179
|
+
reason: "usage window exhausted (429)",
|
|
2180
|
+
until: resetAtMs
|
|
2181
|
+
});
|
|
2182
|
+
else this.states.set(apiKey, {
|
|
2183
|
+
kind: "unknown",
|
|
2184
|
+
cause: "window",
|
|
2185
|
+
reason: "usage window exhausted (429)",
|
|
2186
|
+
until: 0
|
|
2187
|
+
});
|
|
2064
2188
|
}
|
|
2065
2189
|
/**
|
|
2066
2190
|
* One account's key: literal → credential seam → auth file (default slot).
|
|
@@ -2096,6 +2220,74 @@ var CommandCodeAccountPool = class {
|
|
|
2096
2220
|
}
|
|
2097
2221
|
}
|
|
2098
2222
|
}
|
|
2223
|
+
/**
|
|
2224
|
+
* The account the user explicitly asked for: the slot a model rule routes
|
|
2225
|
+
* the request to when that id exists among the resolved slots, else the
|
|
2226
|
+
* manually pinned slot. Consulted only on the fallback path — a usable
|
|
2227
|
+
* routed account already returned above — so unlike
|
|
2228
|
+
* {@link selectAccountForModel} it does NOT require the account to be
|
|
2229
|
+
* usable, which is exactly what {@link resolveKey} re-probes.
|
|
2230
|
+
*/
|
|
2231
|
+
explicitAccount(accounts, model, preferredId) {
|
|
2232
|
+
const rule = matchModelRule(model, this.deps.modelAccountRules?.());
|
|
2233
|
+
const id = rule !== void 0 && accounts.some((account) => account.slot.id === rule.account) ? rule.account : preferredId;
|
|
2234
|
+
return id === void 0 ? void 0 : accounts.find((account) => account.slot.id === id);
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* Whether an explicitly selected account's mark is due for a window probe.
|
|
2238
|
+
* Only an `unknown` mark (a 429 whose reset was never learned) is worth
|
|
2239
|
+
* re-probing: a `cooldown` already carries its reset time and expires by
|
|
2240
|
+
* itself, and a `disabled` (401) key stays out until the stored credential
|
|
2241
|
+
* changes. The interval bounds a probe endpoint that keeps failing.
|
|
2242
|
+
*/
|
|
2243
|
+
canProbeExplicit(account) {
|
|
2244
|
+
if (account.state?.kind !== "unknown") return false;
|
|
2245
|
+
const last = this.explicitProbes.get(account.key);
|
|
2246
|
+
return last === void 0 || this.now() - last >= EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS;
|
|
2247
|
+
}
|
|
2248
|
+
/** The clock the probe throttle reads; injected so a test can travel in time. */
|
|
2249
|
+
now() {
|
|
2250
|
+
return this.deps.now?.() ?? Date.now();
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Probe one explicitly selected account's window and apply the answer. A
|
|
2254
|
+
* window that is no longer exceeded drops the mark, so the user's own
|
|
2255
|
+
* selection serves again on this very request. An exceeded one is stamped
|
|
2256
|
+
* as a cooldown carrying the provider's reset time, after which
|
|
2257
|
+
* {@link accountUsable} lets the account back in with no further probe. A
|
|
2258
|
+
* probe that fails changes nothing: the mark stays `unknown` and the next
|
|
2259
|
+
* attempt waits out the interval.
|
|
2260
|
+
*/
|
|
2261
|
+
async probeExplicit(account) {
|
|
2262
|
+
this.explicitProbes.set(account.key, this.now());
|
|
2263
|
+
let probe;
|
|
2264
|
+
try {
|
|
2265
|
+
probe = await this.deps.probeWindow(account.key);
|
|
2266
|
+
} catch {
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
if (probe === void 0) return void 0;
|
|
2270
|
+
if (!probe.exceeded) {
|
|
2271
|
+
this.states.delete(account.key);
|
|
2272
|
+
return {
|
|
2273
|
+
slot: account.slot,
|
|
2274
|
+
key: account.key,
|
|
2275
|
+
state: void 0
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
if (probe.resetAt > 0) this.states.set(account.key, {
|
|
2279
|
+
kind: "cooldown",
|
|
2280
|
+
cause: "window",
|
|
2281
|
+
reason: account.state?.reason ?? "usage window exhausted (429)",
|
|
2282
|
+
until: probe.resetAt
|
|
2283
|
+
});
|
|
2284
|
+
else if (account.state?.kind !== "cooldown") this.states.set(account.key, {
|
|
2285
|
+
kind: "unknown",
|
|
2286
|
+
cause: "window",
|
|
2287
|
+
reason: account.state?.reason ?? "usage window exhausted (429)",
|
|
2288
|
+
until: 0
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2099
2291
|
/** Hand out the chosen account's key. */
|
|
2100
2292
|
pick(account) {
|
|
2101
2293
|
return {
|
|
@@ -2105,6 +2297,41 @@ var CommandCodeAccountPool = class {
|
|
|
2105
2297
|
}
|
|
2106
2298
|
};
|
|
2107
2299
|
/**
|
|
2300
|
+
* The error for "accounts exist but none of them can serve". Three shapes, each
|
|
2301
|
+
* claiming only what the pool actually knows:
|
|
2302
|
+
*
|
|
2303
|
+
* - every account rejected with 401 → `INVALID_CREDENTIAL`;
|
|
2304
|
+
* - at least one account marked by a NAMED usage window (or by a probe that
|
|
2305
|
+
* read a window as exceeded) → the window diagnosis, naming the earliest
|
|
2306
|
+
* known reset when the provider published one;
|
|
2307
|
+
* - every mark a plain throttle (a 429 that said nothing about a window, and
|
|
2308
|
+
* no probe could confirm one) → a throttle diagnosis that says so. Claiming
|
|
2309
|
+
* "all accounts have exhausted their usage window" here is issue #54: the
|
|
2310
|
+
* report's account panel showed the five-hour window at 2% and the weekly
|
|
2311
|
+
* one at 10% while the turn failed with exactly that sentence.
|
|
2312
|
+
*
|
|
2313
|
+
* Shared by the all-marked tail of {@link CommandCodeAccountPool.resolveKey}
|
|
2314
|
+
* and by its already-tried diagnosis, so the two can never drift apart.
|
|
2315
|
+
*
|
|
2316
|
+
* The attached `providerRetryAfterMs` is the exact wait until the earliest
|
|
2317
|
+
* known window reset, so dsh-llm-retry sleeps through the window instead of
|
|
2318
|
+
* polling at its backoff cadence. It is capped at {@link RETRY_MAX_DELAY_MS}:
|
|
2319
|
+
* the executor honors a provider wait verbatim only at or below the policy's
|
|
2320
|
+
* `maxDelayMs` — a LONGER attached wait makes it abandon the retry entirely
|
|
2321
|
+
* (normal mode), which would turn "poll until the window opens" into "fail
|
|
2322
|
+
* now". Longer resets simply ride the capped local backoff and the probe
|
|
2323
|
+
* revival. A throttle diagnosis attaches none: its marks carry no reset.
|
|
2324
|
+
*/
|
|
2325
|
+
function allAccountsUnusable(accounts) {
|
|
2326
|
+
if (accounts.filter((account) => account.state?.kind === "disabled").length === accounts.length) return new LlmError(`llm-commandcode: every configured Command Code account (${accounts.length}) was rejected with 401 — check the stored API keys (Models page / settings) or the auth file;已配置的 ${accounts.length} 个 Command Code 账户密钥均被拒绝(401)——请在设置页检查存储的 API 密钥,或重新运行 command-code login`, "INVALID_CREDENTIAL");
|
|
2327
|
+
const windowMarked = accounts.map((account) => account.state).filter((state) => state !== void 0).filter((state) => state.cause === "window");
|
|
2328
|
+
if (windowMarked.length === 0) return new LlmError(`llm-commandcode: all ${accounts.length} Command Code account(s) are rate limited (429) — the provider did not report an exhausted usage window; retrying;全部 ${accounts.length} 个 Command Code 账户被限流(429)——服务商未报告用量窗口用尽,正在重试`, "RATE_LIMIT");
|
|
2329
|
+
const resets = windowMarked.filter((state) => state.kind === "cooldown" && state.until > 0).map((state) => state.until);
|
|
2330
|
+
const earliest = resets.length > 0 ? Math.min(...resets) : 0;
|
|
2331
|
+
const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
|
|
2332
|
+
return new LlmError(`llm-commandcode: all ${accounts.length} Command Code account(s) have exhausted their usage window` + (earliest > 0 ? `; the earliest window resets at ${clockLabel(earliest)}` : " — the provider published no reset time for it") + ` — requests will succeed again after the reset (or add another account);已用尽全部 ${accounts.length} 个 Command Code 账户的用量窗口` + (earliest > 0 ? `,最早的重置时间为 ${clockLabel(earliest)}` : ",服务商未公布重置时间") + "——窗口重置后请求会自动恢复(也可以添加更多账户)", "RATE_LIMIT", wait > 0 && wait <= 9e5 ? { providerRetryAfterMs: wait } : void 0);
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2108
2335
|
* Normalize one resolved credential to the form every consumer sees. Trim
|
|
2109
2336
|
* only: a blank-after-trim value means "no key" (the slot is omitted and the
|
|
2110
2337
|
* caller reports `MISSING_CREDENTIAL`), while characters an HTTP header cannot
|
|
@@ -2353,7 +2580,7 @@ function scaleBy(size, factor) {
|
|
|
2353
2580
|
* and API key or subscription, and Command Code's terms apply.
|
|
2354
2581
|
*
|
|
2355
2582
|
* Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.4;
|
|
2356
|
-
* re-verified against command-code@1.
|
|
2583
|
+
* re-verified against command-code@1.58.0 — endpoints, request shape, and
|
|
2357
2584
|
* stream events unchanged):
|
|
2358
2585
|
* POST {apiBase}/alpha/generate
|
|
2359
2586
|
* body: { config, memory, taste, skills, params: { model, messages, tools,
|
|
@@ -2366,7 +2593,7 @@ function scaleBy(size, factor) {
|
|
|
2366
2593
|
* per-request options thunk and an API-key resolver from the plugin entry
|
|
2367
2594
|
* (src/index.ts), so a settings change reaches the very next request.
|
|
2368
2595
|
*/
|
|
2369
|
-
const COMMAND_CODE_CLI_VERSION = "1.
|
|
2596
|
+
const COMMAND_CODE_CLI_VERSION = "1.58.0";
|
|
2370
2597
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
2371
2598
|
const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
|
|
2372
2599
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
@@ -2404,15 +2631,22 @@ function booleanValue(value) {
|
|
|
2404
2631
|
return typeof value === "boolean" ? value : void 0;
|
|
2405
2632
|
}
|
|
2406
2633
|
/**
|
|
2407
|
-
* Thinking text carried by an OpenRouter-shaped `reasoning_details` array
|
|
2408
|
-
*
|
|
2409
|
-
*
|
|
2634
|
+
* Thinking text carried by an OpenRouter-shaped `reasoning_details` array,
|
|
2635
|
+
* concatenated in the order the gateway sent it.
|
|
2636
|
+
*
|
|
2637
|
+
* Entries are read by their own text members rather than filtered on `type`:
|
|
2638
|
+
* DeepSeek sends `{ type: 'reasoning.text', text }` while the OpenAI family
|
|
2639
|
+
* sends `{ type: 'reasoning.summary', summary, format: 'openai-responses-v1' }`
|
|
2640
|
+
* — the same summary text the scalar `delta.reasoning` carries, in the
|
|
2641
|
+
* Responses protocol's vocabulary. A `type` filter would drop one family or the
|
|
2642
|
+
* other, and losing thinking is the failure this exists to prevent.
|
|
2643
|
+
*
|
|
2644
|
+
* `text` wins per entry, but an EMPTY `text` must not shadow a populated
|
|
2645
|
+
* `summary` (the Responses wire emits `text: ''` placeholders), so the fallback
|
|
2646
|
+
* tests for non-empty rather than merely present. Entries carrying neither
|
|
2647
|
+
* member — the encrypted blobs — contribute nothing, and no entry contributes
|
|
2648
|
+
* both, so a summary is never counted twice.
|
|
2410
2649
|
*
|
|
2411
|
-
* Entries are read by their `text` member alone rather than filtered on
|
|
2412
|
-
* `type === 'reasoning.text'`: the sibling shapes in that protocol carry
|
|
2413
|
-
* `summary` or an encrypted blob instead of `text`, so a plain read already
|
|
2414
|
-
* ignores them, while a type filter would silently drop a text-bearing variant
|
|
2415
|
-
* added later — and losing thinking is the failure this exists to prevent.
|
|
2416
2650
|
* Returns undefined when the array yields nothing, so the caller's `??` chain
|
|
2417
2651
|
* keeps falling through instead of treating "no text" as a reasoning delta.
|
|
2418
2652
|
*/
|
|
@@ -2421,10 +2655,24 @@ function reasoningDetailsText(value) {
|
|
|
2421
2655
|
let text = "";
|
|
2422
2656
|
for (const entry of value) {
|
|
2423
2657
|
if (!isRecord(entry)) continue;
|
|
2424
|
-
|
|
2658
|
+
const exact = stringValue(entry.text);
|
|
2659
|
+
const summary = stringValue(entry.summary);
|
|
2660
|
+
text += exact !== void 0 && exact !== "" ? exact : summary ?? "";
|
|
2425
2661
|
}
|
|
2426
2662
|
return text === "" ? void 0 : text;
|
|
2427
2663
|
}
|
|
2664
|
+
/**
|
|
2665
|
+
* One SCALAR thinking delta, treating an empty string as "this chunk carries
|
|
2666
|
+
* none" — the same rule {@link reasoningDetailsText} applies inside the array.
|
|
2667
|
+
* The scalar and the array describe the same thinking, so an empty spelling of
|
|
2668
|
+
* one must not shadow populated text in the other (the Responses wire pads with
|
|
2669
|
+
* `text: ''`). `??` alone cannot express this: an empty string is not nullish,
|
|
2670
|
+
* so `reasoning: ''` beside a populated `reasoning_details` used to drop the
|
|
2671
|
+
* whole chunk's thinking.
|
|
2672
|
+
*/
|
|
2673
|
+
function nonEmptyText(value) {
|
|
2674
|
+
return value === void 0 || value === "" ? void 0 : value;
|
|
2675
|
+
}
|
|
2428
2676
|
/** Parse a billing-period timestamp (ISO string or millis) into millis; 0 when absent/invalid. */
|
|
2429
2677
|
function periodEndValue(value) {
|
|
2430
2678
|
const asNumber = numberValue(value);
|
|
@@ -3674,11 +3922,11 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
3674
3922
|
};
|
|
3675
3923
|
};
|
|
3676
3924
|
if (opts?.unfiltered === true) return catalog.map(toInfo).sort(compareByPlan);
|
|
3677
|
-
const
|
|
3925
|
+
const accesses = this.deps.options().filterModelsByPlan === false ? void 0 : await this.loadPoolBillingAccess();
|
|
3678
3926
|
const visible = this.deps.options().visibleModels;
|
|
3679
3927
|
const allow = Array.isArray(visible) && visible.length > 0 ? new Set(visible.filter((id) => typeof id === "string" && id !== "")) : void 0;
|
|
3680
3928
|
const overrides = this.deps.options().modelVisibility;
|
|
3681
|
-
return catalog.filter((model) =>
|
|
3929
|
+
return catalog.filter((model) => modelVisibleForAnyAccount(model.id, accesses)).filter((model) => {
|
|
3682
3930
|
const override = overrides?.[model.id];
|
|
3683
3931
|
if (typeof override === "boolean") return override;
|
|
3684
3932
|
return allow === void 0 || allow.has(model.id);
|
|
@@ -3734,17 +3982,44 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
3734
3982
|
};
|
|
3735
3983
|
}
|
|
3736
3984
|
/**
|
|
3737
|
-
*
|
|
3738
|
-
*
|
|
3739
|
-
*
|
|
3985
|
+
* Every account's billing facts behind the picker's plan filter, in the
|
|
3986
|
+
* pool's rotation order and cached per key for {@link BILLING_ACCESS_TTL_MS}
|
|
3987
|
+
* (so the extra accounts cost their three requests once per TTL, not once
|
|
3988
|
+
* per picker load). `undefined` means the filter cannot be evaluated at all
|
|
3989
|
+
* — no key resolved — which {@link modelVisibleForAnyAccount} reads as
|
|
3990
|
+
* "show everything".
|
|
3991
|
+
*
|
|
3992
|
+
* The serving account is only the first entry: with several accounts the
|
|
3993
|
+
* model list must not depend on which of them rotation happens to be using.
|
|
3740
3994
|
*/
|
|
3741
|
-
async
|
|
3742
|
-
|
|
3995
|
+
async loadPoolBillingAccess() {
|
|
3996
|
+
const keys = await this.poolAccountKeys();
|
|
3997
|
+
if (keys.length === 0) return void 0;
|
|
3998
|
+
return Promise.all(keys.map((key) => this.loadBillingAccessForKey(key)));
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* The API keys the picker's plan filter must consult: every account the host
|
|
4002
|
+
* can serve from, in rotation order, when the host exposes one. A host
|
|
4003
|
+
* without a pool (or without the seam) reports just the key that would serve
|
|
4004
|
+
* this request — exactly the pre-pool behaviour.
|
|
4005
|
+
*/
|
|
4006
|
+
async poolAccountKeys() {
|
|
4007
|
+
try {
|
|
4008
|
+
const usable = (await this.deps.resolveAccountKeys?.() ?? []).filter((key) => typeof key === "string" && key !== "");
|
|
4009
|
+
if (usable.length > 0) return usable;
|
|
4010
|
+
} catch {}
|
|
3743
4011
|
try {
|
|
3744
|
-
|
|
4012
|
+
return [await this.deps.resolveApiKey(this.deps.options())];
|
|
3745
4013
|
} catch {
|
|
3746
|
-
return;
|
|
4014
|
+
return [];
|
|
3747
4015
|
}
|
|
4016
|
+
}
|
|
4017
|
+
/**
|
|
4018
|
+
* One account's billing facts, cached for {@link BILLING_ACCESS_TTL_MS} and
|
|
4019
|
+
* shared across concurrent callers. `undefined` means "unknown — show
|
|
4020
|
+
* everything" (fail-open).
|
|
4021
|
+
*/
|
|
4022
|
+
async loadBillingAccessForKey(apiKey) {
|
|
3748
4023
|
const cached = this.billingAccess.get(apiKey);
|
|
3749
4024
|
if (cached !== void 0 && Date.now() - cached.at < 3e5) return cached.value;
|
|
3750
4025
|
const existing = this.billingAccessInflight.get(apiKey);
|
|
@@ -3906,14 +4181,29 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
3906
4181
|
return report;
|
|
3907
4182
|
}
|
|
3908
4183
|
/**
|
|
3909
|
-
* Probe one account's
|
|
4184
|
+
* Probe one account's real usage windows from `/alpha/billing/credits`. The
|
|
3910
4185
|
* multi-account pool calls this when every account is marked exhausted: an
|
|
3911
|
-
* account whose
|
|
3912
|
-
* `resetAt` values feed the "earliest reset" error message.
|
|
3913
|
-
*
|
|
3914
|
-
*
|
|
4186
|
+
* account whose windows no longer report `exceeded` is revived, and the
|
|
4187
|
+
* `resetAt` values feed the "earliest reset" error message.
|
|
4188
|
+
*
|
|
4189
|
+
* BOTH windows the endpoint publishes are read, not just `fiveHour`. An
|
|
4190
|
+
* account can have an open five-hour window and an exhausted WEEKLY quota at
|
|
4191
|
+
* the same time (the two are metered separately), and reading only the
|
|
4192
|
+
* shorter one revived such an account as "usable" — the pool then handed it
|
|
4193
|
+
* out, the provider rejected the request again, and the next all-marked pass
|
|
4194
|
+
* revived it once more, so a used-up account kept coming back (issue #51's
|
|
4195
|
+
* follow-up: "切到一个不可用账号"). `exceeded` is therefore true when ANY
|
|
4196
|
+
* published window is exceeded, and `resetAt` is the LATEST reset among the
|
|
4197
|
+
* exceeded ones — the binding constraint, because a clear five-hour window
|
|
4198
|
+
* buys nothing while the weekly quota is spent.
|
|
4199
|
+
*
|
|
4200
|
+
* Returns `undefined` when the probe itself failed (transport, non-200, or a
|
|
4201
|
+
* payload with no window limits at all) — a failed probe never changes pool
|
|
4202
|
+
* state. The monthly/purchased/free credit balances are deliberately NOT part
|
|
4203
|
+
* of this answer: they are balances rather than windows, they carry no reset
|
|
4204
|
+
* time, and the server remains the final gate on them.
|
|
3915
4205
|
*/
|
|
3916
|
-
async
|
|
4206
|
+
async probeWindowLimits(apiKey) {
|
|
3917
4207
|
try {
|
|
3918
4208
|
const connection = this.deps.options();
|
|
3919
4209
|
const response = await this.fetchImpl(`${connection.apiBase}/alpha/billing/credits`, {
|
|
@@ -3924,11 +4214,22 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
3924
4214
|
const parsed = await response.json();
|
|
3925
4215
|
if (!isRecord(parsed)) return void 0;
|
|
3926
4216
|
const windowLimits = isRecord(parsed.windowLimits) ? parsed.windowLimits : void 0;
|
|
3927
|
-
|
|
3928
|
-
|
|
4217
|
+
if (windowLimits === void 0) return void 0;
|
|
4218
|
+
let reported = false;
|
|
4219
|
+
let exceeded = false;
|
|
4220
|
+
let resetAt = 0;
|
|
4221
|
+
for (const raw of [windowLimits.fiveHour, windowLimits.weekly]) {
|
|
4222
|
+
const window = isRecord(raw) ? parseWindowLimit(raw) : void 0;
|
|
4223
|
+
if (window === void 0) continue;
|
|
4224
|
+
reported = true;
|
|
4225
|
+
if (!window.exceeded) continue;
|
|
4226
|
+
exceeded = true;
|
|
4227
|
+
resetAt = Math.max(resetAt, window.resetAt);
|
|
4228
|
+
}
|
|
4229
|
+
if (!reported) return void 0;
|
|
3929
4230
|
return {
|
|
3930
|
-
exceeded
|
|
3931
|
-
resetAt:
|
|
4231
|
+
exceeded,
|
|
4232
|
+
resetAt: exceeded ? resetAt : 0
|
|
3932
4233
|
};
|
|
3933
4234
|
} catch {
|
|
3934
4235
|
return;
|
|
@@ -4002,13 +4303,24 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4002
4303
|
}
|
|
4003
4304
|
}
|
|
4004
4305
|
const rotate = this.deps.rotateApiKey;
|
|
4005
|
-
|
|
4006
|
-
|
|
4306
|
+
const rejection = classifyAccountRejection(attempt.status, attempt.errText);
|
|
4307
|
+
if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
|
|
4308
|
+
const next = await rotate(apiKey, rejection.reason, connection, options.model, {
|
|
4309
|
+
tried: [...tried],
|
|
4310
|
+
...rejection.resetAtMs !== void 0 && { resetAtMs: rejection.resetAtMs }
|
|
4311
|
+
});
|
|
4007
4312
|
if (next !== void 0 && !tried.has(next)) {
|
|
4008
4313
|
apiKey = next;
|
|
4009
4314
|
continue;
|
|
4010
4315
|
}
|
|
4011
4316
|
}
|
|
4317
|
+
if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled") && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
|
|
4318
|
+
const reset = rejection.resetAtMs;
|
|
4319
|
+
const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
|
|
4320
|
+
const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
|
|
4321
|
+
const window = rejection.reason === "rate-limit";
|
|
4322
|
+
throw new LlmError((window ? "llm-commandcode: the Command Code account is rate limited" : "llm-commandcode: the Command Code account is rate limited (429) — the provider did not report an exhausted usage window") + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`) + (window ? ";当前 Command Code 账户已被限流" : ";当前 Command Code 账户被限流(429),服务商未报告用量窗口用尽") + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), "RATE_LIMIT", wait > 0 ? { providerRetryAfterMs: wait } : void 0);
|
|
4323
|
+
}
|
|
4012
4324
|
throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
|
|
4013
4325
|
}
|
|
4014
4326
|
if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
|
|
@@ -4284,7 +4596,7 @@ function handleOpenAIEvent(asm, event) {
|
|
|
4284
4596
|
}
|
|
4285
4597
|
const choice = isRecord(choices[0]) ? choices[0] : {};
|
|
4286
4598
|
const delta = isRecord(choice.delta) ? choice.delta : {};
|
|
4287
|
-
const reasoningDelta = stringValue(delta.reasoning) ?? stringValue(delta.reasoning_content) ?? reasoningDetailsText(delta.reasoning_details) ?? "";
|
|
4599
|
+
const reasoningDelta = nonEmptyText(stringValue(delta.reasoning)) ?? nonEmptyText(stringValue(delta.reasoning_content)) ?? reasoningDetailsText(delta.reasoning_details) ?? "";
|
|
4288
4600
|
if (reasoningDelta !== "") {
|
|
4289
4601
|
chunks.push(...closeText(asm));
|
|
4290
4602
|
if (asm.reasoningIndex < 0) {
|
|
@@ -4400,6 +4712,112 @@ function mapFinishReason(reason) {
|
|
|
4400
4712
|
* back to the CLI /alpha/generate transport; other 4xx/5xx must surface as
|
|
4401
4713
|
* ordinary errors so real account/model problems are not masked.
|
|
4402
4714
|
*/
|
|
4715
|
+
/**
|
|
4716
|
+
* Classify a pre-stream rejection that is about the ACCOUNT rather than about
|
|
4717
|
+
* the request, using the official CLI's own rules (command-code@1.56.0:
|
|
4718
|
+
* `parseWindowLimitError`, `isInsufficientCreditsRequestError`,
|
|
4719
|
+
* `parseSpendCapError` and the terminal-marker list
|
|
4720
|
+
* `["premium_credits_exhausted", "model_not_in_plan", "insufficient credits"]`).
|
|
4721
|
+
* Undefined means "not an account-scoped rejection": a malformed request, an
|
|
4722
|
+
* oversized body or a context overflow must fail fast instead of walking the
|
|
4723
|
+
* whole account pool.
|
|
4724
|
+
*
|
|
4725
|
+
* Issue #51's follow-up is why this exists. Only 429/401 used to rotate, so
|
|
4726
|
+
* every other account-scoped rejection was terminal AND invisible to the pool:
|
|
4727
|
+
* an account that could not pay, or could not run the model, stayed "usable",
|
|
4728
|
+
* was handed out again on the next request, and produced the same error again
|
|
4729
|
+
* ("切到一个不可用账号后就报 400,还挺频繁"). The three reasons differ in what
|
|
4730
|
+
* the host should do with them, which is the caller's decision, not this
|
|
4731
|
+
* function's:
|
|
4732
|
+
*
|
|
4733
|
+
* - `rate-limit`: a usage WINDOW is spent. The code `RATE_LIMITED` is the
|
|
4734
|
+
* authoritative signal — the CLI accepts the code OR a 429 status — and the
|
|
4735
|
+
* body's `error.rateLimit` names the window and its `reset` (in seconds),
|
|
4736
|
+
* so the host can hold the account out until the provider's own reset time.
|
|
4737
|
+
* - `throttled`: the provider refused the request (429 or a bare
|
|
4738
|
+
* `RATE_LIMITED` code) without naming a usage window. It is account-scoped
|
|
4739
|
+
* like the above, so it rotates and marks the key the same way, but it is
|
|
4740
|
+
* NOT evidence that a metered window is spent: the CLI's own
|
|
4741
|
+
* `parseWindowLimitError` returns a window limit only when
|
|
4742
|
+
* `resolveWindowLabel` can name one, and a burst/model/spend limiter the
|
|
4743
|
+
* billing endpoint cannot see is exactly this shape (issue #54's report: a
|
|
4744
|
+
* 429 whose probe read five-hour 2% and weekly 10% while the error claimed
|
|
4745
|
+
* both were exhausted).
|
|
4746
|
+
* - `invalid-credential`: 401; the key itself is bad.
|
|
4747
|
+
* - `unavailable`: the key is fine but this account cannot serve: no credits
|
|
4748
|
+
* (`400 Insufficient credits`, the codes `INSUFFICIENT_CREDITS`,
|
|
4749
|
+
* `USAGE_EXCEEDED`, `PREMIUM_CREDITS_EXHAUSTED`, or a spend-cap wording)
|
|
4750
|
+
* or a model outside its plan (`MODEL_NOT_IN_PLAN`). Rotating must NOT mark
|
|
4751
|
+
* the account: the fact is about the model or the balance, it can change
|
|
4752
|
+
* without the key changing, and a `:free` model can still be served by an
|
|
4753
|
+
* account with an empty balance.
|
|
4754
|
+
*/
|
|
4755
|
+
function classifyAccountRejection(status, errText) {
|
|
4756
|
+
if (status === 429 || readProviderErrorCode(errText)?.toUpperCase() === "RATE_LIMITED") {
|
|
4757
|
+
const evidence = readWindowLimitEvidence(errText);
|
|
4758
|
+
if (evidence.window === void 0) return { reason: "throttled" };
|
|
4759
|
+
return evidence.resetAtMs === void 0 ? { reason: "rate-limit" } : {
|
|
4760
|
+
reason: "rate-limit",
|
|
4761
|
+
resetAtMs: evidence.resetAtMs
|
|
4762
|
+
};
|
|
4763
|
+
}
|
|
4764
|
+
if (status === 401) return { reason: "invalid-credential" };
|
|
4765
|
+
if (status < 400 || status >= 500) return void 0;
|
|
4766
|
+
const lower = errText.toLowerCase().replaceAll("_", " ");
|
|
4767
|
+
const code = readProviderErrorCode(errText)?.toUpperCase();
|
|
4768
|
+
if (code === "USAGE_EXCEEDED" || code === "INSUFFICIENT_CREDITS" || code === "PREMIUM_CREDITS_EXHAUSTED" || code === "MODEL_NOT_IN_PLAN" || lower.includes("insufficient credits") || lower.includes("premium credits exhausted") || lower.includes("model not in plan") || /insufficient (credit|balance)/.test(lower) || /out of credit/.test(lower) || /not (available|included)[^.]{0,40}plan/.test(lower) || lower.includes("upgrade your plan")) return { reason: "unavailable" };
|
|
4769
|
+
}
|
|
4770
|
+
/** The machine-readable `error.code`/`error.type` of a pre-stream failure body. */
|
|
4771
|
+
function readProviderErrorCode(errText) {
|
|
4772
|
+
try {
|
|
4773
|
+
const parsed = JSON.parse(errText);
|
|
4774
|
+
if (!isRecord(parsed)) return void 0;
|
|
4775
|
+
const error = isRecord(parsed.error) ? parsed.error : parsed;
|
|
4776
|
+
return stringValue(error.code) ?? stringValue(error.type);
|
|
4777
|
+
} catch {
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
/**
|
|
4782
|
+
* The usage-window evidence carried by a rejected request, mirroring the CLI's
|
|
4783
|
+
* `parseWindowLimitError` → `resolveWindowLabel`/`extractResetAtMs` pair.
|
|
4784
|
+
*
|
|
4785
|
+
* `window` is present only when the body actually names one: a
|
|
4786
|
+
* `error.rateLimit.window` of `fiveHour`/`weekly`/`daily`, or the
|
|
4787
|
+
* "usage limit for your plan" wording (read as `weekly` when it says so, else
|
|
4788
|
+
* `fiveHour`). That is the CLI's own bar for calling a rejection a WINDOW
|
|
4789
|
+
* limit, and it is what keeps a bare 429 — a burst limiter, a model-level
|
|
4790
|
+
* limit, a spend cap `windowLimits` cannot show — from being reported as an
|
|
4791
|
+
* exhausted window (issue #54).
|
|
4792
|
+
*
|
|
4793
|
+
* `resetAtMs` is the reset the body publishes: `error.rateLimit.reset` is
|
|
4794
|
+
* SECONDS (`1e3 * reset`, like the CLI), and a `resets at <ISO>` stamp in the
|
|
4795
|
+
* message is honored as the fallback. Either can be present without the other;
|
|
4796
|
+
* the window label is what decides the classification.
|
|
4797
|
+
*/
|
|
4798
|
+
function readWindowLimitEvidence(errText) {
|
|
4799
|
+
let named;
|
|
4800
|
+
let message = "";
|
|
4801
|
+
let seconds;
|
|
4802
|
+
try {
|
|
4803
|
+
const parsed = JSON.parse(errText);
|
|
4804
|
+
if (isRecord(parsed)) {
|
|
4805
|
+
const error = isRecord(parsed.error) ? parsed.error : parsed;
|
|
4806
|
+
const rateLimit = isRecord(error.rateLimit) ? error.rateLimit : void 0;
|
|
4807
|
+
named = stringValue(rateLimit?.window);
|
|
4808
|
+
message = stringValue(error.message) ?? stringValue(parsed.message) ?? "";
|
|
4809
|
+
seconds = numberValue(rateLimit?.reset);
|
|
4810
|
+
}
|
|
4811
|
+
} catch {}
|
|
4812
|
+
const window = named === "fiveHour" || named === "weekly" || named === "daily" ? named : /usage limit for your plan/i.test(message) || /usage limit for your plan/i.test(errText) ? /weekly/i.test(message) ? "weekly" : "fiveHour" : void 0;
|
|
4813
|
+
const stamp = /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(message)?.[1] ?? /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(errText)?.[1];
|
|
4814
|
+
const stamped = stamp === void 0 ? void 0 : Date.parse(stamp);
|
|
4815
|
+
const resetAtMs = seconds !== void 0 && seconds > 0 ? Math.round(seconds * 1e3) : stamped !== void 0 && !Number.isNaN(stamped) ? stamped : void 0;
|
|
4816
|
+
return {
|
|
4817
|
+
...window === void 0 ? {} : { window },
|
|
4818
|
+
...resetAtMs === void 0 ? {} : { resetAtMs }
|
|
4819
|
+
};
|
|
4820
|
+
}
|
|
4403
4821
|
function isUpgradeRequiredError(status, errText) {
|
|
4404
4822
|
if (status !== 403) return false;
|
|
4405
4823
|
const lower = errText.toLowerCase();
|
|
@@ -6335,7 +6753,7 @@ function apply(ctx, config) {
|
|
|
6335
6753
|
slots,
|
|
6336
6754
|
resolveRef,
|
|
6337
6755
|
authFileKey: resolveAuthFileApiKey,
|
|
6338
|
-
probeWindow: (apiKey) => adapter.
|
|
6756
|
+
probeWindow: (apiKey) => adapter.probeWindowLimits(apiKey),
|
|
6339
6757
|
preferredId,
|
|
6340
6758
|
modelAccountRules: () => current().modelAccountRules ?? []
|
|
6341
6759
|
});
|
|
@@ -6348,10 +6766,11 @@ function apply(ctx, config) {
|
|
|
6348
6766
|
const adapter = new CommandCodeAdapter({
|
|
6349
6767
|
options,
|
|
6350
6768
|
resolveApiKey,
|
|
6351
|
-
rotateApiKey: async (rejectedKey, rejection, _connection, model) => {
|
|
6352
|
-
pool.markRejected(rejectedKey, rejection);
|
|
6353
|
-
const
|
|
6354
|
-
|
|
6769
|
+
rotateApiKey: async (rejectedKey, rejection, _connection, model, rotation) => {
|
|
6770
|
+
if (rejection !== "unavailable") pool.markRejected(rejectedKey, rejection, rotation?.resetAtMs);
|
|
6771
|
+
const tried = rotation?.tried?.length ? rotation.tried : [rejectedKey];
|
|
6772
|
+
const resolved = await pool.resolveKey(model === void 0 ? { tried } : {
|
|
6773
|
+
tried,
|
|
6355
6774
|
model
|
|
6356
6775
|
});
|
|
6357
6776
|
return resolved === void 0 ? void 0 : assertUsableApiKey(resolved.key, "llm-commandcode", resolved.slot.ref ?? `${resolved.slot.label} (config.apiKey)`);
|
|
@@ -6359,6 +6778,9 @@ function apply(ctx, config) {
|
|
|
6359
6778
|
resolveAttachments: () => {
|
|
6360
6779
|
const attachments = ctx.get("attachments");
|
|
6361
6780
|
return attachments === void 0 ? void 0 : attachments;
|
|
6781
|
+
},
|
|
6782
|
+
resolveAccountKeys: async () => {
|
|
6783
|
+
return (await pool.resolvedAccounts()).map((account) => account.key);
|
|
6362
6784
|
}
|
|
6363
6785
|
});
|
|
6364
6786
|
ctx.llm.registerConfigurableProviders([{
|