@mars-sea/dsh-commandcode-provider 0.10.5 → 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 +47 -0
- package/README.md +8 -4
- 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 +2180 -715
- 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,288 +12,27 @@ 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/
|
|
15
|
+
//#region src/capabilities.ts
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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:
|
|
17
|
+
* Static capability snapshot for the Command Code provider: model →
|
|
18
|
+
* reasoning-effort levels, vision/thinking flags, model → minimum plan tier,
|
|
19
|
+
* subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
|
|
23
20
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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.
|
|
21
|
+
* Everything in this module is synced from official sources (the command-code
|
|
22
|
+
* CLI bundle's model table and the official plan/pricing/model docs — see the
|
|
23
|
+
* dsh-commandcode-upstream skill for the exact extraction procedures), and
|
|
24
|
+
* changes whenever an upstream CLI release reshuffles models/plans/prices.
|
|
25
|
+
* Keeping the snapshot in its own module confines those frequent sync diffs
|
|
26
|
+
* here: src/adapter.ts holds only the stable wire/runtime logic and imports
|
|
27
|
+
* these tables + read helpers.
|
|
39
28
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
29
|
+
* Snapshot read helpers (planLabel, dealLabel, formatContext,
|
|
30
|
+
* capabilityDescription, peakPricing*, compareByPlan, modelVisibleInPlan,
|
|
31
|
+
* subscriptionPlanInfo, isFreeModel) live here too — they exist only to read
|
|
32
|
+
* the tables, so a sync never has to touch src/adapter.ts.
|
|
42
33
|
*
|
|
43
|
-
*
|
|
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
|
-
//#region src/capabilities.ts
|
|
277
|
-
/**
|
|
278
|
-
* Static capability snapshot for the Command Code provider: model →
|
|
279
|
-
* reasoning-effort levels, vision/thinking flags, model → minimum plan tier,
|
|
280
|
-
* subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
|
|
281
|
-
*
|
|
282
|
-
* Everything in this module is synced from official sources (the command-code
|
|
283
|
-
* CLI bundle's model table and the official plan/pricing/model docs — see the
|
|
284
|
-
* dsh-commandcode-upstream skill for the exact extraction procedures), and
|
|
285
|
-
* changes whenever an upstream CLI release reshuffles models/plans/prices.
|
|
286
|
-
* Keeping the snapshot in its own module confines those frequent sync diffs
|
|
287
|
-
* here: src/adapter.ts holds only the stable wire/runtime logic and imports
|
|
288
|
-
* these tables + read helpers.
|
|
289
|
-
*
|
|
290
|
-
* Snapshot read helpers (planLabel, dealLabel, formatContext,
|
|
291
|
-
* capabilityDescription, peakPricing*, compareByPlan, modelVisibleInPlan,
|
|
292
|
-
* subscriptionPlanInfo, isFreeModel) live here too — they exist only to read
|
|
293
|
-
* the tables, so a sync never has to touch src/adapter.ts.
|
|
294
|
-
*
|
|
295
|
-
* Ported from pi-commandcode-provider (MIT); originally part of src/adapter.ts
|
|
296
|
-
* and split out so upstream syncs stay reviewable.
|
|
34
|
+
* Ported from pi-commandcode-provider (MIT); originally part of src/adapter.ts
|
|
35
|
+
* and split out so upstream syncs stay reviewable.
|
|
297
36
|
*/
|
|
298
37
|
const KNOWN_EFFORTS = {
|
|
299
38
|
"Qwen/Qwen3.8-Max": [
|
|
@@ -597,422 +336,1740 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
597
336
|
"z-ai/glm-5.3-flash"
|
|
598
337
|
]);
|
|
599
338
|
/**
|
|
600
|
-
* Models the official CLI's model table (command-code@1.53.0) marks
|
|
601
|
-
* `reasoning:!0` but defines no selectable `reasoning_effort` levels — they
|
|
602
|
-
* think automatically, with Command Code driving the depth. This is the
|
|
603
|
-
* authoritative "thinks, effort not adjustable" set: `KNOWN_EFFORTS` (which
|
|
604
|
-
* mirrors the CLI's effort map exactly) stays the sole source for selectable
|
|
605
|
-
* effort levels, and this snapshot is not surfaced in the picker's compact
|
|
606
|
-
* description — it exists for programmatic consumers.
|
|
339
|
+
* Models the official CLI's model table (command-code@1.53.0) marks
|
|
340
|
+
* `reasoning:!0` but defines no selectable `reasoning_effort` levels — they
|
|
341
|
+
* think automatically, with Command Code driving the depth. This is the
|
|
342
|
+
* authoritative "thinks, effort not adjustable" set: `KNOWN_EFFORTS` (which
|
|
343
|
+
* mirrors the CLI's effort map exactly) stays the sole source for selectable
|
|
344
|
+
* effort levels, and this snapshot is not surfaced in the picker's compact
|
|
345
|
+
* description — it exists for programmatic consumers.
|
|
346
|
+
*
|
|
347
|
+
* Source: the command-code@1.53.0 bundled model table (dist/cli.mjs),
|
|
348
|
+
* cross-checked with https://commandcode.ai/docs/reference/cli/models.
|
|
349
|
+
* (`stealth/ox-alpha` left this set in command-code@1.32.1, which gave it
|
|
350
|
+
* selectable `['low', 'high', 'max']` efforts; the preview then ended in
|
|
351
|
+
* 1.34.0, removing the model from the catalog entirely. `tencent/hy4-preview`
|
|
352
|
+
* joined this set in command-code@1.37.0 — reasoning:!0, no efforts, 1M
|
|
353
|
+
* context, routed through OpenRouter — then gained selectable
|
|
354
|
+
* `['low', 'medium', 'high']` efforts in command-code@1.38.0 and moved to
|
|
355
|
+
* `KNOWN_EFFORTS`. `moonshotai/Kimi-K3` followed the same path in
|
|
356
|
+
* command-code@1.39.3 — it gained `['low', 'high', 'max']` efforts and moved
|
|
357
|
+
* to `KNOWN_EFFORTS`. command-code@1.42.0 added `meituan/LongCat-2.0:free`
|
|
358
|
+
* (reasoning:!0, no efforts). command-code@1.45.0 gave the Muse Spark family
|
|
359
|
+
* (1.1, 1.2, 1.2-contributor, 1.3, 1.3-contributor) selectable
|
|
360
|
+
* `['low', 'medium', 'high', 'xhigh']` efforts — they moved to `KNOWN_EFFORTS`.
|
|
361
|
+
* command-code@1.51.3 gave `MiniMaxAI/MiniMax-M3` selectable
|
|
362
|
+
* `['low', 'medium', 'high']` efforts — it moved to `KNOWN_EFFORTS` too.
|
|
363
|
+
* command-code@1.52.0 added `inclusionai/ling-3.0-flash-sante:free`
|
|
364
|
+
* (reasoning:!0, no efforts).)
|
|
365
|
+
* command-code@1.53.0 added `deepseek/deepseek-v4.1-flash` with selectable
|
|
366
|
+
* ['low', 'high', 'max'] efforts, so it lives in `KNOWN_EFFORTS`, not here.)
|
|
367
|
+
* Keep in sync via the dsh-commandcode-upstream skill.
|
|
368
|
+
*/
|
|
369
|
+
const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
|
|
370
|
+
"Qwen/Qwen3.6-Max-Preview",
|
|
371
|
+
"Qwen/Qwen3.6-Plus",
|
|
372
|
+
"Qwen/Qwen3.7-Flash",
|
|
373
|
+
"Qwen/Qwen3.7-Max",
|
|
374
|
+
"Qwen/Qwen3.7-Plus",
|
|
375
|
+
"moonshotai/Kimi-K2.7-Code",
|
|
376
|
+
"moonshotai/Kimi-K2.7-Code-Highspeed",
|
|
377
|
+
"stepfun/Step-3.5-Flash",
|
|
378
|
+
"stepfun/Step-3.7-Flash",
|
|
379
|
+
"tencent/hy3-paid",
|
|
380
|
+
"nvidia/nemotron-3-ultra-550b-a55b",
|
|
381
|
+
"thinkingmachines/inkling",
|
|
382
|
+
"thinkingmachines/inkling-small",
|
|
383
|
+
"poolside/laguna-s-2.1-free",
|
|
384
|
+
"meituan/LongCat-2.0:free",
|
|
385
|
+
"inclusionai/ling-3.0-flash-sante:free"
|
|
386
|
+
]);
|
|
387
|
+
/**
|
|
388
|
+
* The minimum subscription plan a model is included in, per the official plan
|
|
389
|
+
* pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`, `/docs/plans/max`
|
|
390
|
+
* and `/docs/resources/pricing-limits`). Each plan's model list is a superset of
|
|
391
|
+
* the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max. Models absent from every
|
|
392
|
+
* plan list (Claude Opus/Fable, Fugu Ultra) are Provider-tier.
|
|
393
|
+
* `claude-fable-5-1` (Claude Fable 5.1, added in command-code@1.40.0) is
|
|
394
|
+
* Provider/Max-tier exactly like `claude-fable-5` — its availability matrix on
|
|
395
|
+
* the official plan/pricing pages grants individual-provider/max/ultra and
|
|
396
|
+
* teams-pro only, and the CLI's plan-access map blocks it on Go/GOAT/Pro.
|
|
397
|
+
* command-code@1.41.0 added `Qwen/Qwen3.8-Max-0902` (Go) and 1.42.0 added
|
|
398
|
+
* `meituan/LongCat-2.0:free` (Go, free promo); command-code@1.43.0 added
|
|
399
|
+
* `google/gemini-3.8-flash` (GOAT) and 1.44.0 added `meta/muse-spark-1.3`
|
|
400
|
+
* (GOAT) plus its Contributor sibling (Go); command-code@1.52.0 added the
|
|
401
|
+
* free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
|
|
402
|
+
* added `deepseek/deepseek-v4.1-flash` (Go).
|
|
403
|
+
*
|
|
404
|
+
* The Provider API exposes no plan metadata, so this snapshot is the source of
|
|
405
|
+
* truth for the picker's plan annotation — it answers "which plan do I need to
|
|
406
|
+
* actually use this model?" at a glance. Plan labels use the official tier
|
|
407
|
+
* names (`Go`, `GOAT`, `Pro`, `Provider`), with `Max` implying Provider.
|
|
408
|
+
*
|
|
409
|
+
* Keep in sync with the official plan pages when they change (see the
|
|
410
|
+
* dsh-commandcode-upstream skill).
|
|
411
|
+
*/
|
|
412
|
+
const KNOWN_PLANS = {
|
|
413
|
+
"MiniMaxAI/MiniMax-M2.5": "go",
|
|
414
|
+
"MiniMaxAI/MiniMax-M2.7": "go",
|
|
415
|
+
"MiniMaxAI/MiniMax-M3": "go",
|
|
416
|
+
"Qwen/Qwen3.6-Max-Preview": "go",
|
|
417
|
+
"Qwen/Qwen3.6-Plus": "go",
|
|
418
|
+
"Qwen/Qwen3.7-Flash": "go",
|
|
419
|
+
"Qwen/Qwen3.7-Max": "go",
|
|
420
|
+
"Qwen/Qwen3.7-Plus": "go",
|
|
421
|
+
"Qwen/Qwen3.8-27B": "go",
|
|
422
|
+
"Qwen/Qwen3.8-Flash": "go",
|
|
423
|
+
"Qwen/Qwen3.8-Max": "go",
|
|
424
|
+
"Qwen/Qwen3.8-Max-0902": "go",
|
|
425
|
+
"deepseek/deepseek-v4-flash-fast": "go",
|
|
426
|
+
"deepseek/deepseek-v4.1-flash": "go",
|
|
427
|
+
"deepseek/deepseek-v4-flash": "go",
|
|
428
|
+
"deepseek/deepseek-v4-flash-vision-exp": "go",
|
|
429
|
+
"deepseek/deepseek-v4-pro": "go",
|
|
430
|
+
"gpt-5.6-luna": "go",
|
|
431
|
+
"meituan/LongCat-2.0:free": "go",
|
|
432
|
+
"inclusionai/ling-3.0-flash-sante:free": "go",
|
|
433
|
+
"meta/muse-spark-1.2-contributor": "go",
|
|
434
|
+
"meta/muse-spark-1.3-contributor": "go",
|
|
435
|
+
"moonshotai/Kimi-K2.5": "go",
|
|
436
|
+
"moonshotai/Kimi-K2.6": "go",
|
|
437
|
+
"moonshotai/Kimi-K2.7-Code": "go",
|
|
438
|
+
"moonshotai/Kimi-K2.7-Code-Highspeed": "go",
|
|
439
|
+
"moonshotai/Kimi-K3": "go",
|
|
440
|
+
"nvidia/nemotron-3-ultra-550b-a55b": "go",
|
|
441
|
+
"poolside/laguna-s-2.1-free": "go",
|
|
442
|
+
"stepfun/Step-3.5-Flash": "go",
|
|
443
|
+
"stepfun/Step-3.7-Flash": "go",
|
|
444
|
+
"tencent/hy3-paid": "go",
|
|
445
|
+
"tencent/hy4-preview": "go",
|
|
446
|
+
"thinkingmachines/inkling": "go",
|
|
447
|
+
"thinkingmachines/inkling-small": "go",
|
|
448
|
+
"xai/grok-4.5": "go",
|
|
449
|
+
"xiaomi/mimo-v2.5": "go",
|
|
450
|
+
"xiaomi/mimo-v2.5-pro": "go",
|
|
451
|
+
"z-ai/glm-5.3-flash": "go",
|
|
452
|
+
"zai-org/GLM-5": "go",
|
|
453
|
+
"zai-org/GLM-5.1": "go",
|
|
454
|
+
"zai-org/GLM-5.2": "go",
|
|
455
|
+
"zai-org/GLM-5.2-Fast": "go",
|
|
456
|
+
"zai-org/GLM-5.3": "go",
|
|
457
|
+
"google/gemini-3.7-flash": "goat",
|
|
458
|
+
"google/gemini-3.8-flash": "goat",
|
|
459
|
+
"gpt-5.6-sol": "goat",
|
|
460
|
+
"meta/muse-spark-1.2": "goat",
|
|
461
|
+
"meta/muse-spark-1.3": "goat",
|
|
462
|
+
"xai/grok-4.6": "goat",
|
|
463
|
+
"claude-haiku-4-5-20251001": "pro",
|
|
464
|
+
"claude-sonnet-4-6": "pro",
|
|
465
|
+
"claude-sonnet-5": "pro",
|
|
466
|
+
"google/gemini-3.1-flash-lite": "pro",
|
|
467
|
+
"google/gemini-3.5-flash": "pro",
|
|
468
|
+
"google/gemini-3.5-flash-lite": "pro",
|
|
469
|
+
"google/gemini-3.6-flash": "pro",
|
|
470
|
+
"gpt-5.3-codex": "pro",
|
|
471
|
+
"gpt-5.4": "pro",
|
|
472
|
+
"gpt-5.4-mini": "pro",
|
|
473
|
+
"gpt-5.5": "pro",
|
|
474
|
+
"gpt-5.6-terra": "pro",
|
|
475
|
+
"meta/muse-spark-1.1": "pro",
|
|
476
|
+
"claude-fable-5-1": "provider",
|
|
477
|
+
"claude-fable-5": "provider",
|
|
478
|
+
"claude-opus-4-7": "provider",
|
|
479
|
+
"claude-opus-4-8": "provider",
|
|
480
|
+
"claude-opus-5": "provider",
|
|
481
|
+
"gpt-6-astra": "provider",
|
|
482
|
+
"sakana/fugu-ultra": "provider"
|
|
483
|
+
};
|
|
484
|
+
/** Official display labels for each plan tier. */
|
|
485
|
+
const PLAN_LABELS = {
|
|
486
|
+
go: "Go",
|
|
487
|
+
goat: "GOAT",
|
|
488
|
+
pro: "Pro",
|
|
489
|
+
provider: "Provider",
|
|
490
|
+
max: "Max"
|
|
491
|
+
};
|
|
492
|
+
/**
|
|
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.
|
|
715
|
+
*/
|
|
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
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Official display label for a model's minimum plan, or undefined for models
|
|
723
|
+
* outside the snapshot (e.g. future catalog additions).
|
|
724
|
+
*/
|
|
725
|
+
function planLabel(modelId) {
|
|
726
|
+
const plan = KNOWN_PLANS[modelId];
|
|
727
|
+
return plan === void 0 ? void 0 : PLAN_LABELS[plan];
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
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.
|
|
735
|
+
*/
|
|
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;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
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.
|
|
781
|
+
*
|
|
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.
|
|
790
|
+
*/
|
|
791
|
+
const MODEL_PRICE_ROWS = [
|
|
792
|
+
{
|
|
793
|
+
id: "claude-fable-5",
|
|
794
|
+
rates: [
|
|
795
|
+
10,
|
|
796
|
+
50,
|
|
797
|
+
1,
|
|
798
|
+
12.5
|
|
799
|
+
]
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
id: "claude-fable-5-1",
|
|
803
|
+
rates: [
|
|
804
|
+
10,
|
|
805
|
+
50,
|
|
806
|
+
.25,
|
|
807
|
+
12.5
|
|
808
|
+
]
|
|
809
|
+
},
|
|
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
|
+
]
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
id: "qwen-3.8-max-0902",
|
|
1461
|
+
rates: [
|
|
1462
|
+
2,
|
|
1463
|
+
6,
|
|
1464
|
+
.25
|
|
1465
|
+
]
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
id: "step-3.5-flash",
|
|
1469
|
+
rates: [
|
|
1470
|
+
.1,
|
|
1471
|
+
.3,
|
|
1472
|
+
.02
|
|
1473
|
+
]
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
id: "step-3.7-flash",
|
|
1477
|
+
rates: [
|
|
1478
|
+
.2,
|
|
1479
|
+
1.15,
|
|
1480
|
+
.04
|
|
1481
|
+
]
|
|
1482
|
+
},
|
|
1483
|
+
{
|
|
1484
|
+
id: "tencent/hy3-paid",
|
|
1485
|
+
rates: [
|
|
1486
|
+
.14,
|
|
1487
|
+
.58,
|
|
1488
|
+
.035
|
|
1489
|
+
]
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
id: "tencent/hy4-preview",
|
|
1493
|
+
rates: [
|
|
1494
|
+
.834,
|
|
1495
|
+
2.501,
|
|
1496
|
+
.042
|
|
1497
|
+
]
|
|
1498
|
+
}
|
|
1499
|
+
];
|
|
1500
|
+
/**
|
|
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.
|
|
1505
|
+
*/
|
|
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.
|
|
607
1509
|
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
* context, routed through OpenRouter — then gained selectable
|
|
615
|
-
* `['low', 'medium', 'high']` efforts in command-code@1.38.0 and moved to
|
|
616
|
-
* `KNOWN_EFFORTS`. `moonshotai/Kimi-K3` followed the same path in
|
|
617
|
-
* command-code@1.39.3 — it gained `['low', 'high', 'max']` efforts and moved
|
|
618
|
-
* to `KNOWN_EFFORTS`. command-code@1.42.0 added `meituan/LongCat-2.0:free`
|
|
619
|
-
* (reasoning:!0, no efforts). command-code@1.45.0 gave the Muse Spark family
|
|
620
|
-
* (1.1, 1.2, 1.2-contributor, 1.3, 1.3-contributor) selectable
|
|
621
|
-
* `['low', 'medium', 'high', 'xhigh']` efforts — they moved to `KNOWN_EFFORTS`.
|
|
622
|
-
* command-code@1.51.3 gave `MiniMaxAI/MiniMax-M3` selectable
|
|
623
|
-
* `['low', 'medium', 'high']` efforts — it moved to `KNOWN_EFFORTS` too.
|
|
624
|
-
* command-code@1.52.0 added `inclusionai/ling-3.0-flash-sante:free`
|
|
625
|
-
* (reasoning:!0, no efforts).)
|
|
626
|
-
* command-code@1.53.0 added `deepseek/deepseek-v4.1-flash` with selectable
|
|
627
|
-
* ['low', 'high', 'max'] efforts, so it lives in `KNOWN_EFFORTS`, not here.)
|
|
628
|
-
* Keep in sync via the dsh-commandcode-upstream skill.
|
|
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.
|
|
629
1516
|
*/
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
"
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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;
|
|
1563
|
+
}
|
|
648
1564
|
/**
|
|
649
|
-
*
|
|
650
|
-
* pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`, `/docs/plans/max`
|
|
651
|
-
* and `/docs/resources/pricing-limits`). Each plan's model list is a superset of
|
|
652
|
-
* the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max. Models absent from every
|
|
653
|
-
* plan list (Claude Opus/Fable, Fugu Ultra) are Provider-tier.
|
|
654
|
-
* `claude-fable-5-1` (Claude Fable 5.1, added in command-code@1.40.0) is
|
|
655
|
-
* Provider/Max-tier exactly like `claude-fable-5` — its availability matrix on
|
|
656
|
-
* the official plan/pricing pages grants individual-provider/max/ultra and
|
|
657
|
-
* teams-pro only, and the CLI's plan-access map blocks it on Go/GOAT/Pro.
|
|
658
|
-
* command-code@1.41.0 added `Qwen/Qwen3.8-Max-0902` (Go) and 1.42.0 added
|
|
659
|
-
* `meituan/LongCat-2.0:free` (Go, free promo); command-code@1.43.0 added
|
|
660
|
-
* `google/gemini-3.8-flash` (GOAT) and 1.44.0 added `meta/muse-spark-1.3`
|
|
661
|
-
* (GOAT) plus its Contributor sibling (Go); command-code@1.52.0 added the
|
|
662
|
-
* free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
|
|
663
|
-
* added `deepseek/deepseek-v4.1-flash` (Go).
|
|
1565
|
+
* Build the table the composer prices a session with.
|
|
664
1566
|
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
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.
|
|
669
1574
|
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
1575
|
+
* The peak windows travel with the table, so the browser applies the very
|
|
1576
|
+
* schedule this snapshot knows instead of restating it.
|
|
672
1577
|
*/
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
"
|
|
716
|
-
"
|
|
717
|
-
"
|
|
718
|
-
"
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
"claude-haiku-4-5-20251001": "pro",
|
|
725
|
-
"claude-sonnet-4-6": "pro",
|
|
726
|
-
"claude-sonnet-5": "pro",
|
|
727
|
-
"google/gemini-3.1-flash-lite": "pro",
|
|
728
|
-
"google/gemini-3.5-flash": "pro",
|
|
729
|
-
"google/gemini-3.5-flash-lite": "pro",
|
|
730
|
-
"google/gemini-3.6-flash": "pro",
|
|
731
|
-
"gpt-5.3-codex": "pro",
|
|
732
|
-
"gpt-5.4": "pro",
|
|
733
|
-
"gpt-5.4-mini": "pro",
|
|
734
|
-
"gpt-5.5": "pro",
|
|
735
|
-
"gpt-5.6-terra": "pro",
|
|
736
|
-
"meta/muse-spark-1.1": "pro",
|
|
737
|
-
"claude-fable-5-1": "provider",
|
|
738
|
-
"claude-fable-5": "provider",
|
|
739
|
-
"claude-opus-4-7": "provider",
|
|
740
|
-
"claude-opus-4-8": "provider",
|
|
741
|
-
"claude-opus-5": "provider",
|
|
742
|
-
"gpt-6-astra": "provider",
|
|
743
|
-
"sakana/fugu-ultra": "provider"
|
|
744
|
-
};
|
|
745
|
-
/** Official display labels for each plan tier. */
|
|
746
|
-
const PLAN_LABELS = {
|
|
747
|
-
go: "Go",
|
|
748
|
-
goat: "GOAT",
|
|
749
|
-
pro: "Pro",
|
|
750
|
-
provider: "Provider",
|
|
751
|
-
max: "Max"
|
|
752
|
-
};
|
|
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
|
+
};
|
|
1610
|
+
}
|
|
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
|
+
}
|
|
753
1629
|
/**
|
|
754
|
-
*
|
|
755
|
-
*
|
|
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.
|
|
756
1637
|
*/
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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;
|
|
1697
|
+
}
|
|
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
|
|
764
1815
|
/**
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
1816
|
+
* Multi-account pool for the Command Code provider (host side).
|
|
1817
|
+
*
|
|
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:
|
|
1822
|
+
*
|
|
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.
|
|
1838
|
+
*
|
|
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
|
|
1843
|
+
*/
|
|
1844
|
+
/**
|
|
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".
|
|
769
1852
|
*/
|
|
770
|
-
|
|
771
|
-
|
|
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();
|
|
772
1857
|
}
|
|
773
1858
|
/**
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
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.
|
|
777
1863
|
*/
|
|
778
|
-
function
|
|
779
|
-
|
|
780
|
-
if (
|
|
781
|
-
|
|
782
|
-
const pb = PLAN_ORDER[KNOWN_PLANS[b.id] ?? ""] ?? Number.MAX_SAFE_INTEGER;
|
|
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);
|
|
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;
|
|
787
1868
|
}
|
|
788
1869
|
/**
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
* account's own subscription (from `/alpha/billing/subscriptions`) — distinct
|
|
794
|
-
* from {@link KNOWN_PLANS}, which maps catalog models to their minimum tier.
|
|
795
|
-
*
|
|
796
|
-
* `tierWeight` is plugin-added (not from the CLI maps): the plan's rank on
|
|
797
|
-
* the {@link PLAN_ORDER} scale, used by the picker's plan filter
|
|
798
|
-
* ({@link modelVisibleInPlan}) to hide models above the account's tier.
|
|
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.
|
|
799
1874
|
*/
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
},
|
|
806
|
-
"individual-goat": {
|
|
807
|
-
name: "GOAT",
|
|
808
|
-
monthlyCredits: 70,
|
|
809
|
-
tierWeight: 1
|
|
810
|
-
},
|
|
811
|
-
"individual-pro": {
|
|
812
|
-
name: "Pro",
|
|
813
|
-
monthlyCredits: 30,
|
|
814
|
-
tierWeight: 2
|
|
815
|
-
},
|
|
816
|
-
"individual-pro-v1": {
|
|
817
|
-
name: "Pro",
|
|
818
|
-
monthlyCredits: 80,
|
|
819
|
-
tierWeight: 2
|
|
820
|
-
},
|
|
821
|
-
"individual-provider": {
|
|
822
|
-
name: "Provider",
|
|
823
|
-
monthlyCredits: 15,
|
|
824
|
-
tierWeight: 3
|
|
825
|
-
},
|
|
826
|
-
"individual-max": {
|
|
827
|
-
name: "Max",
|
|
828
|
-
monthlyCredits: 150,
|
|
829
|
-
tierWeight: 4
|
|
830
|
-
},
|
|
831
|
-
"individual-ultra": {
|
|
832
|
-
name: "Ultra",
|
|
833
|
-
monthlyCredits: 300,
|
|
834
|
-
tierWeight: 4
|
|
835
|
-
},
|
|
836
|
-
"teams-pro": {
|
|
837
|
-
name: "Teams Pro",
|
|
838
|
-
monthlyCredits: 40,
|
|
839
|
-
tierWeight: 2
|
|
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;
|
|
840
1880
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const SUBSCRIPTION_PLAN_PREFIXES = Object.keys(KNOWN_SUBSCRIPTION_PLANS).sort((a, b) => b.length - a.length);
|
|
1881
|
+
return usable[0];
|
|
1882
|
+
}
|
|
844
1883
|
/**
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
* normalize (lowercase, `_` → `-`), then longest-prefix match so
|
|
848
|
-
* `individual-pro-v1` wins over `individual-pro`. Unknown ids return
|
|
849
|
-
* `undefined`.
|
|
1884
|
+
* The first routing rule whose model list contains the request's model id.
|
|
1885
|
+
* Undefined when no rule matches.
|
|
850
1886
|
*/
|
|
851
|
-
function
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
return prefix === void 0 ? void 0 : KNOWN_SUBSCRIPTION_PLANS[prefix];
|
|
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;
|
|
855
1890
|
}
|
|
856
1891
|
/**
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
* final gate (`403 MODEL_NOT_IN_PLAN`).
|
|
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).
|
|
862
1896
|
*/
|
|
863
|
-
function
|
|
864
|
-
|
|
865
|
-
if (
|
|
866
|
-
|
|
867
|
-
const tier = KNOWN_PLANS[modelId];
|
|
868
|
-
if (tier === void 0) return true;
|
|
869
|
-
const weight = PLAN_ORDER[tier];
|
|
870
|
-
if (weight === void 0) return true;
|
|
871
|
-
return weight <= access.tierWeight;
|
|
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));
|
|
872
1901
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1902
|
+
/**
|
|
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.
|
|
1906
|
+
*/
|
|
1907
|
+
var CommandCodeAccountPool = class {
|
|
1908
|
+
deps;
|
|
1909
|
+
/** Rotation state by API key. */
|
|
1910
|
+
states = /* @__PURE__ */ new Map();
|
|
1911
|
+
constructor(deps) {
|
|
1912
|
+
this.deps = deps;
|
|
1913
|
+
}
|
|
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
|
+
});
|
|
888
2021
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
*/
|
|
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
|
-
/**
|
|
930
|
-
* Peak hours (UTC, hour-of-day range end-exclusive): 01–03 and 06–09.
|
|
931
|
-
* Weekday-only — see `peakPricingState()`; weekends are fully off-peak.
|
|
932
|
-
*/
|
|
933
|
-
const PEAK_HOUR_RANGES = [[1, 4], [6, 10]];
|
|
934
|
-
/**
|
|
935
|
-
* Whether `now` (defaults to `Date.now()`) falls in a peak-pricing window for
|
|
936
|
-
* time-of-day-priced models. Peak rates apply Monday–Friday (UTC) only: the
|
|
937
|
-
* official rule charges Saturday and Sunday completely off-peak for all 24
|
|
938
|
-
* hours, so a weekend timestamp is off-peak even inside `PEAK_HOUR_RANGES`.
|
|
939
|
-
* `undefined` for models outside the snapshot.
|
|
940
|
-
*/
|
|
941
|
-
function peakPricingState(modelId, now = Date.now()) {
|
|
942
|
-
if (!KNOWN_PEAK_PRICING.has(modelId)) return void 0;
|
|
943
|
-
const at = new Date(now);
|
|
944
|
-
const day = at.getUTCDay();
|
|
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";
|
|
948
|
-
}
|
|
949
|
-
/**
|
|
950
|
-
* Compact label for the current peak/off-peak state: `Peak` (full price) or
|
|
951
|
-
* `Half` (off-peak, half price). These English nouns match the picker's other
|
|
952
|
-
* markers (`Go`, `Image`, `FREE`), and since they appear only on time-of-day
|
|
953
|
-
* priced models they double as a "priced by the hour" signal. Returns undefined
|
|
954
|
-
* for models without time-of-day pricing.
|
|
955
|
-
*/
|
|
956
|
-
function peakPricingLabel(modelId, now = Date.now()) {
|
|
957
|
-
const state = peakPricingState(modelId, now);
|
|
958
|
-
if (state === void 0) return void 0;
|
|
959
|
-
return state === "peak" ? "Peak" : "Half";
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Official display label for a model's minimum plan, or undefined for models
|
|
963
|
-
* outside the snapshot (e.g. future catalog additions).
|
|
964
|
-
*/
|
|
965
|
-
function planLabel(modelId) {
|
|
966
|
-
const plan = KNOWN_PLANS[modelId];
|
|
967
|
-
return plan === void 0 ? void 0 : PLAN_LABELS[plan];
|
|
968
|
-
}
|
|
969
|
-
/**
|
|
970
|
-
* The active deal label for a model, or undefined when the model has no deal
|
|
971
|
-
* or the deal has expired. Expiry is judged against `now` (defaults to
|
|
972
|
-
* `Date.now()`), so a snapshot that has gone stale stops showing its discount
|
|
973
|
-
* the moment the official end date passes — the user never believes a lapsed
|
|
974
|
-
* deal is still live. Permanent deals (no `expiresAt`) never lapse.
|
|
975
|
-
*/
|
|
976
|
-
function dealLabel(modelId, now = Date.now()) {
|
|
977
|
-
const deal = KNOWN_DEALS[modelId];
|
|
978
|
-
if (deal === void 0) return void 0;
|
|
979
|
-
if (deal.expiresAt !== void 0 && now >= Date.parse(deal.expiresAt)) return void 0;
|
|
980
|
-
return deal.label;
|
|
981
|
-
}
|
|
982
|
-
/**
|
|
983
|
-
* Compact human-readable context window, e.g. `1_000_000 -> "1M"`,
|
|
984
|
-
* `256_000 -> "256K"`, `262_144 -> "262K"` (floor to the nearest K).
|
|
985
|
-
* Returns undefined for unknown/absent sizes; values under 1K render raw.
|
|
986
|
-
*/
|
|
987
|
-
function formatContext(contextWindow) {
|
|
988
|
-
if (contextWindow === void 0 || !Number.isFinite(contextWindow) || contextWindow <= 0) return;
|
|
989
|
-
if (contextWindow >= 1e6) {
|
|
990
|
-
const m = contextWindow / 1e6;
|
|
991
|
-
const rounded = Math.round(m * 10) / 10;
|
|
992
|
-
return `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}M`;
|
|
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
|
+
}
|
|
993
2055
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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 {
|
|
@@ -1105,6 +2217,124 @@ function recordOrEmpty(value) {
|
|
|
1105
2217
|
} catch {}
|
|
1106
2218
|
return {};
|
|
1107
2219
|
}
|
|
2220
|
+
/** How deep a root `$ref` / combinator chain is followed while normalizing. */
|
|
2221
|
+
const SCHEMA_NORMALIZE_MAX_DEPTH = 4;
|
|
2222
|
+
/**
|
|
2223
|
+
* Whether a type-less node is object-shaped enough to be one with the type
|
|
2224
|
+
* declared: `properties`/`required`/`additionalProperties` only make sense on
|
|
2225
|
+
* an object, and a schema carrying them was meant to be one.
|
|
2226
|
+
*/
|
|
2227
|
+
function isObjectShaped(node) {
|
|
2228
|
+
return isRecord(node.properties) || isRecord(node.patternProperties) || Array.isArray(node.required) || node.additionalProperties !== void 0;
|
|
2229
|
+
}
|
|
2230
|
+
/** The `required` names of one schema node, ignoring malformed entries. */
|
|
2231
|
+
function requiredNames(node) {
|
|
2232
|
+
return Array.isArray(node.required) ? node.required.filter((name) => typeof name === "string") : [];
|
|
2233
|
+
}
|
|
2234
|
+
/** Resolve a local `#/...` pointer inside the schema that carried it. */
|
|
2235
|
+
function resolveLocalRef(root, ref) {
|
|
2236
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
2237
|
+
let node = root;
|
|
2238
|
+
for (const rawSegment of ref.slice(2).split("/")) {
|
|
2239
|
+
const segment = rawSegment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
2240
|
+
if (!isRecord(node)) return void 0;
|
|
2241
|
+
node = node[segment];
|
|
2242
|
+
}
|
|
2243
|
+
return isRecord(node) ? node : void 0;
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Flatten a type-less combinator node into one object schema, so the model
|
|
2247
|
+
* still sees the branch fields instead of an argument-free tool. `allOf`
|
|
2248
|
+
* branches must all hold, so their `required` entries are all kept; `anyOf` /
|
|
2249
|
+
* `oneOf` branches are alternatives, so only a name required by every branch
|
|
2250
|
+
* survives. Returns undefined when no branch describes an object.
|
|
2251
|
+
*/
|
|
2252
|
+
function mergeCombinatorBranches(node, depth) {
|
|
2253
|
+
if (depth >= SCHEMA_NORMALIZE_MAX_DEPTH) return void 0;
|
|
2254
|
+
const properties = {};
|
|
2255
|
+
const required = /* @__PURE__ */ new Set();
|
|
2256
|
+
const alternatives = [];
|
|
2257
|
+
let sawBranch = false;
|
|
2258
|
+
for (const key of [
|
|
2259
|
+
"allOf",
|
|
2260
|
+
"anyOf",
|
|
2261
|
+
"oneOf"
|
|
2262
|
+
]) {
|
|
2263
|
+
const branches = node[key];
|
|
2264
|
+
if (!Array.isArray(branches)) continue;
|
|
2265
|
+
const objectBranches = [];
|
|
2266
|
+
for (const branch of branches) {
|
|
2267
|
+
if (!isRecord(branch)) continue;
|
|
2268
|
+
objectBranches.push(toolParametersSchema(branch, depth + 1));
|
|
2269
|
+
}
|
|
2270
|
+
if (objectBranches.length === 0) continue;
|
|
2271
|
+
sawBranch = true;
|
|
2272
|
+
const names = objectBranches.map(requiredNames);
|
|
2273
|
+
if (key === "allOf") for (const name of names.flat()) required.add(name);
|
|
2274
|
+
else if (names.length > 0) alternatives.push(names[0].filter((name) => names.every((list) => list.includes(name))));
|
|
2275
|
+
for (const branch of objectBranches) {
|
|
2276
|
+
const branchProperties = isRecord(branch.properties) ? branch.properties : {};
|
|
2277
|
+
for (const [name, schema] of Object.entries(branchProperties)) if (!(name in properties)) properties[name] = schema;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
if (!sawBranch) return void 0;
|
|
2281
|
+
for (const group of alternatives) for (const name of group) required.add(name);
|
|
2282
|
+
const merged = {
|
|
2283
|
+
type: "object",
|
|
2284
|
+
properties
|
|
2285
|
+
};
|
|
2286
|
+
if (required.size > 0) merged.required = [...required];
|
|
2287
|
+
if (node.additionalProperties !== void 0) merged.additionalProperties = node.additionalProperties;
|
|
2288
|
+
for (const key of ["description", "title"]) if (node[key] !== void 0) merged[key] = node[key];
|
|
2289
|
+
return merged;
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Normalize one tool's parameters schema to an object-rooted JSON Schema.
|
|
2293
|
+
* A schema that already declares an object root is passed through untouched;
|
|
2294
|
+
* a type-less object-shaped one gains the type; a root `$ref` or combinator is
|
|
2295
|
+
* resolved/merged; anything else (an array/scalar root, or no schema at all)
|
|
2296
|
+
* degrades to a permissive free-form object, because a request the provider
|
|
2297
|
+
* refuses helps no one and the tool's own description is still in the prompt.
|
|
2298
|
+
*/
|
|
2299
|
+
function toolParametersSchema(parameters, depth = 0) {
|
|
2300
|
+
if (!isRecord(parameters)) return {
|
|
2301
|
+
type: "object",
|
|
2302
|
+
properties: {},
|
|
2303
|
+
additionalProperties: true
|
|
2304
|
+
};
|
|
2305
|
+
if (parameters.type === "object") return parameters;
|
|
2306
|
+
if (Array.isArray(parameters.type) && parameters.type.includes("object")) return {
|
|
2307
|
+
...parameters,
|
|
2308
|
+
type: "object"
|
|
2309
|
+
};
|
|
2310
|
+
if (parameters.type === void 0 || parameters.type === null) {
|
|
2311
|
+
if (typeof parameters.$ref === "string" && depth < SCHEMA_NORMALIZE_MAX_DEPTH) {
|
|
2312
|
+
const target = resolveLocalRef(parameters, parameters.$ref);
|
|
2313
|
+
if (target !== void 0) {
|
|
2314
|
+
const { $ref: _ref, ...rest } = parameters;
|
|
2315
|
+
return toolParametersSchema({
|
|
2316
|
+
...target,
|
|
2317
|
+
...rest
|
|
2318
|
+
}, depth + 1);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
if (isObjectShaped(parameters)) return {
|
|
2322
|
+
...parameters,
|
|
2323
|
+
type: "object"
|
|
2324
|
+
};
|
|
2325
|
+
const merged = mergeCombinatorBranches(parameters, depth);
|
|
2326
|
+
if (merged !== void 0) return merged;
|
|
2327
|
+
if (typeof parameters.$ref === "string") return {
|
|
2328
|
+
...parameters,
|
|
2329
|
+
type: "object"
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
return {
|
|
2333
|
+
type: "object",
|
|
2334
|
+
properties: {},
|
|
2335
|
+
additionalProperties: true
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
1108
2338
|
function projectSlugFromPath(pathName) {
|
|
1109
2339
|
return pathName.toLowerCase().replace(/^[a-z]:/i, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|(?<!-)-+$/g, "") || "project";
|
|
1110
2340
|
}
|
|
@@ -1281,28 +2511,84 @@ function toolResultTextForWire(media) {
|
|
|
1281
2511
|
return media.text || (media.images.length > 0 ? "(image returned; see the attached image)" : "");
|
|
1282
2512
|
}
|
|
1283
2513
|
/** Leading line of the user message that carries a tool result's images. */
|
|
1284
|
-
const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result
|
|
2514
|
+
const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result";
|
|
1285
2515
|
/**
|
|
1286
2516
|
* The model-visible note introducing the images carried out of one tool
|
|
1287
2517
|
* result. Neither transport merges them back into the tool result, so this
|
|
1288
2518
|
* line is what tells the model the image it is about to see belongs to the
|
|
1289
2519
|
* tool it just ran rather than to the user; it also leads the part list,
|
|
1290
|
-
* because an image-first content array is what some gateways reject.
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
2520
|
+
* because an image-first content array is what some gateways reject.
|
|
2521
|
+
*
|
|
2522
|
+
* The note names the tool call the image came out of, because a turn's
|
|
2523
|
+
* carriers are emitted as a group after the whole tool group (issue #33):
|
|
2524
|
+
* position alone would have to carry the association, and every `read_image`
|
|
2525
|
+
* result renders the same envelope text, so two parallel calls would produce
|
|
2526
|
+
* two identical notes. The id is the WIRE id — the one the model saw on the
|
|
2527
|
+
* `tool-call` it issued and on the tool message just above — so an overlong
|
|
2528
|
+
* cross-provider id is named by the alias that replaced it, not by the
|
|
2529
|
+
* harness-side id the model never saw.
|
|
2530
|
+
*
|
|
2531
|
+
* Count and pixel dimensions are appended when the tool's own text does not
|
|
2532
|
+
* already state them (a result that returns the image and nothing else).
|
|
1293
2533
|
*/
|
|
1294
|
-
function toolResultImageNote(media) {
|
|
2534
|
+
function toolResultImageNote(media, toolCallId) {
|
|
2535
|
+
const lead = `${TOOL_RESULT_IMAGE_TEXT} (${toolCallId}):`;
|
|
1295
2536
|
const first = media.images[0];
|
|
1296
|
-
if (first === void 0) return
|
|
2537
|
+
if (first === void 0) return lead;
|
|
1297
2538
|
const dimensions = `${first.width}x${first.height} px`;
|
|
1298
|
-
if (first.width <= 0 || first.height <= 0 || media.text.includes(dimensions)) return
|
|
1299
|
-
|
|
1300
|
-
return `${TOOL_RESULT_IMAGE_TEXT} ${count}${dimensions}`;
|
|
2539
|
+
if (first.width <= 0 || first.height <= 0 || media.text.includes(dimensions)) return lead;
|
|
2540
|
+
return `${lead} ${media.images.length > 1 ? `${media.images.length} images, ` : ""}${dimensions}`;
|
|
1301
2541
|
}
|
|
1302
2542
|
function hasImageContent(message) {
|
|
1303
2543
|
const check = (blocks) => blocks.some((b) => b.type === "image" || b.type === "tool-result" && check(b.content));
|
|
1304
2544
|
return check(message.content);
|
|
1305
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
|
+
}
|
|
1306
2592
|
/**
|
|
1307
2593
|
* Convert one image reference to the Command Code wire format, as the official
|
|
1308
2594
|
* CLI does: `{ type: 'image', source: { type: 'base64', media_type, data } }`.
|
|
@@ -1324,9 +2610,15 @@ async function messagesToCC(messages, readImage) {
|
|
|
1324
2610
|
const out = [];
|
|
1325
2611
|
const { ids: paired, names: toolNames } = pairedToolCalls(messages);
|
|
1326
2612
|
const wireIds = wireToolCallIds(paired);
|
|
2613
|
+
const pendingImages = [];
|
|
2614
|
+
const flushPendingImages = () => {
|
|
2615
|
+
for (const message of pendingImages) out.push(message);
|
|
2616
|
+
pendingImages.length = 0;
|
|
2617
|
+
};
|
|
1327
2618
|
for (const message of messages) {
|
|
1328
2619
|
if (message.role === "system") continue;
|
|
1329
2620
|
if (message.role === "user" && message.source.kind !== "tool") {
|
|
2621
|
+
flushPendingImages();
|
|
1330
2622
|
const parts = [];
|
|
1331
2623
|
for (const block of message.content) {
|
|
1332
2624
|
if (block.type === "text") parts.push({
|
|
@@ -1338,6 +2630,7 @@ async function messagesToCC(messages, readImage) {
|
|
|
1338
2630
|
parts.push(await imageToCommandCode(block.attachment, readImage));
|
|
1339
2631
|
}
|
|
1340
2632
|
}
|
|
2633
|
+
if (parts.length === 0) continue;
|
|
1341
2634
|
out.push({
|
|
1342
2635
|
role: "user",
|
|
1343
2636
|
content: parts
|
|
@@ -1345,11 +2638,16 @@ async function messagesToCC(messages, readImage) {
|
|
|
1345
2638
|
continue;
|
|
1346
2639
|
}
|
|
1347
2640
|
if (message.role === "assistant") {
|
|
2641
|
+
flushPendingImages();
|
|
1348
2642
|
const parts = [];
|
|
1349
2643
|
for (const block of message.content) if (block.type === "text") parts.push({
|
|
1350
2644
|
type: "text",
|
|
1351
2645
|
text: block.text
|
|
1352
2646
|
});
|
|
2647
|
+
else if (block.type === "reasoning") parts.push({
|
|
2648
|
+
type: "reasoning",
|
|
2649
|
+
text: block.text
|
|
2650
|
+
});
|
|
1353
2651
|
else if (block.type === "tool-call" && paired.has(block.id)) parts.push({
|
|
1354
2652
|
type: "tool-call",
|
|
1355
2653
|
toolCallId: wireIds.get(block.id) ?? block.id,
|
|
@@ -1366,11 +2664,12 @@ async function messagesToCC(messages, readImage) {
|
|
|
1366
2664
|
const block = message.content[0];
|
|
1367
2665
|
if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
|
|
1368
2666
|
const media = toolResultMedia(block);
|
|
2667
|
+
const wireToolCallId = wireIds.get(block.toolCallId) ?? block.toolCallId;
|
|
1369
2668
|
out.push({
|
|
1370
2669
|
role: "tool",
|
|
1371
2670
|
content: [{
|
|
1372
2671
|
type: "tool-result",
|
|
1373
|
-
toolCallId:
|
|
2672
|
+
toolCallId: wireToolCallId,
|
|
1374
2673
|
toolName: toolNames.get(block.toolCallId) || "unknown",
|
|
1375
2674
|
output: block.isError ? {
|
|
1376
2675
|
type: "error-text",
|
|
@@ -1385,16 +2684,17 @@ async function messagesToCC(messages, readImage) {
|
|
|
1385
2684
|
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1386
2685
|
const carried = [{
|
|
1387
2686
|
type: "text",
|
|
1388
|
-
text: toolResultImageNote(media)
|
|
2687
|
+
text: toolResultImageNote(media, wireToolCallId)
|
|
1389
2688
|
}];
|
|
1390
2689
|
for (const attachment of media.images) carried.push(await imageToCommandCode(attachment, readImage));
|
|
1391
|
-
|
|
2690
|
+
pendingImages.push({
|
|
1392
2691
|
role: "user",
|
|
1393
2692
|
content: carried
|
|
1394
2693
|
});
|
|
1395
2694
|
}
|
|
1396
2695
|
}
|
|
1397
2696
|
}
|
|
2697
|
+
flushPendingImages();
|
|
1398
2698
|
return out;
|
|
1399
2699
|
}
|
|
1400
2700
|
/**
|
|
@@ -1422,9 +2722,15 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
1422
2722
|
const out = [];
|
|
1423
2723
|
const { ids: paired } = pairedToolCalls(messages);
|
|
1424
2724
|
const wireIds = wireToolCallIds(paired);
|
|
2725
|
+
const pendingImages = [];
|
|
2726
|
+
const flushPendingImages = () => {
|
|
2727
|
+
for (const message of pendingImages) out.push(message);
|
|
2728
|
+
pendingImages.length = 0;
|
|
2729
|
+
};
|
|
1425
2730
|
for (const message of messages) {
|
|
1426
2731
|
if (message.role === "system") continue;
|
|
1427
2732
|
if (message.role === "user" && message.source.kind !== "tool") {
|
|
2733
|
+
flushPendingImages();
|
|
1428
2734
|
const parts = [];
|
|
1429
2735
|
for (const block of message.content) {
|
|
1430
2736
|
if (block.type === "text") parts.push({
|
|
@@ -1448,6 +2754,7 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
1448
2754
|
continue;
|
|
1449
2755
|
}
|
|
1450
2756
|
if (message.role === "assistant") {
|
|
2757
|
+
flushPendingImages();
|
|
1451
2758
|
const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1452
2759
|
const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
|
|
1453
2760
|
const toolCalls = message.content.filter((block) => block.type === "tool-call" && paired.has(block.id)).map((block) => ({
|
|
@@ -1472,25 +2779,27 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
1472
2779
|
const block = message.content[0];
|
|
1473
2780
|
if (!block || block.type !== "tool-result" || !paired.has(block.toolCallId)) continue;
|
|
1474
2781
|
const media = toolResultMedia(block);
|
|
2782
|
+
const wireToolCallId = wireIds.get(block.toolCallId) ?? block.toolCallId;
|
|
1475
2783
|
out.push({
|
|
1476
2784
|
role: "tool",
|
|
1477
|
-
tool_call_id:
|
|
2785
|
+
tool_call_id: wireToolCallId,
|
|
1478
2786
|
content: toolResultTextForWire(media)
|
|
1479
2787
|
});
|
|
1480
2788
|
if (media.images.length > 0) {
|
|
1481
2789
|
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
1482
2790
|
const carried = [{
|
|
1483
2791
|
type: "text",
|
|
1484
|
-
text: toolResultImageNote(media)
|
|
2792
|
+
text: toolResultImageNote(media, wireToolCallId)
|
|
1485
2793
|
}];
|
|
1486
2794
|
for (const attachment of media.images) carried.push(await imageToOpenAI(attachment, readImage));
|
|
1487
|
-
|
|
2795
|
+
pendingImages.push({
|
|
1488
2796
|
role: "user",
|
|
1489
2797
|
content: carried
|
|
1490
2798
|
});
|
|
1491
2799
|
}
|
|
1492
2800
|
}
|
|
1493
2801
|
}
|
|
2802
|
+
flushPendingImages();
|
|
1494
2803
|
return out;
|
|
1495
2804
|
}
|
|
1496
2805
|
/** Account endpoints fetched by one `getUsage()` run (see the classification there). */
|
|
@@ -1514,13 +2823,22 @@ function parseUsageTotals(usage) {
|
|
|
1514
2823
|
};
|
|
1515
2824
|
}
|
|
1516
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
|
+
*/
|
|
1517
2834
|
function parseWindowLimit(value) {
|
|
1518
2835
|
const block = isRecord(value) ? value : void 0;
|
|
2836
|
+
if (block === void 0) return void 0;
|
|
1519
2837
|
return {
|
|
1520
|
-
used: numberValue(block
|
|
1521
|
-
cap: numberValue(block
|
|
1522
|
-
exceeded: block
|
|
1523
|
-
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
|
|
1524
2842
|
};
|
|
1525
2843
|
}
|
|
1526
2844
|
/**
|
|
@@ -1534,13 +2852,19 @@ function parseCreditLimits(credits) {
|
|
|
1534
2852
|
const fiveHour = windowLimits !== void 0 && isRecord(windowLimits.fiveHour) ? windowLimits.fiveHour : void 0;
|
|
1535
2853
|
const weekly = windowLimits !== void 0 && isRecord(windowLimits.weekly) ? windowLimits.weekly : void 0;
|
|
1536
2854
|
if (creditsData === void 0 && fiveHour === void 0 && weekly === void 0) return void 0;
|
|
1537
|
-
|
|
2855
|
+
const parsed = {
|
|
1538
2856
|
monthlyCredits: numberValue(creditsData?.monthlyCredits) ?? 0,
|
|
1539
2857
|
purchasedCredits: numberValue(creditsData?.purchasedCredits) ?? 0,
|
|
1540
2858
|
freeCredits: numberValue(creditsData?.freeCredits) ?? 0,
|
|
1541
|
-
|
|
1542
|
-
|
|
2859
|
+
monthlyReported: numberValue(creditsData?.monthlyCredits) !== void 0,
|
|
2860
|
+
purchasedReported: numberValue(creditsData?.purchasedCredits) !== void 0,
|
|
2861
|
+
freeReported: numberValue(creditsData?.freeCredits) !== void 0
|
|
1543
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;
|
|
1544
2868
|
}
|
|
1545
2869
|
/**
|
|
1546
2870
|
* Parse the account identity from whoami plus the plan identity from the
|
|
@@ -1595,7 +2919,7 @@ async function buildCliBody(options, connection, facts) {
|
|
|
1595
2919
|
type: "function",
|
|
1596
2920
|
name: tool.name,
|
|
1597
2921
|
description: tool.description,
|
|
1598
|
-
input_schema: tool.parameters
|
|
2922
|
+
input_schema: toolParametersSchema(tool.parameters)
|
|
1599
2923
|
})),
|
|
1600
2924
|
system: facts.systemText,
|
|
1601
2925
|
max_tokens: facts.maxTokens,
|
|
@@ -1617,7 +2941,7 @@ async function buildOpenAIBody(options, facts) {
|
|
|
1617
2941
|
function: {
|
|
1618
2942
|
name: tool.name,
|
|
1619
2943
|
description: tool.description,
|
|
1620
|
-
parameters: tool.parameters
|
|
2944
|
+
parameters: toolParametersSchema(tool.parameters)
|
|
1621
2945
|
}
|
|
1622
2946
|
}));
|
|
1623
2947
|
return {
|
|
@@ -2136,12 +3460,14 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
2136
3460
|
const reasoningEffort = effort && effort !== "off" && supported?.includes(effort) ? effort : void 0;
|
|
2137
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");
|
|
2138
3462
|
let protocol = this.resolveProtocol(apiKey);
|
|
2139
|
-
|
|
3463
|
+
let requestOptions = withImageBudget(options, REQUEST_IMAGE_BUDGETS[0]);
|
|
3464
|
+
let imageBudgetIndex = 0;
|
|
3465
|
+
const buildBody = async (target) => target === "cli" ? buildCliBody(requestOptions, connection, {
|
|
2140
3466
|
maxTokens,
|
|
2141
3467
|
reasoningEffort,
|
|
2142
3468
|
systemText,
|
|
2143
3469
|
readImage
|
|
2144
|
-
}) : buildOpenAIBody(
|
|
3470
|
+
}) : buildOpenAIBody(requestOptions, {
|
|
2145
3471
|
maxTokens,
|
|
2146
3472
|
reasoningEffort,
|
|
2147
3473
|
systemText,
|
|
@@ -2168,6 +3494,15 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
2168
3494
|
body = await buildBody("cli");
|
|
2169
3495
|
continue;
|
|
2170
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
|
+
}
|
|
2171
3506
|
const rotate = this.deps.rotateApiKey;
|
|
2172
3507
|
if ((attempt.status === 429 || attempt.status === 401) && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
|
|
2173
3508
|
const next = await rotate(apiKey, attempt.status === 429 ? "rate-limit" : "invalid-credential", connection, options.model);
|
|
@@ -2260,26 +3595,68 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
2260
3595
|
* Map a pre-stream generate HTTP failure onto a stable LlmError. Command
|
|
2261
3596
|
* Code folds several business rejections into 403 (plan limits, CLI version,
|
|
2262
3597
|
* model access): prefer the machine-readable `error.code` when present; the
|
|
2263
|
-
* 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
|
|
2264
3602
|
* `providerRetryAfterMs` so dsh-llm-retry can wait exactly that long instead
|
|
2265
3603
|
* of guessing at the backoff cadence — capped at RETRY_MAX_DELAY_MS, because
|
|
2266
3604
|
* in normal mode a longer attached wait makes the executor abandon the retry
|
|
2267
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.
|
|
2268
3611
|
*/
|
|
2269
3612
|
function generateHttpError(status, errText, retryAfterMs) {
|
|
2270
3613
|
let providerCode;
|
|
3614
|
+
let providerDetail = "";
|
|
2271
3615
|
try {
|
|
2272
3616
|
const parsed = JSON.parse(errText);
|
|
2273
|
-
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
|
+
}
|
|
2274
3625
|
} catch {}
|
|
2275
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 });
|
|
2276
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 });
|
|
2277
|
-
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), {
|
|
2278
3632
|
status,
|
|
2279
3633
|
...retryAfterMs !== void 0 && retryAfterMs > 0 && retryAfterMs <= 9e5 ? { providerRetryAfterMs: retryAfterMs } : {}
|
|
2280
3634
|
});
|
|
2281
3635
|
}
|
|
2282
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
|
+
/**
|
|
2283
3660
|
* Handle one CLI-transport (`/alpha/generate`) stream event, appending
|
|
2284
3661
|
* harness StreamChunks. Pure over the passed assembler — no stream() locals.
|
|
2285
3662
|
*/
|
|
@@ -2386,16 +3763,7 @@ function handleCliEvent(asm, event) {
|
|
|
2386
3763
|
});
|
|
2387
3764
|
break;
|
|
2388
3765
|
}
|
|
2389
|
-
case "error":
|
|
2390
|
-
const err = isRecord(event.error) ? event.error : void 0;
|
|
2391
|
-
const detail = isRecord(event.error) ? stringValue(event.error.message) ?? JSON.stringify(event.error) : stringValue(event.error) ?? stringValue(event.message) ?? "Stream error";
|
|
2392
|
-
const statusCode = err ? numberValue(err.statusCode) : void 0;
|
|
2393
|
-
const isRetryable = err ? booleanValue(err.isRetryable) : void 0;
|
|
2394
|
-
const retryableStatus = statusCode !== void 0 && (statusCode === 429 || statusCode >= 500);
|
|
2395
|
-
const terminal = hasTerminalStreamMarker(detail);
|
|
2396
|
-
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);
|
|
2397
|
-
throw new LlmError(`Command Code stream error: ${detail}`, "SERVER", statusCode !== void 0 ? { status: statusCode } : void 0);
|
|
2398
|
-
}
|
|
3766
|
+
case "error": throw streamErrorToLlmError(event.error, stringValue(event.message));
|
|
2399
3767
|
}
|
|
2400
3768
|
return chunks;
|
|
2401
3769
|
}
|
|
@@ -2407,6 +3775,7 @@ function handleCliEvent(asm, event) {
|
|
|
2407
3775
|
function handleOpenAIEvent(asm, event) {
|
|
2408
3776
|
const chunks = [];
|
|
2409
3777
|
if (!isRecord(event)) return chunks;
|
|
3778
|
+
if (event.error !== void 0) throw streamErrorToLlmError(event.error, stringValue(event.message));
|
|
2410
3779
|
const choices = event.choices;
|
|
2411
3780
|
if (!Array.isArray(choices) || choices.length === 0) {
|
|
2412
3781
|
if (event.usage !== void 0) chunks.push({
|
|
@@ -2729,8 +4098,13 @@ function renderReport(report, locale, title) {
|
|
|
2729
4098
|
}
|
|
2730
4099
|
if (report.credits) {
|
|
2731
4100
|
const c = report.credits;
|
|
2732
|
-
const
|
|
2733
|
-
|
|
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, "");
|
|
2734
4108
|
}
|
|
2735
4109
|
if (report.failures.length > 0) lines.push(commandCopy(locale, "partialFailures").replace("{list}", report.failures.join("; ")), "");
|
|
2736
4110
|
if (!report.account && !report.usage && !report.credits) lines.push(commandCopy(locale, "noData"), "");
|
|
@@ -2846,7 +4220,16 @@ const USAGE_REPORT_ENDPOINT = "commandcode/report";
|
|
|
2846
4220
|
*/
|
|
2847
4221
|
const { reject: reject$1, record: record$1, stringField: stringField$1, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
|
|
2848
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
|
+
*/
|
|
2849
4231
|
function windowLimit(value, field) {
|
|
4232
|
+
if (value === void 0) return void 0;
|
|
2850
4233
|
const source = record$1(value, field);
|
|
2851
4234
|
return {
|
|
2852
4235
|
used: numberField(source, "used", `${field}.used`),
|
|
@@ -2894,13 +4277,18 @@ function parseUsageReport(value) {
|
|
|
2894
4277
|
}
|
|
2895
4278
|
if (source.credits !== void 0) {
|
|
2896
4279
|
const credits = record$1(source.credits, "credits");
|
|
2897
|
-
|
|
4280
|
+
const parsed = {
|
|
2898
4281
|
monthlyCredits: numberField(credits, "monthlyCredits", "credits.monthlyCredits"),
|
|
2899
4282
|
purchasedCredits: numberField(credits, "purchasedCredits", "credits.purchasedCredits"),
|
|
2900
|
-
freeCredits: numberField(credits, "freeCredits", "credits.freeCredits")
|
|
2901
|
-
fiveHour: windowLimit(credits.fiveHour, "credits.fiveHour"),
|
|
2902
|
-
weekly: windowLimit(credits.weekly, "credits.weekly")
|
|
4283
|
+
freeCredits: numberField(credits, "freeCredits", "credits.freeCredits")
|
|
2903
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;
|
|
2904
4292
|
}
|
|
2905
4293
|
if (source.plan !== void 0) {
|
|
2906
4294
|
const plan = record$1(source.plan, "plan");
|
|
@@ -2982,6 +4370,72 @@ function parseCatalog(value) {
|
|
|
2982
4370
|
* service and `commandcode` namespace as the usage report.
|
|
2983
4371
|
*/
|
|
2984
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 });
|
|
2985
4439
|
//#endregion
|
|
2986
4440
|
//#region src/login-wire.ts
|
|
2987
4441
|
/** The canonical endpoint paths of the three login Remotes. */
|
|
@@ -3081,6 +4535,15 @@ var CommandCodeUsageService = class extends TypertRemoteService {
|
|
|
3081
4535
|
return this.deps.listModels?.() ?? { models: [] };
|
|
3082
4536
|
}
|
|
3083
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
|
+
/**
|
|
3084
4547
|
* Start (or rejoin) a browser-login attempt and return its fresh status —
|
|
3085
4548
|
* `waiting` carrying the Studio URL. Rejects when the flow cannot start
|
|
3086
4549
|
* (no free loopback port, disposed plugin); the Gateway folds the throw
|
|
@@ -3117,6 +4580,7 @@ function applyUsageRemote(ctx, deps) {
|
|
|
3117
4580
|
invocations: [
|
|
3118
4581
|
...USAGE_HOST_CONTRIBUTION.invocations,
|
|
3119
4582
|
MODELS_DESCRIPTOR,
|
|
4583
|
+
PRICES_DESCRIPTOR,
|
|
3120
4584
|
...LOGIN_DESCRIPTORS
|
|
3121
4585
|
]
|
|
3122
4586
|
});
|
|
@@ -4219,6 +5683,7 @@ function readModelVisibility(raw) {
|
|
|
4219
5683
|
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
4220
5684
|
}
|
|
4221
5685
|
function apply(ctx, config) {
|
|
5686
|
+
installCostProjection(ctx);
|
|
4222
5687
|
let current = () => config;
|
|
4223
5688
|
let lastRaw;
|
|
4224
5689
|
let lastGood;
|