@mars-sea/dsh-commandcode-provider 0.10.6 → 0.11.0
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 +32 -0
- package/README.md +6 -3
- package/README.zh-CN.md +6 -3
- package/lib/client.js +2336 -193
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +108 -7
- package/lib/index.js +1819 -505
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
5
|
-
import { LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, assertUsableApiKey, attributionHeaders, errorChain, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, assertUsableApiKey, attributionHeaders, errorChain, isContextWindowExceededError, offloadRequestImagesWithPolicy, offloadedImageText, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
6
6
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
7
7
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -12,267 +12,6 @@ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
|
12
12
|
import { createServer } from "node:http";
|
|
13
13
|
import { createServer as createServer$1 } from "node:net";
|
|
14
14
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
15
|
-
//#region src/accounts.ts
|
|
16
|
-
/**
|
|
17
|
-
* Multi-account pool for the Command Code provider (host side).
|
|
18
|
-
*
|
|
19
|
-
* One Command Code subscription (e.g. the Go plan's 5-hour window) is
|
|
20
|
-
* metered; a user with several subscriptions wants a request that hits one
|
|
21
|
-
* account's limit to continue on the next account without a visible failure.
|
|
22
|
-
* This module owns that rotation:
|
|
23
|
-
*
|
|
24
|
-
* - {@link CommandCodeAccountPool.resolveKey} hands out the first account
|
|
25
|
-
* whose key is not currently marked exhausted — or, when the request's
|
|
26
|
-
* model matches a {@link CommandCodeModelAccountRule} and that account is
|
|
27
|
-
* usable, the routed account — resolving each slot's key lazily (literal
|
|
28
|
-
* config key → credential seam → launch environment → the official CLI
|
|
29
|
-
* auth file for the default slot only).
|
|
30
|
-
* - {@link CommandCodeAccountPool.markRejected} records a 429 (rate limit,
|
|
31
|
-
* window unknown) or 401 (invalid key, disabled until the config changes)
|
|
32
|
-
* against the exact API key, so several slots sharing one key share one
|
|
33
|
-
* state.
|
|
34
|
-
* - When every account is marked, the pool probes each key's
|
|
35
|
-
* `/alpha/billing/credits` window limits (through the injected
|
|
36
|
-
* {@link CommandCodeAccountPoolDeps.probeWindow}): an account whose window
|
|
37
|
-
* no longer reports `exceeded` is revived, otherwise the pool throws a
|
|
38
|
-
* `RATE_LIMIT` error naming the earliest reset time.
|
|
39
|
-
*
|
|
40
|
-
* The pool is deliberately cordis-free (like the adapter): every host fact
|
|
41
|
-
* arrives through injected thunks, so node tests can drive it directly.
|
|
42
|
-
*
|
|
43
|
-
* @module dsh-commandcode-provider/accounts
|
|
44
|
-
*/
|
|
45
|
-
/**
|
|
46
|
-
* Upper bound on the retry wait this pool attaches to the all-exhausted
|
|
47
|
-
* `RATE_LIMIT` error. Must equal the `backoff.maxDelayMs` in the adapter's
|
|
48
|
-
* `providerRetryPolicy` (which imports it from here): dsh-llm-retry honors a
|
|
49
|
-
* provider-specified wait verbatim only at or below that cap — in normal mode
|
|
50
|
-
* a LONGER attached wait makes the executor abandon the retry entirely
|
|
51
|
-
* instead of falling back to local backoff, which would turn "poll until the
|
|
52
|
-
* window opens" into "fail now".
|
|
53
|
-
*/
|
|
54
|
-
const RETRY_MAX_DELAY_MS = 9e5;
|
|
55
|
-
/** A labeled, human-readable clock reading for error messages. */
|
|
56
|
-
function clockLabel(ms) {
|
|
57
|
-
return new Date(ms).toLocaleString();
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Whether an account with this rotation state can serve a request right now.
|
|
61
|
-
* `undefined` (never rejected) is usable; a cooldown becomes usable again
|
|
62
|
-
* once its reset time passes; `unknown` (429, reset unprobed) and
|
|
63
|
-
* `disabled` (401) are not.
|
|
64
|
-
*/
|
|
65
|
-
function accountUsable(state) {
|
|
66
|
-
if (state === void 0) return true;
|
|
67
|
-
if (state.kind === "cooldown") return state.until > 0 && Date.now() >= state.until;
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Pick the account that should serve now: the manually preferred slot when it
|
|
72
|
-
* is usable, otherwise the first usable account in rotation order; undefined
|
|
73
|
-
* when no account is usable. Shared by the pool (request path) and the plugin
|
|
74
|
-
* entry (the usage view's active badge) so both always agree.
|
|
75
|
-
*/
|
|
76
|
-
function selectActiveAccount(accounts, preferredId) {
|
|
77
|
-
const usable = accounts.filter((account) => accountUsable(account.state));
|
|
78
|
-
if (preferredId !== void 0) {
|
|
79
|
-
const preferred = usable.find((account) => account.slot.id === preferredId);
|
|
80
|
-
if (preferred !== void 0) return preferred;
|
|
81
|
-
}
|
|
82
|
-
return usable[0];
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* The first routing rule whose model list contains the request's model id.
|
|
86
|
-
* Undefined when no rule matches.
|
|
87
|
-
*/
|
|
88
|
-
function matchModelRule(model, rules) {
|
|
89
|
-
if (model === "" || rules === void 0 || rules.length === 0) return void 0;
|
|
90
|
-
for (const rule of rules) if (rule.models.includes(model)) return rule;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* The routed account for a request's model: the first usable account whose
|
|
94
|
-
* slot id matches the first matching rule's target. Undefined when no rule
|
|
95
|
-
* matches or the routed account is not usable (the caller then falls back to
|
|
96
|
-
* the normal preferred/rotation selection).
|
|
97
|
-
*/
|
|
98
|
-
function selectAccountForModel(accounts, model, rules) {
|
|
99
|
-
const rule = matchModelRule(model, rules);
|
|
100
|
-
if (rule === void 0) return void 0;
|
|
101
|
-
return accounts.find((account) => account.slot.id === rule.account && accountUsable(account.state));
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* The account pool. Rotation state is keyed by API key (never logged), so two
|
|
105
|
-
* slots resolving to the same credential share one mark, and a key changed in
|
|
106
|
-
* the credentials service starts with a clean slate.
|
|
107
|
-
*/
|
|
108
|
-
var CommandCodeAccountPool = class {
|
|
109
|
-
deps;
|
|
110
|
-
/** Rotation state by API key. */
|
|
111
|
-
states = /* @__PURE__ */ new Map();
|
|
112
|
-
constructor(deps) {
|
|
113
|
-
this.deps = deps;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Resolve every slot's key, deduplicated by key (first slot wins). Slots
|
|
117
|
-
* without any resolvable key are omitted — they still appear in the
|
|
118
|
-
* settings page as unconfigured, they just cannot serve requests.
|
|
119
|
-
*/
|
|
120
|
-
async resolvedAccounts() {
|
|
121
|
-
const out = [];
|
|
122
|
-
const seen = /* @__PURE__ */ new Set();
|
|
123
|
-
for (const slot of this.deps.slots()) {
|
|
124
|
-
const key = await this.resolveSlotKey(slot);
|
|
125
|
-
if (key === void 0 || seen.has(key)) continue;
|
|
126
|
-
seen.add(key);
|
|
127
|
-
out.push({
|
|
128
|
-
slot,
|
|
129
|
-
key,
|
|
130
|
-
state: this.states.get(key)
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return out;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Every slot paired with its resolved key and rotation state — NOT
|
|
137
|
-
* deduplicated: two slots sharing one credential both appear (the usage
|
|
138
|
-
* view reports them individually), while slots without any resolvable key
|
|
139
|
-
* are omitted. The serving path uses {@link resolvedAccounts} instead.
|
|
140
|
-
*/
|
|
141
|
-
async describeAccounts() {
|
|
142
|
-
const out = [];
|
|
143
|
-
for (const slot of this.deps.slots()) {
|
|
144
|
-
const key = await this.resolveSlotKey(slot);
|
|
145
|
-
if (key === void 0) continue;
|
|
146
|
-
out.push({
|
|
147
|
-
slot,
|
|
148
|
-
key,
|
|
149
|
-
state: this.states.get(key)
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
return out;
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Hand out the key for a request: the model-routed account when the
|
|
156
|
-
* request's model matches a rule (and that account is usable), else the
|
|
157
|
-
* manually preferred account when usable, else the first usable account in
|
|
158
|
-
* rotation order. Returns `undefined` when no account resolves any key at
|
|
159
|
-
* all (the caller then reports the missing credential). Throws
|
|
160
|
-
* `RATE_LIMIT` — naming the earliest window reset — or
|
|
161
|
-
* `INVALID_CREDENTIAL` when accounts exist but none can serve.
|
|
162
|
-
*
|
|
163
|
-
* `options.model` is the request's model id; routing rules re-read per
|
|
164
|
-
* resolution, so a settings change applies live.
|
|
165
|
-
*
|
|
166
|
-
* `options.exclude` skips one key during the probe-revival pass: the
|
|
167
|
-
* rotation hook excludes the just-rejected key so a probe that clears its
|
|
168
|
-
* window cannot re-offer the same key within the same request (the adapter
|
|
169
|
-
* refuses already-tried keys; the next request picks the revived key up).
|
|
170
|
-
*/
|
|
171
|
-
async resolveKey(options) {
|
|
172
|
-
const accounts = await this.resolvedAccounts();
|
|
173
|
-
if (accounts.length === 0) return;
|
|
174
|
-
const routed = selectAccountForModel(accounts, options?.model ?? "", this.deps.modelAccountRules?.());
|
|
175
|
-
if (routed !== void 0) return this.pick(routed);
|
|
176
|
-
const preferred = this.deps.preferredId?.();
|
|
177
|
-
const chosen = selectActiveAccount(accounts, preferred);
|
|
178
|
-
if (chosen !== void 0) return this.pick(chosen);
|
|
179
|
-
await Promise.all(accounts.map(async (account) => {
|
|
180
|
-
if (account.state?.kind === "disabled") return;
|
|
181
|
-
if (options?.exclude !== void 0 && account.key === options.exclude) return;
|
|
182
|
-
let probe;
|
|
183
|
-
try {
|
|
184
|
-
probe = await this.deps.probeWindow(account.key);
|
|
185
|
-
} catch {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
if (probe === void 0) return;
|
|
189
|
-
if (!probe.exceeded) this.states.delete(account.key);
|
|
190
|
-
else this.states.set(account.key, {
|
|
191
|
-
kind: "cooldown",
|
|
192
|
-
reason: account.state?.reason ?? "rate limited (429)",
|
|
193
|
-
until: probe.resetAt
|
|
194
|
-
});
|
|
195
|
-
}));
|
|
196
|
-
const latest = await this.resolvedAccounts();
|
|
197
|
-
const revived = selectActiveAccount(latest, preferred);
|
|
198
|
-
if (revived !== void 0) return this.pick(revived);
|
|
199
|
-
if (latest.filter((account) => account.state?.kind === "disabled").length === latest.length) throw new LlmError(`llm-commandcode: every configured Command Code account (${latest.length}) was rejected with 401 — check the stored API keys (Models page / settings) or the auth file;已配置的 ${latest.length} 个 Command Code 账户密钥均被拒绝(401)——请在设置页检查存储的 API 密钥,或重新运行 command-code login`, "INVALID_CREDENTIAL");
|
|
200
|
-
const resets = latest.map((account) => account.state).filter((state) => state !== void 0 && state.kind === "cooldown" && state.until > 0).map((state) => state.until);
|
|
201
|
-
const earliest = resets.length > 0 ? Math.min(...resets) : 0;
|
|
202
|
-
const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
|
|
203
|
-
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);
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Record a rejection against one key. `rate-limit` (429) marks the key
|
|
207
|
-
* exhausted with an unknown reset (probed lazily at the next resolution
|
|
208
|
-
* once every account is marked); `invalid-credential` (401) disables the
|
|
209
|
-
* key until the stored credential changes.
|
|
210
|
-
*/
|
|
211
|
-
markRejected(apiKey, rejection) {
|
|
212
|
-
if (rejection === "invalid-credential") this.states.set(apiKey, {
|
|
213
|
-
kind: "disabled",
|
|
214
|
-
reason: "invalid API key (401)",
|
|
215
|
-
until: 0
|
|
216
|
-
});
|
|
217
|
-
else this.states.set(apiKey, {
|
|
218
|
-
kind: "unknown",
|
|
219
|
-
reason: "rate limited (429)",
|
|
220
|
-
until: 0
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* One account's key: literal → credential seam → auth file (default slot).
|
|
225
|
-
*
|
|
226
|
-
* Every source is normalized here, at the single point where a slot's key
|
|
227
|
-
* enters the pool. The adapter sends the key through the harness's
|
|
228
|
-
* `assertUsableApiKey()`, which trims it — a stored key from the credentials
|
|
229
|
-
* seam, a `.env` line, or a shell export all pick up surrounding whitespace
|
|
230
|
-
* — and reports that trimmed form back to `markRejected()`. Returning the
|
|
231
|
-
* raw value would file every 429/401 mark under a key no later lookup can
|
|
232
|
-
* find: rotation would re-offer the same account, the account card would show
|
|
233
|
-
* no mark, and the usage endpoints would 401 while chat kept working.
|
|
234
|
-
* Normalizing once makes resolution, probing, marking, and the request path
|
|
235
|
-
* agree on one string.
|
|
236
|
-
*/
|
|
237
|
-
async resolveSlotKey(slot) {
|
|
238
|
-
if (slot.literal !== void 0) {
|
|
239
|
-
const literal = normalizeResolvedKey(slot.literal);
|
|
240
|
-
if (literal !== void 0) return literal;
|
|
241
|
-
}
|
|
242
|
-
if (slot.ref !== void 0) {
|
|
243
|
-
const hit = await this.deps.resolveRef(slot.ref);
|
|
244
|
-
if (hit !== void 0) {
|
|
245
|
-
const resolved = normalizeResolvedKey(hit);
|
|
246
|
-
if (resolved !== void 0) return resolved;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
if (slot.allowAuthFile) {
|
|
250
|
-
const fromFile = this.deps.authFileKey();
|
|
251
|
-
if (fromFile !== void 0) {
|
|
252
|
-
const fileKey = normalizeResolvedKey(fromFile);
|
|
253
|
-
if (fileKey !== void 0) return fileKey;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
/** Hand out the chosen account's key. */
|
|
258
|
-
pick(account) {
|
|
259
|
-
return {
|
|
260
|
-
key: account.key,
|
|
261
|
-
slot: account.slot
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
/**
|
|
266
|
-
* Normalize one resolved credential to the form every consumer sees. Trim
|
|
267
|
-
* only: a blank-after-trim value means "no key" (the slot is omitted and the
|
|
268
|
-
* caller reports `MISSING_CREDENTIAL`), while characters an HTTP header cannot
|
|
269
|
-
* carry are left for `assertUsableApiKey()` to reject with its own message.
|
|
270
|
-
*/
|
|
271
|
-
function normalizeResolvedKey(value) {
|
|
272
|
-
const trimmed = value.trim();
|
|
273
|
-
return trimmed === "" ? void 0 : trimmed;
|
|
274
|
-
}
|
|
275
|
-
//#endregion
|
|
276
15
|
//#region src/capabilities.ts
|
|
277
16
|
/**
|
|
278
17
|
* Static capability snapshot for the Command Code provider: model →
|
|
@@ -751,268 +490,1586 @@ const PLAN_LABELS = {
|
|
|
751
490
|
max: "Max"
|
|
752
491
|
};
|
|
753
492
|
/**
|
|
754
|
-
* Plan-tier sort weights, low to high. Models outside the snapshot (unknown
|
|
755
|
-
* plans) sort after every known tier, keeping known models predictable.
|
|
493
|
+
* Plan-tier sort weights, low to high. Models outside the snapshot (unknown
|
|
494
|
+
* plans) sort after every known tier, keeping known models predictable.
|
|
495
|
+
*/
|
|
496
|
+
const PLAN_ORDER = {
|
|
497
|
+
go: 0,
|
|
498
|
+
goat: 1,
|
|
499
|
+
pro: 2,
|
|
500
|
+
provider: 3,
|
|
501
|
+
max: 4
|
|
502
|
+
};
|
|
503
|
+
/**
|
|
504
|
+
* Whether a model is free (requests cost no credits), per the pricing page's
|
|
505
|
+
* deals (`KNOWN_DEALS` `free: true`). Free models lead the picker regardless
|
|
506
|
+
* of tier — they are usable by every account, so they are the best default
|
|
507
|
+
* candidates.
|
|
508
|
+
*/
|
|
509
|
+
function isFreeModel(modelId) {
|
|
510
|
+
return KNOWN_DEALS[modelId]?.free === true;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Comparator for the model picker: free models first (zero credit cost, usable
|
|
514
|
+
* by every account), then by plan tier (lowest first), then by model name,
|
|
515
|
+
* then by id as a tiebreak. Models with no known plan sort last.
|
|
516
|
+
*/
|
|
517
|
+
function compareByPlan(a, b) {
|
|
518
|
+
const freeDelta = Number(isFreeModel(b.id)) - Number(isFreeModel(a.id));
|
|
519
|
+
if (freeDelta !== 0) return freeDelta;
|
|
520
|
+
const pa = PLAN_ORDER[KNOWN_PLANS[a.id] ?? ""] ?? Number.MAX_SAFE_INTEGER;
|
|
521
|
+
const pb = PLAN_ORDER[KNOWN_PLANS[b.id] ?? ""] ?? Number.MAX_SAFE_INTEGER;
|
|
522
|
+
if (pa !== pb) return pa - pb;
|
|
523
|
+
const nameDiff = a.name.localeCompare(b.name);
|
|
524
|
+
if (nameDiff !== 0) return nameDiff;
|
|
525
|
+
return a.id.localeCompare(b.id);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Subscription plan table, synced from the official CLI bundle's plan maps
|
|
529
|
+
* (located by the `"individual-go"` key in command-code@1.53.0 `dist/cli.mjs`,
|
|
530
|
+
* re-verified unchanged through 1.53.0): subscription `planId`
|
|
531
|
+
* prefix → display name and the plan's monthly credit total. This is the
|
|
532
|
+
* account's own subscription (from `/alpha/billing/subscriptions`) — distinct
|
|
533
|
+
* from {@link KNOWN_PLANS}, which maps catalog models to their minimum tier.
|
|
534
|
+
*
|
|
535
|
+
* `tierWeight` is plugin-added (not from the CLI maps): the plan's rank on
|
|
536
|
+
* the {@link PLAN_ORDER} scale, used by the picker's plan filter
|
|
537
|
+
* ({@link modelVisibleInPlan}) to hide models above the account's tier.
|
|
538
|
+
*/
|
|
539
|
+
const KNOWN_SUBSCRIPTION_PLANS = {
|
|
540
|
+
"individual-go": {
|
|
541
|
+
name: "Go",
|
|
542
|
+
monthlyCredits: 10,
|
|
543
|
+
tierWeight: 0
|
|
544
|
+
},
|
|
545
|
+
"individual-goat": {
|
|
546
|
+
name: "GOAT",
|
|
547
|
+
monthlyCredits: 70,
|
|
548
|
+
tierWeight: 1
|
|
549
|
+
},
|
|
550
|
+
"individual-pro": {
|
|
551
|
+
name: "Pro",
|
|
552
|
+
monthlyCredits: 30,
|
|
553
|
+
tierWeight: 2
|
|
554
|
+
},
|
|
555
|
+
"individual-pro-v1": {
|
|
556
|
+
name: "Pro",
|
|
557
|
+
monthlyCredits: 80,
|
|
558
|
+
tierWeight: 2
|
|
559
|
+
},
|
|
560
|
+
"individual-provider": {
|
|
561
|
+
name: "Provider",
|
|
562
|
+
monthlyCredits: 15,
|
|
563
|
+
tierWeight: 3
|
|
564
|
+
},
|
|
565
|
+
"individual-max": {
|
|
566
|
+
name: "Max",
|
|
567
|
+
monthlyCredits: 150,
|
|
568
|
+
tierWeight: 4
|
|
569
|
+
},
|
|
570
|
+
"individual-ultra": {
|
|
571
|
+
name: "Ultra",
|
|
572
|
+
monthlyCredits: 300,
|
|
573
|
+
tierWeight: 4
|
|
574
|
+
},
|
|
575
|
+
"teams-pro": {
|
|
576
|
+
name: "Teams Pro",
|
|
577
|
+
monthlyCredits: 40,
|
|
578
|
+
tierWeight: 2
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
/** Plan-id prefixes, longest first — the CLI's prefix-match order. */
|
|
582
|
+
const SUBSCRIPTION_PLAN_PREFIXES = Object.keys(KNOWN_SUBSCRIPTION_PLANS).sort((a, b) => b.length - a.length);
|
|
583
|
+
/**
|
|
584
|
+
* Resolve a subscription `planId` (e.g. `individual-pro-v1`) to its display
|
|
585
|
+
* name and monthly credit total, mirroring the CLI's `getPlanInfo`:
|
|
586
|
+
* normalize (lowercase, `_` → `-`), then longest-prefix match so
|
|
587
|
+
* `individual-pro-v1` wins over `individual-pro`. Unknown ids return
|
|
588
|
+
* `undefined`.
|
|
589
|
+
*/
|
|
590
|
+
function subscriptionPlanInfo(planId) {
|
|
591
|
+
const normalized = planId.toLowerCase().replace(/_/g, "-");
|
|
592
|
+
const prefix = SUBSCRIPTION_PLAN_PREFIXES.find((candidate) => normalized.startsWith(candidate));
|
|
593
|
+
return prefix === void 0 ? void 0 : KNOWN_SUBSCRIPTION_PLANS[prefix];
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Whether the picker lists `modelId` for an account with the given billing
|
|
597
|
+
* access. Fails open at every uncertainty: no billing data, an unknown plan,
|
|
598
|
+
* a non-finite weight (corrupt billing fact), or a model outside
|
|
599
|
+
* {@link KNOWN_PLANS} all keep the model visible — the server remains the
|
|
600
|
+
* final gate (`403 MODEL_NOT_IN_PLAN`).
|
|
601
|
+
*/
|
|
602
|
+
function modelVisibleInPlan(modelId, access) {
|
|
603
|
+
if (access === void 0) return true;
|
|
604
|
+
if (access.onDemandCredits > 0) return true;
|
|
605
|
+
if (access.tierWeight === void 0 || !Number.isFinite(access.tierWeight)) return true;
|
|
606
|
+
const tier = KNOWN_PLANS[modelId];
|
|
607
|
+
if (tier === void 0) return true;
|
|
608
|
+
const weight = PLAN_ORDER[tier];
|
|
609
|
+
if (weight === void 0) return true;
|
|
610
|
+
return weight <= access.tierWeight;
|
|
611
|
+
}
|
|
612
|
+
const KNOWN_DEALS = {
|
|
613
|
+
"MiniMaxAI/MiniMax-M3": { label: "50% off" },
|
|
614
|
+
"xiaomi/mimo-v2.5-pro": { label: "99% off" },
|
|
615
|
+
"xiaomi/mimo-v2.5": { label: "98% off" },
|
|
616
|
+
"poolside/laguna-s-2.1-free": {
|
|
617
|
+
label: "FREE",
|
|
618
|
+
free: true
|
|
619
|
+
},
|
|
620
|
+
"meituan/LongCat-2.0:free": {
|
|
621
|
+
label: "FREE",
|
|
622
|
+
free: true
|
|
623
|
+
},
|
|
624
|
+
"inclusionai/ling-3.0-flash-sante:free": {
|
|
625
|
+
label: "FREE",
|
|
626
|
+
free: true
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
/**
|
|
630
|
+
* Models with time-of-day (peak/off-peak) pricing, per the official pricing
|
|
631
|
+
* page (`/docs/resources/pricing-limits`). Since 2026-08-16 16:00 UTC, DeepSeek
|
|
632
|
+
* charges by the hour: peak hours are 01:00–04:00 and 06:00–10:00 UTC (7h per
|
|
633
|
+
* weekday, full price) **Monday to Friday only**; the other 17 hours of a
|
|
634
|
+
* weekday and every hour of Saturday/Sunday (UTC) are off-peak at half price.
|
|
635
|
+
* The V4 Flash Vision (exp) variant (command-code@1.32.0) shares the V4 Flash
|
|
636
|
+
* rates exactly — $0.15/$0.60 off-peak and $0.30/$1.20 peak, per the page's own
|
|
637
|
+
* `timeOfDay` block, not merely 2× its own off-peak figures: a rate that is
|
|
638
|
+
* internally consistent can still be the wrong row, which is why the vendored
|
|
639
|
+
* price table (`./model-prices.ts`) is synced from the page and not hand-kept.
|
|
640
|
+
* The picker shows the
|
|
641
|
+
* *current* state as a compact
|
|
642
|
+
* label (`Peak`/`Half`) matching the English noun style of the other markers
|
|
643
|
+
* (`Image`, `FREE`), so a developer can tell at a glance whether calling the
|
|
644
|
+
* model right now is cheap or expensive.
|
|
645
|
+
*
|
|
646
|
+
* Authoritative extraction: the pricing page embeds a model JSON array whose
|
|
647
|
+
* hourly-priced entries carry a `timeOfDay` block
|
|
648
|
+
* (`{ windows: "01–04 & 06–10 UTC, Mon–Fri", peakHoursPerDay: 7,
|
|
649
|
+
* offPeakHoursPerDay: 17, peak: {...}, offPeak: {...} }`). Exactly four models
|
|
650
|
+
* carry it: V4 Pro, V4 Flash, V4 Flash Vision (exp), and V4.1 Flash (added in
|
|
651
|
+
* command-code@1.53.0 at $0.15/$0.60 off-peak, $0.30/$1.20 peak — the same
|
|
652
|
+
* schedule as the other three).
|
|
653
|
+
*
|
|
654
|
+
* Extraction caution: the rendered HTML rows are a trap. Each annotation div
|
|
655
|
+
* sits inside its OWN row's container, immediately before the NEXT row starts,
|
|
656
|
+
* so flattening the page to text makes every annotation look like it belongs
|
|
657
|
+
* to the model printed after it — that is how `deepseek/deepseek-v4-flash-fast`
|
|
658
|
+
* was wrongly added here (its row is flat-priced at $0.28/$0.56/$0.07 and has
|
|
659
|
+
* no `timeOfDay` block). Trust the embedded JSON's `timeOfDay` membership and
|
|
660
|
+
* the 2× price relation, never the flat-text neighbor.
|
|
661
|
+
*
|
|
662
|
+
* Keep in sync with the official pricing page when the model set, the peak
|
|
663
|
+
* windows, or the weekday rule change (see the dsh-commandcode-upstream skill).
|
|
664
|
+
*/
|
|
665
|
+
const KNOWN_PEAK_PRICING = /* @__PURE__ */ new Set([
|
|
666
|
+
"deepseek/deepseek-v4-pro",
|
|
667
|
+
"deepseek/deepseek-v4-flash",
|
|
668
|
+
"deepseek/deepseek-v4-flash-vision-exp",
|
|
669
|
+
"deepseek/deepseek-v4.1-flash"
|
|
670
|
+
]);
|
|
671
|
+
/**
|
|
672
|
+
* Peak hours (UTC, hour-of-day range end-exclusive): 01–03 and 06–09.
|
|
673
|
+
* Weekday-only — see `peakPricingState()`; weekends are fully off-peak.
|
|
674
|
+
*
|
|
675
|
+
* Exported because the vendored price table (`./model-prices.ts`) ships these
|
|
676
|
+
* windows to the browser with the table itself: the composer prices a session
|
|
677
|
+
* against the very schedule this snapshot knows rather than restating it, so
|
|
678
|
+
* there is one place to update when the windows move.
|
|
679
|
+
*/
|
|
680
|
+
const PEAK_HOUR_RANGES = [[1, 4], [6, 10]];
|
|
681
|
+
/**
|
|
682
|
+
* Whether `now` (defaults to `Date.now()`) falls inside a peak-pricing window,
|
|
683
|
+
* ignoring which model is asking. Peak rates apply Monday–Friday (UTC) only,
|
|
684
|
+
* so a weekend timestamp is off-peak even inside {@link PEAK_HOUR_RANGES}.
|
|
685
|
+
*
|
|
686
|
+
* Model-independent on purpose: `peakPricingState()` adds the membership test
|
|
687
|
+
* on top for the picker's label, while the price table selects peak rates from
|
|
688
|
+
* a row's own `peak` block — so a model whose catalog id spelling differs from
|
|
689
|
+
* the one in {@link KNOWN_PEAK_PRICING} still gets the right half of the day.
|
|
690
|
+
*/
|
|
691
|
+
function isPeakPricingHour(now = Date.now()) {
|
|
692
|
+
const at = new Date(now);
|
|
693
|
+
const day = at.getUTCDay();
|
|
694
|
+
if (day === 0 || day === 6) return false;
|
|
695
|
+
const hour = at.getUTCHours();
|
|
696
|
+
return PEAK_HOUR_RANGES.some(([start, end]) => hour >= start && hour < end);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Whether `now` (defaults to `Date.now()`) falls in a peak-pricing window for
|
|
700
|
+
* time-of-day-priced models. Peak rates apply Monday–Friday (UTC) only: the
|
|
701
|
+
* official rule charges Saturday and Sunday completely off-peak for all 24
|
|
702
|
+
* hours, so a weekend timestamp is off-peak even inside `PEAK_HOUR_RANGES`.
|
|
703
|
+
* `undefined` for models outside the snapshot.
|
|
704
|
+
*/
|
|
705
|
+
function peakPricingState(modelId, now = Date.now()) {
|
|
706
|
+
if (!KNOWN_PEAK_PRICING.has(modelId)) return void 0;
|
|
707
|
+
return isPeakPricingHour(now) ? "peak" : "off-peak";
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Compact label for the current peak/off-peak state: `Peak` (full price) or
|
|
711
|
+
* `Half` (off-peak, half price). These English nouns match the picker's other
|
|
712
|
+
* markers (`Go`, `Image`, `FREE`), and since they appear only on time-of-day
|
|
713
|
+
* priced models they double as a "priced by the hour" signal. Returns undefined
|
|
714
|
+
* for models without time-of-day pricing.
|
|
756
715
|
*/
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
max: 4
|
|
763
|
-
};
|
|
716
|
+
function peakPricingLabel(modelId, now = Date.now()) {
|
|
717
|
+
const state = peakPricingState(modelId, now);
|
|
718
|
+
if (state === void 0) return void 0;
|
|
719
|
+
return state === "peak" ? "Peak" : "Half";
|
|
720
|
+
}
|
|
764
721
|
/**
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
* of tier — they are usable by every account, so they are the best default
|
|
768
|
-
* candidates.
|
|
722
|
+
* Official display label for a model's minimum plan, or undefined for models
|
|
723
|
+
* outside the snapshot (e.g. future catalog additions).
|
|
769
724
|
*/
|
|
770
|
-
function
|
|
771
|
-
|
|
725
|
+
function planLabel(modelId) {
|
|
726
|
+
const plan = KNOWN_PLANS[modelId];
|
|
727
|
+
return plan === void 0 ? void 0 : PLAN_LABELS[plan];
|
|
772
728
|
}
|
|
773
729
|
/**
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
730
|
+
* The active deal label for a model, or undefined when the model has no deal
|
|
731
|
+
* or the deal has expired. Expiry is judged against `now` (defaults to
|
|
732
|
+
* `Date.now()`), so a snapshot that has gone stale stops showing its discount
|
|
733
|
+
* the moment the official end date passes — the user never believes a lapsed
|
|
734
|
+
* deal is still live. Permanent deals (no `expiresAt`) never lapse.
|
|
777
735
|
*/
|
|
778
|
-
function
|
|
779
|
-
const
|
|
780
|
-
if (
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (pa !== pb) return pa - pb;
|
|
784
|
-
const nameDiff = a.name.localeCompare(b.name);
|
|
785
|
-
if (nameDiff !== 0) return nameDiff;
|
|
786
|
-
return a.id.localeCompare(b.id);
|
|
736
|
+
function dealLabel(modelId, now = Date.now()) {
|
|
737
|
+
const deal = KNOWN_DEALS[modelId];
|
|
738
|
+
if (deal === void 0) return void 0;
|
|
739
|
+
if (deal.expiresAt !== void 0 && now >= Date.parse(deal.expiresAt)) return void 0;
|
|
740
|
+
return deal.label;
|
|
787
741
|
}
|
|
788
742
|
/**
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
743
|
+
* Compact human-readable context window, e.g. `1_000_000 -> "1M"`,
|
|
744
|
+
* `256_000 -> "256K"`, `262_144 -> "262K"` (floor to the nearest K).
|
|
745
|
+
* Returns undefined for unknown/absent sizes; values under 1K render raw.
|
|
746
|
+
*/
|
|
747
|
+
function formatContext(contextWindow) {
|
|
748
|
+
if (contextWindow === void 0 || !Number.isFinite(contextWindow) || contextWindow <= 0) return;
|
|
749
|
+
if (contextWindow >= 1e6) {
|
|
750
|
+
const m = contextWindow / 1e6;
|
|
751
|
+
const rounded = Math.round(m * 10) / 10;
|
|
752
|
+
return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}M`;
|
|
753
|
+
}
|
|
754
|
+
if (contextWindow < 1e3) return String(Math.floor(contextWindow));
|
|
755
|
+
return `${Math.floor(contextWindow / 1e3)}K`;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Compact one-line summary for the model picker: plan tier, then any active
|
|
759
|
+
* deal (discount or FREE), then the current peak/off-peak state (`Peak`/`Half`)
|
|
760
|
+
* for time-of-day-priced models, then `Image` for Vision-capable models, then
|
|
761
|
+
* the context window. Text-only models simply omit the Image marker — "Text
|
|
762
|
+
* only" adds nothing the picker needs to show.
|
|
763
|
+
*/
|
|
764
|
+
function capabilityDescription(modelId, contextWindow, now = Date.now()) {
|
|
765
|
+
const parts = [];
|
|
766
|
+
const plan = planLabel(modelId);
|
|
767
|
+
if (plan !== void 0) parts.push(plan);
|
|
768
|
+
const deal = dealLabel(modelId, now);
|
|
769
|
+
if (deal !== void 0) parts.push(deal);
|
|
770
|
+
const peak = peakPricingLabel(modelId, now);
|
|
771
|
+
if (peak !== void 0) parts.push(peak);
|
|
772
|
+
if (KNOWN_IMAGE_MODELS.has(modelId)) parts.push("Image");
|
|
773
|
+
const ctx = formatContext(contextWindow);
|
|
774
|
+
if (ctx !== void 0) parts.push(ctx);
|
|
775
|
+
return parts.join(" · ");
|
|
776
|
+
}
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region src/model-prices.ts
|
|
779
|
+
/**
|
|
780
|
+
* Every price row the page publishes, ordered by its own slug.
|
|
795
781
|
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
* (
|
|
782
|
+
* Note on the GPT rows: `gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini` and
|
|
783
|
+
* `gpt-5.5` carry a LITERAL `cacheWriteCost: 0` — a different fact from a
|
|
784
|
+
* missing key, because `ratesOf()` keeps it and the readout then charges
|
|
785
|
+
* cache writes at zero instead of reporting them unpriced. That is what the
|
|
786
|
+
* page's JSON says; its rendered column shows `—` for exactly these four while
|
|
787
|
+
* rendering a genuine zero as `$0.00` elsewhere, so the page contradicts
|
|
788
|
+
* itself. The table stays faithful to the machine-readable source and the
|
|
789
|
+
* discrepancy is recorded in AGENTS.md rather than papered over here.
|
|
799
790
|
*/
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
791
|
+
const MODEL_PRICE_ROWS = [
|
|
792
|
+
{
|
|
793
|
+
id: "claude-fable-5",
|
|
794
|
+
rates: [
|
|
795
|
+
10,
|
|
796
|
+
50,
|
|
797
|
+
1,
|
|
798
|
+
12.5
|
|
799
|
+
]
|
|
805
800
|
},
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
801
|
+
{
|
|
802
|
+
id: "claude-fable-5-1",
|
|
803
|
+
rates: [
|
|
804
|
+
10,
|
|
805
|
+
50,
|
|
806
|
+
.25,
|
|
807
|
+
12.5
|
|
808
|
+
]
|
|
810
809
|
},
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
810
|
+
{
|
|
811
|
+
id: "claude-haiku-4-5",
|
|
812
|
+
rates: [
|
|
813
|
+
1,
|
|
814
|
+
5,
|
|
815
|
+
.1,
|
|
816
|
+
1.25
|
|
817
|
+
]
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
id: "claude-opus-4-6",
|
|
821
|
+
rates: [
|
|
822
|
+
5,
|
|
823
|
+
25,
|
|
824
|
+
.5,
|
|
825
|
+
6.25
|
|
826
|
+
]
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
id: "claude-opus-4-7",
|
|
830
|
+
rates: [
|
|
831
|
+
5,
|
|
832
|
+
25,
|
|
833
|
+
.5,
|
|
834
|
+
6.25
|
|
835
|
+
]
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
id: "claude-opus-4-8",
|
|
839
|
+
rates: [
|
|
840
|
+
5,
|
|
841
|
+
25,
|
|
842
|
+
.5,
|
|
843
|
+
6.25
|
|
844
|
+
]
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
id: "claude-opus-5",
|
|
848
|
+
rates: [
|
|
849
|
+
5,
|
|
850
|
+
25,
|
|
851
|
+
.5,
|
|
852
|
+
6.25
|
|
853
|
+
]
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
id: "claude-sonnet-4-6",
|
|
857
|
+
rates: [
|
|
858
|
+
3,
|
|
859
|
+
15,
|
|
860
|
+
.3,
|
|
861
|
+
3.75
|
|
862
|
+
]
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
id: "claude-sonnet-5",
|
|
866
|
+
rates: [
|
|
867
|
+
2,
|
|
868
|
+
10,
|
|
869
|
+
.2,
|
|
870
|
+
2.5
|
|
871
|
+
]
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
id: "deepseek-v4-flash",
|
|
875
|
+
rates: [
|
|
876
|
+
.15,
|
|
877
|
+
.6,
|
|
878
|
+
.003
|
|
879
|
+
],
|
|
880
|
+
peak: [
|
|
881
|
+
.3,
|
|
882
|
+
1.2,
|
|
883
|
+
.006
|
|
884
|
+
]
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
id: "deepseek-v4-flash-fast",
|
|
888
|
+
rates: [
|
|
889
|
+
.28,
|
|
890
|
+
.56,
|
|
891
|
+
.07
|
|
892
|
+
]
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
id: "deepseek-v4-flash-vision-exp",
|
|
896
|
+
rates: [
|
|
897
|
+
.15,
|
|
898
|
+
.6,
|
|
899
|
+
.003
|
|
900
|
+
],
|
|
901
|
+
peak: [
|
|
902
|
+
.3,
|
|
903
|
+
1.2,
|
|
904
|
+
.006
|
|
905
|
+
]
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
id: "deepseek-v4-pro",
|
|
909
|
+
rates: [
|
|
910
|
+
.66,
|
|
911
|
+
1.98,
|
|
912
|
+
.022
|
|
913
|
+
],
|
|
914
|
+
peak: [
|
|
915
|
+
1.32,
|
|
916
|
+
3.96,
|
|
917
|
+
.044
|
|
918
|
+
]
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
id: "deepseek-v4.1-flash",
|
|
922
|
+
rates: [
|
|
923
|
+
.15,
|
|
924
|
+
.6,
|
|
925
|
+
.003
|
|
926
|
+
],
|
|
927
|
+
peak: [
|
|
928
|
+
.3,
|
|
929
|
+
1.2,
|
|
930
|
+
.006
|
|
931
|
+
]
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
id: "fugu-ultra",
|
|
935
|
+
rates: [
|
|
936
|
+
5,
|
|
937
|
+
30,
|
|
938
|
+
.5
|
|
939
|
+
]
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
id: "gemini-3.1-flash-lite",
|
|
943
|
+
rates: [
|
|
944
|
+
.25,
|
|
945
|
+
1.5,
|
|
946
|
+
.03
|
|
947
|
+
]
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
id: "gemini-3.5-flash",
|
|
951
|
+
rates: [
|
|
952
|
+
1.5,
|
|
953
|
+
9,
|
|
954
|
+
.15
|
|
955
|
+
]
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
id: "gemini-3.5-flash-lite",
|
|
959
|
+
rates: [
|
|
960
|
+
.3,
|
|
961
|
+
2.5,
|
|
962
|
+
.03
|
|
963
|
+
]
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
id: "gemini-3.6-flash",
|
|
967
|
+
rates: [
|
|
968
|
+
1.5,
|
|
969
|
+
7.5,
|
|
970
|
+
.15
|
|
971
|
+
]
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
id: "gemini-3.7-flash",
|
|
975
|
+
rates: [
|
|
976
|
+
1.5,
|
|
977
|
+
7.5,
|
|
978
|
+
.15,
|
|
979
|
+
.08334
|
|
980
|
+
]
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
id: "gemini-3.8-flash",
|
|
984
|
+
rates: [
|
|
985
|
+
1.5,
|
|
986
|
+
7.5,
|
|
987
|
+
.15
|
|
988
|
+
]
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
id: "glm-5",
|
|
992
|
+
rates: [
|
|
993
|
+
1,
|
|
994
|
+
3.2,
|
|
995
|
+
.2
|
|
996
|
+
]
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
id: "glm-5.1",
|
|
1000
|
+
rates: [
|
|
1001
|
+
1.4,
|
|
1002
|
+
4.4,
|
|
1003
|
+
.26
|
|
1004
|
+
]
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
id: "glm-5.2",
|
|
1008
|
+
rates: [
|
|
1009
|
+
1.4,
|
|
1010
|
+
4.4,
|
|
1011
|
+
.26
|
|
1012
|
+
]
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
id: "glm-5.2-fast",
|
|
1016
|
+
rates: [
|
|
1017
|
+
3,
|
|
1018
|
+
10.25,
|
|
1019
|
+
.5
|
|
1020
|
+
]
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
id: "glm-5.3",
|
|
1024
|
+
rates: [
|
|
1025
|
+
1.4,
|
|
1026
|
+
4.4,
|
|
1027
|
+
.26
|
|
1028
|
+
]
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
id: "glm-5.3-flash",
|
|
1032
|
+
rates: [
|
|
1033
|
+
.15,
|
|
1034
|
+
.5,
|
|
1035
|
+
.03
|
|
1036
|
+
]
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
id: "gpt-5.3-codex",
|
|
1040
|
+
rates: [
|
|
1041
|
+
2,
|
|
1042
|
+
8,
|
|
1043
|
+
.5,
|
|
1044
|
+
0
|
|
1045
|
+
]
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
id: "gpt-5.4",
|
|
1049
|
+
rates: [
|
|
1050
|
+
2.5,
|
|
1051
|
+
15,
|
|
1052
|
+
.25,
|
|
1053
|
+
0
|
|
1054
|
+
]
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
id: "gpt-5.4-mini",
|
|
1058
|
+
rates: [
|
|
1059
|
+
.75,
|
|
1060
|
+
4.5,
|
|
1061
|
+
.075,
|
|
1062
|
+
0
|
|
1063
|
+
]
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
id: "gpt-5.5",
|
|
1067
|
+
rates: [
|
|
1068
|
+
5,
|
|
1069
|
+
30,
|
|
1070
|
+
.5,
|
|
1071
|
+
0
|
|
1072
|
+
]
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
id: "gpt-5.6-luna",
|
|
1076
|
+
rates: [
|
|
1077
|
+
.2,
|
|
1078
|
+
1.2,
|
|
1079
|
+
.02,
|
|
1080
|
+
.25
|
|
1081
|
+
],
|
|
1082
|
+
contextTiers: [{
|
|
1083
|
+
"maxContext": 272e3,
|
|
1084
|
+
"rates": [
|
|
1085
|
+
.2,
|
|
1086
|
+
1.2,
|
|
1087
|
+
.02,
|
|
1088
|
+
.25
|
|
1089
|
+
]
|
|
1090
|
+
}, { "rates": [
|
|
1091
|
+
.4,
|
|
1092
|
+
1.8,
|
|
1093
|
+
.04,
|
|
1094
|
+
.5
|
|
1095
|
+
] }]
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
id: "gpt-5.6-sol",
|
|
1099
|
+
rates: [
|
|
1100
|
+
5,
|
|
1101
|
+
30,
|
|
1102
|
+
.5,
|
|
1103
|
+
6.25
|
|
1104
|
+
],
|
|
1105
|
+
contextTiers: [{
|
|
1106
|
+
"maxContext": 272e3,
|
|
1107
|
+
"rates": [
|
|
1108
|
+
5,
|
|
1109
|
+
30,
|
|
1110
|
+
.5,
|
|
1111
|
+
6.25
|
|
1112
|
+
]
|
|
1113
|
+
}, { "rates": [
|
|
1114
|
+
10,
|
|
1115
|
+
45,
|
|
1116
|
+
1,
|
|
1117
|
+
12.5
|
|
1118
|
+
] }]
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
id: "gpt-5.6-terra",
|
|
1122
|
+
rates: [
|
|
1123
|
+
2,
|
|
1124
|
+
12,
|
|
1125
|
+
.2,
|
|
1126
|
+
2.5
|
|
1127
|
+
],
|
|
1128
|
+
contextTiers: [{
|
|
1129
|
+
"maxContext": 272e3,
|
|
1130
|
+
"rates": [
|
|
1131
|
+
2,
|
|
1132
|
+
12,
|
|
1133
|
+
.2,
|
|
1134
|
+
2.5
|
|
1135
|
+
]
|
|
1136
|
+
}, { "rates": [
|
|
1137
|
+
4,
|
|
1138
|
+
18,
|
|
1139
|
+
.4,
|
|
1140
|
+
5
|
|
1141
|
+
] }]
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
id: "gpt-6-astra",
|
|
1145
|
+
rates: [
|
|
1146
|
+
10,
|
|
1147
|
+
50,
|
|
1148
|
+
1,
|
|
1149
|
+
12.5
|
|
1150
|
+
],
|
|
1151
|
+
contextTiers: [{
|
|
1152
|
+
"maxContext": 272e3,
|
|
1153
|
+
"rates": [
|
|
1154
|
+
10,
|
|
1155
|
+
50,
|
|
1156
|
+
1,
|
|
1157
|
+
12.5
|
|
1158
|
+
]
|
|
1159
|
+
}, { "rates": [
|
|
1160
|
+
20,
|
|
1161
|
+
75,
|
|
1162
|
+
2,
|
|
1163
|
+
25
|
|
1164
|
+
] }]
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
id: "grok-4.5",
|
|
1168
|
+
rates: [
|
|
1169
|
+
2,
|
|
1170
|
+
6,
|
|
1171
|
+
.5
|
|
1172
|
+
]
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
id: "grok-4.6",
|
|
1176
|
+
rates: [
|
|
1177
|
+
2,
|
|
1178
|
+
6,
|
|
1179
|
+
.5
|
|
1180
|
+
],
|
|
1181
|
+
contextTiers: [{
|
|
1182
|
+
"maxContext": 2e5,
|
|
1183
|
+
"rates": [
|
|
1184
|
+
2,
|
|
1185
|
+
6,
|
|
1186
|
+
.5
|
|
1187
|
+
]
|
|
1188
|
+
}, { "rates": [
|
|
1189
|
+
4,
|
|
1190
|
+
12,
|
|
1191
|
+
1
|
|
1192
|
+
] }]
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
id: "inkling",
|
|
1196
|
+
rates: [
|
|
1197
|
+
1,
|
|
1198
|
+
4.05,
|
|
1199
|
+
.17
|
|
1200
|
+
]
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
id: "inkling-small",
|
|
1204
|
+
rates: [
|
|
1205
|
+
.5,
|
|
1206
|
+
1.2,
|
|
1207
|
+
.1
|
|
1208
|
+
]
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
id: "kimi-k2.5",
|
|
1212
|
+
rates: [
|
|
1213
|
+
.6,
|
|
1214
|
+
3,
|
|
1215
|
+
.1
|
|
1216
|
+
]
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
id: "kimi-k2.6",
|
|
1220
|
+
rates: [
|
|
1221
|
+
.95,
|
|
1222
|
+
4,
|
|
1223
|
+
.16
|
|
1224
|
+
]
|
|
1225
|
+
},
|
|
1226
|
+
{
|
|
1227
|
+
id: "kimi-k2.7-code",
|
|
1228
|
+
rates: [
|
|
1229
|
+
.95,
|
|
1230
|
+
4,
|
|
1231
|
+
.19
|
|
1232
|
+
]
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
id: "kimi-k2.7-code-highspeed",
|
|
1236
|
+
rates: [
|
|
1237
|
+
1.9,
|
|
1238
|
+
8,
|
|
1239
|
+
.38
|
|
1240
|
+
]
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
id: "kimi-k3",
|
|
1244
|
+
rates: [
|
|
1245
|
+
3,
|
|
1246
|
+
15,
|
|
1247
|
+
.3
|
|
1248
|
+
]
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
id: "mimo-v2.5",
|
|
1252
|
+
rates: [
|
|
1253
|
+
.14,
|
|
1254
|
+
.28,
|
|
1255
|
+
.0028
|
|
1256
|
+
]
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
id: "mimo-v2.5-pro",
|
|
1260
|
+
rates: [
|
|
1261
|
+
.435,
|
|
1262
|
+
.87,
|
|
1263
|
+
.0036
|
|
1264
|
+
]
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
id: "minimax-m2.5",
|
|
1268
|
+
rates: [
|
|
1269
|
+
.3,
|
|
1270
|
+
1.2,
|
|
1271
|
+
.03
|
|
1272
|
+
]
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
id: "minimax-m2.7",
|
|
1276
|
+
rates: [
|
|
1277
|
+
.3,
|
|
1278
|
+
1.2,
|
|
1279
|
+
.06
|
|
1280
|
+
]
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
id: "minimax-m3",
|
|
1284
|
+
rates: [
|
|
1285
|
+
.3,
|
|
1286
|
+
1.2,
|
|
1287
|
+
.06
|
|
1288
|
+
]
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
id: "muse-spark-1.1",
|
|
1292
|
+
rates: [
|
|
1293
|
+
1.25,
|
|
1294
|
+
4.25,
|
|
1295
|
+
.15
|
|
1296
|
+
]
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
id: "muse-spark-1.2",
|
|
1300
|
+
rates: [
|
|
1301
|
+
1.25,
|
|
1302
|
+
4.25,
|
|
1303
|
+
.15
|
|
1304
|
+
]
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
id: "muse-spark-1.2-contributor",
|
|
1308
|
+
rates: [
|
|
1309
|
+
.1,
|
|
1310
|
+
.2,
|
|
1311
|
+
.002
|
|
1312
|
+
]
|
|
1313
|
+
},
|
|
1314
|
+
{
|
|
1315
|
+
id: "muse-spark-1.3",
|
|
1316
|
+
rates: [
|
|
1317
|
+
1.25,
|
|
1318
|
+
4.25,
|
|
1319
|
+
.15
|
|
1320
|
+
]
|
|
1321
|
+
},
|
|
1322
|
+
{
|
|
1323
|
+
id: "muse-spark-1.3-contributor",
|
|
1324
|
+
rates: [
|
|
1325
|
+
.1,
|
|
1326
|
+
.2,
|
|
1327
|
+
.002
|
|
1328
|
+
]
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
id: "nemotron-3-ultra",
|
|
1332
|
+
rates: [
|
|
1333
|
+
.6,
|
|
1334
|
+
2.4,
|
|
1335
|
+
.12
|
|
1336
|
+
]
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
id: "qwen-3.6-max",
|
|
1340
|
+
rates: [
|
|
1341
|
+
1.3,
|
|
1342
|
+
7.8,
|
|
1343
|
+
.26,
|
|
1344
|
+
1.63
|
|
1345
|
+
]
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
id: "qwen-3.6-plus",
|
|
1349
|
+
rates: [
|
|
1350
|
+
.5,
|
|
1351
|
+
3,
|
|
1352
|
+
.1
|
|
1353
|
+
],
|
|
1354
|
+
contextTiers: [{
|
|
1355
|
+
"maxContext": 256e3,
|
|
1356
|
+
"rates": [
|
|
1357
|
+
.5,
|
|
1358
|
+
3,
|
|
1359
|
+
.1
|
|
1360
|
+
]
|
|
1361
|
+
}, { "rates": [
|
|
1362
|
+
2,
|
|
1363
|
+
6,
|
|
1364
|
+
.2
|
|
1365
|
+
] }]
|
|
1366
|
+
},
|
|
1367
|
+
{
|
|
1368
|
+
id: "qwen-3.7-flash",
|
|
1369
|
+
rates: [
|
|
1370
|
+
.03,
|
|
1371
|
+
.13,
|
|
1372
|
+
.006,
|
|
1373
|
+
.038
|
|
1374
|
+
],
|
|
1375
|
+
contextTiers: [
|
|
1376
|
+
{
|
|
1377
|
+
"maxContext": 32e3,
|
|
1378
|
+
"rates": [
|
|
1379
|
+
.03,
|
|
1380
|
+
.13,
|
|
1381
|
+
.006,
|
|
1382
|
+
.038
|
|
1383
|
+
]
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
"maxContext": 256e3,
|
|
1387
|
+
"rates": [
|
|
1388
|
+
.1,
|
|
1389
|
+
.4,
|
|
1390
|
+
.02,
|
|
1391
|
+
.125
|
|
1392
|
+
]
|
|
1393
|
+
},
|
|
1394
|
+
{ "rates": [
|
|
1395
|
+
.2,
|
|
1396
|
+
.8,
|
|
1397
|
+
.04,
|
|
1398
|
+
.25
|
|
1399
|
+
] }
|
|
1400
|
+
]
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
id: "qwen-3.7-max",
|
|
1404
|
+
rates: [
|
|
1405
|
+
2.5,
|
|
1406
|
+
7.5,
|
|
1407
|
+
.5,
|
|
1408
|
+
3.13
|
|
1409
|
+
]
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
id: "qwen-3.7-plus",
|
|
1413
|
+
rates: [
|
|
1414
|
+
.4,
|
|
1415
|
+
1.6,
|
|
1416
|
+
.08,
|
|
1417
|
+
.5
|
|
1418
|
+
],
|
|
1419
|
+
contextTiers: [{
|
|
1420
|
+
"maxContext": 256e3,
|
|
1421
|
+
"rates": [
|
|
1422
|
+
.4,
|
|
1423
|
+
1.6,
|
|
1424
|
+
.08,
|
|
1425
|
+
.5
|
|
1426
|
+
]
|
|
1427
|
+
}, { "rates": [
|
|
1428
|
+
1.2,
|
|
1429
|
+
4.8,
|
|
1430
|
+
.24,
|
|
1431
|
+
1.5
|
|
1432
|
+
] }]
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
id: "qwen-3.8-27b",
|
|
1436
|
+
rates: [
|
|
1437
|
+
.4,
|
|
1438
|
+
3,
|
|
1439
|
+
.04
|
|
1440
|
+
]
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
id: "qwen-3.8-flash",
|
|
1444
|
+
rates: [
|
|
1445
|
+
.16,
|
|
1446
|
+
.47,
|
|
1447
|
+
.016
|
|
1448
|
+
]
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
id: "qwen-3.8-max",
|
|
1452
|
+
rates: [
|
|
1453
|
+
2,
|
|
1454
|
+
6,
|
|
1455
|
+
.25,
|
|
1456
|
+
2.5
|
|
1457
|
+
]
|
|
815
1458
|
},
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1459
|
+
{
|
|
1460
|
+
id: "qwen-3.8-max-0902",
|
|
1461
|
+
rates: [
|
|
1462
|
+
2,
|
|
1463
|
+
6,
|
|
1464
|
+
.25
|
|
1465
|
+
]
|
|
820
1466
|
},
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1467
|
+
{
|
|
1468
|
+
id: "step-3.5-flash",
|
|
1469
|
+
rates: [
|
|
1470
|
+
.1,
|
|
1471
|
+
.3,
|
|
1472
|
+
.02
|
|
1473
|
+
]
|
|
825
1474
|
},
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1475
|
+
{
|
|
1476
|
+
id: "step-3.7-flash",
|
|
1477
|
+
rates: [
|
|
1478
|
+
.2,
|
|
1479
|
+
1.15,
|
|
1480
|
+
.04
|
|
1481
|
+
]
|
|
830
1482
|
},
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1483
|
+
{
|
|
1484
|
+
id: "tencent/hy3-paid",
|
|
1485
|
+
rates: [
|
|
1486
|
+
.14,
|
|
1487
|
+
.58,
|
|
1488
|
+
.035
|
|
1489
|
+
]
|
|
835
1490
|
},
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1491
|
+
{
|
|
1492
|
+
id: "tencent/hy4-preview",
|
|
1493
|
+
rates: [
|
|
1494
|
+
.834,
|
|
1495
|
+
2.501,
|
|
1496
|
+
.042
|
|
1497
|
+
]
|
|
840
1498
|
}
|
|
841
|
-
|
|
842
|
-
/** Plan-id prefixes, longest first — the CLI's prefix-match order. */
|
|
843
|
-
const SUBSCRIPTION_PLAN_PREFIXES = Object.keys(KNOWN_SUBSCRIPTION_PLANS).sort((a, b) => b.length - a.length);
|
|
1499
|
+
];
|
|
844
1500
|
/**
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
* `undefined`.
|
|
1501
|
+
* Catalog ids no candidate rule can reach, because the page names the model
|
|
1502
|
+
* differently from the catalog. `tests/model-prices.test.ts` fails whenever a
|
|
1503
|
+
* catalog model has no price, so a new miss lands here as a visible decision
|
|
1504
|
+
* rather than a silently unpriced model.
|
|
850
1505
|
*/
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1506
|
+
const PRICE_SLUG_OVERRIDES = { "nvidia/nemotron-3-ultra-550b-a55b": "nemotron-3-ultra" };
|
|
1507
|
+
/**
|
|
1508
|
+
* Plausible price slugs for one catalog model id, most specific first.
|
|
1509
|
+
*
|
|
1510
|
+
* The page normalizes to lowercase and usually drops the vendor segment, but
|
|
1511
|
+
* not consistently: `tencent/hy4-preview` keeps its prefix while
|
|
1512
|
+
* `Qwen/Qwen3.8-Max-0902` becomes `qwen-3.8-max-0902`, with a hyphen the
|
|
1513
|
+
* catalog id does not have. Generating candidates and taking the first that
|
|
1514
|
+
* exists in the vendored table absorbs that drift without a hand-maintained
|
|
1515
|
+
* map of seventy ids.
|
|
1516
|
+
*/
|
|
1517
|
+
function priceSlugCandidates(modelId) {
|
|
1518
|
+
const lower = modelId.toLowerCase();
|
|
1519
|
+
const bare = lower.includes("/") ? lower.slice(lower.indexOf("/") + 1) : lower;
|
|
1520
|
+
const out = /* @__PURE__ */ new Set();
|
|
1521
|
+
const add = (slug) => {
|
|
1522
|
+
const hyphenated = slug.replace(/^([a-z]+)(\d)/, "$1-$2");
|
|
1523
|
+
out.add(slug);
|
|
1524
|
+
out.add(hyphenated);
|
|
1525
|
+
out.add(slug.replace(/-\d{8}$/, ""));
|
|
1526
|
+
out.add(slug.replace(/-(preview|latest)$/, ""));
|
|
1527
|
+
out.add(hyphenated.replace(/-\d{8}$/, ""));
|
|
1528
|
+
out.add(hyphenated.replace(/-(preview|latest)$/, ""));
|
|
1529
|
+
};
|
|
1530
|
+
add(lower);
|
|
1531
|
+
add(bare);
|
|
1532
|
+
return [...out];
|
|
1533
|
+
}
|
|
1534
|
+
/** The pricing-page slug for one catalog model id, or undefined when unpriced. */
|
|
1535
|
+
function priceSlugFor(modelId, known) {
|
|
1536
|
+
const override = PRICE_SLUG_OVERRIDES[modelId];
|
|
1537
|
+
if (override !== void 0) return known.has(override) ? override : void 0;
|
|
1538
|
+
return priceSlugCandidates(modelId).find((slug) => known.has(slug));
|
|
1539
|
+
}
|
|
1540
|
+
/** Split a stored triplet/quadruplet into the wire rate shape. */
|
|
1541
|
+
function ratesOf(values) {
|
|
1542
|
+
const rates = {
|
|
1543
|
+
inputCost: values[0],
|
|
1544
|
+
outputCost: values[1],
|
|
1545
|
+
cacheReadCost: values[2]
|
|
1546
|
+
};
|
|
1547
|
+
if (values[3] !== void 0) rates.cacheWriteCost = values[3];
|
|
1548
|
+
return rates;
|
|
1549
|
+
}
|
|
1550
|
+
/** Build the full price row the browser prices a session with. */
|
|
1551
|
+
function wireRow(id, slug, row) {
|
|
1552
|
+
const price = {
|
|
1553
|
+
id,
|
|
1554
|
+
slug,
|
|
1555
|
+
...ratesOf(row.rates)
|
|
1556
|
+
};
|
|
1557
|
+
if (row.peak !== void 0) price.peak = ratesOf(row.peak);
|
|
1558
|
+
if (row.contextTiers !== void 0) price.contextTiers = row.contextTiers.map((tier) => ({
|
|
1559
|
+
...ratesOf(tier.rates),
|
|
1560
|
+
...tier.maxContext === void 0 ? {} : { maxContext: tier.maxContext }
|
|
1561
|
+
}));
|
|
1562
|
+
return price;
|
|
855
1563
|
}
|
|
856
1564
|
/**
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
*
|
|
1565
|
+
* Build the table the composer prices a session with.
|
|
1566
|
+
*
|
|
1567
|
+
* Rows are keyed by CATALOG id wherever the two namespaces reconcile, because
|
|
1568
|
+
* that is what a session reports, and every row also carries its page slug as a
|
|
1569
|
+
* second lookup key. Price rows no catalog model claims are served under the
|
|
1570
|
+
* slug alone, so drift in either direction still prices: a page rename the
|
|
1571
|
+
* catalog has not followed, or a model the catalog snapshot has not learned
|
|
1572
|
+
* yet. Free models are served explicitly at zero so the composer can say so
|
|
1573
|
+
* instead of showing nothing.
|
|
1574
|
+
*
|
|
1575
|
+
* The peak windows travel with the table, so the browser applies the very
|
|
1576
|
+
* schedule this snapshot knows instead of restating it.
|
|
862
1577
|
*/
|
|
863
|
-
function
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1578
|
+
function modelPriceTable() {
|
|
1579
|
+
const bySlug = new Map(MODEL_PRICE_ROWS.map((row) => [row.id, row]));
|
|
1580
|
+
const known = new Set(bySlug.keys());
|
|
1581
|
+
const models = [];
|
|
1582
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1583
|
+
for (const catalogId of Object.keys(KNOWN_PLANS)) {
|
|
1584
|
+
if (isFreeModel(catalogId) || catalogId.endsWith(":free")) {
|
|
1585
|
+
models.push({
|
|
1586
|
+
id: catalogId,
|
|
1587
|
+
slug: catalogId,
|
|
1588
|
+
inputCost: 0,
|
|
1589
|
+
outputCost: 0,
|
|
1590
|
+
cacheReadCost: 0,
|
|
1591
|
+
free: true
|
|
1592
|
+
});
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
const slug = priceSlugFor(catalogId, known);
|
|
1596
|
+
if (slug === void 0) continue;
|
|
1597
|
+
const row = bySlug.get(slug);
|
|
1598
|
+
if (row === void 0) continue;
|
|
1599
|
+
claimed.add(slug);
|
|
1600
|
+
models.push(wireRow(catalogId, slug, row));
|
|
1601
|
+
}
|
|
1602
|
+
for (const row of MODEL_PRICE_ROWS) {
|
|
1603
|
+
if (claimed.has(row.id)) continue;
|
|
1604
|
+
models.push(wireRow(row.id, row.id, row));
|
|
1605
|
+
}
|
|
1606
|
+
return {
|
|
1607
|
+
models,
|
|
1608
|
+
peakHours: PEAK_HOUR_RANGES.map(([start, end]) => [start, end])
|
|
1609
|
+
};
|
|
872
1610
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
"
|
|
886
|
-
|
|
887
|
-
|
|
1611
|
+
//#endregion
|
|
1612
|
+
//#region src/cost-facts.ts
|
|
1613
|
+
const zeroCostTokens = () => ({
|
|
1614
|
+
uncachedInputTokens: 0,
|
|
1615
|
+
outputTokens: 0,
|
|
1616
|
+
cacheReadTokens: 0,
|
|
1617
|
+
cacheWriteTokens: 0
|
|
1618
|
+
});
|
|
1619
|
+
const COST_TOKEN_KEYS = [
|
|
1620
|
+
"uncachedInputTokens",
|
|
1621
|
+
"outputTokens",
|
|
1622
|
+
"cacheReadTokens",
|
|
1623
|
+
"cacheWriteTokens"
|
|
1624
|
+
];
|
|
1625
|
+
function peakHour(at, windows) {
|
|
1626
|
+
const time = new Date(at);
|
|
1627
|
+
return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* All prompt billing buckets determine a request's band, never cumulative session input.
|
|
1631
|
+
*
|
|
1632
|
+
* A tiered model returns the matching band and does NOT then apply `peak`: the
|
|
1633
|
+
* page publishes those two dimensions independently, no row carries both today,
|
|
1634
|
+
* and the generator would report a tiered row on every sync — so the combination
|
|
1635
|
+
* becoming real is visible before it reaches a user. This ordering is a latent
|
|
1636
|
+
* choice, not a verified upstream rule.
|
|
1637
|
+
*/
|
|
1638
|
+
function requestRates(price, at, contextTokens, table) {
|
|
1639
|
+
const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
|
|
1640
|
+
if (tier !== void 0) return tier;
|
|
1641
|
+
return price.peak !== void 0 && at !== null && peakHour(at, table.peakHours) ? price.peak : price;
|
|
1642
|
+
}
|
|
1643
|
+
/** Cache version and wire guard: changing bands/rates must refold historical groups. */
|
|
1644
|
+
function pricingKey(table) {
|
|
1645
|
+
const rates = (r) => [
|
|
1646
|
+
r.inputCost,
|
|
1647
|
+
r.outputCost,
|
|
1648
|
+
r.cacheReadCost,
|
|
1649
|
+
r.cacheWriteCost ?? null
|
|
1650
|
+
];
|
|
1651
|
+
const canonical = JSON.stringify([
|
|
1652
|
+
"commandCodeCost-v1",
|
|
1653
|
+
table.peakHours,
|
|
1654
|
+
table.models.map((p) => [
|
|
1655
|
+
p.id,
|
|
1656
|
+
p.slug,
|
|
1657
|
+
p.free === true,
|
|
1658
|
+
rates(p),
|
|
1659
|
+
p.peak ? rates(p.peak) : null,
|
|
1660
|
+
p.contextTiers?.map((t) => [t.maxContext ?? null, rates(t)]) ?? null
|
|
1661
|
+
])
|
|
1662
|
+
]);
|
|
1663
|
+
let hash = 2166136261;
|
|
1664
|
+
for (const c of canonical) hash = Math.imul(hash ^ c.charCodeAt(0), 16777619) >>> 0;
|
|
1665
|
+
return hash;
|
|
1666
|
+
}
|
|
1667
|
+
/** Only safe to merge requests whose billing classification is identical. */
|
|
1668
|
+
function costGroupKey(group, table) {
|
|
1669
|
+
const price = table.models.find((p) => p.id === group.model || p.slug === group.model);
|
|
1670
|
+
return JSON.stringify([
|
|
1671
|
+
group.provider,
|
|
1672
|
+
group.model,
|
|
1673
|
+
price === void 0 ? null : requestRates(price, group.at, group.contextTokens, table),
|
|
1674
|
+
group.at === null
|
|
1675
|
+
]);
|
|
1676
|
+
}
|
|
1677
|
+
//#endregion
|
|
1678
|
+
//#region src/cost-projection.ts
|
|
1679
|
+
const record$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
1680
|
+
const finite = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
1681
|
+
function tokensOf(value) {
|
|
1682
|
+
const u = record$2(value);
|
|
1683
|
+
if (!u || !finite(u.inputTokens) || !finite(u.outputTokens)) return void 0;
|
|
1684
|
+
if (u.cacheReadTokens !== void 0 && !finite(u.cacheReadTokens) || u.cacheWriteTokens !== void 0 && !finite(u.cacheWriteTokens)) return void 0;
|
|
1685
|
+
return {
|
|
1686
|
+
uncachedInputTokens: u.inputTokens,
|
|
1687
|
+
outputTokens: u.outputTokens,
|
|
1688
|
+
cacheReadTokens: u.cacheReadTokens ?? 0,
|
|
1689
|
+
cacheWriteTokens: u.cacheWriteTokens ?? 0
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
/** Current v2 settlements store the last usage in their stream; v1 stores chunk events. */
|
|
1693
|
+
function usageOf(type, data) {
|
|
1694
|
+
if (type === "assistant/chunk") {
|
|
1695
|
+
const chunk = record$2(data.chunk);
|
|
1696
|
+
return chunk?.type === "usage" ? tokensOf(chunk.usage) : void 0;
|
|
888
1697
|
}
|
|
889
|
-
|
|
1698
|
+
if (type !== "assistant/message" && type !== "assistant/attempt") return void 0;
|
|
1699
|
+
const direct = tokensOf(data.usage);
|
|
1700
|
+
if (direct !== void 0) return direct;
|
|
1701
|
+
if (!Array.isArray(data.stream)) return void 0;
|
|
1702
|
+
for (let i = data.stream.length - 1; i >= 0; i--) {
|
|
1703
|
+
const item = record$2(data.stream[i]);
|
|
1704
|
+
const chunk = item?.type === "chunk" ? record$2(item.chunk) : void 0;
|
|
1705
|
+
if (chunk?.type === "usage") return tokensOf(chunk.usage);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
function parseState(value) {
|
|
1709
|
+
const s = record$2(value), facts = record$2(s?.facts);
|
|
1710
|
+
if (!s || !facts || !finite(facts.pricingKey) || !Array.isArray(facts.groups)) throw new TypeError("invalid commandCodeCost state");
|
|
1711
|
+
const group = (v) => {
|
|
1712
|
+
const g = record$2(v), t = record$2(g?.tokens);
|
|
1713
|
+
return !!g && typeof g.provider === "string" && typeof g.model === "string" && (g.at === null || finite(g.at)) && finite(g.contextTokens) && !!t && COST_TOKEN_KEYS.every((k) => finite(t[k]));
|
|
1714
|
+
};
|
|
1715
|
+
const sel = record$2(s.selection), last = record$2(s.last);
|
|
1716
|
+
if (!(s.selection === null || sel && typeof sel.provider === "string" && typeof sel.model === "string") || !(s.at === null || finite(s.at)) || !facts.groups.every(group) || !(s.last === null || last && finite(last.turn) && finite(last.step) && group(last.group))) throw new TypeError("invalid commandCodeCost state");
|
|
1717
|
+
return value;
|
|
1718
|
+
}
|
|
1719
|
+
/** Optional registry uses only synchronous parse/view; no new runtime module is required. */
|
|
1720
|
+
function createCostProjection(table = modelPriceTable()) {
|
|
1721
|
+
const version = pricingKey(table);
|
|
1722
|
+
return {
|
|
1723
|
+
key: "commandCodeCost",
|
|
1724
|
+
stateVersion: version,
|
|
1725
|
+
stateSchema: { parse: parseState },
|
|
1726
|
+
init: () => ({
|
|
1727
|
+
selection: null,
|
|
1728
|
+
at: null,
|
|
1729
|
+
last: null,
|
|
1730
|
+
facts: {
|
|
1731
|
+
pricingKey: version,
|
|
1732
|
+
groups: []
|
|
1733
|
+
}
|
|
1734
|
+
}),
|
|
1735
|
+
apply(state, event) {
|
|
1736
|
+
const data = record$2(event.data);
|
|
1737
|
+
if (!data) return state;
|
|
1738
|
+
if (event.type === "request/header") {
|
|
1739
|
+
const config = record$2(record$2(data.header)?.config);
|
|
1740
|
+
return {
|
|
1741
|
+
...state,
|
|
1742
|
+
selection: config && typeof config.provider === "string" && typeof config.model === "string" ? {
|
|
1743
|
+
provider: config.provider,
|
|
1744
|
+
model: config.model
|
|
1745
|
+
} : null
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
if (event.type === "step/start" || event.type === "llm/retry-started") return {
|
|
1749
|
+
...state,
|
|
1750
|
+
at: finite(event.time) ? event.time : null,
|
|
1751
|
+
last: null
|
|
1752
|
+
};
|
|
1753
|
+
const tokens = usageOf(event.type, data);
|
|
1754
|
+
if (tokens === void 0 || !finite(data.turn) || !finite(data.step)) return state;
|
|
1755
|
+
const previous = state.last?.turn === data.turn && state.last.step === data.step ? state.last.group : void 0;
|
|
1756
|
+
const group = {
|
|
1757
|
+
provider: state.selection?.provider ?? "",
|
|
1758
|
+
model: state.selection?.model ?? "",
|
|
1759
|
+
at: state.at,
|
|
1760
|
+
contextTokens: tokens.uncachedInputTokens + tokens.cacheReadTokens + tokens.cacheWriteTokens,
|
|
1761
|
+
tokens
|
|
1762
|
+
};
|
|
1763
|
+
if (previous && JSON.stringify(previous) === JSON.stringify(group)) return state;
|
|
1764
|
+
const groups = state.facts.groups.map((g) => ({
|
|
1765
|
+
...g,
|
|
1766
|
+
tokens: { ...g.tokens }
|
|
1767
|
+
}));
|
|
1768
|
+
if (previous) {
|
|
1769
|
+
const old = groups.find((g) => costGroupKey(g, table) === costGroupKey(previous, table));
|
|
1770
|
+
if (old) for (const k of COST_TOKEN_KEYS) old.tokens[k] -= previous.tokens[k];
|
|
1771
|
+
}
|
|
1772
|
+
let target = groups.find((g) => costGroupKey(g, table) === costGroupKey(group, table));
|
|
1773
|
+
if (!target) {
|
|
1774
|
+
target = {
|
|
1775
|
+
...group,
|
|
1776
|
+
tokens: zeroCostTokens()
|
|
1777
|
+
};
|
|
1778
|
+
groups.push(target);
|
|
1779
|
+
}
|
|
1780
|
+
for (const k of COST_TOKEN_KEYS) target.tokens[k] += tokens[k];
|
|
1781
|
+
return {
|
|
1782
|
+
...state,
|
|
1783
|
+
last: {
|
|
1784
|
+
turn: data.turn,
|
|
1785
|
+
step: data.step,
|
|
1786
|
+
group
|
|
1787
|
+
},
|
|
1788
|
+
facts: {
|
|
1789
|
+
pricingKey: version,
|
|
1790
|
+
groups: groups.filter((g) => COST_TOKEN_KEYS.some((k) => g.tokens[k] > 0))
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
},
|
|
1794
|
+
wire: {
|
|
1795
|
+
view: (state) => state.facts,
|
|
1796
|
+
viewSchema: { parse(value) {
|
|
1797
|
+
return parseState({
|
|
1798
|
+
selection: null,
|
|
1799
|
+
at: null,
|
|
1800
|
+
last: null,
|
|
1801
|
+
facts: value
|
|
1802
|
+
}).facts;
|
|
1803
|
+
} }
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
function installCostProjection(ctx) {
|
|
1808
|
+
ctx.inject(["sessionProjections"], (c) => {
|
|
1809
|
+
const registry = c.get("sessionProjections");
|
|
1810
|
+
if (typeof registry?.register === "function") c.effect(() => registry.register(createCostProjection()), "commandcode cost projection");
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
//#endregion
|
|
1814
|
+
//#region src/accounts.ts
|
|
890
1815
|
/**
|
|
891
|
-
*
|
|
892
|
-
* page (`/docs/resources/pricing-limits`). Since 2026-08-16 16:00 UTC, DeepSeek
|
|
893
|
-
* charges by the hour: peak hours are 01:00–04:00 and 06:00–10:00 UTC (7h per
|
|
894
|
-
* weekday, full price) **Monday to Friday only**; the other 17 hours of a
|
|
895
|
-
* weekday and every hour of Saturday/Sunday (UTC) are off-peak at half price.
|
|
896
|
-
* The V4 Flash Vision (exp) variant (command-code@1.32.0) shares the V4 Flash
|
|
897
|
-
* windows and peak prices ($0.44/$1.32) — each row's hover annotation states
|
|
898
|
-
* exactly 2× that row's displayed off-peak prices. The picker shows the
|
|
899
|
-
* *current* state as a compact
|
|
900
|
-
* label (`Peak`/`Half`) matching the English noun style of the other markers
|
|
901
|
-
* (`Image`, `FREE`), so a developer can tell at a glance whether calling the
|
|
902
|
-
* model right now is cheap or expensive.
|
|
1816
|
+
* Multi-account pool for the Command Code provider (host side).
|
|
903
1817
|
*
|
|
904
|
-
*
|
|
905
|
-
*
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
* carry it: V4 Pro, V4 Flash, V4 Flash Vision (exp), and V4.1 Flash (added in
|
|
909
|
-
* command-code@1.53.0 at $0.15/$0.60 off-peak, $0.30/$1.20 peak — the same
|
|
910
|
-
* schedule as the other three).
|
|
1818
|
+
* One Command Code subscription (e.g. the Go plan's 5-hour window) is
|
|
1819
|
+
* metered; a user with several subscriptions wants a request that hits one
|
|
1820
|
+
* account's limit to continue on the next account without a visible failure.
|
|
1821
|
+
* This module owns that rotation:
|
|
911
1822
|
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
1823
|
+
* - {@link CommandCodeAccountPool.resolveKey} hands out the first account
|
|
1824
|
+
* whose key is not currently marked exhausted — or, when the request's
|
|
1825
|
+
* model matches a {@link CommandCodeModelAccountRule} and that account is
|
|
1826
|
+
* usable, the routed account — resolving each slot's key lazily (literal
|
|
1827
|
+
* config key → credential seam → launch environment → the official CLI
|
|
1828
|
+
* auth file for the default slot only).
|
|
1829
|
+
* - {@link CommandCodeAccountPool.markRejected} records a 429 (rate limit,
|
|
1830
|
+
* window unknown) or 401 (invalid key, disabled until the config changes)
|
|
1831
|
+
* against the exact API key, so several slots sharing one key share one
|
|
1832
|
+
* state.
|
|
1833
|
+
* - When every account is marked, the pool probes each key's
|
|
1834
|
+
* `/alpha/billing/credits` window limits (through the injected
|
|
1835
|
+
* {@link CommandCodeAccountPoolDeps.probeWindow}): an account whose window
|
|
1836
|
+
* no longer reports `exceeded` is revived, otherwise the pool throws a
|
|
1837
|
+
* `RATE_LIMIT` error naming the earliest reset time.
|
|
919
1838
|
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
1839
|
+
* The pool is deliberately cordis-free (like the adapter): every host fact
|
|
1840
|
+
* arrives through injected thunks, so node tests can drive it directly.
|
|
1841
|
+
*
|
|
1842
|
+
* @module dsh-commandcode-provider/accounts
|
|
922
1843
|
*/
|
|
923
|
-
const KNOWN_PEAK_PRICING = /* @__PURE__ */ new Set([
|
|
924
|
-
"deepseek/deepseek-v4-pro",
|
|
925
|
-
"deepseek/deepseek-v4-flash",
|
|
926
|
-
"deepseek/deepseek-v4-flash-vision-exp",
|
|
927
|
-
"deepseek/deepseek-v4.1-flash"
|
|
928
|
-
]);
|
|
929
1844
|
/**
|
|
930
|
-
*
|
|
931
|
-
*
|
|
1845
|
+
* Upper bound on the retry wait this pool attaches to the all-exhausted
|
|
1846
|
+
* `RATE_LIMIT` error. Must equal the `backoff.maxDelayMs` in the adapter's
|
|
1847
|
+
* `providerRetryPolicy` (which imports it from here): dsh-llm-retry honors a
|
|
1848
|
+
* provider-specified wait verbatim only at or below that cap — in normal mode
|
|
1849
|
+
* a LONGER attached wait makes the executor abandon the retry entirely
|
|
1850
|
+
* instead of falling back to local backoff, which would turn "poll until the
|
|
1851
|
+
* window opens" into "fail now".
|
|
932
1852
|
*/
|
|
933
|
-
const
|
|
1853
|
+
const RETRY_MAX_DELAY_MS = 9e5;
|
|
1854
|
+
/** A labeled, human-readable clock reading for error messages. */
|
|
1855
|
+
function clockLabel(ms) {
|
|
1856
|
+
return new Date(ms).toLocaleString();
|
|
1857
|
+
}
|
|
934
1858
|
/**
|
|
935
|
-
* Whether
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
* `undefined` for models outside the snapshot.
|
|
1859
|
+
* Whether an account with this rotation state can serve a request right now.
|
|
1860
|
+
* `undefined` (never rejected) is usable; a cooldown becomes usable again
|
|
1861
|
+
* once its reset time passes; `unknown` (429, reset unprobed) and
|
|
1862
|
+
* `disabled` (401) are not.
|
|
940
1863
|
*/
|
|
941
|
-
function
|
|
942
|
-
if (
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
if (day === 0 || day === 6) return "off-peak";
|
|
946
|
-
const hour = at.getUTCHours();
|
|
947
|
-
return PEAK_HOUR_RANGES.some(([start, end]) => hour >= start && hour < end) ? "peak" : "off-peak";
|
|
1864
|
+
function accountUsable(state) {
|
|
1865
|
+
if (state === void 0) return true;
|
|
1866
|
+
if (state.kind === "cooldown") return state.until > 0 && Date.now() >= state.until;
|
|
1867
|
+
return false;
|
|
948
1868
|
}
|
|
949
1869
|
/**
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
* for models without time-of-day pricing.
|
|
1870
|
+
* Pick the account that should serve now: the manually preferred slot when it
|
|
1871
|
+
* is usable, otherwise the first usable account in rotation order; undefined
|
|
1872
|
+
* when no account is usable. Shared by the pool (request path) and the plugin
|
|
1873
|
+
* entry (the usage view's active badge) so both always agree.
|
|
955
1874
|
*/
|
|
956
|
-
function
|
|
957
|
-
const
|
|
958
|
-
if (
|
|
959
|
-
|
|
1875
|
+
function selectActiveAccount(accounts, preferredId) {
|
|
1876
|
+
const usable = accounts.filter((account) => accountUsable(account.state));
|
|
1877
|
+
if (preferredId !== void 0) {
|
|
1878
|
+
const preferred = usable.find((account) => account.slot.id === preferredId);
|
|
1879
|
+
if (preferred !== void 0) return preferred;
|
|
1880
|
+
}
|
|
1881
|
+
return usable[0];
|
|
960
1882
|
}
|
|
961
1883
|
/**
|
|
962
|
-
*
|
|
963
|
-
*
|
|
1884
|
+
* The first routing rule whose model list contains the request's model id.
|
|
1885
|
+
* Undefined when no rule matches.
|
|
964
1886
|
*/
|
|
965
|
-
function
|
|
966
|
-
|
|
967
|
-
|
|
1887
|
+
function matchModelRule(model, rules) {
|
|
1888
|
+
if (model === "" || rules === void 0 || rules.length === 0) return void 0;
|
|
1889
|
+
for (const rule of rules) if (rule.models.includes(model)) return rule;
|
|
968
1890
|
}
|
|
969
1891
|
/**
|
|
970
|
-
* The
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
* the
|
|
974
|
-
* deal is still live. Permanent deals (no `expiresAt`) never lapse.
|
|
1892
|
+
* The routed account for a request's model: the first usable account whose
|
|
1893
|
+
* slot id matches the first matching rule's target. Undefined when no rule
|
|
1894
|
+
* matches or the routed account is not usable (the caller then falls back to
|
|
1895
|
+
* the normal preferred/rotation selection).
|
|
975
1896
|
*/
|
|
976
|
-
function
|
|
977
|
-
const
|
|
978
|
-
if (
|
|
979
|
-
|
|
980
|
-
return deal.label;
|
|
1897
|
+
function selectAccountForModel(accounts, model, rules) {
|
|
1898
|
+
const rule = matchModelRule(model, rules);
|
|
1899
|
+
if (rule === void 0) return void 0;
|
|
1900
|
+
return accounts.find((account) => account.slot.id === rule.account && accountUsable(account.state));
|
|
981
1901
|
}
|
|
982
1902
|
/**
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
1903
|
+
* The account pool. Rotation state is keyed by API key (never logged), so two
|
|
1904
|
+
* slots resolving to the same credential share one mark, and a key changed in
|
|
1905
|
+
* the credentials service starts with a clean slate.
|
|
986
1906
|
*/
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1907
|
+
var CommandCodeAccountPool = class {
|
|
1908
|
+
deps;
|
|
1909
|
+
/** Rotation state by API key. */
|
|
1910
|
+
states = /* @__PURE__ */ new Map();
|
|
1911
|
+
constructor(deps) {
|
|
1912
|
+
this.deps = deps;
|
|
993
1913
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1914
|
+
/**
|
|
1915
|
+
* Resolve every slot's key, deduplicated by key (first slot wins). Slots
|
|
1916
|
+
* without any resolvable key are omitted — they still appear in the
|
|
1917
|
+
* settings page as unconfigured, they just cannot serve requests.
|
|
1918
|
+
*/
|
|
1919
|
+
async resolvedAccounts() {
|
|
1920
|
+
const out = [];
|
|
1921
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1922
|
+
for (const slot of this.deps.slots()) {
|
|
1923
|
+
const key = await this.resolveSlotKey(slot);
|
|
1924
|
+
if (key === void 0 || seen.has(key)) continue;
|
|
1925
|
+
seen.add(key);
|
|
1926
|
+
out.push({
|
|
1927
|
+
slot,
|
|
1928
|
+
key,
|
|
1929
|
+
state: this.states.get(key)
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
return out;
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Every slot paired with its resolved key and rotation state — NOT
|
|
1936
|
+
* deduplicated: two slots sharing one credential both appear (the usage
|
|
1937
|
+
* view reports them individually), while slots without any resolvable key
|
|
1938
|
+
* are omitted. The serving path uses {@link resolvedAccounts} instead.
|
|
1939
|
+
*/
|
|
1940
|
+
async describeAccounts() {
|
|
1941
|
+
const out = [];
|
|
1942
|
+
for (const slot of this.deps.slots()) {
|
|
1943
|
+
const key = await this.resolveSlotKey(slot);
|
|
1944
|
+
if (key === void 0) continue;
|
|
1945
|
+
out.push({
|
|
1946
|
+
slot,
|
|
1947
|
+
key,
|
|
1948
|
+
state: this.states.get(key)
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
return out;
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Hand out the key for a request: the model-routed account when the
|
|
1955
|
+
* request's model matches a rule (and that account is usable), else the
|
|
1956
|
+
* manually preferred account when usable, else the first usable account in
|
|
1957
|
+
* rotation order. Returns `undefined` when no account resolves any key at
|
|
1958
|
+
* all (the caller then reports the missing credential). Throws
|
|
1959
|
+
* `RATE_LIMIT` — naming the earliest window reset — or
|
|
1960
|
+
* `INVALID_CREDENTIAL` when accounts exist but none can serve.
|
|
1961
|
+
*
|
|
1962
|
+
* `options.model` is the request's model id; routing rules re-read per
|
|
1963
|
+
* resolution, so a settings change applies live.
|
|
1964
|
+
*
|
|
1965
|
+
* `options.exclude` skips one key during the probe-revival pass: the
|
|
1966
|
+
* rotation hook excludes the just-rejected key so a probe that clears its
|
|
1967
|
+
* window cannot re-offer the same key within the same request (the adapter
|
|
1968
|
+
* refuses already-tried keys; the next request picks the revived key up).
|
|
1969
|
+
*/
|
|
1970
|
+
async resolveKey(options) {
|
|
1971
|
+
const accounts = await this.resolvedAccounts();
|
|
1972
|
+
if (accounts.length === 0) return;
|
|
1973
|
+
const routed = selectAccountForModel(accounts, options?.model ?? "", this.deps.modelAccountRules?.());
|
|
1974
|
+
if (routed !== void 0) return this.pick(routed);
|
|
1975
|
+
const preferred = this.deps.preferredId?.();
|
|
1976
|
+
const chosen = selectActiveAccount(accounts, preferred);
|
|
1977
|
+
if (chosen !== void 0) return this.pick(chosen);
|
|
1978
|
+
await Promise.all(accounts.map(async (account) => {
|
|
1979
|
+
if (account.state?.kind === "disabled") return;
|
|
1980
|
+
if (options?.exclude !== void 0 && account.key === options.exclude) return;
|
|
1981
|
+
let probe;
|
|
1982
|
+
try {
|
|
1983
|
+
probe = await this.deps.probeWindow(account.key);
|
|
1984
|
+
} catch {
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
if (probe === void 0) return;
|
|
1988
|
+
if (!probe.exceeded) this.states.delete(account.key);
|
|
1989
|
+
else this.states.set(account.key, {
|
|
1990
|
+
kind: "cooldown",
|
|
1991
|
+
reason: account.state?.reason ?? "rate limited (429)",
|
|
1992
|
+
until: probe.resetAt
|
|
1993
|
+
});
|
|
1994
|
+
}));
|
|
1995
|
+
const latest = await this.resolvedAccounts();
|
|
1996
|
+
const revived = selectActiveAccount(latest, preferred);
|
|
1997
|
+
if (revived !== void 0) return this.pick(revived);
|
|
1998
|
+
if (latest.filter((account) => account.state?.kind === "disabled").length === latest.length) throw new LlmError(`llm-commandcode: every configured Command Code account (${latest.length}) was rejected with 401 — check the stored API keys (Models page / settings) or the auth file;已配置的 ${latest.length} 个 Command Code 账户密钥均被拒绝(401)——请在设置页检查存储的 API 密钥,或重新运行 command-code login`, "INVALID_CREDENTIAL");
|
|
1999
|
+
const resets = latest.map((account) => account.state).filter((state) => state !== void 0 && state.kind === "cooldown" && state.until > 0).map((state) => state.until);
|
|
2000
|
+
const earliest = resets.length > 0 ? Math.min(...resets) : 0;
|
|
2001
|
+
const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
|
|
2002
|
+
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);
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Record a rejection against one key. `rate-limit` (429) marks the key
|
|
2006
|
+
* exhausted with an unknown reset (probed lazily at the next resolution
|
|
2007
|
+
* once every account is marked); `invalid-credential` (401) disables the
|
|
2008
|
+
* key until the stored credential changes.
|
|
2009
|
+
*/
|
|
2010
|
+
markRejected(apiKey, rejection) {
|
|
2011
|
+
if (rejection === "invalid-credential") this.states.set(apiKey, {
|
|
2012
|
+
kind: "disabled",
|
|
2013
|
+
reason: "invalid API key (401)",
|
|
2014
|
+
until: 0
|
|
2015
|
+
});
|
|
2016
|
+
else this.states.set(apiKey, {
|
|
2017
|
+
kind: "unknown",
|
|
2018
|
+
reason: "rate limited (429)",
|
|
2019
|
+
until: 0
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* One account's key: literal → credential seam → auth file (default slot).
|
|
2024
|
+
*
|
|
2025
|
+
* Every source is normalized here, at the single point where a slot's key
|
|
2026
|
+
* enters the pool. The adapter sends the key through the harness's
|
|
2027
|
+
* `assertUsableApiKey()`, which trims it — a stored key from the credentials
|
|
2028
|
+
* seam, a `.env` line, or a shell export all pick up surrounding whitespace
|
|
2029
|
+
* — and reports that trimmed form back to `markRejected()`. Returning the
|
|
2030
|
+
* raw value would file every 429/401 mark under a key no later lookup can
|
|
2031
|
+
* find: rotation would re-offer the same account, the account card would show
|
|
2032
|
+
* no mark, and the usage endpoints would 401 while chat kept working.
|
|
2033
|
+
* Normalizing once makes resolution, probing, marking, and the request path
|
|
2034
|
+
* agree on one string.
|
|
2035
|
+
*/
|
|
2036
|
+
async resolveSlotKey(slot) {
|
|
2037
|
+
if (slot.literal !== void 0) {
|
|
2038
|
+
const literal = normalizeResolvedKey(slot.literal);
|
|
2039
|
+
if (literal !== void 0) return literal;
|
|
2040
|
+
}
|
|
2041
|
+
if (slot.ref !== void 0) {
|
|
2042
|
+
const hit = await this.deps.resolveRef(slot.ref);
|
|
2043
|
+
if (hit !== void 0) {
|
|
2044
|
+
const resolved = normalizeResolvedKey(hit);
|
|
2045
|
+
if (resolved !== void 0) return resolved;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
if (slot.allowAuthFile) {
|
|
2049
|
+
const fromFile = this.deps.authFileKey();
|
|
2050
|
+
if (fromFile !== void 0) {
|
|
2051
|
+
const fileKey = normalizeResolvedKey(fromFile);
|
|
2052
|
+
if (fileKey !== void 0) return fileKey;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
/** Hand out the chosen account's key. */
|
|
2057
|
+
pick(account) {
|
|
2058
|
+
return {
|
|
2059
|
+
key: account.key,
|
|
2060
|
+
slot: account.slot
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
997
2064
|
/**
|
|
998
|
-
*
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1002
|
-
* only" adds nothing the picker needs to show.
|
|
2065
|
+
* Normalize one resolved credential to the form every consumer sees. Trim
|
|
2066
|
+
* only: a blank-after-trim value means "no key" (the slot is omitted and the
|
|
2067
|
+
* caller reports `MISSING_CREDENTIAL`), while characters an HTTP header cannot
|
|
2068
|
+
* carry are left for `assertUsableApiKey()` to reject with its own message.
|
|
1003
2069
|
*/
|
|
1004
|
-
function
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
if (plan !== void 0) parts.push(plan);
|
|
1008
|
-
const deal = dealLabel(modelId, now);
|
|
1009
|
-
if (deal !== void 0) parts.push(deal);
|
|
1010
|
-
const peak = peakPricingLabel(modelId, now);
|
|
1011
|
-
if (peak !== void 0) parts.push(peak);
|
|
1012
|
-
if (KNOWN_IMAGE_MODELS.has(modelId)) parts.push("Image");
|
|
1013
|
-
const ctx = formatContext(contextWindow);
|
|
1014
|
-
if (ctx !== void 0) parts.push(ctx);
|
|
1015
|
-
return parts.join(" · ");
|
|
2070
|
+
function normalizeResolvedKey(value) {
|
|
2071
|
+
const trimmed = value.trim();
|
|
2072
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
1016
2073
|
}
|
|
1017
2074
|
//#endregion
|
|
1018
2075
|
//#region src/adapter.ts
|
|
@@ -1024,7 +2081,7 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
|
|
|
1024
2081
|
* and API key or subscription, and Command Code's terms apply.
|
|
1025
2082
|
*
|
|
1026
2083
|
* Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.4;
|
|
1027
|
-
* re-verified against command-code@1.
|
|
2084
|
+
* re-verified against command-code@1.54.0 — endpoints, request shape, and
|
|
1028
2085
|
* stream events unchanged):
|
|
1029
2086
|
* POST {apiBase}/alpha/generate
|
|
1030
2087
|
* body: { config, memory, taste, skills, params: { model, messages, tools,
|
|
@@ -1037,7 +2094,7 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
|
|
|
1037
2094
|
* per-request options thunk and an API-key resolver from the plugin entry
|
|
1038
2095
|
* (src/index.ts), so a settings change reaches the very next request.
|
|
1039
2096
|
*/
|
|
1040
|
-
const COMMAND_CODE_CLI_VERSION = "1.
|
|
2097
|
+
const COMMAND_CODE_CLI_VERSION = "1.54.0";
|
|
1041
2098
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
1042
2099
|
const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
|
|
1043
2100
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
@@ -1097,6 +2154,61 @@ function hasTerminalStreamMarker(message) {
|
|
|
1097
2154
|
const lower = message.toLowerCase();
|
|
1098
2155
|
return TERMINAL_STREAM_ERROR_MARKERS.some((marker) => lower.includes(marker));
|
|
1099
2156
|
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Context-window wording the provider uses to reject a request that outgrew
|
|
2159
|
+
* the model's window. `isContextWindowExceededError` is the harness's own
|
|
2160
|
+
* provider-neutral classifier — and the contract `CONTEXT_WINDOW_EXCEEDED`
|
|
2161
|
+
* exists for — so it decides; the official CLI's `truncated` pattern adds the
|
|
2162
|
+
* phrasings Command Code's upstreams emit that the harness helper does not
|
|
2163
|
+
* cover (a bare `prompt is too long`, or a complaint about `max_tokens`).
|
|
2164
|
+
* The CLI matches these first and answers them by compacting and retrying —
|
|
2165
|
+
* never by resending the request unchanged.
|
|
2166
|
+
*/
|
|
2167
|
+
const CLI_CONTEXT_OVERFLOW_PATTERN = /prompt is too long|context.*(length|window)|max_tokens|maximum.*tokens/i;
|
|
2168
|
+
function isContextOverflowDetail(detail) {
|
|
2169
|
+
return isContextWindowExceededError(detail) || CLI_CONTEXT_OVERFLOW_PATTERN.test(detail);
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Classify one in-band stream `error` payload into the failure the caller
|
|
2173
|
+
* throws. Shared by both transports — the CLI transport delivers it as an
|
|
2174
|
+
* `error` event (`{ type: 'error', error }`), the Provider API transport as a
|
|
2175
|
+
* top-level `error` member of an SSE chunk — so the two cannot drift apart.
|
|
2176
|
+
*
|
|
2177
|
+
* The wording decides before the status does, mirroring the official CLI's
|
|
2178
|
+
* `classifyKind` (which reads the message first) and the harness helpers
|
|
2179
|
+
* (which exist so thrown and in-band delivery share one classifier). Order is
|
|
2180
|
+
* load-bearing:
|
|
2181
|
+
*
|
|
2182
|
+
* - A context-window rejection is terminal for THIS request but recoverable by
|
|
2183
|
+
* the harness: `dsh-compaction-basic` listens on `agent/request-error` for
|
|
2184
|
+
* `CONTEXT_WINDOW_EXCEEDED` and answers it with a compaction + retry of the
|
|
2185
|
+
* reduced surface. That is what lets a long session survive a request that
|
|
2186
|
+
* outgrew the model's window, and it is what the official CLI does (its
|
|
2187
|
+
* non-retryable `truncated` kind). Reported as retryable `SERVER` — where
|
|
2188
|
+
* this wording landed before — the adapter resent the byte-identical
|
|
2189
|
+
* oversized request up to `maxRetries` times and the session could never
|
|
2190
|
+
* recover, which is exactly the endless-retry-at-long-context report
|
|
2191
|
+
* (issue #39).
|
|
2192
|
+
* - Credits/plan wording is terminal, so an exhausted balance or a model
|
|
2193
|
+
* outside the plan is not retried as if it were a transient rate limit.
|
|
2194
|
+
* - Only then does the status/`isRetryable` pair decide, as before.
|
|
2195
|
+
*/
|
|
2196
|
+
function streamErrorToLlmError(value, fallbackMessage) {
|
|
2197
|
+
const record = isRecord(value) ? value : void 0;
|
|
2198
|
+
const message = record ? stringValue(record.message) ?? JSON.stringify(record) : stringValue(value) ?? fallbackMessage ?? "Stream error";
|
|
2199
|
+
const statusCode = record === void 0 ? void 0 : numberValue(record.statusCode) ?? numberValue(record.status);
|
|
2200
|
+
const isRetryable = record === void 0 ? void 0 : booleanValue(record.isRetryable);
|
|
2201
|
+
const detail = [
|
|
2202
|
+
stringValue(record?.code),
|
|
2203
|
+
stringValue(record?.type),
|
|
2204
|
+
message
|
|
2205
|
+
].filter((part) => part !== void 0 && part !== "").join(" ");
|
|
2206
|
+
const statusOption = statusCode !== void 0 ? { status: statusCode } : void 0;
|
|
2207
|
+
if (isContextOverflowDetail(detail)) return new LlmError(`Command Code stream error: ${message};Command Code API 拒绝了这次请求:内容超出模型上下文窗口。正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, CONTEXT_WINDOW_EXCEEDED_CODE, statusOption);
|
|
2208
|
+
const terminal = hasTerminalStreamMarker(detail);
|
|
2209
|
+
if (!(isRetryable === true || (statusCode !== void 0 ? statusCode !== void 0 && (statusCode === 429 || statusCode >= 500) : isRetryable !== false && !terminal))) return new LlmError(`Command Code stream error: ${message}`, "PROVIDER_STREAM_ERROR", statusOption);
|
|
2210
|
+
return new LlmError(`Command Code stream error: ${message}`, "SERVER", statusOption);
|
|
2211
|
+
}
|
|
1100
2212
|
function recordOrEmpty(value) {
|
|
1101
2213
|
if (isRecord(value)) return value;
|
|
1102
2214
|
if (typeof value === "string") try {
|
|
@@ -1431,6 +2543,52 @@ function hasImageContent(message) {
|
|
|
1431
2543
|
const check = (blocks) => blocks.some((b) => b.type === "image" || b.type === "tool-result" && check(b.content));
|
|
1432
2544
|
return check(message.content);
|
|
1433
2545
|
}
|
|
2546
|
+
/** The budget ladder: the primary rung first, the 413 eviction rung second. */
|
|
2547
|
+
const REQUEST_IMAGE_BUDGETS = [{
|
|
2548
|
+
maxBytes: 33554432,
|
|
2549
|
+
maxImages: 60,
|
|
2550
|
+
byteQuantum: 16777216,
|
|
2551
|
+
countQuantum: 30
|
|
2552
|
+
}, {
|
|
2553
|
+
maxBytes: 8388608,
|
|
2554
|
+
maxImages: 12,
|
|
2555
|
+
byteQuantum: 8388608,
|
|
2556
|
+
countQuantum: 12
|
|
2557
|
+
}];
|
|
2558
|
+
/**
|
|
2559
|
+
* Project request history under one image budget: the oldest images past it
|
|
2560
|
+
* become placeholder text in place, the newest keep their bytes, and a history
|
|
2561
|
+
* already inside the budget is returned as the SAME array — the caller uses
|
|
2562
|
+
* that identity to tell "nothing evicted" from "evicted".
|
|
2563
|
+
*
|
|
2564
|
+
* Nested tool-result images ride the same core walk, which is exactly right
|
|
2565
|
+
* for this adapter: an evicted `read_image` result surfaces its placeholder as
|
|
2566
|
+
* the tool message's own text, so no carrier user message is emitted for it.
|
|
2567
|
+
*/
|
|
2568
|
+
function projectRequestImages(messages, budget) {
|
|
2569
|
+
return offloadRequestImagesWithPolicy(messages, {
|
|
2570
|
+
representation: "base64",
|
|
2571
|
+
maxBytes: budget.maxBytes,
|
|
2572
|
+
maxImages: budget.maxImages,
|
|
2573
|
+
byteQuantum: budget.byteQuantum,
|
|
2574
|
+
countQuantum: budget.countQuantum,
|
|
2575
|
+
placeholder: (ref) => offloadedImageText(ref)
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
/**
|
|
2579
|
+
* The request options one budget produced: the caller's own object when the
|
|
2580
|
+
* history was already inside the budget, else a copy carrying the projection.
|
|
2581
|
+
* The next rung of the ladder is applied to the CURRENT projection, never to
|
|
2582
|
+
* the raw history — otherwise a retry could reintroduce images the first rung
|
|
2583
|
+
* had already evicted.
|
|
2584
|
+
*/
|
|
2585
|
+
function withImageBudget(options, budget) {
|
|
2586
|
+
const projected = projectRequestImages(options.messages, budget);
|
|
2587
|
+
return projected === options.messages ? options : {
|
|
2588
|
+
...options,
|
|
2589
|
+
messages: [...projected]
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
1434
2592
|
/**
|
|
1435
2593
|
* Convert one image reference to the Command Code wire format, as the official
|
|
1436
2594
|
* CLI does: `{ type: 'image', source: { type: 'base64', media_type, data } }`.
|
|
@@ -1665,13 +2823,22 @@ function parseUsageTotals(usage) {
|
|
|
1665
2823
|
};
|
|
1666
2824
|
}
|
|
1667
2825
|
/** Parse one window-limit block (`fiveHour` / `weekly`). */
|
|
2826
|
+
/**
|
|
2827
|
+
* Parse one window block into a window limit, or undefined when the endpoint
|
|
2828
|
+
* reported no such window.
|
|
2829
|
+
*
|
|
2830
|
+
* Returning undefined (rather than a zeroed window) is load-bearing: the panel
|
|
2831
|
+
* must not draw a quota row for a window the account never reported, and a
|
|
2832
|
+
* zeroed window is indistinguishable from a genuinely uncapped one.
|
|
2833
|
+
*/
|
|
1668
2834
|
function parseWindowLimit(value) {
|
|
1669
2835
|
const block = isRecord(value) ? value : void 0;
|
|
2836
|
+
if (block === void 0) return void 0;
|
|
1670
2837
|
return {
|
|
1671
|
-
used: numberValue(block
|
|
1672
|
-
cap: numberValue(block
|
|
1673
|
-
exceeded: block
|
|
1674
|
-
resetAt: numberValue(block
|
|
2838
|
+
used: numberValue(block.used) ?? 0,
|
|
2839
|
+
cap: numberValue(block.cap) ?? 0,
|
|
2840
|
+
exceeded: block.exceeded === true,
|
|
2841
|
+
resetAt: numberValue(block.resetAt) ?? 0
|
|
1675
2842
|
};
|
|
1676
2843
|
}
|
|
1677
2844
|
/**
|
|
@@ -1685,13 +2852,19 @@ function parseCreditLimits(credits) {
|
|
|
1685
2852
|
const fiveHour = windowLimits !== void 0 && isRecord(windowLimits.fiveHour) ? windowLimits.fiveHour : void 0;
|
|
1686
2853
|
const weekly = windowLimits !== void 0 && isRecord(windowLimits.weekly) ? windowLimits.weekly : void 0;
|
|
1687
2854
|
if (creditsData === void 0 && fiveHour === void 0 && weekly === void 0) return void 0;
|
|
1688
|
-
|
|
2855
|
+
const parsed = {
|
|
1689
2856
|
monthlyCredits: numberValue(creditsData?.monthlyCredits) ?? 0,
|
|
1690
2857
|
purchasedCredits: numberValue(creditsData?.purchasedCredits) ?? 0,
|
|
1691
2858
|
freeCredits: numberValue(creditsData?.freeCredits) ?? 0,
|
|
1692
|
-
|
|
1693
|
-
|
|
2859
|
+
monthlyReported: numberValue(creditsData?.monthlyCredits) !== void 0,
|
|
2860
|
+
purchasedReported: numberValue(creditsData?.purchasedCredits) !== void 0,
|
|
2861
|
+
freeReported: numberValue(creditsData?.freeCredits) !== void 0
|
|
1694
2862
|
};
|
|
2863
|
+
const fiveHourLimit = parseWindowLimit(fiveHour);
|
|
2864
|
+
const weeklyLimit = parseWindowLimit(weekly);
|
|
2865
|
+
if (fiveHourLimit !== void 0) parsed.fiveHour = fiveHourLimit;
|
|
2866
|
+
if (weeklyLimit !== void 0) parsed.weekly = weeklyLimit;
|
|
2867
|
+
return parsed;
|
|
1695
2868
|
}
|
|
1696
2869
|
/**
|
|
1697
2870
|
* Parse the account identity from whoami plus the plan identity from the
|
|
@@ -2287,12 +3460,14 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
2287
3460
|
const reasoningEffort = effort && effort !== "off" && supported?.includes(effort) ? effort : void 0;
|
|
2288
3461
|
const systemText = [options.system ?? "", ...options.messages.filter((m) => m.role === "system").map((m) => m.content.map(blockText).filter(Boolean).join("\n"))].filter(Boolean).join("\n\n");
|
|
2289
3462
|
let protocol = this.resolveProtocol(apiKey);
|
|
2290
|
-
|
|
3463
|
+
let requestOptions = withImageBudget(options, REQUEST_IMAGE_BUDGETS[0]);
|
|
3464
|
+
let imageBudgetIndex = 0;
|
|
3465
|
+
const buildBody = async (target) => target === "cli" ? buildCliBody(requestOptions, connection, {
|
|
2291
3466
|
maxTokens,
|
|
2292
3467
|
reasoningEffort,
|
|
2293
3468
|
systemText,
|
|
2294
3469
|
readImage
|
|
2295
|
-
}) : buildOpenAIBody(
|
|
3470
|
+
}) : buildOpenAIBody(requestOptions, {
|
|
2296
3471
|
maxTokens,
|
|
2297
3472
|
reasoningEffort,
|
|
2298
3473
|
systemText,
|
|
@@ -2319,6 +3494,15 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
2319
3494
|
body = await buildBody("cli");
|
|
2320
3495
|
continue;
|
|
2321
3496
|
}
|
|
3497
|
+
if (attempt.status === 413 && imageBudgetIndex + 1 < REQUEST_IMAGE_BUDGETS.length) {
|
|
3498
|
+
const tightened = withImageBudget(requestOptions, REQUEST_IMAGE_BUDGETS[imageBudgetIndex + 1]);
|
|
3499
|
+
if (tightened !== requestOptions) {
|
|
3500
|
+
imageBudgetIndex += 1;
|
|
3501
|
+
requestOptions = tightened;
|
|
3502
|
+
body = await buildBody(protocol);
|
|
3503
|
+
continue;
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
2322
3506
|
const rotate = this.deps.rotateApiKey;
|
|
2323
3507
|
if ((attempt.status === 429 || attempt.status === 401) && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
|
|
2324
3508
|
const next = await rotate(apiKey, attempt.status === 429 ? "rate-limit" : "invalid-credential", connection, options.model);
|
|
@@ -2411,26 +3595,68 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
2411
3595
|
* Map a pre-stream generate HTTP failure onto a stable LlmError. Command
|
|
2412
3596
|
* Code folds several business rejections into 403 (plan limits, CLI version,
|
|
2413
3597
|
* model access): prefer the machine-readable `error.code` when present; the
|
|
2414
|
-
* status alone cannot distinguish them.
|
|
3598
|
+
* status alone cannot distinguish them. The status also decides the failure
|
|
3599
|
+
* CODE via {@link httpErrorCode}, which is what the retry policy routes on —
|
|
3600
|
+
* a transient 5xx must not be reported as a permanent provider rejection. A
|
|
3601
|
+
* 429's `Retry-After` header rides along as
|
|
2415
3602
|
* `providerRetryAfterMs` so dsh-llm-retry can wait exactly that long instead
|
|
2416
3603
|
* of guessing at the backoff cadence — capped at RETRY_MAX_DELAY_MS, because
|
|
2417
3604
|
* in normal mode a longer attached wait makes the executor abandon the retry
|
|
2418
3605
|
* outright instead of falling back to local backoff.
|
|
3606
|
+
*
|
|
3607
|
+
* One body is read, not just its status: a client-side rejection whose
|
|
3608
|
+
* `error.code`/`type`/`message` names the model context window is reported as
|
|
3609
|
+
* `CONTEXT_WINDOW_EXCEEDED` (see the branch below), because that failure has a
|
|
3610
|
+
* recovery the harness performs and a generic provider error does not.
|
|
2419
3611
|
*/
|
|
2420
3612
|
function generateHttpError(status, errText, retryAfterMs) {
|
|
2421
3613
|
let providerCode;
|
|
3614
|
+
let providerDetail = "";
|
|
2422
3615
|
try {
|
|
2423
3616
|
const parsed = JSON.parse(errText);
|
|
2424
|
-
if (isRecord(parsed) && isRecord(parsed.error))
|
|
3617
|
+
if (isRecord(parsed) && isRecord(parsed.error)) {
|
|
3618
|
+
providerCode = stringValue(parsed.error.code);
|
|
3619
|
+
providerDetail = [
|
|
3620
|
+
stringValue(parsed.error.code),
|
|
3621
|
+
stringValue(parsed.error.type),
|
|
3622
|
+
stringValue(parsed.error.message)
|
|
3623
|
+
].filter((part) => part !== void 0 && part !== "").join(" ");
|
|
3624
|
+
}
|
|
2425
3625
|
} catch {}
|
|
2426
3626
|
const detail = providerCode ?? `HTTP ${status}`;
|
|
3627
|
+
const overflowDetail = providerDetail !== "" ? providerDetail : errText.slice(0, 500);
|
|
3628
|
+
if (status < 500 && isContextOverflowDetail(overflowDetail)) return new LlmError(`Command Code API error ${status}: the request exceeds the model's context window — this session is being compacted and the request retried;Command Code API 返回 ${status}:请求内容超出模型上下文窗口——正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, CONTEXT_WINDOW_EXCEEDED_CODE, { status });
|
|
2427
3629
|
if (status === 401) return new LlmError(`Command Code API error 401 (${detail}): the API key is missing or invalid — check the key stored for COMMANDCODE_API_KEY (Models page) or the auth file;Command Code API 返回 401:API 密钥缺失或无效——请在设置页检查 COMMANDCODE_API_KEY 存储的密钥,或检查 auth 文件`, "INVALID_CREDENTIAL", { status: 401 });
|
|
2428
|
-
return new LlmError(
|
|
3630
|
+
if (status === 413) return new LlmError("Command Code API error 413: the request body exceeds the provider's size limit (the cap is undocumented, measured at about 50 MB) — the session history is too large to send, usually because of accumulated image attachments. Start a new session, or drop the image-heavy part of this one, and retry;Command Code API 返回 413:请求体超过服务端的体积上限(官方未公开,实测约 50 MB)——会话历史过大,通常是历史图片累积所致。请新建会话,或移除本会话中图片较多的部分后重试", "PROVIDER_HTTP_ERROR", { status: 413 });
|
|
3631
|
+
return new LlmError(`Command Code API error ${status}${detail === `HTTP ${status}` ? "" : ` (${detail})`}: ${errText.slice(0, 500)}`, httpErrorCode(status), {
|
|
2429
3632
|
status,
|
|
2430
3633
|
...retryAfterMs !== void 0 && retryAfterMs > 0 && retryAfterMs <= 9e5 ? { providerRetryAfterMs: retryAfterMs } : {}
|
|
2431
3634
|
});
|
|
2432
3635
|
}
|
|
2433
3636
|
/**
|
|
3637
|
+
* Classify a pre-stream HTTP status into the failure code the route's retry
|
|
3638
|
+
* policy keys on. dsh-llm-retry matches `retryableCodes` only — never the
|
|
3639
|
+
* status, never the provider's `error.type` — so a status that arrives here as
|
|
3640
|
+
* a permanent code is never retried, whatever it says about itself.
|
|
3641
|
+
*
|
|
3642
|
+
* 5xx is the provider's own "we are temporarily unavailable" class (Cloudflare's
|
|
3643
|
+
* 520-527 included; a 520 body says exactly that in words) and the only sane
|
|
3644
|
+
* answer to it is the byte-identical resend the retry policy exists to make.
|
|
3645
|
+
* Keeping it on `PROVIDER_HTTP_ERROR` — absent from the whitelist by design, so
|
|
3646
|
+
* that a 403 plan rejection or a 400 shape error fails fast — made the one
|
|
3647
|
+
* failure that asks to be retried the only one that never was, while the same
|
|
3648
|
+
* status arriving as an in-band stream `error` event was classified retryable
|
|
3649
|
+
* `SERVER` (see the `error` case of {@link handleCliEvent}). 408 is the same
|
|
3650
|
+
* story for the request itself. Everything else stays permanent: a 400/403/404/
|
|
3651
|
+
* 409/422 rejection repeats identically on every attempt.
|
|
3652
|
+
*/
|
|
3653
|
+
function httpErrorCode(status) {
|
|
3654
|
+
if (status === 429) return "RATE_LIMIT";
|
|
3655
|
+
if (status === 408) return "TIMEOUT";
|
|
3656
|
+
if (status >= 500) return "SERVER";
|
|
3657
|
+
return "PROVIDER_HTTP_ERROR";
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
2434
3660
|
* Handle one CLI-transport (`/alpha/generate`) stream event, appending
|
|
2435
3661
|
* harness StreamChunks. Pure over the passed assembler — no stream() locals.
|
|
2436
3662
|
*/
|
|
@@ -2537,16 +3763,7 @@ function handleCliEvent(asm, event) {
|
|
|
2537
3763
|
});
|
|
2538
3764
|
break;
|
|
2539
3765
|
}
|
|
2540
|
-
case "error":
|
|
2541
|
-
const err = isRecord(event.error) ? event.error : void 0;
|
|
2542
|
-
const detail = isRecord(event.error) ? stringValue(event.error.message) ?? JSON.stringify(event.error) : stringValue(event.error) ?? stringValue(event.message) ?? "Stream error";
|
|
2543
|
-
const statusCode = err ? numberValue(err.statusCode) : void 0;
|
|
2544
|
-
const isRetryable = err ? booleanValue(err.isRetryable) : void 0;
|
|
2545
|
-
const retryableStatus = statusCode !== void 0 && (statusCode === 429 || statusCode >= 500);
|
|
2546
|
-
const terminal = hasTerminalStreamMarker(detail);
|
|
2547
|
-
if (!(isRetryable === true || (statusCode !== void 0 ? retryableStatus : isRetryable !== false && !terminal))) throw new LlmError(`Command Code stream error: ${detail}`, "PROVIDER_STREAM_ERROR", statusCode !== void 0 ? { status: statusCode } : void 0);
|
|
2548
|
-
throw new LlmError(`Command Code stream error: ${detail}`, "SERVER", statusCode !== void 0 ? { status: statusCode } : void 0);
|
|
2549
|
-
}
|
|
3766
|
+
case "error": throw streamErrorToLlmError(event.error, stringValue(event.message));
|
|
2550
3767
|
}
|
|
2551
3768
|
return chunks;
|
|
2552
3769
|
}
|
|
@@ -2558,6 +3775,7 @@ function handleCliEvent(asm, event) {
|
|
|
2558
3775
|
function handleOpenAIEvent(asm, event) {
|
|
2559
3776
|
const chunks = [];
|
|
2560
3777
|
if (!isRecord(event)) return chunks;
|
|
3778
|
+
if (event.error !== void 0) throw streamErrorToLlmError(event.error, stringValue(event.message));
|
|
2561
3779
|
const choices = event.choices;
|
|
2562
3780
|
if (!Array.isArray(choices) || choices.length === 0) {
|
|
2563
3781
|
if (event.usage !== void 0) chunks.push({
|
|
@@ -2880,8 +4098,13 @@ function renderReport(report, locale, title) {
|
|
|
2880
4098
|
}
|
|
2881
4099
|
if (report.credits) {
|
|
2882
4100
|
const c = report.credits;
|
|
2883
|
-
const
|
|
2884
|
-
|
|
4101
|
+
const balanceTotal = c.monthlyCredits + c.purchasedCredits;
|
|
4102
|
+
const ratioKnown = c.monthlyReported !== false && c.purchasedReported !== false && balanceTotal > 0;
|
|
4103
|
+
lines.push(commandCopy(locale, "creditsHeader"), commandCopy(locale, "monthlyLine").replace("{monthly}", c.monthlyReported === false ? "—" : moneyShort(c.monthlyCredits)).replace("{purchased}", c.purchasedReported === false ? "—" : moneyShort(c.purchasedCredits)).replace("{free}", c.freeReported === false ? "—" : moneyShort(c.freeCredits)), ...ratioKnown ? [commandCopy(locale, "barLine").replace("{bar}", bar(c.monthlyCredits, balanceTotal)).replace("{pct}", (c.monthlyCredits / balanceTotal * 100).toFixed(0))] : [], "");
|
|
4104
|
+
const windowLines = [];
|
|
4105
|
+
if (c.fiveHour !== void 0) windowLines.push(commandCopy(locale, "fiveHourLine").replace("{used}", moneyShort(c.fiveHour.used)).replace("{cap}", moneyShort(c.fiveHour.cap)).replace("{warn}", c.fiveHour.exceeded ? commandCopy(locale, "exceededWarning") : ""), commandCopy(locale, "windowBarLine").replace("{bar}", bar(c.fiveHour.used, c.fiveHour.cap)).replace("{when}", resetLabel(c.fiveHour.resetAt)));
|
|
4106
|
+
if (c.weekly !== void 0) windowLines.push(commandCopy(locale, "weeklyLine").replace("{used}", moneyShort(c.weekly.used)).replace("{cap}", moneyShort(c.weekly.cap)).replace("{warn}", c.weekly.exceeded ? commandCopy(locale, "exceededWarning") : ""), commandCopy(locale, "windowBarLine").replace("{bar}", bar(c.weekly.used, c.weekly.cap)).replace("{when}", resetLabel(c.weekly.resetAt)));
|
|
4107
|
+
if (windowLines.length > 0) lines.push(commandCopy(locale, "windowsHeader"), ...windowLines, "");
|
|
2885
4108
|
}
|
|
2886
4109
|
if (report.failures.length > 0) lines.push(commandCopy(locale, "partialFailures").replace("{list}", report.failures.join("; ")), "");
|
|
2887
4110
|
if (!report.account && !report.usage && !report.credits) lines.push(commandCopy(locale, "noData"), "");
|
|
@@ -2997,7 +4220,16 @@ const USAGE_REPORT_ENDPOINT = "commandcode/report";
|
|
|
2997
4220
|
*/
|
|
2998
4221
|
const { reject: reject$1, record: record$1, stringField: stringField$1, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
|
|
2999
4222
|
/** Validate one window-limit block (`fiveHour` / `weekly`). */
|
|
4223
|
+
/**
|
|
4224
|
+
* Parse one quota window, or undefined when the frame carries no such window.
|
|
4225
|
+
*
|
|
4226
|
+
* The distinction is load-bearing at this boundary: an ABSENT window means the
|
|
4227
|
+
* billing endpoint reported none (an unlimited plan), while a present block with
|
|
4228
|
+
* `cap: 0` means uncapped spend that was really reported. Collapsing the two
|
|
4229
|
+
* would draw a zeroed quota row for an account that has no such limit.
|
|
4230
|
+
*/
|
|
3000
4231
|
function windowLimit(value, field) {
|
|
4232
|
+
if (value === void 0) return void 0;
|
|
3001
4233
|
const source = record$1(value, field);
|
|
3002
4234
|
return {
|
|
3003
4235
|
used: numberField(source, "used", `${field}.used`),
|
|
@@ -3045,13 +4277,18 @@ function parseUsageReport(value) {
|
|
|
3045
4277
|
}
|
|
3046
4278
|
if (source.credits !== void 0) {
|
|
3047
4279
|
const credits = record$1(source.credits, "credits");
|
|
3048
|
-
|
|
4280
|
+
const parsed = {
|
|
3049
4281
|
monthlyCredits: numberField(credits, "monthlyCredits", "credits.monthlyCredits"),
|
|
3050
4282
|
purchasedCredits: numberField(credits, "purchasedCredits", "credits.purchasedCredits"),
|
|
3051
|
-
freeCredits: numberField(credits, "freeCredits", "credits.freeCredits")
|
|
3052
|
-
fiveHour: windowLimit(credits.fiveHour, "credits.fiveHour"),
|
|
3053
|
-
weekly: windowLimit(credits.weekly, "credits.weekly")
|
|
4283
|
+
freeCredits: numberField(credits, "freeCredits", "credits.freeCredits")
|
|
3054
4284
|
};
|
|
4285
|
+
if (credits.monthlyReported !== void 0) parsed.monthlyReported = booleanField(credits, "monthlyReported", "credits.monthlyReported");
|
|
4286
|
+
for (const flag of ["purchasedReported", "freeReported"]) if (credits[flag] !== void 0) parsed[flag] = booleanField(credits, flag, `credits.${flag}`);
|
|
4287
|
+
const fiveHour = windowLimit(credits.fiveHour, "credits.fiveHour");
|
|
4288
|
+
const weekly = windowLimit(credits.weekly, "credits.weekly");
|
|
4289
|
+
if (fiveHour !== void 0) parsed.fiveHour = fiveHour;
|
|
4290
|
+
if (weekly !== void 0) parsed.weekly = weekly;
|
|
4291
|
+
report.credits = parsed;
|
|
3055
4292
|
}
|
|
3056
4293
|
if (source.plan !== void 0) {
|
|
3057
4294
|
const plan = record$1(source.plan, "plan");
|
|
@@ -3133,6 +4370,72 @@ function parseCatalog(value) {
|
|
|
3133
4370
|
* service and `commandcode` namespace as the usage report.
|
|
3134
4371
|
*/
|
|
3135
4372
|
const MODELS_DESCRIPTOR = makeRemoteDescriptor(MODELS_ENDPOINT, "models", `${USAGE_REMOTE_PACKAGE}#CommandCodeCatalog`, { parse: parseCatalog });
|
|
4373
|
+
/** Canonical `<namespace>/<method>` endpoint of the price-table Remote. */
|
|
4374
|
+
const PRICES_ENDPOINT = "commandcode/prices";
|
|
4375
|
+
/**
|
|
4376
|
+
* The shared read/validate helpers for the price-table endpoint — its own
|
|
4377
|
+
* instance so price boundary errors name `commandcode/prices`.
|
|
4378
|
+
*/
|
|
4379
|
+
const { reject: priceReject, record: priceRecord, stringField: priceString, numberField: priceNumber, booleanField: priceBoolean } = makeBoundaryValidator("commandcode/prices result:");
|
|
4380
|
+
/** Parse one rate block (`rates`, or a model's `peak` override). */
|
|
4381
|
+
function parseRates(source, field) {
|
|
4382
|
+
const rates = {
|
|
4383
|
+
inputCost: priceNumber(source, "inputCost", `${field}.inputCost`),
|
|
4384
|
+
outputCost: priceNumber(source, "outputCost", `${field}.outputCost`),
|
|
4385
|
+
cacheReadCost: priceNumber(source, "cacheReadCost", `${field}.cacheReadCost`)
|
|
4386
|
+
};
|
|
4387
|
+
if (source.cacheWriteCost !== void 0) rates.cacheWriteCost = priceNumber(source, "cacheWriteCost", `${field}.cacheWriteCost`);
|
|
4388
|
+
return rates;
|
|
4389
|
+
}
|
|
4390
|
+
/** Parse one untrusted boundary value into a {@link CommandCodeModelPrice}. */
|
|
4391
|
+
function parseModelPrice(value) {
|
|
4392
|
+
const source = priceRecord(value, "model");
|
|
4393
|
+
const price = {
|
|
4394
|
+
id: priceString(source, "id", "model.id"),
|
|
4395
|
+
slug: priceString(source, "slug", "model.slug"),
|
|
4396
|
+
...parseRates(source, "model")
|
|
4397
|
+
};
|
|
4398
|
+
if (source.peak !== void 0) price.peak = parseRates(priceRecord(source.peak, "model.peak"), "model.peak");
|
|
4399
|
+
if (source.contextTiers !== void 0) {
|
|
4400
|
+
if (!Array.isArray(source.contextTiers) || source.contextTiers.length === 0) priceReject("contextTiers");
|
|
4401
|
+
let previous = 0;
|
|
4402
|
+
price.contextTiers = source.contextTiers.map((value, index, tiers) => {
|
|
4403
|
+
const tier = priceRecord(value, "contextTier");
|
|
4404
|
+
const out = parseRates(tier, "contextTier");
|
|
4405
|
+
if (tier.maxContext !== void 0) {
|
|
4406
|
+
const max = priceNumber(tier, "maxContext", "contextTier.maxContext");
|
|
4407
|
+
if (!Number.isSafeInteger(max) || max <= previous || index === tiers.length - 1) priceReject("contextTier.maxContext");
|
|
4408
|
+
previous = max;
|
|
4409
|
+
out.maxContext = max;
|
|
4410
|
+
} else if (index !== tiers.length - 1) priceReject("contextTier.maxContext");
|
|
4411
|
+
return out;
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
if (source.free !== void 0) price.free = priceBoolean(source, "free", "model.free");
|
|
4415
|
+
return price;
|
|
4416
|
+
}
|
|
4417
|
+
/** Parse the wire result into a {@link CommandCodePriceTable}. */
|
|
4418
|
+
function parsePriceTable(value) {
|
|
4419
|
+
const source = priceRecord(value, "result");
|
|
4420
|
+
const models = source.models;
|
|
4421
|
+
if (!Array.isArray(models)) priceReject("models");
|
|
4422
|
+
const peakHours = source.peakHours;
|
|
4423
|
+
if (!Array.isArray(peakHours)) priceReject("peakHours");
|
|
4424
|
+
return {
|
|
4425
|
+
models: models.map(parseModelPrice),
|
|
4426
|
+
peakHours: peakHours.map((window) => {
|
|
4427
|
+
if (!Array.isArray(window) || window.length !== 2) priceReject("peakHours[]");
|
|
4428
|
+
const [start, end] = window;
|
|
4429
|
+
if (typeof start !== "number" || typeof end !== "number") priceReject("peakHours[]");
|
|
4430
|
+
return [start, end];
|
|
4431
|
+
})
|
|
4432
|
+
};
|
|
4433
|
+
}
|
|
4434
|
+
/**
|
|
4435
|
+
* The price-table invocation descriptor, sharing the same `commandcodeUsage`
|
|
4436
|
+
* service and `commandcode` namespace as the report and catalog endpoints.
|
|
4437
|
+
*/
|
|
4438
|
+
const PRICES_DESCRIPTOR = makeRemoteDescriptor(PRICES_ENDPOINT, "prices", `${USAGE_REMOTE_PACKAGE}#CommandCodePriceTable`, { parse: parsePriceTable });
|
|
3136
4439
|
//#endregion
|
|
3137
4440
|
//#region src/login-wire.ts
|
|
3138
4441
|
/** The canonical endpoint paths of the three login Remotes. */
|
|
@@ -3232,6 +4535,15 @@ var CommandCodeUsageService = class extends TypertRemoteService {
|
|
|
3232
4535
|
return this.deps.listModels?.() ?? { models: [] };
|
|
3233
4536
|
}
|
|
3234
4537
|
/**
|
|
4538
|
+
* The model price table the composer prices an in-progress session with.
|
|
4539
|
+
* Static vendored data (the official pricing page's rates), served Host-side
|
|
4540
|
+
* so the browser bundle never carries a copy that could drift from the
|
|
4541
|
+
* snapshot, and so a price update reaches an open page without a rebuild.
|
|
4542
|
+
*/
|
|
4543
|
+
async prices() {
|
|
4544
|
+
return (this.deps.prices ?? modelPriceTable)();
|
|
4545
|
+
}
|
|
4546
|
+
/**
|
|
3235
4547
|
* Start (or rejoin) a browser-login attempt and return its fresh status —
|
|
3236
4548
|
* `waiting` carrying the Studio URL. Rejects when the flow cannot start
|
|
3237
4549
|
* (no free loopback port, disposed plugin); the Gateway folds the throw
|
|
@@ -3268,6 +4580,7 @@ function applyUsageRemote(ctx, deps) {
|
|
|
3268
4580
|
invocations: [
|
|
3269
4581
|
...USAGE_HOST_CONTRIBUTION.invocations,
|
|
3270
4582
|
MODELS_DESCRIPTOR,
|
|
4583
|
+
PRICES_DESCRIPTOR,
|
|
3271
4584
|
...LOGIN_DESCRIPTORS
|
|
3272
4585
|
]
|
|
3273
4586
|
});
|
|
@@ -4370,6 +5683,7 @@ function readModelVisibility(raw) {
|
|
|
4370
5683
|
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
4371
5684
|
}
|
|
4372
5685
|
function apply(ctx, config) {
|
|
5686
|
+
installCostProjection(ctx);
|
|
4373
5687
|
let current = () => config;
|
|
4374
5688
|
let lastRaw;
|
|
4375
5689
|
let lastGood;
|