@mars-sea/dsh-commandcode-provider 0.11.16 → 0.11.17
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 +20 -0
- package/README.md +2 -20
- package/README.zh-CN.md +2 -20
- package/lib/client.js +340 -654
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +294 -471
- package/lib/index.js +741 -1664
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -2,12 +2,12 @@ import { homedir, tmpdir } from "node:os";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
5
|
-
import { CONTEXT_WINDOW_EXCEEDED_CODE, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, assertUsableApiKey, attributionHeaders, errorChain, isContextWindowExceededError, offloadedImageText, projectOffloadedImages, requiredImageOffload, resolveRetryPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, ToolCallId, assertUsableApiKey, attributionHeaders, errorChain, isContextWindowExceededError, offloadedImageText, projectOffloadedImages, requiredImageOffload, resolveRetryPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
|
|
6
6
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
7
7
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
8
8
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
9
9
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
10
|
-
import { randomBytes, randomUUID } from "node:crypto";
|
|
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";
|
|
@@ -18,18 +18,12 @@ import { WebError } from "@deepseek-ai/dsh-web";
|
|
|
18
18
|
* reasoning-effort levels, vision/thinking flags, model → minimum plan tier,
|
|
19
19
|
* subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
|
|
20
20
|
*
|
|
21
|
-
* Everything
|
|
22
|
-
*
|
|
23
|
-
* dsh-commandcode-upstream skill
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* these tables + read helpers.
|
|
28
|
-
*
|
|
29
|
-
* Snapshot read helpers (planLabel, dealLabel, formatContext,
|
|
30
|
-
* capabilityDescription, peakPricing*, compareByPlan, modelVisibleInPlan,
|
|
31
|
-
* subscriptionPlanInfo, isFreeModel) live here too — they exist only to read
|
|
32
|
-
* the tables, so a sync never has to touch src/adapter.ts.
|
|
21
|
+
* 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.66.0) and
|
|
23
|
+
* the official plan/pricing/model docs; see the dsh-commandcode-upstream skill
|
|
24
|
+
* for the extraction procedures. Keeping the snapshot in its own module
|
|
25
|
+
* confines those frequent sync diffs here: src/adapter.ts holds only the stable
|
|
26
|
+
* wire/runtime logic. The read helpers live here too, for the same reason.
|
|
33
27
|
*
|
|
34
28
|
* Ported from pi-commandcode-provider (MIT); originally part of src/adapter.ts
|
|
35
29
|
* and split out so upstream syncs stay reviewable.
|
|
@@ -313,6 +307,11 @@ const KNOWN_EFFORTS = {
|
|
|
313
307
|
"low",
|
|
314
308
|
"medium",
|
|
315
309
|
"high"
|
|
310
|
+
],
|
|
311
|
+
"stealth/pixel-canary": [
|
|
312
|
+
"low",
|
|
313
|
+
"medium",
|
|
314
|
+
"xhigh"
|
|
316
315
|
]
|
|
317
316
|
};
|
|
318
317
|
/**
|
|
@@ -378,6 +377,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
378
377
|
"moonshotai/Kimi-K2.7-Code-Highspeed",
|
|
379
378
|
"moonshotai/Kimi-K3",
|
|
380
379
|
"sakana/fugu-ultra",
|
|
380
|
+
"stealth/pixel-canary",
|
|
381
381
|
"stealth/space-bunny-alpha",
|
|
382
382
|
"stepfun/Step-3.7-Flash",
|
|
383
383
|
"stepfun/Step-5-Preview",
|
|
@@ -395,20 +395,22 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
395
395
|
]);
|
|
396
396
|
/**
|
|
397
397
|
* Models WITHOUT a zero-data-retention upstream, per the official CLI's own
|
|
398
|
-
* registry (`command-code@1.
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
398
|
+
* registry (`command-code@1.66.0` `dist/cli.mjs`): `modelSupportsZdr(id)` is
|
|
399
|
+
* exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
|
|
400
|
+
* carries the same membership in the sibling route table — the UNION of both
|
|
401
|
+
* is this set. Reading only the sibling route table would drop `meituan/
|
|
402
|
+
* LongCat-2.0` and `stealth/pixel-canary`, which each appear in
|
|
403
|
+
* `modelSupportsZdr` alone. The official docs (commandcode.ai/docs/resources/
|
|
404
|
+
* zdr) put it in prose — "99% of our models have ZDR-capable upstreams … only
|
|
405
|
+
* a small handful of models are affected" — so the CLI's exclusion list is
|
|
406
|
+
* the only per-model evidence there is; a ZDR request naming one of these
|
|
407
|
+
* fails with HTTP 422 `cmd_zdr_no_providers` instead of routing through a
|
|
408
|
+
* provider that retains.
|
|
407
409
|
*
|
|
408
410
|
* Why a NEGATIVE set, and why "not listed" answers TRUE: 99% of the catalog is
|
|
409
411
|
* covered, so the maintained difference is the exception list. This helper is
|
|
410
412
|
* informational; the adapter sends the ZDR header for EVERY request when the
|
|
411
|
-
* switch is on
|
|
413
|
+
* switch is on — the provider remains the routing authority and refuses an
|
|
412
414
|
* unsupported model rather than silently dropping the privacy guarantee.
|
|
413
415
|
*
|
|
414
416
|
* `minimax/minimax-m3-free` is the one entry the public catalog
|
|
@@ -419,16 +421,9 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
419
421
|
* Keep in sync via the dsh-commandcode-upstream skill: the CLI's registry data
|
|
420
422
|
* (its `zdr:{only:[…]}` provider routes and the per-provider `zdr`/`noTraining`
|
|
421
423
|
* flags) is upstream-internal routing, not a per-model contract, so this table
|
|
422
|
-
* is the snapshot of the exclusion set and nothing more.
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* how rare a change here is expected to be. The 1.64.0 → 1.65.0 diff (2026-09-24)
|
|
426
|
-
* is the counterexample that proves the check still runs: exactly one member
|
|
427
|
-
* joins (`stealth/space-bunny-alpha`), and it is in both anchors. `meituan/
|
|
428
|
-
* LongCat-2.0` is the one member the two anchors disagree about (it is in
|
|
429
|
-
* `modelSupportsZdr`'s set in both releases and in neither `knownModelSupportsZdr`
|
|
430
|
-
* set), so the union is
|
|
431
|
-
* what this table follows; reading only the sibling route table would drop it.
|
|
424
|
+
* 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, and 1.65.0 and 1.66.0 each added
|
|
426
|
+
* exactly one (the two stealth-preview models below).
|
|
432
427
|
*/
|
|
433
428
|
const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
|
|
434
429
|
"MiniMaxAI/MiniMax-M3",
|
|
@@ -443,6 +438,7 @@ const KNOWN_NON_ZDR_MODELS = /* @__PURE__ */ new Set([
|
|
|
443
438
|
"poolside/laguna-s-2.1-free",
|
|
444
439
|
"sakana/fugu-ultra",
|
|
445
440
|
"stealth/space-bunny-alpha",
|
|
441
|
+
"stealth/pixel-canary",
|
|
446
442
|
"stepfun/Step-3.7-Flash",
|
|
447
443
|
"stepfun/Step-5-Preview",
|
|
448
444
|
"xai/grok-4.5",
|
|
@@ -462,36 +458,18 @@ function supportsZeroDataRetention(modelId) {
|
|
|
462
458
|
return !KNOWN_NON_ZDR_MODELS.has(modelId);
|
|
463
459
|
}
|
|
464
460
|
/**
|
|
465
|
-
* Models the official CLI's model table
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
* description — it exists for programmatic consumers.
|
|
461
|
+
* Models the official CLI's model table marks `reasoning:!0` but defines no
|
|
462
|
+
* selectable `reasoning_effort` levels — they think automatically, with
|
|
463
|
+
* Command Code driving the depth. `KNOWN_EFFORTS` (which mirrors the CLI's
|
|
464
|
+
* effort map exactly) stays the sole source for selectable effort levels, and
|
|
465
|
+
* this snapshot is not surfaced in the picker's compact description — it exists
|
|
466
|
+
* for programmatic consumers.
|
|
472
467
|
*
|
|
473
|
-
* Source: the
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
* joined this set in command-code@1.37.0 — reasoning:!0, no efforts, 1M
|
|
479
|
-
* context, routed through OpenRouter — then gained selectable
|
|
480
|
-
* `['low', 'medium', 'high']` efforts in command-code@1.38.0 and moved to
|
|
481
|
-
* `KNOWN_EFFORTS`. `moonshotai/Kimi-K3` followed the same path in
|
|
482
|
-
* command-code@1.39.3 — it gained `['low', 'high', 'max']` efforts and moved
|
|
483
|
-
* to `KNOWN_EFFORTS`. command-code@1.42.0 added LongCat 2.0 (then
|
|
484
|
-
* `meituan/LongCat-2.0:free`, now the paid `meituan/LongCat-2.0`; reasoning:!0,
|
|
485
|
-
* no efforts). command-code@1.45.0 gave the Muse Spark family
|
|
486
|
-
* (1.1, 1.2, 1.2-contributor, 1.3, 1.3-contributor) selectable
|
|
487
|
-
* `['low', 'medium', 'high', 'xhigh']` efforts — they moved to `KNOWN_EFFORTS`.
|
|
488
|
-
* command-code@1.51.3 gave `MiniMaxAI/MiniMax-M3` selectable
|
|
489
|
-
* `['low', 'medium', 'high']` efforts — it moved to `KNOWN_EFFORTS` too.
|
|
490
|
-
* command-code@1.52.0 added `inclusionai/ling-3.0-flash-sante:free`
|
|
491
|
-
* (reasoning:!0, no efforts).)
|
|
492
|
-
* command-code@1.53.0 added `deepseek/deepseek-v4.1-flash` with selectable
|
|
493
|
-
* ['low', 'high', 'max'] efforts, so it lives in `KNOWN_EFFORTS`, not here.)
|
|
494
|
-
* Keep in sync via the dsh-commandcode-upstream skill.
|
|
468
|
+
* Source: the bundled model table (dist/cli.mjs), cross-checked with
|
|
469
|
+
* https://commandcode.ai/docs/reference/cli/models. Keep in sync via the
|
|
470
|
+
* dsh-commandcode-upstream skill; a model that GAINS selectable efforts leaves
|
|
471
|
+
* this set for `KNOWN_EFFORTS` (Tencent Hy4 Preview, Kimi K3, the Muse Spark
|
|
472
|
+
* family and MiniMax M3 all took that path).
|
|
495
473
|
*/
|
|
496
474
|
const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
|
|
497
475
|
"Qwen/Qwen3.6-Max-Preview",
|
|
@@ -520,7 +498,7 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
|
|
|
520
498
|
*
|
|
521
499
|
* Measured 2026-09-16 against the live catalog (command-code@1.54.0): all 69
|
|
522
500
|
* models were posted to `/provider/v1/chat/completions`; exactly these eight
|
|
523
|
-
* — the whole Claude family — refused, and every one of them is routed
|
|
501
|
+
* — the whole Claude family then — refused, and every one of them is routed
|
|
524
502
|
* normally by `/alpha/generate` (a lower-plan key gets the ordinary
|
|
525
503
|
* `MODEL_NOT_IN_PLAN` 403 there, never a routing error). So the CLI transport
|
|
526
504
|
* is a complete fallback and the adapter does not need a Messages transport.
|
|
@@ -531,13 +509,15 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
|
|
|
531
509
|
* the Provider API by default — picking any Claude model there failed every
|
|
532
510
|
* request before this snapshot existed.
|
|
533
511
|
*
|
|
534
|
-
* Keep in sync when models ship (see the dsh-commandcode-upstream skill).
|
|
535
512
|
* `requiresMessagesEndpoint()` additionally treats any `claude-*` id as
|
|
536
|
-
* Messages-only, so a Claude model added upstream
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
513
|
+
* Messages-only, so a Claude model added upstream (`claude-opus-5-5` in
|
|
514
|
+
* command-code@1.64.0 was the first this snapshot missed) is still routed
|
|
515
|
+
* correctly by an un-updated plugin instead of hard-failing with the 400
|
|
516
|
+
* above. The worst case of that rule going stale the other way (upstream
|
|
517
|
+
* teaching `/provider/v1/chat/completions` to serve Claude) is one model
|
|
518
|
+
* riding the CLI transport it already works on.
|
|
519
|
+
*
|
|
520
|
+
* Keep in sync when models ship (see the dsh-commandcode-upstream skill).
|
|
541
521
|
*/
|
|
542
522
|
const MESSAGES_ONLY_MODELS = /* @__PURE__ */ new Set([
|
|
543
523
|
"claude-sonnet-5",
|
|
@@ -555,31 +535,14 @@ function requiresMessagesEndpoint(modelId) {
|
|
|
555
535
|
}
|
|
556
536
|
/**
|
|
557
537
|
* The minimum subscription plan a model is included in, per the official plan
|
|
558
|
-
* pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`,
|
|
559
|
-
* and `/docs/resources/pricing-limits`). Each plan's model
|
|
560
|
-
* the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
|
|
561
|
-
* plan list (Claude Opus/Fable, Fugu Ultra) are
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
* command-code@1.41.0 added `Qwen/Qwen3.8-Max-0902` (Go) and 1.42.0 added
|
|
567
|
-
* LongCat 2.0 (Go — a free promo until 2026-09-19, when the backend renamed
|
|
568
|
-
* `meituan/LongCat-2.0:free` to the paid `meituan/LongCat-2.0`); 1.43.0 added
|
|
569
|
-
* `google/gemini-3.8-flash` (GOAT) and 1.44.0 added `meta/muse-spark-1.3`
|
|
570
|
-
* (GOAT) plus its Contributor sibling (Go); command-code@1.52.0 added the
|
|
571
|
-
* free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
|
|
572
|
-
* added `deepseek/deepseek-v4.1-flash` (Go); command-code@1.56.0 added
|
|
573
|
-
* `Qwen/Qwen3.8-Omni-Flash` (Go); command-code@1.57.0 added
|
|
574
|
-
* `z-ai/glm-5.3-flashx` (Go); command-code@1.59.0 added `xai/grok-4.7` (GOAT)
|
|
575
|
-
* and 1.60.0 added `stepfun/Step-5-Preview` (Go); command-code@1.62.0 added the
|
|
576
|
-
* MiMo V2.6 family — `xiaomi/mimo-v2.6-flash` + `xiaomi/mimo-v2.6-pro` (Go) and
|
|
577
|
-
* `xiaomi/mimo-v2.6-pro-ultraspeed` (GOAT) — so the 1.58.0 -> 1.62.0 window's
|
|
578
|
-
* only tier changes are additions and the superset chain still holds. The
|
|
579
|
-
* 1.62.0 -> 1.65.0 window continues the pattern: 1.64.0 added
|
|
580
|
-
* `claude-opus-5-5` (Provider/Max), `gpt-6-sol` (Pro) and `gpt-6-luna` (Go),
|
|
581
|
-
* and 1.65.0 added `stealth/space-bunny-alpha` (Go, every tier) — additions
|
|
582
|
-
* only, no tier moves.
|
|
538
|
+
* pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`,
|
|
539
|
+
* `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
|
|
540
|
+
* list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
|
|
541
|
+
* Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
|
|
542
|
+
* Provider-tier. Re-verified at command-code@1.66.0 (2026-09-27): 82 catalog
|
|
543
|
+
* ids at 52/60/74/82 cumulative, a strict superset chain — every release since
|
|
544
|
+
* 1.49.0 has been additive with no tier move, and per-entry tags below name the
|
|
545
|
+
* release that added each row.
|
|
583
546
|
*
|
|
584
547
|
* The Provider API exposes no plan metadata, so this snapshot is the source of
|
|
585
548
|
* truth for the picker's plan annotation — it answers "which plan do I need to
|
|
@@ -624,6 +587,7 @@ const KNOWN_PLANS = {
|
|
|
624
587
|
"stepfun/Step-3.5-Flash": "go",
|
|
625
588
|
"stepfun/Step-3.7-Flash": "go",
|
|
626
589
|
"stepfun/Step-5-Preview": "go",
|
|
590
|
+
"stealth/pixel-canary": "go",
|
|
627
591
|
"stealth/space-bunny-alpha": "go",
|
|
628
592
|
"tencent/hy3-paid": "go",
|
|
629
593
|
"tencent/hy4-preview": "go",
|
|
@@ -717,11 +681,11 @@ function compareByPlan(a, b) {
|
|
|
717
681
|
}
|
|
718
682
|
/**
|
|
719
683
|
* Subscription plan table, synced from the official CLI bundle's plan maps
|
|
720
|
-
* (located by the `"individual-go"` key in
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
684
|
+
* (located by the `"individual-go"` key in `dist/cli.mjs`, re-verified unchanged
|
|
685
|
+
* through command-code@1.66.0): subscription `planId` prefix → display name and
|
|
686
|
+
* the plan's monthly credit total. This is the account's own subscription
|
|
687
|
+
* (from `/alpha/billing/subscriptions`) — distinct from {@link KNOWN_PLANS},
|
|
688
|
+
* which maps catalog models to their minimum tier.
|
|
725
689
|
*
|
|
726
690
|
* `tierWeight` is plugin-added (not from the CLI maps): the plan's rank on
|
|
727
691
|
* the {@link PLAN_ORDER} scale, used by the picker's plan filter
|
|
@@ -836,6 +800,10 @@ const KNOWN_DEALS = {
|
|
|
836
800
|
"stealth/space-bunny-alpha": {
|
|
837
801
|
label: "FREE",
|
|
838
802
|
free: true
|
|
803
|
+
},
|
|
804
|
+
"stealth/pixel-canary": {
|
|
805
|
+
label: "FREE",
|
|
806
|
+
free: true
|
|
839
807
|
}
|
|
840
808
|
};
|
|
841
809
|
/**
|
|
@@ -844,24 +812,11 @@ const KNOWN_DEALS = {
|
|
|
844
812
|
* charges by the hour: peak hours are 01:00–04:00 and 06:00–10:00 UTC (7h per
|
|
845
813
|
* weekday, full price) **Monday to Friday only**; the other 17 hours of a
|
|
846
814
|
* weekday and every hour of Saturday/Sunday (UTC) are off-peak at half price.
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
* The picker shows the
|
|
853
|
-
* *current* state as a compact
|
|
854
|
-
* label (`Peak`/`Half`) matching the English noun style of the other markers
|
|
855
|
-
* (`Image`, `FREE`), so a developer can tell at a glance whether calling the
|
|
856
|
-
* model right now is cheap or expensive.
|
|
857
|
-
*
|
|
858
|
-
* Authoritative extraction: the pricing page embeds a model JSON array whose
|
|
859
|
-
* hourly-priced entries carry a `timeOfDay` block
|
|
860
|
-
* (`{ windows: "01–04 & 06–10 UTC, Mon–Fri", peakHoursPerDay: 7,
|
|
861
|
-
* offPeakHoursPerDay: 17, peak: {...}, offPeak: {...} }`). Exactly four models
|
|
862
|
-
* carry it: V4 Pro, V4 Flash, V4 Flash Vision (exp), and V4.1 Flash (added in
|
|
863
|
-
* command-code@1.53.0 at $0.15/$0.60 off-peak, $0.30/$1.20 peak — the same
|
|
864
|
-
* schedule as the other three).
|
|
815
|
+
* Exactly four models carry the page's `timeOfDay` block (the four rows below).
|
|
816
|
+
* The picker shows the *current* state as a compact label (`Peak`/`Half`)
|
|
817
|
+
* matching the English noun style of the other markers (`Image`, `FREE`), so a
|
|
818
|
+
* developer can tell at a glance whether calling the model right now is cheap
|
|
819
|
+
* or expensive.
|
|
865
820
|
*
|
|
866
821
|
* Extraction caution: the rendered HTML rows are a trap. Each annotation div
|
|
867
822
|
* sits inside its OWN row's container, immediately before the NEXT row starts,
|
|
@@ -869,7 +824,11 @@ const KNOWN_DEALS = {
|
|
|
869
824
|
* to the model printed after it — that is how `deepseek/deepseek-v4-flash-fast`
|
|
870
825
|
* was wrongly added here (its row is flat-priced at $0.28/$0.56/$0.07 and has
|
|
871
826
|
* no `timeOfDay` block). Trust the embedded JSON's `timeOfDay` membership and
|
|
872
|
-
* the 2× price relation, never the flat-text neighbor.
|
|
827
|
+
* the 2× price relation, never the flat-text neighbor. A rate that is
|
|
828
|
+
* internally consistent can still be the wrong row (the V4 Flash Vision (exp)
|
|
829
|
+
* variant is priced from the page's own `timeOfDay` block, not from 2× its own
|
|
830
|
+
* off-peak figures), which is why the vendored price table
|
|
831
|
+
* (`./model-prices.ts`) is synced from the page and not hand-kept.
|
|
873
832
|
*
|
|
874
833
|
* Keep in sync with the official pricing page when the model set, the peak
|
|
875
834
|
* windows, or the weekday rule change (see the dsh-commandcode-upstream skill).
|
|
@@ -908,11 +867,8 @@ function isPeakPricingHour(now = Date.now()) {
|
|
|
908
867
|
return PEAK_HOUR_RANGES.some(([start, end]) => hour >= start && hour < end);
|
|
909
868
|
}
|
|
910
869
|
/**
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
* official rule charges Saturday and Sunday completely off-peak for all 24
|
|
914
|
-
* hours, so a weekend timestamp is off-peak even inside `PEAK_HOUR_RANGES`.
|
|
915
|
-
* `undefined` for models outside the snapshot.
|
|
870
|
+
* As {@link isPeakPricingHour}, plus the {@link KNOWN_PEAK_PRICING} membership
|
|
871
|
+
* test; `undefined` for models outside the snapshot.
|
|
916
872
|
*/
|
|
917
873
|
function peakPricingState(modelId, now = Date.now()) {
|
|
918
874
|
if (!KNOWN_PEAK_PRICING.has(modelId)) return void 0;
|
|
@@ -1802,7 +1758,7 @@ const MODEL_PRICE_ROWS = [
|
|
|
1802
1758
|
{
|
|
1803
1759
|
id: "step-3.5-flash",
|
|
1804
1760
|
rates: [
|
|
1805
|
-
.
|
|
1761
|
+
.09,
|
|
1806
1762
|
.3,
|
|
1807
1763
|
.02
|
|
1808
1764
|
]
|
|
@@ -1838,6 +1794,14 @@ const MODEL_PRICE_ROWS = [
|
|
|
1838
1794
|
2.501,
|
|
1839
1795
|
.042
|
|
1840
1796
|
]
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
id: "typesafe/jev",
|
|
1800
|
+
rates: [
|
|
1801
|
+
.042,
|
|
1802
|
+
0,
|
|
1803
|
+
0
|
|
1804
|
+
]
|
|
1841
1805
|
}
|
|
1842
1806
|
];
|
|
1843
1807
|
/**
|
|
@@ -1854,8 +1818,8 @@ const PRICE_SLUG_OVERRIDES = { "nvidia/nemotron-3-ultra-550b-a55b": "nemotron-3-
|
|
|
1854
1818
|
* not consistently: `tencent/hy4-preview` keeps its prefix while
|
|
1855
1819
|
* `Qwen/Qwen3.8-Max-0902` becomes `qwen-3.8-max-0902`, with a hyphen the
|
|
1856
1820
|
* catalog id does not have. Generating candidates and taking the first that
|
|
1857
|
-
* exists in the vendored table absorbs that drift without a hand-maintained
|
|
1858
|
-
*
|
|
1821
|
+
* exists in the vendored table absorbs that drift without a hand-maintained map
|
|
1822
|
+
* of eighty ids.
|
|
1859
1823
|
*/
|
|
1860
1824
|
function priceSlugCandidates(modelId) {
|
|
1861
1825
|
const lower = modelId.toLowerCase();
|
|
@@ -1910,13 +1874,10 @@ function wireRow(id, slug, row) {
|
|
|
1910
1874
|
* Rows are keyed by CATALOG id wherever the two namespaces reconcile, because
|
|
1911
1875
|
* that is what a session reports, and every row also carries its page slug as a
|
|
1912
1876
|
* second lookup key. Price rows no catalog model claims are served under the
|
|
1913
|
-
* slug alone, so drift in either direction still prices
|
|
1914
|
-
*
|
|
1915
|
-
*
|
|
1916
|
-
* instead of
|
|
1917
|
-
*
|
|
1918
|
-
* The peak windows travel with the table, so the browser applies the very
|
|
1919
|
-
* schedule this snapshot knows instead of restating it.
|
|
1877
|
+
* slug alone, so drift in either direction still prices. Free models are served
|
|
1878
|
+
* explicitly at zero so the composer can say so instead of showing nothing; the
|
|
1879
|
+
* peak windows travel with the table so the browser applies the very schedule
|
|
1880
|
+
* this snapshot knows instead of restating it.
|
|
1920
1881
|
*/
|
|
1921
1882
|
function modelPriceTable() {
|
|
1922
1883
|
const bySlug = new Map(MODEL_PRICE_ROWS.map((row) => [row.id, row]));
|
|
@@ -1970,13 +1931,13 @@ function peakHour(at, windows) {
|
|
|
1970
1931
|
return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
|
|
1971
1932
|
}
|
|
1972
1933
|
/**
|
|
1973
|
-
* All prompt billing buckets determine a request's band, never cumulative
|
|
1934
|
+
* All prompt billing buckets determine a request's band, never cumulative
|
|
1935
|
+
* session input.
|
|
1974
1936
|
*
|
|
1975
1937
|
* A tiered model returns the matching band and does NOT then apply `peak`: the
|
|
1976
|
-
* page publishes those two dimensions independently
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
1979
|
-
* choice, not a verified upstream rule.
|
|
1938
|
+
* page publishes those two dimensions independently and no row carries both
|
|
1939
|
+
* today, so the combination becoming real is visible before it reaches a user.
|
|
1940
|
+
* This ordering is a latent choice, not a verified upstream rule.
|
|
1980
1941
|
*/
|
|
1981
1942
|
function requestRates(price, at, contextTokens, table) {
|
|
1982
1943
|
const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
|
|
@@ -2020,11 +1981,11 @@ function costGroupKey(group, table) {
|
|
|
2020
1981
|
//#endregion
|
|
2021
1982
|
//#region src/cost-projection.ts
|
|
2022
1983
|
const record$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
2023
|
-
const finite
|
|
1984
|
+
const finite = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
2024
1985
|
function tokensOf(value) {
|
|
2025
1986
|
const u = record$2(value);
|
|
2026
|
-
if (!u || !finite
|
|
2027
|
-
if (u.cacheReadTokens !== void 0 && !finite
|
|
1987
|
+
if (!u || !finite(u.inputTokens) || !finite(u.outputTokens)) return void 0;
|
|
1988
|
+
if (u.cacheReadTokens !== void 0 && !finite(u.cacheReadTokens) || u.cacheWriteTokens !== void 0 && !finite(u.cacheWriteTokens)) return void 0;
|
|
2028
1989
|
return {
|
|
2029
1990
|
uncachedInputTokens: u.inputTokens,
|
|
2030
1991
|
outputTokens: u.outputTokens,
|
|
@@ -2050,13 +2011,13 @@ function usageOf(type, data) {
|
|
|
2050
2011
|
}
|
|
2051
2012
|
function parseState(value) {
|
|
2052
2013
|
const s = record$2(value), facts = record$2(s?.facts);
|
|
2053
|
-
if (!s || !facts || !finite
|
|
2014
|
+
if (!s || !facts || !finite(facts.pricingKey) || !Array.isArray(facts.groups)) throw new TypeError("invalid commandCodeCost state");
|
|
2054
2015
|
const group = (v) => {
|
|
2055
2016
|
const g = record$2(v), t = record$2(g?.tokens);
|
|
2056
|
-
return !!g && typeof g.provider === "string" && typeof g.model === "string" && (g.at === null || finite
|
|
2017
|
+
return !!g && typeof g.provider === "string" && typeof g.model === "string" && (g.at === null || finite(g.at)) && finite(g.contextTokens) && !!t && COST_TOKEN_KEYS.every((k) => finite(t[k]));
|
|
2057
2018
|
};
|
|
2058
2019
|
const sel = record$2(s.selection), last = record$2(s.last);
|
|
2059
|
-
if (!(s.selection === null || sel && typeof sel.provider === "string" && typeof sel.model === "string") || !(s.at === null || finite
|
|
2020
|
+
if (!(s.selection === null || sel && typeof sel.provider === "string" && typeof sel.model === "string") || !(s.at === null || finite(s.at)) || !facts.groups.every(group) || !(s.last === null || last && finite(last.turn) && finite(last.step) && group(last.group))) throw new TypeError("invalid commandCodeCost state");
|
|
2060
2021
|
return value;
|
|
2061
2022
|
}
|
|
2062
2023
|
/** Optional registry uses only synchronous parse/view; no new runtime module is required. */
|
|
@@ -2090,11 +2051,11 @@ function createCostProjection(table = modelPriceTable()) {
|
|
|
2090
2051
|
}
|
|
2091
2052
|
if (event.type === "step/start" || event.type === "llm/retry-started") return {
|
|
2092
2053
|
...state,
|
|
2093
|
-
at: finite
|
|
2054
|
+
at: finite(event.time) ? event.time : null,
|
|
2094
2055
|
last: null
|
|
2095
2056
|
};
|
|
2096
2057
|
const tokens = usageOf(event.type, data);
|
|
2097
|
-
if (tokens === void 0 || !finite
|
|
2058
|
+
if (tokens === void 0 || !finite(data.turn) || !finite(data.step)) return state;
|
|
2098
2059
|
const previous = state.last?.turn === data.turn && state.last.step === data.step ? state.last.group : void 0;
|
|
2099
2060
|
const group = {
|
|
2100
2061
|
provider: state.selection?.provider ?? "",
|
|
@@ -2166,34 +2127,18 @@ const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
|
|
|
2166
2127
|
/**
|
|
2167
2128
|
* Multi-account pool for the Command Code provider (host side).
|
|
2168
2129
|
*
|
|
2169
|
-
* One
|
|
2170
|
-
*
|
|
2171
|
-
*
|
|
2172
|
-
*
|
|
2130
|
+
* One subscription's 5-hour window is metered, and a user with several wants a
|
|
2131
|
+
* request that hits one account's limit to continue on the next without a
|
|
2132
|
+
* visible failure. Rotation is passive: a key is marked only on a real
|
|
2133
|
+
* pre-stream rejection, and when nothing can serve, marked keys are revived by
|
|
2134
|
+
* probing their real usage windows.
|
|
2173
2135
|
*
|
|
2174
|
-
*
|
|
2175
|
-
*
|
|
2176
|
-
*
|
|
2177
|
-
* usable, the routed account — resolving each slot's key lazily (literal
|
|
2178
|
-
* config key → credential seam → launch environment → the official CLI
|
|
2179
|
-
* auth file for the default slot only).
|
|
2180
|
-
* - {@link CommandCodeAccountPool.markRejected} records a 429 or 401 against
|
|
2181
|
-
* the exact API key, so several slots sharing one key share one state. A
|
|
2182
|
-
* 429 that NAMED a usage window marks the window exhausted; a 429 that
|
|
2183
|
-
* named none is recorded as a plain throttle, which rotates the same way
|
|
2184
|
-
* but is never reported as an exhausted window (issue #54).
|
|
2185
|
-
* - When every account is marked, the pool probes each key's
|
|
2186
|
-
* `/alpha/billing/credits` window limits (through the injected
|
|
2187
|
-
* {@link CommandCodeAccountPoolDeps.probeWindow}): an account whose window
|
|
2188
|
-
* no longer reports `exceeded` is revived, otherwise the pool throws a
|
|
2189
|
-
* `RATE_LIMIT` error naming the earliest reset time.
|
|
2190
|
-
* - An account the user explicitly asked for — the manual pin
|
|
2191
|
-
* ({@link CommandCodeAccountPoolDeps.preferredId}) or a model rule's
|
|
2192
|
-
* routed account — is probed the same way when a mark would demote it, so
|
|
2193
|
-
* a fallback is never permanent (issue #51).
|
|
2136
|
+
* The non-obvious rules — which rejection may be REPORTED as a spent window, why
|
|
2137
|
+
* a fallback is never permanent, what an explicitly selected account's probe
|
|
2138
|
+
* costs — are stated at the members below, and recorded in full in AGENTS.md.
|
|
2194
2139
|
*
|
|
2195
|
-
*
|
|
2196
|
-
*
|
|
2140
|
+
* Cordis-free like the adapter: every host fact arrives through injected thunks,
|
|
2141
|
+
* so node tests can drive the pool directly.
|
|
2197
2142
|
*
|
|
2198
2143
|
* @module dsh-commandcode-provider/accounts
|
|
2199
2144
|
*/
|
|
@@ -2201,22 +2146,21 @@ const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
|
|
|
2201
2146
|
* Upper bound on the retry wait this pool attaches to the all-exhausted
|
|
2202
2147
|
* `RATE_LIMIT` error. Must equal the `backoff.maxDelayMs` in the adapter's
|
|
2203
2148
|
* `providerRetryPolicy` (which imports it from here): dsh-llm-retry honors a
|
|
2204
|
-
* provider-specified wait verbatim only at or below that cap —
|
|
2205
|
-
*
|
|
2206
|
-
*
|
|
2207
|
-
* window opens" into "fail now".
|
|
2149
|
+
* provider-specified wait verbatim only at or below that cap — a LONGER one
|
|
2150
|
+
* makes the executor abandon the retry instead of falling back to local backoff
|
|
2151
|
+
* (in normal mode), turning "poll until the window opens" into "fail now".
|
|
2208
2152
|
*/
|
|
2209
2153
|
const RETRY_MAX_DELAY_MS = 9e5;
|
|
2210
2154
|
/**
|
|
2211
2155
|
* How long an explicitly selected account's rate-limit mark may stand before
|
|
2212
|
-
* its window is probed again.
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2216
|
-
*
|
|
2217
|
-
* the user's own
|
|
2218
|
-
*
|
|
2219
|
-
*
|
|
2156
|
+
* its window is probed again.
|
|
2157
|
+
*
|
|
2158
|
+
* A 429 whose reset was never learned marks the key `unknown`, which
|
|
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.
|
|
2220
2164
|
*/
|
|
2221
2165
|
const EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS = 6e4;
|
|
2222
2166
|
/** A labeled, human-readable clock reading for error messages. */
|
|
@@ -2235,10 +2179,10 @@ function accountUsable(state) {
|
|
|
2235
2179
|
return false;
|
|
2236
2180
|
}
|
|
2237
2181
|
/**
|
|
2238
|
-
* Pick the account that should serve now: the manually preferred slot when it
|
|
2239
|
-
*
|
|
2240
|
-
*
|
|
2241
|
-
*
|
|
2182
|
+
* Pick the account that should serve now: the manually preferred slot when it is
|
|
2183
|
+
* usable, otherwise the first usable account in rotation order; undefined when
|
|
2184
|
+
* no account is usable. Shared by the pool (request path) and the plugin entry
|
|
2185
|
+
* (the usage view's active badge) so both always agree.
|
|
2242
2186
|
*/
|
|
2243
2187
|
function selectActiveAccount(accounts, preferredId) {
|
|
2244
2188
|
const usable = accounts.filter((account) => accountUsable(account.state));
|
|
@@ -2321,27 +2265,26 @@ var CommandCodeAccountPool = class {
|
|
|
2321
2265
|
return out;
|
|
2322
2266
|
}
|
|
2323
2267
|
/**
|
|
2324
|
-
* Hand out the key for a request: the model-routed account when the
|
|
2325
|
-
*
|
|
2326
|
-
*
|
|
2327
|
-
*
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
2330
|
-
* `
|
|
2268
|
+
* Hand out the key for a request: the model-routed account when the request's
|
|
2269
|
+
* model matches a rule (and that account is usable), else the manually
|
|
2270
|
+
* preferred account when usable, else the first usable account in rotation
|
|
2271
|
+
* order. Returns `undefined` when no account resolves any key at all, or when
|
|
2272
|
+
* an account this request already used is still UNMARKED — that rejection is
|
|
2273
|
+
* the honest answer, so the caller's own rejection surfaces as itself. Throws
|
|
2274
|
+
* `RATE_LIMIT` (naming the earliest window reset) or `INVALID_CREDENTIAL`
|
|
2275
|
+
* when accounts exist but every one of them is marked.
|
|
2331
2276
|
*
|
|
2332
2277
|
* `options.model` is the request's model id; routing rules re-read per
|
|
2333
|
-
* resolution, so a settings change applies live.
|
|
2334
|
-
*
|
|
2335
|
-
*
|
|
2336
|
-
*
|
|
2337
|
-
*
|
|
2338
|
-
*
|
|
2339
|
-
* account's plan) would otherwise be offered again on every attempt, and the
|
|
2340
|
-
* accounts behind it would never be reached.
|
|
2278
|
+
* resolution, so a settings change applies live. `options.tried` lists the
|
|
2279
|
+
* keys this request has already used — the just-rejected one included — and
|
|
2280
|
+
* they are removed from the resolution entirely, which is what lets one
|
|
2281
|
+
* request walk a four-account pool: a rejection that does not mark the key
|
|
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.
|
|
2341
2284
|
*
|
|
2342
|
-
* An explicit selection (the pin or a model rule) that a rate-limit mark
|
|
2343
|
-
*
|
|
2344
|
-
*
|
|
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).
|
|
2345
2288
|
*/
|
|
2346
2289
|
async resolveKey(options) {
|
|
2347
2290
|
const accounts = await this.resolvedAccounts();
|
|
@@ -2397,21 +2340,18 @@ var CommandCodeAccountPool = class {
|
|
|
2397
2340
|
throw allAccountsUnusable(latestAccounts.length > 0 ? latestAccounts : accounts);
|
|
2398
2341
|
}
|
|
2399
2342
|
/**
|
|
2400
|
-
* Record a rejection against one key. `
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
2403
|
-
*
|
|
2404
|
-
*
|
|
2405
|
-
*
|
|
2406
|
-
*
|
|
2407
|
-
* (`invalid-credential`) disables the key until the stored credential
|
|
2343
|
+
* Record a rejection against one key. A `throttled` rejection (the plain 429
|
|
2344
|
+
* that named no window) marks the key with the `throttle` cause, leaving
|
|
2345
|
+
* rotation exactly like a window mark but keeping the pool's own diagnosis at
|
|
2346
|
+
* "rate limited" rather than inventing an exhausted window. `rate-limit`
|
|
2347
|
+
* marks a `cooldown` until `resetAtMs` when the body named the provider's own
|
|
2348
|
+
* reset, otherwise an `unknown` mark whose window is probed lazily.
|
|
2349
|
+
* `invalid-credential` (401) disables the key until the stored credential
|
|
2408
2350
|
* changes.
|
|
2409
2351
|
*
|
|
2410
|
-
* `resetAtMs` is seconds-to-millis converted by the adapter from
|
|
2411
|
-
*
|
|
2412
|
-
*
|
|
2413
|
-
* provider said. It only applies to a window mark; a plain throttle keeps the
|
|
2414
|
-
* window unknown on purpose, so a probe can still find out.
|
|
2352
|
+
* `resetAtMs` is seconds-to-millis converted by the adapter from
|
|
2353
|
+
* `error.rateLimit.reset`; it applies to a window mark only. A plain throttle
|
|
2354
|
+
* keeps its window unknown on purpose, so a probe can still find out.
|
|
2415
2355
|
*/
|
|
2416
2356
|
markRejected(apiKey, rejection, resetAtMs) {
|
|
2417
2357
|
if (rejection === "invalid-credential") this.states.set(apiKey, {
|
|
@@ -2443,15 +2383,12 @@ var CommandCodeAccountPool = class {
|
|
|
2443
2383
|
* One account's key: literal → credential seam → auth file (default slot).
|
|
2444
2384
|
*
|
|
2445
2385
|
* Every source is normalized here, at the single point where a slot's key
|
|
2446
|
-
* enters the pool. The adapter sends the key through
|
|
2447
|
-
* `assertUsableApiKey()`, which trims it
|
|
2448
|
-
*
|
|
2449
|
-
*
|
|
2450
|
-
*
|
|
2451
|
-
*
|
|
2452
|
-
* no mark, and the usage endpoints would 401 while chat kept working.
|
|
2453
|
-
* Normalizing once makes resolution, probing, marking, and the request path
|
|
2454
|
-
* agree on one string.
|
|
2386
|
+
* enters the pool. The adapter sends the key through
|
|
2387
|
+
* `assertUsableApiKey()`, which trims it, and reports that trimmed form back
|
|
2388
|
+
* to {@link markRejected}: returning the raw value would file every 429/401
|
|
2389
|
+
* mark under a key no later lookup can find — rotation would re-offer the same
|
|
2390
|
+
* account, the account card would show no mark, and the usage endpoints would
|
|
2391
|
+
* 401 while chat kept working.
|
|
2455
2392
|
*/
|
|
2456
2393
|
async resolveSlotKey(slot) {
|
|
2457
2394
|
if (slot.literal !== void 0) {
|
|
@@ -2474,12 +2411,11 @@ var CommandCodeAccountPool = class {
|
|
|
2474
2411
|
}
|
|
2475
2412
|
}
|
|
2476
2413
|
/**
|
|
2477
|
-
* The account the user explicitly asked for: the slot a model rule routes
|
|
2478
|
-
*
|
|
2479
|
-
*
|
|
2480
|
-
*
|
|
2481
|
-
* {@link
|
|
2482
|
-
* usable, which is exactly what {@link resolveKey} re-probes.
|
|
2414
|
+
* The account the user explicitly asked for: the slot a model rule routes the
|
|
2415
|
+
* request to when that id exists among the resolved slots, else the manually
|
|
2416
|
+
* pinned slot. Consulted only on the fallback path, so — unlike
|
|
2417
|
+
* {@link selectAccountForModel} — it does NOT require the account to be usable,
|
|
2418
|
+
* which is exactly what {@link resolveKey} re-probes.
|
|
2483
2419
|
*/
|
|
2484
2420
|
explicitAccount(accounts, model, preferredId) {
|
|
2485
2421
|
const rule = matchModelRule(model, this.deps.modelAccountRules?.());
|
|
@@ -2489,9 +2425,9 @@ var CommandCodeAccountPool = class {
|
|
|
2489
2425
|
/**
|
|
2490
2426
|
* Whether an explicitly selected account's mark is due for a window probe.
|
|
2491
2427
|
* Only an `unknown` mark (a 429 whose reset was never learned) is worth
|
|
2492
|
-
* re-probing: a `cooldown`
|
|
2493
|
-
*
|
|
2494
|
-
*
|
|
2428
|
+
* re-probing: a `cooldown` carries its reset and expires by itself, and a
|
|
2429
|
+
* `disabled` (401) key stays out until the stored credential changes. The
|
|
2430
|
+
* interval bounds a probe endpoint that keeps failing.
|
|
2495
2431
|
*/
|
|
2496
2432
|
canProbeExplicit(account) {
|
|
2497
2433
|
if (account.state?.kind !== "unknown") return false;
|
|
@@ -2505,8 +2441,8 @@ var CommandCodeAccountPool = class {
|
|
|
2505
2441
|
/**
|
|
2506
2442
|
* Probe one explicitly selected account's window and apply the answer. A
|
|
2507
2443
|
* window that is no longer exceeded drops the mark, so the user's own
|
|
2508
|
-
* selection serves again on this very request
|
|
2509
|
-
*
|
|
2444
|
+
* selection serves again on this very request; an exceeded one is stamped as
|
|
2445
|
+
* a cooldown carrying the provider's reset time, after which
|
|
2510
2446
|
* {@link accountUsable} lets the account back in with no further probe. A
|
|
2511
2447
|
* probe that fails changes nothing: the mark stays `unknown` and the next
|
|
2512
2448
|
* attempt waits out the interval.
|
|
@@ -2558,22 +2494,20 @@ var CommandCodeAccountPool = class {
|
|
|
2558
2494
|
* read a window as exceeded) → the window diagnosis, naming the earliest
|
|
2559
2495
|
* known reset when the provider published one;
|
|
2560
2496
|
* - every mark a plain throttle (a 429 that said nothing about a window, and
|
|
2561
|
-
* no probe could confirm one) → a throttle diagnosis that says so.
|
|
2562
|
-
* "all accounts have exhausted their usage window" here is issue
|
|
2563
|
-
*
|
|
2564
|
-
*
|
|
2497
|
+
* no probe could confirm one) → a throttle diagnosis that says so.
|
|
2498
|
+
* Claiming "all accounts have exhausted their usage window" here is issue
|
|
2499
|
+
* #54: a bare 429 was reported that way while the account card showed both
|
|
2500
|
+
* windows barely used.
|
|
2565
2501
|
*
|
|
2566
|
-
*
|
|
2567
|
-
*
|
|
2502
|
+
* The attached `providerRetryAfterMs` is the exact wait until the earliest known
|
|
2503
|
+
* window reset, so dsh-llm-retry sleeps through the window instead of polling at
|
|
2504
|
+
* its backoff cadence. It is capped at {@link RETRY_MAX_DELAY_MS} — see there for
|
|
2505
|
+
* why a longer attached wait is worse than a capped one; longer resets simply
|
|
2506
|
+
* ride the capped local backoff and the probe revival. A throttle diagnosis
|
|
2507
|
+
* attaches none: its marks carry no reset.
|
|
2568
2508
|
*
|
|
2569
|
-
*
|
|
2570
|
-
*
|
|
2571
|
-
* polling at its backoff cadence. It is capped at {@link RETRY_MAX_DELAY_MS}:
|
|
2572
|
-
* the executor honors a provider wait verbatim only at or below the policy's
|
|
2573
|
-
* `maxDelayMs` — a LONGER attached wait makes it abandon the retry entirely
|
|
2574
|
-
* (normal mode), which would turn "poll until the window opens" into "fail
|
|
2575
|
-
* now". Longer resets simply ride the capped local backoff and the probe
|
|
2576
|
-
* revival. A throttle diagnosis attaches none: its marks carry no reset.
|
|
2509
|
+
* Shared by the all-marked tail of {@link CommandCodeAccountPool.resolveKey} and
|
|
2510
|
+
* by its already-tried diagnosis, so the two can never drift apart.
|
|
2577
2511
|
*/
|
|
2578
2512
|
function allAccountsUnusable(accounts) {
|
|
2579
2513
|
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");
|
|
@@ -2616,14 +2550,14 @@ const REQUEST_IMAGE_MAX_LONG_EDGE = 1568;
|
|
|
2616
2550
|
const REQUEST_IMAGE_MAX_ENCODED_BYTES = 1048576;
|
|
2617
2551
|
/**
|
|
2618
2552
|
* Aspect-preserving projection onto a long-edge budget; never enlarges, and
|
|
2619
|
-
* never returns a zero dimension
|
|
2620
|
-
* an extreme aspect ratio can produce, and a 0 would make the encoder refuse).
|
|
2553
|
+
* never returns a zero dimension.
|
|
2621
2554
|
*
|
|
2622
2555
|
* `dsh-attachment` exports an equivalent `longEdgeDimensions`, but it does not
|
|
2623
2556
|
* guard its inputs: a reference whose declared size is 0 or NaN — a malformed
|
|
2624
2557
|
* or fabricated attachment record — would project to a zero-sized target and
|
|
2625
|
-
* make the encoder refuse the whole request.
|
|
2626
|
-
*
|
|
2558
|
+
* make the encoder refuse the whole request. This local guard is the ONLY
|
|
2559
|
+
* reason the function is not imported; do not replace it with the upstream one.
|
|
2560
|
+
* `tests/image-tokens.test.ts` pins the degenerate cases.
|
|
2627
2561
|
*/
|
|
2628
2562
|
function longEdgeDimensions(width, height, longEdge) {
|
|
2629
2563
|
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return {
|
|
@@ -2667,16 +2601,13 @@ function requestImageTarget(ref) {
|
|
|
2667
2601
|
* that the heuristic prices at ~0 tokens each, so context pressure is
|
|
2668
2602
|
* under-reported and the session walks into its own context window.
|
|
2669
2603
|
*
|
|
2670
|
-
* This route is a gateway in front of many upstreams
|
|
2671
|
-
*
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
2674
|
-
*
|
|
2675
|
-
*
|
|
2676
|
-
*
|
|
2677
|
-
* These are ESTIMATES for context accounting, not invoices — the provider's own
|
|
2678
|
-
* usage remains authoritative for a completed request, exactly as the published
|
|
2679
|
-
* per-token rates the session-cost readout uses are.
|
|
2604
|
+
* This route is a gateway in front of many upstreams, so the estimate is per
|
|
2605
|
+
* FAMILY, using each vendor's published rule, and the families are recognised
|
|
2606
|
+
* by the catalog id (which names the vendor). Every formula is stated here
|
|
2607
|
+
* rather than imported: the only in-tree implementations live inside the
|
|
2608
|
+
* official adapters, and this plugin deliberately does not depend on those
|
|
2609
|
+
* protocols. These are ESTIMATES for context accounting, not invoices — the
|
|
2610
|
+
* provider's own usage remains authoritative for a completed request.
|
|
2680
2611
|
*
|
|
2681
2612
|
* @module dsh-commandcode-provider/image-tokens
|
|
2682
2613
|
*/
|
|
@@ -2826,14 +2757,11 @@ function scaleBy(size, factor) {
|
|
|
2826
2757
|
* Opt-in raw-stream trace for the Command Code transports.
|
|
2827
2758
|
*
|
|
2828
2759
|
* Why this exists: this route's failure mode of record is a response that ENDS
|
|
2829
|
-
* EARLY
|
|
2830
|
-
*
|
|
2831
|
-
*
|
|
2832
|
-
*
|
|
2833
|
-
*
|
|
2834
|
-
* wire: which events actually arrived, when the last one landed, how long the
|
|
2835
|
-
* socket then sat silent, and how it ended. Without that, a truncated answer is
|
|
2836
|
-
* a report with no evidence attached.
|
|
2760
|
+
* EARLY — the gateway, a proxy or the network closes the socket mid-generation,
|
|
2761
|
+
* so the adapter's read loop sees `done` without the transport's terminal event.
|
|
2762
|
+
* A provider cut, a proxy timeout and a dropped socket are indistinguishable
|
|
2763
|
+
* from inside the adapter; only the raw wire separates them — which events
|
|
2764
|
+
* arrived, when the last one landed, how long the socket then sat silent.
|
|
2837
2765
|
*
|
|
2838
2766
|
* Enable it per process:
|
|
2839
2767
|
*
|
|
@@ -2843,24 +2771,25 @@ function scaleBy(size, factor) {
|
|
|
2843
2771
|
* ```
|
|
2844
2772
|
*
|
|
2845
2773
|
* Any value that is not a flag is a path, and the explicit off-spellings
|
|
2846
|
-
* (`0`, `false`, `no`, `off`) are
|
|
2847
|
-
* for a file name.
|
|
2774
|
+
* (`0`, `false`, `no`, `off`) are read as "off" rather than as a file name.
|
|
2848
2775
|
*
|
|
2849
2776
|
* Every model request appends one JSONL record per raw chunk plus lifecycle
|
|
2850
|
-
* records (`response`, `eof`, `idle-timeout`, `read-error`, `end`,
|
|
2851
|
-
* `stream-close`), each stamped with `at` — milliseconds since
|
|
2852
|
-
*
|
|
2853
|
-
*
|
|
2854
|
-
*
|
|
2855
|
-
*
|
|
2856
|
-
*
|
|
2857
|
-
*
|
|
2858
|
-
*
|
|
2859
|
-
*
|
|
2860
|
-
*
|
|
2861
|
-
*
|
|
2862
|
-
*
|
|
2863
|
-
*
|
|
2777
|
+
* records (`request`, `response`, `eof`, `idle-timeout`, `read-error`, `end`,
|
|
2778
|
+
* `stream-close`), each stamped with `streamId` and `at` — milliseconds since
|
|
2779
|
+
* the trace opened, before connecting. `stream-open.startedAt` is Unix time in
|
|
2780
|
+
* milliseconds, so interleaved calls and session logs can be correlated.
|
|
2781
|
+
*
|
|
2782
|
+
* PRIVACY: the chunk records carry the provider's own payload verbatim, because
|
|
2783
|
+
* a truncated answer is exactly what is being diagnosed — treat the file as
|
|
2784
|
+
* conversation content, not as a log safe to share. The request-side record
|
|
2785
|
+
* records structural counts and SHA-256 fingerprints only; it never carries the
|
|
2786
|
+
* raw request body, an API key, or any request header. Keep it that way.
|
|
2787
|
+
*
|
|
2788
|
+
* Trade-offs: OFF costs one `process.env` read per request; ON appends
|
|
2789
|
+
* synchronously, one file open per chunk, because the trace must survive a host
|
|
2790
|
+
* killed mid-diagnosis. A trace that cannot be written turns itself off rather
|
|
2791
|
+
* than failing the request it observes — a diagnostic must never become the
|
|
2792
|
+
* outage.
|
|
2864
2793
|
*
|
|
2865
2794
|
* @module
|
|
2866
2795
|
*/
|
|
@@ -2883,7 +2812,7 @@ const FLAG_VALUES = /* @__PURE__ */ new Set([
|
|
|
2883
2812
|
* A user turning the trace off writes `false` as readily as they write `1` to
|
|
2884
2813
|
* turn it on, and without this set every one of these spellings fell through to
|
|
2885
2814
|
* "…so it must be a path" — the trace switched ON and wrote the conversation to
|
|
2886
|
-
* a file NAMED `false`
|
|
2815
|
+
* a file NAMED `false` in the process's working directory.
|
|
2887
2816
|
*/
|
|
2888
2817
|
const OFF_VALUES = /* @__PURE__ */ new Set([
|
|
2889
2818
|
"0",
|
|
@@ -2935,6 +2864,7 @@ function openStreamTrace(scope, options = {}) {
|
|
|
2935
2864
|
if (path === void 0) return OFF;
|
|
2936
2865
|
const now = options.now ?? Date.now;
|
|
2937
2866
|
const startedAt = now();
|
|
2867
|
+
const streamId = randomUUID();
|
|
2938
2868
|
let written = 0;
|
|
2939
2869
|
let stopped = false;
|
|
2940
2870
|
let capped = false;
|
|
@@ -2954,6 +2884,7 @@ function openStreamTrace(scope, options = {}) {
|
|
|
2954
2884
|
line = `${JSON.stringify({
|
|
2955
2885
|
at: now() - startedAt,
|
|
2956
2886
|
scope,
|
|
2887
|
+
streamId,
|
|
2957
2888
|
...record
|
|
2958
2889
|
})}\n`;
|
|
2959
2890
|
} catch {
|
|
@@ -2967,6 +2898,7 @@ function openStreamTrace(scope, options = {}) {
|
|
|
2967
2898
|
line = `${JSON.stringify({
|
|
2968
2899
|
at: now() - startedAt,
|
|
2969
2900
|
scope,
|
|
2901
|
+
streamId,
|
|
2970
2902
|
event: "trace-capped",
|
|
2971
2903
|
bytes: written
|
|
2972
2904
|
})}\n`;
|
|
@@ -2978,9 +2910,14 @@ function openStreamTrace(scope, options = {}) {
|
|
|
2978
2910
|
stopped = true;
|
|
2979
2911
|
}
|
|
2980
2912
|
};
|
|
2981
|
-
write({
|
|
2913
|
+
write({
|
|
2914
|
+
event: "stream-open",
|
|
2915
|
+
startedAt
|
|
2916
|
+
});
|
|
2982
2917
|
return {
|
|
2983
|
-
enabled
|
|
2918
|
+
get enabled() {
|
|
2919
|
+
return !stopped && !capped;
|
|
2920
|
+
},
|
|
2984
2921
|
record(event, fields) {
|
|
2985
2922
|
write(fields === void 0 ? { event } : {
|
|
2986
2923
|
event,
|
|
@@ -3004,20 +2941,17 @@ function openStreamTrace(scope, options = {}) {
|
|
|
3004
2941
|
* community-maintained integration; you need your own Command Code account
|
|
3005
2942
|
* and API key or subscription, and Command Code's terms apply.
|
|
3006
2943
|
*
|
|
3007
|
-
*
|
|
3008
|
-
*
|
|
3009
|
-
*
|
|
3010
|
-
*
|
|
3011
|
-
*
|
|
3012
|
-
* SSE-ish JSONL events: text-delta | reasoning-start/delta/end | tool-call
|
|
3013
|
-
* | tool-result | cache-write-tokens | finish | error
|
|
3014
|
-
* Model catalog: GET {apiBase}/provider/v1/models -> { object: 'list', data: [...] }
|
|
2944
|
+
* Owns the two chat transports (`/alpha/generate`, `/provider/v1/chat/completions`),
|
|
2945
|
+
* message conversion, SSE/JSONL parsing, the catalog + its on-disk cache, and
|
|
2946
|
+
* the pre-stream account rotation loop. The wire protocol is
|
|
2947
|
+
* reverse-engineered (command-code@1.28.4, re-verified through 1.66.0);
|
|
2948
|
+
* AGENTS.md holds the full protocol record.
|
|
3015
2949
|
*
|
|
3016
2950
|
* The adapter is deliberately free of cordis/schemastery: it receives a
|
|
3017
2951
|
* per-request options thunk and an API-key resolver from the plugin entry
|
|
3018
2952
|
* (src/index.ts), so a settings change reaches the very next request.
|
|
3019
2953
|
*/
|
|
3020
|
-
const COMMAND_CODE_CLI_VERSION = "1.
|
|
2954
|
+
const COMMAND_CODE_CLI_VERSION = "1.66.0";
|
|
3021
2955
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
3022
2956
|
const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
|
|
3023
2957
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
@@ -3051,6 +2985,97 @@ function stringValue(value) {
|
|
|
3051
2985
|
function numberValue(value) {
|
|
3052
2986
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3053
2987
|
}
|
|
2988
|
+
/** Hash one request value without retaining or logging its contents. */
|
|
2989
|
+
function fingerprintValue(value) {
|
|
2990
|
+
let serialized;
|
|
2991
|
+
try {
|
|
2992
|
+
serialized = JSON.stringify(value) ?? String(value);
|
|
2993
|
+
} catch {
|
|
2994
|
+
serialized = String(value);
|
|
2995
|
+
}
|
|
2996
|
+
return {
|
|
2997
|
+
bytes: Buffer.byteLength(serialized),
|
|
2998
|
+
sha256: createHash("sha256").update(serialized).digest("hex")
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
/**
|
|
3002
|
+
* Structural request facts for issue #64's cache diagnosis.
|
|
3003
|
+
*
|
|
3004
|
+
* This deliberately records hashes and counts, never the prompt, tool
|
|
3005
|
+
* descriptions, image bytes, headers, or credentials. `body` catches changes
|
|
3006
|
+
* in any wire field; the named sections make a changed system/tools/messages
|
|
3007
|
+
* prefix visible without making the trace a copy of the conversation.
|
|
3008
|
+
*/
|
|
3009
|
+
function requestFingerprint(protocol, body) {
|
|
3010
|
+
const params = protocol === "cli" ? recordOrEmpty(body.params) : body;
|
|
3011
|
+
const messages = Array.isArray(params.messages) ? params.messages : [];
|
|
3012
|
+
const tools = Array.isArray(params.tools) ? params.tools : [];
|
|
3013
|
+
return {
|
|
3014
|
+
protocol,
|
|
3015
|
+
model: typeof params.model === "string" ? params.model : void 0,
|
|
3016
|
+
threadId: typeof body.threadId === "string" ? body.threadId : void 0,
|
|
3017
|
+
body: fingerprintValue(body),
|
|
3018
|
+
config: fingerprintValue(body.config),
|
|
3019
|
+
system: fingerprintValue(protocol === "cli" ? params.system : messages.filter((m) => isRecord(m) && m.role === "system")),
|
|
3020
|
+
tools: {
|
|
3021
|
+
count: tools.length,
|
|
3022
|
+
...fingerprintValue(tools)
|
|
3023
|
+
},
|
|
3024
|
+
messages: {
|
|
3025
|
+
count: messages.length,
|
|
3026
|
+
roles: messages.map((message) => isRecord(message) && typeof message.role === "string" ? message.role : "unknown"),
|
|
3027
|
+
...fingerprintValue(messages),
|
|
3028
|
+
items: messages.map((message) => fingerprintValue(message))
|
|
3029
|
+
}
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
/** Only provider correlation headers, never a wholesale response-header dump. */
|
|
3033
|
+
const RESPONSE_ID_HEADERS = [
|
|
3034
|
+
"x-request-id",
|
|
3035
|
+
"request-id",
|
|
3036
|
+
"x-trace-id",
|
|
3037
|
+
"x-generation-id",
|
|
3038
|
+
"traceparent"
|
|
3039
|
+
];
|
|
3040
|
+
function responseIdentifiers(headers) {
|
|
3041
|
+
const result = {};
|
|
3042
|
+
for (const name of RESPONSE_ID_HEADERS) {
|
|
3043
|
+
const value = diagnosticId(headers.get(name));
|
|
3044
|
+
if (value !== void 0) result[name] = value;
|
|
3045
|
+
}
|
|
3046
|
+
return result;
|
|
3047
|
+
}
|
|
3048
|
+
function diagnosticId(value) {
|
|
3049
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= 512 ? value : void 0;
|
|
3050
|
+
}
|
|
3051
|
+
function captureResponseIdentifiers(metadata, event) {
|
|
3052
|
+
if (!isRecord(event)) return;
|
|
3053
|
+
if (metadata.protocol === "openai" && Array.isArray(event.choices) || event.type === "response-metadata") {
|
|
3054
|
+
const id = diagnosticId(event.id);
|
|
3055
|
+
if (id !== void 0) metadata.responseId = id;
|
|
3056
|
+
}
|
|
3057
|
+
for (const name of [
|
|
3058
|
+
"generationId",
|
|
3059
|
+
"providerRequestId",
|
|
3060
|
+
"traceId"
|
|
3061
|
+
]) {
|
|
3062
|
+
const id = diagnosticId(event[name]);
|
|
3063
|
+
if (id !== void 0) metadata[name] = id;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
function hasResponseIdentifiers(metadata) {
|
|
3067
|
+
return Object.keys(metadata.headers).length > 0 || metadata.responseId !== void 0 || metadata.generationId !== void 0 || metadata.providerRequestId !== void 0 || metadata.traceId !== void 0;
|
|
3068
|
+
}
|
|
3069
|
+
/** Keep the provider's request id on DSH's durable failure, when it supplied one. */
|
|
3070
|
+
function failureWithRequestId(error, metadata) {
|
|
3071
|
+
const id = metadata.providerRequestId ?? metadata.headers["x-request-id"] ?? metadata.headers["request-id"];
|
|
3072
|
+
if (!(error instanceof LlmError) || error.failure.requestId !== void 0 || id === void 0) return error;
|
|
3073
|
+
return new LlmError(error.message, error.code, {
|
|
3074
|
+
...error.failure,
|
|
3075
|
+
requestId: ProviderRequestId(id),
|
|
3076
|
+
...error.cause === void 0 ? {} : { cause: error.cause }
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3054
3079
|
function booleanValue(value) {
|
|
3055
3080
|
return typeof value === "boolean" ? value : void 0;
|
|
3056
3081
|
}
|
|
@@ -3060,16 +3085,13 @@ function booleanValue(value) {
|
|
|
3060
3085
|
*
|
|
3061
3086
|
* Entries are read by their own text members rather than filtered on `type`:
|
|
3062
3087
|
* DeepSeek sends `{ type: 'reasoning.text', text }` while the OpenAI family
|
|
3063
|
-
* sends `{ type: 'reasoning.summary', summary
|
|
3064
|
-
*
|
|
3065
|
-
*
|
|
3066
|
-
*
|
|
3067
|
-
*
|
|
3068
|
-
*
|
|
3069
|
-
*
|
|
3070
|
-
* tests for non-empty rather than merely present. Entries carrying neither
|
|
3071
|
-
* member — the encrypted blobs — contribute nothing, and no entry contributes
|
|
3072
|
-
* both, so a summary is never counted twice.
|
|
3088
|
+
* sends `{ type: 'reasoning.summary', summary }` — the same summary text the
|
|
3089
|
+
* scalar `delta.reasoning` carries. A `type` filter would drop one family, and
|
|
3090
|
+
* losing thinking is the failure this exists to prevent. `text` wins per entry,
|
|
3091
|
+
* but an EMPTY `text` must not shadow a populated `summary` (the Responses wire
|
|
3092
|
+
* emits `text: ''` placeholders), so the fallback tests for non-empty rather
|
|
3093
|
+
* than merely present. Encrypted-blob entries carry neither member and
|
|
3094
|
+
* contribute nothing; no entry carries both, so nothing is counted twice.
|
|
3073
3095
|
*
|
|
3074
3096
|
* Returns undefined when the array yields nothing, so the caller's `??` chain
|
|
3075
3097
|
* keeps falling through instead of treating "no text" as a reasoning delta.
|
|
@@ -3090,9 +3112,7 @@ function reasoningDetailsText(value) {
|
|
|
3090
3112
|
* none" — the same rule {@link reasoningDetailsText} applies inside the array.
|
|
3091
3113
|
* The scalar and the array describe the same thinking, so an empty spelling of
|
|
3092
3114
|
* one must not shadow populated text in the other (the Responses wire pads with
|
|
3093
|
-
* `text: ''`)
|
|
3094
|
-
* so `reasoning: ''` beside a populated `reasoning_details` used to drop the
|
|
3095
|
-
* whole chunk's thinking.
|
|
3115
|
+
* `text: ''`), and `??` alone cannot express that.
|
|
3096
3116
|
*/
|
|
3097
3117
|
function nonEmptyText(value) {
|
|
3098
3118
|
return value === void 0 || value === "" ? void 0 : value;
|
|
@@ -3156,30 +3176,21 @@ function isZdrNoProviders(providerCode, providerDetail, errText) {
|
|
|
3156
3176
|
/**
|
|
3157
3177
|
* Classify one in-band stream `error` payload into the failure the caller
|
|
3158
3178
|
* throws. Shared by both transports — the CLI transport delivers it as an
|
|
3159
|
-
* `error` event
|
|
3160
|
-
*
|
|
3179
|
+
* `error` event, the Provider API transport as a top-level `error` member of
|
|
3180
|
+
* an SSE chunk — so the two cannot drift apart.
|
|
3161
3181
|
*
|
|
3162
3182
|
* The wording decides before the status does, mirroring the official CLI's
|
|
3163
|
-
* `classifyKind` (which reads the message first)
|
|
3164
|
-
* (which exist so thrown and in-band delivery share one classifier). Order is
|
|
3165
|
-
* load-bearing:
|
|
3183
|
+
* `classifyKind` (which reads the message first). Order is load-bearing:
|
|
3166
3184
|
*
|
|
3167
3185
|
* - A context-window rejection is terminal for THIS request but recoverable by
|
|
3168
3186
|
* the harness: `dsh-compaction-basic` listens on `agent/request-error` for
|
|
3169
3187
|
* `CONTEXT_WINDOW_EXCEEDED` and answers it with a compaction + retry of the
|
|
3170
|
-
* reduced surface.
|
|
3171
|
-
*
|
|
3172
|
-
*
|
|
3173
|
-
* this wording landed before — the adapter resent the byte-identical
|
|
3174
|
-
* oversized request up to `maxRetries` times and the session could never
|
|
3175
|
-
* recover, which is exactly the endless-retry-at-long-context report
|
|
3176
|
-
* (issue #39).
|
|
3188
|
+
* reduced surface. Reported as retryable `SERVER` instead, the adapter resent
|
|
3189
|
+
* the byte-identical oversized request up to `maxRetries` times and the
|
|
3190
|
+
* session could never recover (issue #39).
|
|
3177
3191
|
* - Credits/plan wording is terminal, so an exhausted balance or a model
|
|
3178
3192
|
* outside the plan is not retried as if it were a transient rate limit.
|
|
3179
|
-
* - Only then does the status/`isRetryable` pair decide
|
|
3180
|
-
* refusal (`isRetryable: false`) or a terminal marker OUTRANKS a status, so a
|
|
3181
|
-
* 5xx carrying "insufficient credits" stays terminal instead of entering the
|
|
3182
|
-
* retry cadence as `SERVER`.
|
|
3193
|
+
* - Only then does the status/`isRetryable` pair decide.
|
|
3183
3194
|
*/
|
|
3184
3195
|
function streamErrorToLlmError(value, fallbackMessage) {
|
|
3185
3196
|
const record = isRecord(value) ? value : void 0;
|
|
@@ -3283,6 +3294,8 @@ function mergeCombinatorBranches(node, depth) {
|
|
|
3283
3294
|
* resolved/merged; anything else (an array/scalar root, or no schema at all)
|
|
3284
3295
|
* degrades to a permissive free-form object, because a request the provider
|
|
3285
3296
|
* refuses helps no one and the tool's own description is still in the prompt.
|
|
3297
|
+
* Only the root is touched and every path returns a copy: the harness may
|
|
3298
|
+
* deep-freeze the caller's schema.
|
|
3286
3299
|
*/
|
|
3287
3300
|
function toolParametersSchema(parameters, depth = 0) {
|
|
3288
3301
|
if (!isRecord(parameters)) return {
|
|
@@ -3401,11 +3414,10 @@ async function writeModelsCache(cachePath, models) {
|
|
|
3401
3414
|
* The tool result one message answers, or undefined when it answers none.
|
|
3402
3415
|
*
|
|
3403
3416
|
* The harness models a tool result as its own `role: 'tool'` message carrying
|
|
3404
|
-
* `toolCallId`/`isError` beside raw content blocks (
|
|
3405
|
-
*
|
|
3406
|
-
*
|
|
3407
|
-
*
|
|
3408
|
-
* unanswered call on the wire.
|
|
3417
|
+
* `toolCallId`/`isError` beside raw content blocks (`MessageRoleMap.tool` is
|
|
3418
|
+
* the only role a tool result can arrive on). Pairing and emission MUST agree
|
|
3419
|
+
* on this same view, or a dropped result either drops its call too (making the
|
|
3420
|
+
* model forget completed work) or leaves an unanswered call on the wire.
|
|
3409
3421
|
*/
|
|
3410
3422
|
function toolResultOf(message) {
|
|
3411
3423
|
if (message.role !== "tool") return void 0;
|
|
@@ -3448,8 +3460,8 @@ function pairedToolCalls(messages) {
|
|
|
3448
3460
|
* another provider issued the call — so overlong paired ids are remapped to
|
|
3449
3461
|
* short per-request aliases. Correlation only needs to hold within one
|
|
3450
3462
|
* request (each call travels with its result in the same body), so a
|
|
3451
|
-
* sequential alias is enough: no durable state,
|
|
3452
|
-
*
|
|
3463
|
+
* sequential alias is enough: no durable state, and the harness log keeps the
|
|
3464
|
+
* original ids.
|
|
3453
3465
|
*/
|
|
3454
3466
|
const MAX_WIRE_TOOL_CALL_ID_LENGTH = 64;
|
|
3455
3467
|
/**
|
|
@@ -3481,7 +3493,7 @@ function blockText(block) {
|
|
|
3481
3493
|
/**
|
|
3482
3494
|
* Collect text and nested images of one tool result. The harness `read_image`
|
|
3483
3495
|
* tool returns both, so dropping the image half is what made a tool-read image
|
|
3484
|
-
* invisible to a Vision model (issue #30)
|
|
3496
|
+
* invisible to a Vision model (issue #30); deduplication keeps a result that
|
|
3485
3497
|
* repeats one attachment from paying for the same pixels twice.
|
|
3486
3498
|
*/
|
|
3487
3499
|
function toolResultMedia(block) {
|
|
@@ -3521,17 +3533,14 @@ const TOOL_RESULT_IMAGE_TEXT = "Attached image(s) from tool result";
|
|
|
3521
3533
|
* tool it just ran rather than to the user; it also leads the part list,
|
|
3522
3534
|
* because an image-first content array is what some gateways reject.
|
|
3523
3535
|
*
|
|
3524
|
-
* The note names the tool call the image came out of
|
|
3525
|
-
*
|
|
3526
|
-
*
|
|
3527
|
-
*
|
|
3528
|
-
*
|
|
3529
|
-
*
|
|
3530
|
-
*
|
|
3531
|
-
*
|
|
3532
|
-
*
|
|
3533
|
-
* Count and pixel dimensions are appended when the tool's own text does not
|
|
3534
|
-
* already state them (a result that returns the image and nothing else).
|
|
3536
|
+
* The note names the tool call the image came out of: a turn's carriers are
|
|
3537
|
+
* emitted as a group after the whole tool group (issue #33), so position
|
|
3538
|
+
* cannot carry the association — and every `read_image` result renders the
|
|
3539
|
+
* same envelope text, so two parallel calls would produce two identical
|
|
3540
|
+
* notes. The id is the WIRE id, the one the model saw on the `tool-call` and
|
|
3541
|
+
* on the tool message just above, so an overlong cross-provider id is named
|
|
3542
|
+
* by the alias that replaced it. Count and pixel dimensions are appended
|
|
3543
|
+
* when the tool's own text does not already state them.
|
|
3535
3544
|
*/
|
|
3536
3545
|
function toolResultImageNote(media, toolCallId) {
|
|
3537
3546
|
const lead = `${TOOL_RESULT_IMAGE_TEXT} (${toolCallId}):`;
|
|
@@ -3562,10 +3571,10 @@ const ENGINE_IMAGE_POLICY = {
|
|
|
3562
3571
|
requiredImageOffload
|
|
3563
3572
|
};
|
|
3564
3573
|
/**
|
|
3565
|
-
* The failure a route raises instead of evicting on its own. The
|
|
3566
|
-
*
|
|
3567
|
-
*
|
|
3568
|
-
*
|
|
3574
|
+
* The failure a route raises instead of evicting on its own. The count is the
|
|
3575
|
+
* ONE thing the harness needs: `dsh-compaction-image-offload` records it as an
|
|
3576
|
+
* `image/offload` event, marks that many oldest retained occurrences, and
|
|
3577
|
+
* retries the step.
|
|
3569
3578
|
*
|
|
3570
3579
|
* Deliberately outside `providerRetryPolicy()`'s whitelist: a byte-identical
|
|
3571
3580
|
* resend cannot succeed, so the retry belongs to the surface mutation, not to
|
|
@@ -3627,8 +3636,9 @@ async function readRequestImage(attachments, ref) {
|
|
|
3627
3636
|
* Price one request occurrence.
|
|
3628
3637
|
*
|
|
3629
3638
|
* Two cases cost no vision tokens at all, and both are priced as their
|
|
3630
|
-
* model-visible TEXT so the caller's estimator can charge that instead
|
|
3631
|
-
*
|
|
3639
|
+
* model-visible TEXT so the caller's estimator can charge that instead (which
|
|
3640
|
+
* is what stops the meter counting an evicted image as context): a model that
|
|
3641
|
+
* accepts text only (the harness projects those images to placeholder text
|
|
3632
3642
|
* before the request), and an occurrence the session surface has already
|
|
3633
3643
|
* offloaded (the same placeholder text rides in every later request). A retained
|
|
3634
3644
|
* occurrence on this route contributes no text at all — the pixels are inlined —
|
|
@@ -3883,7 +3893,6 @@ function parseUsageTotals(usage) {
|
|
|
3883
3893
|
periodBasis: stringValue(usage.periodBasis) ?? "billing-period"
|
|
3884
3894
|
};
|
|
3885
3895
|
}
|
|
3886
|
-
/** Parse one window-limit block (`fiveHour` / `weekly`). */
|
|
3887
3896
|
/**
|
|
3888
3897
|
* Parse one window block into a window limit, or undefined when the endpoint
|
|
3889
3898
|
* reported no such window.
|
|
@@ -3951,6 +3960,46 @@ function classifyTotalFailure(failures, failed) {
|
|
|
3951
3960
|
if (failed.every((item) => item.kind === "invalid-response")) return "invalid-response";
|
|
3952
3961
|
if (failed.every((item) => item.kind === "transport")) return "network";
|
|
3953
3962
|
}
|
|
3963
|
+
/** UUID shape accepted by the official CLI's `toWireThreadId` helper. */
|
|
3964
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3965
|
+
const THREAD_ID_NAMESPACE = Buffer.from("6ba7b8119dad11d180b400c04fd430c8", "hex");
|
|
3966
|
+
/**
|
|
3967
|
+
* Map the harness session identity to the CLI transport's UUID thread ID.
|
|
3968
|
+
*
|
|
3969
|
+
* The official CLI creates one thread ID per agent run and reuses it for the
|
|
3970
|
+
* tool-loop requests that follow. DSH stamps its loop-owned session ID onto
|
|
3971
|
+
* GenerateOptions, but ordinary IDs are `session-<UUID>`, not bare UUIDs.
|
|
3972
|
+
* UUIDv5 gives every non-UUID session ID a stable, wire-compatible mapping.
|
|
3973
|
+
* An already valid UUID remains unchanged; a one-shot call without a session
|
|
3974
|
+
* ID still gets a fresh thread ID.
|
|
3975
|
+
* This alone does not establish cache affinity (issue #64's A/B found none).
|
|
3976
|
+
*/
|
|
3977
|
+
function cliThreadId(sessionId) {
|
|
3978
|
+
if (!sessionId) return randomUUID();
|
|
3979
|
+
if (UUID_PATTERN.test(sessionId)) return sessionId;
|
|
3980
|
+
const digest = createHash("sha1").update(THREAD_ID_NAMESPACE).update("dsh-commandcode-provider/session/").update(sessionId).digest();
|
|
3981
|
+
digest[6] = digest[6] & 15 | 80;
|
|
3982
|
+
digest[8] = digest[8] & 63 | 128;
|
|
3983
|
+
const hex = digest.toString("hex", 0, 16);
|
|
3984
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
3985
|
+
}
|
|
3986
|
+
/**
|
|
3987
|
+
* The CLI wire's explicit system-section form.
|
|
3988
|
+
*
|
|
3989
|
+
* The official CLI's current transport sends system text as an array and marks
|
|
3990
|
+
* stable sections with Anthropic-compatible ephemeral cache control. DSH's
|
|
3991
|
+
* adapter has one already-folded system string rather than the CLI's internal
|
|
3992
|
+
* section list, so preserve that text as one cacheable section. This does not
|
|
3993
|
+
* recover the CLI's separate static/dynamic boundaries or guarantee a hit.
|
|
3994
|
+
*/
|
|
3995
|
+
function cliSystem(systemText) {
|
|
3996
|
+
if (!systemText) return "";
|
|
3997
|
+
return [{
|
|
3998
|
+
type: "text",
|
|
3999
|
+
text: systemText,
|
|
4000
|
+
cache_control: { type: "ephemeral" }
|
|
4001
|
+
}];
|
|
4002
|
+
}
|
|
3954
4003
|
/** Build the legacy CLI (`/alpha/generate`) request body for one call. */
|
|
3955
4004
|
async function buildCliBody(options, connection, facts) {
|
|
3956
4005
|
return {
|
|
@@ -3977,13 +4026,13 @@ async function buildCliBody(options, connection, facts) {
|
|
|
3977
4026
|
description: tool.description,
|
|
3978
4027
|
input_schema: toolParametersSchema(tool.parameters)
|
|
3979
4028
|
})),
|
|
3980
|
-
system: facts.systemText,
|
|
4029
|
+
system: cliSystem(facts.systemText),
|
|
3981
4030
|
max_tokens: facts.maxTokens,
|
|
3982
4031
|
temperature: options.temperature ?? .3,
|
|
3983
4032
|
stream: true,
|
|
3984
4033
|
...facts.reasoningEffort ? { reasoning_effort: facts.reasoningEffort } : {}
|
|
3985
4034
|
},
|
|
3986
|
-
threadId:
|
|
4035
|
+
threadId: cliThreadId(options.sessionId)
|
|
3987
4036
|
};
|
|
3988
4037
|
}
|
|
3989
4038
|
/** Build the documented Provider Chat Completions request body for one call. */
|
|
@@ -4012,12 +4061,11 @@ async function buildOpenAIBody(options, facts) {
|
|
|
4012
4061
|
}
|
|
4013
4062
|
/**
|
|
4014
4063
|
* One pre-stream connect attempt: POST the body and wait for response
|
|
4015
|
-
* headers only (`requestTimeoutMs` must never bound the body stream
|
|
4016
|
-
* the
|
|
4017
|
-
*
|
|
4018
|
-
*
|
|
4019
|
-
*
|
|
4020
|
-
* streaming tail calls cleanup.
|
|
4064
|
+
* headers only (`requestTimeoutMs` must never bound the body stream). Returns
|
|
4065
|
+
* the live response plus its cleanup, or the rejection facts for the rotation
|
|
4066
|
+
* loop to classify. Every failure path cleans up before returning or throwing;
|
|
4067
|
+
* on success the caller-abort listener outlives the connect phase (it aborts a
|
|
4068
|
+
* stalled body read), so the streaming tail calls cleanup.
|
|
4021
4069
|
*/
|
|
4022
4070
|
async function connectGenerate(deps, key, protocol, body) {
|
|
4023
4071
|
const { options, connection, fetchImpl } = deps;
|
|
@@ -4074,6 +4122,7 @@ async function connectGenerate(deps, key, protocol, body) {
|
|
|
4074
4122
|
if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw new LlmError(`Command Code API request to ${endpoint} did not respond within ${connection.requestTimeoutMs}ms: ${errorChain(error)};Command Code API 请求在 ${connection.requestTimeoutMs} 毫秒内未收到响应——通常是网络或代理问题,请检查后重试`, "TIMEOUT", { cause: error });
|
|
4075
4123
|
throw new LlmError(`Command Code API request to ${endpoint} failed: ${errorChain(error)};Command Code API 请求连接失败——通常是网络或代理问题,请检查网络或代理设置后重试`, "TRANSPORT", { cause: error });
|
|
4076
4124
|
}
|
|
4125
|
+
deps.onResponse(response);
|
|
4077
4126
|
if (!response.ok) {
|
|
4078
4127
|
const errText = await response.text().catch(() => "");
|
|
4079
4128
|
cleanup();
|
|
@@ -4203,13 +4252,13 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4203
4252
|
* unbounded loop): `RATE_LIMIT`/`SERVER`/`TIMEOUT`/`TRANSPORT`/
|
|
4204
4253
|
* `EMPTY_RESPONSE` retry up to 1000 times with waits doubling from 500 ms
|
|
4205
4254
|
* and capping at 15 minutes (±10% jitter), so an exhausted 5-hour window
|
|
4206
|
-
* recovers in-session instead of failing after two tries. Permanent
|
|
4207
|
-
*
|
|
4208
|
-
*
|
|
4209
|
-
*
|
|
4210
|
-
*
|
|
4211
|
-
*
|
|
4212
|
-
*
|
|
4255
|
+
* recovers in-session instead of failing after two tries. Permanent failures
|
|
4256
|
+
* (an invalid key's `INVALID_CREDENTIAL`, `UNSUPPORTED_CONTENT`, plan
|
|
4257
|
+
* rejections) are absent from the whitelist and surface immediately instead
|
|
4258
|
+
* of looping. Waits the pool/adapter attach as `providerRetryAfterMs` are
|
|
4259
|
+
* honored verbatim at or below the 15-minute cap and never attached above it
|
|
4260
|
+
* (in normal mode a longer attached wait makes the executor abandon the
|
|
4261
|
+
* retry outright — see RETRY_MAX_DELAY_MS).
|
|
4213
4262
|
*
|
|
4214
4263
|
* Captured once at route registration (dsh-llm snapshots this value), so a
|
|
4215
4264
|
* future config knob for it would apply on profile restart, not per request.
|
|
@@ -4240,12 +4289,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4240
4289
|
* session reports far less context than it is actually carrying. The price is
|
|
4241
4290
|
* computed at the dimensions this route would send (the same request target
|
|
4242
4291
|
* `readImageRequest` encodes to), so the estimate tracks the wire rather than
|
|
4243
|
-
* the stored original.
|
|
4244
|
-
*
|
|
4245
|
-
* Both payload generations are handled, because they disagree: ≤0.1.5 hands
|
|
4246
|
-
* over bare `ImageAttachmentRef`s, ≥0.1.6 hands over `ImageBlock`s that also
|
|
4247
|
-
* carry the surface's `offloaded` mark. Returning a price per occurrence, in
|
|
4248
|
-
* order, is a hard requirement — the meter throws when the counts differ.
|
|
4292
|
+
* the stored original. One price per occurrence, in order, is a hard
|
|
4293
|
+
* requirement — the meter throws when the counts differ.
|
|
4249
4294
|
*/
|
|
4250
4295
|
imageRequestPricing(_provider, model) {
|
|
4251
4296
|
const imageCapable = KNOWN_IMAGE_MODELS.has(model);
|
|
@@ -4331,10 +4376,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4331
4376
|
}
|
|
4332
4377
|
/**
|
|
4333
4378
|
* Fetch one account endpoint and parse its JSON body. Returns the HTTP
|
|
4334
|
-
* status alongside the parsed record so each caller applies its own
|
|
4335
|
-
*
|
|
4336
|
-
*
|
|
4337
|
-
*
|
|
4379
|
+
* status alongside the parsed record so each caller applies its own failure
|
|
4380
|
+
* accounting: the billing probe fails open silently, the usage report books
|
|
4381
|
+
* failures per endpoint. Non-2xx and invalid JSON bodies come back without a
|
|
4382
|
+
* record; only a fetch failure propagates to the caller.
|
|
4338
4383
|
*
|
|
4339
4384
|
* `timeoutMs` is the caller's budget for this one request. The default is the
|
|
4340
4385
|
* CATALOG probe's short cap, which fits the picker's fail-open reads; the
|
|
@@ -4371,9 +4416,6 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4371
4416
|
* per picker load). `undefined` means the filter cannot be evaluated at all
|
|
4372
4417
|
* — no key resolved — which {@link modelVisibleForAnyAccount} reads as
|
|
4373
4418
|
* "show everything".
|
|
4374
|
-
*
|
|
4375
|
-
* The serving account is only the first entry: with several accounts the
|
|
4376
|
-
* model list must not depend on which of them rotation happens to be using.
|
|
4377
4419
|
*/
|
|
4378
4420
|
async loadPoolBillingAccess() {
|
|
4379
4421
|
const keys = await this.poolAccountKeys();
|
|
@@ -4486,10 +4528,9 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4486
4528
|
* but a hard refusal. `/alpha/generate` routes every one of them, so this
|
|
4487
4529
|
* costs nothing.
|
|
4488
4530
|
*
|
|
4489
|
-
* That decision is deliberately NOT written to `protocolCache
|
|
4490
|
-
* keyed by API key alone
|
|
4491
|
-
*
|
|
4492
|
-
* which the Provider API serves correctly — off it until the entry expired.
|
|
4531
|
+
* That decision is deliberately NOT written to `protocolCache`, which is
|
|
4532
|
+
* keyed by API key alone: remembering it would pin the whole ACCOUNT to the
|
|
4533
|
+
* CLI transport and drag every other model off it until the entry expired.
|
|
4493
4534
|
*/
|
|
4494
4535
|
resolveProtocol(apiKey, model) {
|
|
4495
4536
|
const forced = this.deps.options().protocol;
|
|
@@ -4515,18 +4556,14 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4515
4556
|
* the default resolves the currently active account.
|
|
4516
4557
|
*
|
|
4517
4558
|
* Two failure modes are NOT "the network is down", and the report must say
|
|
4518
|
-
* so rather than let {@link classifyTotalFailure} blame the connection:
|
|
4519
|
-
*
|
|
4520
|
-
*
|
|
4521
|
-
*
|
|
4522
|
-
*
|
|
4523
|
-
*
|
|
4524
|
-
*
|
|
4525
|
-
*
|
|
4526
|
-
* (`connection.requestTimeoutMs`, default 60 s), not the catalog's 10 s
|
|
4527
|
-
* probe budget: on a slow link a 10 s cap made every account query time
|
|
4528
|
-
* out while chat kept working, which is exactly a "check your network"
|
|
4529
|
-
* banner that never clears.
|
|
4559
|
+
* so rather than let {@link classifyTotalFailure} blame the connection: a
|
|
4560
|
+
* key that no HTTP header can carry (a stray newline from a paste, a
|
|
4561
|
+
* full-width character) makes `fetch` throw a `TypeError` before any I/O,
|
|
4562
|
+
* for every endpoint at once, so this path runs `assertUsableApiKey` too and
|
|
4563
|
+
* answers `blocked: 'invalid-key'`; and the four endpoints get the SAME
|
|
4564
|
+
* per-request budget as a chat call (`connection.requestTimeoutMs`, default
|
|
4565
|
+
* 60 s), not the catalog's 10 s probe budget, because on a slow link a 10 s
|
|
4566
|
+
* cap made every account query time out while chat kept working.
|
|
4530
4567
|
*/
|
|
4531
4568
|
async getUsage(apiKey) {
|
|
4532
4569
|
const connection = this.deps.options();
|
|
@@ -4604,17 +4641,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4604
4641
|
* the same time (the two are metered separately), and reading only the
|
|
4605
4642
|
* shorter one revived such an account as "usable" — the pool then handed it
|
|
4606
4643
|
* out, the provider rejected the request again, and the next all-marked pass
|
|
4607
|
-
* revived it once more
|
|
4608
|
-
*
|
|
4609
|
-
*
|
|
4610
|
-
*
|
|
4611
|
-
*
|
|
4644
|
+
* revived it once more (issue #51's follow-up: "切到一个不可用账号").
|
|
4645
|
+
* `exceeded` is therefore true when ANY published window is exceeded, and
|
|
4646
|
+
* `resetAt` is the LATEST reset among the exceeded ones — the binding
|
|
4647
|
+
* constraint, because a clear five-hour window buys nothing while the weekly
|
|
4648
|
+
* quota is spent.
|
|
4612
4649
|
*
|
|
4613
4650
|
* Returns `undefined` when the probe itself failed (transport, non-200, or a
|
|
4614
4651
|
* payload with no window limits at all) — a failed probe never changes pool
|
|
4615
|
-
* state. The
|
|
4616
|
-
*
|
|
4617
|
-
* time, and the server remains the final gate on them.
|
|
4652
|
+
* state. The credit balances are deliberately NOT part of this answer: they
|
|
4653
|
+
* carry no reset time, and the server remains the final gate on them.
|
|
4618
4654
|
*/
|
|
4619
4655
|
async probeWindowLimits(apiKey) {
|
|
4620
4656
|
try {
|
|
@@ -4680,73 +4716,110 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4680
4716
|
readImage
|
|
4681
4717
|
});
|
|
4682
4718
|
let body = await buildBody(protocol);
|
|
4719
|
+
const trace = openStreamTrace(`${options.provider}/${options.model}`);
|
|
4683
4720
|
const tried = /* @__PURE__ */ new Set();
|
|
4721
|
+
let attemptNumber = 0;
|
|
4722
|
+
let responseMetadata = {
|
|
4723
|
+
protocol,
|
|
4724
|
+
headers: {}
|
|
4725
|
+
};
|
|
4684
4726
|
const connectDeps = {
|
|
4685
4727
|
options,
|
|
4686
4728
|
connection,
|
|
4687
|
-
fetchImpl: this.fetchImpl
|
|
4729
|
+
fetchImpl: this.fetchImpl,
|
|
4730
|
+
onResponse: (response) => {
|
|
4731
|
+
responseMetadata = {
|
|
4732
|
+
protocol,
|
|
4733
|
+
headers: responseIdentifiers(response.headers)
|
|
4734
|
+
};
|
|
4735
|
+
trace.record("response", {
|
|
4736
|
+
protocol,
|
|
4737
|
+
endpoint: protocol === "cli" ? `${connection.apiBase}/alpha/generate` : `${connection.apiBase}/provider/v1/chat/completions`,
|
|
4738
|
+
status: response.status,
|
|
4739
|
+
attempt: attemptNumber,
|
|
4740
|
+
accountsTried: tried.size,
|
|
4741
|
+
maxTokens,
|
|
4742
|
+
...reasoningEffort === void 0 ? {} : { reasoningEffort },
|
|
4743
|
+
headers: responseMetadata.headers
|
|
4744
|
+
});
|
|
4745
|
+
}
|
|
4688
4746
|
};
|
|
4689
4747
|
let connected;
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
}
|
|
4703
|
-
if (attempt.status === 413 && REQUEST_IMAGE_BUDGETS.length > 1) {
|
|
4704
|
-
const missing = this.surfaceOffload.requiredImageOffload(requestOptions.messages, coreBudget(REQUEST_IMAGE_BUDGETS[1]), imageVersionBytes);
|
|
4705
|
-
if (missing > 0) throw imageOffloadRequired(missing);
|
|
4706
|
-
}
|
|
4707
|
-
const rotate = this.deps.rotateApiKey;
|
|
4708
|
-
const rejection = classifyAccountRejection(attempt.status, attempt.errText);
|
|
4709
|
-
if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
|
|
4710
|
-
const next = await rotate(apiKey, rejection.reason, connection, options.model, {
|
|
4711
|
-
tried: [...tried],
|
|
4712
|
-
...rejection.resetAtMs !== void 0 && { resetAtMs: rejection.resetAtMs }
|
|
4748
|
+
try {
|
|
4749
|
+
for (;;) {
|
|
4750
|
+
tried.add(apiKey);
|
|
4751
|
+
attemptNumber++;
|
|
4752
|
+
responseMetadata = {
|
|
4753
|
+
protocol,
|
|
4754
|
+
headers: {}
|
|
4755
|
+
};
|
|
4756
|
+
if (trace.enabled) trace.record("request", {
|
|
4757
|
+
attempt: attemptNumber,
|
|
4758
|
+
sessionId: options.sessionId,
|
|
4759
|
+
...requestFingerprint(protocol, body)
|
|
4713
4760
|
});
|
|
4714
|
-
|
|
4715
|
-
|
|
4761
|
+
const attempt = await connectGenerate(connectDeps, apiKey, protocol, body);
|
|
4762
|
+
if ("response" in attempt) {
|
|
4763
|
+
connected = attempt;
|
|
4764
|
+
break;
|
|
4765
|
+
}
|
|
4766
|
+
if (protocol === "openai" && isUpgradeRequiredError(attempt.status, attempt.errText)) {
|
|
4767
|
+
protocol = "cli";
|
|
4768
|
+
this.rememberProtocol(apiKey, true);
|
|
4769
|
+
body = await buildBody("cli");
|
|
4716
4770
|
continue;
|
|
4717
4771
|
}
|
|
4772
|
+
if (attempt.status === 413 && REQUEST_IMAGE_BUDGETS.length > 1) {
|
|
4773
|
+
const missing = this.surfaceOffload.requiredImageOffload(requestOptions.messages, coreBudget(REQUEST_IMAGE_BUDGETS[1]), imageVersionBytes);
|
|
4774
|
+
if (missing > 0) throw imageOffloadRequired(missing);
|
|
4775
|
+
}
|
|
4776
|
+
const rotate = this.deps.rotateApiKey;
|
|
4777
|
+
const rejection = classifyAccountRejection(attempt.status, attempt.errText);
|
|
4778
|
+
if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
|
|
4779
|
+
const next = await rotate(apiKey, rejection.reason, connection, options.model, {
|
|
4780
|
+
tried: [...tried],
|
|
4781
|
+
...rejection.resetAtMs !== void 0 && { resetAtMs: rejection.resetAtMs }
|
|
4782
|
+
});
|
|
4783
|
+
if (next !== void 0 && !tried.has(next)) {
|
|
4784
|
+
apiKey = next;
|
|
4785
|
+
continue;
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled") && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
|
|
4789
|
+
const reset = rejection.resetAtMs;
|
|
4790
|
+
const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
|
|
4791
|
+
const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
|
|
4792
|
+
const window = rejection.reason === "rate-limit";
|
|
4793
|
+
throw new LlmError((window ? "llm-commandcode: the Command Code account is rate limited" : "llm-commandcode: the Command Code account is rate limited (429) — the provider did not report an exhausted usage window") + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`) + (window ? ";当前 Command Code 账户已被限流" : ";当前 Command Code 账户被限流(429),服务商未报告用量窗口用尽") + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), "RATE_LIMIT", wait > 0 ? { providerRetryAfterMs: wait } : void 0);
|
|
4794
|
+
}
|
|
4795
|
+
throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
|
|
4718
4796
|
}
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
|
|
4797
|
+
} catch (error) {
|
|
4798
|
+
trace.record("connect-error", {
|
|
4799
|
+
message: boundTraceText(errorChain(error)),
|
|
4800
|
+
responseMetadata
|
|
4801
|
+
});
|
|
4802
|
+
trace.close();
|
|
4803
|
+
throw options.signal?.aborted ? error : failureWithRequestId(error, responseMetadata);
|
|
4727
4804
|
}
|
|
4728
4805
|
if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
|
|
4729
4806
|
const { response, cleanup } = connected;
|
|
4730
4807
|
if (!response.body) {
|
|
4731
4808
|
cleanup();
|
|
4732
|
-
|
|
4809
|
+
trace.record("end", {
|
|
4810
|
+
outcome: "empty",
|
|
4811
|
+
code: "PROVIDER_PROTOCOL_ERROR"
|
|
4812
|
+
});
|
|
4813
|
+
trace.close();
|
|
4814
|
+
throw failureWithRequestId(new LlmError("Command Code API returned no response body", "PROVIDER_PROTOCOL_ERROR"), responseMetadata);
|
|
4733
4815
|
}
|
|
4734
4816
|
const reader = response.body.getReader();
|
|
4735
4817
|
const decoder = new TextDecoder();
|
|
4736
4818
|
let buffer = "";
|
|
4737
|
-
const trace = openStreamTrace(`${options.provider}/${options.model}`);
|
|
4738
4819
|
const streamStartedAt = Date.now();
|
|
4739
4820
|
let chunkCount = 0;
|
|
4740
4821
|
let totalBytes = 0;
|
|
4741
4822
|
let lastChunkAt = streamStartedAt;
|
|
4742
|
-
trace.record("response", {
|
|
4743
|
-
protocol,
|
|
4744
|
-
endpoint: protocol === "cli" ? `${connection.apiBase}/alpha/generate` : `${connection.apiBase}/provider/v1/chat/completions`,
|
|
4745
|
-
status: response.status,
|
|
4746
|
-
attempts: tried.size,
|
|
4747
|
-
maxTokens,
|
|
4748
|
-
...reasoningEffort === void 0 ? {} : { reasoningEffort }
|
|
4749
|
-
});
|
|
4750
4823
|
let idleTimer;
|
|
4751
4824
|
let idleFired = false;
|
|
4752
4825
|
const armIdle = () => {
|
|
@@ -4768,6 +4841,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4768
4841
|
let usageEmitted = false;
|
|
4769
4842
|
let endRecorded = false;
|
|
4770
4843
|
function* handle(event) {
|
|
4844
|
+
captureResponseIdentifiers(responseMetadata, event);
|
|
4771
4845
|
for (const chunk of handleEvent(asm, protocol, event)) if (chunk.type === "finish") finish = chunk;
|
|
4772
4846
|
else if (chunk.type === "usage") usage = chunk.usage;
|
|
4773
4847
|
else yield chunk;
|
|
@@ -4838,11 +4912,15 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4838
4912
|
chunks: chunkCount,
|
|
4839
4913
|
bytes: totalBytes,
|
|
4840
4914
|
totalMs: Date.now() - streamStartedAt,
|
|
4841
|
-
sawContent: asm.sawContent
|
|
4915
|
+
sawContent: asm.sawContent,
|
|
4916
|
+
responseMetadata
|
|
4842
4917
|
});
|
|
4843
4918
|
endRecorded = true;
|
|
4844
4919
|
if (failure !== void 0) throw failure;
|
|
4845
|
-
yield
|
|
4920
|
+
yield hasResponseIdentifiers(responseMetadata) ? {
|
|
4921
|
+
...finish,
|
|
4922
|
+
replayState: { response: responseMetadata }
|
|
4923
|
+
} : finish;
|
|
4846
4924
|
} else {
|
|
4847
4925
|
const elapsed = Date.now() - streamStartedAt;
|
|
4848
4926
|
const detail = `${chunkCount} chunk(s), ${totalBytes} bytes, ${elapsed}ms`;
|
|
@@ -4851,7 +4929,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4851
4929
|
chunks: chunkCount,
|
|
4852
4930
|
bytes: totalBytes,
|
|
4853
4931
|
totalMs: elapsed,
|
|
4854
|
-
sawContent: asm.sawContent
|
|
4932
|
+
sawContent: asm.sawContent,
|
|
4933
|
+
responseMetadata
|
|
4855
4934
|
});
|
|
4856
4935
|
endRecorded = true;
|
|
4857
4936
|
if (!asm.sawContent) throw new LlmError("Command Code returned an empty response;Command Code 返回了空响应,重试通常可恢复", "EMPTY_RESPONSE");
|
|
@@ -4871,9 +4950,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4871
4950
|
sawContent: asm.sawContent,
|
|
4872
4951
|
chunks: chunkCount,
|
|
4873
4952
|
bytes: totalBytes,
|
|
4874
|
-
totalMs: Date.now() - streamStartedAt
|
|
4953
|
+
totalMs: Date.now() - streamStartedAt,
|
|
4954
|
+
responseMetadata
|
|
4875
4955
|
});
|
|
4876
|
-
throw error;
|
|
4956
|
+
throw options.signal?.aborted ? error : failureWithRequestId(error, responseMetadata);
|
|
4877
4957
|
} finally {
|
|
4878
4958
|
clearIdle();
|
|
4879
4959
|
cleanup();
|
|
@@ -4890,16 +4970,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4890
4970
|
* status alone cannot distinguish them. The status also decides the failure
|
|
4891
4971
|
* CODE via {@link httpErrorCode}, which is what the retry policy routes on —
|
|
4892
4972
|
* a transient 5xx must not be reported as a permanent provider rejection. A
|
|
4893
|
-
* 429's `Retry-After` header rides along as
|
|
4894
|
-
*
|
|
4895
|
-
*
|
|
4896
|
-
*
|
|
4897
|
-
* outright instead of falling back to local backoff.
|
|
4973
|
+
* 429's `Retry-After` header rides along as `providerRetryAfterMs` so
|
|
4974
|
+
* dsh-llm-retry can wait exactly that long instead of guessing at the backoff
|
|
4975
|
+
* cadence — capped at RETRY_MAX_DELAY_MS, because in normal mode a longer
|
|
4976
|
+
* attached wait makes the executor abandon the retry outright.
|
|
4898
4977
|
*
|
|
4899
4978
|
* One body is read, not just its status: a client-side rejection whose
|
|
4900
4979
|
* `error.code`/`type`/`message` names the model context window is reported as
|
|
4901
|
-
* `CONTEXT_WINDOW_EXCEEDED
|
|
4902
|
-
*
|
|
4980
|
+
* `CONTEXT_WINDOW_EXCEEDED`, because that failure has a recovery the harness
|
|
4981
|
+
* performs and a generic provider error does not. Every message built here is
|
|
4982
|
+
* bilingual — the harness renders it verbatim in its retry chrome.
|
|
4903
4983
|
*/
|
|
4904
4984
|
function generateHttpError(status, errText, retryAfterMs) {
|
|
4905
4985
|
let providerCode;
|
|
@@ -4932,16 +5012,11 @@ function generateHttpError(status, errText, retryAfterMs) {
|
|
|
4932
5012
|
* status, never the provider's `error.type` — so a status that arrives here as
|
|
4933
5013
|
* a permanent code is never retried, whatever it says about itself.
|
|
4934
5014
|
*
|
|
4935
|
-
* 5xx is the provider's own "
|
|
4936
|
-
* 520-527 included
|
|
4937
|
-
*
|
|
4938
|
-
*
|
|
4939
|
-
*
|
|
4940
|
-
* failure that asks to be retried the only one that never was, while the same
|
|
4941
|
-
* status arriving as an in-band stream `error` event was classified retryable
|
|
4942
|
-
* `SERVER` (see the `error` case of {@link handleCliEvent}). 408 is the same
|
|
4943
|
-
* story for the request itself. Everything else stays permanent: a 400/403/404/
|
|
4944
|
-
* 409/422 rejection repeats identically on every attempt.
|
|
5015
|
+
* 5xx is the provider's own "temporarily unavailable" class (Cloudflare's
|
|
5016
|
+
* 520-527 included) and the only sane answer to it is the byte-identical
|
|
5017
|
+
* resend the retry policy exists to make; 408 is the same story for the
|
|
5018
|
+
* request itself. Everything else stays permanent: a 400/403/404/409/422
|
|
5019
|
+
* rejection repeats identically on every attempt.
|
|
4945
5020
|
*/
|
|
4946
5021
|
function httpErrorCode(status) {
|
|
4947
5022
|
if (status === 429) return "RATE_LIMIT";
|
|
@@ -5206,12 +5281,6 @@ function emptyCompletionError(reason, maxTokens, usage) {
|
|
|
5206
5281
|
return new LlmError(`Command Code finished without producing answer text or a tool call (${detail});Command Code 返回了空响应(可能只有思考内容),重试通常可恢复`, "EMPTY_RESPONSE");
|
|
5207
5282
|
}
|
|
5208
5283
|
/**
|
|
5209
|
-
* True when a Provider API pre-stream rejection is Command Code's Go-plan
|
|
5210
|
-
* gate (`upgrade_required`). Only this exact class of rejection should fall
|
|
5211
|
-
* back to the CLI /alpha/generate transport; other 4xx/5xx must surface as
|
|
5212
|
-
* ordinary errors so real account/model problems are not masked.
|
|
5213
|
-
*/
|
|
5214
|
-
/**
|
|
5215
5284
|
* Classify a pre-stream rejection that is about the ACCOUNT rather than about
|
|
5216
5285
|
* the request, using the official CLI's own rules (command-code@1.56.0:
|
|
5217
5286
|
* `parseWindowLimitError`, `isInsufficientCreditsRequestError`,
|
|
@@ -5221,46 +5290,31 @@ function emptyCompletionError(reason, maxTokens, usage) {
|
|
|
5221
5290
|
* oversized body or a context overflow must fail fast instead of walking the
|
|
5222
5291
|
* whole account pool.
|
|
5223
5292
|
*
|
|
5224
|
-
*
|
|
5225
|
-
*
|
|
5226
|
-
*
|
|
5227
|
-
*
|
|
5228
|
-
*
|
|
5229
|
-
*
|
|
5230
|
-
* function's:
|
|
5293
|
+
* It must recognize the whole account-scoped class, not just 429/401: when it
|
|
5294
|
+
* did not, an account that could not pay, or could not run the model, stayed
|
|
5295
|
+
* "usable" in the pool and was handed out again on the next request (issue
|
|
5296
|
+
* #51's follow-up). The four reasons are defined on
|
|
5297
|
+
* {@link AccountRotationReason}; what this function owes them is the evidence
|
|
5298
|
+
* split:
|
|
5231
5299
|
*
|
|
5232
|
-
* - `rate-limit
|
|
5233
|
-
*
|
|
5234
|
-
*
|
|
5235
|
-
*
|
|
5236
|
-
*
|
|
5237
|
-
*
|
|
5238
|
-
*
|
|
5239
|
-
* NOT
|
|
5240
|
-
*
|
|
5241
|
-
* `resolveWindowLabel` can name one, and a burst/model/spend limiter the
|
|
5242
|
-
* billing endpoint cannot see is exactly this shape (issue #54's report: a
|
|
5243
|
-
* 429 whose probe read five-hour 2% and weekly 10% while the error claimed
|
|
5244
|
-
* both were exhausted).
|
|
5245
|
-
* - `invalid-credential`: 401; the key itself is bad.
|
|
5246
|
-
* - `unavailable`: the key is fine but this account cannot serve: no credits
|
|
5247
|
-
* (`400 Insufficient credits`, the codes `INSUFFICIENT_CREDITS`,
|
|
5248
|
-
* `USAGE_EXCEEDED`, `PREMIUM_CREDITS_EXHAUSTED`, or a spend-cap wording)
|
|
5249
|
-
* or a model outside its plan (`MODEL_NOT_IN_PLAN`). Rotating must NOT mark
|
|
5250
|
-
* the account: the fact is about the model or the balance, it can change
|
|
5251
|
-
* without the key changing, and a `:free` model can still be served by an
|
|
5252
|
-
* account with an empty balance. These codes are recognized on EVERY
|
|
5253
|
-
* status — a 5xx that proxies the provider's own credits/plan body is
|
|
5254
|
-
* still an account fact, and only the prose scan is confined to 4xx.
|
|
5300
|
+
* - `rate-limit` / `throttled`: the code `RATE_LIMITED` is the authoritative
|
|
5301
|
+
* signal — the CLI accepts the code OR a 429 status — and only a body that
|
|
5302
|
+
* NAMES a window (`error.rateLimit.window`, see {@link readWindowLimitEvidence})
|
|
5303
|
+
* may be called a spent one. A bare 429 is a burst/model/spend limiter the
|
|
5304
|
+
* billing endpoint cannot see, so it stays `throttled` and the host claims
|
|
5305
|
+
* no window (issue #54).
|
|
5306
|
+
* - `unavailable`: no credits, or a model outside this account's plan, so it
|
|
5307
|
+
* rotates but must NOT mark the key — the fact is about the model or the
|
|
5308
|
+
* balance, and either can change without the key changing.
|
|
5255
5309
|
*/
|
|
5256
5310
|
/**
|
|
5257
5311
|
* The provider's structured "this account cannot serve this request" codes.
|
|
5258
5312
|
*
|
|
5259
5313
|
* Status-independent on purpose, mirroring the CLI's classifier, which reads
|
|
5260
|
-
* the code before any status guard
|
|
5261
|
-
*
|
|
5262
|
-
*
|
|
5263
|
-
*
|
|
5314
|
+
* the code before any status guard: a 5xx carrying one of these must rotate
|
|
5315
|
+
* past the account instead of being retried as a transient provider failure,
|
|
5316
|
+
* because the same request against the same account cannot succeed. Only the
|
|
5317
|
+
* prose scan below is confined to 4xx.
|
|
5264
5318
|
*/
|
|
5265
5319
|
const ACCOUNT_UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
|
|
5266
5320
|
"USAGE_EXCEEDED",
|
|
@@ -5310,12 +5364,12 @@ const MAX_TRUSTED_RESET_MS = 2592e6;
|
|
|
5310
5364
|
* `parseWindowLimitError` → `resolveWindowLabel`/`extractResetAtMs` pair.
|
|
5311
5365
|
*
|
|
5312
5366
|
* `window` is present only when the body actually names one: a
|
|
5313
|
-
* `error.rateLimit.window` of `fiveHour`/`weekly`/`daily`, or the
|
|
5314
|
-
*
|
|
5315
|
-
*
|
|
5316
|
-
*
|
|
5317
|
-
*
|
|
5318
|
-
*
|
|
5367
|
+
* `error.rateLimit.window` of `fiveHour`/`weekly`/`daily`, or the "usage limit
|
|
5368
|
+
* for your plan" wording (read as `weekly` when it says so, else `fiveHour`).
|
|
5369
|
+
* That is the CLI's own bar for calling a rejection a WINDOW limit, and it is
|
|
5370
|
+
* what keeps a bare 429 — a burst limiter, a model-level limit, a spend cap
|
|
5371
|
+
* `windowLimits` cannot show — from being reported as an exhausted window
|
|
5372
|
+
* (issue #54).
|
|
5319
5373
|
*
|
|
5320
5374
|
* `resetAtMs` is the reset the body publishes: `error.rateLimit.reset` is
|
|
5321
5375
|
* SECONDS (`1e3 * reset`, like the CLI), and a `resets at <ISO>` stamp in the
|
|
@@ -5346,6 +5400,13 @@ function readWindowLimitEvidence(errText) {
|
|
|
5346
5400
|
...resetAtMs === void 0 ? {} : { resetAtMs }
|
|
5347
5401
|
};
|
|
5348
5402
|
}
|
|
5403
|
+
/**
|
|
5404
|
+
* True when a pre-stream rejection is Command Code's Go-plan gate
|
|
5405
|
+
* (`upgrade_required`) — the only plan without Provider API access. Only this
|
|
5406
|
+
* class of rejection should fall back to the CLI `/alpha/generate` transport;
|
|
5407
|
+
* other 4xx/5xx must surface as ordinary errors so real account/model
|
|
5408
|
+
* problems are not masked.
|
|
5409
|
+
*/
|
|
5349
5410
|
function isUpgradeRequiredError(status, errText) {
|
|
5350
5411
|
if (status !== 403) return false;
|
|
5351
5412
|
const lower = errText.toLowerCase();
|
|
@@ -5361,7 +5422,9 @@ function isUpgradeRequiredError(status, errText) {
|
|
|
5361
5422
|
const message = stringValue(error.message) ?? "";
|
|
5362
5423
|
if (message.toLowerCase().includes("go plan") && message.toLowerCase().includes("api access")) return true;
|
|
5363
5424
|
}
|
|
5364
|
-
} catch {
|
|
5425
|
+
} catch {
|
|
5426
|
+
return false;
|
|
5427
|
+
}
|
|
5365
5428
|
return false;
|
|
5366
5429
|
}
|
|
5367
5430
|
/** Map an OpenAI Chat Completions usage payload to harness disjoint counts. */
|
|
@@ -5414,7 +5477,6 @@ const commandcodeCommand = {
|
|
|
5414
5477
|
weeklyLine: " 📅 每周 {used} / {cap}{warn}",
|
|
5415
5478
|
windowBarLine: " └ {bar} 重置 {when}",
|
|
5416
5479
|
exceededWarning: " ⚠️ 超限!",
|
|
5417
|
-
resetSuffix: "重置 {when}",
|
|
5418
5480
|
partialFailures: "⚠️ 部分端点失败: {list}",
|
|
5419
5481
|
noData: "(无数据 — 请检查 API 密钥)",
|
|
5420
5482
|
errorText: "获取 Command Code 用量失败:{message}"
|
|
@@ -5446,25 +5508,19 @@ const commandcodeCommand = {
|
|
|
5446
5508
|
weeklyLine: " 📅 Weekly {used} / {cap}{warn}",
|
|
5447
5509
|
windowBarLine: " └ {bar} resets {when}",
|
|
5448
5510
|
exceededWarning: " ⚠️ exceeded!",
|
|
5449
|
-
resetSuffix: "resets {when}",
|
|
5450
5511
|
partialFailures: "⚠️ some endpoints failed: {list}",
|
|
5451
5512
|
noData: "(no data — check your API key)",
|
|
5452
5513
|
errorText: "Could not fetch Command Code usage: {message}"
|
|
5453
5514
|
}
|
|
5454
5515
|
};
|
|
5455
5516
|
/**
|
|
5456
|
-
* Resolve the active locale for a Host-side command run
|
|
5457
|
-
*
|
|
5458
|
-
*
|
|
5459
|
-
* the conventional fallback (`'zh'`, matching the existing single-language
|
|
5460
|
-
* behavior so unconfigured deployments keep their current output).
|
|
5517
|
+
* Resolve the active locale for a Host-side command run: explicit `override`
|
|
5518
|
+
* (from `Config.lang`) → `LC_ALL` → `LANG` → the `'zh'` fallback that keeps
|
|
5519
|
+
* unconfigured deployments on their current output.
|
|
5461
5520
|
*
|
|
5462
|
-
*
|
|
5463
|
-
*
|
|
5464
|
-
*
|
|
5465
|
-
* already has Chinese in the terminal is the closest sensible default;
|
|
5466
|
-
* a Western shell that happens to be neither keeps the existing Chinese
|
|
5467
|
-
* output rather than swapping to half-translated English).
|
|
5521
|
+
* Matched on the leading tag only — `zh_CN.UTF-8`, `zh-Hans` and `zh` all
|
|
5522
|
+
* map to `'zh'`, anything starting with `en` maps to `'en'`, everything else
|
|
5523
|
+
* falls back to `'zh'` rather than to half-translated English.
|
|
5468
5524
|
*/
|
|
5469
5525
|
function pickCommandLocale(override, env = process.env) {
|
|
5470
5526
|
if (override === "zh" || override === "en") return override;
|
|
@@ -5477,11 +5533,10 @@ function commandCopy(locale, key) {
|
|
|
5477
5533
|
}
|
|
5478
5534
|
//#endregion
|
|
5479
5535
|
//#region src/commands.ts
|
|
5480
|
-
/** Format a dollar amount. */
|
|
5481
5536
|
function money(value) {
|
|
5482
5537
|
return `$${value.toFixed(4)}`;
|
|
5483
5538
|
}
|
|
5484
|
-
/**
|
|
5539
|
+
/** Compact dollar amount (2 decimals), for the summary and window rows. */
|
|
5485
5540
|
function moneyShort(value) {
|
|
5486
5541
|
return `$${value.toFixed(2)}`;
|
|
5487
5542
|
}
|
|
@@ -5493,14 +5548,14 @@ function tokensCompact(value) {
|
|
|
5493
5548
|
return String(value);
|
|
5494
5549
|
}
|
|
5495
5550
|
/**
|
|
5496
|
-
*
|
|
5497
|
-
*
|
|
5551
|
+
* Success-rate percentage (already in percent units): at most two decimals,
|
|
5552
|
+
* trailing zeros trimmed — mirrors `formatSuccessRate` in
|
|
5498
5553
|
* `./client/usage.ts`, which the settings card uses.
|
|
5499
5554
|
*/
|
|
5500
5555
|
function successRateText(value) {
|
|
5501
5556
|
return String(Number(value.toFixed(2)));
|
|
5502
5557
|
}
|
|
5503
|
-
/**
|
|
5558
|
+
/** A local date, or `n/a` when the provider published no reset time. */
|
|
5504
5559
|
function resetLabel(ms) {
|
|
5505
5560
|
if (ms <= 0) return "n/a";
|
|
5506
5561
|
return new Date(ms).toLocaleString();
|
|
@@ -5604,16 +5659,16 @@ const REMOTE_SERVICE = "commandcodeUsage";
|
|
|
5604
5659
|
/** The wire namespace all Command Code endpoints share. */
|
|
5605
5660
|
const REMOTE_NAMESPACE = "commandcode";
|
|
5606
5661
|
/**
|
|
5607
|
-
* Build the validator helpers one endpoint uses. `prefix` names the
|
|
5608
|
-
*
|
|
5609
|
-
*
|
|
5610
|
-
* bodies.
|
|
5662
|
+
* Build the validator helpers one endpoint uses. `prefix` names the endpoint
|
|
5663
|
+
* in the rejection message (e.g. `commandcode/report result:`), so each wire
|
|
5664
|
+
* file keeps its own diagnostic phrasing while sharing the helper bodies.
|
|
5611
5665
|
*
|
|
5612
5666
|
* The helpers return the reject call directly in the failure branch: since
|
|
5613
5667
|
* `reject` is typed `never`, the ternary's union collapses to the success type
|
|
5614
5668
|
* without relying on TypeScript's control-flow analysis of a never-returning
|
|
5615
5669
|
* call (which only recognizes function declarations, not the destructured
|
|
5616
|
-
* arrow `reject` callers receive from this factory).
|
|
5670
|
+
* arrow `reject` callers receive from this factory). Callers that need the
|
|
5671
|
+
* narrowing in an `if` must therefore assign inside the positive branch.
|
|
5617
5672
|
*/
|
|
5618
5673
|
function makeBoundaryValidator(prefix) {
|
|
5619
5674
|
const reject = (field) => {
|
|
@@ -5641,10 +5696,8 @@ function makeStrictCodec(typeSymbol, schema) {
|
|
|
5641
5696
|
}
|
|
5642
5697
|
/**
|
|
5643
5698
|
* Build one strict invocation descriptor. Every Command Code Remote shares the
|
|
5644
|
-
* `commandcode` namespace, the `commandcodeUsage` service, and a strict
|
|
5645
|
-
*
|
|
5646
|
-
* schema differ — so the boilerplate lives here once and each endpoint supplies
|
|
5647
|
-
* only its own facts.
|
|
5699
|
+
* `commandcode` namespace, the `commandcodeUsage` service, and a strict result
|
|
5700
|
+
* — only the endpoint, method, result type symbol, and schema differ.
|
|
5648
5701
|
*/
|
|
5649
5702
|
function makeRemoteDescriptor(endpoint, method, typeSymbol, schema) {
|
|
5650
5703
|
return {
|
|
@@ -5668,7 +5721,6 @@ const USAGE_REPORT_ENDPOINT = "commandcode/report";
|
|
|
5668
5721
|
* so rejection messages name the offending boundary.
|
|
5669
5722
|
*/
|
|
5670
5723
|
const { reject: reject$1, record: record$1, stringField: stringField$1, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
|
|
5671
|
-
/** Validate one window-limit block (`fiveHour` / `weekly`). */
|
|
5672
5724
|
/**
|
|
5673
5725
|
* Parse one quota window, or undefined when the frame carries no such window.
|
|
5674
5726
|
*
|
|
@@ -5969,12 +6021,10 @@ var CommandCodeUsageService = class extends TypertRemoteService {
|
|
|
5969
6021
|
this.deps = deps;
|
|
5970
6022
|
}
|
|
5971
6023
|
/**
|
|
5972
|
-
* Account, usage, and credit state for the settings page's account card
|
|
5973
|
-
*
|
|
5974
|
-
*
|
|
5975
|
-
*
|
|
5976
|
-
* `MISSING_CREDENTIAL` when no key resolves, which the Gateway folds into
|
|
5977
|
-
* the failure branch the page renders as a hint.
|
|
6024
|
+
* Account, usage, and credit state for the settings page's account card.
|
|
6025
|
+
* Degrades per endpoint like the `/commandcode` command (failures land in
|
|
6026
|
+
* `report.failures`); throws `MISSING_CREDENTIAL` when no key resolves, which
|
|
6027
|
+
* the Gateway folds into the failure branch the page renders as a hint.
|
|
5978
6028
|
*/
|
|
5979
6029
|
async report() {
|
|
5980
6030
|
if (this.deps.reports !== void 0) return this.deps.reports();
|
|
@@ -5989,20 +6039,19 @@ var CommandCodeUsageService = class extends TypertRemoteService {
|
|
|
5989
6039
|
}] };
|
|
5990
6040
|
}
|
|
5991
6041
|
/**
|
|
5992
|
-
* The full model catalog for the settings page's model editors
|
|
5993
|
-
*
|
|
5994
|
-
*
|
|
5995
|
-
*
|
|
5996
|
-
* the live list instead of typed by hand.
|
|
6042
|
+
* The full model catalog for the settings page's model editors. The browser
|
|
6043
|
+
* never calls the Command Code API directly — the Host serves the catalog
|
|
6044
|
+
* (already fetched/cached by the adapter) so models can be picked from the
|
|
6045
|
+
* live list instead of typed by hand.
|
|
5997
6046
|
*/
|
|
5998
6047
|
async models() {
|
|
5999
6048
|
return this.deps.listModels?.() ?? { models: [] };
|
|
6000
6049
|
}
|
|
6001
6050
|
/**
|
|
6002
6051
|
* The model price table the composer prices an in-progress session with.
|
|
6003
|
-
* Static vendored data
|
|
6004
|
-
*
|
|
6005
|
-
*
|
|
6052
|
+
* Static vendored data, served Host-side so the browser bundle never carries
|
|
6053
|
+
* a copy that could drift from the snapshot, and so a price update reaches an
|
|
6054
|
+
* open page without a rebuild.
|
|
6006
6055
|
*/
|
|
6007
6056
|
async prices() {
|
|
6008
6057
|
return (this.deps.prices ?? modelPriceTable)();
|
|
@@ -6010,8 +6059,7 @@ var CommandCodeUsageService = class extends TypertRemoteService {
|
|
|
6010
6059
|
/**
|
|
6011
6060
|
* Start (or rejoin) a browser-login attempt and return its fresh status —
|
|
6012
6061
|
* `waiting` carrying the Studio URL. Rejects when the flow cannot start
|
|
6013
|
-
* (no free loopback port, disposed plugin)
|
|
6014
|
-
* into the failure branch the page renders.
|
|
6062
|
+
* (no free loopback port, disposed plugin).
|
|
6015
6063
|
*/
|
|
6016
6064
|
async loginBegin(targetRef) {
|
|
6017
6065
|
return this.requireLogin().begin(targetRef);
|
|
@@ -6054,11 +6102,8 @@ function applyUsageRemote(ctx, deps) {
|
|
|
6054
6102
|
//#endregion
|
|
6055
6103
|
//#region src/login.ts
|
|
6056
6104
|
/**
|
|
6057
|
-
* Host half of the Command Code browser login (the loopback flow)
|
|
6058
|
-
*
|
|
6059
|
-
* Mirrors what the official `command-code login` CLI command performs
|
|
6060
|
-
* (reverse-engineered from `command-code@1.32.1`, `createAuthFlowController`
|
|
6061
|
-
* + `createAuthServer` in its bundle):
|
|
6105
|
+
* Host half of the Command Code browser login (the loopback flow), mirroring
|
|
6106
|
+
* what the official `command-code login` CLI command performs.
|
|
6062
6107
|
*
|
|
6063
6108
|
* 1. Bind a temporary HTTP server on `127.0.0.1`, first available port from
|
|
6064
6109
|
* 5959 upward (10 attempts).
|
|
@@ -6070,12 +6115,12 @@ function applyUsageRemote(ctx, deps) {
|
|
|
6070
6115
|
* 4. The delivered key is validated against `GET {apiBase}/alpha/whoami`
|
|
6071
6116
|
* before anything is stored.
|
|
6072
6117
|
*
|
|
6073
|
-
*
|
|
6074
|
-
* cap, JSON responses (`{success:true}` / `{success:false,error}`),
|
|
6075
|
-
*
|
|
6076
|
-
*
|
|
6077
|
-
*
|
|
6078
|
-
*
|
|
6118
|
+
* The server contract is CLI-mirrored: POST-only `/callback`, a 10 KB body
|
|
6119
|
+
* cap, JSON responses (`{success:true}` / `{success:false,error}`), and
|
|
6120
|
+
* state-token equality as the anti-forgery check. Three deliberate hardenings
|
|
6121
|
+
* over the CLI build: the CORS origin is echoed only when allowlisted (the
|
|
6122
|
+
* CLI falls back to the first origin), `Connection: close`, and the denial
|
|
6123
|
+
* branch checks the state token before it ends the attempt.
|
|
6079
6124
|
*
|
|
6080
6125
|
* Storage stays out of this module: the plugin entry supplies
|
|
6081
6126
|
* {@link CommandCodeLoginFlowDeps.storeKey}, which writes through the dsh
|
|
@@ -6185,16 +6230,15 @@ var CommandCodeLoginFlow = class {
|
|
|
6185
6230
|
* asynchronously (`complete()`), and that window is open to a cancel or a
|
|
6186
6231
|
* fresh `begin()`; the generation lets a late completion recognize that it
|
|
6187
6232
|
* no longer owns the status face and stop instead of storing a credential
|
|
6188
|
-
* the user cancelled
|
|
6233
|
+
* the user cancelled.
|
|
6189
6234
|
*/
|
|
6190
6235
|
attemptSeq = 0;
|
|
6191
6236
|
/**
|
|
6192
|
-
* The start currently binding a port, if any. Every `begin()`
|
|
6193
|
-
*
|
|
6194
|
-
*
|
|
6195
|
-
*
|
|
6196
|
-
*
|
|
6197
|
-
* the port window so browser login dies until the Host restarts.
|
|
6237
|
+
* The start currently binding a port, if any. Every `begin()` arriving before
|
|
6238
|
+
* it settles joins it: two independent starts would each bind a loopback
|
|
6239
|
+
* server, and only the LAST one is reachable by `teardown()` — the orphan
|
|
6240
|
+
* keeps answering `/callback` for the process's lifetime, and ten of them
|
|
6241
|
+
* exhaust the port window so browser login dies until the Host restarts.
|
|
6198
6242
|
*/
|
|
6199
6243
|
starting;
|
|
6200
6244
|
/** A live attempt's destination; different rows may not rejoin it. */
|
|
@@ -6548,20 +6592,15 @@ function corsOrigin(origin) {
|
|
|
6548
6592
|
//#endregion
|
|
6549
6593
|
//#region src/web-search.ts
|
|
6550
6594
|
/**
|
|
6551
|
-
*
|
|
6595
|
+
* Command Code web-search provider over `ctx.web`.
|
|
6552
6596
|
*
|
|
6553
|
-
* The official
|
|
6597
|
+
* The official CLI's built-in `web_search` tool POSTs
|
|
6554
6598
|
* `{ query, numResults, allowedDomains?, blockedDomains? }` to
|
|
6555
6599
|
* `{apiBase}/alpha/web-search` and reads `{ results: [{ title, url, snippet }] }`
|
|
6556
|
-
* back
|
|
6557
|
-
* `x-command-code-version` the model adapter uses
|
|
6558
|
-
* plugin's existing credential chain
|
|
6559
|
-
*
|
|
6560
|
-
*
|
|
6561
|
-
* This mirrors the host-side `@deepseek-ai/dsh-web-search-deepseek` provider in
|
|
6562
|
-
* shape: a cordis-free class registered into the web seam, resolving its key per
|
|
6563
|
-
* search, mapping each server-side result to the harness's normalized
|
|
6564
|
-
* `WebSearchSource`. The web seam owns `maxResults` truncation.
|
|
6600
|
+
* back, authenticating with the SAME `Authorization: Bearer <key>` and
|
|
6601
|
+
* `x-command-code-version` headers the model adapter uses. This provider
|
|
6602
|
+
* therefore reuses the plugin's existing credential chain and `apiBase`, so
|
|
6603
|
+
* the model-facing `web_search` tool needs no separate configuration.
|
|
6565
6604
|
*
|
|
6566
6605
|
* @module dsh-commandcode-provider/web-search
|
|
6567
6606
|
*/
|
|
@@ -6569,12 +6608,9 @@ function corsOrigin(origin) {
|
|
|
6569
6608
|
const COMMANDCODE_SEARCH_PROVIDER_ID = "commandcode";
|
|
6570
6609
|
/**
|
|
6571
6610
|
* The factory-declared search provider id dsh ships by default (from
|
|
6572
|
-
* `dsh-base`'s cordis patch `web.config.searchProvider`).
|
|
6573
|
-
*
|
|
6574
|
-
*
|
|
6575
|
-
* {@link applyCommandCodeSearchSelection}) — it never forces this default,
|
|
6576
|
-
* because forcing it is what used to silence sibling search plugins such as
|
|
6577
|
-
* modsearch even with Command Code search turned off (issue #26).
|
|
6611
|
+
* `dsh-base`'s cordis patch `web.config.searchProvider`). A documented
|
|
6612
|
+
* reference only: disabling this plugin's `webSearch` toggle restores the
|
|
6613
|
+
* PREVIOUSLY selected backend, never this default (issue #26).
|
|
6578
6614
|
*/
|
|
6579
6615
|
const DEFAULT_WEB_SEARCH_PROVIDER_ID = "deepseek-official";
|
|
6580
6616
|
/** Fresh selection state: the plugin starts out not owning the selection. */
|
|
@@ -6590,24 +6626,15 @@ function commandCodeSearchSelection() {
|
|
|
6590
6626
|
* providers (issue #26).
|
|
6591
6627
|
*
|
|
6592
6628
|
* - Enabling writes `commandcode` and remembers whatever it displaced. When
|
|
6593
|
-
* the plugin already owns the selection
|
|
6594
|
-
*
|
|
6595
|
-
*
|
|
6596
|
-
* - Disabling hands the selection back to the remembered backend
|
|
6597
|
-
*
|
|
6598
|
-
*
|
|
6599
|
-
*
|
|
6600
|
-
* auto-select
|
|
6601
|
-
*
|
|
6602
|
-
* surviving runtime the plugin did not set, or a manual
|
|
6603
|
-
* `searchProvider: commandcode` pin), `displaced` stays undefined and
|
|
6604
|
-
* `preexisting` is set, so the later disable is a no-op rather than a guess
|
|
6605
|
-
* at the factory default. A "no-op" means the field is left ALONE: writing
|
|
6606
|
-
* that `undefined` back would destroy the user's own pin, and dsh-web reads
|
|
6607
|
-
* a cleared `searchProviderId` as auto-select — where a second usable
|
|
6608
|
-
* provider (the shipped `deepseek-official` is usable whenever a DeepSeek
|
|
6609
|
-
* key resolves) makes EVERY later search throw
|
|
6610
|
-
* `WEB_PROVIDER_AMBIGUOUS`.
|
|
6629
|
+
* the plugin already owns the selection, the original `displaced` is kept —
|
|
6630
|
+
* the field currently holds our own id, which must never be mistaken for the
|
|
6631
|
+
* user's backend.
|
|
6632
|
+
* - Disabling hands the selection back to the remembered backend only when
|
|
6633
|
+
* this plugin actually took it over. A fresh boot straight into
|
|
6634
|
+
* `webSearch: false`, or a `preexisting` field, leaves it ALONE: writing the
|
|
6635
|
+
* empty memory back would clear the user's own `searchProvider: commandcode`
|
|
6636
|
+
* pin and hand the selection to dsh-web's auto-select, where a second usable
|
|
6637
|
+
* provider makes every later search throw `WEB_PROVIDER_AMBIGUOUS`.
|
|
6611
6638
|
*
|
|
6612
6639
|
* Never throws: like the low-level rewrite, a hardened runtime shape degrades
|
|
6613
6640
|
* to registered-but-unselected.
|
|
@@ -6661,7 +6688,7 @@ function toSource(result) {
|
|
|
6661
6688
|
...snippet !== void 0 && snippet.length > 0 ? { snippet } : {}
|
|
6662
6689
|
};
|
|
6663
6690
|
}
|
|
6664
|
-
function isAbortError
|
|
6691
|
+
function isAbortError(error) {
|
|
6665
6692
|
return error instanceof DOMException && error.name === "AbortError";
|
|
6666
6693
|
}
|
|
6667
6694
|
/** Build the provider's stable cancellation error while retaining the caller's reason. */
|
|
@@ -6672,11 +6699,11 @@ function throwIfAborted(signal) {
|
|
|
6672
6699
|
if (signal?.aborted === true) throw searchAborted(signal, void 0);
|
|
6673
6700
|
}
|
|
6674
6701
|
/**
|
|
6675
|
-
* A `ctx.web` search provider backed by the Command Code Provider API
|
|
6676
|
-
* the plugin's credential chain and `apiBase
|
|
6677
|
-
*
|
|
6678
|
-
*
|
|
6679
|
-
*
|
|
6702
|
+
* A `ctx.web` search provider backed by the Command Code Provider API, reusing
|
|
6703
|
+
* the plugin's credential chain and `apiBase` so the model-facing `web_search`
|
|
6704
|
+
* tool needs no separate configuration. Selection between multiple search
|
|
6705
|
+
* providers is the web seam's job (pin `searchProvider: commandcode` if
|
|
6706
|
+
* ambiguous).
|
|
6680
6707
|
*/
|
|
6681
6708
|
var CommandCodeSearchProvider = class {
|
|
6682
6709
|
deps;
|
|
@@ -6716,7 +6743,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6716
6743
|
...signal !== void 0 ? { signal } : {}
|
|
6717
6744
|
});
|
|
6718
6745
|
} catch (error) {
|
|
6719
|
-
if (signal?.aborted === true || isAbortError
|
|
6746
|
+
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6720
6747
|
throw new WebError(`Command Code web search request failed: ${error instanceof Error ? error.message : String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
|
|
6721
6748
|
}
|
|
6722
6749
|
if (!response.ok) {
|
|
@@ -6731,7 +6758,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6731
6758
|
if (typeof code === "string" || typeof inner === "string") message += `: ${typeof code === "string" ? code : ""}${typeof code === "string" && typeof inner === "string" ? " — " : ""}${typeof inner === "string" ? inner : ""}`;
|
|
6732
6759
|
}
|
|
6733
6760
|
} catch (error) {
|
|
6734
|
-
if (signal?.aborted === true || isAbortError
|
|
6761
|
+
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6735
6762
|
const slice = (await response.text().catch(() => "")).trim().slice(0, 200);
|
|
6736
6763
|
if (slice !== "") message += `: ${slice}`;
|
|
6737
6764
|
}
|
|
@@ -6741,7 +6768,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6741
6768
|
try {
|
|
6742
6769
|
payload = await response.json();
|
|
6743
6770
|
} catch (error) {
|
|
6744
|
-
if (signal?.aborted === true || isAbortError
|
|
6771
|
+
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6745
6772
|
throw new WebError("Command Code web search returned an unparseable response body", "WEB_PROVIDER_ERROR", { cause: error });
|
|
6746
6773
|
}
|
|
6747
6774
|
const results = payload?.results;
|
|
@@ -6765,7 +6792,7 @@ var CommandCodeSearchProvider = class {
|
|
|
6765
6792
|
try {
|
|
6766
6793
|
key = await this.deps.resolveKey();
|
|
6767
6794
|
} catch (error) {
|
|
6768
|
-
if (signal?.aborted === true || isAbortError
|
|
6795
|
+
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
6769
6796
|
if (error instanceof Error && typeof error.code === "string") {
|
|
6770
6797
|
const code = error.code;
|
|
6771
6798
|
if (code === "MISSING_CREDENTIAL") throw new WebError(error.message, "WEB_PROVIDER_CREDENTIAL_MISSING", { cause: error });
|
|
@@ -6806,11 +6833,10 @@ const OTHER_GROUP_ID = "models-other";
|
|
|
6806
6833
|
* free before paid inside a tier, then by id.
|
|
6807
6834
|
*
|
|
6808
6835
|
* The list is the static capability snapshot rather than a live catalog read
|
|
6809
|
-
* on purpose — a settings page must draw synchronously
|
|
6810
|
-
*
|
|
6811
|
-
*
|
|
6812
|
-
*
|
|
6813
|
-
* here is rendered by the "Other" group instead of disappearing.
|
|
6836
|
+
* on purpose — a settings page must draw synchronously. A model added
|
|
6837
|
+
* upstream after this build still reaches the user: an empty allowlist shows
|
|
6838
|
+
* everything, and a model named in the allowlist but absent here is rendered
|
|
6839
|
+
* by the "Other" group instead of disappearing.
|
|
6814
6840
|
*/
|
|
6815
6841
|
function commandCodeTuiModelChoices() {
|
|
6816
6842
|
return Object.keys(KNOWN_PLANS).map((id) => ({
|
|
@@ -6838,14 +6864,13 @@ function storedIds(value) {
|
|
|
6838
6864
|
* Build the section descriptor. Pure, so tests can pin the exact fields
|
|
6839
6865
|
* without a dsh-TUI host.
|
|
6840
6866
|
*
|
|
6841
|
-
*
|
|
6842
|
-
* `
|
|
6843
|
-
*
|
|
6844
|
-
*
|
|
6845
|
-
*
|
|
6846
|
-
* unset
|
|
6847
|
-
*
|
|
6848
|
-
* the screen's "(empty)".
|
|
6867
|
+
* The two option-bearing fields (`activeAccount`, `lang`) are `text` +
|
|
6868
|
+
* `options` rather than `select`, because a `select` cannot express "unset" —
|
|
6869
|
+
* cycling only ever lands on a declared option, so it would strand the user on
|
|
6870
|
+
* a pinned value with no way back to automatic. The `auto` sentinel plus a
|
|
6871
|
+
* `parse` that clears the path keeps unset reachable. `filterModelsByPlan`
|
|
6872
|
+
* formats its EFFECTIVE default (unset means true at the adapter) so a fresh
|
|
6873
|
+
* install reads true instead of the screen's "(empty)".
|
|
6849
6874
|
*/
|
|
6850
6875
|
function buildCommandCodeTuiSection(deps) {
|
|
6851
6876
|
const ref = deps.apiKeyRef();
|
|
@@ -6875,12 +6900,9 @@ function buildCommandCodeTuiSection(deps) {
|
|
|
6875
6900
|
* ONE draft: every one of them then parses that same draft on save, all N
|
|
6876
6901
|
* write ops address the same path, and only the LAST field's op survives —
|
|
6877
6902
|
* which silently rewrote the allowlist from the last catalog model instead
|
|
6878
|
-
* of the one that was toggled.
|
|
6879
|
-
*
|
|
6880
|
-
*
|
|
6881
|
-
* An override equal to what the array already says is written as a CLEAR, so
|
|
6882
|
-
* toggling a model back to its inherited state leaves no residue and the
|
|
6883
|
-
* document stays minimal.
|
|
6903
|
+
* of the one that was toggled. An override equal to what the array already
|
|
6904
|
+
* says is written as a CLEAR, so toggling a model back to its inherited
|
|
6905
|
+
* state leaves no residue.
|
|
6884
6906
|
*/
|
|
6885
6907
|
const modelField = (id, hint, group) => ({
|
|
6886
6908
|
path: ["modelVisibility", id],
|
|
@@ -6953,54 +6975,6 @@ function buildCommandCodeTuiSection(deps) {
|
|
|
6953
6975
|
};
|
|
6954
6976
|
}
|
|
6955
6977
|
},
|
|
6956
|
-
{
|
|
6957
|
-
path: ["commandGuard"],
|
|
6958
|
-
group: "advanced",
|
|
6959
|
-
kind: "boolean",
|
|
6960
|
-
label: "AI command guard",
|
|
6961
|
-
descriptions: { zh: "AI 命令安全预判" },
|
|
6962
|
-
hint: "typesafe/jev judges whether a shell command is safe; sandbox escalations also need separate scope and necessity verdicts. All must be confident enough; everything else asks as usual. Off by default.",
|
|
6963
|
-
hintDescriptions: { zh: "由 typesafe/jev 判断 shell 命令是否安全;沙箱提权还要分别判断范围与必要性,三项都足够确定才自动放行,其余照常弹窗。默认关闭。" },
|
|
6964
|
-
format: (value) => value === true ? "true" : "false",
|
|
6965
|
-
parse: (text) => ({
|
|
6966
|
-
kind: "set",
|
|
6967
|
-
value: text.trim() === "true"
|
|
6968
|
-
})
|
|
6969
|
-
},
|
|
6970
|
-
{
|
|
6971
|
-
path: ["commandGuardLevel"],
|
|
6972
|
-
group: "advanced",
|
|
6973
|
-
kind: "select",
|
|
6974
|
-
label: "Auto-approve threshold",
|
|
6975
|
-
descriptions: { zh: "自动放行阈值" },
|
|
6976
|
-
hint: "How confident typesafe/jev must be before a command runs without asking. Default: medium.",
|
|
6977
|
-
hintDescriptions: { zh: "typesafe/jev 需要多确定才免弹窗放行;默认「中」。" },
|
|
6978
|
-
options: [
|
|
6979
|
-
{
|
|
6980
|
-
value: "high",
|
|
6981
|
-
label: "High (0.95, approves least)",
|
|
6982
|
-
descriptions: { zh: "高(0.95,放行最少)" }
|
|
6983
|
-
},
|
|
6984
|
-
{
|
|
6985
|
-
value: "medium",
|
|
6986
|
-
label: "Medium (0.9)",
|
|
6987
|
-
descriptions: { zh: "中(0.9)" }
|
|
6988
|
-
},
|
|
6989
|
-
{
|
|
6990
|
-
value: "low",
|
|
6991
|
-
label: "Low (0.8, approves most)",
|
|
6992
|
-
descriptions: { zh: "低(0.8,放行最多)" }
|
|
6993
|
-
}
|
|
6994
|
-
],
|
|
6995
|
-
format: (value) => value === "high" || value === "low" ? value : "medium",
|
|
6996
|
-
parse: (text) => {
|
|
6997
|
-
const trimmed = text.trim();
|
|
6998
|
-
return trimmed === "high" || trimmed === "medium" || trimmed === "low" ? {
|
|
6999
|
-
kind: "set",
|
|
7000
|
-
value: trimmed
|
|
7001
|
-
} : void 0;
|
|
7002
|
-
}
|
|
7003
|
-
},
|
|
7004
6978
|
{
|
|
7005
6979
|
path: ["zdr"],
|
|
7006
6980
|
group: "advanced",
|
|
@@ -7135,12 +7109,10 @@ function sectionSignature(section) {
|
|
|
7135
7109
|
/**
|
|
7136
7110
|
* Register the Command Code section on a dsh-TUI host.
|
|
7137
7111
|
*
|
|
7138
|
-
* @param ctx - the context of an activated `tuiSettingsSections` injection.
|
|
7139
|
-
* @param deps - plugin-owned facts the section reads.
|
|
7140
7112
|
* @returns a refresh function that re-registers the section when a fact it
|
|
7141
|
-
* renders changed
|
|
7142
|
-
*
|
|
7143
|
-
* inert after the fiber is torn down.
|
|
7113
|
+
* renders changed — the plugin entry calls it from its
|
|
7114
|
+
* `loader/volatile-update` listener — or `undefined` when the seam is
|
|
7115
|
+
* unusable. The returned function is inert after the fiber is torn down.
|
|
7144
7116
|
*/
|
|
7145
7117
|
function applyCommandCodeTuiSettings(ctx, deps) {
|
|
7146
7118
|
const service = tuiSettingsService(ctx);
|
|
@@ -7176,45 +7148,26 @@ function applyCommandCodeTuiSettings(ctx, deps) {
|
|
|
7176
7148
|
/**
|
|
7177
7149
|
* A bounded retry budget for `TRANSPORT` failures (issue #39, second report).
|
|
7178
7150
|
*
|
|
7179
|
-
* The route
|
|
7180
|
-
*
|
|
7181
|
-
*
|
|
7182
|
-
*
|
|
7183
|
-
*
|
|
7184
|
-
*
|
|
7185
|
-
*
|
|
7186
|
-
*
|
|
7187
|
-
*
|
|
7188
|
-
*
|
|
7189
|
-
* `
|
|
7190
|
-
*
|
|
7191
|
-
*
|
|
7192
|
-
*
|
|
7193
|
-
*
|
|
7194
|
-
*
|
|
7195
|
-
*
|
|
7196
|
-
*
|
|
7197
|
-
*
|
|
7198
|
-
*
|
|
7199
|
-
* the user's call — the next send starts a fresh budget — and
|
|
7200
|
-
* `dsh-llm-retry`'s window for rate limits is untouched.
|
|
7201
|
-
*
|
|
7202
|
-
* Deliberately NOT counted here: the pre-stream recovery the ADAPTER performs
|
|
7203
|
-
* inside one `stream()` call (account rotation on 429/401, the 413 image-budget
|
|
7204
|
-
* step-down, the Provider-API → CLI protocol switch). Those are invisible to
|
|
7205
|
-
* the caller and bounded on their own; this budget caps only the harness's
|
|
7206
|
-
* retry loop.
|
|
7207
|
-
*
|
|
7208
|
-
* Reset rule: the budget clears at each new STEP — one step is exactly one
|
|
7209
|
-
* model request (`step/start` is appended immediately before the call) — so the
|
|
7210
|
-
* cap is per logical request and the next step of the same turn gets its own
|
|
7211
|
-
* grace. `agent/status` → `idle` is NOT the reset signal even though it looks
|
|
7212
|
-
* like the natural one: the agent loop's `setPhase` emits it only on a status
|
|
7213
|
-
* CHANGE, and a turn's steps all run inside one `running` phase, so it never
|
|
7214
|
-
* fires between them — a budget reset only there would be per-turn, and an
|
|
7215
|
-
* outage would fail every later step of that turn immediately instead of
|
|
7216
|
-
* retrying each. `assistant/attempt` is wrong for the opposite reason: the loop
|
|
7217
|
-
* appends it for the FAILED attempt itself, *before* dispatching
|
|
7151
|
+
* The route policy (`providerRetryPolicy` in `./adapter.ts`) is near-unbounded
|
|
7152
|
+
* on purpose — 1000 attempts doubling to a 15-minute cap — because it exists
|
|
7153
|
+
* for failures the provider ASKS to have retried, where the wait is the point.
|
|
7154
|
+
* A connection that cannot be established is not that: either a blip the first
|
|
7155
|
+
* few attempts absorb, or an outage no in-request waiting fixes (and its waits
|
|
7156
|
+
* count BEFORE each attempt, so attempt 11 alone already waits 512 s). So
|
|
7157
|
+
* transport failures get their own budget; waiting out a real outage is the
|
|
7158
|
+
* user's call, and the next send starts a fresh budget.
|
|
7159
|
+
*
|
|
7160
|
+
* Not counted here: the pre-stream recovery the ADAPTER performs inside one
|
|
7161
|
+
* `stream()` call (account rotation, the 413 image-budget step-down, the
|
|
7162
|
+
* Provider-API → CLI switch) — invisible to the caller and bounded on its own.
|
|
7163
|
+
* This budget caps only the harness's retry loop.
|
|
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
|
|
7218
7171
|
* `agent/request-error`, so resetting on it would clear the budget on every
|
|
7219
7172
|
* failure and restore the very loop this module exists to stop.
|
|
7220
7173
|
*
|
|
@@ -7248,15 +7201,13 @@ function resetTransportFailures(agent) {
|
|
|
7248
7201
|
/**
|
|
7249
7202
|
* What one session event means for the transport budget.
|
|
7250
7203
|
*
|
|
7251
|
-
*
|
|
7252
|
-
* that is easy to get plausibly wrong (
|
|
7253
|
-
*
|
|
7254
|
-
* here, where a test can pin it, instead of being implied by a listener body.
|
|
7204
|
+
* Pure and exported on purpose: the choice of event is the part of this design
|
|
7205
|
+
* that is easy to get plausibly wrong (see the module comment), so it is stated
|
|
7206
|
+
* once, where a test can pin it.
|
|
7255
7207
|
*
|
|
7256
7208
|
* @param type - the appended session event's type.
|
|
7257
|
-
* @returns `'reset'` for a new step
|
|
7258
|
-
*
|
|
7259
|
-
* `'none'`.
|
|
7209
|
+
* @returns `'reset'` for a new step, `'forget'` when the turn ended (the
|
|
7210
|
+
* session → agent mapping is no longer needed), otherwise `'none'`.
|
|
7260
7211
|
*/
|
|
7261
7212
|
function transportResetAction(type) {
|
|
7262
7213
|
if (type === "step/start") return "reset";
|
|
@@ -7266,880 +7217,44 @@ function transportResetAction(type) {
|
|
|
7266
7217
|
/**
|
|
7267
7218
|
* The diagnosis a capped turn ends with. Bilingual, because the harness renders
|
|
7268
7219
|
* a failed turn's message verbatim and this plugin's other user-facing failures
|
|
7269
|
-
* are
|
|
7270
|
-
*
|
|
7271
|
-
* chain the retry chrome would otherwise show.
|
|
7220
|
+
* are too; it names the retry budget and the checks that actually help instead
|
|
7221
|
+
* of the bare "fetch failed" chain the retry chrome would otherwise show.
|
|
7272
7222
|
*/
|
|
7273
7223
|
function transportBudgetMessage(failureMessage, maxRetries) {
|
|
7274
7224
|
return `${failureMessage};Command Code API 连接在连续重试后仍失败——已停止重试以免整轮卡死。插件已自动重试 ${maxRetries} 次。通常说明当前网络到 api.commandcode.ai 的连接不通,常见原因是代理未生效:DSH 只读取环境变量 HTTPS_PROXY/HTTP_PROXY(系统代理/PAC 不会被读取),请确认启动 DSH 的终端里已导出代理,或先关闭代理直连后重试。网络恢复后直接重发即可 — Command Code API transport still failed after ${maxRetries} automatic retries, which were stopped so the turn could not stall for minutes. This is a connectivity problem between this machine and api.commandcode.ai, not a context-window or request-size rejection. Common cause: the proxy is not reaching the harness — DSH reads HTTPS_PROXY/HTTP_PROXY from the environment only (a Windows system proxy/PAC setting is not consulted), so export it in the launching shell, or turn the proxy off and retry. Resend when the network recovers; the budget is per request and has been reset`;
|
|
7275
7225
|
}
|
|
7276
7226
|
//#endregion
|
|
7277
|
-
//#region src/systemone.ts
|
|
7278
|
-
/**
|
|
7279
|
-
* Host-side client for Command Code's System One decision endpoint
|
|
7280
|
-
* (`POST {apiBase}/provider/v1/systemone`).
|
|
7281
|
-
*
|
|
7282
|
-
* `typesafe/jev` is **not a chat model**: it takes a `state` plus typed
|
|
7283
|
-
* questions and answers with probabilities, never text, and it never streams.
|
|
7284
|
-
* It is therefore absent from `/provider/v1/models` (so the picker can never
|
|
7285
|
-
* offer it) and must never be registered as a model route — this plugin uses it
|
|
7286
|
-
* as an internal advisor instead: the command guard in `./command-guard.ts`
|
|
7287
|
-
* asks it whether a shell command is safe to auto-approve.
|
|
7288
|
-
*
|
|
7289
|
-
* The three question types are the whole vocabulary (command-code@1.65.2
|
|
7290
|
-
* `systemOneRequestSchema`, unchanged since 1.64.0):
|
|
7291
|
-
*
|
|
7292
|
-
* - `noul` — "is this true?", optional `criteria: { true, false }` describing
|
|
7293
|
-
* each verdict. Answers with `noul`, the probability of *yes*.
|
|
7294
|
-
* - `choice` — "which one?", `criteria` maps each option key to its
|
|
7295
|
-
* description. Answers with `choice`, plus optional `confidence` and
|
|
7296
|
-
* `probabilities`.
|
|
7297
|
-
* - `score` — "where on this rubric?", `criteria` is an array of at least two
|
|
7298
|
-
* levels, lowest first. Answers with `score`, `legend` and `probabilities`.
|
|
7299
|
-
*
|
|
7300
|
-
* A request carries at most 20 questions and every answer is keyed by the
|
|
7301
|
-
* question id it belongs to. `usage` is reported per request, in tokens.
|
|
7302
|
-
*
|
|
7303
|
-
* Billing: free through 2026-09-24T23:59:59Z, then $0.042 per 1M input tokens
|
|
7304
|
-
* with output free. The model has **no ZDR-capable upstream**, so a request
|
|
7305
|
-
* carrying `x-cmd-zdr: 1` is refused with HTTP 422 `cmd_zdr_no_providers` —
|
|
7306
|
-
* this client never sends that header, and it must stay that way even though
|
|
7307
|
-
* the plugin now ships a ZDR switch for chat (`Config.zdr` → the `x-cmd-zdr: 1`
|
|
7308
|
-
* header per model, gated on `KNOWN_NON_ZDR_MODELS`): the decision endpoint is
|
|
7309
|
-
* reached through THIS module's own request path, never through the adapter's
|
|
7310
|
-
* chat connection, so the two cannot drift — and a deployment that later widens
|
|
7311
|
-
* ZDR must exclude this endpoint rather than route it through the switch.
|
|
7312
|
-
*
|
|
7313
|
-
* Auth is the plugin's ordinary chain (the multi-account pool, the credentials
|
|
7314
|
-
* seam, the CLI auth file) through the same `Authorization: Bearer <key>` and
|
|
7315
|
-
* `x-command-code-version` headers every other endpoint uses.
|
|
7316
|
-
*
|
|
7317
|
-
* @module dsh-commandcode-provider/systemone
|
|
7318
|
-
*/
|
|
7319
|
-
/** Path of the decision endpoint, relative to `apiBase`. */
|
|
7320
|
-
const SYSTEMONE_ROUTE = "/provider/v1/systemone";
|
|
7321
|
-
/** The one error type this client throws. Callers fail closed on it. */
|
|
7322
|
-
var SystemOneError = class extends Error {
|
|
7323
|
-
kind;
|
|
7324
|
-
status;
|
|
7325
|
-
constructor(message, kind, status, options) {
|
|
7326
|
-
super(message, options);
|
|
7327
|
-
this.kind = kind;
|
|
7328
|
-
this.status = status;
|
|
7329
|
-
this.name = "SystemOneError";
|
|
7330
|
-
}
|
|
7331
|
-
};
|
|
7332
|
-
/**
|
|
7333
|
-
* Combine the caller's signal with a timeout, keeping the two distinguishable:
|
|
7334
|
-
* a timeout is this client's own budget expiring (the guard delegates to the
|
|
7335
|
-
* user) while a caller abort means the approval was already withdrawn (answer
|
|
7336
|
-
* nothing) — both fail closed, but the diagnosis must not lie about which.
|
|
7337
|
-
*
|
|
7338
|
-
* The timer is a plain `setTimeout` rather than `AbortSignal.timeout()`, whose
|
|
7339
|
-
* timer Node unrefs: with nothing else pending (a test double, a socket that
|
|
7340
|
-
* never opened) an unref'd timer lets the process exit while the promise is
|
|
7341
|
-
* still pending, which reads as a hang rather than as the timeout it is.
|
|
7342
|
-
*/
|
|
7343
|
-
function deadlineOf(signal, timeoutMs) {
|
|
7344
|
-
const controller = new AbortController();
|
|
7345
|
-
let timedOut = false;
|
|
7346
|
-
const timer = setTimeout(() => {
|
|
7347
|
-
timedOut = true;
|
|
7348
|
-
controller.abort();
|
|
7349
|
-
}, timeoutMs);
|
|
7350
|
-
const onCallerAbort = () => controller.abort();
|
|
7351
|
-
if (signal !== void 0) {
|
|
7352
|
-
if (signal.aborted) controller.abort();
|
|
7353
|
-
else signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
7354
|
-
}
|
|
7355
|
-
return {
|
|
7356
|
-
signal: controller.signal,
|
|
7357
|
-
callerAborted: () => signal?.aborted === true,
|
|
7358
|
-
timedOut: () => timedOut,
|
|
7359
|
-
dispose: () => {
|
|
7360
|
-
clearTimeout(timer);
|
|
7361
|
-
signal?.removeEventListener("abort", onCallerAbort);
|
|
7362
|
-
}
|
|
7363
|
-
};
|
|
7364
|
-
}
|
|
7365
|
-
/** Whether a caught value is an abort-shaped failure. */
|
|
7366
|
-
function isAbortError(error) {
|
|
7367
|
-
return error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
7368
|
-
}
|
|
7369
|
-
/** Stop waiting for an operation that does not accept an AbortSignal. */
|
|
7370
|
-
function withinDeadline(operation, deadline, timeoutMs) {
|
|
7371
|
-
return new Promise((resolve, reject) => {
|
|
7372
|
-
const onAbort = () => {
|
|
7373
|
-
reject(deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout"));
|
|
7374
|
-
};
|
|
7375
|
-
deadline.signal.addEventListener("abort", onAbort, { once: true });
|
|
7376
|
-
operation.then((value) => {
|
|
7377
|
-
deadline.signal.removeEventListener("abort", onAbort);
|
|
7378
|
-
resolve(value);
|
|
7379
|
-
}, (error) => {
|
|
7380
|
-
deadline.signal.removeEventListener("abort", onAbort);
|
|
7381
|
-
reject(error);
|
|
7382
|
-
});
|
|
7383
|
-
if (deadline.signal.aborted) onAbort();
|
|
7384
|
-
});
|
|
7385
|
-
}
|
|
7386
|
-
/** A finite number in `[0, 1]`, or undefined. */
|
|
7387
|
-
function probability(value) {
|
|
7388
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
|
|
7389
|
-
}
|
|
7390
|
-
/** A finite number, or undefined. */
|
|
7391
|
-
function finite(value) {
|
|
7392
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
7393
|
-
}
|
|
7394
|
-
/** A record of finite numbers (probabilities), or undefined when the shape is off. */
|
|
7395
|
-
function numberRecord(value) {
|
|
7396
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
7397
|
-
const out = {};
|
|
7398
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
7399
|
-
const parsed = finite(entry);
|
|
7400
|
-
if (parsed === void 0) return void 0;
|
|
7401
|
-
out[key] = parsed;
|
|
7402
|
-
}
|
|
7403
|
-
return out;
|
|
7404
|
-
}
|
|
7405
|
-
/**
|
|
7406
|
-
* Parse one answer entry strictly by its own `type`. A body whose answer does
|
|
7407
|
-
* not carry the member its type promises is rejected rather than defaulted:
|
|
7408
|
-
* an absent probability is exactly the case the caller must not read as "safe".
|
|
7409
|
-
*
|
|
7410
|
-
* @param value - one raw entry from the response's `answers` object.
|
|
7411
|
-
* @returns the parsed answer, or undefined when the entry is malformed.
|
|
7412
|
-
*/
|
|
7413
|
-
function parseSystemOneAnswer(value) {
|
|
7414
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
7415
|
-
const raw = value;
|
|
7416
|
-
if (raw.type === "noul") {
|
|
7417
|
-
const noul = probability(raw.noul);
|
|
7418
|
-
return noul === void 0 ? void 0 : {
|
|
7419
|
-
type: "noul",
|
|
7420
|
-
noul
|
|
7421
|
-
};
|
|
7422
|
-
}
|
|
7423
|
-
if (raw.type === "choice") {
|
|
7424
|
-
if (typeof raw.choice !== "string" || raw.choice === "") return void 0;
|
|
7425
|
-
const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
|
|
7426
|
-
if (raw.confidence !== void 0 && confidence === void 0) return void 0;
|
|
7427
|
-
const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
|
|
7428
|
-
if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
|
|
7429
|
-
return {
|
|
7430
|
-
type: "choice",
|
|
7431
|
-
choice: raw.choice,
|
|
7432
|
-
...confidence === void 0 ? {} : { confidence },
|
|
7433
|
-
...probabilities === void 0 ? {} : { probabilities }
|
|
7434
|
-
};
|
|
7435
|
-
}
|
|
7436
|
-
if (raw.type === "score") {
|
|
7437
|
-
const score = finite(raw.score);
|
|
7438
|
-
if (score === void 0) return void 0;
|
|
7439
|
-
const confidence = raw.confidence === void 0 ? void 0 : probability(raw.confidence);
|
|
7440
|
-
if (raw.confidence !== void 0 && confidence === void 0) return void 0;
|
|
7441
|
-
const probabilities = raw.probabilities === void 0 ? void 0 : numberRecord(raw.probabilities);
|
|
7442
|
-
if (raw.probabilities !== void 0 && probabilities === void 0) return void 0;
|
|
7443
|
-
return {
|
|
7444
|
-
type: "score",
|
|
7445
|
-
score,
|
|
7446
|
-
...confidence === void 0 ? {} : { confidence },
|
|
7447
|
-
...probabilities === void 0 ? {} : { probabilities }
|
|
7448
|
-
};
|
|
7449
|
-
}
|
|
7450
|
-
}
|
|
7451
|
-
/**
|
|
7452
|
-
* Parse a decision response body. Every answer entry must be well formed — one
|
|
7453
|
-
* malformed entry fails the whole response, because a partially understood
|
|
7454
|
-
* decision is not a decision. Unknown extra members are ignored so a server-side
|
|
7455
|
-
* addition cannot break the guard.
|
|
7456
|
-
*
|
|
7457
|
-
* @param value - the decoded JSON body.
|
|
7458
|
-
* @returns the parsed response, or undefined when the body is not one.
|
|
7459
|
-
*/
|
|
7460
|
-
function parseSystemOneResponse(value) {
|
|
7461
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
7462
|
-
const raw = value;
|
|
7463
|
-
const rawAnswers = raw.answers;
|
|
7464
|
-
if (typeof rawAnswers !== "object" || rawAnswers === null || Array.isArray(rawAnswers)) return void 0;
|
|
7465
|
-
const entries = Object.entries(rawAnswers);
|
|
7466
|
-
if (entries.length === 0) return void 0;
|
|
7467
|
-
const answers = {};
|
|
7468
|
-
for (const [id, entry] of entries) {
|
|
7469
|
-
const answer = parseSystemOneAnswer(entry);
|
|
7470
|
-
if (answer === void 0) return void 0;
|
|
7471
|
-
answers[id] = answer;
|
|
7472
|
-
}
|
|
7473
|
-
const usage = raw.usage === void 0 ? void 0 : parseUsage(raw.usage);
|
|
7474
|
-
if (raw.usage !== void 0 && usage === void 0) return void 0;
|
|
7475
|
-
return {
|
|
7476
|
-
...typeof raw.model === "string" ? { model: raw.model } : {},
|
|
7477
|
-
answers,
|
|
7478
|
-
...usage === void 0 ? {} : { usage }
|
|
7479
|
-
};
|
|
7480
|
-
}
|
|
7481
|
-
/** Parse the optional `usage` member; an unreadable one is a malformed body. */
|
|
7482
|
-
function parseUsage(value) {
|
|
7483
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
7484
|
-
const raw = value;
|
|
7485
|
-
const inputTokens = raw.input_tokens === void 0 ? void 0 : finite(raw.input_tokens);
|
|
7486
|
-
if (raw.input_tokens !== void 0 && inputTokens === void 0) return void 0;
|
|
7487
|
-
const outputTokens = raw.output_tokens === void 0 ? void 0 : finite(raw.output_tokens);
|
|
7488
|
-
if (raw.output_tokens !== void 0 && outputTokens === void 0) return void 0;
|
|
7489
|
-
return {
|
|
7490
|
-
...inputTokens === void 0 ? {} : { inputTokens },
|
|
7491
|
-
...outputTokens === void 0 ? {} : { outputTokens }
|
|
7492
|
-
};
|
|
7493
|
-
}
|
|
7494
|
-
/** Validate the outgoing request where the server would only reject it with a 400. */
|
|
7495
|
-
function assertRequest(request) {
|
|
7496
|
-
const ids = Object.keys(request.questions);
|
|
7497
|
-
if (ids.length === 0) throw new SystemOneError("a decision request needs at least one question", "invalid-request");
|
|
7498
|
-
if (ids.length > 20) throw new SystemOneError(`a decision request carries at most 20 questions (got ${ids.length})`, "invalid-request");
|
|
7499
|
-
for (const id of ids) if (id.trim() === "") throw new SystemOneError("every decision question needs a non-empty id", "invalid-request");
|
|
7500
|
-
}
|
|
7501
|
-
/** Read the endpoint's error body into a one-line diagnosis. */
|
|
7502
|
-
async function httpFailure(response) {
|
|
7503
|
-
let detail = "";
|
|
7504
|
-
try {
|
|
7505
|
-
const slice = (await response.text()).trim().slice(0, 200);
|
|
7506
|
-
if (slice !== "") detail = `: ${slice}`;
|
|
7507
|
-
} catch {}
|
|
7508
|
-
return new SystemOneError(`Command Code decision request failed (HTTP ${response.status})${detail}`, "http", response.status);
|
|
7509
|
-
}
|
|
7510
|
-
/**
|
|
7511
|
-
* Run one decision request.
|
|
7512
|
-
*
|
|
7513
|
-
* @param deps - connection facts, credential chain and fetch seam.
|
|
7514
|
-
* @param request - the state and typed questions.
|
|
7515
|
-
* @param options - caller cancellation and the per-call budget.
|
|
7516
|
-
* @returns the parsed decision.
|
|
7517
|
-
* @throws {SystemOneError} on every failure path; callers fail closed.
|
|
7518
|
-
*/
|
|
7519
|
-
async function runSystemOne(deps, request, options = {}) {
|
|
7520
|
-
assertRequest(request);
|
|
7521
|
-
const apiBase = deps.apiBase();
|
|
7522
|
-
if (!URL.canParse(apiBase)) throw new SystemOneError(`Command Code decision request is misconfigured: apiBase ${JSON.stringify(apiBase)} is not a valid URL`, "invalid-request");
|
|
7523
|
-
if (options.signal?.aborted === true) throw new SystemOneError("decision request aborted", "aborted");
|
|
7524
|
-
const timeoutMs = options.timeoutMs ?? 1500;
|
|
7525
|
-
const deadline = deadlineOf(options.signal, timeoutMs);
|
|
7526
|
-
try {
|
|
7527
|
-
let key;
|
|
7528
|
-
try {
|
|
7529
|
-
key = await withinDeadline(Promise.resolve().then(() => deps.resolveApiKey()), deadline, timeoutMs);
|
|
7530
|
-
} catch (error) {
|
|
7531
|
-
if (error instanceof SystemOneError && (error.kind === "aborted" || error.kind === "timeout")) throw error;
|
|
7532
|
-
throw new SystemOneError(`Command Code decision request has no usable credential: ${error instanceof Error ? error.message : String(error)}`, "credential", void 0, { cause: error });
|
|
7533
|
-
}
|
|
7534
|
-
const body = {
|
|
7535
|
-
model: request.model ?? "typesafe/jev",
|
|
7536
|
-
state: request.state,
|
|
7537
|
-
questions: request.questions
|
|
7538
|
-
};
|
|
7539
|
-
if (deadline.signal.aborted) throw deadline.callerAborted() ? new SystemOneError("decision request aborted", "aborted") : new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout");
|
|
7540
|
-
let response;
|
|
7541
|
-
try {
|
|
7542
|
-
response = await (deps.fetchImpl ?? fetch)(`${apiBase.replace(/\/$/, "")}${SYSTEMONE_ROUTE}`, {
|
|
7543
|
-
method: "POST",
|
|
7544
|
-
headers: {
|
|
7545
|
-
"Content-Type": "application/json",
|
|
7546
|
-
...IDENTITY_ENCODING_HEADER,
|
|
7547
|
-
Authorization: `Bearer ${key}`,
|
|
7548
|
-
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
7549
|
-
...attributionHeaders()
|
|
7550
|
-
},
|
|
7551
|
-
body: JSON.stringify(body),
|
|
7552
|
-
signal: deadline.signal
|
|
7553
|
-
});
|
|
7554
|
-
} catch (error) {
|
|
7555
|
-
if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
|
|
7556
|
-
if (deadline.timedOut() || isAbortError(error)) throw new SystemOneError(`decision request timed out after ${timeoutMs}ms`, "timeout", void 0, { cause: error });
|
|
7557
|
-
throw new SystemOneError(`Command Code decision request failed: ${error instanceof Error ? error.message : String(error)}`, "network", void 0, { cause: error });
|
|
7558
|
-
}
|
|
7559
|
-
if (!response.ok) throw await httpFailure(response);
|
|
7560
|
-
let payload;
|
|
7561
|
-
try {
|
|
7562
|
-
payload = await response.json();
|
|
7563
|
-
} catch (error) {
|
|
7564
|
-
if (deadline.callerAborted()) throw new SystemOneError("decision request aborted", "aborted", void 0, { cause: error });
|
|
7565
|
-
throw new SystemOneError("Command Code returned an unparseable decision body", "invalid-response", void 0, { cause: error });
|
|
7566
|
-
}
|
|
7567
|
-
const parsed = parseSystemOneResponse(payload);
|
|
7568
|
-
if (parsed === void 0) throw new SystemOneError("Command Code returned a decision body this plugin cannot read", "invalid-response");
|
|
7569
|
-
return parsed;
|
|
7570
|
-
} finally {
|
|
7571
|
-
deadline.dispose();
|
|
7572
|
-
}
|
|
7573
|
-
}
|
|
7574
|
-
//#endregion
|
|
7575
|
-
//#region src/command-guard.ts
|
|
7576
|
-
/** Tool names whose `arguments.command` this guard judges. */
|
|
7577
|
-
const COMMAND_GUARD_SHELL_TOOLS = ["bash", "pwsh"];
|
|
7578
|
-
/** Question id the primary safety verdict is keyed by. */
|
|
7579
|
-
const COMMAND_GUARD_QUESTION_ID = "safe";
|
|
7580
|
-
/** Question id for the narrow-purpose part of a sandbox escalation. */
|
|
7581
|
-
const COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID = "escalation_scope";
|
|
7582
|
-
/** Question id for the necessity/proportionality part of a sandbox escalation. */
|
|
7583
|
-
const COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID = "escalation_necessity";
|
|
7584
|
-
/** The selectable levels, strictest first (the order the settings surfaces list them in). */
|
|
7585
|
-
const COMMAND_GUARD_LEVELS = [
|
|
7586
|
-
"high",
|
|
7587
|
-
"medium",
|
|
7588
|
-
"low"
|
|
7589
|
-
];
|
|
7590
|
-
/** Each level's probability threshold. */
|
|
7591
|
-
const COMMAND_GUARD_LEVEL_THRESHOLDS = {
|
|
7592
|
-
high: .95,
|
|
7593
|
-
medium: .9,
|
|
7594
|
-
low: .8
|
|
7595
|
-
};
|
|
7596
|
-
/** The level an unset config reads as. */
|
|
7597
|
-
const COMMAND_GUARD_DEFAULT_LEVEL = "medium";
|
|
7598
|
-
/** Probability of "safe" at or above which the guard grants `allowed-once` by default. */
|
|
7599
|
-
const COMMAND_GUARD_DEFAULT_THRESHOLD = COMMAND_GUARD_LEVEL_THRESHOLDS[COMMAND_GUARD_DEFAULT_LEVEL];
|
|
7600
|
-
/** The threshold for a stored level; anything unrecognized reads as the default level. */
|
|
7601
|
-
function commandGuardThreshold(level) {
|
|
7602
|
-
return typeof level === "string" && Object.hasOwn(COMMAND_GUARD_LEVEL_THRESHOLDS, level) ? COMMAND_GUARD_LEVEL_THRESHOLDS[level] : COMMAND_GUARD_DEFAULT_THRESHOLD;
|
|
7603
|
-
}
|
|
7604
|
-
/**
|
|
7605
|
-
* Decision budget; the approval prompt must not wait longer than this. Fixed
|
|
7606
|
-
* rather than configurable: it only bounds how long a late verdict can delay
|
|
7607
|
-
* the prompt, and every failure (a timeout included) already falls back to
|
|
7608
|
-
* asking the user, so there is nothing for a user to tune. 3 s leaves room for
|
|
7609
|
-
* credential resolution plus one decision round trip on a slow link.
|
|
7610
|
-
*/
|
|
7611
|
-
const COMMAND_GUARD_TIMEOUT_MS = 3e3;
|
|
7612
|
-
/**
|
|
7613
|
-
* Longest command the guard will send for judgement. A longer one is delegated
|
|
7614
|
-
* instead of truncated: a verdict on the first N characters of a command is a
|
|
7615
|
-
* verdict on a different command.
|
|
7616
|
-
*/
|
|
7617
|
-
const COMMAND_GUARD_MAX_COMMAND_CHARS = 6e3;
|
|
7618
|
-
/**
|
|
7619
|
-
* Hard denylist: commands the guard never auto-approves, and never even sends
|
|
7620
|
-
* for judgement. Each entry is a deliberate, narrow reading of one destructive
|
|
7621
|
-
* family — the goal is not to classify every dangerous command (that is the
|
|
7622
|
-
* model's job) but to make sure the classics cannot be talked into a grant by a
|
|
7623
|
-
* persuasive-looking command string.
|
|
7624
|
-
*
|
|
7625
|
-
* Every pattern is allowed to OVER-match: a false positive costs one ordinary
|
|
7626
|
-
* human prompt, while a false negative would hand a destructive command to a
|
|
7627
|
-
* model that has to guess. That asymmetry is why `sudo` is matched anywhere in
|
|
7628
|
-
* the line rather than only in command position, and why `fdisk` is listed even
|
|
7629
|
-
* though `fdisk -l` only lists partitions.
|
|
7630
|
-
*/
|
|
7631
|
-
const COMMAND_GUARD_DENY_LIST = [
|
|
7632
|
-
{
|
|
7633
|
-
id: "rm-root",
|
|
7634
|
-
pattern: /\brm\s+(?:-[a-zA-Z]*[rR][a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*[rR][a-zA-Z]*|--recursive[^\n]*--force|--force[^\n]*--recursive)\s+(?:\/|\/\*|~|~\/\*|\$HOME(?:\/\*)?|\*|\.\/\*|\.)(?:\s|$)/
|
|
7635
|
-
},
|
|
7636
|
-
{
|
|
7637
|
-
id: "rm-home",
|
|
7638
|
-
pattern: /\brm\s+-[a-zA-Z]*[rR][a-zA-Z]*\s+(?:~|\$HOME)\//
|
|
7639
|
-
},
|
|
7640
|
-
{
|
|
7641
|
-
id: "filesystem-write",
|
|
7642
|
-
pattern: /\b(?:mkfs(?:\.\w+)?|wipefs|fdisk|parted|shred)\b/
|
|
7643
|
-
},
|
|
7644
|
-
{
|
|
7645
|
-
id: "raw-device-write",
|
|
7646
|
-
pattern: /\bdd\b[^\n]*\bof=\/dev\//
|
|
7647
|
-
},
|
|
7648
|
-
{
|
|
7649
|
-
id: "device-redirect",
|
|
7650
|
-
pattern: />{1,2}\s*\/dev\/(?:sd|disk|nvme|hd)/
|
|
7651
|
-
},
|
|
7652
|
-
{
|
|
7653
|
-
id: "fork-bomb",
|
|
7654
|
-
pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/
|
|
7655
|
-
},
|
|
7656
|
-
{
|
|
7657
|
-
id: "privilege-escalation",
|
|
7658
|
-
pattern: /\b(?:sudo|doas)\b/
|
|
7659
|
-
},
|
|
7660
|
-
{
|
|
7661
|
-
id: "su",
|
|
7662
|
-
pattern: /\bsu\s/
|
|
7663
|
-
},
|
|
7664
|
-
{
|
|
7665
|
-
id: "chmod-world-write",
|
|
7666
|
-
pattern: /\bchmod\b[^\n]*\b(?:777|a\+w|o\+w)\b/
|
|
7667
|
-
},
|
|
7668
|
-
{
|
|
7669
|
-
id: "chown-root",
|
|
7670
|
-
pattern: /\bchown\b[^\n]*\b(?:root|0):/
|
|
7671
|
-
},
|
|
7672
|
-
{
|
|
7673
|
-
id: "git-push",
|
|
7674
|
-
pattern: /\bgit\s+push\b/
|
|
7675
|
-
},
|
|
7676
|
-
{
|
|
7677
|
-
id: "git-history-rewrite",
|
|
7678
|
-
pattern: /\bgit\s+(?:filter-branch|filter-repo)\b/
|
|
7679
|
-
},
|
|
7680
|
-
{
|
|
7681
|
-
id: "git-hard-reset",
|
|
7682
|
-
pattern: /\bgit\s+reset\s+--hard\b/
|
|
7683
|
-
},
|
|
7684
|
-
{
|
|
7685
|
-
id: "git-clean-force",
|
|
7686
|
-
pattern: /\bgit\s+clean\b[^\n]*\s-[a-zA-Z]*f/
|
|
7687
|
-
},
|
|
7688
|
-
{
|
|
7689
|
-
id: "git-discard-worktree",
|
|
7690
|
-
pattern: /\bgit\s+(?:checkout|restore)\s+(?:--\s+)?\.(?:\s|$)/
|
|
7691
|
-
},
|
|
7692
|
-
{
|
|
7693
|
-
id: "git-stash-drop",
|
|
7694
|
-
pattern: /\bgit\s+stash\s+(?:drop|clear)\b/
|
|
7695
|
-
},
|
|
7696
|
-
{
|
|
7697
|
-
id: "publish",
|
|
7698
|
-
pattern: /\b(?:npm|pnpm|yarn|bun)\s+publish\b/
|
|
7699
|
-
},
|
|
7700
|
-
{
|
|
7701
|
-
id: "release-upload",
|
|
7702
|
-
pattern: /\b(?:gh\s+release|twine\s+upload)\b/
|
|
7703
|
-
},
|
|
7704
|
-
{
|
|
7705
|
-
id: "docker-push",
|
|
7706
|
-
pattern: /\bdocker\s+push\b/
|
|
7707
|
-
},
|
|
7708
|
-
{
|
|
7709
|
-
id: "cloud-upload",
|
|
7710
|
-
pattern: /\b(?:aws\s+s3|gsutil|gcloud\s+storage|az\s+storage)\b[^\n]*\b(?:cp|sync|mv|rm|rb)\b/
|
|
7711
|
-
},
|
|
7712
|
-
{
|
|
7713
|
-
id: "remote-copy",
|
|
7714
|
-
pattern: /\b(?:scp|rsync)\b[^\n]*(?::|@)/
|
|
7715
|
-
},
|
|
7716
|
-
{
|
|
7717
|
-
id: "pipe-to-shell",
|
|
7718
|
-
pattern: /\b(?:curl|wget|fetch)\b[^\n|]*\|\s*(?:sudo\s+)?(?:sh|bash|zsh|dash|ksh|python\d*|node|ruby|perl)\b/
|
|
7719
|
-
},
|
|
7720
|
-
{
|
|
7721
|
-
id: "power",
|
|
7722
|
-
pattern: /\b(?:shutdown|reboot|halt|poweroff)\b/
|
|
7723
|
-
},
|
|
7724
|
-
{
|
|
7725
|
-
id: "kill-everything",
|
|
7726
|
-
pattern: /\bkill\s+(?:-9\s+)?-1\b|\b(?:killall|pkill)\s+-9\b/
|
|
7727
|
-
},
|
|
7728
|
-
{
|
|
7729
|
-
id: "credential-write",
|
|
7730
|
-
pattern: /(?:>{1,2}|tee|cp|mv|rm|chmod|chown)\s[^\n]*(?:\.ssh\b|\.aws\b|\.gnupg\b|\.commandcode\/auth\.json|id_rsa|id_ed25519)/
|
|
7731
|
-
},
|
|
7732
|
-
{
|
|
7733
|
-
id: "history-clear",
|
|
7734
|
-
pattern: /\bhistory\s+-c\b|\bunset\s+HISTFILE\b/
|
|
7735
|
-
},
|
|
7736
|
-
{
|
|
7737
|
-
id: "global-install",
|
|
7738
|
-
pattern: /\b(?:npm|pnpm|yarn|bun)\s+(?:i|install|add)\s+(?:-g\b|--global\b)/
|
|
7739
|
-
},
|
|
7740
|
-
{
|
|
7741
|
-
id: "system-install",
|
|
7742
|
-
pattern: /\b(?:apt|apt-get|dnf|yum|zypper|apk|pacman|brew)\s+(?:install|remove|purge|uninstall|upgrade)\b/
|
|
7743
|
-
},
|
|
7744
|
-
{
|
|
7745
|
-
id: "language-install",
|
|
7746
|
-
pattern: /\b(?:pip3?|pipx|gem|cargo)\s+(?:install|add)\b/
|
|
7747
|
-
}
|
|
7748
|
-
];
|
|
7749
|
-
/**
|
|
7750
|
-
* Why the guard refuses to judge a command, when it does.
|
|
7751
|
-
*
|
|
7752
|
-
* @param command - the raw command text.
|
|
7753
|
-
* @returns the matching denylist id, or undefined.
|
|
7754
|
-
*/
|
|
7755
|
-
function denyReasonOf(command) {
|
|
7756
|
-
for (const entry of COMMAND_GUARD_DENY_LIST) if (entry.pattern.test(command)) return entry.id;
|
|
7757
|
-
}
|
|
7758
|
-
/** A non-empty string member, or undefined. */
|
|
7759
|
-
function stringMember(value, key) {
|
|
7760
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
7761
|
-
const member = value[key];
|
|
7762
|
-
return typeof member === "string" && member.trim() !== "" ? member : void 0;
|
|
7763
|
-
}
|
|
7764
|
-
/**
|
|
7765
|
-
* Read a shell command out of a tool execution, or undefined when this call is
|
|
7766
|
-
* not one the guard judges (a different tool, or arguments without a command).
|
|
7767
|
-
*
|
|
7768
|
-
* @param name - the executed tool's name.
|
|
7769
|
-
* @param args - the execution's parsed `arguments`.
|
|
7770
|
-
* @returns the fact to judge, without the asker's reason (that arrives later).
|
|
7771
|
-
*/
|
|
7772
|
-
function shellCommandOf(name, args) {
|
|
7773
|
-
if (!COMMAND_GUARD_SHELL_TOOLS.includes(name)) return void 0;
|
|
7774
|
-
const command = stringMember(args, "command");
|
|
7775
|
-
if (command === void 0) return void 0;
|
|
7776
|
-
const description = stringMember(args, "description");
|
|
7777
|
-
const workdir = stringMember(args, "workdir");
|
|
7778
|
-
const sandboxPermissions = stringMember(args, "sandbox_permissions");
|
|
7779
|
-
return {
|
|
7780
|
-
tool: name,
|
|
7781
|
-
command,
|
|
7782
|
-
...description === void 0 ? {} : { description },
|
|
7783
|
-
...workdir === void 0 ? {} : { workdir },
|
|
7784
|
-
...sandboxPermissions === void 0 ? {} : { sandboxPermissions }
|
|
7785
|
-
};
|
|
7786
|
-
}
|
|
7787
|
-
/**
|
|
7788
|
-
* Build the decision request for one command.
|
|
7789
|
-
*
|
|
7790
|
-
* A normal approval asks one question. A call that explicitly requests a wider
|
|
7791
|
-
* sandbox gets the same safety question plus two veto questions. This keeps the
|
|
7792
|
-
* common case cheap while making the broader permission change an explicit
|
|
7793
|
-
* decision: JEV must agree that the extra access has a narrow purpose and is
|
|
7794
|
-
* necessary, not merely that the command text looks familiar.
|
|
7795
|
-
*
|
|
7796
|
-
* The agent's own `description` and the harness's `reason` are included as
|
|
7797
|
-
* context, but the criteria say the command text itself decides — a
|
|
7798
|
-
* model-written description is a claim, not evidence.
|
|
7799
|
-
*
|
|
7800
|
-
* @param fact - the command and its context.
|
|
7801
|
-
* @returns the request to send.
|
|
7802
|
-
*/
|
|
7803
|
-
function buildSafetyRequest(fact) {
|
|
7804
|
-
const escalated = fact.sandboxPermissions !== void 0;
|
|
7805
|
-
const lines = [
|
|
7806
|
-
`Tool: ${fact.tool}`,
|
|
7807
|
-
"Command:",
|
|
7808
|
-
fact.command,
|
|
7809
|
-
...fact.description === void 0 ? [] : ["", `The agent describes this command as: ${fact.description}`],
|
|
7810
|
-
...fact.workdir === void 0 ? [] : ["", `Working directory: ${fact.workdir}`],
|
|
7811
|
-
...fact.sandboxPermissions === void 0 ? [] : ["", `The harness is asking because this call requests a wider sandbox mode (${fact.sandboxPermissions}) than the one currently in force.`],
|
|
7812
|
-
...fact.reason === void 0 ? [] : ["", `The asker's reason for needing approval: ${fact.reason}`]
|
|
7813
|
-
];
|
|
7814
|
-
const question = {
|
|
7815
|
-
type: "noul",
|
|
7816
|
-
instructions: escalated ? "Is it safe to run this command right now WITHOUT asking the user first, including the requested sandbox escalation? Judge the command text itself; a description or stated reason is only a claim about it." : "Is it safe to run this command right now WITHOUT asking the user first? Judge the command text itself; a description is only a claim about it.",
|
|
7817
|
-
criteria: {
|
|
7818
|
-
true: escalated ? "A normal, non-obfuscated read, build, or test operation whose outside-workspace effects are limited to conventional build/dependency caches or temporary files and are easily reversible: no unrelated user or system data is accessed, no data leaves the machine, and no credentials, git history or remote state are touched." : "Read-only, or its effects stay inside the current project and are trivially reversible: no data leaves the machine, nothing outside the project is modified, and no credentials, git history or remote state are touched.",
|
|
7819
|
-
false: "Anything else. Broad or unrelated outside-workspace access, deleting or overwriting files outside the project, rewriting or publishing git history, uploading data anywhere, installing system or global packages, changing permissions, ownership or credentials, killing processes, running as another user or as root, piping a download into a shell, or a command obfuscated enough that its effect cannot be read."
|
|
7820
|
-
}
|
|
7821
|
-
};
|
|
7822
|
-
const questions = { [COMMAND_GUARD_QUESTION_ID]: question };
|
|
7823
|
-
if (escalated) {
|
|
7824
|
-
questions[COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID] = {
|
|
7825
|
-
type: "noul",
|
|
7826
|
-
instructions: "Is the requested wider sandbox access limited to a narrow, conventional purpose supported by this command, such as a build cache, dependency cache, or temporary directory, rather than unrelated user files, credentials, other projects, or the network?",
|
|
7827
|
-
criteria: {
|
|
7828
|
-
true: "The wider access is plausibly limited to conventional local build/dependency caches or temporary files required by the command, with no unrelated host data or remote transfer in scope.",
|
|
7829
|
-
false: "The command needs broad or unrelated host access, cannot establish a narrow purpose, or would use the wider permission to reach credentials, other projects, user files, or the network."
|
|
7830
|
-
}
|
|
7831
|
-
};
|
|
7832
|
-
questions[COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID] = {
|
|
7833
|
-
type: "noul",
|
|
7834
|
-
instructions: "Is the requested sandbox escalation necessary and proportionate for this one command, rather than broader than the command needs? Treat the stated reason as a claim to check against the command, not as proof.",
|
|
7835
|
-
criteria: {
|
|
7836
|
-
true: "The command and its stated purpose support a one-shot escalation that is needed for the task and is no broader than the requested effect requires.",
|
|
7837
|
-
false: "The wider mode is unnecessary, broader than needed, unsupported by the command, or the command is too ambiguous to justify the extra authority."
|
|
7838
|
-
}
|
|
7839
|
-
};
|
|
7840
|
-
}
|
|
7841
|
-
return {
|
|
7842
|
-
state: lines.join("\n"),
|
|
7843
|
-
questions
|
|
7844
|
-
};
|
|
7845
|
-
}
|
|
7846
|
-
/**
|
|
7847
|
-
* Read a probability out of a decision response.
|
|
7848
|
-
*
|
|
7849
|
-
* @param response - the parsed response.
|
|
7850
|
-
* @param questionId - the question id to read (defaults to the guard's own).
|
|
7851
|
-
* @returns the probability, or undefined when the answer is missing or is not
|
|
7852
|
-
* a `noul` verdict.
|
|
7853
|
-
*/
|
|
7854
|
-
function safeProbabilityOf(response, questionId = COMMAND_GUARD_QUESTION_ID) {
|
|
7855
|
-
const answer = response.answers[questionId];
|
|
7856
|
-
return answer !== void 0 && answer.type === "noul" ? answer.noul : void 0;
|
|
7857
|
-
}
|
|
7858
|
-
/**
|
|
7859
|
-
* One-line, length-bounded excerpt of a command for the log. The full text is
|
|
7860
|
-
* already in the session's tool call and was already sent to the decision
|
|
7861
|
-
* endpoint, so this adds no exposure — it is what makes "why did this run
|
|
7862
|
-
* without asking?" answerable from the log alone.
|
|
7863
|
-
*/
|
|
7864
|
-
function commandExcerpt(command, limit = 160) {
|
|
7865
|
-
const flat = command.replace(/\s+/g, " ").trim();
|
|
7866
|
-
return flat.length <= limit ? flat : `${flat.slice(0, limit - 1)}…`;
|
|
7867
|
-
}
|
|
7868
|
-
/**
|
|
7869
|
-
* The guard's stateful half: what was executed (by call id) and what has
|
|
7870
|
-
* already been decided (by agent and complete decision context), both bounded.
|
|
7871
|
-
*
|
|
7872
|
-
* The execution cache is the bridge between the two seams — `tools/pre-execute`
|
|
7873
|
-
* knows the arguments, `approval/request` knows the call id — and it is keyed by
|
|
7874
|
-
* call id with an agent-identity check when read: an entry for an execution
|
|
7875
|
-
* that never asked is never read, so there is nothing to clean up on the result
|
|
7876
|
-
* side (and the ask happens before the result anyway).
|
|
7877
|
-
*/
|
|
7878
|
-
var CommandGuard = class {
|
|
7879
|
-
deps;
|
|
7880
|
-
executions = /* @__PURE__ */ new Map();
|
|
7881
|
-
decisions = /* @__PURE__ */ new WeakMap();
|
|
7882
|
-
constructor(deps) {
|
|
7883
|
-
this.deps = deps;
|
|
7884
|
-
}
|
|
7885
|
-
/**
|
|
7886
|
-
* Remember one execution's command. Never throws, never changes a decision.
|
|
7887
|
-
*
|
|
7888
|
-
* @param execution - the execution about to run.
|
|
7889
|
-
*/
|
|
7890
|
-
noteExecution(execution) {
|
|
7891
|
-
const callId = execution?.callId;
|
|
7892
|
-
if (typeof callId !== "string" || callId === "") return;
|
|
7893
|
-
const fact = shellCommandOf(execution.name, execution.arguments);
|
|
7894
|
-
if (fact === void 0) return;
|
|
7895
|
-
this.executions.delete(callId);
|
|
7896
|
-
this.executions.set(callId, {
|
|
7897
|
-
fact,
|
|
7898
|
-
agent: execution.agent
|
|
7899
|
-
});
|
|
7900
|
-
while (this.executions.size > 64) {
|
|
7901
|
-
const oldest = this.executions.keys().next();
|
|
7902
|
-
if (oldest.done === true) break;
|
|
7903
|
-
this.executions.delete(oldest.value);
|
|
7904
|
-
}
|
|
7905
|
-
}
|
|
7906
|
-
/** Forget every remembered execution and decision (disposal, tests). */
|
|
7907
|
-
clear() {
|
|
7908
|
-
this.executions.clear();
|
|
7909
|
-
this.decisions = /* @__PURE__ */ new WeakMap();
|
|
7910
|
-
}
|
|
7911
|
-
/**
|
|
7912
|
-
* Judge one approval request.
|
|
7913
|
-
*
|
|
7914
|
-
* @param request - the pending approval.
|
|
7915
|
-
* @returns the verdict; `approve` is the only outcome that grants anything.
|
|
7916
|
-
*/
|
|
7917
|
-
async judge(request) {
|
|
7918
|
-
const settings = this.deps.settings();
|
|
7919
|
-
if (!settings.enabled) return {
|
|
7920
|
-
kind: "delegate",
|
|
7921
|
-
reason: "guard disabled"
|
|
7922
|
-
};
|
|
7923
|
-
const callId = request.callId;
|
|
7924
|
-
if (callId === void 0) return {
|
|
7925
|
-
kind: "delegate",
|
|
7926
|
-
reason: "approval carries no call id"
|
|
7927
|
-
};
|
|
7928
|
-
const observed = this.executions.get(callId);
|
|
7929
|
-
if (observed === void 0 || observed.agent !== request.agent) return {
|
|
7930
|
-
kind: "delegate",
|
|
7931
|
-
reason: "the guard never saw this call for this agent"
|
|
7932
|
-
};
|
|
7933
|
-
const fact = observed.fact;
|
|
7934
|
-
const excerpt = commandExcerpt(fact.command);
|
|
7935
|
-
if (!COMMAND_GUARD_SHELL_TOOLS.includes(request.toolName)) return {
|
|
7936
|
-
kind: "delegate",
|
|
7937
|
-
reason: `not a shell tool (${request.toolName})`
|
|
7938
|
-
};
|
|
7939
|
-
if (fact.command.length > 6e3) return {
|
|
7940
|
-
kind: "delegate",
|
|
7941
|
-
reason: `command longer than ${COMMAND_GUARD_MAX_COMMAND_CHARS} characters`,
|
|
7942
|
-
command: excerpt
|
|
7943
|
-
};
|
|
7944
|
-
const denied = denyReasonOf(fact.command);
|
|
7945
|
-
if (denied !== void 0) return {
|
|
7946
|
-
kind: "delegate",
|
|
7947
|
-
reason: `denylisted command (${denied})`,
|
|
7948
|
-
command: excerpt
|
|
7949
|
-
};
|
|
7950
|
-
const full = {
|
|
7951
|
-
...fact,
|
|
7952
|
-
...request.reason === void 0 ? {} : { reason: request.reason }
|
|
7953
|
-
};
|
|
7954
|
-
const memoKey = JSON.stringify(full);
|
|
7955
|
-
const now = this.deps.now?.() ?? Date.now();
|
|
7956
|
-
const agentDecisions = request.agent === void 0 ? void 0 : this.decisions.get(request.agent);
|
|
7957
|
-
const memo = agentDecisions?.get(memoKey);
|
|
7958
|
-
if (memo !== void 0) {
|
|
7959
|
-
if (now - memo.at < 6e5) return this.verdict(memo, settings.threshold, "memo", excerpt, full.sandboxPermissions !== void 0);
|
|
7960
|
-
agentDecisions?.delete(memoKey);
|
|
7961
|
-
}
|
|
7962
|
-
let response;
|
|
7963
|
-
try {
|
|
7964
|
-
response = await this.deps.decide(buildSafetyRequest(full), {
|
|
7965
|
-
...request.signal === void 0 ? {} : { signal: request.signal },
|
|
7966
|
-
timeoutMs: settings.timeoutMs
|
|
7967
|
-
});
|
|
7968
|
-
} catch (error) {
|
|
7969
|
-
return {
|
|
7970
|
-
kind: "delegate",
|
|
7971
|
-
reason: `decision unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
7972
|
-
command: excerpt
|
|
7973
|
-
};
|
|
7974
|
-
}
|
|
7975
|
-
const probability = safeProbabilityOf(response);
|
|
7976
|
-
if (probability === void 0) return {
|
|
7977
|
-
kind: "delegate",
|
|
7978
|
-
reason: "the decision carried no safe verdict",
|
|
7979
|
-
command: excerpt
|
|
7980
|
-
};
|
|
7981
|
-
let escalation;
|
|
7982
|
-
if (full.sandboxPermissions !== void 0) {
|
|
7983
|
-
const scope = safeProbabilityOf(response, COMMAND_GUARD_ESCALATION_SCOPE_QUESTION_ID);
|
|
7984
|
-
const necessity = safeProbabilityOf(response, COMMAND_GUARD_ESCALATION_NECESSITY_QUESTION_ID);
|
|
7985
|
-
if (scope === void 0 || necessity === void 0) return {
|
|
7986
|
-
kind: "delegate",
|
|
7987
|
-
reason: "the decision carried no complete sandbox-escalation verdict",
|
|
7988
|
-
command: excerpt
|
|
7989
|
-
};
|
|
7990
|
-
escalation = {
|
|
7991
|
-
scope,
|
|
7992
|
-
necessity
|
|
7993
|
-
};
|
|
7994
|
-
}
|
|
7995
|
-
const memoEntry = {
|
|
7996
|
-
probability,
|
|
7997
|
-
...escalation === void 0 ? {} : { escalation },
|
|
7998
|
-
at: now
|
|
7999
|
-
};
|
|
8000
|
-
if (request.agent !== void 0) {
|
|
8001
|
-
const decisions = agentDecisions ?? /* @__PURE__ */ new Map();
|
|
8002
|
-
decisions.set(memoKey, memoEntry);
|
|
8003
|
-
while (decisions.size > 64) {
|
|
8004
|
-
const oldest = decisions.keys().next();
|
|
8005
|
-
if (oldest.done === true) break;
|
|
8006
|
-
decisions.delete(oldest.value);
|
|
8007
|
-
}
|
|
8008
|
-
this.decisions.set(request.agent, decisions);
|
|
8009
|
-
}
|
|
8010
|
-
return this.verdict(memoEntry, settings.threshold, "model", excerpt, full.sandboxPermissions !== void 0);
|
|
8011
|
-
}
|
|
8012
|
-
/** Apply the threshold to the primary verdict and any escalation vetoes. */
|
|
8013
|
-
verdict(entry, threshold, source, command, escalated) {
|
|
8014
|
-
if (entry.probability < threshold) return {
|
|
8015
|
-
kind: "delegate",
|
|
8016
|
-
reason: `safe probability ${entry.probability.toFixed(3)} below ${threshold}`,
|
|
8017
|
-
probability: entry.probability,
|
|
8018
|
-
command
|
|
8019
|
-
};
|
|
8020
|
-
if (!escalated) return {
|
|
8021
|
-
kind: "approve",
|
|
8022
|
-
probability: entry.probability,
|
|
8023
|
-
source,
|
|
8024
|
-
command
|
|
8025
|
-
};
|
|
8026
|
-
if (entry.escalation === void 0) return {
|
|
8027
|
-
kind: "delegate",
|
|
8028
|
-
reason: "the cached sandbox-escalation verdict was incomplete",
|
|
8029
|
-
command
|
|
8030
|
-
};
|
|
8031
|
-
if (entry.escalation.scope < threshold) return {
|
|
8032
|
-
kind: "delegate",
|
|
8033
|
-
reason: `sandbox scope probability ${entry.escalation.scope.toFixed(3)} below ${threshold}`,
|
|
8034
|
-
probability: entry.probability,
|
|
8035
|
-
command
|
|
8036
|
-
};
|
|
8037
|
-
if (entry.escalation.necessity < threshold) return {
|
|
8038
|
-
kind: "delegate",
|
|
8039
|
-
reason: `sandbox necessity probability ${entry.escalation.necessity.toFixed(3)} below ${threshold}`,
|
|
8040
|
-
probability: entry.probability,
|
|
8041
|
-
command
|
|
8042
|
-
};
|
|
8043
|
-
return {
|
|
8044
|
-
kind: "approve",
|
|
8045
|
-
probability: entry.probability,
|
|
8046
|
-
source,
|
|
8047
|
-
...entry.escalation === void 0 ? {} : { escalation: entry.escalation },
|
|
8048
|
-
command
|
|
8049
|
-
};
|
|
8050
|
-
}
|
|
8051
|
-
};
|
|
8052
|
-
/**
|
|
8053
|
-
* Register the guard on a context that has the approval seam.
|
|
8054
|
-
*
|
|
8055
|
-
* Call this from inside `ctx.inject(['approval'], …)`: the seam's presence is
|
|
8056
|
-
* what makes the feature meaningful (no approval service means no asks), and
|
|
8057
|
-
* tying the listeners to that injected fiber is what makes them go away with it.
|
|
8058
|
-
*
|
|
8059
|
-
* @param ctx - the injected context (or a test double with `on`).
|
|
8060
|
-
* @param deps - settings, the decision client, clock and logger.
|
|
8061
|
-
* @returns a disposer for both listeners.
|
|
8062
|
-
*/
|
|
8063
|
-
function applyCommandGuard(ctx, deps) {
|
|
8064
|
-
const guard = new CommandGuard(deps);
|
|
8065
|
-
const events = ctx;
|
|
8066
|
-
const offPreExecute = events.on("tools/pre-execute", (execution, next) => {
|
|
8067
|
-
guard.noteExecution(execution);
|
|
8068
|
-
return next();
|
|
8069
|
-
}, { prepend: true });
|
|
8070
|
-
const offApproval = events.on("approval/request", async (request, next) => {
|
|
8071
|
-
try {
|
|
8072
|
-
if (!deps.settings().enabled) return next();
|
|
8073
|
-
} catch (error) {
|
|
8074
|
-
deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
8075
|
-
return next();
|
|
8076
|
-
}
|
|
8077
|
-
let verdict;
|
|
8078
|
-
try {
|
|
8079
|
-
verdict = await guard.judge(request);
|
|
8080
|
-
} catch (error) {
|
|
8081
|
-
deps.log?.(`command guard failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
8082
|
-
return next();
|
|
8083
|
-
}
|
|
8084
|
-
if (verdict.kind === "approve") {
|
|
8085
|
-
deps.log?.(`command guard auto-approved a ${request.toolName} call (safe probability ${verdict.probability.toFixed(3)}${verdict.escalation === void 0 ? "" : `, sandbox scope ${verdict.escalation.scope.toFixed(3)}, necessity ${verdict.escalation.necessity.toFixed(3)}`}, ${verdict.source}): ${verdict.command ?? "(command unknown)"}`);
|
|
8086
|
-
return "allowed-once";
|
|
8087
|
-
}
|
|
8088
|
-
deps.log?.(`command guard delegated a ${request.toolName} call: ${verdict.reason}${verdict.command === void 0 ? "" : ` — ${verdict.command}`}`);
|
|
8089
|
-
return next();
|
|
8090
|
-
}, { prepend: true });
|
|
8091
|
-
return () => {
|
|
8092
|
-
guard.clear();
|
|
8093
|
-
for (const off of [offPreExecute, offApproval]) if (typeof off === "function") off();
|
|
8094
|
-
};
|
|
8095
|
-
}
|
|
8096
|
-
//#endregion
|
|
8097
7227
|
//#region src/config-volatile.ts
|
|
8098
7228
|
/**
|
|
8099
7229
|
* Volatile-config helpers for dsh 0.1.7's schema-derived profile Config.
|
|
8100
7230
|
*
|
|
8101
|
-
* 0.1.7
|
|
8102
|
-
*
|
|
8103
|
-
*
|
|
8104
|
-
*
|
|
8105
|
-
*
|
|
8106
|
-
*
|
|
8107
|
-
*
|
|
8108
|
-
*
|
|
8109
|
-
*
|
|
8110
|
-
*
|
|
8111
|
-
*
|
|
8112
|
-
* -
|
|
8113
|
-
*
|
|
8114
|
-
*
|
|
8115
|
-
*
|
|
8116
|
-
* -
|
|
8117
|
-
* reference when one is present — returning a FRESH object per call, since
|
|
8118
|
-
* a reference's identity is stable while its value changes — and returns the
|
|
8119
|
-
* config untouched when none is.
|
|
8120
|
-
*
|
|
8121
|
-
* Only TOP-LEVEL fields are marked (form writes name top-level paths such as
|
|
8122
|
-
* `accounts` or `modelVisibility.<id>`, whose first segment is the marked
|
|
8123
|
-
* node), so one unwrap level is complete. The literal `apiKey` secret stays
|
|
8124
|
-
* unmarked on purpose: no settings surface writes it — the settings page and
|
|
8125
|
-
* the dsh-TUI section both write keys through the credentials seam — so it
|
|
8126
|
-
* keeps composition-only semantics, and a config-file edit to it reloading the
|
|
8127
|
-
* fiber is the correct behavior for a secret literal.
|
|
7231
|
+
* 0.1.7's `settings.describe()` projects each entry's schema through
|
|
7232
|
+
* `volatileForm()`, so a form contains ONLY nodes carrying `meta.volatile`
|
|
7233
|
+
* and a form edit is refused unless its path lies beneath a marked node. The
|
|
7234
|
+
* loader then resolves each marked field into a frozen `{ get() }` reference
|
|
7235
|
+
* that is written IN PLACE (no remount) and announced with
|
|
7236
|
+
* `loader/volatile-update`. Two helpers keep the rest of the plugin reading a
|
|
7237
|
+
* PLAIN `Config`: `markVolatile()` applies the mark, and
|
|
7238
|
+
* `unwrapVolatileConfig()` reads every top-level field through its reference
|
|
7239
|
+
* when one is present — FRESH per call, because a reference's identity is
|
|
7240
|
+
* stable while its value changes.
|
|
7241
|
+
*
|
|
7242
|
+
* Only TOP-LEVEL fields are marked (a form write names a top-level path such
|
|
7243
|
+
* as `accounts` or `modelVisibility.<id>`), so one unwrap level is complete.
|
|
7244
|
+
* The literal `apiKey` secret stays unmarked on purpose: no settings surface
|
|
7245
|
+
* writes it — page and TUI both write keys through the credentials seam — so a
|
|
7246
|
+
* config-file edit to it reloading the fiber is correct for a secret literal.
|
|
8128
7247
|
*
|
|
8129
7248
|
* @module dsh-commandcode-provider/config-volatile
|
|
8130
7249
|
*/
|
|
8131
7250
|
/**
|
|
8132
7251
|
* Mark one schema field volatile.
|
|
8133
7252
|
*
|
|
8134
|
-
* `.volatile()` is unconditional:
|
|
8135
|
-
*
|
|
8136
|
-
*
|
|
8137
|
-
*
|
|
8138
|
-
*
|
|
8139
|
-
* consumer reads while the schema parses to live references.
|
|
8140
|
-
*
|
|
8141
|
-
* @param schema - The field schema.
|
|
8142
|
-
* @returns The schema carrying `meta.volatile`.
|
|
7253
|
+
* `.volatile()` is unconditional: it is a schemastery 3.18.3 feature and the
|
|
7254
|
+
* only supported engine (`dsh 0.1.7-rc.2`) pins `~3.18.4`, so the method is
|
|
7255
|
+
* always present. It is reached through a structural member so the caller's
|
|
7256
|
+
* declared field type survives — the mark changes what the schema PARSES to (a
|
|
7257
|
+
* live reference), not the plugin-facing `Config` shape.
|
|
8143
7258
|
*/
|
|
8144
7259
|
function markVolatile(schema) {
|
|
8145
7260
|
return schema.volatile();
|
|
@@ -8147,10 +7262,6 @@ function markVolatile(schema) {
|
|
|
8147
7262
|
/**
|
|
8148
7263
|
* Mark every field of a Config field dict volatile, except the named
|
|
8149
7264
|
* composition-only secrets (`apiKey` here, which no settings surface writes).
|
|
8150
|
-
*
|
|
8151
|
-
* @param fields - Freshly built field schemas.
|
|
8152
|
-
* @param skip - Field names that must stay unmarked.
|
|
8153
|
-
* @returns The field dict with every non-skipped field marked.
|
|
8154
7265
|
*/
|
|
8155
7266
|
function markVolatileFields(fields, skip = []) {
|
|
8156
7267
|
return Object.fromEntries(Object.entries(fields).map(([name, schema]) => [name, skip.includes(name) ? schema : markVolatile(schema)]));
|
|
@@ -8172,8 +7283,8 @@ function isVolatileRef(value) {
|
|
|
8172
7283
|
*
|
|
8173
7284
|
* Every top-level field found to be a volatile reference is read through
|
|
8174
7285
|
* `.get()` — freshly on every call, so a live update lands on the very next
|
|
8175
|
-
* read
|
|
8176
|
-
* included, so a loader-parsed config always yields a fresh object here
|
|
7286
|
+
* read (the loader resolves EVERY marked node into a reference, unset fields
|
|
7287
|
+
* included, so a loader-parsed config always yields a fresh object here). The
|
|
8177
7288
|
* pass-through for a field that is not a reference is what keeps the unmarked
|
|
8178
7289
|
* `apiKey` literal readable.
|
|
8179
7290
|
*/
|
|
@@ -8188,15 +7299,14 @@ function unwrapVolatileConfig(config) {
|
|
|
8188
7299
|
//#region src/index.ts
|
|
8189
7300
|
/**
|
|
8190
7301
|
* dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
|
|
8191
|
-
* Code (unofficial; ported from pi-commandcode-provider@0.5.1).
|
|
7302
|
+
* Code (unofficial; ported from pi-commandcode-provider@0.5.1, MIT).
|
|
8192
7303
|
*
|
|
8193
7304
|
* Registers the `commandcode` provider route on `ctx.llm` and declares it in
|
|
8194
7305
|
* the configurable-provider directory, so the web Models page shows a
|
|
8195
|
-
* "Command Code" card
|
|
8196
|
-
*
|
|
8197
|
-
*
|
|
8198
|
-
*
|
|
8199
|
-
* next request without a restart.
|
|
7306
|
+
* "Command Code" card and the model picker lists the live catalog. Connection
|
|
7307
|
+
* facts are resolved per request from the live profile Config (volatile
|
|
7308
|
+
* fields, unwrapped per read) and the credential seam, so a changed key,
|
|
7309
|
+
* endpoint or cache path reaches the next request without a restart.
|
|
8200
7310
|
*
|
|
8201
7311
|
* ```yaml
|
|
8202
7312
|
* - id: llm-commandcode
|
|
@@ -8223,14 +7333,12 @@ const PROVIDER = "commandcode";
|
|
|
8223
7333
|
const DEFAULT_MODELS_CACHE_PATH = join(homedir(), ".commandcode", "models-cache.json");
|
|
8224
7334
|
/**
|
|
8225
7335
|
* The Config schema: every field volatile except the composition-only `apiKey`
|
|
8226
|
-
* secret.
|
|
8227
|
-
*
|
|
8228
|
-
*
|
|
8229
|
-
*
|
|
8230
|
-
*
|
|
8231
|
-
*
|
|
8232
|
-
* without remounting this fiber (the `loader/volatile-update` listener at the
|
|
8233
|
-
* bottom of `apply` covers the facts that are not re-derived per read).
|
|
7336
|
+
* secret. 0.1.7's settings forms are projected from the schema's
|
|
7337
|
+
* `meta.volatile` nodes, so an unmarked field would be invisible to AND
|
|
7338
|
+
* unwritable from the settings page. The loader also hands `apply()` a live
|
|
7339
|
+
* reference per marked field, so writes commit in place without remounting
|
|
7340
|
+
* this fiber — the `loader/volatile-update` listener at the bottom of `apply`
|
|
7341
|
+
* re-derives the two facts that are not re-read per request.
|
|
8234
7342
|
*/
|
|
8235
7343
|
const Config = z.object(markVolatileFields({
|
|
8236
7344
|
apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
|
|
@@ -8243,14 +7351,6 @@ const Config = z.object(markVolatileFields({
|
|
|
8243
7351
|
transportMaxRetries: z.number().min(0).max(50),
|
|
8244
7352
|
filterModelsByPlan: z.boolean(),
|
|
8245
7353
|
visibleModels: z.array(z.string()),
|
|
8246
|
-
/**
|
|
8247
|
-
* Per-model visibility overrides for the terminal settings page's checkbox
|
|
8248
|
-
* list, keyed by catalog id. dsh-TUI addresses a staged edit by its field
|
|
8249
|
-
* PATH, so two checkboxes sharing one path would overwrite each other's
|
|
8250
|
-
* draft and the section's last model would decide every write; a map gives
|
|
8251
|
-
* each checkbox a path of its own. An id listed here wins over
|
|
8252
|
-
* {@link Config.visibleModels}; ids absent here keep following it.
|
|
8253
|
-
*/
|
|
8254
7354
|
modelVisibility: z.dict(z.boolean()),
|
|
8255
7355
|
webSearch: z.boolean().default(true),
|
|
8256
7356
|
showSidebarQuota: z.boolean().default(false),
|
|
@@ -8265,8 +7365,6 @@ const Config = z.object(markVolatileFields({
|
|
|
8265
7365
|
models: z.array(z.string()),
|
|
8266
7366
|
account: z.string()
|
|
8267
7367
|
})),
|
|
8268
|
-
commandGuard: z.boolean().default(false),
|
|
8269
|
-
commandGuardLevel: z.union([...COMMAND_GUARD_LEVELS]).default(COMMAND_GUARD_DEFAULT_LEVEL),
|
|
8270
7368
|
zdr: z.boolean().default(false),
|
|
8271
7369
|
lang: z.string().pattern(/^(zh|en)$/).default("zh")
|
|
8272
7370
|
}, ["apiKey"]));
|
|
@@ -8274,7 +7372,7 @@ const Config = z.object(markVolatileFields({
|
|
|
8274
7372
|
* The one explicit resolve step from raw config to validated connection
|
|
8275
7373
|
* facts. Programmatic construction may bypass Schemastery normalization, so
|
|
8276
7374
|
* every default is re-judged here — for the composition entry at load and for
|
|
8277
|
-
*
|
|
7375
|
+
* every settings-backed read.
|
|
8278
7376
|
*/
|
|
8279
7377
|
function resolveAdapterOptions(config) {
|
|
8280
7378
|
return {
|
|
@@ -8291,11 +7389,10 @@ function resolveAdapterOptions(config) {
|
|
|
8291
7389
|
};
|
|
8292
7390
|
}
|
|
8293
7391
|
/**
|
|
8294
|
-
* Per-model visibility overrides, cleaned for the adapter
|
|
8295
|
-
*
|
|
8296
|
-
*
|
|
8297
|
-
*
|
|
8298
|
-
* @returns A frozen id → boolean map, or undefined when nothing is set.
|
|
7392
|
+
* Per-model visibility overrides, cleaned for the adapter: a non-object or a
|
|
7393
|
+
* non-boolean entry is dropped rather than reaching the picker filter, and an
|
|
7394
|
+
* all-empty map reads as no map. Returns a fresh id → boolean map, or
|
|
7395
|
+
* undefined when nothing is set.
|
|
8299
7396
|
*/
|
|
8300
7397
|
function readModelVisibility(raw) {
|
|
8301
7398
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
@@ -8493,26 +7590,6 @@ function apply(ctx, config) {
|
|
|
8493
7590
|
applyCommandCodeSearchSelection(webCtx.web, searchSelection, false);
|
|
8494
7591
|
}, "dsh-commandcode-provider: web search selection");
|
|
8495
7592
|
});
|
|
8496
|
-
const commandGuardSettings = () => {
|
|
8497
|
-
const raw = current();
|
|
8498
|
-
return {
|
|
8499
|
-
enabled: raw.commandGuard === true,
|
|
8500
|
-
threshold: commandGuardThreshold(raw.commandGuardLevel),
|
|
8501
|
-
timeoutMs: COMMAND_GUARD_TIMEOUT_MS
|
|
8502
|
-
};
|
|
8503
|
-
};
|
|
8504
|
-
ctx.inject(["approval"], (approvalCtx) => {
|
|
8505
|
-
applyCommandGuard(approvalCtx, {
|
|
8506
|
-
settings: commandGuardSettings,
|
|
8507
|
-
decide: (request, decideOptions) => runSystemOne({
|
|
8508
|
-
apiBase: () => options().apiBase,
|
|
8509
|
-
resolveApiKey: () => resolveApiKey(options())
|
|
8510
|
-
}, request, decideOptions),
|
|
8511
|
-
log: (message) => {
|
|
8512
|
-
ctx.logger.info(`llm-commandcode: ${message}`);
|
|
8513
|
-
}
|
|
8514
|
-
});
|
|
8515
|
-
});
|
|
8516
7593
|
let refreshTuiSettings;
|
|
8517
7594
|
ctx.inject(["tuiSettingsSections"], (tuiCtx) => {
|
|
8518
7595
|
refreshTuiSettings = applyCommandCodeTuiSettings(tuiCtx, {
|