@mars-sea/dsh-commandcode-provider 0.11.17 → 0.12.1
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 +117 -1
- package/README.md +16 -10
- package/README.zh-CN.md +13 -7
- package/lib/client.js +197 -123
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +267 -92
- package/lib/index.js +2502 -717
- package/lib/index.js.map +1 -1
- package/package.json +40 -37
package/lib/index.js
CHANGED
|
@@ -6,12 +6,96 @@ import { CONTEXT_WINDOW_EXCEEDED_CODE, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter,
|
|
|
6
6
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
7
7
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
8
8
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
9
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
import { appendFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
10
10
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
11
11
|
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/cost-facts.ts
|
|
16
|
+
const zeroCostTokens = () => ({
|
|
17
|
+
uncachedInputTokens: 0,
|
|
18
|
+
outputTokens: 0,
|
|
19
|
+
cacheReadTokens: 0,
|
|
20
|
+
cacheWriteTokens: 0
|
|
21
|
+
});
|
|
22
|
+
const COST_TOKEN_KEYS = [
|
|
23
|
+
"uncachedInputTokens",
|
|
24
|
+
"outputTokens",
|
|
25
|
+
"cacheReadTokens",
|
|
26
|
+
"cacheWriteTokens"
|
|
27
|
+
];
|
|
28
|
+
function peakHour(at, windows) {
|
|
29
|
+
const time = new Date(at);
|
|
30
|
+
return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* All prompt billing buckets determine a request's band, never cumulative
|
|
34
|
+
* session input.
|
|
35
|
+
*
|
|
36
|
+
* A tiered model returns the matching band and does NOT then apply `peak`: the
|
|
37
|
+
* page publishes those two dimensions independently and no row carries both
|
|
38
|
+
* today, so the combination becoming real is visible before it reaches a user.
|
|
39
|
+
* This ordering is a latent choice, not a verified upstream rule.
|
|
40
|
+
*/
|
|
41
|
+
function requestRates(price, at, contextTokens, table) {
|
|
42
|
+
const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
|
|
43
|
+
if (tier !== void 0) return tier;
|
|
44
|
+
return price.peak !== void 0 && at !== null && peakHour(at, table.peakHours) ? price.peak : price;
|
|
45
|
+
}
|
|
46
|
+
/** Cache version and wire guard: changing bands/rates must refold historical groups. */
|
|
47
|
+
function pricingKey(table) {
|
|
48
|
+
const rates = (r) => [
|
|
49
|
+
r.inputCost,
|
|
50
|
+
r.outputCost,
|
|
51
|
+
r.cacheReadCost,
|
|
52
|
+
r.cacheWriteCost ?? null
|
|
53
|
+
];
|
|
54
|
+
const canonical = JSON.stringify([
|
|
55
|
+
"commandCodeCost-v1",
|
|
56
|
+
table.peakHours,
|
|
57
|
+
table.models.map((p) => [
|
|
58
|
+
p.id,
|
|
59
|
+
p.slug,
|
|
60
|
+
p.free === true,
|
|
61
|
+
rates(p),
|
|
62
|
+
p.peak ? rates(p.peak) : null,
|
|
63
|
+
p.contextTiers?.map((t) => [t.maxContext ?? null, rates(t)]) ?? null
|
|
64
|
+
])
|
|
65
|
+
]);
|
|
66
|
+
let hash = 2166136261;
|
|
67
|
+
for (const c of canonical) hash = Math.imul(hash ^ c.charCodeAt(0), 16777619) >>> 0;
|
|
68
|
+
return hash;
|
|
69
|
+
}
|
|
70
|
+
/** Only safe to merge requests whose billing classification is identical. */
|
|
71
|
+
function costGroupKey(group, table) {
|
|
72
|
+
const price = table.models.find((p) => p.id === group.model || p.slug === group.model);
|
|
73
|
+
return JSON.stringify([
|
|
74
|
+
group.provider,
|
|
75
|
+
group.model,
|
|
76
|
+
price === void 0 ? null : requestRates(price, group.at, group.contextTokens, table),
|
|
77
|
+
group.at === null
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/plan-tiers.ts
|
|
82
|
+
/** Shared plan-tier presentation facts; safe to inline into the browser bundle. */
|
|
83
|
+
const PLAN_TIER_ORDER = [
|
|
84
|
+
"go",
|
|
85
|
+
"goat",
|
|
86
|
+
"pro",
|
|
87
|
+
"provider",
|
|
88
|
+
"max"
|
|
89
|
+
];
|
|
90
|
+
const PLAN_LABELS = {
|
|
91
|
+
go: "Go",
|
|
92
|
+
goat: "GOAT",
|
|
93
|
+
pro: "Pro",
|
|
94
|
+
provider: "Provider",
|
|
95
|
+
max: "Max"
|
|
96
|
+
};
|
|
97
|
+
const PLAN_ORDER = Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier, index]));
|
|
98
|
+
//#endregion
|
|
15
99
|
//#region src/capabilities.ts
|
|
16
100
|
/**
|
|
17
101
|
* Static capability snapshot for the Command Code provider: model →
|
|
@@ -19,7 +103,7 @@ import { WebError } from "@deepseek-ai/dsh-web";
|
|
|
19
103
|
* subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
|
|
20
104
|
*
|
|
21
105
|
* Everything here is synced from official sources — the command-code CLI
|
|
22
|
-
* bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.
|
|
106
|
+
* bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.72.1) and
|
|
23
107
|
* the official plan/pricing/model docs; see the dsh-commandcode-upstream skill
|
|
24
108
|
* for the extraction procedures. Keeping the snapshot in its own module
|
|
25
109
|
* confines those frequent sync diffs here: src/adapter.ts holds only the stable
|
|
@@ -103,6 +187,13 @@ const KNOWN_EFFORTS = {
|
|
|
103
187
|
"xhigh",
|
|
104
188
|
"max"
|
|
105
189
|
],
|
|
190
|
+
"claude-sonnet-5-5": [
|
|
191
|
+
"low",
|
|
192
|
+
"medium",
|
|
193
|
+
"high",
|
|
194
|
+
"xhigh",
|
|
195
|
+
"max"
|
|
196
|
+
],
|
|
106
197
|
"deepseek/deepseek-v4-flash-fast": [
|
|
107
198
|
"low",
|
|
108
199
|
"high",
|
|
@@ -113,6 +204,11 @@ const KNOWN_EFFORTS = {
|
|
|
113
204
|
"high",
|
|
114
205
|
"max"
|
|
115
206
|
],
|
|
207
|
+
"deepseek/deepseek-v4.1-flash-fast": [
|
|
208
|
+
"low",
|
|
209
|
+
"high",
|
|
210
|
+
"max"
|
|
211
|
+
],
|
|
116
212
|
"deepseek/deepseek-v4-flash": ["high", "max"],
|
|
117
213
|
"deepseek/deepseek-v4-flash-vision-exp": ["high", "max"],
|
|
118
214
|
"deepseek/deepseek-v4-pro": ["high", "max"],
|
|
@@ -312,6 +408,18 @@ const KNOWN_EFFORTS = {
|
|
|
312
408
|
"low",
|
|
313
409
|
"medium",
|
|
314
410
|
"xhigh"
|
|
411
|
+
],
|
|
412
|
+
"gpt-6.1-sol": [
|
|
413
|
+
"low",
|
|
414
|
+
"medium",
|
|
415
|
+
"high",
|
|
416
|
+
"xhigh",
|
|
417
|
+
"max"
|
|
418
|
+
],
|
|
419
|
+
"inclusionai/ling-3.1-flash:free": [
|
|
420
|
+
"low",
|
|
421
|
+
"medium",
|
|
422
|
+
"high"
|
|
315
423
|
]
|
|
316
424
|
};
|
|
317
425
|
/**
|
|
@@ -348,8 +456,10 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
348
456
|
"claude-opus-5",
|
|
349
457
|
"claude-sonnet-4-6",
|
|
350
458
|
"claude-sonnet-5",
|
|
459
|
+
"claude-sonnet-5-5",
|
|
351
460
|
"deepseek/deepseek-v4-flash-vision-exp",
|
|
352
461
|
"deepseek/deepseek-v4.1-flash",
|
|
462
|
+
"deepseek/deepseek-v4.1-flash-fast",
|
|
353
463
|
"google/gemini-3.1-flash-lite",
|
|
354
464
|
"google/gemini-3.5-flash",
|
|
355
465
|
"google/gemini-3.5-flash-lite",
|
|
@@ -366,6 +476,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
366
476
|
"gpt-6-astra",
|
|
367
477
|
"gpt-6-luna",
|
|
368
478
|
"gpt-6-sol",
|
|
479
|
+
"gpt-6.1-sol",
|
|
369
480
|
"meta/muse-spark-1.1",
|
|
370
481
|
"meta/muse-spark-1.2",
|
|
371
482
|
"meta/muse-spark-1.2-contributor",
|
|
@@ -395,12 +506,13 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
395
506
|
]);
|
|
396
507
|
/**
|
|
397
508
|
* Models WITHOUT a zero-data-retention upstream, per the official CLI's own
|
|
398
|
-
* registry (`command-code@1.
|
|
509
|
+
* registry (`command-code@1.72.1` `dist/cli.mjs`): `modelSupportsZdr(id)` is
|
|
399
510
|
* exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
|
|
400
511
|
* carries the same membership in the sibling route table — the UNION of both
|
|
401
512
|
* is this set. Reading only the sibling route table would drop `meituan/
|
|
402
|
-
* LongCat-2.0
|
|
403
|
-
*
|
|
513
|
+
* LongCat-2.0`, which appears in `modelSupportsZdr` alone (re-read from the
|
|
514
|
+
* 1.66.0 / 1.67.0 / 1.68.0 artifacts on 2026-09-29: the two stealth-preview
|
|
515
|
+
* rows sit in BOTH sets, so LongCat is the only divergence). The official docs (commandcode.ai/docs/resources/
|
|
404
516
|
* zdr) put it in prose — "99% of our models have ZDR-capable upstreams … only
|
|
405
517
|
* a small handful of models are affected" — so the CLI's exclusion list is
|
|
406
518
|
* the only per-model evidence there is; a ZDR request naming one of these
|
|
@@ -422,12 +534,15 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
422
534
|
* (its `zdr:{only:[…]}` provider routes and the per-provider `zdr`/`noTraining`
|
|
423
535
|
* flags) is upstream-internal routing, not a per-model contract, so this table
|
|
424
536
|
* is the snapshot of the exclusion set and nothing more. It is a rare change:
|
|
425
|
-
* 20 members held across 1.62.0 → 1.64.0,
|
|
426
|
-
*
|
|
537
|
+
* 20 members held across 1.62.0 → 1.64.0, 1.65.0 and 1.66.0 each added exactly
|
|
538
|
+
* one (the two stealth-preview models below), 1.67.0 added one
|
|
539
|
+
* (`deepseek/deepseek-v4.1-flash-fast`), and 1.68.0 changed nothing — 23
|
|
540
|
+
* members as of 2026-09-29.
|
|
427
541
|
*/
|
|
428
542
|
const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
|
|
429
543
|
"MiniMaxAI/MiniMax-M3",
|
|
430
544
|
"Qwen/Qwen3.8-Max-0902",
|
|
545
|
+
"deepseek/deepseek-v4.1-flash-fast",
|
|
431
546
|
"meituan/LongCat-2.0",
|
|
432
547
|
"meta/muse-spark-1.1",
|
|
433
548
|
"meta/muse-spark-1.2",
|
|
@@ -492,16 +607,22 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
|
|
|
492
607
|
/**
|
|
493
608
|
* Catalog models the Provider API serves ONLY through `/provider/v1/messages`
|
|
494
609
|
* (Anthropic Messages shape). Every other model in the catalog answers on
|
|
495
|
-
* `/provider/v1/chat/completions
|
|
496
|
-
* `Model "<id>" must be called via /provider/v1/messages (Anthropic Messages
|
|
497
|
-
* shape)`.
|
|
610
|
+
* `/provider/v1/chat/completions`.
|
|
498
611
|
*
|
|
499
|
-
* Measured 2026-09-16
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
612
|
+
* Measured 2026-09-16 (command-code@1.54.0, 69 models posted to
|
|
613
|
+
* `/provider/v1/chat/completions`): exactly the then-entire Claude family
|
|
614
|
+
* refused with HTTP 400 `Model "<id>" must be called via
|
|
615
|
+
* /provider/v1/messages (Anthropic Messages shape)`, and every one of them is
|
|
616
|
+
* routed normally by `/alpha/generate`.
|
|
617
|
+
*
|
|
618
|
+
* Re-measured 2026-09-29 against the 84-model catalog: the same ten
|
|
619
|
+
* `claude-*` ids are the only ones carrying `supported_endpoints:
|
|
620
|
+
* ["/messages"]`, and posting a non-Claude model to the endpoint answers 400
|
|
621
|
+
* `Model "<id>" is not supported on this endpoint. Use
|
|
622
|
+
* /provider/v1/chat/completions for OpenAI and OSS models.` The adapter routes
|
|
623
|
+
* on that field when the catalog supplies it, so this set is the FALLBACK for
|
|
624
|
+
* a cached or hand-built catalog, not the primary source — see
|
|
625
|
+
* `requiresMessagesEndpoint()`.
|
|
505
626
|
*
|
|
506
627
|
* Note what this list is NOT: it is not a plan gate. `claude-sonnet-5` is
|
|
507
628
|
* Pro-tier and `claude-opus-4-8` Provider-tier, so the accounts entitled to
|
|
@@ -510,20 +631,21 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
|
|
|
510
631
|
* request before this snapshot existed.
|
|
511
632
|
*
|
|
512
633
|
* `requiresMessagesEndpoint()` additionally treats any `claude-*` id as
|
|
513
|
-
* Messages-only, so a Claude model added upstream
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
* riding the CLI transport it already works on.
|
|
634
|
+
* Messages-only, so a Claude model added upstream is still routed correctly
|
|
635
|
+
* by an un-updated plugin instead of hard-failing with the 400 above. The
|
|
636
|
+
* worst case of that rule going stale the other way (upstream teaching
|
|
637
|
+
* `/provider/v1/chat/completions` to serve Claude) is one model riding a
|
|
638
|
+
* transport it already works on.
|
|
519
639
|
*
|
|
520
640
|
* Keep in sync when models ship (see the dsh-commandcode-upstream skill).
|
|
521
641
|
*/
|
|
522
642
|
const MESSAGES_ONLY_MODELS = /* @__PURE__ */ new Set([
|
|
643
|
+
"claude-sonnet-5-5",
|
|
523
644
|
"claude-sonnet-5",
|
|
524
645
|
"claude-sonnet-4-6",
|
|
525
646
|
"claude-fable-5-1",
|
|
526
647
|
"claude-fable-5",
|
|
648
|
+
"claude-opus-5-5",
|
|
527
649
|
"claude-opus-5",
|
|
528
650
|
"claude-opus-4-8",
|
|
529
651
|
"claude-opus-4-7",
|
|
@@ -534,13 +656,140 @@ function requiresMessagesEndpoint(modelId) {
|
|
|
534
656
|
return MESSAGES_ONLY_MODELS.has(modelId) || modelId.startsWith("claude-");
|
|
535
657
|
}
|
|
536
658
|
/**
|
|
659
|
+
* Per-model output ceilings, which `/provider/v1/models` does NOT publish (it
|
|
660
|
+
* carries `context_length` and `supported_endpoints` only) while every endpoint
|
|
661
|
+
* refuses a larger `max_tokens` per model. Sending the context window as the
|
|
662
|
+
* output budget is what produced issue #71: `claude-sonnet-5-5` has a
|
|
663
|
+
* 1 000 000-token window and a 128 000-token ceiling, the adapter derived
|
|
664
|
+
* 131072, and the endpoint refused every request of a fresh process.
|
|
665
|
+
*
|
|
666
|
+
* **Generated — do not hand-edit.** `scripts/sync-output-limits.mjs` rewrites
|
|
667
|
+
* this literal from models.dev; run it after a catalog sync, and use its
|
|
668
|
+
* `--check` in CI. Command Code resells these models through their VENDOR's own
|
|
669
|
+
* API, so the vendor's published ceiling is the value recorded: 76 of the 86
|
|
670
|
+
* catalog ids as of 2026-09-30. Each row carries a comment naming the entry it
|
|
671
|
+
* came from.
|
|
672
|
+
*
|
|
673
|
+
* Two of those 76 rest on more than a vendor lookup. `gpt-6.1-sol` (128000) comes
|
|
674
|
+
* from OpenAI's own row and is corroborated by every other provider carrying it.
|
|
675
|
+
* `inclusionai/ling-3.1-flash:free` (32768) is hand-recorded in the script's
|
|
676
|
+
* `MANUAL_CEILINGS`: models.dev has no `ling-3.1` entry yet, so it takes the Ling
|
|
677
|
+
* family's uniform ceiling. Both are still correctable — a stricter gateway costs
|
|
678
|
+
* one refused request, and `streamRequest()` remembers what the endpoint said.
|
|
679
|
+
*
|
|
680
|
+
* The 10 unlisted models are ones no vendor publishes and the aggregators
|
|
681
|
+
* disagree about. They are NOT a gap: a refusal states the ceiling in full, and
|
|
682
|
+
* `streamRequest()` reads it off, retries once and remembers it for the model
|
|
683
|
+
* (issue #71). A vendor figure the gateway turns out to be stricter than costs
|
|
684
|
+
* one round trip; a conservative guess costs half of every answer, forever —
|
|
685
|
+
* which is why the unlisted path defers to the endpoint rather than to a
|
|
686
|
+
* fallback here.
|
|
687
|
+
*
|
|
688
|
+
* An unlisted Messages-route model still falls back to
|
|
689
|
+
* {@link DEFAULT_MESSAGES_MAX_TOKENS} before any request is made, so a ceiling
|
|
690
|
+
* is never absent when the body is built.
|
|
691
|
+
*/
|
|
692
|
+
const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
|
|
693
|
+
["claude-fable-5", 128e3],
|
|
694
|
+
["claude-fable-5-1", 128e3],
|
|
695
|
+
["claude-haiku-4-5-20251001", 64e3],
|
|
696
|
+
["claude-opus-4-7", 128e3],
|
|
697
|
+
["claude-opus-4-8", 128e3],
|
|
698
|
+
["claude-opus-5", 128e3],
|
|
699
|
+
["claude-opus-5-5", 128e3],
|
|
700
|
+
["claude-sonnet-4-6", 128e3],
|
|
701
|
+
["claude-sonnet-5", 128e3],
|
|
702
|
+
["claude-sonnet-5-5", 128e3],
|
|
703
|
+
["deepseek/deepseek-v4-flash", 393216],
|
|
704
|
+
["deepseek/deepseek-v4-flash-vision-exp", 393216],
|
|
705
|
+
["deepseek/deepseek-v4-pro", 393216],
|
|
706
|
+
["google/gemini-3.1-flash-lite", 65536],
|
|
707
|
+
["google/gemini-3.5-flash", 65536],
|
|
708
|
+
["google/gemini-3.5-flash-lite", 65536],
|
|
709
|
+
["google/gemini-3.6-flash", 65536],
|
|
710
|
+
["google/gemini-3.7-flash", 65536],
|
|
711
|
+
["google/gemini-3.8-flash", 65536],
|
|
712
|
+
["gpt-5.3-codex", 128e3],
|
|
713
|
+
["gpt-5.4", 128e3],
|
|
714
|
+
["gpt-5.4-mini", 128e3],
|
|
715
|
+
["gpt-5.5", 128e3],
|
|
716
|
+
["gpt-5.6-luna", 128e3],
|
|
717
|
+
["gpt-5.6-sol", 128e3],
|
|
718
|
+
["gpt-5.6-terra", 128e3],
|
|
719
|
+
["gpt-6-astra", 128e3],
|
|
720
|
+
["gpt-6-luna", 128e3],
|
|
721
|
+
["gpt-6-sol", 128e3],
|
|
722
|
+
["gpt-6.1-sol", 128e3],
|
|
723
|
+
["inclusionai/ling-3.0-flash-sante:free", 32768],
|
|
724
|
+
["inclusionai/ling-3.1-flash:free", 32768],
|
|
725
|
+
["meituan/LongCat-2.0", 131072],
|
|
726
|
+
["meta/muse-spark-1.1", 131072],
|
|
727
|
+
["meta/muse-spark-1.2", 131072],
|
|
728
|
+
["meta/muse-spark-1.2-contributor", 131072],
|
|
729
|
+
["meta/muse-spark-1.3", 131072],
|
|
730
|
+
["meta/muse-spark-1.3-contributor", 131072],
|
|
731
|
+
["MiniMaxAI/MiniMax-M2.5", 131072],
|
|
732
|
+
["MiniMaxAI/MiniMax-M2.7", 131072],
|
|
733
|
+
["MiniMaxAI/MiniMax-M3", 512e3],
|
|
734
|
+
["moonshotai/Kimi-K2.6", 262144],
|
|
735
|
+
["moonshotai/Kimi-K2.7-Code", 262144],
|
|
736
|
+
["moonshotai/Kimi-K2.7-Code-Highspeed", 262144],
|
|
737
|
+
["moonshotai/Kimi-K3", 1048576],
|
|
738
|
+
["nvidia/nemotron-3-ultra-550b-a55b", 65536],
|
|
739
|
+
["poolside/laguna-s-2.1-free", 32768],
|
|
740
|
+
["Qwen/Qwen3.6-Max-Preview", 65536],
|
|
741
|
+
["Qwen/Qwen3.6-Plus", 65536],
|
|
742
|
+
["Qwen/Qwen3.7-Flash", 131072],
|
|
743
|
+
["Qwen/Qwen3.7-Max", 131072],
|
|
744
|
+
["Qwen/Qwen3.7-Plus", 131072],
|
|
745
|
+
["Qwen/Qwen3.8-Flash", 131072],
|
|
746
|
+
["Qwen/Qwen3.8-Max", 131072],
|
|
747
|
+
["Qwen/Qwen3.8-Omni-Flash", 131072],
|
|
748
|
+
["sakana/fugu-ultra", 1e6],
|
|
749
|
+
["stealth/pixel-canary", 131072],
|
|
750
|
+
["stealth/space-bunny-alpha", 524288],
|
|
751
|
+
["stepfun/Step-3.5-Flash", 256e3],
|
|
752
|
+
["stepfun/Step-3.7-Flash", 256e3],
|
|
753
|
+
["stepfun/Step-5-Preview", 65536],
|
|
754
|
+
["thinkingmachines/inkling", 65536],
|
|
755
|
+
["xai/grok-4.5", 5e5],
|
|
756
|
+
["xai/grok-4.6", 5e5],
|
|
757
|
+
["xai/grok-4.7", 5e5],
|
|
758
|
+
["xiaomi/mimo-v2.5", 131072],
|
|
759
|
+
["xiaomi/mimo-v2.5-pro", 131072],
|
|
760
|
+
["xiaomi/mimo-v2.6-flash", 131072],
|
|
761
|
+
["xiaomi/mimo-v2.6-pro", 131072],
|
|
762
|
+
["xiaomi/mimo-v2.6-pro-ultraspeed", 131072],
|
|
763
|
+
["z-ai/glm-5.3-flash", 131072],
|
|
764
|
+
["z-ai/glm-5.3-flashx", 131072],
|
|
765
|
+
["zai-org/GLM-5", 131072],
|
|
766
|
+
["zai-org/GLM-5.1", 131072],
|
|
767
|
+
["zai-org/GLM-5.2", 131072],
|
|
768
|
+
["zai-org/GLM-5.3", 131072]
|
|
769
|
+
]);
|
|
770
|
+
/**
|
|
771
|
+
* Output ceiling for a Messages-route model that {@link MODEL_OUTPUT_TOKEN_LIMITS}
|
|
772
|
+
* does not carry — today only models no vendor publishes yet. Deliberately
|
|
773
|
+
* conservative: a 400 on `max_tokens` is a wasted round trip on every request,
|
|
774
|
+
* whereas a lower ceiling only shortens an answer, and the endpoint states the
|
|
775
|
+
* real one the first time it refuses, at which point the runtime overrides this
|
|
776
|
+
* anyway (issue #71).
|
|
777
|
+
*
|
|
778
|
+
* 64 000 is not arbitrary: it is `claude-haiku-4-5-20251001`'s actual ceiling,
|
|
779
|
+
* the lowest in the Claude family. A Claude model added to the catalog before
|
|
780
|
+
* its row exists here therefore answers at a size the family really supports,
|
|
781
|
+
* and is corrected upward on its first rejection rather than clamped here
|
|
782
|
+
* forever.
|
|
783
|
+
*/
|
|
784
|
+
const DEFAULT_MESSAGES_MAX_TOKENS = 64e3;
|
|
785
|
+
/**
|
|
537
786
|
* The minimum subscription plan a model is included in, per the official plan
|
|
538
787
|
* pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`,
|
|
539
788
|
* `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
|
|
540
789
|
* list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
|
|
541
790
|
* Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
|
|
542
|
-
* Provider-tier. Re-verified at command-code@1.
|
|
543
|
-
* ids at
|
|
791
|
+
* Provider-tier. Re-verified at command-code@1.72.1 (2026-09-30): 86 catalog
|
|
792
|
+
* ids at 53/62/76/84 cumulative, a strict superset chain — every release since
|
|
544
793
|
* 1.49.0 has been additive with no tier move, and per-entry tags below name the
|
|
545
794
|
* release that added each row.
|
|
546
795
|
*
|
|
@@ -568,6 +817,7 @@ const KNOWN_PLANS = {
|
|
|
568
817
|
"Qwen/Qwen3.8-Omni-Flash": "go",
|
|
569
818
|
"deepseek/deepseek-v4-flash-fast": "go",
|
|
570
819
|
"deepseek/deepseek-v4.1-flash": "go",
|
|
820
|
+
"deepseek/deepseek-v4.1-flash-fast": "go",
|
|
571
821
|
"deepseek/deepseek-v4-flash": "go",
|
|
572
822
|
"deepseek/deepseek-v4-flash-vision-exp": "go",
|
|
573
823
|
"deepseek/deepseek-v4-pro": "go",
|
|
@@ -575,6 +825,7 @@ const KNOWN_PLANS = {
|
|
|
575
825
|
"gpt-6-luna": "go",
|
|
576
826
|
"meituan/LongCat-2.0": "go",
|
|
577
827
|
"inclusionai/ling-3.0-flash-sante:free": "go",
|
|
828
|
+
"inclusionai/ling-3.1-flash:free": "go",
|
|
578
829
|
"meta/muse-spark-1.2-contributor": "go",
|
|
579
830
|
"meta/muse-spark-1.3-contributor": "go",
|
|
580
831
|
"moonshotai/Kimi-K2.5": "go",
|
|
@@ -605,6 +856,7 @@ const KNOWN_PLANS = {
|
|
|
605
856
|
"zai-org/GLM-5.2": "go",
|
|
606
857
|
"zai-org/GLM-5.2-Fast": "go",
|
|
607
858
|
"zai-org/GLM-5.3": "go",
|
|
859
|
+
"claude-sonnet-5-5": "goat",
|
|
608
860
|
"google/gemini-3.7-flash": "goat",
|
|
609
861
|
"google/gemini-3.8-flash": "goat",
|
|
610
862
|
"gpt-5.6-sol": "goat",
|
|
@@ -634,26 +886,8 @@ const KNOWN_PLANS = {
|
|
|
634
886
|
"claude-opus-5-5": "provider",
|
|
635
887
|
"claude-opus-5": "provider",
|
|
636
888
|
"gpt-6-astra": "provider",
|
|
637
|
-
"sakana/fugu-ultra": "provider"
|
|
638
|
-
|
|
639
|
-
/** Official display labels for each plan tier. */
|
|
640
|
-
const PLAN_LABELS = {
|
|
641
|
-
go: "Go",
|
|
642
|
-
goat: "GOAT",
|
|
643
|
-
pro: "Pro",
|
|
644
|
-
provider: "Provider",
|
|
645
|
-
max: "Max"
|
|
646
|
-
};
|
|
647
|
-
/**
|
|
648
|
-
* Plan-tier sort weights, low to high. Models outside the snapshot (unknown
|
|
649
|
-
* plans) sort after every known tier, keeping known models predictable.
|
|
650
|
-
*/
|
|
651
|
-
const PLAN_ORDER = {
|
|
652
|
-
go: 0,
|
|
653
|
-
goat: 1,
|
|
654
|
-
pro: 2,
|
|
655
|
-
provider: 3,
|
|
656
|
-
max: 4
|
|
889
|
+
"sakana/fugu-ultra": "provider",
|
|
890
|
+
"gpt-6.1-sol": "max"
|
|
657
891
|
};
|
|
658
892
|
/**
|
|
659
893
|
* Whether a model is free (requests cost no credits), per the pricing page's
|
|
@@ -682,7 +916,7 @@ function compareByPlan(a, b) {
|
|
|
682
916
|
/**
|
|
683
917
|
* Subscription plan table, synced from the official CLI bundle's plan maps
|
|
684
918
|
* (located by the `"individual-go"` key in `dist/cli.mjs`, re-verified unchanged
|
|
685
|
-
* through command-code@1.
|
|
919
|
+
* through command-code@1.72.1): subscription `planId` prefix → display name and
|
|
686
920
|
* the plan's monthly credit total. This is the account's own subscription
|
|
687
921
|
* (from `/alpha/billing/subscriptions`) — distinct from {@link KNOWN_PLANS},
|
|
688
922
|
* which maps catalog models to their minimum tier.
|
|
@@ -697,6 +931,11 @@ const KNOWN_SUBSCRIPTION_PLANS = {
|
|
|
697
931
|
monthlyCredits: 10,
|
|
698
932
|
tierWeight: 0
|
|
699
933
|
},
|
|
934
|
+
"individual-go-v1": {
|
|
935
|
+
name: "Go",
|
|
936
|
+
monthlyCredits: 10,
|
|
937
|
+
tierWeight: 0
|
|
938
|
+
},
|
|
700
939
|
"individual-goat": {
|
|
701
940
|
name: "GOAT",
|
|
702
941
|
monthlyCredits: 70,
|
|
@@ -785,10 +1024,6 @@ const KNOWN_DEALS = {
|
|
|
785
1024
|
"MiniMaxAI/MiniMax-M3": { label: "50% off" },
|
|
786
1025
|
"xiaomi/mimo-v2.5-pro": { label: "99% off" },
|
|
787
1026
|
"xiaomi/mimo-v2.5": { label: "98% off" },
|
|
788
|
-
"xai/grok-4.7": {
|
|
789
|
-
label: "40% off",
|
|
790
|
-
expiresAt: "2026-09-27T23:59:59.999Z"
|
|
791
|
-
},
|
|
792
1027
|
"poolside/laguna-s-2.1-free": {
|
|
793
1028
|
label: "FREE",
|
|
794
1029
|
free: true
|
|
@@ -797,6 +1032,10 @@ const KNOWN_DEALS = {
|
|
|
797
1032
|
label: "FREE",
|
|
798
1033
|
free: true
|
|
799
1034
|
},
|
|
1035
|
+
"inclusionai/ling-3.1-flash:free": {
|
|
1036
|
+
label: "FREE",
|
|
1037
|
+
free: true
|
|
1038
|
+
},
|
|
800
1039
|
"stealth/space-bunny-alpha": {
|
|
801
1040
|
label: "FREE",
|
|
802
1041
|
free: true
|
|
@@ -812,7 +1051,7 @@ const KNOWN_DEALS = {
|
|
|
812
1051
|
* charges by the hour: peak hours are 01:00–04:00 and 06:00–10:00 UTC (7h per
|
|
813
1052
|
* weekday, full price) **Monday to Friday only**; the other 17 hours of a
|
|
814
1053
|
* weekday and every hour of Saturday/Sunday (UTC) are off-peak at half price.
|
|
815
|
-
* Exactly
|
|
1054
|
+
* Exactly five models carry the page's `timeOfDay` block (the five rows below).
|
|
816
1055
|
* The picker shows the *current* state as a compact label (`Peak`/`Half`)
|
|
817
1056
|
* matching the English noun style of the other markers (`Image`, `FREE`), so a
|
|
818
1057
|
* developer can tell at a glance whether calling the model right now is cheap
|
|
@@ -837,7 +1076,8 @@ const KNOWN_PEAK_PRICING = /* @__PURE__ */ new Set([
|
|
|
837
1076
|
"deepseek/deepseek-v4-pro",
|
|
838
1077
|
"deepseek/deepseek-v4-flash",
|
|
839
1078
|
"deepseek/deepseek-v4-flash-vision-exp",
|
|
840
|
-
"deepseek/deepseek-v4.1-flash"
|
|
1079
|
+
"deepseek/deepseek-v4.1-flash",
|
|
1080
|
+
"deepseek/deepseek-v4.1-flash-fast"
|
|
841
1081
|
]);
|
|
842
1082
|
/**
|
|
843
1083
|
* Peak hours (UTC, hour-of-day range end-exclusive): 01–03 and 06–09.
|
|
@@ -860,11 +1100,7 @@ const PEAK_HOUR_RANGES = [[1, 4], [6, 10]];
|
|
|
860
1100
|
* the one in {@link KNOWN_PEAK_PRICING} still gets the right half of the day.
|
|
861
1101
|
*/
|
|
862
1102
|
function isPeakPricingHour(now = Date.now()) {
|
|
863
|
-
|
|
864
|
-
const day = at.getUTCDay();
|
|
865
|
-
if (day === 0 || day === 6) return false;
|
|
866
|
-
const hour = at.getUTCHours();
|
|
867
|
-
return PEAK_HOUR_RANGES.some(([start, end]) => hour >= start && hour < end);
|
|
1103
|
+
return peakHour(now, PEAK_HOUR_RANGES);
|
|
868
1104
|
}
|
|
869
1105
|
/**
|
|
870
1106
|
* As {@link isPeakPricingHour}, plus the {@link KNOWN_PEAK_PRICING} membership
|
|
@@ -945,6 +1181,27 @@ function capabilityDescription(modelId, contextWindow, now = Date.now()) {
|
|
|
945
1181
|
//#endregion
|
|
946
1182
|
//#region src/model-prices.ts
|
|
947
1183
|
/**
|
|
1184
|
+
* The plan tiers a per-model allowance exists for, cheapest first. Ordering
|
|
1185
|
+
* matters: {@link allowanceTierForWeight} matches a tier weight against these
|
|
1186
|
+
* rather than against hard-coded numbers, so this table and the subscription
|
|
1187
|
+
* table cannot drift apart silently.
|
|
1188
|
+
*/
|
|
1189
|
+
const ALLOWANCE_TIERS = ["goat", "pro"];
|
|
1190
|
+
/**
|
|
1191
|
+
* Which allowance bracket a plan-tier weight belongs to, or undefined when that
|
|
1192
|
+
* plan has no per-model allowance.
|
|
1193
|
+
*
|
|
1194
|
+
* Weights come from `KNOWN_SUBSCRIPTION_PLANS` (go 0 · goat 1 · pro 2 ·
|
|
1195
|
+
* provider 3 · max/ultra 4) and {@link PLAN_ORDER} is the same scale, which is
|
|
1196
|
+
* what makes the lookup exact: Go, Provider, Max and Ultra have no published
|
|
1197
|
+
* allowance, and answering with a neighbouring tier's figure would be a
|
|
1198
|
+
* fabricated number rather than a missing one.
|
|
1199
|
+
*/
|
|
1200
|
+
function allowanceTierForWeight(tierWeight) {
|
|
1201
|
+
if (tierWeight === void 0) return void 0;
|
|
1202
|
+
return ALLOWANCE_TIERS.find((tier) => PLAN_ORDER[tier] === tierWeight);
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
948
1205
|
* Every price row the page publishes, ordered by its own slug.
|
|
949
1206
|
*
|
|
950
1207
|
* Note on the GPT rows: `gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini` and
|
|
@@ -964,7 +1221,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
964
1221
|
50,
|
|
965
1222
|
1,
|
|
966
1223
|
12.5
|
|
967
|
-
]
|
|
1224
|
+
],
|
|
1225
|
+
allowance: {
|
|
1226
|
+
goat: 20,
|
|
1227
|
+
pro: 20
|
|
1228
|
+
}
|
|
968
1229
|
},
|
|
969
1230
|
{
|
|
970
1231
|
id: "claude-fable-5-1",
|
|
@@ -973,7 +1234,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
973
1234
|
50,
|
|
974
1235
|
.25,
|
|
975
1236
|
12.5
|
|
976
|
-
]
|
|
1237
|
+
],
|
|
1238
|
+
allowance: {
|
|
1239
|
+
goat: 20,
|
|
1240
|
+
pro: 20
|
|
1241
|
+
}
|
|
977
1242
|
},
|
|
978
1243
|
{
|
|
979
1244
|
id: "claude-haiku-4-5",
|
|
@@ -982,7 +1247,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
982
1247
|
5,
|
|
983
1248
|
.1,
|
|
984
1249
|
1.25
|
|
985
|
-
]
|
|
1250
|
+
],
|
|
1251
|
+
allowance: {
|
|
1252
|
+
goat: 20,
|
|
1253
|
+
pro: 20
|
|
1254
|
+
}
|
|
986
1255
|
},
|
|
987
1256
|
{
|
|
988
1257
|
id: "claude-opus-4-6",
|
|
@@ -991,7 +1260,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
991
1260
|
25,
|
|
992
1261
|
.5,
|
|
993
1262
|
6.25
|
|
994
|
-
]
|
|
1263
|
+
],
|
|
1264
|
+
allowance: {
|
|
1265
|
+
goat: 20,
|
|
1266
|
+
pro: 20
|
|
1267
|
+
}
|
|
995
1268
|
},
|
|
996
1269
|
{
|
|
997
1270
|
id: "claude-opus-4-7",
|
|
@@ -1000,7 +1273,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1000
1273
|
25,
|
|
1001
1274
|
.5,
|
|
1002
1275
|
6.25
|
|
1003
|
-
]
|
|
1276
|
+
],
|
|
1277
|
+
allowance: {
|
|
1278
|
+
goat: 20,
|
|
1279
|
+
pro: 20
|
|
1280
|
+
}
|
|
1004
1281
|
},
|
|
1005
1282
|
{
|
|
1006
1283
|
id: "claude-opus-4-8",
|
|
@@ -1009,7 +1286,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1009
1286
|
25,
|
|
1010
1287
|
.5,
|
|
1011
1288
|
6.25
|
|
1012
|
-
]
|
|
1289
|
+
],
|
|
1290
|
+
allowance: {
|
|
1291
|
+
goat: 20,
|
|
1292
|
+
pro: 20
|
|
1293
|
+
}
|
|
1013
1294
|
},
|
|
1014
1295
|
{
|
|
1015
1296
|
id: "claude-opus-5",
|
|
@@ -1018,7 +1299,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1018
1299
|
25,
|
|
1019
1300
|
.5,
|
|
1020
1301
|
6.25
|
|
1021
|
-
]
|
|
1302
|
+
],
|
|
1303
|
+
allowance: {
|
|
1304
|
+
goat: 20,
|
|
1305
|
+
pro: 20
|
|
1306
|
+
}
|
|
1022
1307
|
},
|
|
1023
1308
|
{
|
|
1024
1309
|
id: "claude-opus-5-5",
|
|
@@ -1027,7 +1312,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1027
1312
|
20,
|
|
1028
1313
|
.2,
|
|
1029
1314
|
5
|
|
1030
|
-
]
|
|
1315
|
+
],
|
|
1316
|
+
allowance: {
|
|
1317
|
+
goat: 20,
|
|
1318
|
+
pro: 20
|
|
1319
|
+
}
|
|
1031
1320
|
},
|
|
1032
1321
|
{
|
|
1033
1322
|
id: "claude-sonnet-4-6",
|
|
@@ -1036,7 +1325,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1036
1325
|
15,
|
|
1037
1326
|
.3,
|
|
1038
1327
|
3.75
|
|
1039
|
-
]
|
|
1328
|
+
],
|
|
1329
|
+
allowance: {
|
|
1330
|
+
goat: 20,
|
|
1331
|
+
pro: 20
|
|
1332
|
+
}
|
|
1040
1333
|
},
|
|
1041
1334
|
{
|
|
1042
1335
|
id: "claude-sonnet-5",
|
|
@@ -1045,7 +1338,24 @@ const MODEL_PRICE_ROWS = [
|
|
|
1045
1338
|
10,
|
|
1046
1339
|
.2,
|
|
1047
1340
|
2.5
|
|
1048
|
-
]
|
|
1341
|
+
],
|
|
1342
|
+
allowance: {
|
|
1343
|
+
goat: 20,
|
|
1344
|
+
pro: 20
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
id: "claude-sonnet-5-5",
|
|
1349
|
+
rates: [
|
|
1350
|
+
2,
|
|
1351
|
+
10,
|
|
1352
|
+
.2,
|
|
1353
|
+
2.5
|
|
1354
|
+
],
|
|
1355
|
+
allowance: {
|
|
1356
|
+
goat: 10,
|
|
1357
|
+
pro: 20
|
|
1358
|
+
}
|
|
1049
1359
|
},
|
|
1050
1360
|
{
|
|
1051
1361
|
id: "deepseek-v4-flash",
|
|
@@ -1058,7 +1368,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1058
1368
|
.3,
|
|
1059
1369
|
1.2,
|
|
1060
1370
|
.006
|
|
1061
|
-
]
|
|
1371
|
+
],
|
|
1372
|
+
allowance: {
|
|
1373
|
+
goat: 60,
|
|
1374
|
+
pro: 70
|
|
1375
|
+
}
|
|
1062
1376
|
},
|
|
1063
1377
|
{
|
|
1064
1378
|
id: "deepseek-v4-flash-fast",
|
|
@@ -1066,7 +1380,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1066
1380
|
.28,
|
|
1067
1381
|
.56,
|
|
1068
1382
|
.07
|
|
1069
|
-
]
|
|
1383
|
+
],
|
|
1384
|
+
allowance: {
|
|
1385
|
+
goat: 20,
|
|
1386
|
+
pro: 30
|
|
1387
|
+
}
|
|
1070
1388
|
},
|
|
1071
1389
|
{
|
|
1072
1390
|
id: "deepseek-v4-flash-vision-exp",
|
|
@@ -1079,7 +1397,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1079
1397
|
.3,
|
|
1080
1398
|
1.2,
|
|
1081
1399
|
.006
|
|
1082
|
-
]
|
|
1400
|
+
],
|
|
1401
|
+
allowance: {
|
|
1402
|
+
goat: 20,
|
|
1403
|
+
pro: 30
|
|
1404
|
+
}
|
|
1083
1405
|
},
|
|
1084
1406
|
{
|
|
1085
1407
|
id: "deepseek-v4-pro",
|
|
@@ -1092,7 +1414,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1092
1414
|
1.32,
|
|
1093
1415
|
3.96,
|
|
1094
1416
|
.044
|
|
1095
|
-
]
|
|
1417
|
+
],
|
|
1418
|
+
allowance: {
|
|
1419
|
+
goat: 20,
|
|
1420
|
+
pro: 30
|
|
1421
|
+
}
|
|
1096
1422
|
},
|
|
1097
1423
|
{
|
|
1098
1424
|
id: "deepseek-v4.1-flash",
|
|
@@ -1105,7 +1431,28 @@ const MODEL_PRICE_ROWS = [
|
|
|
1105
1431
|
.3,
|
|
1106
1432
|
1.2,
|
|
1107
1433
|
.006
|
|
1108
|
-
]
|
|
1434
|
+
],
|
|
1435
|
+
allowance: {
|
|
1436
|
+
goat: 60,
|
|
1437
|
+
pro: 70
|
|
1438
|
+
}
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
id: "deepseek-v4.1-flash-fast",
|
|
1442
|
+
rates: [
|
|
1443
|
+
.16,
|
|
1444
|
+
.58,
|
|
1445
|
+
.016
|
|
1446
|
+
],
|
|
1447
|
+
peak: [
|
|
1448
|
+
.32,
|
|
1449
|
+
1.16,
|
|
1450
|
+
.032
|
|
1451
|
+
],
|
|
1452
|
+
allowance: {
|
|
1453
|
+
goat: 60,
|
|
1454
|
+
pro: 70
|
|
1455
|
+
}
|
|
1109
1456
|
},
|
|
1110
1457
|
{
|
|
1111
1458
|
id: "fugu-ultra",
|
|
@@ -1113,7 +1460,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1113
1460
|
5,
|
|
1114
1461
|
30,
|
|
1115
1462
|
.5
|
|
1116
|
-
]
|
|
1463
|
+
],
|
|
1464
|
+
allowance: {
|
|
1465
|
+
goat: 20,
|
|
1466
|
+
pro: 20
|
|
1467
|
+
}
|
|
1117
1468
|
},
|
|
1118
1469
|
{
|
|
1119
1470
|
id: "gemini-3.1-flash-lite",
|
|
@@ -1121,7 +1472,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1121
1472
|
.25,
|
|
1122
1473
|
1.5,
|
|
1123
1474
|
.03
|
|
1124
|
-
]
|
|
1475
|
+
],
|
|
1476
|
+
allowance: {
|
|
1477
|
+
goat: 20,
|
|
1478
|
+
pro: 20
|
|
1479
|
+
}
|
|
1125
1480
|
},
|
|
1126
1481
|
{
|
|
1127
1482
|
id: "gemini-3.5-flash",
|
|
@@ -1129,7 +1484,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1129
1484
|
1.5,
|
|
1130
1485
|
9,
|
|
1131
1486
|
.15
|
|
1132
|
-
]
|
|
1487
|
+
],
|
|
1488
|
+
allowance: {
|
|
1489
|
+
goat: 20,
|
|
1490
|
+
pro: 20
|
|
1491
|
+
}
|
|
1133
1492
|
},
|
|
1134
1493
|
{
|
|
1135
1494
|
id: "gemini-3.5-flash-lite",
|
|
@@ -1137,7 +1496,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1137
1496
|
.3,
|
|
1138
1497
|
2.5,
|
|
1139
1498
|
.03
|
|
1140
|
-
]
|
|
1499
|
+
],
|
|
1500
|
+
allowance: {
|
|
1501
|
+
goat: 20,
|
|
1502
|
+
pro: 20
|
|
1503
|
+
}
|
|
1141
1504
|
},
|
|
1142
1505
|
{
|
|
1143
1506
|
id: "gemini-3.6-flash",
|
|
@@ -1145,7 +1508,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1145
1508
|
1.5,
|
|
1146
1509
|
7.5,
|
|
1147
1510
|
.15
|
|
1148
|
-
]
|
|
1511
|
+
],
|
|
1512
|
+
allowance: {
|
|
1513
|
+
goat: 20,
|
|
1514
|
+
pro: 20
|
|
1515
|
+
}
|
|
1149
1516
|
},
|
|
1150
1517
|
{
|
|
1151
1518
|
id: "gemini-3.7-flash",
|
|
@@ -1154,7 +1521,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1154
1521
|
7.5,
|
|
1155
1522
|
.15,
|
|
1156
1523
|
.08334
|
|
1157
|
-
]
|
|
1524
|
+
],
|
|
1525
|
+
allowance: {
|
|
1526
|
+
goat: 40,
|
|
1527
|
+
pro: 50
|
|
1528
|
+
}
|
|
1158
1529
|
},
|
|
1159
1530
|
{
|
|
1160
1531
|
id: "gemini-3.8-flash",
|
|
@@ -1162,7 +1533,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1162
1533
|
1.5,
|
|
1163
1534
|
7.5,
|
|
1164
1535
|
.15
|
|
1165
|
-
]
|
|
1536
|
+
],
|
|
1537
|
+
allowance: {
|
|
1538
|
+
goat: 40,
|
|
1539
|
+
pro: 50
|
|
1540
|
+
}
|
|
1166
1541
|
},
|
|
1167
1542
|
{
|
|
1168
1543
|
id: "glm-5",
|
|
@@ -1170,7 +1545,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1170
1545
|
1,
|
|
1171
1546
|
3.2,
|
|
1172
1547
|
.2
|
|
1173
|
-
]
|
|
1548
|
+
],
|
|
1549
|
+
allowance: {
|
|
1550
|
+
goat: 20,
|
|
1551
|
+
pro: 30
|
|
1552
|
+
}
|
|
1174
1553
|
},
|
|
1175
1554
|
{
|
|
1176
1555
|
id: "glm-5.1",
|
|
@@ -1178,7 +1557,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1178
1557
|
1.4,
|
|
1179
1558
|
4.4,
|
|
1180
1559
|
.26
|
|
1181
|
-
]
|
|
1560
|
+
],
|
|
1561
|
+
allowance: {
|
|
1562
|
+
goat: 20,
|
|
1563
|
+
pro: 30
|
|
1564
|
+
}
|
|
1182
1565
|
},
|
|
1183
1566
|
{
|
|
1184
1567
|
id: "glm-5.2",
|
|
@@ -1186,7 +1569,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1186
1569
|
1.4,
|
|
1187
1570
|
4.4,
|
|
1188
1571
|
.26
|
|
1189
|
-
]
|
|
1572
|
+
],
|
|
1573
|
+
allowance: {
|
|
1574
|
+
goat: 70,
|
|
1575
|
+
pro: 80
|
|
1576
|
+
}
|
|
1190
1577
|
},
|
|
1191
1578
|
{
|
|
1192
1579
|
id: "glm-5.2-fast",
|
|
@@ -1194,7 +1581,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1194
1581
|
3,
|
|
1195
1582
|
10.25,
|
|
1196
1583
|
.5
|
|
1197
|
-
]
|
|
1584
|
+
],
|
|
1585
|
+
allowance: {
|
|
1586
|
+
goat: 20,
|
|
1587
|
+
pro: 30
|
|
1588
|
+
}
|
|
1198
1589
|
},
|
|
1199
1590
|
{
|
|
1200
1591
|
id: "glm-5.3",
|
|
@@ -1202,7 +1593,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1202
1593
|
1.4,
|
|
1203
1594
|
4.4,
|
|
1204
1595
|
.26
|
|
1205
|
-
]
|
|
1596
|
+
],
|
|
1597
|
+
allowance: {
|
|
1598
|
+
goat: 20,
|
|
1599
|
+
pro: 30
|
|
1600
|
+
}
|
|
1206
1601
|
},
|
|
1207
1602
|
{
|
|
1208
1603
|
id: "glm-5.3-flash",
|
|
@@ -1210,7 +1605,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1210
1605
|
.15,
|
|
1211
1606
|
.5,
|
|
1212
1607
|
.03
|
|
1213
|
-
]
|
|
1608
|
+
],
|
|
1609
|
+
allowance: {
|
|
1610
|
+
goat: 40,
|
|
1611
|
+
pro: 50
|
|
1612
|
+
}
|
|
1214
1613
|
},
|
|
1215
1614
|
{
|
|
1216
1615
|
id: "glm-5.3-flashx",
|
|
@@ -1218,7 +1617,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1218
1617
|
.37,
|
|
1219
1618
|
1.25,
|
|
1220
1619
|
.075
|
|
1221
|
-
]
|
|
1620
|
+
],
|
|
1621
|
+
allowance: {
|
|
1622
|
+
goat: 20,
|
|
1623
|
+
pro: 30
|
|
1624
|
+
}
|
|
1222
1625
|
},
|
|
1223
1626
|
{
|
|
1224
1627
|
id: "gpt-5.3-codex",
|
|
@@ -1227,7 +1630,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1227
1630
|
8,
|
|
1228
1631
|
.5,
|
|
1229
1632
|
0
|
|
1230
|
-
]
|
|
1633
|
+
],
|
|
1634
|
+
allowance: {
|
|
1635
|
+
goat: 20,
|
|
1636
|
+
pro: 20
|
|
1637
|
+
}
|
|
1231
1638
|
},
|
|
1232
1639
|
{
|
|
1233
1640
|
id: "gpt-5.4",
|
|
@@ -1236,7 +1643,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1236
1643
|
15,
|
|
1237
1644
|
.25,
|
|
1238
1645
|
0
|
|
1239
|
-
]
|
|
1646
|
+
],
|
|
1647
|
+
allowance: {
|
|
1648
|
+
goat: 20,
|
|
1649
|
+
pro: 20
|
|
1650
|
+
}
|
|
1240
1651
|
},
|
|
1241
1652
|
{
|
|
1242
1653
|
id: "gpt-5.4-mini",
|
|
@@ -1245,7 +1656,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1245
1656
|
4.5,
|
|
1246
1657
|
.075,
|
|
1247
1658
|
0
|
|
1248
|
-
]
|
|
1659
|
+
],
|
|
1660
|
+
allowance: {
|
|
1661
|
+
goat: 20,
|
|
1662
|
+
pro: 20
|
|
1663
|
+
}
|
|
1249
1664
|
},
|
|
1250
1665
|
{
|
|
1251
1666
|
id: "gpt-5.5",
|
|
@@ -1254,7 +1669,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1254
1669
|
30,
|
|
1255
1670
|
.5,
|
|
1256
1671
|
0
|
|
1257
|
-
]
|
|
1672
|
+
],
|
|
1673
|
+
allowance: {
|
|
1674
|
+
goat: 20,
|
|
1675
|
+
pro: 20
|
|
1676
|
+
}
|
|
1258
1677
|
},
|
|
1259
1678
|
{
|
|
1260
1679
|
id: "gpt-5.6-luna",
|
|
@@ -1264,6 +1683,10 @@ const MODEL_PRICE_ROWS = [
|
|
|
1264
1683
|
.02,
|
|
1265
1684
|
.25
|
|
1266
1685
|
],
|
|
1686
|
+
allowance: {
|
|
1687
|
+
goat: 20,
|
|
1688
|
+
pro: 30
|
|
1689
|
+
},
|
|
1267
1690
|
contextTiers: [{
|
|
1268
1691
|
"maxContext": 272e3,
|
|
1269
1692
|
"rates": [
|
|
@@ -1287,6 +1710,10 @@ const MODEL_PRICE_ROWS = [
|
|
|
1287
1710
|
.5,
|
|
1288
1711
|
6.25
|
|
1289
1712
|
],
|
|
1713
|
+
allowance: {
|
|
1714
|
+
goat: 70,
|
|
1715
|
+
pro: 80
|
|
1716
|
+
},
|
|
1290
1717
|
contextTiers: [{
|
|
1291
1718
|
"maxContext": 272e3,
|
|
1292
1719
|
"rates": [
|
|
@@ -1310,6 +1737,10 @@ const MODEL_PRICE_ROWS = [
|
|
|
1310
1737
|
.2,
|
|
1311
1738
|
2.5
|
|
1312
1739
|
],
|
|
1740
|
+
allowance: {
|
|
1741
|
+
goat: 20,
|
|
1742
|
+
pro: 20
|
|
1743
|
+
},
|
|
1313
1744
|
contextTiers: [{
|
|
1314
1745
|
"maxContext": 272e3,
|
|
1315
1746
|
"rates": [
|
|
@@ -1333,6 +1764,10 @@ const MODEL_PRICE_ROWS = [
|
|
|
1333
1764
|
1,
|
|
1334
1765
|
12.5
|
|
1335
1766
|
],
|
|
1767
|
+
allowance: {
|
|
1768
|
+
goat: 20,
|
|
1769
|
+
pro: 20
|
|
1770
|
+
},
|
|
1336
1771
|
contextTiers: [{
|
|
1337
1772
|
"maxContext": 272e3,
|
|
1338
1773
|
"rates": [
|
|
@@ -1348,6 +1783,33 @@ const MODEL_PRICE_ROWS = [
|
|
|
1348
1783
|
25
|
|
1349
1784
|
] }]
|
|
1350
1785
|
},
|
|
1786
|
+
{
|
|
1787
|
+
id: "gpt-6.1-sol",
|
|
1788
|
+
rates: [
|
|
1789
|
+
2,
|
|
1790
|
+
10,
|
|
1791
|
+
.1,
|
|
1792
|
+
2.5
|
|
1793
|
+
],
|
|
1794
|
+
allowance: {
|
|
1795
|
+
goat: 20,
|
|
1796
|
+
pro: 20
|
|
1797
|
+
},
|
|
1798
|
+
contextTiers: [{
|
|
1799
|
+
"maxContext": 272e3,
|
|
1800
|
+
"rates": [
|
|
1801
|
+
2,
|
|
1802
|
+
10,
|
|
1803
|
+
.1,
|
|
1804
|
+
2.5
|
|
1805
|
+
]
|
|
1806
|
+
}, { "rates": [
|
|
1807
|
+
4,
|
|
1808
|
+
15,
|
|
1809
|
+
.2,
|
|
1810
|
+
5
|
|
1811
|
+
] }]
|
|
1812
|
+
},
|
|
1351
1813
|
{
|
|
1352
1814
|
id: "gpt-6-luna",
|
|
1353
1815
|
rates: [
|
|
@@ -1356,6 +1818,10 @@ const MODEL_PRICE_ROWS = [
|
|
|
1356
1818
|
.01,
|
|
1357
1819
|
.125
|
|
1358
1820
|
],
|
|
1821
|
+
allowance: {
|
|
1822
|
+
goat: 20,
|
|
1823
|
+
pro: 30
|
|
1824
|
+
},
|
|
1359
1825
|
contextTiers: [{
|
|
1360
1826
|
"maxContext": 272e3,
|
|
1361
1827
|
"rates": [
|
|
@@ -1379,6 +1845,10 @@ const MODEL_PRICE_ROWS = [
|
|
|
1379
1845
|
.2,
|
|
1380
1846
|
2.5
|
|
1381
1847
|
],
|
|
1848
|
+
allowance: {
|
|
1849
|
+
goat: 20,
|
|
1850
|
+
pro: 20
|
|
1851
|
+
},
|
|
1382
1852
|
contextTiers: [{
|
|
1383
1853
|
"maxContext": 272e3,
|
|
1384
1854
|
"rates": [
|
|
@@ -1400,7 +1870,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1400
1870
|
2,
|
|
1401
1871
|
6,
|
|
1402
1872
|
.5
|
|
1403
|
-
]
|
|
1873
|
+
],
|
|
1874
|
+
allowance: {
|
|
1875
|
+
goat: 20,
|
|
1876
|
+
pro: 30
|
|
1877
|
+
}
|
|
1404
1878
|
},
|
|
1405
1879
|
{
|
|
1406
1880
|
id: "grok-4.6",
|
|
@@ -1409,6 +1883,10 @@ const MODEL_PRICE_ROWS = [
|
|
|
1409
1883
|
6,
|
|
1410
1884
|
.5
|
|
1411
1885
|
],
|
|
1886
|
+
allowance: {
|
|
1887
|
+
goat: 20,
|
|
1888
|
+
pro: 30
|
|
1889
|
+
},
|
|
1412
1890
|
contextTiers: [{
|
|
1413
1891
|
"maxContext": 2e5,
|
|
1414
1892
|
"rates": [
|
|
@@ -1425,21 +1903,25 @@ const MODEL_PRICE_ROWS = [
|
|
|
1425
1903
|
{
|
|
1426
1904
|
id: "grok-4.7",
|
|
1427
1905
|
rates: [
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
.
|
|
1906
|
+
2,
|
|
1907
|
+
6,
|
|
1908
|
+
.5
|
|
1431
1909
|
],
|
|
1910
|
+
allowance: {
|
|
1911
|
+
goat: 20,
|
|
1912
|
+
pro: 30
|
|
1913
|
+
},
|
|
1432
1914
|
contextTiers: [{
|
|
1433
1915
|
"maxContext": 2e5,
|
|
1434
1916
|
"rates": [
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
.
|
|
1917
|
+
2,
|
|
1918
|
+
6,
|
|
1919
|
+
.5
|
|
1438
1920
|
]
|
|
1439
1921
|
}, { "rates": [
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1922
|
+
4,
|
|
1923
|
+
12,
|
|
1924
|
+
1
|
|
1443
1925
|
] }]
|
|
1444
1926
|
},
|
|
1445
1927
|
{
|
|
@@ -1448,7 +1930,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1448
1930
|
1,
|
|
1449
1931
|
4.05,
|
|
1450
1932
|
.17
|
|
1451
|
-
]
|
|
1933
|
+
],
|
|
1934
|
+
allowance: {
|
|
1935
|
+
goat: 20,
|
|
1936
|
+
pro: 30
|
|
1937
|
+
}
|
|
1452
1938
|
},
|
|
1453
1939
|
{
|
|
1454
1940
|
id: "inkling-small",
|
|
@@ -1456,7 +1942,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1456
1942
|
.5,
|
|
1457
1943
|
1.2,
|
|
1458
1944
|
.1
|
|
1459
|
-
]
|
|
1945
|
+
],
|
|
1946
|
+
allowance: {
|
|
1947
|
+
goat: 20,
|
|
1948
|
+
pro: 30
|
|
1949
|
+
}
|
|
1460
1950
|
},
|
|
1461
1951
|
{
|
|
1462
1952
|
id: "kimi-k2.5",
|
|
@@ -1464,7 +1954,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1464
1954
|
.6,
|
|
1465
1955
|
3,
|
|
1466
1956
|
.1
|
|
1467
|
-
]
|
|
1957
|
+
],
|
|
1958
|
+
allowance: {
|
|
1959
|
+
goat: 20,
|
|
1960
|
+
pro: 30
|
|
1961
|
+
}
|
|
1468
1962
|
},
|
|
1469
1963
|
{
|
|
1470
1964
|
id: "kimi-k2.6",
|
|
@@ -1472,7 +1966,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1472
1966
|
.95,
|
|
1473
1967
|
4,
|
|
1474
1968
|
.16
|
|
1475
|
-
]
|
|
1969
|
+
],
|
|
1970
|
+
allowance: {
|
|
1971
|
+
goat: 20,
|
|
1972
|
+
pro: 30
|
|
1973
|
+
}
|
|
1476
1974
|
},
|
|
1477
1975
|
{
|
|
1478
1976
|
id: "kimi-k2.7-code",
|
|
@@ -1480,7 +1978,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1480
1978
|
.95,
|
|
1481
1979
|
4,
|
|
1482
1980
|
.19
|
|
1483
|
-
]
|
|
1981
|
+
],
|
|
1982
|
+
allowance: {
|
|
1983
|
+
goat: 60,
|
|
1984
|
+
pro: 70
|
|
1985
|
+
}
|
|
1484
1986
|
},
|
|
1485
1987
|
{
|
|
1486
1988
|
id: "kimi-k2.7-code-highspeed",
|
|
@@ -1488,7 +1990,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1488
1990
|
1.9,
|
|
1489
1991
|
8,
|
|
1490
1992
|
.38
|
|
1491
|
-
]
|
|
1993
|
+
],
|
|
1994
|
+
allowance: {
|
|
1995
|
+
goat: 20,
|
|
1996
|
+
pro: 30
|
|
1997
|
+
}
|
|
1492
1998
|
},
|
|
1493
1999
|
{
|
|
1494
2000
|
id: "kimi-k3",
|
|
@@ -1496,7 +2002,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1496
2002
|
3,
|
|
1497
2003
|
15,
|
|
1498
2004
|
.3
|
|
1499
|
-
]
|
|
2005
|
+
],
|
|
2006
|
+
allowance: {
|
|
2007
|
+
goat: 20,
|
|
2008
|
+
pro: 30
|
|
2009
|
+
}
|
|
1500
2010
|
},
|
|
1501
2011
|
{
|
|
1502
2012
|
id: "longcat-2.0",
|
|
@@ -1504,7 +2014,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1504
2014
|
.3,
|
|
1505
2015
|
1.2,
|
|
1506
2016
|
.006
|
|
1507
|
-
]
|
|
2017
|
+
],
|
|
2018
|
+
allowance: {
|
|
2019
|
+
goat: 50,
|
|
2020
|
+
pro: 60
|
|
2021
|
+
}
|
|
1508
2022
|
},
|
|
1509
2023
|
{
|
|
1510
2024
|
id: "mimo-v2.5",
|
|
@@ -1512,7 +2026,16 @@ const MODEL_PRICE_ROWS = [
|
|
|
1512
2026
|
.14,
|
|
1513
2027
|
.28,
|
|
1514
2028
|
.0028
|
|
1515
|
-
]
|
|
2029
|
+
],
|
|
2030
|
+
listRates: [
|
|
2031
|
+
.8,
|
|
2032
|
+
4,
|
|
2033
|
+
.16
|
|
2034
|
+
],
|
|
2035
|
+
allowance: {
|
|
2036
|
+
goat: 30,
|
|
2037
|
+
pro: 40
|
|
2038
|
+
}
|
|
1516
2039
|
},
|
|
1517
2040
|
{
|
|
1518
2041
|
id: "mimo-v2.5-pro",
|
|
@@ -1520,7 +2043,16 @@ const MODEL_PRICE_ROWS = [
|
|
|
1520
2043
|
.435,
|
|
1521
2044
|
.87,
|
|
1522
2045
|
.0036
|
|
1523
|
-
]
|
|
2046
|
+
],
|
|
2047
|
+
listRates: [
|
|
2048
|
+
2,
|
|
2049
|
+
6,
|
|
2050
|
+
.4
|
|
2051
|
+
],
|
|
2052
|
+
allowance: {
|
|
2053
|
+
goat: 20,
|
|
2054
|
+
pro: 30
|
|
2055
|
+
}
|
|
1524
2056
|
},
|
|
1525
2057
|
{
|
|
1526
2058
|
id: "mimo-v2.6-flash",
|
|
@@ -1528,7 +2060,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1528
2060
|
.14,
|
|
1529
2061
|
.28,
|
|
1530
2062
|
.0028
|
|
1531
|
-
]
|
|
2063
|
+
],
|
|
2064
|
+
allowance: {
|
|
2065
|
+
goat: 20,
|
|
2066
|
+
pro: 30
|
|
2067
|
+
}
|
|
1532
2068
|
},
|
|
1533
2069
|
{
|
|
1534
2070
|
id: "mimo-v2.6-pro",
|
|
@@ -1536,7 +2072,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1536
2072
|
.435,
|
|
1537
2073
|
.87,
|
|
1538
2074
|
.0036
|
|
1539
|
-
]
|
|
2075
|
+
],
|
|
2076
|
+
allowance: {
|
|
2077
|
+
goat: 20,
|
|
2078
|
+
pro: 30
|
|
2079
|
+
}
|
|
1540
2080
|
},
|
|
1541
2081
|
{
|
|
1542
2082
|
id: "mimo-v2.6-pro-ultraspeed",
|
|
@@ -1544,7 +2084,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1544
2084
|
4.35,
|
|
1545
2085
|
8.7,
|
|
1546
2086
|
.036
|
|
1547
|
-
]
|
|
2087
|
+
],
|
|
2088
|
+
allowance: {
|
|
2089
|
+
goat: 10,
|
|
2090
|
+
pro: 20
|
|
2091
|
+
}
|
|
1548
2092
|
},
|
|
1549
2093
|
{
|
|
1550
2094
|
id: "minimax-m2.5",
|
|
@@ -1552,7 +2096,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1552
2096
|
.3,
|
|
1553
2097
|
1.2,
|
|
1554
2098
|
.03
|
|
1555
|
-
]
|
|
2099
|
+
],
|
|
2100
|
+
allowance: {
|
|
2101
|
+
goat: 20,
|
|
2102
|
+
pro: 30
|
|
2103
|
+
}
|
|
1556
2104
|
},
|
|
1557
2105
|
{
|
|
1558
2106
|
id: "minimax-m2.7",
|
|
@@ -1560,7 +2108,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1560
2108
|
.3,
|
|
1561
2109
|
1.2,
|
|
1562
2110
|
.06
|
|
1563
|
-
]
|
|
2111
|
+
],
|
|
2112
|
+
allowance: {
|
|
2113
|
+
goat: 20,
|
|
2114
|
+
pro: 30
|
|
2115
|
+
}
|
|
1564
2116
|
},
|
|
1565
2117
|
{
|
|
1566
2118
|
id: "minimax-m3",
|
|
@@ -1568,7 +2120,16 @@ const MODEL_PRICE_ROWS = [
|
|
|
1568
2120
|
.3,
|
|
1569
2121
|
1.2,
|
|
1570
2122
|
.06
|
|
1571
|
-
]
|
|
2123
|
+
],
|
|
2124
|
+
listRates: [
|
|
2125
|
+
.6,
|
|
2126
|
+
2.4,
|
|
2127
|
+
.12
|
|
2128
|
+
],
|
|
2129
|
+
allowance: {
|
|
2130
|
+
goat: 47,
|
|
2131
|
+
pro: 57
|
|
2132
|
+
}
|
|
1572
2133
|
},
|
|
1573
2134
|
{
|
|
1574
2135
|
id: "muse-spark-1.1",
|
|
@@ -1576,7 +2137,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1576
2137
|
1.25,
|
|
1577
2138
|
4.25,
|
|
1578
2139
|
.15
|
|
1579
|
-
]
|
|
2140
|
+
],
|
|
2141
|
+
allowance: {
|
|
2142
|
+
goat: 20,
|
|
2143
|
+
pro: 20
|
|
2144
|
+
}
|
|
1580
2145
|
},
|
|
1581
2146
|
{
|
|
1582
2147
|
id: "muse-spark-1.2",
|
|
@@ -1584,7 +2149,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1584
2149
|
1.25,
|
|
1585
2150
|
4.25,
|
|
1586
2151
|
.15
|
|
1587
|
-
]
|
|
2152
|
+
],
|
|
2153
|
+
allowance: {
|
|
2154
|
+
goat: 20,
|
|
2155
|
+
pro: 30
|
|
2156
|
+
}
|
|
1588
2157
|
},
|
|
1589
2158
|
{
|
|
1590
2159
|
id: "muse-spark-1.2-contributor",
|
|
@@ -1592,7 +2161,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1592
2161
|
.1,
|
|
1593
2162
|
.2,
|
|
1594
2163
|
.002
|
|
1595
|
-
]
|
|
2164
|
+
],
|
|
2165
|
+
allowance: {
|
|
2166
|
+
goat: 20,
|
|
2167
|
+
pro: 30
|
|
2168
|
+
}
|
|
1596
2169
|
},
|
|
1597
2170
|
{
|
|
1598
2171
|
id: "muse-spark-1.3",
|
|
@@ -1600,7 +2173,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1600
2173
|
1.25,
|
|
1601
2174
|
4.25,
|
|
1602
2175
|
.15
|
|
1603
|
-
]
|
|
2176
|
+
],
|
|
2177
|
+
allowance: {
|
|
2178
|
+
goat: 20,
|
|
2179
|
+
pro: 30
|
|
2180
|
+
}
|
|
1604
2181
|
},
|
|
1605
2182
|
{
|
|
1606
2183
|
id: "muse-spark-1.3-contributor",
|
|
@@ -1608,7 +2185,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1608
2185
|
.1,
|
|
1609
2186
|
.2,
|
|
1610
2187
|
.002
|
|
1611
|
-
]
|
|
2188
|
+
],
|
|
2189
|
+
allowance: {
|
|
2190
|
+
goat: 20,
|
|
2191
|
+
pro: 30
|
|
2192
|
+
}
|
|
1612
2193
|
},
|
|
1613
2194
|
{
|
|
1614
2195
|
id: "nemotron-3-ultra",
|
|
@@ -1616,7 +2197,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1616
2197
|
.6,
|
|
1617
2198
|
2.4,
|
|
1618
2199
|
.12
|
|
1619
|
-
]
|
|
2200
|
+
],
|
|
2201
|
+
allowance: {
|
|
2202
|
+
goat: 20,
|
|
2203
|
+
pro: 30
|
|
2204
|
+
}
|
|
1620
2205
|
},
|
|
1621
2206
|
{
|
|
1622
2207
|
id: "qwen-3.6-max",
|
|
@@ -1625,7 +2210,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1625
2210
|
7.8,
|
|
1626
2211
|
.26,
|
|
1627
2212
|
1.63
|
|
1628
|
-
]
|
|
2213
|
+
],
|
|
2214
|
+
allowance: {
|
|
2215
|
+
goat: 20,
|
|
2216
|
+
pro: 30
|
|
2217
|
+
}
|
|
1629
2218
|
},
|
|
1630
2219
|
{
|
|
1631
2220
|
id: "qwen-3.6-plus",
|
|
@@ -1634,6 +2223,10 @@ const MODEL_PRICE_ROWS = [
|
|
|
1634
2223
|
3,
|
|
1635
2224
|
.1
|
|
1636
2225
|
],
|
|
2226
|
+
allowance: {
|
|
2227
|
+
goat: 33,
|
|
2228
|
+
pro: 43
|
|
2229
|
+
},
|
|
1637
2230
|
contextTiers: [{
|
|
1638
2231
|
"maxContext": 256e3,
|
|
1639
2232
|
"rates": [
|
|
@@ -1655,6 +2248,10 @@ const MODEL_PRICE_ROWS = [
|
|
|
1655
2248
|
.006,
|
|
1656
2249
|
.038
|
|
1657
2250
|
],
|
|
2251
|
+
allowance: {
|
|
2252
|
+
goat: 20,
|
|
2253
|
+
pro: 30
|
|
2254
|
+
},
|
|
1658
2255
|
contextTiers: [
|
|
1659
2256
|
{
|
|
1660
2257
|
"maxContext": 32e3,
|
|
@@ -1689,7 +2286,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1689
2286
|
7.5,
|
|
1690
2287
|
.5,
|
|
1691
2288
|
3.13
|
|
1692
|
-
]
|
|
2289
|
+
],
|
|
2290
|
+
allowance: {
|
|
2291
|
+
goat: 33,
|
|
2292
|
+
pro: 43
|
|
2293
|
+
}
|
|
1693
2294
|
},
|
|
1694
2295
|
{
|
|
1695
2296
|
id: "qwen-3.7-plus",
|
|
@@ -1699,6 +2300,10 @@ const MODEL_PRICE_ROWS = [
|
|
|
1699
2300
|
.08,
|
|
1700
2301
|
.5
|
|
1701
2302
|
],
|
|
2303
|
+
allowance: {
|
|
2304
|
+
goat: 33,
|
|
2305
|
+
pro: 43
|
|
2306
|
+
},
|
|
1702
2307
|
contextTiers: [{
|
|
1703
2308
|
"maxContext": 256e3,
|
|
1704
2309
|
"rates": [
|
|
@@ -1720,7 +2325,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1720
2325
|
.4,
|
|
1721
2326
|
3,
|
|
1722
2327
|
.04
|
|
1723
|
-
]
|
|
2328
|
+
],
|
|
2329
|
+
allowance: {
|
|
2330
|
+
goat: 70,
|
|
2331
|
+
pro: 80
|
|
2332
|
+
}
|
|
1724
2333
|
},
|
|
1725
2334
|
{
|
|
1726
2335
|
id: "qwen-3.8-flash",
|
|
@@ -1728,7 +2337,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1728
2337
|
.16,
|
|
1729
2338
|
.47,
|
|
1730
2339
|
.016
|
|
1731
|
-
]
|
|
2340
|
+
],
|
|
2341
|
+
allowance: {
|
|
2342
|
+
goat: 20,
|
|
2343
|
+
pro: 30
|
|
2344
|
+
}
|
|
1732
2345
|
},
|
|
1733
2346
|
{
|
|
1734
2347
|
id: "qwen-3.8-max",
|
|
@@ -1737,7 +2350,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1737
2350
|
6,
|
|
1738
2351
|
.25,
|
|
1739
2352
|
2.5
|
|
1740
|
-
]
|
|
2353
|
+
],
|
|
2354
|
+
allowance: {
|
|
2355
|
+
goat: 20,
|
|
2356
|
+
pro: 30
|
|
2357
|
+
}
|
|
1741
2358
|
},
|
|
1742
2359
|
{
|
|
1743
2360
|
id: "qwen-3.8-max-0902",
|
|
@@ -1745,7 +2362,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1745
2362
|
2,
|
|
1746
2363
|
6,
|
|
1747
2364
|
.25
|
|
1748
|
-
]
|
|
2365
|
+
],
|
|
2366
|
+
allowance: {
|
|
2367
|
+
goat: 20,
|
|
2368
|
+
pro: 30
|
|
2369
|
+
}
|
|
1749
2370
|
},
|
|
1750
2371
|
{
|
|
1751
2372
|
id: "qwen-3.8-omni-flash",
|
|
@@ -1753,7 +2374,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1753
2374
|
.15,
|
|
1754
2375
|
.47,
|
|
1755
2376
|
.016
|
|
1756
|
-
]
|
|
2377
|
+
],
|
|
2378
|
+
allowance: {
|
|
2379
|
+
goat: 20,
|
|
2380
|
+
pro: 30
|
|
2381
|
+
}
|
|
1757
2382
|
},
|
|
1758
2383
|
{
|
|
1759
2384
|
id: "step-3.5-flash",
|
|
@@ -1761,7 +2386,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1761
2386
|
.09,
|
|
1762
2387
|
.3,
|
|
1763
2388
|
.02
|
|
1764
|
-
]
|
|
2389
|
+
],
|
|
2390
|
+
allowance: {
|
|
2391
|
+
goat: 20,
|
|
2392
|
+
pro: 30
|
|
2393
|
+
}
|
|
1765
2394
|
},
|
|
1766
2395
|
{
|
|
1767
2396
|
id: "step-3.7-flash",
|
|
@@ -1769,7 +2398,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1769
2398
|
.2,
|
|
1770
2399
|
1.15,
|
|
1771
2400
|
.04
|
|
1772
|
-
]
|
|
2401
|
+
],
|
|
2402
|
+
allowance: {
|
|
2403
|
+
goat: 20,
|
|
2404
|
+
pro: 30
|
|
2405
|
+
}
|
|
1773
2406
|
},
|
|
1774
2407
|
{
|
|
1775
2408
|
id: "step-5-preview",
|
|
@@ -1777,7 +2410,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1777
2410
|
1,
|
|
1778
2411
|
2.7,
|
|
1779
2412
|
.05
|
|
1780
|
-
]
|
|
2413
|
+
],
|
|
2414
|
+
allowance: {
|
|
2415
|
+
goat: 20,
|
|
2416
|
+
pro: 30
|
|
2417
|
+
}
|
|
1781
2418
|
},
|
|
1782
2419
|
{
|
|
1783
2420
|
id: "tencent/hy3-paid",
|
|
@@ -1785,7 +2422,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1785
2422
|
.14,
|
|
1786
2423
|
.58,
|
|
1787
2424
|
.035
|
|
1788
|
-
]
|
|
2425
|
+
],
|
|
2426
|
+
allowance: {
|
|
2427
|
+
goat: 70,
|
|
2428
|
+
pro: 80
|
|
2429
|
+
}
|
|
1789
2430
|
},
|
|
1790
2431
|
{
|
|
1791
2432
|
id: "tencent/hy4-preview",
|
|
@@ -1793,7 +2434,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1793
2434
|
.834,
|
|
1794
2435
|
2.501,
|
|
1795
2436
|
.042
|
|
1796
|
-
]
|
|
2437
|
+
],
|
|
2438
|
+
allowance: {
|
|
2439
|
+
goat: 20,
|
|
2440
|
+
pro: 30
|
|
2441
|
+
}
|
|
1797
2442
|
},
|
|
1798
2443
|
{
|
|
1799
2444
|
id: "typesafe/jev",
|
|
@@ -1801,7 +2446,11 @@ const MODEL_PRICE_ROWS = [
|
|
|
1801
2446
|
.042,
|
|
1802
2447
|
0,
|
|
1803
2448
|
0
|
|
1804
|
-
]
|
|
2449
|
+
],
|
|
2450
|
+
allowance: {
|
|
2451
|
+
goat: 20,
|
|
2452
|
+
pro: 30
|
|
2453
|
+
}
|
|
1805
2454
|
}
|
|
1806
2455
|
];
|
|
1807
2456
|
/**
|
|
@@ -1854,16 +2503,23 @@ function ratesOf(values) {
|
|
|
1854
2503
|
if (values[3] !== void 0) rates.cacheWriteCost = values[3];
|
|
1855
2504
|
return rates;
|
|
1856
2505
|
}
|
|
1857
|
-
/**
|
|
1858
|
-
|
|
2506
|
+
/**
|
|
2507
|
+
* Build the full price row the browser prices a session with.
|
|
2508
|
+
*
|
|
2509
|
+
* Every figure switches together: once the deal stops being live, the base
|
|
2510
|
+
* rates AND each context band come from the pre-discount set, so a session
|
|
2511
|
+
* priced after a promotion ends can never mix the two.
|
|
2512
|
+
*/
|
|
2513
|
+
function wireRow(id, slug, row, now) {
|
|
2514
|
+
const promotional = dealLabel(id, now) !== void 0;
|
|
1859
2515
|
const price = {
|
|
1860
2516
|
id,
|
|
1861
2517
|
slug,
|
|
1862
|
-
...ratesOf(row.rates)
|
|
2518
|
+
...ratesOf(promotional ? row.rates : row.listRates ?? row.rates)
|
|
1863
2519
|
};
|
|
1864
2520
|
if (row.peak !== void 0) price.peak = ratesOf(row.peak);
|
|
1865
2521
|
if (row.contextTiers !== void 0) price.contextTiers = row.contextTiers.map((tier) => ({
|
|
1866
|
-
...ratesOf(tier.rates),
|
|
2522
|
+
...ratesOf(promotional ? tier.rates : tier.listRates ?? tier.rates),
|
|
1867
2523
|
...tier.maxContext === void 0 ? {} : { maxContext: tier.maxContext }
|
|
1868
2524
|
}));
|
|
1869
2525
|
return price;
|
|
@@ -1878,8 +2534,13 @@ function wireRow(id, slug, row) {
|
|
|
1878
2534
|
* explicitly at zero so the composer can say so instead of showing nothing; the
|
|
1879
2535
|
* peak windows travel with the table so the browser applies the very schedule
|
|
1880
2536
|
* this snapshot knows instead of restating it.
|
|
2537
|
+
*
|
|
2538
|
+
* `now` (defaults to `Date.now()`) resolves lapsed promotions: a discounted row
|
|
2539
|
+
* whose `expiresAt` has passed is served at its pre-discount `listRates`, the
|
|
2540
|
+
* same instant {@link dealLabel} stops showing its badge. The table is rebuilt
|
|
2541
|
+
* per request, so that switch needs no redeploy.
|
|
1881
2542
|
*/
|
|
1882
|
-
function modelPriceTable() {
|
|
2543
|
+
function modelPriceTable(now = Date.now()) {
|
|
1883
2544
|
const bySlug = new Map(MODEL_PRICE_ROWS.map((row) => [row.id, row]));
|
|
1884
2545
|
const known = new Set(bySlug.keys());
|
|
1885
2546
|
const models = [];
|
|
@@ -1901,86 +2562,56 @@ function modelPriceTable() {
|
|
|
1901
2562
|
const row = bySlug.get(slug);
|
|
1902
2563
|
if (row === void 0) continue;
|
|
1903
2564
|
claimed.add(slug);
|
|
1904
|
-
models.push(wireRow(catalogId, slug, row));
|
|
2565
|
+
models.push(wireRow(catalogId, slug, row, now));
|
|
1905
2566
|
}
|
|
1906
2567
|
for (const row of MODEL_PRICE_ROWS) {
|
|
1907
2568
|
if (claimed.has(row.id)) continue;
|
|
1908
|
-
models.push(wireRow(row.id, row.id, row));
|
|
2569
|
+
models.push(wireRow(row.id, row.id, row, now));
|
|
1909
2570
|
}
|
|
1910
2571
|
return {
|
|
1911
2572
|
models,
|
|
1912
2573
|
peakHours: PEAK_HOUR_RANGES.map(([start, end]) => [start, end])
|
|
1913
2574
|
};
|
|
1914
2575
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
const
|
|
1918
|
-
uncachedInputTokens: 0,
|
|
1919
|
-
outputTokens: 0,
|
|
1920
|
-
cacheReadTokens: 0,
|
|
1921
|
-
cacheWriteTokens: 0
|
|
1922
|
-
});
|
|
1923
|
-
const COST_TOKEN_KEYS = [
|
|
1924
|
-
"uncachedInputTokens",
|
|
1925
|
-
"outputTokens",
|
|
1926
|
-
"cacheReadTokens",
|
|
1927
|
-
"cacheWriteTokens"
|
|
1928
|
-
];
|
|
1929
|
-
function peakHour(at, windows) {
|
|
1930
|
-
const time = new Date(at);
|
|
1931
|
-
return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
|
|
1932
|
-
}
|
|
2576
|
+
/** Slug-keyed view of the rows, built once: neither table nor allowance mutates. */
|
|
2577
|
+
const ROWS_BY_SLUG = new Map(MODEL_PRICE_ROWS.map((row) => [row.id, row]));
|
|
2578
|
+
const KNOWN_SLUGS = new Set(ROWS_BY_SLUG.keys());
|
|
1933
2579
|
/**
|
|
1934
|
-
*
|
|
1935
|
-
*
|
|
2580
|
+
* The per-model monthly allowance for a CATALOG model id, or undefined when the
|
|
2581
|
+
* page carries none (a model without a price row, or the free stealth previews
|
|
2582
|
+
* the estimator array omits).
|
|
1936
2583
|
*
|
|
1937
|
-
*
|
|
1938
|
-
*
|
|
1939
|
-
*
|
|
1940
|
-
*
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2584
|
+
* Resolution rides the same slug rules as the price table, so
|
|
2585
|
+
* `MiniMaxAI/MiniMax-M3` finds the row the page files under `minimax-m3` — the
|
|
2586
|
+
* settings page must not grow a second mapping that can drift from this one.
|
|
2587
|
+
* The settings page asks for a model only while rendering a list it already
|
|
2588
|
+
* has, so this stays a pure lookup with no clock: an allowance is not
|
|
2589
|
+
* time-dependent (a plan's figure does not lapse the way a promotional rate
|
|
2590
|
+
* does; when the page re-prices it, the next sync carries it).
|
|
2591
|
+
*/
|
|
2592
|
+
function modelAllowanceFor(modelId) {
|
|
2593
|
+
const slug = priceSlugFor(modelId, KNOWN_SLUGS);
|
|
2594
|
+
if (slug === void 0) return void 0;
|
|
2595
|
+
return ROWS_BY_SLUG.get(slug)?.allowance;
|
|
2596
|
+
}
|
|
2597
|
+
//#endregion
|
|
2598
|
+
//#region src/wire-guards.ts
|
|
2599
|
+
/** Non-throwing value guards for untrusted provider payloads. */
|
|
2600
|
+
function isRecord(value) {
|
|
2601
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1946
2602
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
const rates = (r) => [
|
|
1950
|
-
r.inputCost,
|
|
1951
|
-
r.outputCost,
|
|
1952
|
-
r.cacheReadCost,
|
|
1953
|
-
r.cacheWriteCost ?? null
|
|
1954
|
-
];
|
|
1955
|
-
const canonical = JSON.stringify([
|
|
1956
|
-
"commandCodeCost-v1",
|
|
1957
|
-
table.peakHours,
|
|
1958
|
-
table.models.map((p) => [
|
|
1959
|
-
p.id,
|
|
1960
|
-
p.slug,
|
|
1961
|
-
p.free === true,
|
|
1962
|
-
rates(p),
|
|
1963
|
-
p.peak ? rates(p.peak) : null,
|
|
1964
|
-
p.contextTiers?.map((t) => [t.maxContext ?? null, rates(t)]) ?? null
|
|
1965
|
-
])
|
|
1966
|
-
]);
|
|
1967
|
-
let hash = 2166136261;
|
|
1968
|
-
for (const c of canonical) hash = Math.imul(hash ^ c.charCodeAt(0), 16777619) >>> 0;
|
|
1969
|
-
return hash;
|
|
2603
|
+
function stringValue(value) {
|
|
2604
|
+
return typeof value === "string" ? value : void 0;
|
|
1970
2605
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
group.model,
|
|
1977
|
-
price === void 0 ? null : requestRates(price, group.at, group.contextTokens, table),
|
|
1978
|
-
group.at === null
|
|
1979
|
-
]);
|
|
2606
|
+
function numberValue(value) {
|
|
2607
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2608
|
+
}
|
|
2609
|
+
function booleanValue(value) {
|
|
2610
|
+
return typeof value === "boolean" ? value : void 0;
|
|
1980
2611
|
}
|
|
1981
2612
|
//#endregion
|
|
1982
2613
|
//#region src/cost-projection.ts
|
|
1983
|
-
const record$2 = (v) =>
|
|
2614
|
+
const record$2 = (v) => isRecord(v) ? v : void 0;
|
|
1984
2615
|
const finite = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
1985
2616
|
function tokensOf(value) {
|
|
1986
2617
|
const u = record$2(value);
|
|
@@ -2069,11 +2700,18 @@ function createCostProjection(table = modelPriceTable()) {
|
|
|
2069
2700
|
...g,
|
|
2070
2701
|
tokens: { ...g.tokens }
|
|
2071
2702
|
}));
|
|
2703
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2704
|
+
groups.forEach((g, i) => {
|
|
2705
|
+
const key = costGroupKey(g, table);
|
|
2706
|
+
if (!byKey.has(key)) byKey.set(key, i);
|
|
2707
|
+
});
|
|
2708
|
+
const groupKey = costGroupKey(group, table);
|
|
2072
2709
|
if (previous) {
|
|
2073
|
-
const old =
|
|
2074
|
-
if (old) for (const k of COST_TOKEN_KEYS) old.tokens[k] -= previous.tokens[k];
|
|
2710
|
+
const old = byKey.get(costGroupKey(previous, table));
|
|
2711
|
+
if (old !== void 0) for (const k of COST_TOKEN_KEYS) groups[old].tokens[k] -= previous.tokens[k];
|
|
2075
2712
|
}
|
|
2076
|
-
|
|
2713
|
+
const targetIdx = byKey.get(groupKey);
|
|
2714
|
+
let target = targetIdx === void 0 ? void 0 : groups[targetIdx];
|
|
2077
2715
|
if (!target) {
|
|
2078
2716
|
target = {
|
|
2079
2717
|
...group,
|
|
@@ -2122,6 +2760,27 @@ function installCostProjection(ctx) {
|
|
|
2122
2760
|
* JSON, SSE, and pre-stream error bodies undecoded.
|
|
2123
2761
|
*/
|
|
2124
2762
|
const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
|
|
2763
|
+
const THROTTLED_CODE = "THROTTLED";
|
|
2764
|
+
const boundedCodes = /* @__PURE__ */ new Set([
|
|
2765
|
+
"EMPTY_RESPONSE",
|
|
2766
|
+
"SERVER",
|
|
2767
|
+
"TIMEOUT",
|
|
2768
|
+
THROTTLED_CODE
|
|
2769
|
+
]);
|
|
2770
|
+
const counts$1 = /* @__PURE__ */ new WeakMap();
|
|
2771
|
+
function absorbTransientFailure(agent, code) {
|
|
2772
|
+
if (!boundedCodes.has(code)) return "ignored";
|
|
2773
|
+
const used = counts$1.get(agent) ?? 0;
|
|
2774
|
+
if (used >= 3) return "exhausted";
|
|
2775
|
+
counts$1.set(agent, used + 1);
|
|
2776
|
+
return "retry";
|
|
2777
|
+
}
|
|
2778
|
+
function resetTransientFailures(agent) {
|
|
2779
|
+
counts$1.delete(agent);
|
|
2780
|
+
}
|
|
2781
|
+
function transientBudgetMessage(code, message) {
|
|
2782
|
+
return `Command Code 请求在 3 次自动重试后仍失败,已停止重试(${code})。请稍后重新发送;若反复出现,请检查服务状态或请求耗时诊断。 原因:${message}`;
|
|
2783
|
+
}
|
|
2125
2784
|
//#endregion
|
|
2126
2785
|
//#region src/accounts.ts
|
|
2127
2786
|
/**
|
|
@@ -2143,24 +2802,18 @@ const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
|
|
|
2143
2802
|
* @module dsh-commandcode-provider/accounts
|
|
2144
2803
|
*/
|
|
2145
2804
|
/**
|
|
2146
|
-
* Upper bound on the retry wait
|
|
2147
|
-
*
|
|
2148
|
-
*
|
|
2149
|
-
*
|
|
2150
|
-
*
|
|
2151
|
-
* (in normal mode), turning "poll until the window opens" into "fail now".
|
|
2805
|
+
* Upper bound on the retry wait attached to the all-exhausted `RATE_LIMIT`.
|
|
2806
|
+
* Must equal the adapter's `backoff.maxDelayMs`: dsh-llm-retry honors a
|
|
2807
|
+
* provider wait verbatim only at or below that cap — a longer one abandons the
|
|
2808
|
+
* retry instead of falling back to local backoff, turning "poll until the
|
|
2809
|
+
* window opens" into "fail now".
|
|
2152
2810
|
*/
|
|
2153
2811
|
const RETRY_MAX_DELAY_MS = 9e5;
|
|
2154
2812
|
/**
|
|
2155
2813
|
* How long an explicitly selected account's rate-limit mark may stand before
|
|
2156
|
-
* its window is probed again.
|
|
2157
|
-
*
|
|
2158
|
-
*
|
|
2159
|
-
* {@link accountUsable} refuses — for a key nobody asked for that is correct
|
|
2160
|
-
* (only the all-marked pass clears it, and it costs one billing call), but for
|
|
2161
|
-
* the user's own pinned account a single 429 would demote it until the process
|
|
2162
|
-
* restarts (issue #51). Once per interval per key bounds a probe endpoint that
|
|
2163
|
-
* keeps failing; a successful probe resolves the mark for good.
|
|
2814
|
+
* its window is probed again. Without this, a single 429 on the user's pinned
|
|
2815
|
+
* account demotes it until the process restarts (issue #51); probing at most
|
|
2816
|
+
* once per interval per key bounds a probe endpoint that keeps failing.
|
|
2164
2817
|
*/
|
|
2165
2818
|
const EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS = 6e4;
|
|
2166
2819
|
/** A labeled, human-readable clock reading for error messages. */
|
|
@@ -2265,26 +2918,18 @@ var CommandCodeAccountPool = class {
|
|
|
2265
2918
|
return out;
|
|
2266
2919
|
}
|
|
2267
2920
|
/**
|
|
2268
|
-
* Hand out the key for a request: the model-routed account when
|
|
2269
|
-
*
|
|
2270
|
-
*
|
|
2271
|
-
*
|
|
2272
|
-
*
|
|
2273
|
-
*
|
|
2274
|
-
* `RATE_LIMIT` (naming the earliest window reset) or `INVALID_CREDENTIAL`
|
|
2275
|
-
* when accounts exist but every one of them is marked.
|
|
2921
|
+
* Hand out the key for a request: the model-routed account when usable, else
|
|
2922
|
+
* the manually preferred account when usable, else the first usable account
|
|
2923
|
+
* in rotation order. Returns `undefined` when no account resolves any key,
|
|
2924
|
+
* or when an account this request already used is still UNMARKED — the
|
|
2925
|
+
* caller's own rejection is the honest answer then. Throws `RATE_LIMIT` /
|
|
2926
|
+
* `INVALID_CREDENTIAL` when every account is marked.
|
|
2276
2927
|
*
|
|
2277
|
-
* `options.
|
|
2278
|
-
* resolution,
|
|
2279
|
-
*
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2282
|
-
* (no credits, a model outside the account's plan) would otherwise be offered
|
|
2283
|
-
* again on every attempt and the accounts behind it never reached.
|
|
2284
|
-
*
|
|
2285
|
-
* An explicit selection (the pin or a model rule) that a rate-limit mark would
|
|
2286
|
-
* demote is probed before the fallback serves, so "falls back while exhausted"
|
|
2287
|
-
* never becomes "stays demoted until the process restarts" (issue #51).
|
|
2928
|
+
* `options.tried` lists the keys this request already used; they are removed
|
|
2929
|
+
* from the resolution entirely, which is what lets one request walk a
|
|
2930
|
+
* four-account pool. An explicit selection (pin or model rule) that a
|
|
2931
|
+
* rate-limit mark would demote is probed first, so a fallback never becomes
|
|
2932
|
+
* permanent (issue #51). Rules re-read per resolution, so settings apply live.
|
|
2288
2933
|
*/
|
|
2289
2934
|
async resolveKey(options) {
|
|
2290
2935
|
const accounts = await this.resolvedAccounts();
|
|
@@ -2320,18 +2965,7 @@ var CommandCodeAccountPool = class {
|
|
|
2320
2965
|
this.states.delete(account.key);
|
|
2321
2966
|
return;
|
|
2322
2967
|
}
|
|
2323
|
-
|
|
2324
|
-
kind: "cooldown",
|
|
2325
|
-
cause: "window",
|
|
2326
|
-
reason: account.state?.reason ?? "usage window exhausted (429)",
|
|
2327
|
-
until: probe.resetAt
|
|
2328
|
-
});
|
|
2329
|
-
else if (account.state?.kind !== "cooldown") this.states.set(account.key, {
|
|
2330
|
-
kind: "unknown",
|
|
2331
|
-
cause: "window",
|
|
2332
|
-
reason: account.state?.reason ?? "usage window exhausted (429)",
|
|
2333
|
-
until: 0
|
|
2334
|
-
});
|
|
2968
|
+
this.stampWindowMark(account, probe);
|
|
2335
2969
|
}));
|
|
2336
2970
|
const latestAccounts = await this.resolvedAccounts();
|
|
2337
2971
|
const revived = selectActiveAccount(tried === void 0 || tried.length === 0 ? latestAccounts : latestAccounts.filter((account) => !tried.includes(account.key)), preferred);
|
|
@@ -2464,6 +3098,18 @@ var CommandCodeAccountPool = class {
|
|
|
2464
3098
|
state: void 0
|
|
2465
3099
|
};
|
|
2466
3100
|
}
|
|
3101
|
+
this.stampWindowMark(account, probe);
|
|
3102
|
+
}
|
|
3103
|
+
/**
|
|
3104
|
+
* Stamp a probe-read window onto one key. A known reset becomes a cooldown
|
|
3105
|
+
* that expires by itself; without one the mark stays `unknown`, so a later
|
|
3106
|
+
* probe can still learn it — and a mark that already carries a cooldown
|
|
3107
|
+
* keeps it, never trading a known reset for an unknown one. The cause is
|
|
3108
|
+
* `window` either way: a probe that read a window as exceeded IS window
|
|
3109
|
+
* evidence, whatever the key was marked for. An `until: 0` cooldown would
|
|
3110
|
+
* read as "never usable again" to {@link accountUsable}, so `unknown` it is.
|
|
3111
|
+
*/
|
|
3112
|
+
stampWindowMark(account, probe) {
|
|
2467
3113
|
if (probe.resetAt > 0) this.states.set(account.key, {
|
|
2468
3114
|
kind: "cooldown",
|
|
2469
3115
|
cause: "window",
|
|
@@ -2512,7 +3158,7 @@ var CommandCodeAccountPool = class {
|
|
|
2512
3158
|
function allAccountsUnusable(accounts) {
|
|
2513
3159
|
if (accounts.filter((account) => account.state?.kind === "disabled").length === accounts.length) return new LlmError(`llm-commandcode: every configured Command Code account (${accounts.length}) was rejected with 401 — check the stored API keys (Models page / settings) or the auth file;已配置的 ${accounts.length} 个 Command Code 账户密钥均被拒绝(401)——请在设置页检查存储的 API 密钥,或重新运行 command-code login`, "INVALID_CREDENTIAL");
|
|
2514
3160
|
const windowMarked = accounts.map((account) => account.state).filter((state) => state !== void 0).filter((state) => state.cause === "window");
|
|
2515
|
-
if (windowMarked.length === 0) return new LlmError(`llm-commandcode: all ${accounts.length} Command Code account(s) are rate limited (429) — the provider did not report an exhausted usage window; retrying;全部 ${accounts.length} 个 Command Code 账户被限流(429)——服务商未报告用量窗口用尽,正在重试`,
|
|
3161
|
+
if (windowMarked.length === 0) return new LlmError(`llm-commandcode: all ${accounts.length} Command Code account(s) are rate limited (429) — the provider did not report an exhausted usage window; retrying;全部 ${accounts.length} 个 Command Code 账户被限流(429)——服务商未报告用量窗口用尽,正在重试`, THROTTLED_CODE);
|
|
2516
3162
|
const resets = windowMarked.filter((state) => state.kind === "cooldown" && state.until > 0).map((state) => state.until);
|
|
2517
3163
|
const earliest = resets.length > 0 ? Math.min(...resets) : 0;
|
|
2518
3164
|
const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
|
|
@@ -2647,12 +3293,12 @@ function commandCodeImageTokens(modelId, width, height) {
|
|
|
2647
3293
|
return imageTokenCost(imageTokenFamily(modelId), width, height);
|
|
2648
3294
|
}
|
|
2649
3295
|
/**
|
|
2650
|
-
* Anthropic: the image is first scaled
|
|
2651
|
-
*
|
|
3296
|
+
* Anthropic: the image is first scaled to the request long edge (never
|
|
3297
|
+
* enlarged), then charged roughly one token per 750 pixels.
|
|
2652
3298
|
* Source: Anthropic vision documentation, "Image size and token usage".
|
|
2653
3299
|
*/
|
|
2654
3300
|
function anthropicImageTokens(width, height) {
|
|
2655
|
-
const projected = longEdgeDimensions(width, height,
|
|
3301
|
+
const projected = longEdgeDimensions(width, height, REQUEST_IMAGE_MAX_LONG_EDGE);
|
|
2656
3302
|
return Math.max(1, Math.ceil(projected.width * projected.height / 750));
|
|
2657
3303
|
}
|
|
2658
3304
|
/**
|
|
@@ -2933,6 +3579,95 @@ function openStreamTrace(scope, options = {}) {
|
|
|
2933
3579
|
};
|
|
2934
3580
|
}
|
|
2935
3581
|
//#endregion
|
|
3582
|
+
//#region src/request-timing.ts
|
|
3583
|
+
/** 请求耗时只记录阶段、计数和状态;不记录消息、凭据、请求头或错误正文。 */
|
|
3584
|
+
const REQUEST_TIMING_ENV = "DSH_COMMANDCODE_TIMING";
|
|
3585
|
+
/** 每次生成独立计时;可注入时钟验证顺序,不依赖真实等待。 */
|
|
3586
|
+
var RequestTiming = class {
|
|
3587
|
+
sink;
|
|
3588
|
+
now;
|
|
3589
|
+
started;
|
|
3590
|
+
summary;
|
|
3591
|
+
constructor(model, sink, now = () => performance.now()) {
|
|
3592
|
+
this.sink = sink;
|
|
3593
|
+
this.now = now;
|
|
3594
|
+
this.started = now();
|
|
3595
|
+
this.summary = {
|
|
3596
|
+
version: 1,
|
|
3597
|
+
startedAt: Date.now(),
|
|
3598
|
+
model,
|
|
3599
|
+
outcome: "cancelled",
|
|
3600
|
+
totalMs: 0,
|
|
3601
|
+
phasesMs: {
|
|
3602
|
+
credentials: 0,
|
|
3603
|
+
images: 0,
|
|
3604
|
+
body: 0,
|
|
3605
|
+
serialization: 0,
|
|
3606
|
+
headers: 0
|
|
3607
|
+
},
|
|
3608
|
+
attempts: []
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
3611
|
+
/** 返回结束函数,确保成功和异常都计入同一阶段。阶段可嵌套,不能直接相加。 */
|
|
3612
|
+
phase(name) {
|
|
3613
|
+
if (!this.sink) return () => {};
|
|
3614
|
+
const start = this.now();
|
|
3615
|
+
return () => {
|
|
3616
|
+
this.summary.phasesMs[name] += this.now() - start;
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
attempt(protocol, bodyBytes) {
|
|
3620
|
+
if (!this.sink) return () => {};
|
|
3621
|
+
const start = this.now();
|
|
3622
|
+
const attempt = {
|
|
3623
|
+
protocol,
|
|
3624
|
+
bodyBytes,
|
|
3625
|
+
headersMs: 0
|
|
3626
|
+
};
|
|
3627
|
+
this.summary.attempts.push(attempt);
|
|
3628
|
+
return (status) => {
|
|
3629
|
+
Object.assign(attempt, {
|
|
3630
|
+
headersMs: this.now() - start,
|
|
3631
|
+
...status === void 0 ? {} : { status }
|
|
3632
|
+
});
|
|
3633
|
+
this.summary.phasesMs.headers += attempt.headersMs;
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
first(kind) {
|
|
3637
|
+
if (this.sink && this.summary[kind] === void 0) this.summary[kind] = this.now() - this.started;
|
|
3638
|
+
}
|
|
3639
|
+
async close(outcome, errorCode) {
|
|
3640
|
+
if (!this.sink) return;
|
|
3641
|
+
this.summary.outcome = outcome;
|
|
3642
|
+
this.summary.totalMs = this.now() - this.started;
|
|
3643
|
+
if (errorCode !== void 0) this.summary.errorCode = errorCode;
|
|
3644
|
+
try {
|
|
3645
|
+
await this.sink(this.summary);
|
|
3646
|
+
} catch {}
|
|
3647
|
+
}
|
|
3648
|
+
};
|
|
3649
|
+
/** 与原始流诊断独立开关,便于仅收集数值耗时。 */
|
|
3650
|
+
function openRequestTiming(model, sink) {
|
|
3651
|
+
if (sink) return new RequestTiming(model, sink);
|
|
3652
|
+
const raw = process.env[REQUEST_TIMING_ENV]?.trim();
|
|
3653
|
+
if (!raw || [
|
|
3654
|
+
"0",
|
|
3655
|
+
"false",
|
|
3656
|
+
"no",
|
|
3657
|
+
"off"
|
|
3658
|
+
].includes(raw.toLowerCase())) return new RequestTiming(model);
|
|
3659
|
+
const path = [
|
|
3660
|
+
"1",
|
|
3661
|
+
"true",
|
|
3662
|
+
"yes",
|
|
3663
|
+
"on"
|
|
3664
|
+
].includes(raw.toLowerCase()) ? join(tmpdir(), "dsh-commandcode-timing.jsonl") : raw;
|
|
3665
|
+
return new RequestTiming(model, async (summary) => {
|
|
3666
|
+
await mkdir(dirname(path), { recursive: true });
|
|
3667
|
+
await appendFile(path, `${JSON.stringify(summary)}\n`, { mode: 384 });
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
//#endregion
|
|
2936
3671
|
//#region src/adapter.ts
|
|
2937
3672
|
/**
|
|
2938
3673
|
* DeepSeek Harness LLM adapter for the Command Code Provider API.
|
|
@@ -2941,21 +3676,151 @@ function openStreamTrace(scope, options = {}) {
|
|
|
2941
3676
|
* community-maintained integration; you need your own Command Code account
|
|
2942
3677
|
* and API key or subscription, and Command Code's terms apply.
|
|
2943
3678
|
*
|
|
2944
|
-
* Owns the
|
|
3679
|
+
* Owns the three chat transports (`/alpha/generate`, `/provider/v1/chat/completions`,
|
|
3680
|
+
* `/provider/v1/messages`),
|
|
2945
3681
|
* message conversion, SSE/JSONL parsing, the catalog + its on-disk cache, and
|
|
2946
3682
|
* the pre-stream account rotation loop. The wire protocol is
|
|
2947
|
-
* reverse-engineered (command-code@1.28.4, re-verified through 1.
|
|
2948
|
-
*
|
|
3683
|
+
* reverse-engineered (command-code@1.28.4, re-verified through 1.72.1);
|
|
3684
|
+
* docs/agent-reference/adapter-protocol.md holds the full protocol record.
|
|
2949
3685
|
*
|
|
2950
3686
|
* The adapter is deliberately free of cordis/schemastery: it receives a
|
|
2951
3687
|
* per-request options thunk and an API-key resolver from the plugin entry
|
|
2952
3688
|
* (src/index.ts), so a settings change reaches the very next request.
|
|
2953
3689
|
*/
|
|
2954
|
-
|
|
3690
|
+
/**
|
|
3691
|
+
* Output ceilings the endpoint has named in a live rejection, per model.
|
|
3692
|
+
*
|
|
3693
|
+
* The snapshot in ./capabilities.ts carries the vendor-published ceiling for
|
|
3694
|
+
* 74 of the 84 catalog models (see scripts/sync-output-limits.mjs). The other
|
|
3695
|
+
* 10 are models no vendor publishes yet, and a gateway is free to sit below
|
|
3696
|
+
* the vendor figure even where one exists — so this map is the correction
|
|
3697
|
+
* channel, not a fallback of last resort: the endpoint states the model's own
|
|
3698
|
+
* ceiling in its rejection, so one wasted round trip is enough to know it for
|
|
3699
|
+
* the rest of the process.
|
|
3700
|
+
*
|
|
3701
|
+
* Shared by adapters for the same gateway, but not across apiBase values: a
|
|
3702
|
+
* proxy can enforce a smaller ceiling for the same model id. Only ever tightens
|
|
3703
|
+
* within that gateway, so a stale catalog row cannot undo a live refusal.
|
|
3704
|
+
*/
|
|
3705
|
+
const learnedOutputLimits = /* @__PURE__ */ new Map();
|
|
3706
|
+
/**
|
|
3707
|
+
* Output ceilings the CATALOG published, per model.
|
|
3708
|
+
*
|
|
3709
|
+
* `/provider/v1/models` carries `max_output_tokens` in its schema and the
|
|
3710
|
+
* official pi provider already prefers it, but the endpoint does not send it
|
|
3711
|
+
* yet — so this is empty today and fills in on its own the day it does. It sits
|
|
3712
|
+
* above the models.dev snapshot and below what a live rejection proved, because
|
|
3713
|
+
* that ordering is also the order of trust: the gateway knows its own ceiling,
|
|
3714
|
+
* a rejection proves it, and models.dev is a third party describing the same
|
|
3715
|
+
* model. Restored from the on-disk catalog cache so a published ceiling
|
|
3716
|
+
* survives a restart without waiting for the next catalog fetch.
|
|
3717
|
+
*/
|
|
3718
|
+
const publishedOutputLimits = /* @__PURE__ */ new Map();
|
|
3719
|
+
/** Keep model facts from one gateway from constraining another gateway. */
|
|
3720
|
+
function outputLimitsFor(store, apiBase) {
|
|
3721
|
+
let limits = store.get(apiBase);
|
|
3722
|
+
if (limits === void 0) {
|
|
3723
|
+
limits = /* @__PURE__ */ new Map();
|
|
3724
|
+
store.set(apiBase, limits);
|
|
3725
|
+
}
|
|
3726
|
+
return limits;
|
|
3727
|
+
}
|
|
3728
|
+
/**
|
|
3729
|
+
* The output ceiling this gateway route sends for `modelId`.
|
|
3730
|
+
*
|
|
3731
|
+
* `/provider/v1/models` publishes `context_length` and `supported_endpoints`
|
|
3732
|
+
* but no output cap today, while each endpoint refuses a larger `max_tokens` per
|
|
3733
|
+
* model — the Messages endpoint names its own in the rejection (`max_tokens:
|
|
3734
|
+
* 200000 > 128000, which is the maximum allowed number of output tokens for
|
|
3735
|
+
* claude-sonnet-5-5`). What the endpoint has already named wins, then the
|
|
3736
|
+
* catalog's own figure once it publishes one, then the snapshot, then — for
|
|
3737
|
+
* the Messages-route models only — the conservative value, because the ceilings
|
|
3738
|
+
* actually observed are below the global default and assuming otherwise would
|
|
3739
|
+
* fail every request.
|
|
3740
|
+
*/
|
|
3741
|
+
function modelOutputTokenLimit(apiBase, modelId) {
|
|
3742
|
+
const learned = outputLimitsFor(learnedOutputLimits, apiBase).get(modelId);
|
|
3743
|
+
const known = outputLimitsFor(publishedOutputLimits, apiBase).get(modelId) ?? MODEL_OUTPUT_TOKEN_LIMITS.get(modelId);
|
|
3744
|
+
if (learned !== void 0 && (known === void 0 || learned < known)) return learned;
|
|
3745
|
+
if (known !== void 0) return known;
|
|
3746
|
+
return requiresMessagesEndpoint(modelId) ? DEFAULT_MESSAGES_MAX_TOKENS : DEFAULT_MAX_OUTPUT_TOKENS;
|
|
3747
|
+
}
|
|
3748
|
+
/**
|
|
3749
|
+
* Record a ceiling the endpoint just stated, if it is one this route can act
|
|
3750
|
+
* on: a real number, and one below what the next request would otherwise send.
|
|
3751
|
+
* A ceiling at or above the current budget describes a request that was not
|
|
3752
|
+
* refused for being too large, and acting on it would be a guess.
|
|
3753
|
+
*/
|
|
3754
|
+
function noteOutputCeiling(apiBase, modelId, ceiling) {
|
|
3755
|
+
if (!Number.isSafeInteger(ceiling) || ceiling <= 0) return false;
|
|
3756
|
+
if (ceiling >= modelOutputTokenLimit(apiBase, modelId)) return false;
|
|
3757
|
+
outputLimitsFor(learnedOutputLimits, apiBase).set(modelId, ceiling);
|
|
3758
|
+
return true;
|
|
3759
|
+
}
|
|
3760
|
+
/**
|
|
3761
|
+
* The ceiling to retry this turn with, when `error` is a request-side output
|
|
3762
|
+
* refusal the route can act on — else undefined, meaning surface it.
|
|
3763
|
+
*
|
|
3764
|
+
* Read from the message, so it covers both shapes the same refusal arrives in:
|
|
3765
|
+
* an HTTP 400 (`PROVIDER_HTTP_ERROR`) and an in-stream error frame
|
|
3766
|
+
* (`PROVIDER_STREAM_ERROR`). Both carry the endpoint's wording, and both are
|
|
3767
|
+
* already classified as output refusals rather than context overflows by the
|
|
3768
|
+
* time they reach here.
|
|
3769
|
+
*
|
|
3770
|
+
* Recording is what makes the retry pay off: the ceiling is kept for this
|
|
3771
|
+
* gateway and model, so a refused turn leaves its next request correct.
|
|
3772
|
+
*/
|
|
3773
|
+
function outputCeilingRefusal(apiBase, modelId, error) {
|
|
3774
|
+
if (!(error instanceof LlmError)) return void 0;
|
|
3775
|
+
const detail = error.message;
|
|
3776
|
+
if (!OUTPUT_LIMIT_REJECTION.test(detail)) return void 0;
|
|
3777
|
+
const stated = parseOutputCeiling(detail);
|
|
3778
|
+
if (stated === void 0) return void 0;
|
|
3779
|
+
return noteOutputCeiling(apiBase, modelId, stated) ? stated : void 0;
|
|
3780
|
+
}
|
|
3781
|
+
/** Keep the two user-facing languages and error metadata together. */
|
|
3782
|
+
function bilingual(code, en, zh, options) {
|
|
3783
|
+
return new LlmError(`${en};${zh}`, code, options);
|
|
3784
|
+
}
|
|
3785
|
+
const COMMAND_CODE_CLI_VERSION = "1.72.1";
|
|
2955
3786
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
2956
|
-
|
|
2957
|
-
|
|
3787
|
+
/**
|
|
3788
|
+
* The output budget this bundle asks for, and the ceiling it will never exceed.
|
|
3789
|
+
*
|
|
3790
|
+
* 131 072, chosen against a measured provider limit rather than a round guess.
|
|
3791
|
+
* Probing `/alpha/generate` directly on 2026-09-28: 64 000 → 200, 131 072 → 200,
|
|
3792
|
+
* 196 608 → 200, **200 000 → 200, 200 704 → 400**, 1 000 000 → 400. The server
|
|
3793
|
+
* therefore caps `max_tokens` at 200 000, and it does so identically for a
|
|
3794
|
+
* 262 144-window model (`stealth/pixel-canary`) and a 1 000 000-window one
|
|
3795
|
+
* (`stealth/space-bunny-alpha`) — the cap is global, not per model. 131 072
|
|
3796
|
+
* keeps a 35 % margin under it while covering the real output ceiling of the
|
|
3797
|
+
* models that have one (Claude 64 K, Gemini 64 K, several at 128 K).
|
|
3798
|
+
*
|
|
3799
|
+
* The old 64 000 was never a provider requirement: it cost nothing in latency
|
|
3800
|
+
* (a 64 000 request answered headers in 1.28 s against 1.57 s for 131 072) but
|
|
3801
|
+
* it did cap what a model could ever say in one turn.
|
|
3802
|
+
*
|
|
3803
|
+
* This is only an UPPER bound. `requestContextBudget` lowers it per request
|
|
3804
|
+
* from the prompt actually being sent, so raising the ceiling cannot push a
|
|
3805
|
+
* large session over its window.
|
|
3806
|
+
*/
|
|
3807
|
+
const DEFAULT_GENERATE_MAX_TOKENS = 131072;
|
|
3808
|
+
/**
|
|
3809
|
+
* The fallback when the catalog carries no output ceiling for a model.
|
|
3810
|
+
*
|
|
3811
|
+
* `/provider/v1/models` publishes only `context_length` — no output limit — so
|
|
3812
|
+
* the catalog cannot learn one per model and derives a stand-in from the
|
|
3813
|
+
* window instead. Kept equal to {@link DEFAULT_GENERATE_MAX_TOKENS} on purpose:
|
|
3814
|
+
* a lower value here would silently re-cap every request through
|
|
3815
|
+
* `Math.min(contextLength, this)` before the ceiling above is ever consulted.
|
|
3816
|
+
*/
|
|
3817
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 131072;
|
|
3818
|
+
/** Preserve a small usable answer budget when the heuristic overshoots. */
|
|
3819
|
+
const MIN_CONTEXT_OUTPUT_TOKENS = 1024;
|
|
2958
3820
|
const MODELS_TIMEOUT_MS = 1e4;
|
|
3821
|
+
/** 目录短期复用;刷新失败只短暂退避,避免反复阻塞模型选择器。 */
|
|
3822
|
+
const MODEL_CATALOG_TTL_MS = 3e5;
|
|
3823
|
+
const MODEL_CATALOG_RETRY_MS = 1e4;
|
|
2959
3824
|
/** How long the picker's plan-filter billing facts stay cached before refetching. */
|
|
2960
3825
|
const BILLING_ACCESS_TTL_MS = 3e5;
|
|
2961
3826
|
/** The entry-plan tier weight (`individual-go` in KNOWN_SUBSCRIPTION_PLANS). */
|
|
@@ -2971,19 +3836,70 @@ const ACTIVE_SUBSCRIPTION_STATUSES = /* @__PURE__ */ new Set([
|
|
|
2971
3836
|
"trialing",
|
|
2972
3837
|
"past_due"
|
|
2973
3838
|
]);
|
|
2974
|
-
/**
|
|
2975
|
-
|
|
3839
|
+
/**
|
|
3840
|
+
* Head-of-request timeout: how long to wait for the first response byte.
|
|
3841
|
+
*
|
|
3842
|
+
* 300 s, and that number is the official CLI's, not a round choice. Decompiled
|
|
3843
|
+
* from `command-code@1.66.0`: its `createNodeTransport` fetch passes only the
|
|
3844
|
+
* caller's `signal` and sets no timeout at all, so the model request inherits
|
|
3845
|
+
* Node/undici's own default — measured at 301.0 s against a local server that
|
|
3846
|
+
* accepts the connection and never sends headers. The previous 60 s default was
|
|
3847
|
+
* five times stricter than the CLI it emulates, and being stricter only ever
|
|
3848
|
+
* costs legitimate requests: a slow upstream first token on the Provider API
|
|
3849
|
+
* route can outlast 60 s, and the official CLI simply waits it out. Measured
|
|
3850
|
+
* headers (2026-09-28, 26 250-token prompt, `max_tokens: 16` to separate the
|
|
3851
|
+
* header from the generation): `/alpha/generate` 2.12 / 2.56 / 2.75 s,
|
|
3852
|
+
* `/provider/v1/chat/completions` 5.52 / 2.98 / 3.70 s — a ~1.1 s median gap
|
|
3853
|
+
* with the Provider API route varying over a 2.5 s span against the CLI route's
|
|
3854
|
+
* 0.6 s. A gateway that is genuinely dead now takes longer to name itself,
|
|
3855
|
+
* which is what the consecutive-timeout streak exists to bound (three
|
|
3856
|
+
* attempts, then a diagnosis instead of another re-send).
|
|
3857
|
+
*/
|
|
3858
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 3e5;
|
|
2976
3859
|
/** Stream idle timeout: a generation that stalls this long is a dead connection. */
|
|
2977
3860
|
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3861
|
+
/** Version 3 binds cached catalog facts to their gateway; older files lack a safe source. */
|
|
3862
|
+
const MODEL_CACHE_VERSION = 3;
|
|
3863
|
+
/**
|
|
3864
|
+
* A lone UTF-16 surrogate half: a high surrogate not followed by a low one, or
|
|
3865
|
+
* a low surrogate not preceded by a high one. Such a half is not a character —
|
|
3866
|
+
* it only arises from a truncated slice, a lossy decode, or a lone `\uD83D`
|
|
3867
|
+
* escape — and it survives `JSON.stringify` as an escape (`"\ud83d"`) rather
|
|
3868
|
+
* than failing locally, so the request leaves the process carrying a string no
|
|
3869
|
+
* UTF-8 consumer can round-trip. Providers running strict decoders reject it.
|
|
3870
|
+
*
|
|
3871
|
+
* This is the same expression `@earendil-works/pi-ai` applies to every outgoing
|
|
3872
|
+
* string on this endpoint. Valid astral characters (emoji and anything else
|
|
3873
|
+
* outside the BMP) are PAIRED surrogates and pass through untouched.
|
|
3874
|
+
*/
|
|
3875
|
+
const LONE_SURROGATE_PATTERN = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
3876
|
+
/**
|
|
3877
|
+
* A cheap pre-test for "this string contains any surrogate code unit at all",
|
|
3878
|
+
* paired or not. The exact pattern above needs lookaround, which V8 cannot
|
|
3879
|
+
* optimize into a fast scan; this one is a plain range test. Real prompts are
|
|
3880
|
+
* overwhelmingly ASCII-plus-CJK with no surrogates, so the common case pays one
|
|
3881
|
+
* linear scan instead of the lookaround pass. Measured on a 412 KB request
|
|
3882
|
+
* body: 1.40 ms with the exact pattern alone against 0.77 ms with this gate,
|
|
3883
|
+
* where a bare `JSON.stringify` is 0.59 ms.
|
|
3884
|
+
*/
|
|
3885
|
+
const ANY_SURROGATE_PATTERN = /[\uD800-\uDFFF]/;
|
|
3886
|
+
/**
|
|
3887
|
+
* Drop lone surrogate halves from one outgoing string.
|
|
3888
|
+
*
|
|
3889
|
+
* Applied at the single point a request body is serialized, so every field of
|
|
3890
|
+
* every transport is covered — system text, message content, tool schemas and
|
|
3891
|
+
* tool results alike — instead of enumerating the fields that can carry text
|
|
3892
|
+
* and missing one. The value is returned unchanged whenever it holds no
|
|
3893
|
+
* surrogate at all, which is the ordinary case.
|
|
3894
|
+
*
|
|
3895
|
+
* The `test()` call deliberately uses the flagless {@link ANY_SURROGATE_PATTERN}:
|
|
3896
|
+
* a global regex is stateful, so testing with one would advance `lastIndex` and
|
|
3897
|
+
* make consecutive calls on the same string alternate between true and false.
|
|
3898
|
+
* `replace()` resets `lastIndex` itself, so the global pattern is safe there.
|
|
3899
|
+
*/
|
|
3900
|
+
function sanitizeSurrogates(value) {
|
|
3901
|
+
if (typeof value !== "string") return value;
|
|
3902
|
+
return ANY_SURROGATE_PATTERN.test(value) ? value.replace(LONE_SURROGATE_PATTERN, "") : value;
|
|
2987
3903
|
}
|
|
2988
3904
|
/** Hash one request value without retaining or logging its contents. */
|
|
2989
3905
|
function fingerprintValue(value) {
|
|
@@ -3076,9 +3992,6 @@ function failureWithRequestId(error, metadata) {
|
|
|
3076
3992
|
...error.cause === void 0 ? {} : { cause: error.cause }
|
|
3077
3993
|
});
|
|
3078
3994
|
}
|
|
3079
|
-
function booleanValue(value) {
|
|
3080
|
-
return typeof value === "boolean" ? value : void 0;
|
|
3081
|
-
}
|
|
3082
3995
|
/**
|
|
3083
3996
|
* Thinking text carried by an OpenRouter-shaped `reasoning_details` array,
|
|
3084
3997
|
* concatenated in the order the gateway sent it.
|
|
@@ -3160,6 +4073,109 @@ const CLI_CONTEXT_OVERFLOW_PATTERN = /prompt is too long|context.*(length|window
|
|
|
3160
4073
|
function isContextOverflowDetail(detail) {
|
|
3161
4074
|
return isContextWindowExceededError(detail) || CLI_CONTEXT_OVERFLOW_PATTERN.test(detail);
|
|
3162
4075
|
}
|
|
4076
|
+
/** A gateway refusal observed for an over-reserved request, but not unique to that cause. */
|
|
4077
|
+
const AMBIGUOUS_SIZE_REJECTION = /\bmodel could not complete the request\b/i;
|
|
4078
|
+
/**
|
|
4079
|
+
* A request-side `max_tokens` refusal, told apart from a context overflow that
|
|
4080
|
+
* mentions the same field. Kept narrow on purpose: the overflow pattern also
|
|
4081
|
+
* matches a bare `max_tokens`, so this must name the specific shapes the
|
|
4082
|
+
* endpoint uses — a ceiling comparison, its prose about output tokens, or a
|
|
4083
|
+
* request-validation error on the field.
|
|
4084
|
+
*/
|
|
4085
|
+
const OUTPUT_LIMIT_REJECTION = new RegExp([
|
|
4086
|
+
"max_tokens:\\s*\\d+\\s*>\\s*\\d+",
|
|
4087
|
+
"maximum allowed number of output tokens",
|
|
4088
|
+
"invalid input[^\\n]*max_tokens",
|
|
4089
|
+
"expected number[^\\n]*at max_tokens"
|
|
4090
|
+
].join("|"), "i");
|
|
4091
|
+
/**
|
|
4092
|
+
* The output ceiling the endpoint states in a `max_tokens` refusal, read off
|
|
4093
|
+
* its own comparison: `max_tokens: 131072 > 128000` yields 128000.
|
|
4094
|
+
*
|
|
4095
|
+
* Only the comparison form is parsed. The other shapes
|
|
4096
|
+
* {@link OUTPUT_LIMIT_REJECTION} matches — a bare validation error, or the
|
|
4097
|
+
* prose without the numbers — name no ceiling, and guessing one would be worse
|
|
4098
|
+
* than failing: a fabricated value is indistinguishable from a real one at the
|
|
4099
|
+
* point where it decides the next request's budget.
|
|
4100
|
+
*/
|
|
4101
|
+
function parseOutputCeiling(detail) {
|
|
4102
|
+
const stated = /max_tokens:\s*\d+\s*>\s*(\d+)/i.exec(detail)?.[1];
|
|
4103
|
+
if (stated === void 0) return void 0;
|
|
4104
|
+
const ceiling = Number(stated);
|
|
4105
|
+
return Number.isSafeInteger(ceiling) && ceiling > 0 ? ceiling : void 0;
|
|
4106
|
+
}
|
|
4107
|
+
/**
|
|
4108
|
+
* The advice floor for a Provider-API timeout: doubling a 60 s budget is still
|
|
4109
|
+
* short for a slow upstream first token, so the suggestion never goes below 5
|
|
4110
|
+
* minutes. Capped by `RETRY_MAX_DELAY_MS` rather than the schema's timer
|
|
4111
|
+
* ceiling on purpose — a single header wait that outlasts the whole retry
|
|
4112
|
+
* backoff is not a budget, and clamping to the engine constant would also pull
|
|
4113
|
+
* a runtime dependency into the bundle.
|
|
4114
|
+
*/
|
|
4115
|
+
const MIN_SUGGESTED_TIMEOUT_MS = 3e5;
|
|
4116
|
+
/**
|
|
4117
|
+
* Why one attempt saw no response headers, and what the user can do about it.
|
|
4118
|
+
*
|
|
4119
|
+
* The TRANSPORT is the reliable signal; the request size is not — and the
|
|
4120
|
+
* mechanism is read straight out of the official CLI rather than inferred from
|
|
4121
|
+
* a timing. `createNodeTransport` passes only the caller's `signal` and sets no
|
|
4122
|
+
* timeout, so the two routes differ in WHO answers the headers:
|
|
4123
|
+
* `/alpha/generate` has the gateway answer them before the model starts, while
|
|
4124
|
+
* `/provider/v1/chat/completions` cannot answer until its upstream emits a
|
|
4125
|
+
* first token, so whatever that upstream is doing is charged to the header
|
|
4126
|
+
* wait. Measured 2026-09-28 (26 250-token prompt, `max_tokens: 16`):
|
|
4127
|
+
* `/alpha/generate` 2.12 / 2.56 / 2.75 s, `/provider/v1/chat/completions`
|
|
4128
|
+
* 5.52 / 2.98 / 3.70 s — a ~1.1 s median gap, with the Provider API route
|
|
4129
|
+
* swinging across 2.5 s where the CLI route moved 0.6 s. A larger prompt moves
|
|
4130
|
+
* neither, which is why an earlier draft that branched on a 1 MiB body
|
|
4131
|
+
* threshold was wrong: size is not the variable here. Only the route decides.
|
|
4132
|
+
*
|
|
4133
|
+
* @param protocol - the transport the attempt used.
|
|
4134
|
+
* @param timeoutMs - the header budget that just expired.
|
|
4135
|
+
*/
|
|
4136
|
+
function headersTimeoutAdvice(protocol, timeoutMs) {
|
|
4137
|
+
if (protocol === "openai") return {
|
|
4138
|
+
cause: "upstream-first-token",
|
|
4139
|
+
suggestionMs: Math.min(RETRY_MAX_DELAY_MS, Math.max(timeoutMs * 2, MIN_SUGGESTED_TIMEOUT_MS))
|
|
4140
|
+
};
|
|
4141
|
+
return { cause: "gateway" };
|
|
4142
|
+
}
|
|
4143
|
+
/**
|
|
4144
|
+
* How many consecutive header timeouts on ONE unchanged payload stop being a
|
|
4145
|
+
* blip. The retry policy whitelists `TIMEOUT` with `maxRetries: 1000` and a
|
|
4146
|
+
* 500 ms → 15 min backoff, which is right for a provider that says when to
|
|
4147
|
+
* come back but wrong for a gateway that simply never answers: the reporter
|
|
4148
|
+
* saw the identical multi-megabyte body re-sent for hours (issue #67) instead
|
|
4149
|
+
* of a diagnosis. Three is where a 60 s budget has already spent three
|
|
4150
|
+
* minutes waiting, so the cost of being wrong is bounded and the user gets an
|
|
4151
|
+
* answer either way.
|
|
4152
|
+
*/
|
|
4153
|
+
const MAX_CONSECUTIVE_HEADER_TIMEOUTS = 3;
|
|
4154
|
+
/** Streak key for a session-less call, so those keep the old single-slot semantics instead of never escalating. */
|
|
4155
|
+
const GLOBAL_HEADER_TIMEOUT_KEY = "\0global";
|
|
4156
|
+
/** UTF-8 byte length, computed only on the timeout path so a large body is encoded once, not per attempt. */
|
|
4157
|
+
function utf8ByteLength(text) {
|
|
4158
|
+
return new TextEncoder().encode(text).length;
|
|
4159
|
+
}
|
|
4160
|
+
/**
|
|
4161
|
+
* The thrown headers timeout, worded for the cause it actually has.
|
|
4162
|
+
*
|
|
4163
|
+
* Every earlier version of this message said "通常是网络或代理问题" for both
|
|
4164
|
+
* transports, which is actively wrong for a large `/provider/v1/chat/completions`
|
|
4165
|
+
* prompt: there the model simply needs longer than any budget, and a user sent
|
|
4166
|
+
* to check their proxy never finds anything. The code stays `TIMEOUT` (still
|
|
4167
|
+
* retryable) in both branches — only the diagnosis and the advice change.
|
|
4168
|
+
*/
|
|
4169
|
+
function headersTimeoutError(input) {
|
|
4170
|
+
const { protocol, endpoint, timeoutMs, cause } = input;
|
|
4171
|
+
const megabytes = `${(utf8ByteLength(input.body) / 1048576).toFixed(1)} MB`;
|
|
4172
|
+
const advice = headersTimeoutAdvice(protocol, timeoutMs);
|
|
4173
|
+
if (advice.cause === "upstream-first-token") {
|
|
4174
|
+
const suggestion = advice.suggestionMs ?? timeoutMs;
|
|
4175
|
+
return bilingual("TIMEOUT", `Command Code API request to ${endpoint} did not respond within ${timeoutMs}ms: this route withholds response headers until the upstream model emits its first token, and it did not start within the budget. Raise "Request timeout" in the plugin's advanced settings to ${suggestion}ms, or compact the session or start a new one. The request body was ${megabytes}, but size is not the cause here — a slow upstream is. ${errorChain(cause)}`, `Command Code API 请求在 ${timeoutMs} 毫秒内未收到响应:该路由要等上游模型吐出第一个 token 才返回响应头,而上游在预算内没有开始作答。可在插件「高级」设置里把「请求超时」调到 ${suggestion} 毫秒,或压缩上下文 / 新开会话。本次请求体约 ${megabytes},但体积不是原因——慢的是上游。(${errorChain(cause)})`, { cause });
|
|
4176
|
+
}
|
|
4177
|
+
return bilingual("TIMEOUT", `Command Code API request to ${endpoint} did not respond within ${timeoutMs}ms: the gateway returned no response headers at all — it answers headers itself before the model replies. This is usually provider queueing, a Go-plan capacity limit, or a network/proxy path problem; a longer timeout will not help, but retrying usually recovers. The request body was ${megabytes}, and size is not the cause on this route. ${errorChain(cause)}`, `Command Code API 请求在 ${timeoutMs} 毫秒内未收到响应:网关完全没有返回响应头——它会在模型开始回答前先返回响应头。这通常是服务商排队、Go 套餐容量限制,或网络/代理链路问题;调大超时不会有帮助,重试通常可恢复。本次请求体约 ${megabytes},而在该路由上体积不是原因。(${errorChain(cause)})`, { cause });
|
|
4178
|
+
}
|
|
3163
4179
|
/**
|
|
3164
4180
|
* Whether one rejection says "ZDR requested, no ZDR-capable upstream": the
|
|
3165
4181
|
* provider spells the code `cmd_zdr_no_providers` in JSON error bodies and
|
|
@@ -3203,9 +4219,21 @@ function streamErrorToLlmError(value, fallbackMessage) {
|
|
|
3203
4219
|
message
|
|
3204
4220
|
].filter((part) => part !== void 0 && part !== "").join(" ");
|
|
3205
4221
|
const statusOption = statusCode !== void 0 ? { status: statusCode } : void 0;
|
|
3206
|
-
if (
|
|
4222
|
+
if (OUTPUT_LIMIT_REJECTION.test(detail)) {
|
|
4223
|
+
const stated = /max_tokens:\s*\d+\s*>\s*\d+/i.exec(detail)?.[0];
|
|
4224
|
+
return bilingual("PROVIDER_STREAM_ERROR", `Command Code refused the request's max_tokens for this model` + (stated !== void 0 ? ` (${stated})` : "") + " — the session is not oversized, so compacting it will not help; lower \"Max output tokens\" or pick a model with a larger output ceiling", `Command Code 拒绝了本次请求的 max_tokens(该模型的输出上限更低)` + (stated !== void 0 ? `(${stated})` : "") + "——会话内容并未超长,压缩上下文无效;请调低「最大输出 token」或改用输出上限更大的模型", statusOption);
|
|
4225
|
+
}
|
|
4226
|
+
if (isContextOverflowDetail(detail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, `Command Code stream error: ${message}`, "Command Code API 拒绝了这次请求:内容超出模型上下文窗口。正在压缩上下文后重试;如仍失败,请新建会话或减少上下文", statusOption);
|
|
3207
4227
|
const terminal = hasTerminalStreamMarker(detail);
|
|
3208
4228
|
if (!(isRetryable === true || (isRetryable === false || terminal ? false : statusCode !== void 0 ? statusCode !== void 0 && (statusCode === 429 || statusCode >= 500) : true))) return new LlmError(`Command Code stream error: ${message}`, "PROVIDER_STREAM_ERROR", statusOption);
|
|
4229
|
+
const rejection = classifyAccountRejection(statusCode ?? 0, JSON.stringify(value) ?? message);
|
|
4230
|
+
if (rejection?.reason === "rate-limit" || rejection?.reason === "throttled") {
|
|
4231
|
+
const wait = rejection.resetAtMs === void 0 ? 0 : Math.min(RETRY_MAX_DELAY_MS, Math.max(1e3, rejection.resetAtMs - Date.now()));
|
|
4232
|
+
return bilingual(rejection.reason === "rate-limit" ? "RATE_LIMIT" : THROTTLED_CODE, `Command Code stream error: ${message}`, rejection.reason === "rate-limit" ? "Command Code 用量窗口已限流,等待恢复" : "Command Code 请求暂时被限流,正在重试", {
|
|
4233
|
+
...statusOption,
|
|
4234
|
+
...wait > 0 ? { providerRetryAfterMs: wait } : {}
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
3209
4237
|
return new LlmError(`Command Code stream error: ${message}`, "SERVER", statusOption);
|
|
3210
4238
|
}
|
|
3211
4239
|
function recordOrEmpty(value) {
|
|
@@ -3370,8 +4398,9 @@ function resolveAuthFileApiKey() {
|
|
|
3370
4398
|
return apiKeyFromCredentialRecord(parsed.commandcode) ?? apiKeyFromCredentialRecord(parsed["command-code"]);
|
|
3371
4399
|
} catch {}
|
|
3372
4400
|
}
|
|
3373
|
-
function parseCatalogResponse(value) {
|
|
4401
|
+
function parseCatalogResponse(value, apiBase) {
|
|
3374
4402
|
if (!isRecord(value) || value.object !== "list" || !Array.isArray(value.data)) throw new LlmError("Unexpected Command Code models response shape", "PROVIDER_PROTOCOL_ERROR");
|
|
4403
|
+
publishedOutputLimits.set(apiBase, /* @__PURE__ */ new Map());
|
|
3375
4404
|
const models = [];
|
|
3376
4405
|
for (const entry of value.data) {
|
|
3377
4406
|
if (!isRecord(entry)) continue;
|
|
@@ -3379,33 +4408,52 @@ function parseCatalogResponse(value) {
|
|
|
3379
4408
|
const name = stringValue(entry.name);
|
|
3380
4409
|
const contextLength = numberValue(entry.context_length);
|
|
3381
4410
|
if (!id || !name || !contextLength || contextLength <= 0) continue;
|
|
4411
|
+
const endpoints = Array.isArray(entry.supported_endpoints) ? entry.supported_endpoints.filter((item) => typeof item === "string") : [];
|
|
4412
|
+
const published = numberValue(entry.max_output_tokens);
|
|
4413
|
+
if (published !== void 0 && published > 0) outputLimitsFor(publishedOutputLimits, apiBase).set(id, published);
|
|
3382
4414
|
models.push({
|
|
3383
4415
|
id,
|
|
3384
4416
|
name,
|
|
3385
4417
|
contextWindow: contextLength,
|
|
3386
|
-
maxTokens: Math.min(contextLength,
|
|
4418
|
+
maxTokens: Math.min(contextLength, modelOutputTokenLimit(apiBase, id)),
|
|
4419
|
+
...published !== void 0 && published > 0 ? { publishedMaxTokens: published } : {},
|
|
4420
|
+
supportedEndpoints: endpoints
|
|
3387
4421
|
});
|
|
3388
4422
|
}
|
|
3389
4423
|
if (models.length === 0) throw new LlmError("Command Code returned an empty model catalog", "PROVIDER_PROTOCOL_ERROR");
|
|
3390
4424
|
return models;
|
|
3391
4425
|
}
|
|
3392
|
-
async function readModelsCache(cachePath) {
|
|
4426
|
+
async function readModelsCache(cachePath, apiBase) {
|
|
3393
4427
|
const parsed = JSON.parse(await readFile(cachePath, "utf-8"));
|
|
3394
|
-
if (!isRecord(parsed) || parsed.version !== MODEL_CACHE_VERSION || !Array.isArray(parsed.models)) throw new Error(`Invalid model cache at ${cachePath}`);
|
|
3395
|
-
|
|
4428
|
+
if (!isRecord(parsed) || parsed.version !== MODEL_CACHE_VERSION || parsed.apiBase !== apiBase || !Array.isArray(parsed.models)) throw new Error(`Invalid model cache at ${cachePath}`);
|
|
4429
|
+
publishedOutputLimits.set(apiBase, /* @__PURE__ */ new Map());
|
|
4430
|
+
return parsed.models.map((entry) => {
|
|
4431
|
+
const model = isRecord(entry) ? entry : {};
|
|
4432
|
+
const endpoints = Array.isArray(model.supportedEndpoints) ? model.supportedEndpoints.filter((item) => typeof item === "string") : [];
|
|
4433
|
+
const id = stringValue(model.id);
|
|
4434
|
+
const published = numberValue(model.publishedMaxTokens);
|
|
4435
|
+
if (id !== void 0 && published !== void 0 && published > 0) outputLimitsFor(publishedOutputLimits, apiBase).set(id, published);
|
|
4436
|
+
const contextWindow = numberValue(model.contextWindow);
|
|
4437
|
+
return {
|
|
4438
|
+
...model,
|
|
4439
|
+
supportedEndpoints: endpoints,
|
|
4440
|
+
maxTokens: id === void 0 || contextWindow === void 0 ? numberValue(model.maxTokens) ?? 131072 : Math.min(contextWindow, modelOutputTokenLimit(apiBase, id))
|
|
4441
|
+
};
|
|
4442
|
+
});
|
|
3396
4443
|
}
|
|
3397
|
-
async function writeModelsCache(cachePath, models) {
|
|
4444
|
+
async function writeModelsCache(cachePath, apiBase, models, mayCommit) {
|
|
3398
4445
|
await mkdir(dirname(cachePath), { recursive: true });
|
|
3399
|
-
const tmp = `${cachePath}.${process.pid}.tmp`;
|
|
4446
|
+
const tmp = `${cachePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
3400
4447
|
try {
|
|
3401
4448
|
await writeFile(tmp, `${JSON.stringify({
|
|
3402
4449
|
version: MODEL_CACHE_VERSION,
|
|
4450
|
+
apiBase,
|
|
3403
4451
|
models
|
|
3404
4452
|
}, null, 2)}\n`, {
|
|
3405
4453
|
encoding: "utf-8",
|
|
3406
4454
|
mode: 384
|
|
3407
4455
|
});
|
|
3408
|
-
await rename(tmp, cachePath);
|
|
4456
|
+
if (mayCommit()) await rename(tmp, cachePath);
|
|
3409
4457
|
} finally {
|
|
3410
4458
|
await rm(tmp, { force: true }).catch(() => void 0);
|
|
3411
4459
|
}
|
|
@@ -3581,9 +4629,9 @@ const ENGINE_IMAGE_POLICY = {
|
|
|
3581
4629
|
* dsh-llm-retry. On a profile that does not mount that plugin the failure is
|
|
3582
4630
|
* terminal — every shipped profile reaches it through dsh-base.
|
|
3583
4631
|
*/
|
|
3584
|
-
function imageOffloadRequired(missing) {
|
|
4632
|
+
function imageOffloadRequired(missing, reason = "budget") {
|
|
3585
4633
|
const options = { offloadImages: missing };
|
|
3586
|
-
return new LlmError(`commandcode request images exceed the route budget; ${missing} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, options);
|
|
4634
|
+
return new LlmError(reason === "cache" ? `commandcode cache-aware image history: ${missing} already-seen oldest image occurrence(s) must be offloaded before replay.` : `commandcode request images exceed the route budget; ${missing} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, options);
|
|
3587
4635
|
}
|
|
3588
4636
|
/** The budget in the shape the core counter takes (representation + the rung). */
|
|
3589
4637
|
function coreBudget(budget) {
|
|
@@ -3609,17 +4657,40 @@ const imageVersionBytes = (block) => block.attachment.bytes;
|
|
|
3609
4657
|
* otherwise the 413 retry could reintroduce images the first rung had already
|
|
3610
4658
|
* evicted.
|
|
3611
4659
|
*/
|
|
3612
|
-
function withSurfaceOffload(options, policy, budget) {
|
|
4660
|
+
function withSurfaceOffload(options, policy, budget, cacheOffloadModel) {
|
|
3613
4661
|
const marked = policy.projectOffloadedImages(options.messages, (ref) => offloadedImageText(ref));
|
|
3614
4662
|
const messages = marked === options.messages ? options.messages : [...marked];
|
|
3615
|
-
const
|
|
3616
|
-
|
|
4663
|
+
const budgetMissing = policy.requiredImageOffload(messages, coreBudget(budget), imageVersionBytes);
|
|
4664
|
+
const cacheMissing = cacheOffloadModel === void 0 ? 0 : seenImageOffloadCount(messages, cacheOffloadModel);
|
|
4665
|
+
const missing = Math.max(budgetMissing, cacheMissing);
|
|
4666
|
+
if (missing > 0) throw imageOffloadRequired(missing, cacheMissing > budgetMissing ? "cache" : "budget");
|
|
3617
4667
|
return messages === options.messages ? options : {
|
|
3618
4668
|
...options,
|
|
3619
4669
|
messages
|
|
3620
4670
|
};
|
|
3621
4671
|
}
|
|
3622
4672
|
/**
|
|
4673
|
+
* Count retained images already followed by an answer from this same model.
|
|
4674
|
+
* The current tool result's image has not yet been seen by the model and must
|
|
4675
|
+
* stay on the wire. The engine can only offload durable user/tool input nodes;
|
|
4676
|
+
* assistant output and request-only input have no selectable surface event.
|
|
4677
|
+
*/
|
|
4678
|
+
function seenImageOffloadCount(messages, model) {
|
|
4679
|
+
let lastAnswer = -1;
|
|
4680
|
+
for (let index = 0; index < messages.length; index++) {
|
|
4681
|
+
const message = messages[index];
|
|
4682
|
+
if (message.role === "assistant" && message.source.kind === "model" && message.source.provider === "commandcode" && message.source.model === model) lastAnswer = index;
|
|
4683
|
+
}
|
|
4684
|
+
if (lastAnswer < 0) return 0;
|
|
4685
|
+
let count = 0;
|
|
4686
|
+
for (let index = 0; index < lastAnswer; index++) {
|
|
4687
|
+
const message = messages[index];
|
|
4688
|
+
if (message.role !== "user" && message.role !== "tool" || message.id === void 0) continue;
|
|
4689
|
+
for (const block of message.content) if (block.type === "image") count++;
|
|
4690
|
+
}
|
|
4691
|
+
return count;
|
|
4692
|
+
}
|
|
4693
|
+
/**
|
|
3623
4694
|
* One image's bytes for the wire: the attachment service's REQUEST VERSION.
|
|
3624
4695
|
*
|
|
3625
4696
|
* `readImage()` answers the stored original, which is what admission accepted —
|
|
@@ -3630,7 +4701,39 @@ function withSurfaceOffload(options, policy, budget) {
|
|
|
3630
4701
|
* the point here, the encoded bytes).
|
|
3631
4702
|
*/
|
|
3632
4703
|
async function readRequestImage(attachments, ref) {
|
|
3633
|
-
return
|
|
4704
|
+
return attachments.readImageRequest(ref, requestImageTarget(ref));
|
|
4705
|
+
}
|
|
4706
|
+
/** 同一次生成及其协议切换共享图片读取,最多同时准备四张,避免内存峰值失控。 */
|
|
4707
|
+
function requestImages(attachments, signal, timing) {
|
|
4708
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4709
|
+
const read = (ref) => {
|
|
4710
|
+
signal?.throwIfAborted();
|
|
4711
|
+
const key = JSON.stringify([ref.attachmentId, requestImageTarget(ref)]);
|
|
4712
|
+
let pending = cache.get(key);
|
|
4713
|
+
if (!pending) {
|
|
4714
|
+
pending = readRequestImage(attachments, ref);
|
|
4715
|
+
cache.set(key, pending);
|
|
4716
|
+
}
|
|
4717
|
+
return pending;
|
|
4718
|
+
};
|
|
4719
|
+
read.prepare = async (refs) => {
|
|
4720
|
+
if (refs.length === 0) return;
|
|
4721
|
+
const finishImages = timing.phase("images");
|
|
4722
|
+
let cursor = 0;
|
|
4723
|
+
try {
|
|
4724
|
+
const workers = Array.from({ length: Math.min(4, refs.length) }, async () => {
|
|
4725
|
+
while (cursor < refs.length) {
|
|
4726
|
+
signal?.throwIfAborted();
|
|
4727
|
+
await read(refs[cursor++]);
|
|
4728
|
+
}
|
|
4729
|
+
});
|
|
4730
|
+
const failure = (await Promise.allSettled(workers)).find((result) => result.status === "rejected");
|
|
4731
|
+
if (failure?.status === "rejected") throw failure.reason;
|
|
4732
|
+
} finally {
|
|
4733
|
+
finishImages();
|
|
4734
|
+
}
|
|
4735
|
+
};
|
|
4736
|
+
return read;
|
|
3634
4737
|
}
|
|
3635
4738
|
/**
|
|
3636
4739
|
* Price one request occurrence.
|
|
@@ -3661,31 +4764,58 @@ function priceRequestImage(block, model, imageCapable) {
|
|
|
3661
4764
|
};
|
|
3662
4765
|
}
|
|
3663
4766
|
/**
|
|
3664
|
-
* Convert one image reference to the
|
|
3665
|
-
* CLI
|
|
3666
|
-
*
|
|
3667
|
-
*
|
|
4767
|
+
* Convert one image reference to the final `/alpha/generate` wire format.
|
|
4768
|
+
* The official CLI accepts a base64 source internally, then toWireMessages()
|
|
4769
|
+
* sends a data URL plus mimeType. Use the request version's media type because
|
|
4770
|
+
* readImageRequest() can transcode the stored image to JPEG or WebP.
|
|
3668
4771
|
*/
|
|
3669
4772
|
async function imageToCommandCode(ref, readImage) {
|
|
3670
|
-
const
|
|
4773
|
+
const version = await readImage(ref);
|
|
3671
4774
|
return {
|
|
3672
4775
|
type: "image",
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
media_type: ref.mediaType,
|
|
3676
|
-
data: Buffer.from(data).toString("base64")
|
|
3677
|
-
}
|
|
4776
|
+
image: `data:${version.mediaType};base64,${Buffer.from(version.data).toString("base64")}`,
|
|
4777
|
+
mimeType: version.mediaType
|
|
3678
4778
|
};
|
|
3679
4779
|
}
|
|
3680
|
-
|
|
4780
|
+
/**
|
|
4781
|
+
* The signature recorded for the reasoning block at `position` of a
|
|
4782
|
+
* historical assistant message, or undefined when the message was produced
|
|
4783
|
+
* by another route or an older plugin version.
|
|
4784
|
+
*/
|
|
4785
|
+
function messagesSignatureAt(message, position) {
|
|
4786
|
+
const replay = message.source?.replayState;
|
|
4787
|
+
if (!isRecord(replay) || !Array.isArray(replay.blocks)) return void 0;
|
|
4788
|
+
const entry = replay.blocks[position];
|
|
4789
|
+
return isRecord(entry) ? stringValue(entry.signature) : void 0;
|
|
4790
|
+
}
|
|
4791
|
+
/**
|
|
4792
|
+
* The common pairing and image-carrier ordering for both transports. Each
|
|
4793
|
+
* codec owns its wire shape: CLI content blocks and Chat Completions
|
|
4794
|
+
* reasoning/tool calls differ beyond the image envelope.
|
|
4795
|
+
*/
|
|
4796
|
+
async function convertMessages(messages, readImage, codec) {
|
|
3681
4797
|
const out = [];
|
|
3682
4798
|
const { ids: paired, names: toolNames } = pairedToolCalls(messages);
|
|
4799
|
+
if (readImage?.prepare) {
|
|
4800
|
+
const refs = [];
|
|
4801
|
+
for (const message of messages) {
|
|
4802
|
+
const result = toolResultOf(message);
|
|
4803
|
+
if (message.role === "user" && !result) {
|
|
4804
|
+
for (const block of message.content) if (block.type === "image") refs.push(block.attachment);
|
|
4805
|
+
} else if (result && paired.has(result.toolCallId)) refs.push(...toolResultMedia(result).images);
|
|
4806
|
+
}
|
|
4807
|
+
await readImage.prepare(refs);
|
|
4808
|
+
}
|
|
3683
4809
|
const wireIds = wireToolCallIds(paired);
|
|
3684
4810
|
const pendingImages = [];
|
|
3685
4811
|
const flushPendingImages = () => {
|
|
3686
4812
|
for (const message of pendingImages) out.push(message);
|
|
3687
4813
|
pendingImages.length = 0;
|
|
3688
4814
|
};
|
|
4815
|
+
const encodeImage = (ref) => {
|
|
4816
|
+
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
4817
|
+
return codec.encodeImage(ref, readImage);
|
|
4818
|
+
};
|
|
3689
4819
|
for (const message of messages) {
|
|
3690
4820
|
if (message.role === "system") continue;
|
|
3691
4821
|
const result = toolResultOf(message);
|
|
@@ -3697,86 +4827,89 @@ async function messagesToCC(messages, readImage) {
|
|
|
3697
4827
|
type: "text",
|
|
3698
4828
|
text: block.text
|
|
3699
4829
|
});
|
|
3700
|
-
if (block.type === "image")
|
|
3701
|
-
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
3702
|
-
parts.push(await imageToCommandCode(block.attachment, readImage));
|
|
3703
|
-
}
|
|
4830
|
+
if (block.type === "image") parts.push(await encodeImage(block.attachment));
|
|
3704
4831
|
}
|
|
3705
|
-
if (parts.length
|
|
3706
|
-
out.push({
|
|
3707
|
-
role: "user",
|
|
3708
|
-
content: parts
|
|
3709
|
-
});
|
|
4832
|
+
if (parts.length > 0) out.push(codec.encodeUser(parts));
|
|
3710
4833
|
continue;
|
|
3711
4834
|
}
|
|
3712
4835
|
if (message.role === "assistant") {
|
|
3713
4836
|
flushPendingImages();
|
|
3714
|
-
const
|
|
3715
|
-
|
|
3716
|
-
type: "text",
|
|
3717
|
-
text: block.text
|
|
3718
|
-
});
|
|
3719
|
-
else if (block.type === "reasoning") parts.push({
|
|
3720
|
-
type: "reasoning",
|
|
3721
|
-
text: block.text
|
|
3722
|
-
});
|
|
3723
|
-
else if (block.type === "tool-call" && paired.has(block.id)) parts.push({
|
|
3724
|
-
type: "tool-call",
|
|
3725
|
-
toolCallId: wireIds.get(block.id) ?? block.id,
|
|
3726
|
-
toolName: block.name,
|
|
3727
|
-
input: recordOrEmpty(block.arguments)
|
|
3728
|
-
});
|
|
3729
|
-
if (parts.length > 0) out.push({
|
|
3730
|
-
role: "assistant",
|
|
3731
|
-
content: parts
|
|
3732
|
-
});
|
|
4837
|
+
const assistant = codec.encodeAssistant(message, paired, wireIds);
|
|
4838
|
+
if (assistant !== void 0) out.push(assistant);
|
|
3733
4839
|
continue;
|
|
3734
4840
|
}
|
|
3735
|
-
if (result)
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
type: "error-text",
|
|
3747
|
-
value: toolResultTextForWire(media)
|
|
3748
|
-
} : {
|
|
3749
|
-
type: "text",
|
|
3750
|
-
value: toolResultTextForWire(media)
|
|
3751
|
-
}
|
|
3752
|
-
}]
|
|
3753
|
-
});
|
|
3754
|
-
if (media.images.length > 0) {
|
|
3755
|
-
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
3756
|
-
const carried = [{
|
|
3757
|
-
type: "text",
|
|
3758
|
-
text: toolResultImageNote(media, wireToolCallId)
|
|
3759
|
-
}];
|
|
3760
|
-
for (const attachment of media.images) carried.push(await imageToCommandCode(attachment, readImage));
|
|
3761
|
-
pendingImages.push({
|
|
3762
|
-
role: "user",
|
|
3763
|
-
content: carried
|
|
3764
|
-
});
|
|
3765
|
-
}
|
|
4841
|
+
if (!result || !paired.has(result.toolCallId)) continue;
|
|
4842
|
+
const media = toolResultMedia(result);
|
|
4843
|
+
const wireId = wireIds.get(result.toolCallId) ?? result.toolCallId;
|
|
4844
|
+
out.push(await codec.encodeTool(result, media, wireId, toolNames.get(result.toolCallId) || "unknown", readImage));
|
|
4845
|
+
if (!codec.carriesToolResultMedia && media.images.length > 0) {
|
|
4846
|
+
const carried = [{
|
|
4847
|
+
type: "text",
|
|
4848
|
+
text: toolResultImageNote(media, wireId)
|
|
4849
|
+
}];
|
|
4850
|
+
for (const attachment of media.images) carried.push(await encodeImage(attachment));
|
|
4851
|
+
pendingImages.push(codec.encodeUser(carried));
|
|
3766
4852
|
}
|
|
3767
4853
|
}
|
|
3768
4854
|
flushPendingImages();
|
|
3769
4855
|
return out;
|
|
3770
4856
|
}
|
|
4857
|
+
const ccMessageCodec = {
|
|
4858
|
+
carriesToolResultMedia: false,
|
|
4859
|
+
encodeImage: imageToCommandCode,
|
|
4860
|
+
encodeUser: (parts) => ({
|
|
4861
|
+
role: "user",
|
|
4862
|
+
content: parts
|
|
4863
|
+
}),
|
|
4864
|
+
encodeAssistant: (message, paired, wireIds) => {
|
|
4865
|
+
const parts = [];
|
|
4866
|
+
for (const block of message.content) if (block.type === "text") parts.push({
|
|
4867
|
+
type: "text",
|
|
4868
|
+
text: block.text
|
|
4869
|
+
});
|
|
4870
|
+
else if (block.type === "reasoning") parts.push({
|
|
4871
|
+
type: "reasoning",
|
|
4872
|
+
text: block.text
|
|
4873
|
+
});
|
|
4874
|
+
else if (block.type === "tool-call" && paired.has(block.id)) parts.push({
|
|
4875
|
+
type: "tool-call",
|
|
4876
|
+
toolCallId: wireIds.get(block.id) ?? block.id,
|
|
4877
|
+
toolName: block.name,
|
|
4878
|
+
input: recordOrEmpty(block.arguments)
|
|
4879
|
+
});
|
|
4880
|
+
return parts.length > 0 ? {
|
|
4881
|
+
role: "assistant",
|
|
4882
|
+
content: parts
|
|
4883
|
+
} : void 0;
|
|
4884
|
+
},
|
|
4885
|
+
encodeTool: (result, media, wireId, toolName) => ({
|
|
4886
|
+
role: "tool",
|
|
4887
|
+
content: [{
|
|
4888
|
+
type: "tool-result",
|
|
4889
|
+
toolCallId: wireId,
|
|
4890
|
+
toolName,
|
|
4891
|
+
output: result.isError ? {
|
|
4892
|
+
type: "error-text",
|
|
4893
|
+
value: toolResultTextForWire(media)
|
|
4894
|
+
} : {
|
|
4895
|
+
type: "text",
|
|
4896
|
+
value: toolResultTextForWire(media)
|
|
4897
|
+
}
|
|
4898
|
+
}]
|
|
4899
|
+
})
|
|
4900
|
+
};
|
|
4901
|
+
async function messagesToCC(messages, readImage) {
|
|
4902
|
+
return convertMessages(messages, readImage, ccMessageCodec);
|
|
4903
|
+
}
|
|
3771
4904
|
/**
|
|
3772
4905
|
* Convert one image reference to the OpenAI Chat Completions wire format:
|
|
3773
4906
|
* `{ type: 'image_url', image_url: { url: 'data:...;base64,...' } }`.
|
|
3774
4907
|
*/
|
|
3775
4908
|
async function imageToOpenAI(ref, readImage) {
|
|
3776
|
-
const
|
|
4909
|
+
const version = await readImage(ref);
|
|
3777
4910
|
return {
|
|
3778
4911
|
type: "image_url",
|
|
3779
|
-
image_url: { url: `data:${
|
|
4912
|
+
image_url: { url: `data:${version.mediaType};base64,${Buffer.from(version.data).toString("base64")}` }
|
|
3780
4913
|
};
|
|
3781
4914
|
}
|
|
3782
4915
|
/**
|
|
@@ -3789,89 +4922,159 @@ async function imageToOpenAI(ref, readImage) {
|
|
|
3789
4922
|
* on assistant messages. Only tool calls with a paired tool result are
|
|
3790
4923
|
* replayed (same policy as the CLI path).
|
|
3791
4924
|
*/
|
|
4925
|
+
const openAiMessageCodec = {
|
|
4926
|
+
carriesToolResultMedia: false,
|
|
4927
|
+
encodeImage: imageToOpenAI,
|
|
4928
|
+
encodeUser: (parts) => {
|
|
4929
|
+
return !parts.some((part) => part.type === "image_url") && parts.length === 1 ? {
|
|
4930
|
+
role: "user",
|
|
4931
|
+
content: parts[0].text
|
|
4932
|
+
} : {
|
|
4933
|
+
role: "user",
|
|
4934
|
+
content: parts
|
|
4935
|
+
};
|
|
4936
|
+
},
|
|
4937
|
+
encodeAssistant: (message, paired, wireIds) => {
|
|
4938
|
+
const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
4939
|
+
const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
|
|
4940
|
+
const toolCalls = message.content.filter((block) => block.type === "tool-call" && paired.has(block.id)).map((block) => ({
|
|
4941
|
+
id: wireIds.get(block.id) ?? block.id,
|
|
4942
|
+
type: "function",
|
|
4943
|
+
function: {
|
|
4944
|
+
name: block.name,
|
|
4945
|
+
arguments: block.arguments
|
|
4946
|
+
}
|
|
4947
|
+
}));
|
|
4948
|
+
if (text === "" && reasoning === "" && toolCalls.length === 0) return void 0;
|
|
4949
|
+
const assistant = {
|
|
4950
|
+
role: "assistant",
|
|
4951
|
+
content: text === "" ? null : text
|
|
4952
|
+
};
|
|
4953
|
+
if (reasoning !== "") assistant.reasoning_content = reasoning;
|
|
4954
|
+
if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
|
|
4955
|
+
return assistant;
|
|
4956
|
+
},
|
|
4957
|
+
encodeTool: (_result, media, wireId) => ({
|
|
4958
|
+
role: "tool",
|
|
4959
|
+
tool_call_id: wireId,
|
|
4960
|
+
content: toolResultTextForWire(media)
|
|
4961
|
+
})
|
|
4962
|
+
};
|
|
3792
4963
|
async function messagesToOpenAI(messages, readImage) {
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
4964
|
+
return convertMessages(messages, readImage, openAiMessageCodec);
|
|
4965
|
+
}
|
|
4966
|
+
/**
|
|
4967
|
+
* Convert one image reference to the Messages wire shape. Unlike the other two
|
|
4968
|
+
* transports this one keeps the native media type rather than a data URL, and
|
|
4969
|
+
* it may be used INSIDE a tool result — `tool_result.content` accepts image
|
|
4970
|
+
* blocks, which is why this codec sets `carriesToolResultMedia`.
|
|
4971
|
+
*/
|
|
4972
|
+
async function imageToMessages(ref, readImage) {
|
|
4973
|
+
const version = await readImage(ref);
|
|
4974
|
+
return {
|
|
4975
|
+
type: "image",
|
|
4976
|
+
source: {
|
|
4977
|
+
type: "base64",
|
|
4978
|
+
media_type: version.mediaType,
|
|
4979
|
+
data: Buffer.from(version.data).toString("base64")
|
|
4980
|
+
}
|
|
3800
4981
|
};
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
4982
|
+
}
|
|
4983
|
+
/** A tool call's `arguments` is a raw JSON string in the harness, an object here. */
|
|
4984
|
+
function parseToolArguments(raw) {
|
|
4985
|
+
try {
|
|
4986
|
+
const parsed = JSON.parse(raw);
|
|
4987
|
+
return isRecord(parsed) ? parsed : {};
|
|
4988
|
+
} catch {
|
|
4989
|
+
return {};
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
/**
|
|
4993
|
+
* Convert harness messages to the Anthropic Messages request shape.
|
|
4994
|
+
*
|
|
4995
|
+
* Differences from the other two transports, all measured against the live
|
|
4996
|
+
* endpoint on 2026-09-29:
|
|
4997
|
+
*
|
|
4998
|
+
* - Content is always a block array; there is no string shorthand, so a
|
|
4999
|
+
* single-text user turn is still `{ role: 'user', content: [{type:'text'…}] }`.
|
|
5000
|
+
* - Tool results live in a `user` turn as `tool_result` blocks (not a
|
|
5001
|
+
* `role: 'tool'` message) and may carry images natively, so no image is
|
|
5002
|
+
* split into a separate user message here.
|
|
5003
|
+
* - A replayed `thinking` block is only valid WITH its `signature`: the
|
|
5004
|
+
* endpoint answers `thinking.signature: Field required` without it, and
|
|
5005
|
+
* `each thinking block must contain thinking` when the text is empty and
|
|
5006
|
+
* the signature is blank. A reasoning block with no recorded signature is
|
|
5007
|
+
* therefore dropped rather than sent — replaying it would fail the request,
|
|
5008
|
+
* while omitting it is accepted (verified 200 both ways).
|
|
5009
|
+
* - The conversation must end with a user turn; the endpoint refuses an
|
|
5010
|
+
* assistant prefill (`This model does not support assistant message
|
|
5011
|
+
* prefill`), which `convertMessages` already guarantees for a tool loop.
|
|
5012
|
+
*/
|
|
5013
|
+
const messagesMessageCodec = {
|
|
5014
|
+
carriesToolResultMedia: true,
|
|
5015
|
+
encodeImage: imageToMessages,
|
|
5016
|
+
encodeUser: (parts) => ({
|
|
5017
|
+
role: "user",
|
|
5018
|
+
content: parts
|
|
5019
|
+
}),
|
|
5020
|
+
encodeAssistant: (message, paired, wireIds) => {
|
|
5021
|
+
const parts = [];
|
|
5022
|
+
let position = 0;
|
|
5023
|
+
for (const block of message.content) {
|
|
5024
|
+
if (block.type === "text") {
|
|
5025
|
+
parts.push({
|
|
3809
5026
|
type: "text",
|
|
3810
5027
|
text: block.text
|
|
3811
5028
|
});
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
parts.push(await imageToOpenAI(block.attachment, readImage));
|
|
3815
|
-
}
|
|
5029
|
+
position++;
|
|
5030
|
+
continue;
|
|
3816
5031
|
}
|
|
3817
|
-
if (
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
const reasoning = message.content.filter((block) => block.type === "reasoning").map((block) => block.text).join("");
|
|
3832
|
-
const toolCalls = message.content.filter((block) => block.type === "tool-call" && paired.has(block.id)).map((block) => ({
|
|
3833
|
-
id: wireIds.get(block.id) ?? block.id,
|
|
3834
|
-
type: "function",
|
|
3835
|
-
function: {
|
|
5032
|
+
if (block.type === "reasoning") {
|
|
5033
|
+
const signature = messagesSignatureAt(message, position);
|
|
5034
|
+
if (signature !== void 0) parts.push({
|
|
5035
|
+
type: "thinking",
|
|
5036
|
+
thinking: block.text,
|
|
5037
|
+
signature
|
|
5038
|
+
});
|
|
5039
|
+
position++;
|
|
5040
|
+
continue;
|
|
5041
|
+
}
|
|
5042
|
+
if (block.type === "tool-call" && paired.has(block.id)) {
|
|
5043
|
+
parts.push({
|
|
5044
|
+
type: "tool_use",
|
|
5045
|
+
id: wireIds.get(block.id) ?? block.id,
|
|
3836
5046
|
name: block.name,
|
|
3837
|
-
|
|
3838
|
-
}
|
|
3839
|
-
}));
|
|
3840
|
-
if (text === "" && reasoning === "" && toolCalls.length === 0) continue;
|
|
3841
|
-
const assistant = {
|
|
3842
|
-
role: "assistant",
|
|
3843
|
-
content: text === "" ? null : text
|
|
3844
|
-
};
|
|
3845
|
-
if (reasoning !== "") assistant.reasoning_content = reasoning;
|
|
3846
|
-
if (toolCalls.length > 0) assistant.tool_calls = toolCalls;
|
|
3847
|
-
out.push(assistant);
|
|
3848
|
-
continue;
|
|
3849
|
-
}
|
|
3850
|
-
if (result) {
|
|
3851
|
-
if (!paired.has(result.toolCallId)) continue;
|
|
3852
|
-
const media = toolResultMedia(result);
|
|
3853
|
-
const wireToolCallId = wireIds.get(result.toolCallId) ?? result.toolCallId;
|
|
3854
|
-
out.push({
|
|
3855
|
-
role: "tool",
|
|
3856
|
-
tool_call_id: wireToolCallId,
|
|
3857
|
-
content: toolResultTextForWire(media)
|
|
3858
|
-
});
|
|
3859
|
-
if (media.images.length > 0) {
|
|
3860
|
-
if (!readImage) throw new LlmError("Image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
3861
|
-
const carried = [{
|
|
3862
|
-
type: "text",
|
|
3863
|
-
text: toolResultImageNote(media, wireToolCallId)
|
|
3864
|
-
}];
|
|
3865
|
-
for (const attachment of media.images) carried.push(await imageToOpenAI(attachment, readImage));
|
|
3866
|
-
pendingImages.push({
|
|
3867
|
-
role: "user",
|
|
3868
|
-
content: carried
|
|
5047
|
+
input: parseToolArguments(block.arguments)
|
|
3869
5048
|
});
|
|
5049
|
+
position++;
|
|
3870
5050
|
}
|
|
3871
5051
|
}
|
|
5052
|
+
return parts.length > 0 ? {
|
|
5053
|
+
role: "assistant",
|
|
5054
|
+
content: parts
|
|
5055
|
+
} : void 0;
|
|
5056
|
+
},
|
|
5057
|
+
encodeTool: async (result, media, wireId, _toolName, readImage) => {
|
|
5058
|
+
const content = [];
|
|
5059
|
+
if (readImage) for (const attachment of media.images) content.push(await imageToMessages(attachment, readImage));
|
|
5060
|
+
content.push({
|
|
5061
|
+
type: "text",
|
|
5062
|
+
text: toolResultTextForWire(media)
|
|
5063
|
+
});
|
|
5064
|
+
const toolResult = {
|
|
5065
|
+
type: "tool_result",
|
|
5066
|
+
tool_use_id: wireId,
|
|
5067
|
+
content
|
|
5068
|
+
};
|
|
5069
|
+
if (result.isError) toolResult.is_error = true;
|
|
5070
|
+
return {
|
|
5071
|
+
role: "user",
|
|
5072
|
+
content: [toolResult]
|
|
5073
|
+
};
|
|
3872
5074
|
}
|
|
3873
|
-
|
|
3874
|
-
|
|
5075
|
+
};
|
|
5076
|
+
async function messagesToMessages(messages, readImage) {
|
|
5077
|
+
return convertMessages(messages, readImage, messagesMessageCodec);
|
|
3875
5078
|
}
|
|
3876
5079
|
/** Account endpoints fetched by one `getUsage()` run (see the classification there). */
|
|
3877
5080
|
const USAGE_ENDPOINT_COUNT = 4;
|
|
@@ -4000,6 +5203,50 @@ function cliSystem(systemText) {
|
|
|
4000
5203
|
cache_control: { type: "ephemeral" }
|
|
4001
5204
|
}];
|
|
4002
5205
|
}
|
|
5206
|
+
/**
|
|
5207
|
+
* The Anthropic ephemeral cache marker, shared by every breakpoint this route
|
|
5208
|
+
* sets. One object is reused across positions because it is only ever
|
|
5209
|
+
* serialized, never mutated.
|
|
5210
|
+
*
|
|
5211
|
+
* No `ttl` is sent: the default 5-minute entry covers a continuously advancing
|
|
5212
|
+
* session, and `ttl: "1h"` is a separate billable tier whose read/write prices
|
|
5213
|
+
* are identical — it would only lengthen survival, not reduce cost.
|
|
5214
|
+
*/
|
|
5215
|
+
const EPHEMERAL_CACHE_CONTROL = { type: "ephemeral" };
|
|
5216
|
+
/**
|
|
5217
|
+
* Content-block types that may carry the rolling breakpoint. Matches what the
|
|
5218
|
+
* official Command Code provider for pi marks through
|
|
5219
|
+
* `@earendil-works/pi-ai`'s `anthropic-messages` transport (its own list also
|
|
5220
|
+
* holds pi-internal `tool_addition` / `tool_removal`, which this route never
|
|
5221
|
+
* emits). Assistant-side blocks are deliberately excluded: a breakpoint on a
|
|
5222
|
+
* `thinking` or `tool_use` block caches a prefix the next turn invalidates.
|
|
5223
|
+
*/
|
|
5224
|
+
const CACHEABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
|
|
5225
|
+
"text",
|
|
5226
|
+
"image",
|
|
5227
|
+
"tool_result"
|
|
5228
|
+
]);
|
|
5229
|
+
/**
|
|
5230
|
+
* Mark the last user turn as the rolling cache breakpoint.
|
|
5231
|
+
*
|
|
5232
|
+
* Anthropic caching is explicit — a prefix is stored only when a
|
|
5233
|
+
* `cache_control` marker names it — so an unmarked route re-prefills the whole
|
|
5234
|
+
* replayed history at the full input price on every turn. Measured cost of that
|
|
5235
|
+
* omission on a 10-turn session: $0.5648, all of it at the undiscounted
|
|
5236
|
+
* $2/M input rate, against $0.1869 with the three breakpoints this route sets.
|
|
5237
|
+
*
|
|
5238
|
+
* The final user message is the rolling boundary: DSH resends the whole
|
|
5239
|
+
* history each turn, so a marker here grows with the conversation, whereas a
|
|
5240
|
+
* fixed `system`-only marker would leave every later turn's history
|
|
5241
|
+
* uncached. {@link messagesMessageCodec} renders both user input and tool
|
|
5242
|
+
* results as `role: 'user'`, so this covers a tool loop's trailing turn too.
|
|
5243
|
+
*/
|
|
5244
|
+
function markLastUserCacheControl(messages) {
|
|
5245
|
+
const last = messages[messages.length - 1];
|
|
5246
|
+
if (!isRecord(last) || last.role !== "user" || !Array.isArray(last.content)) return;
|
|
5247
|
+
const lastBlock = last.content[last.content.length - 1];
|
|
5248
|
+
if (isRecord(lastBlock) && typeof lastBlock.type === "string" && CACHEABLE_BLOCK_TYPES.has(lastBlock.type)) lastBlock.cache_control = EPHEMERAL_CACHE_CONTROL;
|
|
5249
|
+
}
|
|
4003
5250
|
/** Build the legacy CLI (`/alpha/generate`) request body for one call. */
|
|
4004
5251
|
async function buildCliBody(options, connection, facts) {
|
|
4005
5252
|
return {
|
|
@@ -4060,6 +5307,108 @@ async function buildOpenAIBody(options, facts) {
|
|
|
4060
5307
|
};
|
|
4061
5308
|
}
|
|
4062
5309
|
/**
|
|
5310
|
+
* Build the Anthropic Messages request body for one call.
|
|
5311
|
+
*
|
|
5312
|
+
* Measured deviations from DeepSeek's own Messages API (the shape
|
|
5313
|
+
* `dsh-llm-deepseek` implements), all confirmed on 2026-09-29:
|
|
5314
|
+
*
|
|
5315
|
+
* - `thinking` accepts ONLY `{ type: 'adaptive' }` here. `enabled` and
|
|
5316
|
+
* `disabled` are refused with `"thinking.type.enabled" is not supported for
|
|
5317
|
+
* this model. Use "thinking.type.adaptive" and "output_config.effort" to
|
|
5318
|
+
* control thinking behavior.`, and a `between_tools` value fails validation
|
|
5319
|
+
* outright. The model reasons on its own; this switch only admits it.
|
|
5320
|
+
* - Reasoning strength travels in `output_config.effort`, whose accepted
|
|
5321
|
+
* values are `low | medium | high | xhigh | max` — there is no `none`, and
|
|
5322
|
+
* `xhigh` is absent from DeepSeek's own type.
|
|
5323
|
+
* - `stop_sequences` is a real field here, but this adapter still rejects
|
|
5324
|
+
* `GenerateOptions.stop` in `stream()` because Command Code documents no
|
|
5325
|
+
* equivalent for the other two transports.
|
|
5326
|
+
* - **`temperature` is never sent.** Adaptive thinking constrains it to 1:
|
|
5327
|
+
* `` `temperature` may only be set to 1 when thinking is enabled or in
|
|
5328
|
+
* adaptive mode `` (measured 2026-09-29 — every request carrying the other
|
|
5329
|
+
* transports' 0.3 default was refused with HTTP 400). Omitting the field
|
|
5330
|
+
* is the only way to honour both the thinking switch and a caller that
|
|
5331
|
+
* asked for a temperature, so Claude loses sampling control on this route
|
|
5332
|
+
* because the endpoint, not this adapter, removed it.
|
|
5333
|
+
* - `system` travels as a one-block ARRAY carrying `cache_control`, not as the
|
|
5334
|
+
* bare string the other two transports send. Anthropic's caching is opt-in
|
|
5335
|
+
* per block, and this route is the only Claude path, so the marker is what
|
|
5336
|
+
* makes a session's replayed history bill at the cache-read rate instead of
|
|
5337
|
+
* the full input rate. See {@link EPHEMERAL_CACHE_CONTROL}.
|
|
5338
|
+
*/
|
|
5339
|
+
async function buildMessagesBody(options, facts) {
|
|
5340
|
+
const body = {
|
|
5341
|
+
model: options.model,
|
|
5342
|
+
max_tokens: facts.maxTokens,
|
|
5343
|
+
stream: true,
|
|
5344
|
+
messages: await messagesToMessages(options.messages, facts.readImage)
|
|
5345
|
+
};
|
|
5346
|
+
if (facts.selectableEffort) body.thinking = { type: "adaptive" };
|
|
5347
|
+
if (facts.systemText) body.system = [{
|
|
5348
|
+
type: "text",
|
|
5349
|
+
text: facts.systemText,
|
|
5350
|
+
cache_control: EPHEMERAL_CACHE_CONTROL
|
|
5351
|
+
}];
|
|
5352
|
+
if (facts.reasoningEffort) body.output_config = { effort: facts.reasoningEffort };
|
|
5353
|
+
const tools = (options.tools ?? []).map((tool) => ({
|
|
5354
|
+
name: tool.name,
|
|
5355
|
+
description: tool.description,
|
|
5356
|
+
input_schema: toolParametersSchema(tool.parameters)
|
|
5357
|
+
}));
|
|
5358
|
+
if (tools.length > 0) {
|
|
5359
|
+
tools[tools.length - 1].cache_control = EPHEMERAL_CACHE_CONTROL;
|
|
5360
|
+
body.tools = tools;
|
|
5361
|
+
}
|
|
5362
|
+
markLastUserCacheControl(body.messages);
|
|
5363
|
+
return body;
|
|
5364
|
+
}
|
|
5365
|
+
/**
|
|
5366
|
+
* Estimate the text the selected transport actually sends. JSON framing and
|
|
5367
|
+
* punctuation each get a token; ordinary ASCII words get one, with an extra
|
|
5368
|
+
* allowance for long identifiers. UTF-8 bytes / 4 tracks the measured CJK
|
|
5369
|
+
* case better than the engine's UTF-16 characters / 4. This is a heuristic,
|
|
5370
|
+
* never proof that a prompt cannot fit: a valid long English prompt can still
|
|
5371
|
+
* be larger under this estimate than under the provider's tokenizer.
|
|
5372
|
+
*/
|
|
5373
|
+
function estimateWireInputTokens(protocol, body, messages, model) {
|
|
5374
|
+
const params = protocol === "cli" ? recordOrEmpty(body.params) : body;
|
|
5375
|
+
const wire = JSON.stringify(params, (_key, value) => typeof value === "string" && /^data:[^,]*;base64,/.test(value) ? "[image]" : value);
|
|
5376
|
+
let tokens = 0;
|
|
5377
|
+
for (const match of wire.matchAll(/[A-Za-z0-9_]+|[^\x00-\x7f]+|[^\s]/gu)) {
|
|
5378
|
+
const part = match[0];
|
|
5379
|
+
if (/^[A-Za-z0-9_]+$/.test(part)) tokens += 1 + Math.ceil(Math.max(0, part.length - 12) / 4);
|
|
5380
|
+
else if (part.charCodeAt(0) > 127) tokens += Math.ceil(Buffer.byteLength(part) / 4);
|
|
5381
|
+
else tokens += 1;
|
|
5382
|
+
}
|
|
5383
|
+
for (const message of messages) for (const block of message.content) if (block.type === "image") tokens += priceRequestImage(block, model, true).visualTokens;
|
|
5384
|
+
return tokens;
|
|
5385
|
+
}
|
|
5386
|
+
function requestContextBudget(protocol, body, messages, model, contextWindow, requestedMaxTokens) {
|
|
5387
|
+
if (contextWindow === void 0) return {
|
|
5388
|
+
maxTokens: requestedMaxTokens,
|
|
5389
|
+
sizePressure: false
|
|
5390
|
+
};
|
|
5391
|
+
const estimatedInput = estimateWireInputTokens(protocol, body, messages, model);
|
|
5392
|
+
const headroom = Math.min(16384, Math.max(2048, Math.ceil(contextWindow * .02)));
|
|
5393
|
+
const available = Math.floor(contextWindow - estimatedInput - headroom);
|
|
5394
|
+
const floor = Math.min(requestedMaxTokens, MIN_CONTEXT_OUTPUT_TOKENS, Math.max(1, Math.floor(contextWindow / 8)));
|
|
5395
|
+
return {
|
|
5396
|
+
maxTokens: Math.min(requestedMaxTokens, Math.max(floor, available)),
|
|
5397
|
+
sizePressure: estimatedInput >= contextWindow / 4 && estimatedInput + requestedMaxTokens + headroom >= contextWindow
|
|
5398
|
+
};
|
|
5399
|
+
}
|
|
5400
|
+
/**
|
|
5401
|
+
* One connect attempt's inputs: everything `connectGenerate` needs beyond
|
|
5402
|
+
* the per-attempt key and protocol. Passed explicitly (not closed over) so
|
|
5403
|
+
* the rotation loop's mutation of `protocol`/`body` stays visible at the
|
|
5404
|
+
* call site.
|
|
5405
|
+
*/
|
|
5406
|
+
/** The generate endpoint one protocol posts to. */
|
|
5407
|
+
function protocolEndpoint(protocol, apiBase) {
|
|
5408
|
+
if (protocol === "cli") return `${apiBase}/alpha/generate`;
|
|
5409
|
+
return protocol === "messages" ? `${apiBase}/provider/v1/messages` : `${apiBase}/provider/v1/chat/completions`;
|
|
5410
|
+
}
|
|
5411
|
+
/**
|
|
4063
5412
|
* One pre-stream connect attempt: POST the body and wait for response
|
|
4064
5413
|
* headers only (`requestTimeoutMs` must never bound the body stream). Returns
|
|
4065
5414
|
* the live response plus its cleanup, or the rejection facts for the rotation
|
|
@@ -4067,11 +5416,11 @@ async function buildOpenAIBody(options, facts) {
|
|
|
4067
5416
|
* on success the caller-abort listener outlives the connect phase (it aborts a
|
|
4068
5417
|
* stalled body read), so the streaming tail calls cleanup.
|
|
4069
5418
|
*/
|
|
4070
|
-
async function connectGenerate(deps, key, protocol,
|
|
5419
|
+
async function connectGenerate(deps, key, protocol, serializedBody) {
|
|
4071
5420
|
const { options, connection, fetchImpl } = deps;
|
|
4072
5421
|
const connectAbort = new AbortController();
|
|
4073
5422
|
let connectTimedOut = false;
|
|
4074
|
-
const endpoint = protocol
|
|
5423
|
+
const endpoint = protocolEndpoint(protocol, connection.apiBase);
|
|
4075
5424
|
const connectTimer = setTimeout(() => {
|
|
4076
5425
|
connectTimedOut = true;
|
|
4077
5426
|
connectAbort.abort(new DOMException(`Command Code API request to ${endpoint} did not respond within ${connection.requestTimeoutMs}ms`, "TimeoutError"));
|
|
@@ -4089,38 +5438,46 @@ async function connectGenerate(deps, key, protocol, body) {
|
|
|
4089
5438
|
};
|
|
4090
5439
|
let response;
|
|
4091
5440
|
const zdr = connection.zdr === true;
|
|
4092
|
-
const
|
|
5441
|
+
const sharedHeaders = {
|
|
4093
5442
|
"Content-Type": "application/json",
|
|
4094
5443
|
...IDENTITY_ENCODING_HEADER,
|
|
4095
5444
|
Authorization: `Bearer ${key}`,
|
|
5445
|
+
...zdr ? { "x-cmd-zdr": "1" } : {},
|
|
5446
|
+
...attributionHeaders()
|
|
5447
|
+
};
|
|
5448
|
+
const headers = protocol === "cli" ? {
|
|
5449
|
+
...sharedHeaders,
|
|
4096
5450
|
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
4097
5451
|
"x-cli-environment": "production",
|
|
4098
5452
|
"x-project-slug": projectSlugFromPath(connection.workingDir),
|
|
4099
5453
|
"x-taste-learning": "true",
|
|
4100
|
-
"x-co-flag": "false"
|
|
4101
|
-
...zdr ? { "x-cmd-zdr": "1" } : {},
|
|
4102
|
-
...attributionHeaders()
|
|
5454
|
+
"x-co-flag": "false"
|
|
4103
5455
|
} : {
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
Authorization: `Bearer ${key}`,
|
|
4107
|
-
Accept: "text/event-stream",
|
|
4108
|
-
...zdr ? { "x-cmd-zdr": "1" } : {},
|
|
4109
|
-
...attributionHeaders()
|
|
5456
|
+
...sharedHeaders,
|
|
5457
|
+
Accept: "text/event-stream"
|
|
4110
5458
|
};
|
|
5459
|
+
const finishHeaders = deps.timing.attempt(protocol, Buffer.byteLength(serializedBody));
|
|
4111
5460
|
try {
|
|
4112
5461
|
response = await fetchImpl(endpoint, {
|
|
4113
5462
|
method: "POST",
|
|
4114
5463
|
headers,
|
|
4115
|
-
body:
|
|
5464
|
+
body: serializedBody,
|
|
4116
5465
|
signal: connectAbort.signal
|
|
4117
5466
|
});
|
|
5467
|
+
finishHeaders(response.status);
|
|
4118
5468
|
clearTimeout(connectTimer);
|
|
4119
5469
|
} catch (error) {
|
|
5470
|
+
finishHeaders();
|
|
4120
5471
|
cleanup();
|
|
4121
5472
|
if (options.signal?.aborted) throw error;
|
|
4122
|
-
if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw
|
|
4123
|
-
|
|
5473
|
+
if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw headersTimeoutError({
|
|
5474
|
+
protocol,
|
|
5475
|
+
endpoint,
|
|
5476
|
+
body: serializedBody,
|
|
5477
|
+
timeoutMs: connection.requestTimeoutMs,
|
|
5478
|
+
cause: error
|
|
5479
|
+
});
|
|
5480
|
+
throw bilingual("TRANSPORT", `Command Code API request to ${endpoint} failed: ${errorChain(error)}`, "Command Code API 请求连接失败——通常是网络或代理问题,请检查网络或代理设置后重试", { cause: error });
|
|
4124
5481
|
}
|
|
4125
5482
|
deps.onResponse(response);
|
|
4126
5483
|
if (!response.ok) {
|
|
@@ -4152,7 +5509,9 @@ function createBlockAssembler() {
|
|
|
4152
5509
|
sawContent: false,
|
|
4153
5510
|
finishReason: void 0,
|
|
4154
5511
|
cliCacheWriteTokens: void 0,
|
|
4155
|
-
openAiToolCalls: []
|
|
5512
|
+
openAiToolCalls: [],
|
|
5513
|
+
messagesBlocks: /* @__PURE__ */ new Map(),
|
|
5514
|
+
replayBlocks: []
|
|
4156
5515
|
};
|
|
4157
5516
|
}
|
|
4158
5517
|
function* closeText(asm) {
|
|
@@ -4213,9 +5572,10 @@ function* emitOpenAiToolCalls(asm) {
|
|
|
4213
5572
|
}
|
|
4214
5573
|
asm.openAiToolCalls.length = 0;
|
|
4215
5574
|
}
|
|
4216
|
-
var CommandCodeAdapter = class extends LlmAdapter {
|
|
5575
|
+
var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
4217
5576
|
deps;
|
|
4218
5577
|
catalog = [];
|
|
5578
|
+
catalogState;
|
|
4219
5579
|
fetchImpl;
|
|
4220
5580
|
resolveAttachments;
|
|
4221
5581
|
/**
|
|
@@ -4226,6 +5586,25 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4226
5586
|
billingAccess = /* @__PURE__ */ new Map();
|
|
4227
5587
|
billingAccessInflight = /* @__PURE__ */ new Map();
|
|
4228
5588
|
protocolCache = /* @__PURE__ */ new Map();
|
|
5589
|
+
/** Namespace account-derived facts by gateway without exposing the key. */
|
|
5590
|
+
accountCacheKey(apiBase, apiKey) {
|
|
5591
|
+
return JSON.stringify([apiBase, apiKey]);
|
|
5592
|
+
}
|
|
5593
|
+
/**
|
|
5594
|
+
* Consecutive header timeouts for the last request shape, counted ACROSS
|
|
5595
|
+
* `stream()` calls: dsh-llm-retry re-invokes this same instance, so this is
|
|
5596
|
+
* the only place a streak can live. Keyed by `sessionId` (not by `agent`:
|
|
5597
|
+
* `GenerateOptions` carries no agent reference, only `sessionId`) so
|
|
5598
|
+
* concurrent sessions on this one shared adapter instance cannot clear or
|
|
5599
|
+
* inflate each other's count. A call with no `sessionId` (a one-shot,
|
|
5600
|
+
* session-less caller) falls back to {@link GLOBAL_HEADER_TIMEOUT_KEY},
|
|
5601
|
+
* matching the old single-slot behavior for exactly that case. Bounded by
|
|
5602
|
+
* {@link MAX_HEADER_TIMEOUT_STREAK_ENTRIES} with FIFO eviction (`Map`
|
|
5603
|
+
* preserves insertion order) since the key space is per-session rather than
|
|
5604
|
+
* per-account/model, so it is not naturally small like `protocolCache`.
|
|
5605
|
+
*/
|
|
5606
|
+
headerTimeoutStreaks = /* @__PURE__ */ new Map();
|
|
5607
|
+
static MAX_HEADER_TIMEOUT_STREAK_ENTRIES = 200;
|
|
4229
5608
|
constructor(deps) {
|
|
4230
5609
|
super();
|
|
4231
5610
|
this.deps = deps;
|
|
@@ -4234,6 +5613,38 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4234
5613
|
this.surfaceOffload = deps.imageOffload ?? ENGINE_IMAGE_POLICY;
|
|
4235
5614
|
}
|
|
4236
5615
|
/**
|
|
5616
|
+
* Grade one failed attempt: a headers timeout that repeats on an unchanged
|
|
5617
|
+
* payload eventually leaves the retryable whitelist.
|
|
5618
|
+
*
|
|
5619
|
+
* `providerRetryPolicy()` cannot express this itself — dsh-llm's
|
|
5620
|
+
* `NormalRetryPolicyConfig` carries one `maxRetries` for every listed code.
|
|
5621
|
+
* Escalating here stops repeated identical header timeouts with a specific
|
|
5622
|
+
* diagnosis; the agent listener separately caps ordinary transient errors.
|
|
5623
|
+
*
|
|
5624
|
+
* @returns the error to surface: unchanged for any other failure, the
|
|
5625
|
+
* original timeout below the threshold, and a terminal one at it.
|
|
5626
|
+
*/
|
|
5627
|
+
gradeHeaderTimeout(error, protocol, body, timeoutMs, sessionId) {
|
|
5628
|
+
if (!(error instanceof LlmError && error.code === "TIMEOUT")) return error;
|
|
5629
|
+
const key = sessionId ?? GLOBAL_HEADER_TIMEOUT_KEY;
|
|
5630
|
+
const fingerprint = `${protocol}:${fingerprintValue(body).sha256}`;
|
|
5631
|
+
const previous = this.headerTimeoutStreaks.get(key);
|
|
5632
|
+
const count = previous?.fingerprint === fingerprint ? previous.count + 1 : 1;
|
|
5633
|
+
if (previous === void 0) {
|
|
5634
|
+
if (this.headerTimeoutStreaks.size >= CommandCodeAdapter.MAX_HEADER_TIMEOUT_STREAK_ENTRIES) {
|
|
5635
|
+
const oldest = this.headerTimeoutStreaks.keys().next().value;
|
|
5636
|
+
if (oldest !== void 0) this.headerTimeoutStreaks.delete(oldest);
|
|
5637
|
+
}
|
|
5638
|
+
} else this.headerTimeoutStreaks.delete(key);
|
|
5639
|
+
this.headerTimeoutStreaks.set(key, {
|
|
5640
|
+
fingerprint,
|
|
5641
|
+
count
|
|
5642
|
+
});
|
|
5643
|
+
if (count < MAX_CONSECUTIVE_HEADER_TIMEOUTS) return error;
|
|
5644
|
+
this.headerTimeoutStreaks.delete(key);
|
|
5645
|
+
return bilingual("PROVIDER_HTTP_ERROR", `Command Code API stopped retrying: the gateway returned no response headers on ${count} consecutive attempts at the same request, each after the full ${timeoutMs}ms budget, so this is not a network blip — it is provider queueing, a Go-plan capacity limit, or a network/proxy path problem. Resending the identical payload again is unlikely to help; wait for the provider to recover, or switch model or account.`, `Command Code API 已停止重试:同一请求连续 ${count} 次在 ${timeoutMs} 毫秒内都没有收到任何响应头,这不是网络抖动,而是服务商排队、Go 套餐容量限制或网络/代理链路问题。继续原样重发同一个请求通常无济于事——可以等服务商恢复,或换模型/换账号。`);
|
|
5646
|
+
}
|
|
5647
|
+
/**
|
|
4237
5648
|
* Display metadata for the picker's provider group header. The base class
|
|
4238
5649
|
* returns the raw route id (`commandcode`, all lowercase) as the name, which
|
|
4239
5650
|
* is what the model selector shows as this group's sticky title; return the
|
|
@@ -4247,12 +5658,11 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4247
5658
|
};
|
|
4248
5659
|
}
|
|
4249
5660
|
/**
|
|
4250
|
-
*
|
|
4251
|
-
*
|
|
4252
|
-
*
|
|
4253
|
-
* `
|
|
4254
|
-
* and
|
|
4255
|
-
* recovers in-session instead of failing after two tries. Permanent failures
|
|
5661
|
+
* The official executor owns backoff and provider reset waits. Its high
|
|
5662
|
+
* route-wide cap lets a confirmed `RATE_LIMIT` window recover in-session;
|
|
5663
|
+
* our request-error listener limits `SERVER`/`TIMEOUT`/`EMPTY_RESPONSE`/
|
|
5664
|
+
* `THROTTLED` to three retries and `TRANSPORT` to its separate budget.
|
|
5665
|
+
* Waits double from 500 ms and cap at 15 minutes (±10% jitter). Permanent failures
|
|
4256
5666
|
* (an invalid key's `INVALID_CREDENTIAL`, `UNSUPPORTED_CONTENT`, plan
|
|
4257
5667
|
* rejections) are absent from the whitelist and surface immediately instead
|
|
4258
5668
|
* of looping. Waits the pool/adapter attach as `providerRetryAfterMs` are
|
|
@@ -4270,6 +5680,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4270
5680
|
retryableCodes: [
|
|
4271
5681
|
"EMPTY_RESPONSE",
|
|
4272
5682
|
"RATE_LIMIT",
|
|
5683
|
+
THROTTLED_CODE,
|
|
4273
5684
|
"SERVER",
|
|
4274
5685
|
"TIMEOUT",
|
|
4275
5686
|
"TRANSPORT"
|
|
@@ -4296,26 +5707,61 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4296
5707
|
const imageCapable = KNOWN_IMAGE_MODELS.has(model);
|
|
4297
5708
|
return { priceImages: (images) => images.map((image) => priceRequestImage(image, model, imageCapable)) };
|
|
4298
5709
|
}
|
|
4299
|
-
/**
|
|
5710
|
+
/** 地址或缓存文件变化时丢弃旧目录,防止旧网关的路由声明影响新请求。 */
|
|
5711
|
+
currentCatalogState(connection = this.deps.options()) {
|
|
5712
|
+
const { apiBase, modelsCachePath } = connection;
|
|
5713
|
+
const source = JSON.stringify([apiBase, modelsCachePath]);
|
|
5714
|
+
if (this.catalogState?.source !== source) {
|
|
5715
|
+
this.catalog = [];
|
|
5716
|
+
this.catalogState = {
|
|
5717
|
+
source,
|
|
5718
|
+
expiresAt: 0
|
|
5719
|
+
};
|
|
5720
|
+
}
|
|
5721
|
+
return this.catalogState;
|
|
5722
|
+
}
|
|
5723
|
+
/** 共享一次刷新;调用者取消只结束自己的等待,不取消其他调用者的请求。 */
|
|
4300
5724
|
async loadCatalog(signal) {
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
5725
|
+
signal?.throwIfAborted();
|
|
5726
|
+
const connection = this.deps.options();
|
|
5727
|
+
const state = this.currentCatalogState(connection);
|
|
5728
|
+
if (Date.now() < state.expiresAt) return this.catalog;
|
|
5729
|
+
const { apiBase, modelsCachePath } = connection;
|
|
5730
|
+
if (!state.inflight) {
|
|
5731
|
+
const fallback = this.catalog;
|
|
5732
|
+
state.inflight = (async () => {
|
|
5733
|
+
let models;
|
|
5734
|
+
let ttl = MODEL_CATALOG_TTL_MS;
|
|
5735
|
+
try {
|
|
5736
|
+
const { status, record } = await this.fetchJson(`${apiBase}/provider/v1/models`, {
|
|
5737
|
+
accept: "application/json",
|
|
5738
|
+
...IDENTITY_ENCODING_HEADER,
|
|
5739
|
+
...attributionHeaders()
|
|
5740
|
+
}, MODELS_TIMEOUT_MS);
|
|
5741
|
+
if (status < 200 || status >= 300) throw new Error(`models endpoint returned ${status}`);
|
|
5742
|
+
models = parseCatalogResponse(record, apiBase);
|
|
5743
|
+
if (this.catalogState === state) await writeModelsCache(modelsCachePath, apiBase, models, () => this.catalogState === state).catch(() => void 0);
|
|
5744
|
+
} catch {
|
|
5745
|
+
ttl = MODEL_CATALOG_RETRY_MS;
|
|
5746
|
+
models = await readModelsCache(modelsCachePath, apiBase).catch(() => fallback);
|
|
5747
|
+
}
|
|
5748
|
+
if (this.catalogState === state) {
|
|
5749
|
+
this.catalog = models;
|
|
5750
|
+
state.expiresAt = Date.now() + ttl;
|
|
5751
|
+
}
|
|
5752
|
+
return models;
|
|
5753
|
+
})().finally(() => {
|
|
5754
|
+
delete state.inflight;
|
|
4310
5755
|
});
|
|
4311
|
-
if (!response.ok) throw new Error(`models endpoint returned ${response.status}`);
|
|
4312
|
-
this.catalog = parseCatalogResponse(await response.json());
|
|
4313
|
-
await writeModelsCache(modelsCachePath, this.catalog).catch(() => void 0);
|
|
4314
|
-
} catch (error) {
|
|
4315
|
-
if (signal?.aborted) throw error;
|
|
4316
|
-
this.catalog = await readModelsCache(modelsCachePath).catch(() => this.catalog);
|
|
4317
5756
|
}
|
|
4318
|
-
return
|
|
5757
|
+
if (!signal) return state.inflight;
|
|
5758
|
+
const inflight = state.inflight;
|
|
5759
|
+
return new Promise((resolve, reject) => {
|
|
5760
|
+
const abort = () => reject(signal.reason);
|
|
5761
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
5762
|
+
inflight.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
5763
|
+
if (signal.aborted) abort();
|
|
5764
|
+
});
|
|
4319
5765
|
}
|
|
4320
5766
|
async listModels(provider, opts) {
|
|
4321
5767
|
const catalog = await this.loadCatalog();
|
|
@@ -4340,7 +5786,33 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4340
5786
|
return allow === void 0 || allow.has(model.id);
|
|
4341
5787
|
}).map(toInfo).sort(compareByPlan);
|
|
4342
5788
|
}
|
|
5789
|
+
/**
|
|
5790
|
+
* The per-model allowance bracket this account POOL falls into, or undefined
|
|
5791
|
+
* when there is nothing honest to show.
|
|
5792
|
+
*
|
|
5793
|
+
* The pricing page publishes a per-model monthly allowance for GOAT and Pro
|
|
5794
|
+
* only — its own words are "the boost is a per-model allowance, so it lives on
|
|
5795
|
+
* the plans that have them" — so a pool whose highest plan is Go, Provider,
|
|
5796
|
+
* Max or Ultra gets undefined rather than a neighbouring tier's figure. Taking
|
|
5797
|
+
* the HIGHEST tier matches the question the picker already answers for a pool
|
|
5798
|
+
* (see `modelVisibleForAnyAccount`): what the user can reach, not what the one
|
|
5799
|
+
* account serving this request happens to be. The fail-open shape is the same
|
|
5800
|
+
* too — an unreachable billing endpoint yields undefined, which hides the
|
|
5801
|
+
* allowance instead of inventing one.
|
|
5802
|
+
*/
|
|
5803
|
+
async allowanceTier() {
|
|
5804
|
+
const accesses = await this.loadPoolBillingAccess();
|
|
5805
|
+
if (accesses === void 0) return void 0;
|
|
5806
|
+
let highest;
|
|
5807
|
+
for (const access of accesses) {
|
|
5808
|
+
const weight = access?.tierWeight;
|
|
5809
|
+
if (weight === void 0) continue;
|
|
5810
|
+
if (highest === void 0 || weight > highest) highest = weight;
|
|
5811
|
+
}
|
|
5812
|
+
return allowanceTierForWeight(highest);
|
|
5813
|
+
}
|
|
4343
5814
|
async resolveModel(provider, model, signal) {
|
|
5815
|
+
this.currentCatalogState();
|
|
4344
5816
|
const entry = this.catalog.find((m) => m.id === model) ?? (await this.loadCatalog(signal)).find((m) => m.id === model);
|
|
4345
5817
|
const efforts = KNOWN_EFFORTS[model];
|
|
4346
5818
|
const vision = KNOWN_IMAGE_MODELS.has(model);
|
|
@@ -4375,21 +5847,19 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4375
5847
|
};
|
|
4376
5848
|
}
|
|
4377
5849
|
/**
|
|
4378
|
-
* Fetch one
|
|
4379
|
-
*
|
|
4380
|
-
*
|
|
4381
|
-
*
|
|
4382
|
-
* record; only a fetch failure propagates to the caller.
|
|
5850
|
+
* Fetch one JSON GET and return its HTTP status with a parsed record. The
|
|
5851
|
+
* catalog, billing probe, and usage report each apply their own failure
|
|
5852
|
+
* policy. Non-2xx and invalid JSON bodies come back without a record; only
|
|
5853
|
+
* a fetch failure propagates to the caller.
|
|
4383
5854
|
*
|
|
4384
|
-
* `timeoutMs` is the caller's budget
|
|
4385
|
-
*
|
|
4386
|
-
*
|
|
4387
|
-
* account query is not held to a stricter (and invisible) limit than chat.
|
|
5855
|
+
* `timeoutMs` is the caller's budget, unless it provides its own signal.
|
|
5856
|
+
* The usage report passes the connection's request budget rather than the
|
|
5857
|
+
* catalog/picker's shorter default.
|
|
4388
5858
|
*/
|
|
4389
|
-
async
|
|
5859
|
+
async fetchJson(url, headers, timeoutMs = MODELS_TIMEOUT_MS, signal) {
|
|
4390
5860
|
const response = await this.fetchImpl(url, {
|
|
4391
5861
|
headers,
|
|
4392
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
5862
|
+
signal: signal ?? AbortSignal.timeout(timeoutMs)
|
|
4393
5863
|
});
|
|
4394
5864
|
if (!response.ok) return { status: response.status };
|
|
4395
5865
|
let parsed;
|
|
@@ -4420,7 +5890,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4420
5890
|
async loadPoolBillingAccess() {
|
|
4421
5891
|
const keys = await this.poolAccountKeys();
|
|
4422
5892
|
if (keys.length === 0) return void 0;
|
|
4423
|
-
|
|
5893
|
+
const apiBase = this.deps.options().apiBase;
|
|
5894
|
+
return Promise.all(keys.map((key) => this.loadBillingAccessForKey(apiBase, key)));
|
|
4424
5895
|
}
|
|
4425
5896
|
/**
|
|
4426
5897
|
* The API keys the picker's plan filter must consult: every account the host
|
|
@@ -4444,21 +5915,22 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4444
5915
|
* shared across concurrent callers. `undefined` means "unknown — show
|
|
4445
5916
|
* everything" (fail-open).
|
|
4446
5917
|
*/
|
|
4447
|
-
async loadBillingAccessForKey(apiKey) {
|
|
4448
|
-
const
|
|
5918
|
+
async loadBillingAccessForKey(apiBase, apiKey) {
|
|
5919
|
+
const cacheKey = this.accountCacheKey(apiBase, apiKey);
|
|
5920
|
+
const cached = this.billingAccess.get(cacheKey);
|
|
4449
5921
|
if (cached !== void 0 && Date.now() - cached.at < 3e5) return cached.value;
|
|
4450
|
-
const existing = this.billingAccessInflight.get(
|
|
5922
|
+
const existing = this.billingAccessInflight.get(cacheKey);
|
|
4451
5923
|
if (existing !== void 0) return existing;
|
|
4452
|
-
const inflight = this.fetchBillingAccess(apiKey).then((value) => {
|
|
4453
|
-
this.billingAccess.set(
|
|
5924
|
+
const inflight = this.fetchBillingAccess(apiBase, apiKey).then((value) => {
|
|
5925
|
+
this.billingAccess.set(cacheKey, {
|
|
4454
5926
|
value,
|
|
4455
5927
|
at: Date.now()
|
|
4456
5928
|
});
|
|
4457
5929
|
return value;
|
|
4458
5930
|
}).finally(() => {
|
|
4459
|
-
this.billingAccessInflight.delete(
|
|
5931
|
+
this.billingAccessInflight.delete(cacheKey);
|
|
4460
5932
|
});
|
|
4461
|
-
this.billingAccessInflight.set(
|
|
5933
|
+
this.billingAccessInflight.set(cacheKey, inflight);
|
|
4462
5934
|
return inflight;
|
|
4463
5935
|
}
|
|
4464
5936
|
/**
|
|
@@ -4470,12 +5942,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4470
5942
|
* fallback (the CLI stamps plan identity from it too). Any failure resolves
|
|
4471
5943
|
* to `undefined` (fail-open) rather than breaking the picker.
|
|
4472
5944
|
*/
|
|
4473
|
-
async fetchBillingAccess(apiKey) {
|
|
5945
|
+
async fetchBillingAccess(apiBase, apiKey) {
|
|
4474
5946
|
try {
|
|
4475
|
-
const connection = this.deps.options();
|
|
4476
5947
|
const headers = await this.accountHeaders(apiKey);
|
|
4477
|
-
const
|
|
4478
|
-
const getJson = async (path) => (await this.fetchEndpointJson(`${base}${path}`, headers)).record;
|
|
5948
|
+
const getJson = async (path) => (await this.fetchJson(`${apiBase}${path}`, headers)).record;
|
|
4479
5949
|
const whoami = await getJson("/alpha/whoami");
|
|
4480
5950
|
const orgData = whoami && isRecord(whoami.org) ? whoami.org : void 0;
|
|
4481
5951
|
const orgId = orgData === void 0 ? void 0 : stringValue(orgData.id);
|
|
@@ -4497,50 +5967,65 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4497
5967
|
}
|
|
4498
5968
|
}
|
|
4499
5969
|
/** Fresh cached billing tier weight for a key, or undefined when not known. */
|
|
4500
|
-
cachedBillingTierWeight(apiKey) {
|
|
4501
|
-
const hit = this.billingAccess.get(apiKey);
|
|
5970
|
+
cachedBillingTierWeight(apiBase, apiKey) {
|
|
5971
|
+
const hit = this.billingAccess.get(this.accountCacheKey(apiBase, apiKey));
|
|
4502
5972
|
if (hit === void 0 || Date.now() - hit.at >= 3e5) return void 0;
|
|
4503
5973
|
return hit.value?.tierWeight;
|
|
4504
5974
|
}
|
|
4505
5975
|
/** Cached protocol decision for a key, or undefined when expired/unknown. */
|
|
4506
|
-
|
|
4507
|
-
const hit = this.protocolCache.get(apiKey);
|
|
5976
|
+
cachedProtocol(apiBase, apiKey) {
|
|
5977
|
+
const hit = this.protocolCache.get(this.accountCacheKey(apiBase, apiKey));
|
|
4508
5978
|
if (hit === void 0 || Date.now() - hit.at >= 9e5) return void 0;
|
|
4509
|
-
return hit.
|
|
5979
|
+
return hit.protocol;
|
|
4510
5980
|
}
|
|
4511
|
-
rememberProtocol(apiKey,
|
|
4512
|
-
this.protocolCache.set(apiKey, {
|
|
4513
|
-
|
|
5981
|
+
rememberProtocol(apiBase, apiKey, protocol) {
|
|
5982
|
+
this.protocolCache.set(this.accountCacheKey(apiBase, apiKey), {
|
|
5983
|
+
protocol,
|
|
4514
5984
|
at: Date.now()
|
|
4515
5985
|
});
|
|
4516
5986
|
}
|
|
4517
5987
|
/**
|
|
4518
|
-
*
|
|
4519
|
-
*
|
|
4520
|
-
*
|
|
4521
|
-
*
|
|
5988
|
+
* True when the catalog routes `model` to `/provider/v1/messages`.
|
|
5989
|
+
*
|
|
5990
|
+
* The catalog's own `supported_endpoints` is authoritative (it is what
|
|
5991
|
+
* upstream publishes per model); `requiresMessagesEndpoint()` — the
|
|
5992
|
+
* `claude-*` prefix rule — is the fallback for a catalog entry that carries
|
|
5993
|
+
* no route list, which is the shape every pre-1.55 cache file has.
|
|
5994
|
+
*/
|
|
5995
|
+
routesToMessages(model) {
|
|
5996
|
+
const entry = this.catalog.find((candidate) => candidate.id === model);
|
|
5997
|
+
if (entry !== void 0 && entry.supportedEndpoints.length > 0) return entry.supportedEndpoints.includes("/messages");
|
|
5998
|
+
return requiresMessagesEndpoint(model);
|
|
5999
|
+
}
|
|
6000
|
+
/**
|
|
6001
|
+
* Choose the initial protocol for one request.
|
|
4522
6002
|
*
|
|
4523
|
-
* Models the Provider API serves
|
|
4524
|
-
* Claude family
|
|
4525
|
-
*
|
|
4526
|
-
*
|
|
4527
|
-
*
|
|
4528
|
-
*
|
|
4529
|
-
* costs nothing.
|
|
6003
|
+
* Models the Provider API serves only through `/provider/v1/messages` (the
|
|
6004
|
+
* Claude family) take the Messages transport on any tier and even under a
|
|
6005
|
+
* forced `'openai'` preference: that option is documented as "prefer the
|
|
6006
|
+
* Provider API, still fall back", and the 400 these models answer with on
|
|
6007
|
+
* Chat Completions (`Model "<id>" must be called via /provider/v1/messages`)
|
|
6008
|
+
* is a hard refusal, not a preference to honour.
|
|
4530
6009
|
*
|
|
4531
6010
|
* That decision is deliberately NOT written to `protocolCache`, which is
|
|
4532
|
-
* keyed by API key
|
|
4533
|
-
*
|
|
6011
|
+
* keyed by gateway and API key: it depends on the model, not just the account, so
|
|
6012
|
+
* remembering it would pin the whole ACCOUNT to one transport and drag
|
|
6013
|
+
* every other model along (issue #46).
|
|
6014
|
+
*
|
|
6015
|
+
* Otherwise a fresh protocol cache entry wins, then a cached (not
|
|
6016
|
+
* network-fetched) billing tier of Go — the one plan without Provider API
|
|
6017
|
+
* access. Unknown accounts default to Chat Completions and fall back to the
|
|
6018
|
+
* CLI transport only after an `upgrade_required` rejection.
|
|
4534
6019
|
*/
|
|
4535
|
-
resolveProtocol(apiKey, model) {
|
|
6020
|
+
resolveProtocol(apiBase, apiKey, model) {
|
|
4536
6021
|
const forced = this.deps.options().protocol;
|
|
4537
6022
|
if (forced === "cli") return forced;
|
|
4538
|
-
if (
|
|
6023
|
+
if (this.routesToMessages(model)) return "messages";
|
|
4539
6024
|
if (forced === "openai") return forced;
|
|
4540
|
-
const cached = this.
|
|
4541
|
-
if (cached !== void 0) return cached
|
|
4542
|
-
if (this.cachedBillingTierWeight(apiKey) === GO_TIER_WEIGHT) {
|
|
4543
|
-
this.rememberProtocol(apiKey,
|
|
6025
|
+
const cached = this.cachedProtocol(apiBase, apiKey);
|
|
6026
|
+
if (cached !== void 0) return cached;
|
|
6027
|
+
if (this.cachedBillingTierWeight(apiBase, apiKey) === GO_TIER_WEIGHT) {
|
|
6028
|
+
this.rememberProtocol(apiBase, apiKey, "cli");
|
|
4544
6029
|
return "cli";
|
|
4545
6030
|
}
|
|
4546
6031
|
return "openai";
|
|
@@ -4582,7 +6067,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4582
6067
|
const failed = [];
|
|
4583
6068
|
const getJson = async (path) => {
|
|
4584
6069
|
try {
|
|
4585
|
-
const { status, record, invalidResponse } = await this.
|
|
6070
|
+
const { status, record, invalidResponse } = await this.fetchJson(`${base}${path}`, headers, connection.requestTimeoutMs);
|
|
4586
6071
|
if (record === void 0) {
|
|
4587
6072
|
failures.push(`${path}: HTTP ${status}${invalidResponse ? " returned an unreadable or invalid JSON body" : ""}`);
|
|
4588
6073
|
failed.push(invalidResponse ? {
|
|
@@ -4655,13 +6140,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4655
6140
|
async probeWindowLimits(apiKey) {
|
|
4656
6141
|
try {
|
|
4657
6142
|
const connection = this.deps.options();
|
|
4658
|
-
const
|
|
4659
|
-
|
|
4660
|
-
signal: AbortSignal.timeout(MODELS_TIMEOUT_MS)
|
|
4661
|
-
});
|
|
4662
|
-
if (!response.ok) return void 0;
|
|
4663
|
-
const parsed = await response.json();
|
|
4664
|
-
if (!isRecord(parsed)) return void 0;
|
|
6143
|
+
const { record: parsed } = await this.fetchJson(`${connection.apiBase}/alpha/billing/credits`, await this.accountHeaders(apiKey));
|
|
6144
|
+
if (parsed === void 0) return void 0;
|
|
4665
6145
|
const windowLimits = isRecord(parsed.windowLimits) ? parsed.windowLimits : void 0;
|
|
4666
6146
|
if (windowLimits === void 0) return void 0;
|
|
4667
6147
|
let reported = false;
|
|
@@ -4685,6 +6165,59 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4685
6165
|
}
|
|
4686
6166
|
}
|
|
4687
6167
|
async *stream(options) {
|
|
6168
|
+
const timing = openRequestTiming(options.model, this.deps.onRequestTiming);
|
|
6169
|
+
let outcome = "cancelled";
|
|
6170
|
+
let errorCode;
|
|
6171
|
+
try {
|
|
6172
|
+
for await (const chunk of this.streamRequest(options, timing)) {
|
|
6173
|
+
if (chunk.type === "text-delta" || chunk.type === "reasoning-delta" || chunk.type === "tool-call-delta") timing.first("firstContentMs");
|
|
6174
|
+
if (chunk.type === "finish") outcome = "finished";
|
|
6175
|
+
yield chunk;
|
|
6176
|
+
}
|
|
6177
|
+
} catch (error) {
|
|
6178
|
+
outcome = options.signal?.aborted ? "aborted" : "error";
|
|
6179
|
+
errorCode = error instanceof LlmError ? error.code : "unknown";
|
|
6180
|
+
throw error;
|
|
6181
|
+
} finally {
|
|
6182
|
+
await timing.close(outcome, errorCode);
|
|
6183
|
+
}
|
|
6184
|
+
}
|
|
6185
|
+
/**
|
|
6186
|
+
* One turn, with at most one output-ceiling downshift.
|
|
6187
|
+
*
|
|
6188
|
+
* The ceiling a model accepts is not published anywhere the request can be
|
|
6189
|
+
* built from (see `modelOutputTokenLimit`), and the endpoint states it only
|
|
6190
|
+
* by refusing. That refusal is worth one retry rather than a failed turn: the
|
|
6191
|
+
* harness's own recovery for it — compact the context — cannot help, because
|
|
6192
|
+
* nothing about the context is wrong, and resending the identical request
|
|
6193
|
+
* would be refused identically (issue #71). Reading the stated ceiling off
|
|
6194
|
+
* the rejection turns one wasted turn into one, for good: it is recorded
|
|
6195
|
+
* process-wide, so the next request for this model is built correctly
|
|
6196
|
+
* without spending the round trip again.
|
|
6197
|
+
*
|
|
6198
|
+
* Only BEFORE the first chunk reaches the host. Once a delta has been
|
|
6199
|
+
* delivered the answer is already partly on screen, and a second request
|
|
6200
|
+
* would duplicate it — so from that point the error stands and says what is
|
|
6201
|
+
* actually wrong. One downshift per turn, and a second refusal surfaces.
|
|
6202
|
+
*/
|
|
6203
|
+
async *streamRequest(options, timing) {
|
|
6204
|
+
for (let attempt = 0;; attempt++) {
|
|
6205
|
+
const connection = this.deps.options();
|
|
6206
|
+
let delivered = false;
|
|
6207
|
+
try {
|
|
6208
|
+
for await (const chunk of this.streamAttempt(options, timing, connection)) {
|
|
6209
|
+
delivered = true;
|
|
6210
|
+
yield chunk;
|
|
6211
|
+
}
|
|
6212
|
+
return;
|
|
6213
|
+
} catch (error) {
|
|
6214
|
+
const ceiling = delivered ? void 0 : outputCeilingRefusal(connection.apiBase, options.model, error);
|
|
6215
|
+
if (attempt > 0 || ceiling === void 0) throw error;
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
async *streamAttempt(options, timing, connection) {
|
|
6220
|
+
this.currentCatalogState(connection);
|
|
4688
6221
|
if (options.stop?.length) throw new LlmError("Command Code adapter does not support stop sequences", "UNSUPPORTED_OPTION");
|
|
4689
6222
|
const hasImages = options.messages.some(hasImageContent);
|
|
4690
6223
|
let readImage;
|
|
@@ -4692,30 +6225,51 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4692
6225
|
if (!KNOWN_IMAGE_MODELS.has(options.model)) throw new LlmError(`Command Code model "${options.model}" does not support image input; switch to a Vision-capable model (see the model registry)`, "UNSUPPORTED_CONTENT");
|
|
4693
6226
|
const attachments = this.resolveAttachments?.();
|
|
4694
6227
|
if (attachments === void 0) throw new LlmError("Command Code image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
4695
|
-
readImage = (
|
|
6228
|
+
readImage = requestImages(attachments, options.signal, timing);
|
|
4696
6229
|
}
|
|
4697
|
-
const
|
|
4698
|
-
let apiKey
|
|
4699
|
-
|
|
4700
|
-
|
|
6230
|
+
const finishCredentials = timing.phase("credentials");
|
|
6231
|
+
let apiKey;
|
|
6232
|
+
try {
|
|
6233
|
+
apiKey = await this.deps.resolveApiKey(connection, options.model);
|
|
6234
|
+
} finally {
|
|
6235
|
+
finishCredentials();
|
|
6236
|
+
}
|
|
6237
|
+
const modelEntry = this.catalog.find((m) => m.id === options.model);
|
|
6238
|
+
const modelMax = Math.min(modelEntry?.maxTokens ?? Infinity, modelOutputTokenLimit(connection.apiBase, options.model));
|
|
6239
|
+
const requestedMaxTokens = Math.min(options.maxTokens ?? modelMax, modelMax, DEFAULT_GENERATE_MAX_TOKENS);
|
|
6240
|
+
let maxTokens = requestedMaxTokens;
|
|
6241
|
+
let contextBudget = {
|
|
6242
|
+
maxTokens,
|
|
6243
|
+
sizePressure: false
|
|
6244
|
+
};
|
|
4701
6245
|
const effort = options.reasoningEffort;
|
|
4702
6246
|
const supported = KNOWN_EFFORTS[options.model];
|
|
4703
6247
|
const reasoningEffort = effort && effort !== "off" && supported?.includes(effort) ? effort : void 0;
|
|
4704
6248
|
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");
|
|
4705
|
-
let protocol = this.resolveProtocol(apiKey, options.model);
|
|
4706
|
-
let requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0]);
|
|
4707
|
-
const buildBody = async (target) =>
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
6249
|
+
let protocol = this.resolveProtocol(connection.apiBase, apiKey, options.model);
|
|
6250
|
+
let requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
|
|
6251
|
+
const buildBody = async (target) => {
|
|
6252
|
+
const finishBody = timing.phase("body");
|
|
6253
|
+
try {
|
|
6254
|
+
const facts = {
|
|
6255
|
+
maxTokens: requestedMaxTokens,
|
|
6256
|
+
reasoningEffort,
|
|
6257
|
+
selectableEffort: supported !== void 0 && supported.length > 0,
|
|
6258
|
+
systemText,
|
|
6259
|
+
readImage
|
|
6260
|
+
};
|
|
6261
|
+
const built = target === "cli" ? await buildCliBody(requestOptions, connection, facts) : target === "messages" ? await buildMessagesBody(requestOptions, facts) : await buildOpenAIBody(requestOptions, facts);
|
|
6262
|
+
contextBudget = requestContextBudget(target, built, requestOptions.messages, options.model, modelEntry?.contextWindow, requestedMaxTokens);
|
|
6263
|
+
maxTokens = contextBudget.maxTokens;
|
|
6264
|
+
if (target === "cli") recordOrEmpty(built.params).max_tokens = maxTokens;
|
|
6265
|
+
else built.max_tokens = maxTokens;
|
|
6266
|
+
return built;
|
|
6267
|
+
} finally {
|
|
6268
|
+
finishBody();
|
|
6269
|
+
}
|
|
6270
|
+
};
|
|
4718
6271
|
let body = await buildBody(protocol);
|
|
6272
|
+
const serializedBodies = /* @__PURE__ */ new WeakMap();
|
|
4719
6273
|
const trace = openStreamTrace(`${options.provider}/${options.model}`);
|
|
4720
6274
|
const tried = /* @__PURE__ */ new Set();
|
|
4721
6275
|
let attemptNumber = 0;
|
|
@@ -4727,14 +6281,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4727
6281
|
options,
|
|
4728
6282
|
connection,
|
|
4729
6283
|
fetchImpl: this.fetchImpl,
|
|
6284
|
+
timing,
|
|
4730
6285
|
onResponse: (response) => {
|
|
6286
|
+
this.headerTimeoutStreaks.delete(options.sessionId ?? GLOBAL_HEADER_TIMEOUT_KEY);
|
|
4731
6287
|
responseMetadata = {
|
|
4732
6288
|
protocol,
|
|
4733
6289
|
headers: responseIdentifiers(response.headers)
|
|
4734
6290
|
};
|
|
4735
6291
|
trace.record("response", {
|
|
4736
6292
|
protocol,
|
|
4737
|
-
endpoint: protocol
|
|
6293
|
+
endpoint: protocolEndpoint(protocol, connection.apiBase),
|
|
4738
6294
|
status: response.status,
|
|
4739
6295
|
attempt: attemptNumber,
|
|
4740
6296
|
accountsTried: tried.size,
|
|
@@ -4758,14 +6314,27 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4758
6314
|
sessionId: options.sessionId,
|
|
4759
6315
|
...requestFingerprint(protocol, body)
|
|
4760
6316
|
});
|
|
4761
|
-
|
|
6317
|
+
let serialized = serializedBodies.get(body);
|
|
6318
|
+
if (serialized === void 0) {
|
|
6319
|
+
const finishSerialization = timing.phase("serialization");
|
|
6320
|
+
try {
|
|
6321
|
+
serialized = JSON.stringify(body, (_key, value) => sanitizeSurrogates(value));
|
|
6322
|
+
} finally {
|
|
6323
|
+
finishSerialization();
|
|
6324
|
+
}
|
|
6325
|
+
serializedBodies.set(body, serialized);
|
|
6326
|
+
}
|
|
6327
|
+
const attempt = await connectGenerate(connectDeps, apiKey, protocol, serialized);
|
|
4762
6328
|
if ("response" in attempt) {
|
|
4763
6329
|
connected = attempt;
|
|
4764
6330
|
break;
|
|
4765
6331
|
}
|
|
4766
|
-
|
|
6332
|
+
const providerError = parseProviderError(attempt.errText);
|
|
6333
|
+
if (protocol !== "cli" && isUpgradeRequiredError(attempt.status, attempt.errText, providerError)) {
|
|
6334
|
+
const wasMessages = protocol === "messages";
|
|
4767
6335
|
protocol = "cli";
|
|
4768
|
-
this.rememberProtocol(apiKey,
|
|
6336
|
+
if (!wasMessages) this.rememberProtocol(connection.apiBase, apiKey, "cli");
|
|
6337
|
+
requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true ? options.model : void 0);
|
|
4769
6338
|
body = await buildBody("cli");
|
|
4770
6339
|
continue;
|
|
4771
6340
|
}
|
|
@@ -4774,7 +6343,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4774
6343
|
if (missing > 0) throw imageOffloadRequired(missing);
|
|
4775
6344
|
}
|
|
4776
6345
|
const rotate = this.deps.rotateApiKey;
|
|
4777
|
-
const rejection = classifyAccountRejection(attempt.status, attempt.errText);
|
|
6346
|
+
const rejection = classifyAccountRejection(attempt.status, attempt.errText, providerError);
|
|
4778
6347
|
if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
|
|
4779
6348
|
const next = await rotate(apiKey, rejection.reason, connection, options.model, {
|
|
4780
6349
|
tried: [...tried],
|
|
@@ -4782,17 +6351,26 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4782
6351
|
});
|
|
4783
6352
|
if (next !== void 0 && !tried.has(next)) {
|
|
4784
6353
|
apiKey = next;
|
|
6354
|
+
const nextProtocol = this.resolveProtocol(connection.apiBase, apiKey, options.model);
|
|
6355
|
+
if (nextProtocol !== protocol) {
|
|
6356
|
+
protocol = nextProtocol;
|
|
6357
|
+
requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
|
|
6358
|
+
body = await buildBody(protocol);
|
|
6359
|
+
}
|
|
4785
6360
|
continue;
|
|
4786
6361
|
}
|
|
4787
6362
|
}
|
|
4788
|
-
if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled")
|
|
6363
|
+
if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled")) {
|
|
4789
6364
|
const reset = rejection.resetAtMs;
|
|
4790
|
-
const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
|
|
6365
|
+
const wait = reset === void 0 ? attempt.retryAfterMs ?? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
|
|
4791
6366
|
const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
|
|
4792
6367
|
const window = rejection.reason === "rate-limit";
|
|
4793
|
-
throw
|
|
6368
|
+
throw bilingual(window ? "RATE_LIMIT" : THROTTLED_CODE, (window ? "llm-commandcode: the Command Code account is rate limited" : "llm-commandcode: the Command Code account is rate limited (429) — the provider did not report an exhausted usage window") + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`), (window ? "当前 Command Code 账户已被限流" : "当前 Command Code 账户被限流(429),服务商未报告用量窗口用尽") + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), {
|
|
6369
|
+
status: attempt.status,
|
|
6370
|
+
...wait > 0 && wait <= 9e5 ? { providerRetryAfterMs: wait } : {}
|
|
6371
|
+
});
|
|
4794
6372
|
}
|
|
4795
|
-
throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
|
|
6373
|
+
throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs, providerError, contextBudget.sizePressure);
|
|
4796
6374
|
}
|
|
4797
6375
|
} catch (error) {
|
|
4798
6376
|
trace.record("connect-error", {
|
|
@@ -4800,7 +6378,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4800
6378
|
responseMetadata
|
|
4801
6379
|
});
|
|
4802
6380
|
trace.close();
|
|
4803
|
-
|
|
6381
|
+
if (options.signal?.aborted) throw error;
|
|
6382
|
+
throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs, options.sessionId), responseMetadata);
|
|
4804
6383
|
}
|
|
4805
6384
|
if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
|
|
4806
6385
|
const { response, cleanup } = connected;
|
|
@@ -4860,7 +6439,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4860
6439
|
silentMs: Date.now() - lastChunkAt,
|
|
4861
6440
|
message: errorChain(error)
|
|
4862
6441
|
});
|
|
4863
|
-
throw
|
|
6442
|
+
throw bilingual("TRANSPORT", `Command Code API stream from ${connection.apiBase} failed while reading: ${errorChain(error)}`, "Command Code API 流式响应中途断开——网络波动所致,重试通常可恢复", { cause: error });
|
|
4864
6443
|
} finally {
|
|
4865
6444
|
clearIdle();
|
|
4866
6445
|
}
|
|
@@ -4875,11 +6454,12 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4875
6454
|
idleFired,
|
|
4876
6455
|
buffered: buffer.length
|
|
4877
6456
|
});
|
|
4878
|
-
if (idleFired) throw
|
|
6457
|
+
if (idleFired) throw bilingual("TIMEOUT", `Command Code API stream from ${connection.apiBase} was idle for ${connection.streamIdleTimeoutMs}ms (no events) and was treated as a dead connection`, `Command Code API 流式响应已 ${connection.streamIdleTimeoutMs} 毫秒无任何事件,被判定为死连接——长思考模型可在设置中调大流空闲超时`);
|
|
4879
6458
|
if (buffer.trim()) yield* handle(parseStreamEventLine(buffer));
|
|
4880
6459
|
break;
|
|
4881
6460
|
}
|
|
4882
6461
|
const text = decoder.decode(value, { stream: true });
|
|
6462
|
+
if (value.byteLength > 0) timing.first("firstByteMs");
|
|
4883
6463
|
buffer += text;
|
|
4884
6464
|
chunkCount += 1;
|
|
4885
6465
|
totalBytes += value.byteLength;
|
|
@@ -4917,9 +6497,13 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4917
6497
|
});
|
|
4918
6498
|
endRecorded = true;
|
|
4919
6499
|
if (failure !== void 0) throw failure;
|
|
4920
|
-
|
|
6500
|
+
const replayBlocks = asm.replayBlocks;
|
|
6501
|
+
yield hasResponseIdentifiers(responseMetadata) || replayBlocks.length > 0 ? {
|
|
4921
6502
|
...finish,
|
|
4922
|
-
replayState: {
|
|
6503
|
+
replayState: {
|
|
6504
|
+
response: responseMetadata,
|
|
6505
|
+
...replayBlocks.length > 0 ? { blocks: replayBlocks } : {}
|
|
6506
|
+
}
|
|
4923
6507
|
} : finish;
|
|
4924
6508
|
} else {
|
|
4925
6509
|
const elapsed = Date.now() - streamStartedAt;
|
|
@@ -4933,8 +6517,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4933
6517
|
responseMetadata
|
|
4934
6518
|
});
|
|
4935
6519
|
endRecorded = true;
|
|
4936
|
-
if (!asm.sawContent) throw
|
|
4937
|
-
throw
|
|
6520
|
+
if (!asm.sawContent) throw bilingual("EMPTY_RESPONSE", "Command Code returned an empty response", "Command Code 返回了空响应,重试通常可恢复");
|
|
6521
|
+
throw bilingual("STREAM_CLOSED", `Command Code API stream from ${connection.apiBase} ended before its finish event (${detail}) — the response was closed mid-generation, so this is not the model stopping by itself`, `Command Code API 流式响应在结束事件之前被关闭(生成中途断流),并非模型主动结束——请重试;若频繁出现,可用 ${STREAM_TRACE_ENV}=<文件路径> 抓取原始流以定位断流原因`);
|
|
4938
6522
|
}
|
|
4939
6523
|
} catch (error) {
|
|
4940
6524
|
if (!usageEmitted && usage !== void 0) yield {
|
|
@@ -4964,43 +6548,66 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4964
6548
|
}
|
|
4965
6549
|
};
|
|
4966
6550
|
/**
|
|
4967
|
-
*
|
|
4968
|
-
*
|
|
4969
|
-
*
|
|
4970
|
-
*
|
|
4971
|
-
*
|
|
4972
|
-
*
|
|
4973
|
-
*
|
|
4974
|
-
*
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
6551
|
+
* Unwrap one level of a double-encoded provider message.
|
|
6552
|
+
*
|
|
6553
|
+
* The Messages endpoint sometimes reports a rejection with `error.message`
|
|
6554
|
+
* holding a whole JSON document rather than the sentence
|
|
6555
|
+
* (`{"type":"error","error":{"type":"invalid_request_error","message":"{\"type\":
|
|
6556
|
+
* \"error\",…}"},"request_id":"req_…"}`, measured 2026-09-29). Showing that blob
|
|
6557
|
+
* to the user hides the actual refusal, so one nesting level is peeled; the
|
|
6558
|
+
* outer text is returned unchanged when it is not JSON or carries no message.
|
|
6559
|
+
*/
|
|
6560
|
+
function unwrapProviderMessage(raw) {
|
|
6561
|
+
if (raw === void 0) return void 0;
|
|
6562
|
+
const trimmed = raw.trim();
|
|
6563
|
+
if (!trimmed.startsWith("{")) return raw;
|
|
6564
|
+
try {
|
|
6565
|
+
const inner = JSON.parse(trimmed);
|
|
6566
|
+
if (!isRecord(inner)) return raw;
|
|
6567
|
+
return stringValue(((isRecord(inner.error) ? inner.error : void 0) ?? inner).message) ?? raw;
|
|
6568
|
+
} catch {
|
|
6569
|
+
return raw;
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
6572
|
+
/** Parse a rejected body once, retaining the nested/root distinction. */
|
|
6573
|
+
function parseProviderError(errText) {
|
|
4987
6574
|
try {
|
|
4988
6575
|
const parsed = JSON.parse(errText);
|
|
4989
|
-
if (isRecord(parsed)
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
6576
|
+
if (!isRecord(parsed)) return { message: errText };
|
|
6577
|
+
const nested = isRecord(parsed.error) ? parsed.error : void 0;
|
|
6578
|
+
const body = nested ?? parsed;
|
|
6579
|
+
const rawMessage = stringValue(body.message);
|
|
6580
|
+
return {
|
|
6581
|
+
root: parsed,
|
|
6582
|
+
nested,
|
|
6583
|
+
code: stringValue(body.code),
|
|
6584
|
+
type: stringValue(body.type),
|
|
6585
|
+
rawMessage,
|
|
6586
|
+
message: unwrapProviderMessage(rawMessage) ?? errText,
|
|
6587
|
+
rateLimit: isRecord(body.rateLimit) ? body.rateLimit : void 0
|
|
6588
|
+
};
|
|
6589
|
+
} catch {
|
|
6590
|
+
return { message: errText };
|
|
6591
|
+
}
|
|
6592
|
+
}
|
|
6593
|
+
function generateHttpError(status, errText, retryAfterMs, parsed = parseProviderError(errText), sizePressure = false) {
|
|
6594
|
+
const providerCode = parsed.nested === void 0 ? void 0 : parsed.code;
|
|
6595
|
+
const providerDetail = parsed.nested === void 0 ? "" : [
|
|
6596
|
+
parsed.code,
|
|
6597
|
+
parsed.type,
|
|
6598
|
+
parsed.rawMessage ?? parsed.message
|
|
6599
|
+
].filter((part) => part !== void 0 && part !== "").join(" ");
|
|
4998
6600
|
const detail = providerCode ?? `HTTP ${status}`;
|
|
4999
6601
|
const overflowDetail = providerDetail !== "" ? providerDetail : errText.slice(0, 500);
|
|
5000
|
-
if (status < 500 &&
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
6602
|
+
if (status < 500 && OUTPUT_LIMIT_REJECTION.test(overflowDetail)) {
|
|
6603
|
+
const stated = /max_tokens:\s*\d+\s*>\s*\d+/.exec(overflowDetail)?.[0] ?? (parsed.message !== void 0 && parsed.message.length <= 200 ? parsed.message : void 0);
|
|
6604
|
+
return bilingual("PROVIDER_HTTP_ERROR", `Command Code API error ${status}: this request's max_tokens is not valid for this model` + (stated ? ` (${stated})` : "") + " — the session is not oversized, so compacting it will not help; lower \"Max output tokens\" or pick a model with a larger output ceiling", `Command Code API 返回 ${status}:本次请求的 max_tokens 对该模型不合法` + (stated ? `(${stated})` : "") + "——会话内容并未超长,压缩上下文无效;请调低「最大输出 token」或改用输出上限更大的模型", { status });
|
|
6605
|
+
}
|
|
6606
|
+
if (status < 500 && isContextOverflowDetail(overflowDetail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, `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}:请求内容超出模型上下文窗口——正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, { status });
|
|
6607
|
+
if (status === 400 && sizePressure && AMBIGUOUS_SIZE_REJECTION.test(providerDetail) && /\bprovider_error\b/i.test(providerDetail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, "Command Code API error 400: the provider could not complete a request near the model context limit — compacting the session and retrying", "Command Code API 返回 400:请求接近模型上下文上限,服务商无法完成——正在压缩上下文后重试", { status });
|
|
6608
|
+
if (status === 422 && isZdrNoProviders(providerCode, providerDetail, errText)) return bilingual("PROVIDER_HTTP_ERROR", `Command Code API error 422 (${detail}): zero data retention is enforced but no ZDR-capable upstream is available for this model — turn the ZDR setting off (or use another model)`, "Command Code API 返回 422:已开启零数据保留(ZDR),但该模型当前没有可用的 ZDR 上游——请关闭 ZDR 开关,或改用其他模型", { status: 422 });
|
|
6609
|
+
if (status === 401) return bilingual("INVALID_CREDENTIAL", `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 文件", { status: 401 });
|
|
6610
|
+
if (status === 413) return bilingual("PROVIDER_HTTP_ERROR", "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)——会话历史过大,通常是历史图片累积所致。请新建会话,或移除本会话中图片较多的部分后重试", { status: 413 });
|
|
5004
6611
|
return new LlmError(`Command Code API error ${status}${detail === `HTTP ${status}` ? "" : ` (${detail})`}: ${errText.slice(0, 500)}`, httpErrorCode(status), {
|
|
5005
6612
|
status,
|
|
5006
6613
|
...retryAfterMs !== void 0 && retryAfterMs > 0 && retryAfterMs <= 9e5 ? { providerRetryAfterMs: retryAfterMs } : {}
|
|
@@ -5019,7 +6626,7 @@ function generateHttpError(status, errText, retryAfterMs) {
|
|
|
5019
6626
|
* rejection repeats identically on every attempt.
|
|
5020
6627
|
*/
|
|
5021
6628
|
function httpErrorCode(status) {
|
|
5022
|
-
if (status === 429) return
|
|
6629
|
+
if (status === 429) return THROTTLED_CODE;
|
|
5023
6630
|
if (status === 408) return "TIMEOUT";
|
|
5024
6631
|
if (status >= 500) return "SERVER";
|
|
5025
6632
|
return "PROVIDER_HTTP_ERROR";
|
|
@@ -5239,8 +6846,195 @@ function handleOpenAIEvent(asm, event) {
|
|
|
5239
6846
|
return chunks;
|
|
5240
6847
|
}
|
|
5241
6848
|
/** Dispatch one parsed stream event to the active transport's handler. */
|
|
6849
|
+
/**
|
|
6850
|
+
* Messages `usage` → the harness meter.
|
|
6851
|
+
*
|
|
6852
|
+
* The field names are the Anthropic ones, and unlike Chat Completions
|
|
6853
|
+
* `input_tokens` already excludes cache reads and writes, so it is mapped
|
|
6854
|
+
* as-is rather than reduced (the shape `dsh-llm-deepseek` implements).
|
|
6855
|
+
* Measured 2026-09-29: `{ input_tokens, cache_creation_input_tokens,
|
|
6856
|
+
* cache_read_input_tokens, output_tokens, output_tokens_details:
|
|
6857
|
+
* { thinking_tokens } }`, plus gateway additions the meter ignores.
|
|
6858
|
+
*/
|
|
6859
|
+
function mapMessagesUsage(raw) {
|
|
6860
|
+
if (!isRecord(raw)) return void 0;
|
|
6861
|
+
const inputTokens = numberValue(raw.input_tokens) ?? 0;
|
|
6862
|
+
const outputTokens = numberValue(raw.output_tokens) ?? 0;
|
|
6863
|
+
const cacheRead = numberValue(raw.cache_read_input_tokens) ?? 0;
|
|
6864
|
+
const cacheWrite = numberValue(raw.cache_creation_input_tokens) ?? 0;
|
|
6865
|
+
const reasoningTokens = numberValue((isRecord(raw.output_tokens_details) ? raw.output_tokens_details : void 0)?.thinking_tokens);
|
|
6866
|
+
const usage = {
|
|
6867
|
+
inputTokens: Math.max(0, inputTokens),
|
|
6868
|
+
outputTokens,
|
|
6869
|
+
cacheReadTokens: cacheRead
|
|
6870
|
+
};
|
|
6871
|
+
if (cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
|
|
6872
|
+
if (reasoningTokens !== void 0) usage.reasoningTokens = reasoningTokens;
|
|
6873
|
+
return usage;
|
|
6874
|
+
}
|
|
6875
|
+
/** The harness block type one Messages content block maps to. */
|
|
6876
|
+
function messagesBlockType(native) {
|
|
6877
|
+
if (native.type === "text") return "text";
|
|
6878
|
+
if (native.type === "thinking") return "reasoning";
|
|
6879
|
+
if (native.type === "tool_use") return "tool-call";
|
|
6880
|
+
}
|
|
6881
|
+
/**
|
|
6882
|
+
* Handle one Messages-transport stream event, appending harness StreamChunks.
|
|
6883
|
+
*
|
|
6884
|
+
* The event sequence is the Anthropic one measured on 2026-09-29:
|
|
6885
|
+
* `message_start`, then per content block `content_block_start` /
|
|
6886
|
+
* `content_block_delta`* / `content_block_stop`, then `message_delta`
|
|
6887
|
+
* (carrying `stop_reason` and the final usage) and `message_stop`. A `ping`
|
|
6888
|
+
* keepalive interleaves freely and carries no content. The terminal chunks are
|
|
6889
|
+
* NOT emitted here: `stream()` holds the success finish until it has validated
|
|
6890
|
+
* the assembled answer, and it attaches the replay envelope.
|
|
6891
|
+
*/
|
|
6892
|
+
function handleMessagesEvent(asm, event) {
|
|
6893
|
+
const chunks = [];
|
|
6894
|
+
if (!isRecord(event)) return chunks;
|
|
6895
|
+
switch (event.type) {
|
|
6896
|
+
case "message_start": {
|
|
6897
|
+
const usage = mapMessagesUsage((isRecord(event.message) ? event.message : void 0)?.usage);
|
|
6898
|
+
if (usage) chunks.push({
|
|
6899
|
+
type: "usage",
|
|
6900
|
+
usage
|
|
6901
|
+
});
|
|
6902
|
+
break;
|
|
6903
|
+
}
|
|
6904
|
+
case "content_block_start": {
|
|
6905
|
+
const wireIndex = numberValue(event.index);
|
|
6906
|
+
const native = isRecord(event.content_block) ? event.content_block : void 0;
|
|
6907
|
+
const type = native ? messagesBlockType(native) : void 0;
|
|
6908
|
+
if (wireIndex === void 0 || type === void 0) break;
|
|
6909
|
+
const chunkIndex = asm.nextIndex++;
|
|
6910
|
+
const state = {
|
|
6911
|
+
type,
|
|
6912
|
+
text: stringValue(native?.text) ?? "",
|
|
6913
|
+
signature: stringValue(native?.signature) ?? "",
|
|
6914
|
+
json: "",
|
|
6915
|
+
id: stringValue(native?.id) ?? "",
|
|
6916
|
+
name: stringValue(native?.name) ?? "",
|
|
6917
|
+
chunkIndex
|
|
6918
|
+
};
|
|
6919
|
+
asm.messagesBlocks.set(wireIndex, state);
|
|
6920
|
+
asm.replayBlocks[chunkIndex] = { type };
|
|
6921
|
+
chunks.push({
|
|
6922
|
+
type: "block-start",
|
|
6923
|
+
index: chunkIndex,
|
|
6924
|
+
blockType: type
|
|
6925
|
+
});
|
|
6926
|
+
if (type === "tool-call" && state.id !== "") chunks.push({
|
|
6927
|
+
type: "tool-call-delta",
|
|
6928
|
+
index: chunkIndex,
|
|
6929
|
+
id: ToolCallId(state.id),
|
|
6930
|
+
name: state.name,
|
|
6931
|
+
argumentsDelta: ""
|
|
6932
|
+
});
|
|
6933
|
+
break;
|
|
6934
|
+
}
|
|
6935
|
+
case "content_block_delta": {
|
|
6936
|
+
const wireIndex = numberValue(event.index);
|
|
6937
|
+
const state = wireIndex === void 0 ? void 0 : asm.messagesBlocks.get(wireIndex);
|
|
6938
|
+
const delta = isRecord(event.delta) ? event.delta : void 0;
|
|
6939
|
+
if (!state || !delta) break;
|
|
6940
|
+
const index = state.chunkIndex;
|
|
6941
|
+
if (delta.type === "text_delta" && state.type === "text") {
|
|
6942
|
+
const text = stringValue(delta.text) ?? "";
|
|
6943
|
+
state.text += text;
|
|
6944
|
+
if (text.trim() !== "") asm.sawContent = true;
|
|
6945
|
+
chunks.push({
|
|
6946
|
+
type: "text-delta",
|
|
6947
|
+
index,
|
|
6948
|
+
text
|
|
6949
|
+
});
|
|
6950
|
+
} else if (delta.type === "thinking_delta" && state.type === "reasoning") {
|
|
6951
|
+
const text = stringValue(delta.thinking) ?? "";
|
|
6952
|
+
state.text += text;
|
|
6953
|
+
chunks.push({
|
|
6954
|
+
type: "reasoning-delta",
|
|
6955
|
+
index,
|
|
6956
|
+
text
|
|
6957
|
+
});
|
|
6958
|
+
} else if (delta.type === "signature_delta" && state.type === "reasoning") {
|
|
6959
|
+
state.signature += stringValue(delta.signature) ?? "";
|
|
6960
|
+
const entry = asm.replayBlocks[index];
|
|
6961
|
+
if (entry) entry.signature = state.signature;
|
|
6962
|
+
} else if (delta.type === "input_json_delta" && state.type === "tool-call") {
|
|
6963
|
+
const fragment = stringValue(delta.partial_json) ?? "";
|
|
6964
|
+
state.json += fragment;
|
|
6965
|
+
chunks.push({
|
|
6966
|
+
type: "tool-call-delta",
|
|
6967
|
+
index,
|
|
6968
|
+
id: ToolCallId(state.id),
|
|
6969
|
+
name: state.name,
|
|
6970
|
+
argumentsDelta: fragment
|
|
6971
|
+
});
|
|
6972
|
+
}
|
|
6973
|
+
break;
|
|
6974
|
+
}
|
|
6975
|
+
case "content_block_stop": {
|
|
6976
|
+
const wireIndex = numberValue(event.index);
|
|
6977
|
+
if (wireIndex === void 0) break;
|
|
6978
|
+
const state = asm.messagesBlocks.get(wireIndex);
|
|
6979
|
+
if (!state) break;
|
|
6980
|
+
asm.messagesBlocks.delete(wireIndex);
|
|
6981
|
+
const index = state.chunkIndex;
|
|
6982
|
+
if (state.type === "text") {
|
|
6983
|
+
if (state.text.trim() !== "") asm.sawContent = true;
|
|
6984
|
+
chunks.push({
|
|
6985
|
+
type: "block-end",
|
|
6986
|
+
index,
|
|
6987
|
+
block: {
|
|
6988
|
+
type: "text",
|
|
6989
|
+
text: state.text
|
|
6990
|
+
}
|
|
6991
|
+
});
|
|
6992
|
+
} else if (state.type === "reasoning") chunks.push({
|
|
6993
|
+
type: "block-end",
|
|
6994
|
+
index,
|
|
6995
|
+
block: {
|
|
6996
|
+
type: "reasoning",
|
|
6997
|
+
text: state.text
|
|
6998
|
+
}
|
|
6999
|
+
});
|
|
7000
|
+
else {
|
|
7001
|
+
if (state.id !== "") asm.sawContent = true;
|
|
7002
|
+
chunks.push({
|
|
7003
|
+
type: "block-end",
|
|
7004
|
+
index,
|
|
7005
|
+
block: {
|
|
7006
|
+
type: "tool-call",
|
|
7007
|
+
id: ToolCallId(state.id),
|
|
7008
|
+
name: state.name,
|
|
7009
|
+
arguments: state.json || "{}"
|
|
7010
|
+
}
|
|
7011
|
+
});
|
|
7012
|
+
}
|
|
7013
|
+
break;
|
|
7014
|
+
}
|
|
7015
|
+
case "message_delta": {
|
|
7016
|
+
const reason = stringValue((isRecord(event.delta) ? event.delta : void 0)?.stop_reason);
|
|
7017
|
+
if (reason !== void 0) asm.finishReason = reason;
|
|
7018
|
+
const usage = mapMessagesUsage(event.usage);
|
|
7019
|
+
if (usage) chunks.push({
|
|
7020
|
+
type: "usage",
|
|
7021
|
+
usage
|
|
7022
|
+
});
|
|
7023
|
+
chunks.push({
|
|
7024
|
+
type: "finish",
|
|
7025
|
+
reason: mapFinishReason(reason)
|
|
7026
|
+
});
|
|
7027
|
+
break;
|
|
7028
|
+
}
|
|
7029
|
+
case "message_stop":
|
|
7030
|
+
case "ping": break;
|
|
7031
|
+
case "error": throw streamErrorToLlmError(event.error ?? event, stringValue(event.message));
|
|
7032
|
+
}
|
|
7033
|
+
return chunks;
|
|
7034
|
+
}
|
|
5242
7035
|
function handleEvent(asm, protocol, event) {
|
|
5243
|
-
|
|
7036
|
+
if (protocol === "cli") return handleCliEvent(asm, event);
|
|
7037
|
+
return protocol === "messages" ? handleMessagesEvent(asm, event) : handleOpenAIEvent(asm, event);
|
|
5244
7038
|
}
|
|
5245
7039
|
/**
|
|
5246
7040
|
* Parse an HTTP `Retry-After` value (delay-seconds or an HTTP-date) into
|
|
@@ -5269,16 +7063,16 @@ function parseRetryAfterMs(value, now = Date.now()) {
|
|
|
5269
7063
|
* one function serves both — a new reason cannot drift between them.
|
|
5270
7064
|
*/
|
|
5271
7065
|
function mapFinishReason(reason) {
|
|
5272
|
-
if (reason === "tool-calls" || reason === "tool_calls") return { kind: "tool-calls" };
|
|
7066
|
+
if (reason === "tool-calls" || reason === "tool_calls" || reason === "tool_use") return { kind: "tool-calls" };
|
|
5273
7067
|
if (reason === "length" || reason === "max_tokens" || reason === "max-tokens" || reason === "max_output_tokens") return { kind: "max-tokens" };
|
|
5274
7068
|
return { kind: "stop" };
|
|
5275
7069
|
}
|
|
5276
7070
|
/** A terminal marker without text or tools is a failed attempt, not a completed turn. */
|
|
5277
7071
|
function emptyCompletionError(reason, maxTokens, usage) {
|
|
5278
7072
|
const detail = `finish_reason=${reason ?? "unknown"}, max_tokens=${maxTokens}, outputTokens=${usage?.outputTokens ?? "unknown"}, reasoningTokens=${usage?.reasoningTokens ?? "unknown"}`;
|
|
5279
|
-
if (mapFinishReason(reason).kind === "max-tokens") return
|
|
5280
|
-
if (reason === "content_filter" || reason === "content-filter") return
|
|
5281
|
-
return
|
|
7073
|
+
if (mapFinishReason(reason).kind === "max-tokens") return bilingual("OUTPUT_TOKEN_LIMIT", `Command Code reached the output token limit without producing answer text or a tool call (${detail})`, "Command Code 已达到输出 token 上限,但没有生成正文或工具调用;请调整思考强度,或在模型和接口上限内增加输出预算");
|
|
7074
|
+
if (reason === "content_filter" || reason === "content-filter") return bilingual("CONTENT_FILTER", `Command Code filtered the response (${detail})`, "Command Code 拦截了响应,未生成正文或工具调用");
|
|
7075
|
+
return bilingual("EMPTY_RESPONSE", `Command Code finished without producing answer text or a tool call (${detail})`, "Command Code 返回了空响应(可能只有思考内容),重试通常可恢复");
|
|
5282
7076
|
}
|
|
5283
7077
|
/**
|
|
5284
7078
|
* Classify a pre-stream rejection that is about the ACCOUNT rather than about
|
|
@@ -5322,9 +7116,10 @@ const ACCOUNT_UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
|
|
|
5322
7116
|
"PREMIUM_CREDITS_EXHAUSTED",
|
|
5323
7117
|
"MODEL_NOT_IN_PLAN"
|
|
5324
7118
|
]);
|
|
5325
|
-
function classifyAccountRejection(status, errText) {
|
|
5326
|
-
|
|
5327
|
-
|
|
7119
|
+
function classifyAccountRejection(status, errText, parsed = parseProviderError(errText)) {
|
|
7120
|
+
const code = (parsed.code ?? parsed.type)?.toUpperCase();
|
|
7121
|
+
if (status === 429 || code === "RATE_LIMITED") {
|
|
7122
|
+
const evidence = readWindowLimitEvidence(errText, parsed);
|
|
5328
7123
|
if (evidence.window === void 0) return { reason: "throttled" };
|
|
5329
7124
|
return evidence.resetAtMs === void 0 ? { reason: "rate-limit" } : {
|
|
5330
7125
|
reason: "rate-limit",
|
|
@@ -5332,23 +7127,11 @@ function classifyAccountRejection(status, errText) {
|
|
|
5332
7127
|
};
|
|
5333
7128
|
}
|
|
5334
7129
|
if (status === 401) return { reason: "invalid-credential" };
|
|
5335
|
-
const code = readProviderErrorCode(errText)?.toUpperCase();
|
|
5336
7130
|
if (code !== void 0 && ACCOUNT_UNAVAILABLE_CODES.has(code)) return { reason: "unavailable" };
|
|
5337
7131
|
if (status < 400 || status >= 500) return void 0;
|
|
5338
7132
|
const lower = errText.toLowerCase().replaceAll("_", " ");
|
|
5339
7133
|
if (lower.includes("insufficient credits") || lower.includes("premium credits exhausted") || lower.includes("model not in plan") || /insufficient (credit|balance)/.test(lower) || /out of credit/.test(lower) || /not (available|included)[^.]{0,40}plan/.test(lower) || lower.includes("upgrade your plan")) return { reason: "unavailable" };
|
|
5340
7134
|
}
|
|
5341
|
-
/** The machine-readable `error.code`/`error.type` of a pre-stream failure body. */
|
|
5342
|
-
function readProviderErrorCode(errText) {
|
|
5343
|
-
try {
|
|
5344
|
-
const parsed = JSON.parse(errText);
|
|
5345
|
-
if (!isRecord(parsed)) return void 0;
|
|
5346
|
-
const error = isRecord(parsed.error) ? parsed.error : parsed;
|
|
5347
|
-
return stringValue(error.code) ?? stringValue(error.type);
|
|
5348
|
-
} catch {
|
|
5349
|
-
return;
|
|
5350
|
-
}
|
|
5351
|
-
}
|
|
5352
7135
|
/**
|
|
5353
7136
|
* How far ahead a provider-published reset may be and still be trusted.
|
|
5354
7137
|
*
|
|
@@ -5376,20 +7159,10 @@ const MAX_TRUSTED_RESET_MS = 2592e6;
|
|
|
5376
7159
|
* message is honored as the fallback. Either can be present without the other;
|
|
5377
7160
|
* the window label is what decides the classification.
|
|
5378
7161
|
*/
|
|
5379
|
-
function readWindowLimitEvidence(errText) {
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
try {
|
|
5384
|
-
const parsed = JSON.parse(errText);
|
|
5385
|
-
if (isRecord(parsed)) {
|
|
5386
|
-
const error = isRecord(parsed.error) ? parsed.error : parsed;
|
|
5387
|
-
const rateLimit = isRecord(error.rateLimit) ? error.rateLimit : void 0;
|
|
5388
|
-
named = stringValue(rateLimit?.window);
|
|
5389
|
-
message = stringValue(error.message) ?? stringValue(parsed.message) ?? "";
|
|
5390
|
-
seconds = numberValue(rateLimit?.reset);
|
|
5391
|
-
}
|
|
5392
|
-
} catch {}
|
|
7162
|
+
function readWindowLimitEvidence(errText, parsed) {
|
|
7163
|
+
const named = stringValue(parsed.rateLimit?.window);
|
|
7164
|
+
const message = parsed.message ?? stringValue(parsed.root?.message) ?? "";
|
|
7165
|
+
const seconds = numberValue(parsed.rateLimit?.reset);
|
|
5393
7166
|
const window = named === "fiveHour" || named === "weekly" || named === "daily" ? named : /usage limit for your plan/i.test(message) || /usage limit for your plan/i.test(errText) ? /weekly/i.test(message) ? "weekly" : "fiveHour" : void 0;
|
|
5394
7167
|
const stamp = /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(message)?.[1] ?? /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(errText)?.[1];
|
|
5395
7168
|
const stamped = stamp === void 0 ? void 0 : Date.parse(stamp);
|
|
@@ -5407,24 +7180,15 @@ function readWindowLimitEvidence(errText) {
|
|
|
5407
7180
|
* other 4xx/5xx must surface as ordinary errors so real account/model
|
|
5408
7181
|
* problems are not masked.
|
|
5409
7182
|
*/
|
|
5410
|
-
function isUpgradeRequiredError(status, errText) {
|
|
7183
|
+
function isUpgradeRequiredError(status, errText, parsed = parseProviderError(errText)) {
|
|
5411
7184
|
if (status !== 403) return false;
|
|
5412
7185
|
const lower = errText.toLowerCase();
|
|
5413
7186
|
if (lower.includes("upgrade_required")) return true;
|
|
5414
7187
|
if (lower.includes("go plan") && lower.includes("api access")) return true;
|
|
5415
7188
|
if (lower.includes("only plan without api access")) return true;
|
|
5416
7189
|
if (lower.includes("upgrade to goat or higher")) return true;
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
if (isRecord(parsed)) {
|
|
5420
|
-
const error = isRecord(parsed.error) ? parsed.error : parsed;
|
|
5421
|
-
if ((stringValue(error.code) ?? stringValue(error.type))?.toLowerCase() === "upgrade_required") return true;
|
|
5422
|
-
const message = stringValue(error.message) ?? "";
|
|
5423
|
-
if (message.toLowerCase().includes("go plan") && message.toLowerCase().includes("api access")) return true;
|
|
5424
|
-
}
|
|
5425
|
-
} catch {
|
|
5426
|
-
return false;
|
|
5427
|
-
}
|
|
7190
|
+
if ((parsed.code ?? parsed.type)?.toLowerCase() === "upgrade_required") return true;
|
|
7191
|
+
if (parsed.message?.toLowerCase().includes("go plan") && parsed.message.toLowerCase().includes("api access")) return true;
|
|
5428
7192
|
return false;
|
|
5429
7193
|
}
|
|
5430
7194
|
/** Map an OpenAI Chat Completions usage payload to harness disjoint counts. */
|
|
@@ -5532,33 +7296,42 @@ function commandCopy(locale, key) {
|
|
|
5532
7296
|
return commandcodeCommand[locale][key] ?? commandcodeCommand.en[key] ?? key;
|
|
5533
7297
|
}
|
|
5534
7298
|
//#endregion
|
|
5535
|
-
//#region src/
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
}
|
|
5539
|
-
/** Compact dollar amount (2 decimals), for the summary and window rows. */
|
|
5540
|
-
function moneyShort(value) {
|
|
7299
|
+
//#region src/display-format.ts
|
|
7300
|
+
/** Shared, dependency-free formatting for Host commands and browser surfaces. */
|
|
7301
|
+
function formatMoney(value) {
|
|
5541
7302
|
return `$${value.toFixed(2)}`;
|
|
5542
7303
|
}
|
|
5543
|
-
|
|
5544
|
-
|
|
7304
|
+
function formatMoneyExact(value) {
|
|
7305
|
+
return `$${value.toFixed(4)}`;
|
|
7306
|
+
}
|
|
7307
|
+
function formatTokensCompact(value) {
|
|
5545
7308
|
if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
|
|
5546
7309
|
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
5547
7310
|
if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
5548
7311
|
return String(value);
|
|
5549
7312
|
}
|
|
5550
|
-
/**
|
|
5551
|
-
|
|
5552
|
-
* trailing zeros trimmed — mirrors `formatSuccessRate` in
|
|
5553
|
-
* `./client/usage.ts`, which the settings card uses.
|
|
5554
|
-
*/
|
|
5555
|
-
function successRateText(value) {
|
|
7313
|
+
/** The input is already in percent units; callers add the percent sign. */
|
|
7314
|
+
function formatSuccessRate(value) {
|
|
5556
7315
|
return String(Number(value.toFixed(2)));
|
|
5557
7316
|
}
|
|
7317
|
+
/** One window's fill ratio in [0, 1]; zero when uncapped. */
|
|
7318
|
+
function windowRatio(used, cap) {
|
|
7319
|
+
if (cap <= 0) return 0;
|
|
7320
|
+
return Math.max(0, Math.min(1, used / cap));
|
|
7321
|
+
}
|
|
7322
|
+
/** Local date-time, with the empty state chosen by the caller. */
|
|
7323
|
+
function formatResetAt(ms) {
|
|
7324
|
+
return ms <= 0 ? "" : new Date(ms).toLocaleString();
|
|
7325
|
+
}
|
|
7326
|
+
//#endregion
|
|
7327
|
+
//#region src/commands.ts
|
|
7328
|
+
const money = formatMoneyExact;
|
|
7329
|
+
const moneyShort = formatMoney;
|
|
7330
|
+
const tokensCompact = formatTokensCompact;
|
|
7331
|
+
const successRateText = formatSuccessRate;
|
|
5558
7332
|
/** A local date, or `n/a` when the provider published no reset time. */
|
|
5559
7333
|
function resetLabel(ms) {
|
|
5560
|
-
|
|
5561
|
-
return new Date(ms).toLocaleString();
|
|
7334
|
+
return formatResetAt(ms) || "n/a";
|
|
5562
7335
|
}
|
|
5563
7336
|
/**
|
|
5564
7337
|
* A 10-cell horizontal bar: `██████████` for 100%, `███░░░░░░░` for ~33%.
|
|
@@ -5566,7 +7339,7 @@ function resetLabel(ms) {
|
|
|
5566
7339
|
*/
|
|
5567
7340
|
function bar(used, cap) {
|
|
5568
7341
|
if (cap <= 0) return "—";
|
|
5569
|
-
const ratio =
|
|
7342
|
+
const ratio = windowRatio(used, cap);
|
|
5570
7343
|
const filled = Math.round(ratio * 10);
|
|
5571
7344
|
return "█".repeat(filled) + "░".repeat(10 - filled);
|
|
5572
7345
|
}
|
|
@@ -5849,7 +7622,7 @@ const MODELS_ENDPOINT = "commandcode/models";
|
|
|
5849
7622
|
* separate instance so catalog boundary errors name `commandcode/models`,
|
|
5850
7623
|
* not the report endpoint.
|
|
5851
7624
|
*/
|
|
5852
|
-
const { record: catalogRecord, stringField: catalogString } = makeBoundaryValidator("commandcode/models result:");
|
|
7625
|
+
const { record: catalogRecord, stringField: catalogString, numberField: catalogNumber } = makeBoundaryValidator("commandcode/models result:");
|
|
5853
7626
|
/** Parse one untrusted boundary value into a {@link CommandCodeCatalogModel}. */
|
|
5854
7627
|
function parseCatalogModel(value) {
|
|
5855
7628
|
const source = catalogRecord(value, "model");
|
|
@@ -5858,6 +7631,7 @@ function parseCatalogModel(value) {
|
|
|
5858
7631
|
name: catalogString(source, "name", "model.name")
|
|
5859
7632
|
};
|
|
5860
7633
|
if (source.tier !== void 0) model.tier = catalogString(source, "tier", "model.tier");
|
|
7634
|
+
if (source.allowance !== void 0) model.allowance = catalogNumber(source, "allowance", "model.allowance");
|
|
5861
7635
|
return model;
|
|
5862
7636
|
}
|
|
5863
7637
|
/** Parse the wire result into a {@link CommandCodeCatalog}. */
|
|
@@ -5999,12 +7773,17 @@ function loginDescriptor(endpoint, method) {
|
|
|
5999
7773
|
}]
|
|
6000
7774
|
} : descriptor;
|
|
6001
7775
|
}
|
|
6002
|
-
/** The
|
|
6003
|
-
const
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
7776
|
+
/** The Host-face contribution fragment registered on `ctx.typert`. */
|
|
7777
|
+
const LOGIN_HOST_CONTRIBUTION = {
|
|
7778
|
+
package: REMOTE_PACKAGE,
|
|
7779
|
+
face: "host",
|
|
7780
|
+
schemas: [],
|
|
7781
|
+
invocations: [
|
|
7782
|
+
loginDescriptor(LOGIN_BEGIN_ENDPOINT, "loginBegin"),
|
|
7783
|
+
loginDescriptor(LOGIN_STATUS_ENDPOINT, "loginStatus"),
|
|
7784
|
+
loginDescriptor(LOGIN_CANCEL_ENDPOINT, "loginCancel")
|
|
7785
|
+
]
|
|
7786
|
+
};
|
|
6008
7787
|
//#endregion
|
|
6009
7788
|
//#region src/usage-remote.ts
|
|
6010
7789
|
/**
|
|
@@ -6093,7 +7872,7 @@ function applyUsageRemote(ctx, deps) {
|
|
|
6093
7872
|
...USAGE_HOST_CONTRIBUTION.invocations,
|
|
6094
7873
|
MODELS_DESCRIPTOR,
|
|
6095
7874
|
PRICES_DESCRIPTOR,
|
|
6096
|
-
...
|
|
7875
|
+
...LOGIN_HOST_CONTRIBUTION.invocations
|
|
6097
7876
|
]
|
|
6098
7877
|
});
|
|
6099
7878
|
remoteCtx.effect(() => () => void unregister(), "dsh-commandcode-provider: usage remote");
|
|
@@ -6810,22 +8589,8 @@ var CommandCodeSearchProvider = class {
|
|
|
6810
8589
|
const ACTIVE_ACCOUNT_AUTO = "auto";
|
|
6811
8590
|
/** The selector value meaning "no language override — follow the shell locale". */
|
|
6812
8591
|
const LANG_AUTO = "auto";
|
|
6813
|
-
/** Tier display names, mirroring the web dropdown's headings (`model-select.ts`). */
|
|
6814
|
-
const TIER_TITLES = {
|
|
6815
|
-
go: "Go",
|
|
6816
|
-
goat: "GOAT",
|
|
6817
|
-
pro: "Pro",
|
|
6818
|
-
provider: "Provider",
|
|
6819
|
-
max: "Max"
|
|
6820
|
-
};
|
|
6821
8592
|
/** Tiers in picker order; a model outside this set joins the "Other" group. */
|
|
6822
|
-
const TIER_ORDER =
|
|
6823
|
-
"go",
|
|
6824
|
-
"goat",
|
|
6825
|
-
"pro",
|
|
6826
|
-
"provider",
|
|
6827
|
-
"max"
|
|
6828
|
-
];
|
|
8593
|
+
const TIER_ORDER = PLAN_TIER_ORDER;
|
|
6829
8594
|
/** The group holding models this build's catalog does not know. */
|
|
6830
8595
|
const OTHER_GROUP_ID = "models-other";
|
|
6831
8596
|
/**
|
|
@@ -6883,8 +8648,8 @@ function buildCommandCodeTuiSection(deps) {
|
|
|
6883
8648
|
const extras = [.../* @__PURE__ */ new Set([...stored, ...Object.keys(overrides)])].filter((id) => !known.has(id));
|
|
6884
8649
|
const tierGroups = TIER_ORDER.filter((tier) => choices.some((choice) => choice.tier === tier)).map((tier) => ({
|
|
6885
8650
|
id: `models-${tier}`,
|
|
6886
|
-
title: `${
|
|
6887
|
-
descriptions: { zh: `${
|
|
8651
|
+
title: `${PLAN_LABELS[tier] ?? tier} models`,
|
|
8652
|
+
descriptions: { zh: `${PLAN_LABELS[tier] ?? tier} 模型` }
|
|
6888
8653
|
}));
|
|
6889
8654
|
const unranked = choices.filter((choice) => tierRank(choice.tier) === TIER_ORDER.length);
|
|
6890
8655
|
const otherGroup = unranked.length > 0 || extras.length > 0 ? [{
|
|
@@ -6989,6 +8754,20 @@ function buildCommandCodeTuiSection(deps) {
|
|
|
6989
8754
|
value: text.trim() === "true"
|
|
6990
8755
|
})
|
|
6991
8756
|
},
|
|
8757
|
+
{
|
|
8758
|
+
path: ["offloadSeenImagesForCache"],
|
|
8759
|
+
group: "advanced",
|
|
8760
|
+
kind: "boolean",
|
|
8761
|
+
label: "Stop replaying images already seen",
|
|
8762
|
+
descriptions: { zh: "已看过的图片不再重复发送" },
|
|
8763
|
+
hint: "CLI transport only. After this model has answered, durably replace older images with text to keep later prompt text cacheable. Old pixels need a fresh file read or attachment to inspect again. Off by default.",
|
|
8764
|
+
hintDescriptions: { zh: "仅 CLI 传输。模型看过图片并回复后,永久用文字替代旧图,以保留后续文本缓存;重看旧图需要重新读取文件或附加。默认关闭。" },
|
|
8765
|
+
format: (value) => value === true ? "true" : "false",
|
|
8766
|
+
parse: (text) => ({
|
|
8767
|
+
kind: "set",
|
|
8768
|
+
value: text.trim() === "true"
|
|
8769
|
+
})
|
|
8770
|
+
},
|
|
6992
8771
|
{
|
|
6993
8772
|
path: ["filterModelsByPlan"],
|
|
6994
8773
|
group: "models",
|
|
@@ -7149,27 +8928,18 @@ function applyCommandCodeTuiSettings(ctx, deps) {
|
|
|
7149
8928
|
* A bounded retry budget for `TRANSPORT` failures (issue #39, second report).
|
|
7150
8929
|
*
|
|
7151
8930
|
* The route policy (`providerRetryPolicy` in `./adapter.ts`) is near-unbounded
|
|
7152
|
-
* on purpose — 1000 attempts doubling to a 15-minute cap —
|
|
7153
|
-
*
|
|
7154
|
-
*
|
|
7155
|
-
*
|
|
7156
|
-
*
|
|
7157
|
-
*
|
|
7158
|
-
*
|
|
7159
|
-
*
|
|
7160
|
-
*
|
|
7161
|
-
*
|
|
7162
|
-
* Provider-API → CLI switch) — invisible to
|
|
7163
|
-
*
|
|
7164
|
-
*
|
|
7165
|
-
* Reset rule: the budget clears at each new STEP (one step = one model request).
|
|
7166
|
-
* `agent/status` → `idle` is NOT it, though it looks natural: the loop's
|
|
7167
|
-
* `setPhase` emits it only on a status CHANGE, and a turn's steps all run inside
|
|
7168
|
-
* one `running` phase, so a reset there would fail every later step of an outage
|
|
7169
|
-
* turn at once instead of retrying each. `assistant/attempt` is wrong the other
|
|
7170
|
-
* way — the loop appends it for the FAILED attempt itself, before dispatching
|
|
7171
|
-
* `agent/request-error`, so resetting on it would clear the budget on every
|
|
7172
|
-
* failure and restore the very loop this module exists to stop.
|
|
8931
|
+
* on purpose — 1000 attempts doubling to a 15-minute cap — for failures the
|
|
8932
|
+
* provider ASKS to have retried. A connection that cannot be established is
|
|
8933
|
+
* not that: a blip the first few attempts absorb, or an outage no in-request
|
|
8934
|
+
* waiting fixes (attempt 11 alone already waits 512 s). So transport failures
|
|
8935
|
+
* get their own budget, cleared at each new STEP (one step = one model
|
|
8936
|
+
* request). `agent/status` → `idle` is NOT the reset (one `running` phase spans
|
|
8937
|
+
* a turn's steps), and neither is `assistant/attempt` (appended for the FAILED
|
|
8938
|
+
* attempt itself, which would clear the budget on every failure).
|
|
8939
|
+
*
|
|
8940
|
+
* Not counted here: the adapter's in-`stream()` pre-stream recovery (account
|
|
8941
|
+
* rotation, the 413 step-down, the Provider-API → CLI switch) — invisible to
|
|
8942
|
+
* the caller and bounded on its own.
|
|
7173
8943
|
*
|
|
7174
8944
|
* @module
|
|
7175
8945
|
*/
|
|
@@ -7251,7 +9021,7 @@ function transportBudgetMessage(failureMessage, maxRetries) {
|
|
|
7251
9021
|
* Mark one schema field volatile.
|
|
7252
9022
|
*
|
|
7253
9023
|
* `.volatile()` is unconditional: it is a schemastery 3.18.3 feature and the
|
|
7254
|
-
* only supported engine (`dsh 0.
|
|
9024
|
+
* only supported engine (`dsh 0.2.0-rc.2`) pins `~3.18.4`, so the method is
|
|
7255
9025
|
* always present. It is reached through a structural member so the caller's
|
|
7256
9026
|
* declared field type survives — the mark changes what the schema PARSES to (a
|
|
7257
9027
|
* live reference), not the plugin-facing `Config` shape.
|
|
@@ -7348,6 +9118,7 @@ const Config = z.object(markVolatileFields({
|
|
|
7348
9118
|
modelsCachePath: z.string(),
|
|
7349
9119
|
requestTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
|
|
7350
9120
|
streamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
|
|
9121
|
+
offloadSeenImagesForCache: z.boolean().default(false),
|
|
7351
9122
|
transportMaxRetries: z.number().min(0).max(50),
|
|
7352
9123
|
filterModelsByPlan: z.boolean(),
|
|
7353
9124
|
visibleModels: z.array(z.string()),
|
|
@@ -7380,8 +9151,9 @@ function resolveAdapterOptions(config) {
|
|
|
7380
9151
|
apiBase: config.apiBase ?? "https://api.commandcode.ai",
|
|
7381
9152
|
workingDir: config.workingDir ?? process.cwd(),
|
|
7382
9153
|
modelsCachePath: config.modelsCachePath ?? DEFAULT_MODELS_CACHE_PATH,
|
|
7383
|
-
requestTimeoutMs: config.requestTimeoutMs ??
|
|
9154
|
+
requestTimeoutMs: config.requestTimeoutMs ?? 3e5,
|
|
7384
9155
|
streamIdleTimeoutMs: config.streamIdleTimeoutMs ?? 3e5,
|
|
9156
|
+
offloadSeenImagesForCache: config.offloadSeenImagesForCache === true,
|
|
7385
9157
|
filterModelsByPlan: config.filterModelsByPlan ?? true,
|
|
7386
9158
|
visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : void 0,
|
|
7387
9159
|
modelVisibility: readModelVisibility(config.modelVisibility),
|
|
@@ -7483,10 +9255,13 @@ function apply(ctx, config) {
|
|
|
7483
9255
|
return Math.min(Math.trunc(value), 50);
|
|
7484
9256
|
};
|
|
7485
9257
|
const sessionAgents = /* @__PURE__ */ new WeakMap();
|
|
7486
|
-
ctx.on("agent/request-error", async ({ agent, failure, signal }, next) => {
|
|
7487
|
-
if (signal.aborted) return next();
|
|
9258
|
+
ctx.on("agent/request-error", async ({ agent, provider, failure, signal }, next) => {
|
|
9259
|
+
if (signal.aborted || provider !== "commandcode") return next();
|
|
7488
9260
|
const session = Reflect.get(agent, "session");
|
|
7489
9261
|
if (typeof session === "object" && session !== null) sessionAgents.set(session, agent);
|
|
9262
|
+
const transient = absorbTransientFailure(agent, failure.code);
|
|
9263
|
+
if (transient === "exhausted") throw new Error(transientBudgetMessage(failure.code, failure.message));
|
|
9264
|
+
if (transient === "retry") return next();
|
|
7490
9265
|
const decision = absorbTransportFailure(agent, failure.code, transportMaxRetries());
|
|
7491
9266
|
if (decision === "ignored") return next();
|
|
7492
9267
|
if (decision === "retry") return { kind: "retry" };
|
|
@@ -7502,10 +9277,16 @@ function apply(ctx, config) {
|
|
|
7502
9277
|
}
|
|
7503
9278
|
if (action !== "reset") return;
|
|
7504
9279
|
const agent = sessionAgents.get(session);
|
|
7505
|
-
if (agent !== void 0)
|
|
9280
|
+
if (agent !== void 0) {
|
|
9281
|
+
resetTransportFailures(agent);
|
|
9282
|
+
resetTransientFailures(agent);
|
|
9283
|
+
}
|
|
7506
9284
|
});
|
|
7507
9285
|
ctx.on("agent/status", ({ agent, status }) => {
|
|
7508
|
-
if (status === "idle")
|
|
9286
|
+
if (status === "idle") {
|
|
9287
|
+
resetTransportFailures(agent);
|
|
9288
|
+
resetTransientFailures(agent);
|
|
9289
|
+
}
|
|
7509
9290
|
});
|
|
7510
9291
|
const usageReports = async () => {
|
|
7511
9292
|
const described = await pool.describeAccounts();
|
|
@@ -7555,12 +9336,16 @@ function apply(ctx, config) {
|
|
|
7555
9336
|
});
|
|
7556
9337
|
ctx.effect(() => () => loginFlow.dispose(), "dsh-commandcode-provider: login flow");
|
|
7557
9338
|
const catalogForEditors = async () => {
|
|
7558
|
-
|
|
9339
|
+
const models = await adapter.listModels(PROVIDER, { unfiltered: true });
|
|
9340
|
+
const bracket = await adapter.allowanceTier();
|
|
9341
|
+
return { models: models.map((model) => {
|
|
7559
9342
|
const tier = KNOWN_PLANS[model.id];
|
|
9343
|
+
const allowance = bracket === void 0 ? void 0 : modelAllowanceFor(model.id)?.[bracket];
|
|
7560
9344
|
return {
|
|
7561
9345
|
id: model.id,
|
|
7562
9346
|
name: model.name.replace(/\s*\(CC\)$/, ""),
|
|
7563
|
-
...tier === void 0 ? {} : { tier }
|
|
9347
|
+
...tier === void 0 ? {} : { tier },
|
|
9348
|
+
...allowance === void 0 ? {} : { allowance }
|
|
7564
9349
|
};
|
|
7565
9350
|
}) };
|
|
7566
9351
|
};
|