@mars-sea/dsh-commandcode-provider 0.11.5 → 0.11.6
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 +15 -0
- package/lib/client.js +1 -1
- package/lib/index.d.ts +171 -36
- package/lib/index.js +429 -73
- 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: [
|
|
@@ -1878,6 +1933,10 @@ function installCostProjection(ctx) {
|
|
|
1878
1933
|
* {@link CommandCodeAccountPoolDeps.probeWindow}): an account whose window
|
|
1879
1934
|
* no longer reports `exceeded` is revived, otherwise the pool throws a
|
|
1880
1935
|
* `RATE_LIMIT` error naming the earliest reset time.
|
|
1936
|
+
* - An account the user explicitly asked for — the manual pin
|
|
1937
|
+
* ({@link CommandCodeAccountPoolDeps.preferredId}) or a model rule's
|
|
1938
|
+
* routed account — is probed the same way when a mark would demote it, so
|
|
1939
|
+
* a fallback is never permanent (issue #51).
|
|
1881
1940
|
*
|
|
1882
1941
|
* The pool is deliberately cordis-free (like the adapter): every host fact
|
|
1883
1942
|
* arrives through injected thunks, so node tests can drive it directly.
|
|
@@ -1894,6 +1953,18 @@ function installCostProjection(ctx) {
|
|
|
1894
1953
|
* window opens" into "fail now".
|
|
1895
1954
|
*/
|
|
1896
1955
|
const RETRY_MAX_DELAY_MS = 9e5;
|
|
1956
|
+
/**
|
|
1957
|
+
* How long an explicitly selected account's rate-limit mark may stand before
|
|
1958
|
+
* its window is probed again. A 429 whose reset was never learned marks the
|
|
1959
|
+
* key `unknown`, which {@link accountUsable} refuses forever — deliberately,
|
|
1960
|
+
* because the all-marked revival pass is the only thing meant to clear it and
|
|
1961
|
+
* it costs one billing call per key. That is fine for a key nobody asked for,
|
|
1962
|
+
* and wrong for the one the user pinned: a single 429 would otherwise demote
|
|
1963
|
+
* the user's own selection until the process restarts (issue #51). The probe
|
|
1964
|
+
* is therefore allowed once per interval per key, which bounds an endpoint
|
|
1965
|
+
* that keeps failing while a successful probe resolves the mark for good.
|
|
1966
|
+
*/
|
|
1967
|
+
const EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS = 6e4;
|
|
1897
1968
|
/** A labeled, human-readable clock reading for error messages. */
|
|
1898
1969
|
function clockLabel(ms) {
|
|
1899
1970
|
return new Date(ms).toLocaleString();
|
|
@@ -1951,6 +2022,8 @@ var CommandCodeAccountPool = class {
|
|
|
1951
2022
|
deps;
|
|
1952
2023
|
/** Rotation state by API key. */
|
|
1953
2024
|
states = /* @__PURE__ */ new Map();
|
|
2025
|
+
/** Last explicit-revival probe attempt by API key (throttles a failing probe). */
|
|
2026
|
+
explicitProbes = /* @__PURE__ */ new Map();
|
|
1954
2027
|
constructor(deps) {
|
|
1955
2028
|
this.deps = deps;
|
|
1956
2029
|
}
|
|
@@ -2005,22 +2078,40 @@ var CommandCodeAccountPool = class {
|
|
|
2005
2078
|
* `options.model` is the request's model id; routing rules re-read per
|
|
2006
2079
|
* resolution, so a settings change applies live.
|
|
2007
2080
|
*
|
|
2008
|
-
* `options.
|
|
2009
|
-
*
|
|
2010
|
-
*
|
|
2011
|
-
*
|
|
2081
|
+
* `options.tried` lists the keys this request has already used — the
|
|
2082
|
+
* just-rejected one included. They are removed from the resolution entirely,
|
|
2083
|
+
* which is what lets one request walk a four-account pool: an account-scoped
|
|
2084
|
+
* rejection that does not mark the key (no credits, a model outside the
|
|
2085
|
+
* account's plan) would otherwise be offered again on every attempt, and the
|
|
2086
|
+
* accounts behind it would never be reached.
|
|
2087
|
+
*
|
|
2088
|
+
* An explicit selection (the pin or a model rule) that a rate-limit mark
|
|
2089
|
+
* would demote is probed before the fallback serves, so "falls back while
|
|
2090
|
+
* exhausted" never becomes "stays demoted until the process restarts".
|
|
2012
2091
|
*/
|
|
2013
2092
|
async resolveKey(options) {
|
|
2014
2093
|
const accounts = await this.resolvedAccounts();
|
|
2015
2094
|
if (accounts.length === 0) return;
|
|
2016
|
-
const
|
|
2017
|
-
|
|
2095
|
+
const tried = options?.tried;
|
|
2096
|
+
const available = tried === void 0 || tried.length === 0 ? accounts : accounts.filter((account) => !tried.includes(account.key));
|
|
2018
2097
|
const preferred = this.deps.preferredId?.();
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2098
|
+
if (available.length === 0) {
|
|
2099
|
+
if (selectActiveAccount(accounts, preferred) !== void 0) return void 0;
|
|
2100
|
+
throw allAccountsUnusable(accounts);
|
|
2101
|
+
}
|
|
2102
|
+
const routed = selectAccountForModel(available, options?.model ?? "", this.deps.modelAccountRules?.());
|
|
2103
|
+
if (routed !== void 0) return this.pick(routed);
|
|
2104
|
+
const chosen = selectActiveAccount(available, preferred);
|
|
2105
|
+
if (chosen !== void 0) {
|
|
2106
|
+
const explicit = this.explicitAccount(available, options?.model ?? "", preferred);
|
|
2107
|
+
if (explicit !== void 0 && explicit.key !== chosen.key && this.canProbeExplicit(explicit)) {
|
|
2108
|
+
const revived = await this.probeExplicit(explicit);
|
|
2109
|
+
if (revived !== void 0) return this.pick(revived);
|
|
2110
|
+
}
|
|
2111
|
+
return this.pick(chosen);
|
|
2112
|
+
}
|
|
2113
|
+
await Promise.all(available.map(async (account) => {
|
|
2022
2114
|
if (account.state?.kind === "disabled") return;
|
|
2023
|
-
if (options?.exclude !== void 0 && account.key === options.exclude) return;
|
|
2024
2115
|
let probe;
|
|
2025
2116
|
try {
|
|
2026
2117
|
probe = await this.deps.probeWindow(account.key);
|
|
@@ -2028,34 +2119,45 @@ var CommandCodeAccountPool = class {
|
|
|
2028
2119
|
return;
|
|
2029
2120
|
}
|
|
2030
2121
|
if (probe === void 0) return;
|
|
2031
|
-
if (!probe.exceeded)
|
|
2032
|
-
|
|
2122
|
+
if (!probe.exceeded) {
|
|
2123
|
+
this.states.delete(account.key);
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
if (probe.resetAt > 0) this.states.set(account.key, {
|
|
2033
2127
|
kind: "cooldown",
|
|
2034
2128
|
reason: account.state?.reason ?? "rate limited (429)",
|
|
2035
2129
|
until: probe.resetAt
|
|
2036
2130
|
});
|
|
2037
2131
|
}));
|
|
2038
|
-
const
|
|
2039
|
-
const revived = selectActiveAccount(
|
|
2132
|
+
const latestAccounts = await this.resolvedAccounts();
|
|
2133
|
+
const revived = selectActiveAccount(tried === void 0 || tried.length === 0 ? latestAccounts : latestAccounts.filter((account) => !tried.includes(account.key)), preferred);
|
|
2040
2134
|
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);
|
|
2135
|
+
if (selectActiveAccount(latestAccounts, preferred) !== void 0) return void 0;
|
|
2136
|
+
throw allAccountsUnusable(latestAccounts.length > 0 ? latestAccounts : accounts);
|
|
2046
2137
|
}
|
|
2047
2138
|
/**
|
|
2048
|
-
* Record a rejection against one key. `rate-limit`
|
|
2049
|
-
*
|
|
2050
|
-
*
|
|
2051
|
-
*
|
|
2139
|
+
* Record a rejection against one key. `rate-limit` marks the key exhausted
|
|
2140
|
+
* (`429`/`RATE_LIMITED`) — as a `cooldown` until `resetAtMs` when the
|
|
2141
|
+
* rejection body published the provider's own reset time, otherwise as an
|
|
2142
|
+
* `unknown` mark whose reset is probed lazily — and `invalid-credential`
|
|
2143
|
+
* (401) disables the key until the stored credential changes.
|
|
2144
|
+
*
|
|
2145
|
+
* `resetAtMs` is seconds-to-millis converted by the adapter from the
|
|
2146
|
+
* provider's `error.rateLimit.reset`: knowing the real reset immediately is
|
|
2147
|
+
* what keeps an exhausted account out of rotation for exactly as long as the
|
|
2148
|
+
* provider said, instead of until a probe happens to run.
|
|
2052
2149
|
*/
|
|
2053
|
-
markRejected(apiKey, rejection) {
|
|
2150
|
+
markRejected(apiKey, rejection, resetAtMs) {
|
|
2054
2151
|
if (rejection === "invalid-credential") this.states.set(apiKey, {
|
|
2055
2152
|
kind: "disabled",
|
|
2056
2153
|
reason: "invalid API key (401)",
|
|
2057
2154
|
until: 0
|
|
2058
2155
|
});
|
|
2156
|
+
else if (resetAtMs !== void 0 && resetAtMs > Date.now()) this.states.set(apiKey, {
|
|
2157
|
+
kind: "cooldown",
|
|
2158
|
+
reason: "rate limited (429)",
|
|
2159
|
+
until: resetAtMs
|
|
2160
|
+
});
|
|
2059
2161
|
else this.states.set(apiKey, {
|
|
2060
2162
|
kind: "unknown",
|
|
2061
2163
|
reason: "rate limited (429)",
|
|
@@ -2096,6 +2198,67 @@ var CommandCodeAccountPool = class {
|
|
|
2096
2198
|
}
|
|
2097
2199
|
}
|
|
2098
2200
|
}
|
|
2201
|
+
/**
|
|
2202
|
+
* The account the user explicitly asked for: the slot a model rule routes
|
|
2203
|
+
* the request to when that id exists among the resolved slots, else the
|
|
2204
|
+
* manually pinned slot. Consulted only on the fallback path — a usable
|
|
2205
|
+
* routed account already returned above — so unlike
|
|
2206
|
+
* {@link selectAccountForModel} it does NOT require the account to be
|
|
2207
|
+
* usable, which is exactly what {@link resolveKey} re-probes.
|
|
2208
|
+
*/
|
|
2209
|
+
explicitAccount(accounts, model, preferredId) {
|
|
2210
|
+
const rule = matchModelRule(model, this.deps.modelAccountRules?.());
|
|
2211
|
+
const id = rule !== void 0 && accounts.some((account) => account.slot.id === rule.account) ? rule.account : preferredId;
|
|
2212
|
+
return id === void 0 ? void 0 : accounts.find((account) => account.slot.id === id);
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Whether an explicitly selected account's mark is due for a window probe.
|
|
2216
|
+
* Only an `unknown` mark (a 429 whose reset was never learned) is worth
|
|
2217
|
+
* re-probing: a `cooldown` already carries its reset time and expires by
|
|
2218
|
+
* itself, and a `disabled` (401) key stays out until the stored credential
|
|
2219
|
+
* changes. The interval bounds a probe endpoint that keeps failing.
|
|
2220
|
+
*/
|
|
2221
|
+
canProbeExplicit(account) {
|
|
2222
|
+
if (account.state?.kind !== "unknown") return false;
|
|
2223
|
+
const last = this.explicitProbes.get(account.key);
|
|
2224
|
+
return last === void 0 || this.now() - last >= EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS;
|
|
2225
|
+
}
|
|
2226
|
+
/** The clock the probe throttle reads; injected so a test can travel in time. */
|
|
2227
|
+
now() {
|
|
2228
|
+
return this.deps.now?.() ?? Date.now();
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Probe one explicitly selected account's window and apply the answer. A
|
|
2232
|
+
* window that is no longer exceeded drops the mark, so the user's own
|
|
2233
|
+
* selection serves again on this very request. An exceeded one is stamped
|
|
2234
|
+
* as a cooldown carrying the provider's reset time, after which
|
|
2235
|
+
* {@link accountUsable} lets the account back in with no further probe. A
|
|
2236
|
+
* probe that fails changes nothing: the mark stays `unknown` and the next
|
|
2237
|
+
* attempt waits out the interval.
|
|
2238
|
+
*/
|
|
2239
|
+
async probeExplicit(account) {
|
|
2240
|
+
this.explicitProbes.set(account.key, this.now());
|
|
2241
|
+
let probe;
|
|
2242
|
+
try {
|
|
2243
|
+
probe = await this.deps.probeWindow(account.key);
|
|
2244
|
+
} catch {
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
if (probe === void 0) return void 0;
|
|
2248
|
+
if (!probe.exceeded) {
|
|
2249
|
+
this.states.delete(account.key);
|
|
2250
|
+
return {
|
|
2251
|
+
slot: account.slot,
|
|
2252
|
+
key: account.key,
|
|
2253
|
+
state: void 0
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
if (probe.resetAt > 0) this.states.set(account.key, {
|
|
2257
|
+
kind: "cooldown",
|
|
2258
|
+
reason: account.state?.reason ?? "rate limited (429)",
|
|
2259
|
+
until: probe.resetAt
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2099
2262
|
/** Hand out the chosen account's key. */
|
|
2100
2263
|
pick(account) {
|
|
2101
2264
|
return {
|
|
@@ -2105,6 +2268,28 @@ var CommandCodeAccountPool = class {
|
|
|
2105
2268
|
}
|
|
2106
2269
|
};
|
|
2107
2270
|
/**
|
|
2271
|
+
* The error for "accounts exist but none of them can serve": all-401 becomes
|
|
2272
|
+
* `INVALID_CREDENTIAL`, anything else `RATE_LIMIT` naming the earliest known
|
|
2273
|
+
* window reset. Shared by the all-marked tail of {@link CommandCodeAccountPool.resolveKey}
|
|
2274
|
+
* and by its already-tried diagnosis, so the two can never drift apart.
|
|
2275
|
+
*
|
|
2276
|
+
* The attached `providerRetryAfterMs` is the exact wait until the earliest
|
|
2277
|
+
* known reset, so dsh-llm-retry sleeps through the window instead of polling
|
|
2278
|
+
* at its backoff cadence. It is capped at {@link RETRY_MAX_DELAY_MS}: the
|
|
2279
|
+
* executor honors a provider wait verbatim only at or below the policy's
|
|
2280
|
+
* `maxDelayMs` — a LONGER attached wait makes it abandon the retry entirely
|
|
2281
|
+
* (normal mode), which would turn "poll until the window opens" into "fail
|
|
2282
|
+
* now". Longer resets simply ride the capped local backoff and the probe
|
|
2283
|
+
* revival.
|
|
2284
|
+
*/
|
|
2285
|
+
function allAccountsUnusable(accounts) {
|
|
2286
|
+
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");
|
|
2287
|
+
const resets = accounts.map((account) => account.state).filter((state) => state !== void 0 && state.kind === "cooldown" && state.until > 0).map((state) => state.until);
|
|
2288
|
+
const earliest = resets.length > 0 ? Math.min(...resets) : 0;
|
|
2289
|
+
const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
|
|
2290
|
+
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)}` : "") + ` — 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);
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2108
2293
|
* Normalize one resolved credential to the form every consumer sees. Trim
|
|
2109
2294
|
* only: a blank-after-trim value means "no key" (the slot is omitted and the
|
|
2110
2295
|
* caller reports `MISSING_CREDENTIAL`), while characters an HTTP header cannot
|
|
@@ -2353,7 +2538,7 @@ function scaleBy(size, factor) {
|
|
|
2353
2538
|
* and API key or subscription, and Command Code's terms apply.
|
|
2354
2539
|
*
|
|
2355
2540
|
* Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.4;
|
|
2356
|
-
* re-verified against command-code@1.
|
|
2541
|
+
* re-verified against command-code@1.57.0 — endpoints, request shape, and
|
|
2357
2542
|
* stream events unchanged):
|
|
2358
2543
|
* POST {apiBase}/alpha/generate
|
|
2359
2544
|
* body: { config, memory, taste, skills, params: { model, messages, tools,
|
|
@@ -2366,7 +2551,7 @@ function scaleBy(size, factor) {
|
|
|
2366
2551
|
* per-request options thunk and an API-key resolver from the plugin entry
|
|
2367
2552
|
* (src/index.ts), so a settings change reaches the very next request.
|
|
2368
2553
|
*/
|
|
2369
|
-
const COMMAND_CODE_CLI_VERSION = "1.
|
|
2554
|
+
const COMMAND_CODE_CLI_VERSION = "1.57.0";
|
|
2370
2555
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
2371
2556
|
const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
|
|
2372
2557
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
@@ -2404,15 +2589,22 @@ function booleanValue(value) {
|
|
|
2404
2589
|
return typeof value === "boolean" ? value : void 0;
|
|
2405
2590
|
}
|
|
2406
2591
|
/**
|
|
2407
|
-
* Thinking text carried by an OpenRouter-shaped `reasoning_details` array
|
|
2408
|
-
*
|
|
2409
|
-
*
|
|
2592
|
+
* Thinking text carried by an OpenRouter-shaped `reasoning_details` array,
|
|
2593
|
+
* concatenated in the order the gateway sent it.
|
|
2594
|
+
*
|
|
2595
|
+
* Entries are read by their own text members rather than filtered on `type`:
|
|
2596
|
+
* DeepSeek sends `{ type: 'reasoning.text', text }` while the OpenAI family
|
|
2597
|
+
* sends `{ type: 'reasoning.summary', summary, format: 'openai-responses-v1' }`
|
|
2598
|
+
* — the same summary text the scalar `delta.reasoning` carries, in the
|
|
2599
|
+
* Responses protocol's vocabulary. A `type` filter would drop one family or the
|
|
2600
|
+
* other, and losing thinking is the failure this exists to prevent.
|
|
2601
|
+
*
|
|
2602
|
+
* `text` wins per entry, but an EMPTY `text` must not shadow a populated
|
|
2603
|
+
* `summary` (the Responses wire emits `text: ''` placeholders), so the fallback
|
|
2604
|
+
* tests for non-empty rather than merely present. Entries carrying neither
|
|
2605
|
+
* member — the encrypted blobs — contribute nothing, and no entry contributes
|
|
2606
|
+
* both, so a summary is never counted twice.
|
|
2410
2607
|
*
|
|
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
2608
|
* Returns undefined when the array yields nothing, so the caller's `??` chain
|
|
2417
2609
|
* keeps falling through instead of treating "no text" as a reasoning delta.
|
|
2418
2610
|
*/
|
|
@@ -2421,10 +2613,24 @@ function reasoningDetailsText(value) {
|
|
|
2421
2613
|
let text = "";
|
|
2422
2614
|
for (const entry of value) {
|
|
2423
2615
|
if (!isRecord(entry)) continue;
|
|
2424
|
-
|
|
2616
|
+
const exact = stringValue(entry.text);
|
|
2617
|
+
const summary = stringValue(entry.summary);
|
|
2618
|
+
text += exact !== void 0 && exact !== "" ? exact : summary ?? "";
|
|
2425
2619
|
}
|
|
2426
2620
|
return text === "" ? void 0 : text;
|
|
2427
2621
|
}
|
|
2622
|
+
/**
|
|
2623
|
+
* One SCALAR thinking delta, treating an empty string as "this chunk carries
|
|
2624
|
+
* none" — the same rule {@link reasoningDetailsText} applies inside the array.
|
|
2625
|
+
* The scalar and the array describe the same thinking, so an empty spelling of
|
|
2626
|
+
* one must not shadow populated text in the other (the Responses wire pads with
|
|
2627
|
+
* `text: ''`). `??` alone cannot express this: an empty string is not nullish,
|
|
2628
|
+
* so `reasoning: ''` beside a populated `reasoning_details` used to drop the
|
|
2629
|
+
* whole chunk's thinking.
|
|
2630
|
+
*/
|
|
2631
|
+
function nonEmptyText(value) {
|
|
2632
|
+
return value === void 0 || value === "" ? void 0 : value;
|
|
2633
|
+
}
|
|
2428
2634
|
/** Parse a billing-period timestamp (ISO string or millis) into millis; 0 when absent/invalid. */
|
|
2429
2635
|
function periodEndValue(value) {
|
|
2430
2636
|
const asNumber = numberValue(value);
|
|
@@ -3674,11 +3880,11 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
3674
3880
|
};
|
|
3675
3881
|
};
|
|
3676
3882
|
if (opts?.unfiltered === true) return catalog.map(toInfo).sort(compareByPlan);
|
|
3677
|
-
const
|
|
3883
|
+
const accesses = this.deps.options().filterModelsByPlan === false ? void 0 : await this.loadPoolBillingAccess();
|
|
3678
3884
|
const visible = this.deps.options().visibleModels;
|
|
3679
3885
|
const allow = Array.isArray(visible) && visible.length > 0 ? new Set(visible.filter((id) => typeof id === "string" && id !== "")) : void 0;
|
|
3680
3886
|
const overrides = this.deps.options().modelVisibility;
|
|
3681
|
-
return catalog.filter((model) =>
|
|
3887
|
+
return catalog.filter((model) => modelVisibleForAnyAccount(model.id, accesses)).filter((model) => {
|
|
3682
3888
|
const override = overrides?.[model.id];
|
|
3683
3889
|
if (typeof override === "boolean") return override;
|
|
3684
3890
|
return allow === void 0 || allow.has(model.id);
|
|
@@ -3734,17 +3940,44 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
3734
3940
|
};
|
|
3735
3941
|
}
|
|
3736
3942
|
/**
|
|
3737
|
-
*
|
|
3738
|
-
*
|
|
3739
|
-
*
|
|
3943
|
+
* Every account's billing facts behind the picker's plan filter, in the
|
|
3944
|
+
* pool's rotation order and cached per key for {@link BILLING_ACCESS_TTL_MS}
|
|
3945
|
+
* (so the extra accounts cost their three requests once per TTL, not once
|
|
3946
|
+
* per picker load). `undefined` means the filter cannot be evaluated at all
|
|
3947
|
+
* — no key resolved — which {@link modelVisibleForAnyAccount} reads as
|
|
3948
|
+
* "show everything".
|
|
3949
|
+
*
|
|
3950
|
+
* The serving account is only the first entry: with several accounts the
|
|
3951
|
+
* model list must not depend on which of them rotation happens to be using.
|
|
3952
|
+
*/
|
|
3953
|
+
async loadPoolBillingAccess() {
|
|
3954
|
+
const keys = await this.poolAccountKeys();
|
|
3955
|
+
if (keys.length === 0) return void 0;
|
|
3956
|
+
return Promise.all(keys.map((key) => this.loadBillingAccessForKey(key)));
|
|
3957
|
+
}
|
|
3958
|
+
/**
|
|
3959
|
+
* The API keys the picker's plan filter must consult: every account the host
|
|
3960
|
+
* can serve from, in rotation order, when the host exposes one. A host
|
|
3961
|
+
* without a pool (or without the seam) reports just the key that would serve
|
|
3962
|
+
* this request — exactly the pre-pool behaviour.
|
|
3740
3963
|
*/
|
|
3741
|
-
async
|
|
3742
|
-
let apiKey;
|
|
3964
|
+
async poolAccountKeys() {
|
|
3743
3965
|
try {
|
|
3744
|
-
|
|
3966
|
+
const usable = (await this.deps.resolveAccountKeys?.() ?? []).filter((key) => typeof key === "string" && key !== "");
|
|
3967
|
+
if (usable.length > 0) return usable;
|
|
3968
|
+
} catch {}
|
|
3969
|
+
try {
|
|
3970
|
+
return [await this.deps.resolveApiKey(this.deps.options())];
|
|
3745
3971
|
} catch {
|
|
3746
|
-
return;
|
|
3972
|
+
return [];
|
|
3747
3973
|
}
|
|
3974
|
+
}
|
|
3975
|
+
/**
|
|
3976
|
+
* One account's billing facts, cached for {@link BILLING_ACCESS_TTL_MS} and
|
|
3977
|
+
* shared across concurrent callers. `undefined` means "unknown — show
|
|
3978
|
+
* everything" (fail-open).
|
|
3979
|
+
*/
|
|
3980
|
+
async loadBillingAccessForKey(apiKey) {
|
|
3748
3981
|
const cached = this.billingAccess.get(apiKey);
|
|
3749
3982
|
if (cached !== void 0 && Date.now() - cached.at < 3e5) return cached.value;
|
|
3750
3983
|
const existing = this.billingAccessInflight.get(apiKey);
|
|
@@ -3906,14 +4139,29 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
3906
4139
|
return report;
|
|
3907
4140
|
}
|
|
3908
4141
|
/**
|
|
3909
|
-
* Probe one account's
|
|
4142
|
+
* Probe one account's real usage windows from `/alpha/billing/credits`. The
|
|
3910
4143
|
* 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
|
-
*
|
|
4144
|
+
* account whose windows no longer report `exceeded` is revived, and the
|
|
4145
|
+
* `resetAt` values feed the "earliest reset" error message.
|
|
4146
|
+
*
|
|
4147
|
+
* BOTH windows the endpoint publishes are read, not just `fiveHour`. An
|
|
4148
|
+
* account can have an open five-hour window and an exhausted WEEKLY quota at
|
|
4149
|
+
* the same time (the two are metered separately), and reading only the
|
|
4150
|
+
* shorter one revived such an account as "usable" — the pool then handed it
|
|
4151
|
+
* out, the provider rejected the request again, and the next all-marked pass
|
|
4152
|
+
* revived it once more, so a used-up account kept coming back (issue #51's
|
|
4153
|
+
* follow-up: "切到一个不可用账号"). `exceeded` is therefore true when ANY
|
|
4154
|
+
* published window is exceeded, and `resetAt` is the LATEST reset among the
|
|
4155
|
+
* exceeded ones — the binding constraint, because a clear five-hour window
|
|
4156
|
+
* buys nothing while the weekly quota is spent.
|
|
4157
|
+
*
|
|
4158
|
+
* Returns `undefined` when the probe itself failed (transport, non-200, or a
|
|
4159
|
+
* payload with no window limits at all) — a failed probe never changes pool
|
|
4160
|
+
* state. The monthly/purchased/free credit balances are deliberately NOT part
|
|
4161
|
+
* of this answer: they are balances rather than windows, they carry no reset
|
|
4162
|
+
* time, and the server remains the final gate on them.
|
|
3915
4163
|
*/
|
|
3916
|
-
async
|
|
4164
|
+
async probeWindowLimits(apiKey) {
|
|
3917
4165
|
try {
|
|
3918
4166
|
const connection = this.deps.options();
|
|
3919
4167
|
const response = await this.fetchImpl(`${connection.apiBase}/alpha/billing/credits`, {
|
|
@@ -3924,11 +4172,22 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
3924
4172
|
const parsed = await response.json();
|
|
3925
4173
|
if (!isRecord(parsed)) return void 0;
|
|
3926
4174
|
const windowLimits = isRecord(parsed.windowLimits) ? parsed.windowLimits : void 0;
|
|
3927
|
-
|
|
3928
|
-
|
|
4175
|
+
if (windowLimits === void 0) return void 0;
|
|
4176
|
+
let reported = false;
|
|
4177
|
+
let exceeded = false;
|
|
4178
|
+
let resetAt = 0;
|
|
4179
|
+
for (const raw of [windowLimits.fiveHour, windowLimits.weekly]) {
|
|
4180
|
+
const window = isRecord(raw) ? parseWindowLimit(raw) : void 0;
|
|
4181
|
+
if (window === void 0) continue;
|
|
4182
|
+
reported = true;
|
|
4183
|
+
if (!window.exceeded) continue;
|
|
4184
|
+
exceeded = true;
|
|
4185
|
+
resetAt = Math.max(resetAt, window.resetAt);
|
|
4186
|
+
}
|
|
4187
|
+
if (!reported) return void 0;
|
|
3929
4188
|
return {
|
|
3930
|
-
exceeded
|
|
3931
|
-
resetAt:
|
|
4189
|
+
exceeded,
|
|
4190
|
+
resetAt: exceeded ? resetAt : 0
|
|
3932
4191
|
};
|
|
3933
4192
|
} catch {
|
|
3934
4193
|
return;
|
|
@@ -4002,13 +4261,23 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4002
4261
|
}
|
|
4003
4262
|
}
|
|
4004
4263
|
const rotate = this.deps.rotateApiKey;
|
|
4005
|
-
|
|
4006
|
-
|
|
4264
|
+
const rejection = classifyAccountRejection(attempt.status, attempt.errText);
|
|
4265
|
+
if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
|
|
4266
|
+
const next = await rotate(apiKey, rejection.reason, connection, options.model, {
|
|
4267
|
+
tried: [...tried],
|
|
4268
|
+
...rejection.resetAtMs !== void 0 && { resetAtMs: rejection.resetAtMs }
|
|
4269
|
+
});
|
|
4007
4270
|
if (next !== void 0 && !tried.has(next)) {
|
|
4008
4271
|
apiKey = next;
|
|
4009
4272
|
continue;
|
|
4010
4273
|
}
|
|
4011
4274
|
}
|
|
4275
|
+
if (rejection?.reason === "rate-limit" && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
|
|
4276
|
+
const reset = rejection.resetAtMs;
|
|
4277
|
+
const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
|
|
4278
|
+
const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
|
|
4279
|
+
throw new LlmError("llm-commandcode: the Command Code account is rate limited" + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`) + ";当前 Command Code 账户已被限流" + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), "RATE_LIMIT", wait > 0 ? { providerRetryAfterMs: wait } : void 0);
|
|
4280
|
+
}
|
|
4012
4281
|
throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
|
|
4013
4282
|
}
|
|
4014
4283
|
if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
|
|
@@ -4284,7 +4553,7 @@ function handleOpenAIEvent(asm, event) {
|
|
|
4284
4553
|
}
|
|
4285
4554
|
const choice = isRecord(choices[0]) ? choices[0] : {};
|
|
4286
4555
|
const delta = isRecord(choice.delta) ? choice.delta : {};
|
|
4287
|
-
const reasoningDelta = stringValue(delta.reasoning) ?? stringValue(delta.reasoning_content) ?? reasoningDetailsText(delta.reasoning_details) ?? "";
|
|
4556
|
+
const reasoningDelta = nonEmptyText(stringValue(delta.reasoning)) ?? nonEmptyText(stringValue(delta.reasoning_content)) ?? reasoningDetailsText(delta.reasoning_details) ?? "";
|
|
4288
4557
|
if (reasoningDelta !== "") {
|
|
4289
4558
|
chunks.push(...closeText(asm));
|
|
4290
4559
|
if (asm.reasoningIndex < 0) {
|
|
@@ -4400,6 +4669,89 @@ function mapFinishReason(reason) {
|
|
|
4400
4669
|
* back to the CLI /alpha/generate transport; other 4xx/5xx must surface as
|
|
4401
4670
|
* ordinary errors so real account/model problems are not masked.
|
|
4402
4671
|
*/
|
|
4672
|
+
/**
|
|
4673
|
+
* Classify a pre-stream rejection that is about the ACCOUNT rather than about
|
|
4674
|
+
* the request, using the official CLI's own rules (command-code@1.56.0:
|
|
4675
|
+
* `parseWindowLimitError`, `isInsufficientCreditsRequestError`,
|
|
4676
|
+
* `parseSpendCapError` and the terminal-marker list
|
|
4677
|
+
* `["premium_credits_exhausted", "model_not_in_plan", "insufficient credits"]`).
|
|
4678
|
+
* Undefined means "not an account-scoped rejection": a malformed request, an
|
|
4679
|
+
* oversized body or a context overflow must fail fast instead of walking the
|
|
4680
|
+
* whole account pool.
|
|
4681
|
+
*
|
|
4682
|
+
* Issue #51's follow-up is why this exists. Only 429/401 used to rotate, so
|
|
4683
|
+
* every other account-scoped rejection was terminal AND invisible to the pool:
|
|
4684
|
+
* an account that could not pay, or could not run the model, stayed "usable",
|
|
4685
|
+
* was handed out again on the next request, and produced the same error again
|
|
4686
|
+
* ("切到一个不可用账号后就报 400,还挺频繁"). The three reasons differ in what
|
|
4687
|
+
* the host should do with them, which is the caller's decision, not this
|
|
4688
|
+
* function's:
|
|
4689
|
+
*
|
|
4690
|
+
* - `rate-limit`: a usage window is spent. The code `RATE_LIMITED` is the
|
|
4691
|
+
* authoritative signal — the CLI accepts the code OR a 429 status — and the
|
|
4692
|
+
* body's `error.rateLimit` names the window and its `reset` (in seconds),
|
|
4693
|
+
* so the host can hold the account out until the provider's own reset time.
|
|
4694
|
+
* - `invalid-credential`: 401; the key itself is bad.
|
|
4695
|
+
* - `unavailable`: the key is fine but this account cannot serve: no credits
|
|
4696
|
+
* (`400 Insufficient credits`, the codes `INSUFFICIENT_CREDITS`,
|
|
4697
|
+
* `USAGE_EXCEEDED`, `PREMIUM_CREDITS_EXHAUSTED`, or a spend-cap wording)
|
|
4698
|
+
* or a model outside its plan (`MODEL_NOT_IN_PLAN`). Rotating must NOT mark
|
|
4699
|
+
* the account: the fact is about the model or the balance, it can change
|
|
4700
|
+
* without the key changing, and a `:free` model can still be served by an
|
|
4701
|
+
* account with an empty balance.
|
|
4702
|
+
*/
|
|
4703
|
+
function classifyAccountRejection(status, errText) {
|
|
4704
|
+
if (status === 429 || readProviderErrorCode(errText)?.toUpperCase() === "RATE_LIMITED") {
|
|
4705
|
+
const resetAtMs = readWindowResetAtMs(errText);
|
|
4706
|
+
return resetAtMs === void 0 ? { reason: "rate-limit" } : {
|
|
4707
|
+
reason: "rate-limit",
|
|
4708
|
+
resetAtMs
|
|
4709
|
+
};
|
|
4710
|
+
}
|
|
4711
|
+
if (status === 401) return { reason: "invalid-credential" };
|
|
4712
|
+
if (status < 400 || status >= 500) return void 0;
|
|
4713
|
+
const lower = errText.toLowerCase().replaceAll("_", " ");
|
|
4714
|
+
const code = readProviderErrorCode(errText)?.toUpperCase();
|
|
4715
|
+
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" };
|
|
4716
|
+
}
|
|
4717
|
+
/** The machine-readable `error.code`/`error.type` of a pre-stream failure body. */
|
|
4718
|
+
function readProviderErrorCode(errText) {
|
|
4719
|
+
try {
|
|
4720
|
+
const parsed = JSON.parse(errText);
|
|
4721
|
+
if (!isRecord(parsed)) return void 0;
|
|
4722
|
+
const error = isRecord(parsed.error) ? parsed.error : parsed;
|
|
4723
|
+
return stringValue(error.code) ?? stringValue(error.type);
|
|
4724
|
+
} catch {
|
|
4725
|
+
return;
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
/**
|
|
4729
|
+
* The exact reset time carried by a window-limit rejection body, in millis.
|
|
4730
|
+
* The CLI reads `error.rateLimit.reset` as SECONDS (`1e3 * reset`) and falls
|
|
4731
|
+
* back to a `resets at <ISO>` stamp in the message; both are honored here so a
|
|
4732
|
+
* 429 does not need a billing probe to learn a fact the provider already told
|
|
4733
|
+
* us. Undefined when the body carries neither.
|
|
4734
|
+
*/
|
|
4735
|
+
function readWindowResetAtMs(errText) {
|
|
4736
|
+
try {
|
|
4737
|
+
const parsed = JSON.parse(errText);
|
|
4738
|
+
if (isRecord(parsed)) {
|
|
4739
|
+
const error = isRecord(parsed.error) ? parsed.error : parsed;
|
|
4740
|
+
const seconds = numberValue((isRecord(error.rateLimit) ? error.rateLimit : void 0)?.reset);
|
|
4741
|
+
if (seconds !== void 0 && seconds > 0) return Math.round(seconds * 1e3);
|
|
4742
|
+
const message = stringValue(error.message) ?? "";
|
|
4743
|
+
const stamp = /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(message)?.[1];
|
|
4744
|
+
if (stamp !== void 0) {
|
|
4745
|
+
const at = Date.parse(stamp);
|
|
4746
|
+
if (!Number.isNaN(at)) return at;
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
} catch {}
|
|
4750
|
+
const stamp = /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(errText)?.[1];
|
|
4751
|
+
if (stamp === void 0) return void 0;
|
|
4752
|
+
const at = Date.parse(stamp);
|
|
4753
|
+
return Number.isNaN(at) ? void 0 : at;
|
|
4754
|
+
}
|
|
4403
4755
|
function isUpgradeRequiredError(status, errText) {
|
|
4404
4756
|
if (status !== 403) return false;
|
|
4405
4757
|
const lower = errText.toLowerCase();
|
|
@@ -6335,7 +6687,7 @@ function apply(ctx, config) {
|
|
|
6335
6687
|
slots,
|
|
6336
6688
|
resolveRef,
|
|
6337
6689
|
authFileKey: resolveAuthFileApiKey,
|
|
6338
|
-
probeWindow: (apiKey) => adapter.
|
|
6690
|
+
probeWindow: (apiKey) => adapter.probeWindowLimits(apiKey),
|
|
6339
6691
|
preferredId,
|
|
6340
6692
|
modelAccountRules: () => current().modelAccountRules ?? []
|
|
6341
6693
|
});
|
|
@@ -6348,10 +6700,11 @@ function apply(ctx, config) {
|
|
|
6348
6700
|
const adapter = new CommandCodeAdapter({
|
|
6349
6701
|
options,
|
|
6350
6702
|
resolveApiKey,
|
|
6351
|
-
rotateApiKey: async (rejectedKey, rejection, _connection, model) => {
|
|
6352
|
-
pool.markRejected(rejectedKey, rejection);
|
|
6353
|
-
const
|
|
6354
|
-
|
|
6703
|
+
rotateApiKey: async (rejectedKey, rejection, _connection, model, rotation) => {
|
|
6704
|
+
if (rejection !== "unavailable") pool.markRejected(rejectedKey, rejection, rotation?.resetAtMs);
|
|
6705
|
+
const tried = rotation?.tried?.length ? rotation.tried : [rejectedKey];
|
|
6706
|
+
const resolved = await pool.resolveKey(model === void 0 ? { tried } : {
|
|
6707
|
+
tried,
|
|
6355
6708
|
model
|
|
6356
6709
|
});
|
|
6357
6710
|
return resolved === void 0 ? void 0 : assertUsableApiKey(resolved.key, "llm-commandcode", resolved.slot.ref ?? `${resolved.slot.label} (config.apiKey)`);
|
|
@@ -6359,6 +6712,9 @@ function apply(ctx, config) {
|
|
|
6359
6712
|
resolveAttachments: () => {
|
|
6360
6713
|
const attachments = ctx.get("attachments");
|
|
6361
6714
|
return attachments === void 0 ? void 0 : attachments;
|
|
6715
|
+
},
|
|
6716
|
+
resolveAccountKeys: async () => {
|
|
6717
|
+
return (await pool.resolvedAccounts()).map((account) => account.key);
|
|
6362
6718
|
}
|
|
6363
6719
|
});
|
|
6364
6720
|
ctx.llm.registerConfigurableProviders([{
|