@mars-sea/dsh-commandcode-provider 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/lib/client.js +1 -1
- package/lib/index.d.ts +114 -39
- package/lib/index.js +1335 -191
- package/lib/index.js.map +1 -1
- package/package.json +40 -37
package/lib/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { CONTEXT_WINDOW_EXCEEDED_CODE, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter,
|
|
|
6
6
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
7
7
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
8
8
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
9
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
import { appendFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
10
10
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
11
11
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
12
12
|
import { createServer } from "node:http";
|
|
@@ -103,7 +103,7 @@ const PLAN_ORDER = Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier
|
|
|
103
103
|
* subscription-plan labels, deals, and hourly (peak/off-peak) pricing.
|
|
104
104
|
*
|
|
105
105
|
* Everything here is synced from official sources — the command-code CLI
|
|
106
|
-
* bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.
|
|
106
|
+
* bundle's model table (`dist/cli.mjs`, re-verified at command-code@1.72.1) and
|
|
107
107
|
* the official plan/pricing/model docs; see the dsh-commandcode-upstream skill
|
|
108
108
|
* for the extraction procedures. Keeping the snapshot in its own module
|
|
109
109
|
* confines those frequent sync diffs here: src/adapter.ts holds only the stable
|
|
@@ -408,6 +408,18 @@ const KNOWN_EFFORTS = {
|
|
|
408
408
|
"low",
|
|
409
409
|
"medium",
|
|
410
410
|
"xhigh"
|
|
411
|
+
],
|
|
412
|
+
"gpt-6.1-sol": [
|
|
413
|
+
"low",
|
|
414
|
+
"medium",
|
|
415
|
+
"high",
|
|
416
|
+
"xhigh",
|
|
417
|
+
"max"
|
|
418
|
+
],
|
|
419
|
+
"inclusionai/ling-3.1-flash:free": [
|
|
420
|
+
"low",
|
|
421
|
+
"medium",
|
|
422
|
+
"high"
|
|
411
423
|
]
|
|
412
424
|
};
|
|
413
425
|
/**
|
|
@@ -464,6 +476,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
464
476
|
"gpt-6-astra",
|
|
465
477
|
"gpt-6-luna",
|
|
466
478
|
"gpt-6-sol",
|
|
479
|
+
"gpt-6.1-sol",
|
|
467
480
|
"meta/muse-spark-1.1",
|
|
468
481
|
"meta/muse-spark-1.2",
|
|
469
482
|
"meta/muse-spark-1.2-contributor",
|
|
@@ -493,7 +506,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
493
506
|
]);
|
|
494
507
|
/**
|
|
495
508
|
* Models WITHOUT a zero-data-retention upstream, per the official CLI's own
|
|
496
|
-
* registry (`command-code@1.
|
|
509
|
+
* registry (`command-code@1.72.1` `dist/cli.mjs`): `modelSupportsZdr(id)` is
|
|
497
510
|
* exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
|
|
498
511
|
* carries the same membership in the sibling route table — the UNION of both
|
|
499
512
|
* is this set. Reading only the sibling route table would drop `meituan/
|
|
@@ -594,16 +607,22 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
|
|
|
594
607
|
/**
|
|
595
608
|
* Catalog models the Provider API serves ONLY through `/provider/v1/messages`
|
|
596
609
|
* (Anthropic Messages shape). Every other model in the catalog answers on
|
|
597
|
-
* `/provider/v1/chat/completions
|
|
598
|
-
*
|
|
599
|
-
*
|
|
610
|
+
* `/provider/v1/chat/completions`.
|
|
611
|
+
*
|
|
612
|
+
* Measured 2026-09-16 (command-code@1.54.0, 69 models posted to
|
|
613
|
+
* `/provider/v1/chat/completions`): exactly the then-entire Claude family
|
|
614
|
+
* refused with HTTP 400 `Model "<id>" must be called via
|
|
615
|
+
* /provider/v1/messages (Anthropic Messages shape)`, and every one of them is
|
|
616
|
+
* routed normally by `/alpha/generate`.
|
|
600
617
|
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
618
|
+
* Re-measured 2026-09-29 against the 84-model catalog: the same ten
|
|
619
|
+
* `claude-*` ids are the only ones carrying `supported_endpoints:
|
|
620
|
+
* ["/messages"]`, and posting a non-Claude model to the endpoint answers 400
|
|
621
|
+
* `Model "<id>" is not supported on this endpoint. Use
|
|
622
|
+
* /provider/v1/chat/completions for OpenAI and OSS models.` The adapter routes
|
|
623
|
+
* on that field when the catalog supplies it, so this set is the FALLBACK for
|
|
624
|
+
* a cached or hand-built catalog, not the primary source — see
|
|
625
|
+
* `requiresMessagesEndpoint()`.
|
|
607
626
|
*
|
|
608
627
|
* Note what this list is NOT: it is not a plan gate. `claude-sonnet-5` is
|
|
609
628
|
* Pro-tier and `claude-opus-4-8` Provider-tier, so the accounts entitled to
|
|
@@ -612,20 +631,21 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
|
|
|
612
631
|
* request before this snapshot existed.
|
|
613
632
|
*
|
|
614
633
|
* `requiresMessagesEndpoint()` additionally treats any `claude-*` id as
|
|
615
|
-
* Messages-only, so a Claude model added upstream
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
* riding the CLI transport it already works on.
|
|
634
|
+
* Messages-only, so a Claude model added upstream is still routed correctly
|
|
635
|
+
* by an un-updated plugin instead of hard-failing with the 400 above. The
|
|
636
|
+
* worst case of that rule going stale the other way (upstream teaching
|
|
637
|
+
* `/provider/v1/chat/completions` to serve Claude) is one model riding a
|
|
638
|
+
* transport it already works on.
|
|
621
639
|
*
|
|
622
640
|
* Keep in sync when models ship (see the dsh-commandcode-upstream skill).
|
|
623
641
|
*/
|
|
624
642
|
const MESSAGES_ONLY_MODELS = /* @__PURE__ */ new Set([
|
|
643
|
+
"claude-sonnet-5-5",
|
|
625
644
|
"claude-sonnet-5",
|
|
626
645
|
"claude-sonnet-4-6",
|
|
627
646
|
"claude-fable-5-1",
|
|
628
647
|
"claude-fable-5",
|
|
648
|
+
"claude-opus-5-5",
|
|
629
649
|
"claude-opus-5",
|
|
630
650
|
"claude-opus-4-8",
|
|
631
651
|
"claude-opus-4-7",
|
|
@@ -636,12 +656,139 @@ function requiresMessagesEndpoint(modelId) {
|
|
|
636
656
|
return MESSAGES_ONLY_MODELS.has(modelId) || modelId.startsWith("claude-");
|
|
637
657
|
}
|
|
638
658
|
/**
|
|
659
|
+
* Per-model output ceilings, which `/provider/v1/models` does NOT publish (it
|
|
660
|
+
* carries `context_length` and `supported_endpoints` only) while every endpoint
|
|
661
|
+
* refuses a larger `max_tokens` per model. Sending the context window as the
|
|
662
|
+
* output budget is what produced issue #71: `claude-sonnet-5-5` has a
|
|
663
|
+
* 1 000 000-token window and a 128 000-token ceiling, the adapter derived
|
|
664
|
+
* 131072, and the endpoint refused every request of a fresh process.
|
|
665
|
+
*
|
|
666
|
+
* **Generated — do not hand-edit.** `scripts/sync-output-limits.mjs` rewrites
|
|
667
|
+
* this literal from models.dev; run it after a catalog sync, and use its
|
|
668
|
+
* `--check` in CI. Command Code resells these models through their VENDOR's own
|
|
669
|
+
* API, so the vendor's published ceiling is the value recorded: 76 of the 86
|
|
670
|
+
* catalog ids as of 2026-09-30. Each row carries a comment naming the entry it
|
|
671
|
+
* came from.
|
|
672
|
+
*
|
|
673
|
+
* Two of those 76 rest on more than a vendor lookup. `gpt-6.1-sol` (128000) comes
|
|
674
|
+
* from OpenAI's own row and is corroborated by every other provider carrying it.
|
|
675
|
+
* `inclusionai/ling-3.1-flash:free` (32768) is hand-recorded in the script's
|
|
676
|
+
* `MANUAL_CEILINGS`: models.dev has no `ling-3.1` entry yet, so it takes the Ling
|
|
677
|
+
* family's uniform ceiling. Both are still correctable — a stricter gateway costs
|
|
678
|
+
* one refused request, and `streamRequest()` remembers what the endpoint said.
|
|
679
|
+
*
|
|
680
|
+
* The 10 unlisted models are ones no vendor publishes and the aggregators
|
|
681
|
+
* disagree about. They are NOT a gap: a refusal states the ceiling in full, and
|
|
682
|
+
* `streamRequest()` reads it off, retries once and remembers it for the model
|
|
683
|
+
* (issue #71). A vendor figure the gateway turns out to be stricter than costs
|
|
684
|
+
* one round trip; a conservative guess costs half of every answer, forever —
|
|
685
|
+
* which is why the unlisted path defers to the endpoint rather than to a
|
|
686
|
+
* fallback here.
|
|
687
|
+
*
|
|
688
|
+
* An unlisted Messages-route model still falls back to
|
|
689
|
+
* {@link DEFAULT_MESSAGES_MAX_TOKENS} before any request is made, so a ceiling
|
|
690
|
+
* is never absent when the body is built.
|
|
691
|
+
*/
|
|
692
|
+
const MODEL_OUTPUT_TOKEN_LIMITS = /* @__PURE__ */ new Map([
|
|
693
|
+
["claude-fable-5", 128e3],
|
|
694
|
+
["claude-fable-5-1", 128e3],
|
|
695
|
+
["claude-haiku-4-5-20251001", 64e3],
|
|
696
|
+
["claude-opus-4-7", 128e3],
|
|
697
|
+
["claude-opus-4-8", 128e3],
|
|
698
|
+
["claude-opus-5", 128e3],
|
|
699
|
+
["claude-opus-5-5", 128e3],
|
|
700
|
+
["claude-sonnet-4-6", 128e3],
|
|
701
|
+
["claude-sonnet-5", 128e3],
|
|
702
|
+
["claude-sonnet-5-5", 128e3],
|
|
703
|
+
["deepseek/deepseek-v4-flash", 393216],
|
|
704
|
+
["deepseek/deepseek-v4-flash-vision-exp", 393216],
|
|
705
|
+
["deepseek/deepseek-v4-pro", 393216],
|
|
706
|
+
["google/gemini-3.1-flash-lite", 65536],
|
|
707
|
+
["google/gemini-3.5-flash", 65536],
|
|
708
|
+
["google/gemini-3.5-flash-lite", 65536],
|
|
709
|
+
["google/gemini-3.6-flash", 65536],
|
|
710
|
+
["google/gemini-3.7-flash", 65536],
|
|
711
|
+
["google/gemini-3.8-flash", 65536],
|
|
712
|
+
["gpt-5.3-codex", 128e3],
|
|
713
|
+
["gpt-5.4", 128e3],
|
|
714
|
+
["gpt-5.4-mini", 128e3],
|
|
715
|
+
["gpt-5.5", 128e3],
|
|
716
|
+
["gpt-5.6-luna", 128e3],
|
|
717
|
+
["gpt-5.6-sol", 128e3],
|
|
718
|
+
["gpt-5.6-terra", 128e3],
|
|
719
|
+
["gpt-6-astra", 128e3],
|
|
720
|
+
["gpt-6-luna", 128e3],
|
|
721
|
+
["gpt-6-sol", 128e3],
|
|
722
|
+
["gpt-6.1-sol", 128e3],
|
|
723
|
+
["inclusionai/ling-3.0-flash-sante:free", 32768],
|
|
724
|
+
["inclusionai/ling-3.1-flash:free", 32768],
|
|
725
|
+
["meituan/LongCat-2.0", 131072],
|
|
726
|
+
["meta/muse-spark-1.1", 131072],
|
|
727
|
+
["meta/muse-spark-1.2", 131072],
|
|
728
|
+
["meta/muse-spark-1.2-contributor", 131072],
|
|
729
|
+
["meta/muse-spark-1.3", 131072],
|
|
730
|
+
["meta/muse-spark-1.3-contributor", 131072],
|
|
731
|
+
["MiniMaxAI/MiniMax-M2.5", 131072],
|
|
732
|
+
["MiniMaxAI/MiniMax-M2.7", 131072],
|
|
733
|
+
["MiniMaxAI/MiniMax-M3", 512e3],
|
|
734
|
+
["moonshotai/Kimi-K2.6", 262144],
|
|
735
|
+
["moonshotai/Kimi-K2.7-Code", 262144],
|
|
736
|
+
["moonshotai/Kimi-K2.7-Code-Highspeed", 262144],
|
|
737
|
+
["moonshotai/Kimi-K3", 1048576],
|
|
738
|
+
["nvidia/nemotron-3-ultra-550b-a55b", 65536],
|
|
739
|
+
["poolside/laguna-s-2.1-free", 32768],
|
|
740
|
+
["Qwen/Qwen3.6-Max-Preview", 65536],
|
|
741
|
+
["Qwen/Qwen3.6-Plus", 65536],
|
|
742
|
+
["Qwen/Qwen3.7-Flash", 131072],
|
|
743
|
+
["Qwen/Qwen3.7-Max", 131072],
|
|
744
|
+
["Qwen/Qwen3.7-Plus", 131072],
|
|
745
|
+
["Qwen/Qwen3.8-Flash", 131072],
|
|
746
|
+
["Qwen/Qwen3.8-Max", 131072],
|
|
747
|
+
["Qwen/Qwen3.8-Omni-Flash", 131072],
|
|
748
|
+
["sakana/fugu-ultra", 1e6],
|
|
749
|
+
["stealth/pixel-canary", 131072],
|
|
750
|
+
["stealth/space-bunny-alpha", 524288],
|
|
751
|
+
["stepfun/Step-3.5-Flash", 256e3],
|
|
752
|
+
["stepfun/Step-3.7-Flash", 256e3],
|
|
753
|
+
["stepfun/Step-5-Preview", 65536],
|
|
754
|
+
["thinkingmachines/inkling", 65536],
|
|
755
|
+
["xai/grok-4.5", 5e5],
|
|
756
|
+
["xai/grok-4.6", 5e5],
|
|
757
|
+
["xai/grok-4.7", 5e5],
|
|
758
|
+
["xiaomi/mimo-v2.5", 131072],
|
|
759
|
+
["xiaomi/mimo-v2.5-pro", 131072],
|
|
760
|
+
["xiaomi/mimo-v2.6-flash", 131072],
|
|
761
|
+
["xiaomi/mimo-v2.6-pro", 131072],
|
|
762
|
+
["xiaomi/mimo-v2.6-pro-ultraspeed", 131072],
|
|
763
|
+
["z-ai/glm-5.3-flash", 131072],
|
|
764
|
+
["z-ai/glm-5.3-flashx", 131072],
|
|
765
|
+
["zai-org/GLM-5", 131072],
|
|
766
|
+
["zai-org/GLM-5.1", 131072],
|
|
767
|
+
["zai-org/GLM-5.2", 131072],
|
|
768
|
+
["zai-org/GLM-5.3", 131072]
|
|
769
|
+
]);
|
|
770
|
+
/**
|
|
771
|
+
* Output ceiling for a Messages-route model that {@link MODEL_OUTPUT_TOKEN_LIMITS}
|
|
772
|
+
* does not carry — today only models no vendor publishes yet. Deliberately
|
|
773
|
+
* conservative: a 400 on `max_tokens` is a wasted round trip on every request,
|
|
774
|
+
* whereas a lower ceiling only shortens an answer, and the endpoint states the
|
|
775
|
+
* real one the first time it refuses, at which point the runtime overrides this
|
|
776
|
+
* anyway (issue #71).
|
|
777
|
+
*
|
|
778
|
+
* 64 000 is not arbitrary: it is `claude-haiku-4-5-20251001`'s actual ceiling,
|
|
779
|
+
* the lowest in the Claude family. A Claude model added to the catalog before
|
|
780
|
+
* its row exists here therefore answers at a size the family really supports,
|
|
781
|
+
* and is corrected upward on its first rejection rather than clamped here
|
|
782
|
+
* forever.
|
|
783
|
+
*/
|
|
784
|
+
const DEFAULT_MESSAGES_MAX_TOKENS = 64e3;
|
|
785
|
+
/**
|
|
639
786
|
* The minimum subscription plan a model is included in, per the official plan
|
|
640
787
|
* pages (`/docs/plans/go`, `/docs/plans/goat`, `/docs/plans/pro`,
|
|
641
788
|
* `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
|
|
642
789
|
* list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
|
|
643
790
|
* Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
|
|
644
|
-
* Provider-tier. Re-verified at command-code@1.
|
|
791
|
+
* Provider-tier. Re-verified at command-code@1.72.1 (2026-09-30): 86 catalog
|
|
645
792
|
* ids at 53/62/76/84 cumulative, a strict superset chain — every release since
|
|
646
793
|
* 1.49.0 has been additive with no tier move, and per-entry tags below name the
|
|
647
794
|
* release that added each row.
|
|
@@ -678,6 +825,7 @@ const KNOWN_PLANS = {
|
|
|
678
825
|
"gpt-6-luna": "go",
|
|
679
826
|
"meituan/LongCat-2.0": "go",
|
|
680
827
|
"inclusionai/ling-3.0-flash-sante:free": "go",
|
|
828
|
+
"inclusionai/ling-3.1-flash:free": "go",
|
|
681
829
|
"meta/muse-spark-1.2-contributor": "go",
|
|
682
830
|
"meta/muse-spark-1.3-contributor": "go",
|
|
683
831
|
"moonshotai/Kimi-K2.5": "go",
|
|
@@ -738,7 +886,8 @@ const KNOWN_PLANS = {
|
|
|
738
886
|
"claude-opus-5-5": "provider",
|
|
739
887
|
"claude-opus-5": "provider",
|
|
740
888
|
"gpt-6-astra": "provider",
|
|
741
|
-
"sakana/fugu-ultra": "provider"
|
|
889
|
+
"sakana/fugu-ultra": "provider",
|
|
890
|
+
"gpt-6.1-sol": "max"
|
|
742
891
|
};
|
|
743
892
|
/**
|
|
744
893
|
* Whether a model is free (requests cost no credits), per the pricing page's
|
|
@@ -767,7 +916,7 @@ function compareByPlan(a, b) {
|
|
|
767
916
|
/**
|
|
768
917
|
* Subscription plan table, synced from the official CLI bundle's plan maps
|
|
769
918
|
* (located by the `"individual-go"` key in `dist/cli.mjs`, re-verified unchanged
|
|
770
|
-
* through command-code@1.
|
|
919
|
+
* through command-code@1.72.1): subscription `planId` prefix → display name and
|
|
771
920
|
* the plan's monthly credit total. This is the account's own subscription
|
|
772
921
|
* (from `/alpha/billing/subscriptions`) — distinct from {@link KNOWN_PLANS},
|
|
773
922
|
* which maps catalog models to their minimum tier.
|
|
@@ -782,6 +931,11 @@ const KNOWN_SUBSCRIPTION_PLANS = {
|
|
|
782
931
|
monthlyCredits: 10,
|
|
783
932
|
tierWeight: 0
|
|
784
933
|
},
|
|
934
|
+
"individual-go-v1": {
|
|
935
|
+
name: "Go",
|
|
936
|
+
monthlyCredits: 10,
|
|
937
|
+
tierWeight: 0
|
|
938
|
+
},
|
|
785
939
|
"individual-goat": {
|
|
786
940
|
name: "GOAT",
|
|
787
941
|
monthlyCredits: 70,
|
|
@@ -870,10 +1024,6 @@ const KNOWN_DEALS = {
|
|
|
870
1024
|
"MiniMaxAI/MiniMax-M3": { label: "50% off" },
|
|
871
1025
|
"xiaomi/mimo-v2.5-pro": { label: "99% off" },
|
|
872
1026
|
"xiaomi/mimo-v2.5": { label: "98% off" },
|
|
873
|
-
"xai/grok-4.7": {
|
|
874
|
-
label: "40% off",
|
|
875
|
-
expiresAt: "2026-09-27T23:59:59.999Z"
|
|
876
|
-
},
|
|
877
1027
|
"poolside/laguna-s-2.1-free": {
|
|
878
1028
|
label: "FREE",
|
|
879
1029
|
free: true
|
|
@@ -882,6 +1032,10 @@ const KNOWN_DEALS = {
|
|
|
882
1032
|
label: "FREE",
|
|
883
1033
|
free: true
|
|
884
1034
|
},
|
|
1035
|
+
"inclusionai/ling-3.1-flash:free": {
|
|
1036
|
+
label: "FREE",
|
|
1037
|
+
free: true
|
|
1038
|
+
},
|
|
885
1039
|
"stealth/space-bunny-alpha": {
|
|
886
1040
|
label: "FREE",
|
|
887
1041
|
free: true
|
|
@@ -1629,6 +1783,33 @@ const MODEL_PRICE_ROWS = [
|
|
|
1629
1783
|
25
|
|
1630
1784
|
] }]
|
|
1631
1785
|
},
|
|
1786
|
+
{
|
|
1787
|
+
id: "gpt-6.1-sol",
|
|
1788
|
+
rates: [
|
|
1789
|
+
2,
|
|
1790
|
+
10,
|
|
1791
|
+
.1,
|
|
1792
|
+
2.5
|
|
1793
|
+
],
|
|
1794
|
+
allowance: {
|
|
1795
|
+
goat: 20,
|
|
1796
|
+
pro: 20
|
|
1797
|
+
},
|
|
1798
|
+
contextTiers: [{
|
|
1799
|
+
"maxContext": 272e3,
|
|
1800
|
+
"rates": [
|
|
1801
|
+
2,
|
|
1802
|
+
10,
|
|
1803
|
+
.1,
|
|
1804
|
+
2.5
|
|
1805
|
+
]
|
|
1806
|
+
}, { "rates": [
|
|
1807
|
+
4,
|
|
1808
|
+
15,
|
|
1809
|
+
.2,
|
|
1810
|
+
5
|
|
1811
|
+
] }]
|
|
1812
|
+
},
|
|
1632
1813
|
{
|
|
1633
1814
|
id: "gpt-6-luna",
|
|
1634
1815
|
rates: [
|
|
@@ -1722,11 +1903,6 @@ const MODEL_PRICE_ROWS = [
|
|
|
1722
1903
|
{
|
|
1723
1904
|
id: "grok-4.7",
|
|
1724
1905
|
rates: [
|
|
1725
|
-
1.2,
|
|
1726
|
-
3.6,
|
|
1727
|
-
.3
|
|
1728
|
-
],
|
|
1729
|
-
listRates: [
|
|
1730
1906
|
2,
|
|
1731
1907
|
6,
|
|
1732
1908
|
.5
|
|
@@ -1738,27 +1914,15 @@ const MODEL_PRICE_ROWS = [
|
|
|
1738
1914
|
contextTiers: [{
|
|
1739
1915
|
"maxContext": 2e5,
|
|
1740
1916
|
"rates": [
|
|
1741
|
-
1.2,
|
|
1742
|
-
3.6,
|
|
1743
|
-
.3
|
|
1744
|
-
],
|
|
1745
|
-
"listRates": [
|
|
1746
1917
|
2,
|
|
1747
1918
|
6,
|
|
1748
1919
|
.5
|
|
1749
1920
|
]
|
|
1750
|
-
}, {
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
],
|
|
1756
|
-
"listRates": [
|
|
1757
|
-
4,
|
|
1758
|
-
12,
|
|
1759
|
-
1
|
|
1760
|
-
]
|
|
1761
|
-
}]
|
|
1921
|
+
}, { "rates": [
|
|
1922
|
+
4,
|
|
1923
|
+
12,
|
|
1924
|
+
1
|
|
1925
|
+
] }]
|
|
1762
1926
|
},
|
|
1763
1927
|
{
|
|
1764
1928
|
id: "inkling",
|
|
@@ -2596,6 +2760,27 @@ function installCostProjection(ctx) {
|
|
|
2596
2760
|
* JSON, SSE, and pre-stream error bodies undecoded.
|
|
2597
2761
|
*/
|
|
2598
2762
|
const IDENTITY_ENCODING_HEADER = { "accept-encoding": "identity" };
|
|
2763
|
+
const THROTTLED_CODE = "THROTTLED";
|
|
2764
|
+
const boundedCodes = /* @__PURE__ */ new Set([
|
|
2765
|
+
"EMPTY_RESPONSE",
|
|
2766
|
+
"SERVER",
|
|
2767
|
+
"TIMEOUT",
|
|
2768
|
+
THROTTLED_CODE
|
|
2769
|
+
]);
|
|
2770
|
+
const counts$1 = /* @__PURE__ */ new WeakMap();
|
|
2771
|
+
function absorbTransientFailure(agent, code) {
|
|
2772
|
+
if (!boundedCodes.has(code)) return "ignored";
|
|
2773
|
+
const used = counts$1.get(agent) ?? 0;
|
|
2774
|
+
if (used >= 3) return "exhausted";
|
|
2775
|
+
counts$1.set(agent, used + 1);
|
|
2776
|
+
return "retry";
|
|
2777
|
+
}
|
|
2778
|
+
function resetTransientFailures(agent) {
|
|
2779
|
+
counts$1.delete(agent);
|
|
2780
|
+
}
|
|
2781
|
+
function transientBudgetMessage(code, message) {
|
|
2782
|
+
return `Command Code 请求在 3 次自动重试后仍失败,已停止重试(${code})。请稍后重新发送;若反复出现,请检查服务状态或请求耗时诊断。 原因:${message}`;
|
|
2783
|
+
}
|
|
2599
2784
|
//#endregion
|
|
2600
2785
|
//#region src/accounts.ts
|
|
2601
2786
|
/**
|
|
@@ -2973,7 +3158,7 @@ var CommandCodeAccountPool = class {
|
|
|
2973
3158
|
function allAccountsUnusable(accounts) {
|
|
2974
3159
|
if (accounts.filter((account) => account.state?.kind === "disabled").length === accounts.length) return new LlmError(`llm-commandcode: every configured Command Code account (${accounts.length}) was rejected with 401 — check the stored API keys (Models page / settings) or the auth file;已配置的 ${accounts.length} 个 Command Code 账户密钥均被拒绝(401)——请在设置页检查存储的 API 密钥,或重新运行 command-code login`, "INVALID_CREDENTIAL");
|
|
2975
3160
|
const windowMarked = accounts.map((account) => account.state).filter((state) => state !== void 0).filter((state) => state.cause === "window");
|
|
2976
|
-
if (windowMarked.length === 0) return new LlmError(`llm-commandcode: all ${accounts.length} Command Code account(s) are rate limited (429) — the provider did not report an exhausted usage window; retrying;全部 ${accounts.length} 个 Command Code 账户被限流(429)——服务商未报告用量窗口用尽,正在重试`,
|
|
3161
|
+
if (windowMarked.length === 0) return new LlmError(`llm-commandcode: all ${accounts.length} Command Code account(s) are rate limited (429) — the provider did not report an exhausted usage window; retrying;全部 ${accounts.length} 个 Command Code 账户被限流(429)——服务商未报告用量窗口用尽,正在重试`, THROTTLED_CODE);
|
|
2977
3162
|
const resets = windowMarked.filter((state) => state.kind === "cooldown" && state.until > 0).map((state) => state.until);
|
|
2978
3163
|
const earliest = resets.length > 0 ? Math.min(...resets) : 0;
|
|
2979
3164
|
const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
|
|
@@ -3394,6 +3579,95 @@ function openStreamTrace(scope, options = {}) {
|
|
|
3394
3579
|
};
|
|
3395
3580
|
}
|
|
3396
3581
|
//#endregion
|
|
3582
|
+
//#region src/request-timing.ts
|
|
3583
|
+
/** 请求耗时只记录阶段、计数和状态;不记录消息、凭据、请求头或错误正文。 */
|
|
3584
|
+
const REQUEST_TIMING_ENV = "DSH_COMMANDCODE_TIMING";
|
|
3585
|
+
/** 每次生成独立计时;可注入时钟验证顺序,不依赖真实等待。 */
|
|
3586
|
+
var RequestTiming = class {
|
|
3587
|
+
sink;
|
|
3588
|
+
now;
|
|
3589
|
+
started;
|
|
3590
|
+
summary;
|
|
3591
|
+
constructor(model, sink, now = () => performance.now()) {
|
|
3592
|
+
this.sink = sink;
|
|
3593
|
+
this.now = now;
|
|
3594
|
+
this.started = now();
|
|
3595
|
+
this.summary = {
|
|
3596
|
+
version: 1,
|
|
3597
|
+
startedAt: Date.now(),
|
|
3598
|
+
model,
|
|
3599
|
+
outcome: "cancelled",
|
|
3600
|
+
totalMs: 0,
|
|
3601
|
+
phasesMs: {
|
|
3602
|
+
credentials: 0,
|
|
3603
|
+
images: 0,
|
|
3604
|
+
body: 0,
|
|
3605
|
+
serialization: 0,
|
|
3606
|
+
headers: 0
|
|
3607
|
+
},
|
|
3608
|
+
attempts: []
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
3611
|
+
/** 返回结束函数,确保成功和异常都计入同一阶段。阶段可嵌套,不能直接相加。 */
|
|
3612
|
+
phase(name) {
|
|
3613
|
+
if (!this.sink) return () => {};
|
|
3614
|
+
const start = this.now();
|
|
3615
|
+
return () => {
|
|
3616
|
+
this.summary.phasesMs[name] += this.now() - start;
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
attempt(protocol, bodyBytes) {
|
|
3620
|
+
if (!this.sink) return () => {};
|
|
3621
|
+
const start = this.now();
|
|
3622
|
+
const attempt = {
|
|
3623
|
+
protocol,
|
|
3624
|
+
bodyBytes,
|
|
3625
|
+
headersMs: 0
|
|
3626
|
+
};
|
|
3627
|
+
this.summary.attempts.push(attempt);
|
|
3628
|
+
return (status) => {
|
|
3629
|
+
Object.assign(attempt, {
|
|
3630
|
+
headersMs: this.now() - start,
|
|
3631
|
+
...status === void 0 ? {} : { status }
|
|
3632
|
+
});
|
|
3633
|
+
this.summary.phasesMs.headers += attempt.headersMs;
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
first(kind) {
|
|
3637
|
+
if (this.sink && this.summary[kind] === void 0) this.summary[kind] = this.now() - this.started;
|
|
3638
|
+
}
|
|
3639
|
+
async close(outcome, errorCode) {
|
|
3640
|
+
if (!this.sink) return;
|
|
3641
|
+
this.summary.outcome = outcome;
|
|
3642
|
+
this.summary.totalMs = this.now() - this.started;
|
|
3643
|
+
if (errorCode !== void 0) this.summary.errorCode = errorCode;
|
|
3644
|
+
try {
|
|
3645
|
+
await this.sink(this.summary);
|
|
3646
|
+
} catch {}
|
|
3647
|
+
}
|
|
3648
|
+
};
|
|
3649
|
+
/** 与原始流诊断独立开关,便于仅收集数值耗时。 */
|
|
3650
|
+
function openRequestTiming(model, sink) {
|
|
3651
|
+
if (sink) return new RequestTiming(model, sink);
|
|
3652
|
+
const raw = process.env[REQUEST_TIMING_ENV]?.trim();
|
|
3653
|
+
if (!raw || [
|
|
3654
|
+
"0",
|
|
3655
|
+
"false",
|
|
3656
|
+
"no",
|
|
3657
|
+
"off"
|
|
3658
|
+
].includes(raw.toLowerCase())) return new RequestTiming(model);
|
|
3659
|
+
const path = [
|
|
3660
|
+
"1",
|
|
3661
|
+
"true",
|
|
3662
|
+
"yes",
|
|
3663
|
+
"on"
|
|
3664
|
+
].includes(raw.toLowerCase()) ? join(tmpdir(), "dsh-commandcode-timing.jsonl") : raw;
|
|
3665
|
+
return new RequestTiming(model, async (summary) => {
|
|
3666
|
+
await mkdir(dirname(path), { recursive: true });
|
|
3667
|
+
await appendFile(path, `${JSON.stringify(summary)}\n`, { mode: 384 });
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
//#endregion
|
|
3397
3671
|
//#region src/adapter.ts
|
|
3398
3672
|
/**
|
|
3399
3673
|
* DeepSeek Harness LLM adapter for the Command Code Provider API.
|
|
@@ -3402,21 +3676,113 @@ function openStreamTrace(scope, options = {}) {
|
|
|
3402
3676
|
* community-maintained integration; you need your own Command Code account
|
|
3403
3677
|
* and API key or subscription, and Command Code's terms apply.
|
|
3404
3678
|
*
|
|
3405
|
-
* Owns the
|
|
3679
|
+
* Owns the three chat transports (`/alpha/generate`, `/provider/v1/chat/completions`,
|
|
3680
|
+
* `/provider/v1/messages`),
|
|
3406
3681
|
* message conversion, SSE/JSONL parsing, the catalog + its on-disk cache, and
|
|
3407
3682
|
* the pre-stream account rotation loop. The wire protocol is
|
|
3408
|
-
* reverse-engineered (command-code@1.28.4, re-verified through 1.
|
|
3409
|
-
*
|
|
3683
|
+
* reverse-engineered (command-code@1.28.4, re-verified through 1.72.1);
|
|
3684
|
+
* docs/agent-reference/adapter-protocol.md holds the full protocol record.
|
|
3410
3685
|
*
|
|
3411
3686
|
* The adapter is deliberately free of cordis/schemastery: it receives a
|
|
3412
3687
|
* per-request options thunk and an API-key resolver from the plugin entry
|
|
3413
3688
|
* (src/index.ts), so a settings change reaches the very next request.
|
|
3414
3689
|
*/
|
|
3690
|
+
/**
|
|
3691
|
+
* Output ceilings the endpoint has named in a live rejection, per model.
|
|
3692
|
+
*
|
|
3693
|
+
* The snapshot in ./capabilities.ts carries the vendor-published ceiling for
|
|
3694
|
+
* 74 of the 84 catalog models (see scripts/sync-output-limits.mjs). The other
|
|
3695
|
+
* 10 are models no vendor publishes yet, and a gateway is free to sit below
|
|
3696
|
+
* the vendor figure even where one exists — so this map is the correction
|
|
3697
|
+
* channel, not a fallback of last resort: the endpoint states the model's own
|
|
3698
|
+
* ceiling in its rejection, so one wasted round trip is enough to know it for
|
|
3699
|
+
* the rest of the process.
|
|
3700
|
+
*
|
|
3701
|
+
* Shared by adapters for the same gateway, but not across apiBase values: a
|
|
3702
|
+
* proxy can enforce a smaller ceiling for the same model id. Only ever tightens
|
|
3703
|
+
* within that gateway, so a stale catalog row cannot undo a live refusal.
|
|
3704
|
+
*/
|
|
3705
|
+
const learnedOutputLimits = /* @__PURE__ */ new Map();
|
|
3706
|
+
/**
|
|
3707
|
+
* Output ceilings the CATALOG published, per model.
|
|
3708
|
+
*
|
|
3709
|
+
* `/provider/v1/models` carries `max_output_tokens` in its schema and the
|
|
3710
|
+
* official pi provider already prefers it, but the endpoint does not send it
|
|
3711
|
+
* yet — so this is empty today and fills in on its own the day it does. It sits
|
|
3712
|
+
* above the models.dev snapshot and below what a live rejection proved, because
|
|
3713
|
+
* that ordering is also the order of trust: the gateway knows its own ceiling,
|
|
3714
|
+
* a rejection proves it, and models.dev is a third party describing the same
|
|
3715
|
+
* model. Restored from the on-disk catalog cache so a published ceiling
|
|
3716
|
+
* survives a restart without waiting for the next catalog fetch.
|
|
3717
|
+
*/
|
|
3718
|
+
const publishedOutputLimits = /* @__PURE__ */ new Map();
|
|
3719
|
+
/** Keep model facts from one gateway from constraining another gateway. */
|
|
3720
|
+
function outputLimitsFor(store, apiBase) {
|
|
3721
|
+
let limits = store.get(apiBase);
|
|
3722
|
+
if (limits === void 0) {
|
|
3723
|
+
limits = /* @__PURE__ */ new Map();
|
|
3724
|
+
store.set(apiBase, limits);
|
|
3725
|
+
}
|
|
3726
|
+
return limits;
|
|
3727
|
+
}
|
|
3728
|
+
/**
|
|
3729
|
+
* The output ceiling this gateway route sends for `modelId`.
|
|
3730
|
+
*
|
|
3731
|
+
* `/provider/v1/models` publishes `context_length` and `supported_endpoints`
|
|
3732
|
+
* but no output cap today, while each endpoint refuses a larger `max_tokens` per
|
|
3733
|
+
* model — the Messages endpoint names its own in the rejection (`max_tokens:
|
|
3734
|
+
* 200000 > 128000, which is the maximum allowed number of output tokens for
|
|
3735
|
+
* claude-sonnet-5-5`). What the endpoint has already named wins, then the
|
|
3736
|
+
* catalog's own figure once it publishes one, then the snapshot, then — for
|
|
3737
|
+
* the Messages-route models only — the conservative value, because the ceilings
|
|
3738
|
+
* actually observed are below the global default and assuming otherwise would
|
|
3739
|
+
* fail every request.
|
|
3740
|
+
*/
|
|
3741
|
+
function modelOutputTokenLimit(apiBase, modelId) {
|
|
3742
|
+
const learned = outputLimitsFor(learnedOutputLimits, apiBase).get(modelId);
|
|
3743
|
+
const known = outputLimitsFor(publishedOutputLimits, apiBase).get(modelId) ?? MODEL_OUTPUT_TOKEN_LIMITS.get(modelId);
|
|
3744
|
+
if (learned !== void 0 && (known === void 0 || learned < known)) return learned;
|
|
3745
|
+
if (known !== void 0) return known;
|
|
3746
|
+
return requiresMessagesEndpoint(modelId) ? DEFAULT_MESSAGES_MAX_TOKENS : DEFAULT_MAX_OUTPUT_TOKENS;
|
|
3747
|
+
}
|
|
3748
|
+
/**
|
|
3749
|
+
* Record a ceiling the endpoint just stated, if it is one this route can act
|
|
3750
|
+
* on: a real number, and one below what the next request would otherwise send.
|
|
3751
|
+
* A ceiling at or above the current budget describes a request that was not
|
|
3752
|
+
* refused for being too large, and acting on it would be a guess.
|
|
3753
|
+
*/
|
|
3754
|
+
function noteOutputCeiling(apiBase, modelId, ceiling) {
|
|
3755
|
+
if (!Number.isSafeInteger(ceiling) || ceiling <= 0) return false;
|
|
3756
|
+
if (ceiling >= modelOutputTokenLimit(apiBase, modelId)) return false;
|
|
3757
|
+
outputLimitsFor(learnedOutputLimits, apiBase).set(modelId, ceiling);
|
|
3758
|
+
return true;
|
|
3759
|
+
}
|
|
3760
|
+
/**
|
|
3761
|
+
* The ceiling to retry this turn with, when `error` is a request-side output
|
|
3762
|
+
* refusal the route can act on — else undefined, meaning surface it.
|
|
3763
|
+
*
|
|
3764
|
+
* Read from the message, so it covers both shapes the same refusal arrives in:
|
|
3765
|
+
* an HTTP 400 (`PROVIDER_HTTP_ERROR`) and an in-stream error frame
|
|
3766
|
+
* (`PROVIDER_STREAM_ERROR`). Both carry the endpoint's wording, and both are
|
|
3767
|
+
* already classified as output refusals rather than context overflows by the
|
|
3768
|
+
* time they reach here.
|
|
3769
|
+
*
|
|
3770
|
+
* Recording is what makes the retry pay off: the ceiling is kept for this
|
|
3771
|
+
* gateway and model, so a refused turn leaves its next request correct.
|
|
3772
|
+
*/
|
|
3773
|
+
function outputCeilingRefusal(apiBase, modelId, error) {
|
|
3774
|
+
if (!(error instanceof LlmError)) return void 0;
|
|
3775
|
+
const detail = error.message;
|
|
3776
|
+
if (!OUTPUT_LIMIT_REJECTION.test(detail)) return void 0;
|
|
3777
|
+
const stated = parseOutputCeiling(detail);
|
|
3778
|
+
if (stated === void 0) return void 0;
|
|
3779
|
+
return noteOutputCeiling(apiBase, modelId, stated) ? stated : void 0;
|
|
3780
|
+
}
|
|
3415
3781
|
/** Keep the two user-facing languages and error metadata together. */
|
|
3416
3782
|
function bilingual(code, en, zh, options) {
|
|
3417
3783
|
return new LlmError(`${en};${zh}`, code, options);
|
|
3418
3784
|
}
|
|
3419
|
-
const COMMAND_CODE_CLI_VERSION = "1.
|
|
3785
|
+
const COMMAND_CODE_CLI_VERSION = "1.72.1";
|
|
3420
3786
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
3421
3787
|
/**
|
|
3422
3788
|
* The output budget this bundle asks for, and the ceiling it will never exceed.
|
|
@@ -3452,6 +3818,9 @@ const DEFAULT_MAX_OUTPUT_TOKENS = 131072;
|
|
|
3452
3818
|
/** Preserve a small usable answer budget when the heuristic overshoots. */
|
|
3453
3819
|
const MIN_CONTEXT_OUTPUT_TOKENS = 1024;
|
|
3454
3820
|
const MODELS_TIMEOUT_MS = 1e4;
|
|
3821
|
+
/** 目录短期复用;刷新失败只短暂退避,避免反复阻塞模型选择器。 */
|
|
3822
|
+
const MODEL_CATALOG_TTL_MS = 3e5;
|
|
3823
|
+
const MODEL_CATALOG_RETRY_MS = 1e4;
|
|
3455
3824
|
/** How long the picker's plan-filter billing facts stay cached before refetching. */
|
|
3456
3825
|
const BILLING_ACCESS_TTL_MS = 3e5;
|
|
3457
3826
|
/** The entry-plan tier weight (`individual-go` in KNOWN_SUBSCRIPTION_PLANS). */
|
|
@@ -3489,7 +3858,49 @@ const ACTIVE_SUBSCRIPTION_STATUSES = /* @__PURE__ */ new Set([
|
|
|
3489
3858
|
const DEFAULT_REQUEST_TIMEOUT_MS = 3e5;
|
|
3490
3859
|
/** Stream idle timeout: a generation that stalls this long is a dead connection. */
|
|
3491
3860
|
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
3492
|
-
|
|
3861
|
+
/** Version 3 binds cached catalog facts to their gateway; older files lack a safe source. */
|
|
3862
|
+
const MODEL_CACHE_VERSION = 3;
|
|
3863
|
+
/**
|
|
3864
|
+
* A lone UTF-16 surrogate half: a high surrogate not followed by a low one, or
|
|
3865
|
+
* a low surrogate not preceded by a high one. Such a half is not a character —
|
|
3866
|
+
* it only arises from a truncated slice, a lossy decode, or a lone `\uD83D`
|
|
3867
|
+
* escape — and it survives `JSON.stringify` as an escape (`"\ud83d"`) rather
|
|
3868
|
+
* than failing locally, so the request leaves the process carrying a string no
|
|
3869
|
+
* UTF-8 consumer can round-trip. Providers running strict decoders reject it.
|
|
3870
|
+
*
|
|
3871
|
+
* This is the same expression `@earendil-works/pi-ai` applies to every outgoing
|
|
3872
|
+
* string on this endpoint. Valid astral characters (emoji and anything else
|
|
3873
|
+
* outside the BMP) are PAIRED surrogates and pass through untouched.
|
|
3874
|
+
*/
|
|
3875
|
+
const LONE_SURROGATE_PATTERN = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
3876
|
+
/**
|
|
3877
|
+
* A cheap pre-test for "this string contains any surrogate code unit at all",
|
|
3878
|
+
* paired or not. The exact pattern above needs lookaround, which V8 cannot
|
|
3879
|
+
* optimize into a fast scan; this one is a plain range test. Real prompts are
|
|
3880
|
+
* overwhelmingly ASCII-plus-CJK with no surrogates, so the common case pays one
|
|
3881
|
+
* linear scan instead of the lookaround pass. Measured on a 412 KB request
|
|
3882
|
+
* body: 1.40 ms with the exact pattern alone against 0.77 ms with this gate,
|
|
3883
|
+
* where a bare `JSON.stringify` is 0.59 ms.
|
|
3884
|
+
*/
|
|
3885
|
+
const ANY_SURROGATE_PATTERN = /[\uD800-\uDFFF]/;
|
|
3886
|
+
/**
|
|
3887
|
+
* Drop lone surrogate halves from one outgoing string.
|
|
3888
|
+
*
|
|
3889
|
+
* Applied at the single point a request body is serialized, so every field of
|
|
3890
|
+
* every transport is covered — system text, message content, tool schemas and
|
|
3891
|
+
* tool results alike — instead of enumerating the fields that can carry text
|
|
3892
|
+
* and missing one. The value is returned unchanged whenever it holds no
|
|
3893
|
+
* surrogate at all, which is the ordinary case.
|
|
3894
|
+
*
|
|
3895
|
+
* The `test()` call deliberately uses the flagless {@link ANY_SURROGATE_PATTERN}:
|
|
3896
|
+
* a global regex is stateful, so testing with one would advance `lastIndex` and
|
|
3897
|
+
* make consecutive calls on the same string alternate between true and false.
|
|
3898
|
+
* `replace()` resets `lastIndex` itself, so the global pattern is safe there.
|
|
3899
|
+
*/
|
|
3900
|
+
function sanitizeSurrogates(value) {
|
|
3901
|
+
if (typeof value !== "string") return value;
|
|
3902
|
+
return ANY_SURROGATE_PATTERN.test(value) ? value.replace(LONE_SURROGATE_PATTERN, "") : value;
|
|
3903
|
+
}
|
|
3493
3904
|
/** Hash one request value without retaining or logging its contents. */
|
|
3494
3905
|
function fingerprintValue(value) {
|
|
3495
3906
|
let serialized;
|
|
@@ -3665,6 +4076,35 @@ function isContextOverflowDetail(detail) {
|
|
|
3665
4076
|
/** A gateway refusal observed for an over-reserved request, but not unique to that cause. */
|
|
3666
4077
|
const AMBIGUOUS_SIZE_REJECTION = /\bmodel could not complete the request\b/i;
|
|
3667
4078
|
/**
|
|
4079
|
+
* A request-side `max_tokens` refusal, told apart from a context overflow that
|
|
4080
|
+
* mentions the same field. Kept narrow on purpose: the overflow pattern also
|
|
4081
|
+
* matches a bare `max_tokens`, so this must name the specific shapes the
|
|
4082
|
+
* endpoint uses — a ceiling comparison, its prose about output tokens, or a
|
|
4083
|
+
* request-validation error on the field.
|
|
4084
|
+
*/
|
|
4085
|
+
const OUTPUT_LIMIT_REJECTION = new RegExp([
|
|
4086
|
+
"max_tokens:\\s*\\d+\\s*>\\s*\\d+",
|
|
4087
|
+
"maximum allowed number of output tokens",
|
|
4088
|
+
"invalid input[^\\n]*max_tokens",
|
|
4089
|
+
"expected number[^\\n]*at max_tokens"
|
|
4090
|
+
].join("|"), "i");
|
|
4091
|
+
/**
|
|
4092
|
+
* The output ceiling the endpoint states in a `max_tokens` refusal, read off
|
|
4093
|
+
* its own comparison: `max_tokens: 131072 > 128000` yields 128000.
|
|
4094
|
+
*
|
|
4095
|
+
* Only the comparison form is parsed. The other shapes
|
|
4096
|
+
* {@link OUTPUT_LIMIT_REJECTION} matches — a bare validation error, or the
|
|
4097
|
+
* prose without the numbers — name no ceiling, and guessing one would be worse
|
|
4098
|
+
* than failing: a fabricated value is indistinguishable from a real one at the
|
|
4099
|
+
* point where it decides the next request's budget.
|
|
4100
|
+
*/
|
|
4101
|
+
function parseOutputCeiling(detail) {
|
|
4102
|
+
const stated = /max_tokens:\s*\d+\s*>\s*(\d+)/i.exec(detail)?.[1];
|
|
4103
|
+
if (stated === void 0) return void 0;
|
|
4104
|
+
const ceiling = Number(stated);
|
|
4105
|
+
return Number.isSafeInteger(ceiling) && ceiling > 0 ? ceiling : void 0;
|
|
4106
|
+
}
|
|
4107
|
+
/**
|
|
3668
4108
|
* The advice floor for a Provider-API timeout: doubling a 60 s budget is still
|
|
3669
4109
|
* short for a slow upstream first token, so the suggestion never goes below 5
|
|
3670
4110
|
* minutes. Capped by `RETRY_MAX_DELAY_MS` rather than the schema's timer
|
|
@@ -3711,6 +4151,8 @@ function headersTimeoutAdvice(protocol, timeoutMs) {
|
|
|
3711
4151
|
* answer either way.
|
|
3712
4152
|
*/
|
|
3713
4153
|
const MAX_CONSECUTIVE_HEADER_TIMEOUTS = 3;
|
|
4154
|
+
/** Streak key for a session-less call, so those keep the old single-slot semantics instead of never escalating. */
|
|
4155
|
+
const GLOBAL_HEADER_TIMEOUT_KEY = "\0global";
|
|
3714
4156
|
/** UTF-8 byte length, computed only on the timeout path so a large body is encoded once, not per attempt. */
|
|
3715
4157
|
function utf8ByteLength(text) {
|
|
3716
4158
|
return new TextEncoder().encode(text).length;
|
|
@@ -3777,9 +4219,21 @@ function streamErrorToLlmError(value, fallbackMessage) {
|
|
|
3777
4219
|
message
|
|
3778
4220
|
].filter((part) => part !== void 0 && part !== "").join(" ");
|
|
3779
4221
|
const statusOption = statusCode !== void 0 ? { status: statusCode } : void 0;
|
|
4222
|
+
if (OUTPUT_LIMIT_REJECTION.test(detail)) {
|
|
4223
|
+
const stated = /max_tokens:\s*\d+\s*>\s*\d+/i.exec(detail)?.[0];
|
|
4224
|
+
return bilingual("PROVIDER_STREAM_ERROR", `Command Code refused the request's max_tokens for this model` + (stated !== void 0 ? ` (${stated})` : "") + " — the session is not oversized, so compacting it will not help; lower \"Max output tokens\" or pick a model with a larger output ceiling", `Command Code 拒绝了本次请求的 max_tokens(该模型的输出上限更低)` + (stated !== void 0 ? `(${stated})` : "") + "——会话内容并未超长,压缩上下文无效;请调低「最大输出 token」或改用输出上限更大的模型", statusOption);
|
|
4225
|
+
}
|
|
3780
4226
|
if (isContextOverflowDetail(detail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, `Command Code stream error: ${message}`, "Command Code API 拒绝了这次请求:内容超出模型上下文窗口。正在压缩上下文后重试;如仍失败,请新建会话或减少上下文", statusOption);
|
|
3781
4227
|
const terminal = hasTerminalStreamMarker(detail);
|
|
3782
4228
|
if (!(isRetryable === true || (isRetryable === false || terminal ? false : statusCode !== void 0 ? statusCode !== void 0 && (statusCode === 429 || statusCode >= 500) : true))) return new LlmError(`Command Code stream error: ${message}`, "PROVIDER_STREAM_ERROR", statusOption);
|
|
4229
|
+
const rejection = classifyAccountRejection(statusCode ?? 0, JSON.stringify(value) ?? message);
|
|
4230
|
+
if (rejection?.reason === "rate-limit" || rejection?.reason === "throttled") {
|
|
4231
|
+
const wait = rejection.resetAtMs === void 0 ? 0 : Math.min(RETRY_MAX_DELAY_MS, Math.max(1e3, rejection.resetAtMs - Date.now()));
|
|
4232
|
+
return bilingual(rejection.reason === "rate-limit" ? "RATE_LIMIT" : THROTTLED_CODE, `Command Code stream error: ${message}`, rejection.reason === "rate-limit" ? "Command Code 用量窗口已限流,等待恢复" : "Command Code 请求暂时被限流,正在重试", {
|
|
4233
|
+
...statusOption,
|
|
4234
|
+
...wait > 0 ? { providerRetryAfterMs: wait } : {}
|
|
4235
|
+
});
|
|
4236
|
+
}
|
|
3783
4237
|
return new LlmError(`Command Code stream error: ${message}`, "SERVER", statusOption);
|
|
3784
4238
|
}
|
|
3785
4239
|
function recordOrEmpty(value) {
|
|
@@ -3944,8 +4398,9 @@ function resolveAuthFileApiKey() {
|
|
|
3944
4398
|
return apiKeyFromCredentialRecord(parsed.commandcode) ?? apiKeyFromCredentialRecord(parsed["command-code"]);
|
|
3945
4399
|
} catch {}
|
|
3946
4400
|
}
|
|
3947
|
-
function parseCatalogResponse(value) {
|
|
4401
|
+
function parseCatalogResponse(value, apiBase) {
|
|
3948
4402
|
if (!isRecord(value) || value.object !== "list" || !Array.isArray(value.data)) throw new LlmError("Unexpected Command Code models response shape", "PROVIDER_PROTOCOL_ERROR");
|
|
4403
|
+
publishedOutputLimits.set(apiBase, /* @__PURE__ */ new Map());
|
|
3949
4404
|
const models = [];
|
|
3950
4405
|
for (const entry of value.data) {
|
|
3951
4406
|
if (!isRecord(entry)) continue;
|
|
@@ -3953,33 +4408,52 @@ function parseCatalogResponse(value) {
|
|
|
3953
4408
|
const name = stringValue(entry.name);
|
|
3954
4409
|
const contextLength = numberValue(entry.context_length);
|
|
3955
4410
|
if (!id || !name || !contextLength || contextLength <= 0) continue;
|
|
4411
|
+
const endpoints = Array.isArray(entry.supported_endpoints) ? entry.supported_endpoints.filter((item) => typeof item === "string") : [];
|
|
4412
|
+
const published = numberValue(entry.max_output_tokens);
|
|
4413
|
+
if (published !== void 0 && published > 0) outputLimitsFor(publishedOutputLimits, apiBase).set(id, published);
|
|
3956
4414
|
models.push({
|
|
3957
4415
|
id,
|
|
3958
4416
|
name,
|
|
3959
4417
|
contextWindow: contextLength,
|
|
3960
|
-
maxTokens: Math.min(contextLength,
|
|
4418
|
+
maxTokens: Math.min(contextLength, modelOutputTokenLimit(apiBase, id)),
|
|
4419
|
+
...published !== void 0 && published > 0 ? { publishedMaxTokens: published } : {},
|
|
4420
|
+
supportedEndpoints: endpoints
|
|
3961
4421
|
});
|
|
3962
4422
|
}
|
|
3963
4423
|
if (models.length === 0) throw new LlmError("Command Code returned an empty model catalog", "PROVIDER_PROTOCOL_ERROR");
|
|
3964
4424
|
return models;
|
|
3965
4425
|
}
|
|
3966
|
-
async function readModelsCache(cachePath) {
|
|
4426
|
+
async function readModelsCache(cachePath, apiBase) {
|
|
3967
4427
|
const parsed = JSON.parse(await readFile(cachePath, "utf-8"));
|
|
3968
|
-
if (!isRecord(parsed) || parsed.version !== MODEL_CACHE_VERSION || !Array.isArray(parsed.models)) throw new Error(`Invalid model cache at ${cachePath}`);
|
|
3969
|
-
|
|
4428
|
+
if (!isRecord(parsed) || parsed.version !== MODEL_CACHE_VERSION || parsed.apiBase !== apiBase || !Array.isArray(parsed.models)) throw new Error(`Invalid model cache at ${cachePath}`);
|
|
4429
|
+
publishedOutputLimits.set(apiBase, /* @__PURE__ */ new Map());
|
|
4430
|
+
return parsed.models.map((entry) => {
|
|
4431
|
+
const model = isRecord(entry) ? entry : {};
|
|
4432
|
+
const endpoints = Array.isArray(model.supportedEndpoints) ? model.supportedEndpoints.filter((item) => typeof item === "string") : [];
|
|
4433
|
+
const id = stringValue(model.id);
|
|
4434
|
+
const published = numberValue(model.publishedMaxTokens);
|
|
4435
|
+
if (id !== void 0 && published !== void 0 && published > 0) outputLimitsFor(publishedOutputLimits, apiBase).set(id, published);
|
|
4436
|
+
const contextWindow = numberValue(model.contextWindow);
|
|
4437
|
+
return {
|
|
4438
|
+
...model,
|
|
4439
|
+
supportedEndpoints: endpoints,
|
|
4440
|
+
maxTokens: id === void 0 || contextWindow === void 0 ? numberValue(model.maxTokens) ?? 131072 : Math.min(contextWindow, modelOutputTokenLimit(apiBase, id))
|
|
4441
|
+
};
|
|
4442
|
+
});
|
|
3970
4443
|
}
|
|
3971
|
-
async function writeModelsCache(cachePath, models) {
|
|
4444
|
+
async function writeModelsCache(cachePath, apiBase, models, mayCommit) {
|
|
3972
4445
|
await mkdir(dirname(cachePath), { recursive: true });
|
|
3973
|
-
const tmp = `${cachePath}.${process.pid}.tmp`;
|
|
4446
|
+
const tmp = `${cachePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
3974
4447
|
try {
|
|
3975
4448
|
await writeFile(tmp, `${JSON.stringify({
|
|
3976
4449
|
version: MODEL_CACHE_VERSION,
|
|
4450
|
+
apiBase,
|
|
3977
4451
|
models
|
|
3978
4452
|
}, null, 2)}\n`, {
|
|
3979
4453
|
encoding: "utf-8",
|
|
3980
4454
|
mode: 384
|
|
3981
4455
|
});
|
|
3982
|
-
await rename(tmp, cachePath);
|
|
4456
|
+
if (mayCommit()) await rename(tmp, cachePath);
|
|
3983
4457
|
} finally {
|
|
3984
4458
|
await rm(tmp, { force: true }).catch(() => void 0);
|
|
3985
4459
|
}
|
|
@@ -4229,6 +4703,38 @@ function seenImageOffloadCount(messages, model) {
|
|
|
4229
4703
|
async function readRequestImage(attachments, ref) {
|
|
4230
4704
|
return attachments.readImageRequest(ref, requestImageTarget(ref));
|
|
4231
4705
|
}
|
|
4706
|
+
/** 同一次生成及其协议切换共享图片读取,最多同时准备四张,避免内存峰值失控。 */
|
|
4707
|
+
function requestImages(attachments, signal, timing) {
|
|
4708
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4709
|
+
const read = (ref) => {
|
|
4710
|
+
signal?.throwIfAborted();
|
|
4711
|
+
const key = JSON.stringify([ref.attachmentId, requestImageTarget(ref)]);
|
|
4712
|
+
let pending = cache.get(key);
|
|
4713
|
+
if (!pending) {
|
|
4714
|
+
pending = readRequestImage(attachments, ref);
|
|
4715
|
+
cache.set(key, pending);
|
|
4716
|
+
}
|
|
4717
|
+
return pending;
|
|
4718
|
+
};
|
|
4719
|
+
read.prepare = async (refs) => {
|
|
4720
|
+
if (refs.length === 0) return;
|
|
4721
|
+
const finishImages = timing.phase("images");
|
|
4722
|
+
let cursor = 0;
|
|
4723
|
+
try {
|
|
4724
|
+
const workers = Array.from({ length: Math.min(4, refs.length) }, async () => {
|
|
4725
|
+
while (cursor < refs.length) {
|
|
4726
|
+
signal?.throwIfAborted();
|
|
4727
|
+
await read(refs[cursor++]);
|
|
4728
|
+
}
|
|
4729
|
+
});
|
|
4730
|
+
const failure = (await Promise.allSettled(workers)).find((result) => result.status === "rejected");
|
|
4731
|
+
if (failure?.status === "rejected") throw failure.reason;
|
|
4732
|
+
} finally {
|
|
4733
|
+
finishImages();
|
|
4734
|
+
}
|
|
4735
|
+
};
|
|
4736
|
+
return read;
|
|
4737
|
+
}
|
|
4232
4738
|
/**
|
|
4233
4739
|
* Price one request occurrence.
|
|
4234
4740
|
*
|
|
@@ -4272,6 +4778,17 @@ async function imageToCommandCode(ref, readImage) {
|
|
|
4272
4778
|
};
|
|
4273
4779
|
}
|
|
4274
4780
|
/**
|
|
4781
|
+
* The signature recorded for the reasoning block at `position` of a
|
|
4782
|
+
* historical assistant message, or undefined when the message was produced
|
|
4783
|
+
* by another route or an older plugin version.
|
|
4784
|
+
*/
|
|
4785
|
+
function messagesSignatureAt(message, position) {
|
|
4786
|
+
const replay = message.source?.replayState;
|
|
4787
|
+
if (!isRecord(replay) || !Array.isArray(replay.blocks)) return void 0;
|
|
4788
|
+
const entry = replay.blocks[position];
|
|
4789
|
+
return isRecord(entry) ? stringValue(entry.signature) : void 0;
|
|
4790
|
+
}
|
|
4791
|
+
/**
|
|
4275
4792
|
* The common pairing and image-carrier ordering for both transports. Each
|
|
4276
4793
|
* codec owns its wire shape: CLI content blocks and Chat Completions
|
|
4277
4794
|
* reasoning/tool calls differ beyond the image envelope.
|
|
@@ -4279,6 +4796,16 @@ async function imageToCommandCode(ref, readImage) {
|
|
|
4279
4796
|
async function convertMessages(messages, readImage, codec) {
|
|
4280
4797
|
const out = [];
|
|
4281
4798
|
const { ids: paired, names: toolNames } = pairedToolCalls(messages);
|
|
4799
|
+
if (readImage?.prepare) {
|
|
4800
|
+
const refs = [];
|
|
4801
|
+
for (const message of messages) {
|
|
4802
|
+
const result = toolResultOf(message);
|
|
4803
|
+
if (message.role === "user" && !result) {
|
|
4804
|
+
for (const block of message.content) if (block.type === "image") refs.push(block.attachment);
|
|
4805
|
+
} else if (result && paired.has(result.toolCallId)) refs.push(...toolResultMedia(result).images);
|
|
4806
|
+
}
|
|
4807
|
+
await readImage.prepare(refs);
|
|
4808
|
+
}
|
|
4282
4809
|
const wireIds = wireToolCallIds(paired);
|
|
4283
4810
|
const pendingImages = [];
|
|
4284
4811
|
const flushPendingImages = () => {
|
|
@@ -4314,8 +4841,8 @@ async function convertMessages(messages, readImage, codec) {
|
|
|
4314
4841
|
if (!result || !paired.has(result.toolCallId)) continue;
|
|
4315
4842
|
const media = toolResultMedia(result);
|
|
4316
4843
|
const wireId = wireIds.get(result.toolCallId) ?? result.toolCallId;
|
|
4317
|
-
out.push(codec.encodeTool(result, media, wireId, toolNames.get(result.toolCallId) || "unknown"));
|
|
4318
|
-
if (media.images.length > 0) {
|
|
4844
|
+
out.push(await codec.encodeTool(result, media, wireId, toolNames.get(result.toolCallId) || "unknown", readImage));
|
|
4845
|
+
if (!codec.carriesToolResultMedia && media.images.length > 0) {
|
|
4319
4846
|
const carried = [{
|
|
4320
4847
|
type: "text",
|
|
4321
4848
|
text: toolResultImageNote(media, wireId)
|
|
@@ -4328,6 +4855,7 @@ async function convertMessages(messages, readImage, codec) {
|
|
|
4328
4855
|
return out;
|
|
4329
4856
|
}
|
|
4330
4857
|
const ccMessageCodec = {
|
|
4858
|
+
carriesToolResultMedia: false,
|
|
4331
4859
|
encodeImage: imageToCommandCode,
|
|
4332
4860
|
encodeUser: (parts) => ({
|
|
4333
4861
|
role: "user",
|
|
@@ -4395,6 +4923,7 @@ async function imageToOpenAI(ref, readImage) {
|
|
|
4395
4923
|
* replayed (same policy as the CLI path).
|
|
4396
4924
|
*/
|
|
4397
4925
|
const openAiMessageCodec = {
|
|
4926
|
+
carriesToolResultMedia: false,
|
|
4398
4927
|
encodeImage: imageToOpenAI,
|
|
4399
4928
|
encodeUser: (parts) => {
|
|
4400
4929
|
return !parts.some((part) => part.type === "image_url") && parts.length === 1 ? {
|
|
@@ -4434,6 +4963,119 @@ const openAiMessageCodec = {
|
|
|
4434
4963
|
async function messagesToOpenAI(messages, readImage) {
|
|
4435
4964
|
return convertMessages(messages, readImage, openAiMessageCodec);
|
|
4436
4965
|
}
|
|
4966
|
+
/**
|
|
4967
|
+
* Convert one image reference to the Messages wire shape. Unlike the other two
|
|
4968
|
+
* transports this one keeps the native media type rather than a data URL, and
|
|
4969
|
+
* it may be used INSIDE a tool result — `tool_result.content` accepts image
|
|
4970
|
+
* blocks, which is why this codec sets `carriesToolResultMedia`.
|
|
4971
|
+
*/
|
|
4972
|
+
async function imageToMessages(ref, readImage) {
|
|
4973
|
+
const version = await readImage(ref);
|
|
4974
|
+
return {
|
|
4975
|
+
type: "image",
|
|
4976
|
+
source: {
|
|
4977
|
+
type: "base64",
|
|
4978
|
+
media_type: version.mediaType,
|
|
4979
|
+
data: Buffer.from(version.data).toString("base64")
|
|
4980
|
+
}
|
|
4981
|
+
};
|
|
4982
|
+
}
|
|
4983
|
+
/** A tool call's `arguments` is a raw JSON string in the harness, an object here. */
|
|
4984
|
+
function parseToolArguments(raw) {
|
|
4985
|
+
try {
|
|
4986
|
+
const parsed = JSON.parse(raw);
|
|
4987
|
+
return isRecord(parsed) ? parsed : {};
|
|
4988
|
+
} catch {
|
|
4989
|
+
return {};
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
/**
|
|
4993
|
+
* Convert harness messages to the Anthropic Messages request shape.
|
|
4994
|
+
*
|
|
4995
|
+
* Differences from the other two transports, all measured against the live
|
|
4996
|
+
* endpoint on 2026-09-29:
|
|
4997
|
+
*
|
|
4998
|
+
* - Content is always a block array; there is no string shorthand, so a
|
|
4999
|
+
* single-text user turn is still `{ role: 'user', content: [{type:'text'…}] }`.
|
|
5000
|
+
* - Tool results live in a `user` turn as `tool_result` blocks (not a
|
|
5001
|
+
* `role: 'tool'` message) and may carry images natively, so no image is
|
|
5002
|
+
* split into a separate user message here.
|
|
5003
|
+
* - A replayed `thinking` block is only valid WITH its `signature`: the
|
|
5004
|
+
* endpoint answers `thinking.signature: Field required` without it, and
|
|
5005
|
+
* `each thinking block must contain thinking` when the text is empty and
|
|
5006
|
+
* the signature is blank. A reasoning block with no recorded signature is
|
|
5007
|
+
* therefore dropped rather than sent — replaying it would fail the request,
|
|
5008
|
+
* while omitting it is accepted (verified 200 both ways).
|
|
5009
|
+
* - The conversation must end with a user turn; the endpoint refuses an
|
|
5010
|
+
* assistant prefill (`This model does not support assistant message
|
|
5011
|
+
* prefill`), which `convertMessages` already guarantees for a tool loop.
|
|
5012
|
+
*/
|
|
5013
|
+
const messagesMessageCodec = {
|
|
5014
|
+
carriesToolResultMedia: true,
|
|
5015
|
+
encodeImage: imageToMessages,
|
|
5016
|
+
encodeUser: (parts) => ({
|
|
5017
|
+
role: "user",
|
|
5018
|
+
content: parts
|
|
5019
|
+
}),
|
|
5020
|
+
encodeAssistant: (message, paired, wireIds) => {
|
|
5021
|
+
const parts = [];
|
|
5022
|
+
let position = 0;
|
|
5023
|
+
for (const block of message.content) {
|
|
5024
|
+
if (block.type === "text") {
|
|
5025
|
+
parts.push({
|
|
5026
|
+
type: "text",
|
|
5027
|
+
text: block.text
|
|
5028
|
+
});
|
|
5029
|
+
position++;
|
|
5030
|
+
continue;
|
|
5031
|
+
}
|
|
5032
|
+
if (block.type === "reasoning") {
|
|
5033
|
+
const signature = messagesSignatureAt(message, position);
|
|
5034
|
+
if (signature !== void 0) parts.push({
|
|
5035
|
+
type: "thinking",
|
|
5036
|
+
thinking: block.text,
|
|
5037
|
+
signature
|
|
5038
|
+
});
|
|
5039
|
+
position++;
|
|
5040
|
+
continue;
|
|
5041
|
+
}
|
|
5042
|
+
if (block.type === "tool-call" && paired.has(block.id)) {
|
|
5043
|
+
parts.push({
|
|
5044
|
+
type: "tool_use",
|
|
5045
|
+
id: wireIds.get(block.id) ?? block.id,
|
|
5046
|
+
name: block.name,
|
|
5047
|
+
input: parseToolArguments(block.arguments)
|
|
5048
|
+
});
|
|
5049
|
+
position++;
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
return parts.length > 0 ? {
|
|
5053
|
+
role: "assistant",
|
|
5054
|
+
content: parts
|
|
5055
|
+
} : void 0;
|
|
5056
|
+
},
|
|
5057
|
+
encodeTool: async (result, media, wireId, _toolName, readImage) => {
|
|
5058
|
+
const content = [];
|
|
5059
|
+
if (readImage) for (const attachment of media.images) content.push(await imageToMessages(attachment, readImage));
|
|
5060
|
+
content.push({
|
|
5061
|
+
type: "text",
|
|
5062
|
+
text: toolResultTextForWire(media)
|
|
5063
|
+
});
|
|
5064
|
+
const toolResult = {
|
|
5065
|
+
type: "tool_result",
|
|
5066
|
+
tool_use_id: wireId,
|
|
5067
|
+
content
|
|
5068
|
+
};
|
|
5069
|
+
if (result.isError) toolResult.is_error = true;
|
|
5070
|
+
return {
|
|
5071
|
+
role: "user",
|
|
5072
|
+
content: [toolResult]
|
|
5073
|
+
};
|
|
5074
|
+
}
|
|
5075
|
+
};
|
|
5076
|
+
async function messagesToMessages(messages, readImage) {
|
|
5077
|
+
return convertMessages(messages, readImage, messagesMessageCodec);
|
|
5078
|
+
}
|
|
4437
5079
|
/** Account endpoints fetched by one `getUsage()` run (see the classification there). */
|
|
4438
5080
|
const USAGE_ENDPOINT_COUNT = 4;
|
|
4439
5081
|
/**
|
|
@@ -4561,6 +5203,50 @@ function cliSystem(systemText) {
|
|
|
4561
5203
|
cache_control: { type: "ephemeral" }
|
|
4562
5204
|
}];
|
|
4563
5205
|
}
|
|
5206
|
+
/**
|
|
5207
|
+
* The Anthropic ephemeral cache marker, shared by every breakpoint this route
|
|
5208
|
+
* sets. One object is reused across positions because it is only ever
|
|
5209
|
+
* serialized, never mutated.
|
|
5210
|
+
*
|
|
5211
|
+
* No `ttl` is sent: the default 5-minute entry covers a continuously advancing
|
|
5212
|
+
* session, and `ttl: "1h"` is a separate billable tier whose read/write prices
|
|
5213
|
+
* are identical — it would only lengthen survival, not reduce cost.
|
|
5214
|
+
*/
|
|
5215
|
+
const EPHEMERAL_CACHE_CONTROL = { type: "ephemeral" };
|
|
5216
|
+
/**
|
|
5217
|
+
* Content-block types that may carry the rolling breakpoint. Matches what the
|
|
5218
|
+
* official Command Code provider for pi marks through
|
|
5219
|
+
* `@earendil-works/pi-ai`'s `anthropic-messages` transport (its own list also
|
|
5220
|
+
* holds pi-internal `tool_addition` / `tool_removal`, which this route never
|
|
5221
|
+
* emits). Assistant-side blocks are deliberately excluded: a breakpoint on a
|
|
5222
|
+
* `thinking` or `tool_use` block caches a prefix the next turn invalidates.
|
|
5223
|
+
*/
|
|
5224
|
+
const CACHEABLE_BLOCK_TYPES = /* @__PURE__ */ new Set([
|
|
5225
|
+
"text",
|
|
5226
|
+
"image",
|
|
5227
|
+
"tool_result"
|
|
5228
|
+
]);
|
|
5229
|
+
/**
|
|
5230
|
+
* Mark the last user turn as the rolling cache breakpoint.
|
|
5231
|
+
*
|
|
5232
|
+
* Anthropic caching is explicit — a prefix is stored only when a
|
|
5233
|
+
* `cache_control` marker names it — so an unmarked route re-prefills the whole
|
|
5234
|
+
* replayed history at the full input price on every turn. Measured cost of that
|
|
5235
|
+
* omission on a 10-turn session: $0.5648, all of it at the undiscounted
|
|
5236
|
+
* $2/M input rate, against $0.1869 with the three breakpoints this route sets.
|
|
5237
|
+
*
|
|
5238
|
+
* The final user message is the rolling boundary: DSH resends the whole
|
|
5239
|
+
* history each turn, so a marker here grows with the conversation, whereas a
|
|
5240
|
+
* fixed `system`-only marker would leave every later turn's history
|
|
5241
|
+
* uncached. {@link messagesMessageCodec} renders both user input and tool
|
|
5242
|
+
* results as `role: 'user'`, so this covers a tool loop's trailing turn too.
|
|
5243
|
+
*/
|
|
5244
|
+
function markLastUserCacheControl(messages) {
|
|
5245
|
+
const last = messages[messages.length - 1];
|
|
5246
|
+
if (!isRecord(last) || last.role !== "user" || !Array.isArray(last.content)) return;
|
|
5247
|
+
const lastBlock = last.content[last.content.length - 1];
|
|
5248
|
+
if (isRecord(lastBlock) && typeof lastBlock.type === "string" && CACHEABLE_BLOCK_TYPES.has(lastBlock.type)) lastBlock.cache_control = EPHEMERAL_CACHE_CONTROL;
|
|
5249
|
+
}
|
|
4564
5250
|
/** Build the legacy CLI (`/alpha/generate`) request body for one call. */
|
|
4565
5251
|
async function buildCliBody(options, connection, facts) {
|
|
4566
5252
|
return {
|
|
@@ -4621,6 +5307,62 @@ async function buildOpenAIBody(options, facts) {
|
|
|
4621
5307
|
};
|
|
4622
5308
|
}
|
|
4623
5309
|
/**
|
|
5310
|
+
* Build the Anthropic Messages request body for one call.
|
|
5311
|
+
*
|
|
5312
|
+
* Measured deviations from DeepSeek's own Messages API (the shape
|
|
5313
|
+
* `dsh-llm-deepseek` implements), all confirmed on 2026-09-29:
|
|
5314
|
+
*
|
|
5315
|
+
* - `thinking` accepts ONLY `{ type: 'adaptive' }` here. `enabled` and
|
|
5316
|
+
* `disabled` are refused with `"thinking.type.enabled" is not supported for
|
|
5317
|
+
* this model. Use "thinking.type.adaptive" and "output_config.effort" to
|
|
5318
|
+
* control thinking behavior.`, and a `between_tools` value fails validation
|
|
5319
|
+
* outright. The model reasons on its own; this switch only admits it.
|
|
5320
|
+
* - Reasoning strength travels in `output_config.effort`, whose accepted
|
|
5321
|
+
* values are `low | medium | high | xhigh | max` — there is no `none`, and
|
|
5322
|
+
* `xhigh` is absent from DeepSeek's own type.
|
|
5323
|
+
* - `stop_sequences` is a real field here, but this adapter still rejects
|
|
5324
|
+
* `GenerateOptions.stop` in `stream()` because Command Code documents no
|
|
5325
|
+
* equivalent for the other two transports.
|
|
5326
|
+
* - **`temperature` is never sent.** Adaptive thinking constrains it to 1:
|
|
5327
|
+
* `` `temperature` may only be set to 1 when thinking is enabled or in
|
|
5328
|
+
* adaptive mode `` (measured 2026-09-29 — every request carrying the other
|
|
5329
|
+
* transports' 0.3 default was refused with HTTP 400). Omitting the field
|
|
5330
|
+
* is the only way to honour both the thinking switch and a caller that
|
|
5331
|
+
* asked for a temperature, so Claude loses sampling control on this route
|
|
5332
|
+
* because the endpoint, not this adapter, removed it.
|
|
5333
|
+
* - `system` travels as a one-block ARRAY carrying `cache_control`, not as the
|
|
5334
|
+
* bare string the other two transports send. Anthropic's caching is opt-in
|
|
5335
|
+
* per block, and this route is the only Claude path, so the marker is what
|
|
5336
|
+
* makes a session's replayed history bill at the cache-read rate instead of
|
|
5337
|
+
* the full input rate. See {@link EPHEMERAL_CACHE_CONTROL}.
|
|
5338
|
+
*/
|
|
5339
|
+
async function buildMessagesBody(options, facts) {
|
|
5340
|
+
const body = {
|
|
5341
|
+
model: options.model,
|
|
5342
|
+
max_tokens: facts.maxTokens,
|
|
5343
|
+
stream: true,
|
|
5344
|
+
messages: await messagesToMessages(options.messages, facts.readImage)
|
|
5345
|
+
};
|
|
5346
|
+
if (facts.selectableEffort) body.thinking = { type: "adaptive" };
|
|
5347
|
+
if (facts.systemText) body.system = [{
|
|
5348
|
+
type: "text",
|
|
5349
|
+
text: facts.systemText,
|
|
5350
|
+
cache_control: EPHEMERAL_CACHE_CONTROL
|
|
5351
|
+
}];
|
|
5352
|
+
if (facts.reasoningEffort) body.output_config = { effort: facts.reasoningEffort };
|
|
5353
|
+
const tools = (options.tools ?? []).map((tool) => ({
|
|
5354
|
+
name: tool.name,
|
|
5355
|
+
description: tool.description,
|
|
5356
|
+
input_schema: toolParametersSchema(tool.parameters)
|
|
5357
|
+
}));
|
|
5358
|
+
if (tools.length > 0) {
|
|
5359
|
+
tools[tools.length - 1].cache_control = EPHEMERAL_CACHE_CONTROL;
|
|
5360
|
+
body.tools = tools;
|
|
5361
|
+
}
|
|
5362
|
+
markLastUserCacheControl(body.messages);
|
|
5363
|
+
return body;
|
|
5364
|
+
}
|
|
5365
|
+
/**
|
|
4624
5366
|
* Estimate the text the selected transport actually sends. JSON framing and
|
|
4625
5367
|
* punctuation each get a token; ordinary ASCII words get one, with an extra
|
|
4626
5368
|
* allowance for long identifiers. UTF-8 bytes / 4 tracks the measured CJK
|
|
@@ -4656,6 +5398,17 @@ function requestContextBudget(protocol, body, messages, model, contextWindow, re
|
|
|
4656
5398
|
};
|
|
4657
5399
|
}
|
|
4658
5400
|
/**
|
|
5401
|
+
* One connect attempt's inputs: everything `connectGenerate` needs beyond
|
|
5402
|
+
* the per-attempt key and protocol. Passed explicitly (not closed over) so
|
|
5403
|
+
* the rotation loop's mutation of `protocol`/`body` stays visible at the
|
|
5404
|
+
* call site.
|
|
5405
|
+
*/
|
|
5406
|
+
/** The generate endpoint one protocol posts to. */
|
|
5407
|
+
function protocolEndpoint(protocol, apiBase) {
|
|
5408
|
+
if (protocol === "cli") return `${apiBase}/alpha/generate`;
|
|
5409
|
+
return protocol === "messages" ? `${apiBase}/provider/v1/messages` : `${apiBase}/provider/v1/chat/completions`;
|
|
5410
|
+
}
|
|
5411
|
+
/**
|
|
4659
5412
|
* One pre-stream connect attempt: POST the body and wait for response
|
|
4660
5413
|
* headers only (`requestTimeoutMs` must never bound the body stream). Returns
|
|
4661
5414
|
* the live response plus its cleanup, or the rejection facts for the rotation
|
|
@@ -4663,11 +5416,11 @@ function requestContextBudget(protocol, body, messages, model, contextWindow, re
|
|
|
4663
5416
|
* on success the caller-abort listener outlives the connect phase (it aborts a
|
|
4664
5417
|
* stalled body read), so the streaming tail calls cleanup.
|
|
4665
5418
|
*/
|
|
4666
|
-
async function connectGenerate(deps, key, protocol,
|
|
5419
|
+
async function connectGenerate(deps, key, protocol, serializedBody) {
|
|
4667
5420
|
const { options, connection, fetchImpl } = deps;
|
|
4668
5421
|
const connectAbort = new AbortController();
|
|
4669
5422
|
let connectTimedOut = false;
|
|
4670
|
-
const endpoint = protocol
|
|
5423
|
+
const endpoint = protocolEndpoint(protocol, connection.apiBase);
|
|
4671
5424
|
const connectTimer = setTimeout(() => {
|
|
4672
5425
|
connectTimedOut = true;
|
|
4673
5426
|
connectAbort.abort(new DOMException(`Command Code API request to ${endpoint} did not respond within ${connection.requestTimeoutMs}ms`, "TimeoutError"));
|
|
@@ -4685,26 +5438,25 @@ async function connectGenerate(deps, key, protocol, body) {
|
|
|
4685
5438
|
};
|
|
4686
5439
|
let response;
|
|
4687
5440
|
const zdr = connection.zdr === true;
|
|
4688
|
-
const
|
|
5441
|
+
const sharedHeaders = {
|
|
4689
5442
|
"Content-Type": "application/json",
|
|
4690
5443
|
...IDENTITY_ENCODING_HEADER,
|
|
4691
5444
|
Authorization: `Bearer ${key}`,
|
|
5445
|
+
...zdr ? { "x-cmd-zdr": "1" } : {},
|
|
5446
|
+
...attributionHeaders()
|
|
5447
|
+
};
|
|
5448
|
+
const headers = protocol === "cli" ? {
|
|
5449
|
+
...sharedHeaders,
|
|
4692
5450
|
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
4693
5451
|
"x-cli-environment": "production",
|
|
4694
5452
|
"x-project-slug": projectSlugFromPath(connection.workingDir),
|
|
4695
5453
|
"x-taste-learning": "true",
|
|
4696
|
-
"x-co-flag": "false"
|
|
4697
|
-
...zdr ? { "x-cmd-zdr": "1" } : {},
|
|
4698
|
-
...attributionHeaders()
|
|
5454
|
+
"x-co-flag": "false"
|
|
4699
5455
|
} : {
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
Authorization: `Bearer ${key}`,
|
|
4703
|
-
Accept: "text/event-stream",
|
|
4704
|
-
...zdr ? { "x-cmd-zdr": "1" } : {},
|
|
4705
|
-
...attributionHeaders()
|
|
5456
|
+
...sharedHeaders,
|
|
5457
|
+
Accept: "text/event-stream"
|
|
4706
5458
|
};
|
|
4707
|
-
const
|
|
5459
|
+
const finishHeaders = deps.timing.attempt(protocol, Buffer.byteLength(serializedBody));
|
|
4708
5460
|
try {
|
|
4709
5461
|
response = await fetchImpl(endpoint, {
|
|
4710
5462
|
method: "POST",
|
|
@@ -4712,8 +5464,10 @@ async function connectGenerate(deps, key, protocol, body) {
|
|
|
4712
5464
|
body: serializedBody,
|
|
4713
5465
|
signal: connectAbort.signal
|
|
4714
5466
|
});
|
|
5467
|
+
finishHeaders(response.status);
|
|
4715
5468
|
clearTimeout(connectTimer);
|
|
4716
5469
|
} catch (error) {
|
|
5470
|
+
finishHeaders();
|
|
4717
5471
|
cleanup();
|
|
4718
5472
|
if (options.signal?.aborted) throw error;
|
|
4719
5473
|
if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw headersTimeoutError({
|
|
@@ -4755,7 +5509,9 @@ function createBlockAssembler() {
|
|
|
4755
5509
|
sawContent: false,
|
|
4756
5510
|
finishReason: void 0,
|
|
4757
5511
|
cliCacheWriteTokens: void 0,
|
|
4758
|
-
openAiToolCalls: []
|
|
5512
|
+
openAiToolCalls: [],
|
|
5513
|
+
messagesBlocks: /* @__PURE__ */ new Map(),
|
|
5514
|
+
replayBlocks: []
|
|
4759
5515
|
};
|
|
4760
5516
|
}
|
|
4761
5517
|
function* closeText(asm) {
|
|
@@ -4816,9 +5572,10 @@ function* emitOpenAiToolCalls(asm) {
|
|
|
4816
5572
|
}
|
|
4817
5573
|
asm.openAiToolCalls.length = 0;
|
|
4818
5574
|
}
|
|
4819
|
-
var CommandCodeAdapter = class extends LlmAdapter {
|
|
5575
|
+
var CommandCodeAdapter = class CommandCodeAdapter extends LlmAdapter {
|
|
4820
5576
|
deps;
|
|
4821
5577
|
catalog = [];
|
|
5578
|
+
catalogState;
|
|
4822
5579
|
fetchImpl;
|
|
4823
5580
|
resolveAttachments;
|
|
4824
5581
|
/**
|
|
@@ -4829,13 +5586,25 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4829
5586
|
billingAccess = /* @__PURE__ */ new Map();
|
|
4830
5587
|
billingAccessInflight = /* @__PURE__ */ new Map();
|
|
4831
5588
|
protocolCache = /* @__PURE__ */ new Map();
|
|
5589
|
+
/** Namespace account-derived facts by gateway without exposing the key. */
|
|
5590
|
+
accountCacheKey(apiBase, apiKey) {
|
|
5591
|
+
return JSON.stringify([apiBase, apiKey]);
|
|
5592
|
+
}
|
|
4832
5593
|
/**
|
|
4833
5594
|
* Consecutive header timeouts for the last request shape, counted ACROSS
|
|
4834
5595
|
* `stream()` calls: dsh-llm-retry re-invokes this same instance, so this is
|
|
4835
|
-
* the only place a streak can live.
|
|
4836
|
-
*
|
|
5596
|
+
* the only place a streak can live. Keyed by `sessionId` (not by `agent`:
|
|
5597
|
+
* `GenerateOptions` carries no agent reference, only `sessionId`) so
|
|
5598
|
+
* concurrent sessions on this one shared adapter instance cannot clear or
|
|
5599
|
+
* inflate each other's count. A call with no `sessionId` (a one-shot,
|
|
5600
|
+
* session-less caller) falls back to {@link GLOBAL_HEADER_TIMEOUT_KEY},
|
|
5601
|
+
* matching the old single-slot behavior for exactly that case. Bounded by
|
|
5602
|
+
* {@link MAX_HEADER_TIMEOUT_STREAK_ENTRIES} with FIFO eviction (`Map`
|
|
5603
|
+
* preserves insertion order) since the key space is per-session rather than
|
|
5604
|
+
* per-account/model, so it is not naturally small like `protocolCache`.
|
|
4837
5605
|
*/
|
|
4838
|
-
|
|
5606
|
+
headerTimeoutStreaks = /* @__PURE__ */ new Map();
|
|
5607
|
+
static MAX_HEADER_TIMEOUT_STREAK_ENTRIES = 200;
|
|
4839
5608
|
constructor(deps) {
|
|
4840
5609
|
super();
|
|
4841
5610
|
this.deps = deps;
|
|
@@ -4848,25 +5617,31 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4848
5617
|
* payload eventually leaves the retryable whitelist.
|
|
4849
5618
|
*
|
|
4850
5619
|
* `providerRetryPolicy()` cannot express this itself — dsh-llm's
|
|
4851
|
-
* `NormalRetryPolicyConfig` carries
|
|
4852
|
-
*
|
|
4853
|
-
* the
|
|
4854
|
-
* carries a real "try again later" signal) while stopping the one code that,
|
|
4855
|
-
* repeated unchanged, only burns the user's session.
|
|
5620
|
+
* `NormalRetryPolicyConfig` carries one `maxRetries` for every listed code.
|
|
5621
|
+
* Escalating here stops repeated identical header timeouts with a specific
|
|
5622
|
+
* diagnosis; the agent listener separately caps ordinary transient errors.
|
|
4856
5623
|
*
|
|
4857
5624
|
* @returns the error to surface: unchanged for any other failure, the
|
|
4858
5625
|
* original timeout below the threshold, and a terminal one at it.
|
|
4859
5626
|
*/
|
|
4860
|
-
gradeHeaderTimeout(error, protocol, body, timeoutMs) {
|
|
5627
|
+
gradeHeaderTimeout(error, protocol, body, timeoutMs, sessionId) {
|
|
4861
5628
|
if (!(error instanceof LlmError && error.code === "TIMEOUT")) return error;
|
|
5629
|
+
const key = sessionId ?? GLOBAL_HEADER_TIMEOUT_KEY;
|
|
4862
5630
|
const fingerprint = `${protocol}:${fingerprintValue(body).sha256}`;
|
|
4863
|
-
const
|
|
4864
|
-
|
|
5631
|
+
const previous = this.headerTimeoutStreaks.get(key);
|
|
5632
|
+
const count = previous?.fingerprint === fingerprint ? previous.count + 1 : 1;
|
|
5633
|
+
if (previous === void 0) {
|
|
5634
|
+
if (this.headerTimeoutStreaks.size >= CommandCodeAdapter.MAX_HEADER_TIMEOUT_STREAK_ENTRIES) {
|
|
5635
|
+
const oldest = this.headerTimeoutStreaks.keys().next().value;
|
|
5636
|
+
if (oldest !== void 0) this.headerTimeoutStreaks.delete(oldest);
|
|
5637
|
+
}
|
|
5638
|
+
} else this.headerTimeoutStreaks.delete(key);
|
|
5639
|
+
this.headerTimeoutStreaks.set(key, {
|
|
4865
5640
|
fingerprint,
|
|
4866
5641
|
count
|
|
4867
|
-
};
|
|
5642
|
+
});
|
|
4868
5643
|
if (count < MAX_CONSECUTIVE_HEADER_TIMEOUTS) return error;
|
|
4869
|
-
this.
|
|
5644
|
+
this.headerTimeoutStreaks.delete(key);
|
|
4870
5645
|
return bilingual("PROVIDER_HTTP_ERROR", `Command Code API stopped retrying: the gateway returned no response headers on ${count} consecutive attempts at the same request, each after the full ${timeoutMs}ms budget, so this is not a network blip — it is provider queueing, a Go-plan capacity limit, or a network/proxy path problem. Resending the identical payload again is unlikely to help; wait for the provider to recover, or switch model or account.`, `Command Code API 已停止重试:同一请求连续 ${count} 次在 ${timeoutMs} 毫秒内都没有收到任何响应头,这不是网络抖动,而是服务商排队、Go 套餐容量限制或网络/代理链路问题。继续原样重发同一个请求通常无济于事——可以等服务商恢复,或换模型/换账号。`);
|
|
4871
5646
|
}
|
|
4872
5647
|
/**
|
|
@@ -4883,12 +5658,11 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4883
5658
|
};
|
|
4884
5659
|
}
|
|
4885
5660
|
/**
|
|
4886
|
-
*
|
|
4887
|
-
*
|
|
4888
|
-
*
|
|
4889
|
-
* `
|
|
4890
|
-
* and
|
|
4891
|
-
* recovers in-session instead of failing after two tries. Permanent failures
|
|
5661
|
+
* The official executor owns backoff and provider reset waits. Its high
|
|
5662
|
+
* route-wide cap lets a confirmed `RATE_LIMIT` window recover in-session;
|
|
5663
|
+
* our request-error listener limits `SERVER`/`TIMEOUT`/`EMPTY_RESPONSE`/
|
|
5664
|
+
* `THROTTLED` to three retries and `TRANSPORT` to its separate budget.
|
|
5665
|
+
* Waits double from 500 ms and cap at 15 minutes (±10% jitter). Permanent failures
|
|
4892
5666
|
* (an invalid key's `INVALID_CREDENTIAL`, `UNSUPPORTED_CONTENT`, plan
|
|
4893
5667
|
* rejections) are absent from the whitelist and surface immediately instead
|
|
4894
5668
|
* of looping. Waits the pool/adapter attach as `providerRetryAfterMs` are
|
|
@@ -4906,6 +5680,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4906
5680
|
retryableCodes: [
|
|
4907
5681
|
"EMPTY_RESPONSE",
|
|
4908
5682
|
"RATE_LIMIT",
|
|
5683
|
+
THROTTLED_CODE,
|
|
4909
5684
|
"SERVER",
|
|
4910
5685
|
"TIMEOUT",
|
|
4911
5686
|
"TRANSPORT"
|
|
@@ -4932,23 +5707,61 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4932
5707
|
const imageCapable = KNOWN_IMAGE_MODELS.has(model);
|
|
4933
5708
|
return { priceImages: (images) => images.map((image) => priceRequestImage(image, model, imageCapable)) };
|
|
4934
5709
|
}
|
|
4935
|
-
/**
|
|
5710
|
+
/** 地址或缓存文件变化时丢弃旧目录,防止旧网关的路由声明影响新请求。 */
|
|
5711
|
+
currentCatalogState(connection = this.deps.options()) {
|
|
5712
|
+
const { apiBase, modelsCachePath } = connection;
|
|
5713
|
+
const source = JSON.stringify([apiBase, modelsCachePath]);
|
|
5714
|
+
if (this.catalogState?.source !== source) {
|
|
5715
|
+
this.catalog = [];
|
|
5716
|
+
this.catalogState = {
|
|
5717
|
+
source,
|
|
5718
|
+
expiresAt: 0
|
|
5719
|
+
};
|
|
5720
|
+
}
|
|
5721
|
+
return this.catalogState;
|
|
5722
|
+
}
|
|
5723
|
+
/** 共享一次刷新;调用者取消只结束自己的等待,不取消其他调用者的请求。 */
|
|
4936
5724
|
async loadCatalog(signal) {
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
5725
|
+
signal?.throwIfAborted();
|
|
5726
|
+
const connection = this.deps.options();
|
|
5727
|
+
const state = this.currentCatalogState(connection);
|
|
5728
|
+
if (Date.now() < state.expiresAt) return this.catalog;
|
|
5729
|
+
const { apiBase, modelsCachePath } = connection;
|
|
5730
|
+
if (!state.inflight) {
|
|
5731
|
+
const fallback = this.catalog;
|
|
5732
|
+
state.inflight = (async () => {
|
|
5733
|
+
let models;
|
|
5734
|
+
let ttl = MODEL_CATALOG_TTL_MS;
|
|
5735
|
+
try {
|
|
5736
|
+
const { status, record } = await this.fetchJson(`${apiBase}/provider/v1/models`, {
|
|
5737
|
+
accept: "application/json",
|
|
5738
|
+
...IDENTITY_ENCODING_HEADER,
|
|
5739
|
+
...attributionHeaders()
|
|
5740
|
+
}, MODELS_TIMEOUT_MS);
|
|
5741
|
+
if (status < 200 || status >= 300) throw new Error(`models endpoint returned ${status}`);
|
|
5742
|
+
models = parseCatalogResponse(record, apiBase);
|
|
5743
|
+
if (this.catalogState === state) await writeModelsCache(modelsCachePath, apiBase, models, () => this.catalogState === state).catch(() => void 0);
|
|
5744
|
+
} catch {
|
|
5745
|
+
ttl = MODEL_CATALOG_RETRY_MS;
|
|
5746
|
+
models = await readModelsCache(modelsCachePath, apiBase).catch(() => fallback);
|
|
5747
|
+
}
|
|
5748
|
+
if (this.catalogState === state) {
|
|
5749
|
+
this.catalog = models;
|
|
5750
|
+
state.expiresAt = Date.now() + ttl;
|
|
5751
|
+
}
|
|
5752
|
+
return models;
|
|
5753
|
+
})().finally(() => {
|
|
5754
|
+
delete state.inflight;
|
|
5755
|
+
});
|
|
4950
5756
|
}
|
|
4951
|
-
return
|
|
5757
|
+
if (!signal) return state.inflight;
|
|
5758
|
+
const inflight = state.inflight;
|
|
5759
|
+
return new Promise((resolve, reject) => {
|
|
5760
|
+
const abort = () => reject(signal.reason);
|
|
5761
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
5762
|
+
inflight.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
5763
|
+
if (signal.aborted) abort();
|
|
5764
|
+
});
|
|
4952
5765
|
}
|
|
4953
5766
|
async listModels(provider, opts) {
|
|
4954
5767
|
const catalog = await this.loadCatalog();
|
|
@@ -4999,6 +5812,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4999
5812
|
return allowanceTierForWeight(highest);
|
|
5000
5813
|
}
|
|
5001
5814
|
async resolveModel(provider, model, signal) {
|
|
5815
|
+
this.currentCatalogState();
|
|
5002
5816
|
const entry = this.catalog.find((m) => m.id === model) ?? (await this.loadCatalog(signal)).find((m) => m.id === model);
|
|
5003
5817
|
const efforts = KNOWN_EFFORTS[model];
|
|
5004
5818
|
const vision = KNOWN_IMAGE_MODELS.has(model);
|
|
@@ -5076,7 +5890,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5076
5890
|
async loadPoolBillingAccess() {
|
|
5077
5891
|
const keys = await this.poolAccountKeys();
|
|
5078
5892
|
if (keys.length === 0) return void 0;
|
|
5079
|
-
|
|
5893
|
+
const apiBase = this.deps.options().apiBase;
|
|
5894
|
+
return Promise.all(keys.map((key) => this.loadBillingAccessForKey(apiBase, key)));
|
|
5080
5895
|
}
|
|
5081
5896
|
/**
|
|
5082
5897
|
* The API keys the picker's plan filter must consult: every account the host
|
|
@@ -5100,21 +5915,22 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5100
5915
|
* shared across concurrent callers. `undefined` means "unknown — show
|
|
5101
5916
|
* everything" (fail-open).
|
|
5102
5917
|
*/
|
|
5103
|
-
async loadBillingAccessForKey(apiKey) {
|
|
5104
|
-
const
|
|
5918
|
+
async loadBillingAccessForKey(apiBase, apiKey) {
|
|
5919
|
+
const cacheKey = this.accountCacheKey(apiBase, apiKey);
|
|
5920
|
+
const cached = this.billingAccess.get(cacheKey);
|
|
5105
5921
|
if (cached !== void 0 && Date.now() - cached.at < 3e5) return cached.value;
|
|
5106
|
-
const existing = this.billingAccessInflight.get(
|
|
5922
|
+
const existing = this.billingAccessInflight.get(cacheKey);
|
|
5107
5923
|
if (existing !== void 0) return existing;
|
|
5108
|
-
const inflight = this.fetchBillingAccess(apiKey).then((value) => {
|
|
5109
|
-
this.billingAccess.set(
|
|
5924
|
+
const inflight = this.fetchBillingAccess(apiBase, apiKey).then((value) => {
|
|
5925
|
+
this.billingAccess.set(cacheKey, {
|
|
5110
5926
|
value,
|
|
5111
5927
|
at: Date.now()
|
|
5112
5928
|
});
|
|
5113
5929
|
return value;
|
|
5114
5930
|
}).finally(() => {
|
|
5115
|
-
this.billingAccessInflight.delete(
|
|
5931
|
+
this.billingAccessInflight.delete(cacheKey);
|
|
5116
5932
|
});
|
|
5117
|
-
this.billingAccessInflight.set(
|
|
5933
|
+
this.billingAccessInflight.set(cacheKey, inflight);
|
|
5118
5934
|
return inflight;
|
|
5119
5935
|
}
|
|
5120
5936
|
/**
|
|
@@ -5126,12 +5942,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5126
5942
|
* fallback (the CLI stamps plan identity from it too). Any failure resolves
|
|
5127
5943
|
* to `undefined` (fail-open) rather than breaking the picker.
|
|
5128
5944
|
*/
|
|
5129
|
-
async fetchBillingAccess(apiKey) {
|
|
5945
|
+
async fetchBillingAccess(apiBase, apiKey) {
|
|
5130
5946
|
try {
|
|
5131
|
-
const connection = this.deps.options();
|
|
5132
5947
|
const headers = await this.accountHeaders(apiKey);
|
|
5133
|
-
const
|
|
5134
|
-
const getJson = async (path) => (await this.fetchJson(`${base}${path}`, headers)).record;
|
|
5948
|
+
const getJson = async (path) => (await this.fetchJson(`${apiBase}${path}`, headers)).record;
|
|
5135
5949
|
const whoami = await getJson("/alpha/whoami");
|
|
5136
5950
|
const orgData = whoami && isRecord(whoami.org) ? whoami.org : void 0;
|
|
5137
5951
|
const orgId = orgData === void 0 ? void 0 : stringValue(orgData.id);
|
|
@@ -5153,50 +5967,65 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5153
5967
|
}
|
|
5154
5968
|
}
|
|
5155
5969
|
/** Fresh cached billing tier weight for a key, or undefined when not known. */
|
|
5156
|
-
cachedBillingTierWeight(apiKey) {
|
|
5157
|
-
const hit = this.billingAccess.get(apiKey);
|
|
5970
|
+
cachedBillingTierWeight(apiBase, apiKey) {
|
|
5971
|
+
const hit = this.billingAccess.get(this.accountCacheKey(apiBase, apiKey));
|
|
5158
5972
|
if (hit === void 0 || Date.now() - hit.at >= 3e5) return void 0;
|
|
5159
5973
|
return hit.value?.tierWeight;
|
|
5160
5974
|
}
|
|
5161
5975
|
/** Cached protocol decision for a key, or undefined when expired/unknown. */
|
|
5162
|
-
|
|
5163
|
-
const hit = this.protocolCache.get(apiKey);
|
|
5976
|
+
cachedProtocol(apiBase, apiKey) {
|
|
5977
|
+
const hit = this.protocolCache.get(this.accountCacheKey(apiBase, apiKey));
|
|
5164
5978
|
if (hit === void 0 || Date.now() - hit.at >= 9e5) return void 0;
|
|
5165
|
-
return hit.
|
|
5979
|
+
return hit.protocol;
|
|
5166
5980
|
}
|
|
5167
|
-
rememberProtocol(apiKey,
|
|
5168
|
-
this.protocolCache.set(apiKey, {
|
|
5169
|
-
|
|
5981
|
+
rememberProtocol(apiBase, apiKey, protocol) {
|
|
5982
|
+
this.protocolCache.set(this.accountCacheKey(apiBase, apiKey), {
|
|
5983
|
+
protocol,
|
|
5170
5984
|
at: Date.now()
|
|
5171
5985
|
});
|
|
5172
5986
|
}
|
|
5173
5987
|
/**
|
|
5174
|
-
*
|
|
5175
|
-
*
|
|
5176
|
-
*
|
|
5177
|
-
*
|
|
5988
|
+
* True when the catalog routes `model` to `/provider/v1/messages`.
|
|
5989
|
+
*
|
|
5990
|
+
* The catalog's own `supported_endpoints` is authoritative (it is what
|
|
5991
|
+
* upstream publishes per model); `requiresMessagesEndpoint()` — the
|
|
5992
|
+
* `claude-*` prefix rule — is the fallback for a catalog entry that carries
|
|
5993
|
+
* no route list, which is the shape every pre-1.55 cache file has.
|
|
5994
|
+
*/
|
|
5995
|
+
routesToMessages(model) {
|
|
5996
|
+
const entry = this.catalog.find((candidate) => candidate.id === model);
|
|
5997
|
+
if (entry !== void 0 && entry.supportedEndpoints.length > 0) return entry.supportedEndpoints.includes("/messages");
|
|
5998
|
+
return requiresMessagesEndpoint(model);
|
|
5999
|
+
}
|
|
6000
|
+
/**
|
|
6001
|
+
* Choose the initial protocol for one request.
|
|
5178
6002
|
*
|
|
5179
|
-
* Models the Provider API serves
|
|
5180
|
-
* Claude family
|
|
5181
|
-
*
|
|
5182
|
-
*
|
|
5183
|
-
*
|
|
5184
|
-
*
|
|
5185
|
-
* costs nothing.
|
|
6003
|
+
* Models the Provider API serves only through `/provider/v1/messages` (the
|
|
6004
|
+
* Claude family) take the Messages transport on any tier and even under a
|
|
6005
|
+
* forced `'openai'` preference: that option is documented as "prefer the
|
|
6006
|
+
* Provider API, still fall back", and the 400 these models answer with on
|
|
6007
|
+
* Chat Completions (`Model "<id>" must be called via /provider/v1/messages`)
|
|
6008
|
+
* is a hard refusal, not a preference to honour.
|
|
5186
6009
|
*
|
|
5187
6010
|
* That decision is deliberately NOT written to `protocolCache`, which is
|
|
5188
|
-
* keyed by API key
|
|
5189
|
-
*
|
|
6011
|
+
* keyed by gateway and API key: it depends on the model, not just the account, so
|
|
6012
|
+
* remembering it would pin the whole ACCOUNT to one transport and drag
|
|
6013
|
+
* every other model along (issue #46).
|
|
6014
|
+
*
|
|
6015
|
+
* Otherwise a fresh protocol cache entry wins, then a cached (not
|
|
6016
|
+
* network-fetched) billing tier of Go — the one plan without Provider API
|
|
6017
|
+
* access. Unknown accounts default to Chat Completions and fall back to the
|
|
6018
|
+
* CLI transport only after an `upgrade_required` rejection.
|
|
5190
6019
|
*/
|
|
5191
|
-
resolveProtocol(apiKey, model) {
|
|
6020
|
+
resolveProtocol(apiBase, apiKey, model) {
|
|
5192
6021
|
const forced = this.deps.options().protocol;
|
|
5193
6022
|
if (forced === "cli") return forced;
|
|
5194
|
-
if (
|
|
6023
|
+
if (this.routesToMessages(model)) return "messages";
|
|
5195
6024
|
if (forced === "openai") return forced;
|
|
5196
|
-
const cached = this.
|
|
5197
|
-
if (cached !== void 0) return cached
|
|
5198
|
-
if (this.cachedBillingTierWeight(apiKey) === GO_TIER_WEIGHT) {
|
|
5199
|
-
this.rememberProtocol(apiKey,
|
|
6025
|
+
const cached = this.cachedProtocol(apiBase, apiKey);
|
|
6026
|
+
if (cached !== void 0) return cached;
|
|
6027
|
+
if (this.cachedBillingTierWeight(apiBase, apiKey) === GO_TIER_WEIGHT) {
|
|
6028
|
+
this.rememberProtocol(apiBase, apiKey, "cli");
|
|
5200
6029
|
return "cli";
|
|
5201
6030
|
}
|
|
5202
6031
|
return "openai";
|
|
@@ -5336,6 +6165,59 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5336
6165
|
}
|
|
5337
6166
|
}
|
|
5338
6167
|
async *stream(options) {
|
|
6168
|
+
const timing = openRequestTiming(options.model, this.deps.onRequestTiming);
|
|
6169
|
+
let outcome = "cancelled";
|
|
6170
|
+
let errorCode;
|
|
6171
|
+
try {
|
|
6172
|
+
for await (const chunk of this.streamRequest(options, timing)) {
|
|
6173
|
+
if (chunk.type === "text-delta" || chunk.type === "reasoning-delta" || chunk.type === "tool-call-delta") timing.first("firstContentMs");
|
|
6174
|
+
if (chunk.type === "finish") outcome = "finished";
|
|
6175
|
+
yield chunk;
|
|
6176
|
+
}
|
|
6177
|
+
} catch (error) {
|
|
6178
|
+
outcome = options.signal?.aborted ? "aborted" : "error";
|
|
6179
|
+
errorCode = error instanceof LlmError ? error.code : "unknown";
|
|
6180
|
+
throw error;
|
|
6181
|
+
} finally {
|
|
6182
|
+
await timing.close(outcome, errorCode);
|
|
6183
|
+
}
|
|
6184
|
+
}
|
|
6185
|
+
/**
|
|
6186
|
+
* One turn, with at most one output-ceiling downshift.
|
|
6187
|
+
*
|
|
6188
|
+
* The ceiling a model accepts is not published anywhere the request can be
|
|
6189
|
+
* built from (see `modelOutputTokenLimit`), and the endpoint states it only
|
|
6190
|
+
* by refusing. That refusal is worth one retry rather than a failed turn: the
|
|
6191
|
+
* harness's own recovery for it — compact the context — cannot help, because
|
|
6192
|
+
* nothing about the context is wrong, and resending the identical request
|
|
6193
|
+
* would be refused identically (issue #71). Reading the stated ceiling off
|
|
6194
|
+
* the rejection turns one wasted turn into one, for good: it is recorded
|
|
6195
|
+
* process-wide, so the next request for this model is built correctly
|
|
6196
|
+
* without spending the round trip again.
|
|
6197
|
+
*
|
|
6198
|
+
* Only BEFORE the first chunk reaches the host. Once a delta has been
|
|
6199
|
+
* delivered the answer is already partly on screen, and a second request
|
|
6200
|
+
* would duplicate it — so from that point the error stands and says what is
|
|
6201
|
+
* actually wrong. One downshift per turn, and a second refusal surfaces.
|
|
6202
|
+
*/
|
|
6203
|
+
async *streamRequest(options, timing) {
|
|
6204
|
+
for (let attempt = 0;; attempt++) {
|
|
6205
|
+
const connection = this.deps.options();
|
|
6206
|
+
let delivered = false;
|
|
6207
|
+
try {
|
|
6208
|
+
for await (const chunk of this.streamAttempt(options, timing, connection)) {
|
|
6209
|
+
delivered = true;
|
|
6210
|
+
yield chunk;
|
|
6211
|
+
}
|
|
6212
|
+
return;
|
|
6213
|
+
} catch (error) {
|
|
6214
|
+
const ceiling = delivered ? void 0 : outputCeilingRefusal(connection.apiBase, options.model, error);
|
|
6215
|
+
if (attempt > 0 || ceiling === void 0) throw error;
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
6219
|
+
async *streamAttempt(options, timing, connection) {
|
|
6220
|
+
this.currentCatalogState(connection);
|
|
5339
6221
|
if (options.stop?.length) throw new LlmError("Command Code adapter does not support stop sequences", "UNSUPPORTED_OPTION");
|
|
5340
6222
|
const hasImages = options.messages.some(hasImageContent);
|
|
5341
6223
|
let readImage;
|
|
@@ -5343,12 +6225,17 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5343
6225
|
if (!KNOWN_IMAGE_MODELS.has(options.model)) throw new LlmError(`Command Code model "${options.model}" does not support image input; switch to a Vision-capable model (see the model registry)`, "UNSUPPORTED_CONTENT");
|
|
5344
6226
|
const attachments = this.resolveAttachments?.();
|
|
5345
6227
|
if (attachments === void 0) throw new LlmError("Command Code image input requires the durable attachment service", "UNSUPPORTED_CONTENT");
|
|
5346
|
-
readImage = (
|
|
6228
|
+
readImage = requestImages(attachments, options.signal, timing);
|
|
6229
|
+
}
|
|
6230
|
+
const finishCredentials = timing.phase("credentials");
|
|
6231
|
+
let apiKey;
|
|
6232
|
+
try {
|
|
6233
|
+
apiKey = await this.deps.resolveApiKey(connection, options.model);
|
|
6234
|
+
} finally {
|
|
6235
|
+
finishCredentials();
|
|
5347
6236
|
}
|
|
5348
|
-
const connection = this.deps.options();
|
|
5349
|
-
let apiKey = await this.deps.resolveApiKey(connection, options.model);
|
|
5350
6237
|
const modelEntry = this.catalog.find((m) => m.id === options.model);
|
|
5351
|
-
const modelMax = modelEntry?.maxTokens ??
|
|
6238
|
+
const modelMax = Math.min(modelEntry?.maxTokens ?? Infinity, modelOutputTokenLimit(connection.apiBase, options.model));
|
|
5352
6239
|
const requestedMaxTokens = Math.min(options.maxTokens ?? modelMax, modelMax, DEFAULT_GENERATE_MAX_TOKENS);
|
|
5353
6240
|
let maxTokens = requestedMaxTokens;
|
|
5354
6241
|
let contextBudget = {
|
|
@@ -5359,23 +6246,30 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5359
6246
|
const supported = KNOWN_EFFORTS[options.model];
|
|
5360
6247
|
const reasoningEffort = effort && effort !== "off" && supported?.includes(effort) ? effort : void 0;
|
|
5361
6248
|
const systemText = [options.system ?? "", ...options.messages.filter((m) => m.role === "system").map((m) => m.content.map(blockText).filter(Boolean).join("\n"))].filter(Boolean).join("\n\n");
|
|
5362
|
-
let protocol = this.resolveProtocol(apiKey, options.model);
|
|
6249
|
+
let protocol = this.resolveProtocol(connection.apiBase, apiKey, options.model);
|
|
5363
6250
|
let requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
|
|
5364
6251
|
const buildBody = async (target) => {
|
|
5365
|
-
const
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
6252
|
+
const finishBody = timing.phase("body");
|
|
6253
|
+
try {
|
|
6254
|
+
const facts = {
|
|
6255
|
+
maxTokens: requestedMaxTokens,
|
|
6256
|
+
reasoningEffort,
|
|
6257
|
+
selectableEffort: supported !== void 0 && supported.length > 0,
|
|
6258
|
+
systemText,
|
|
6259
|
+
readImage
|
|
6260
|
+
};
|
|
6261
|
+
const built = target === "cli" ? await buildCliBody(requestOptions, connection, facts) : target === "messages" ? await buildMessagesBody(requestOptions, facts) : await buildOpenAIBody(requestOptions, facts);
|
|
6262
|
+
contextBudget = requestContextBudget(target, built, requestOptions.messages, options.model, modelEntry?.contextWindow, requestedMaxTokens);
|
|
6263
|
+
maxTokens = contextBudget.maxTokens;
|
|
6264
|
+
if (target === "cli") recordOrEmpty(built.params).max_tokens = maxTokens;
|
|
6265
|
+
else built.max_tokens = maxTokens;
|
|
6266
|
+
return built;
|
|
6267
|
+
} finally {
|
|
6268
|
+
finishBody();
|
|
6269
|
+
}
|
|
5377
6270
|
};
|
|
5378
6271
|
let body = await buildBody(protocol);
|
|
6272
|
+
const serializedBodies = /* @__PURE__ */ new WeakMap();
|
|
5379
6273
|
const trace = openStreamTrace(`${options.provider}/${options.model}`);
|
|
5380
6274
|
const tried = /* @__PURE__ */ new Set();
|
|
5381
6275
|
let attemptNumber = 0;
|
|
@@ -5387,15 +6281,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5387
6281
|
options,
|
|
5388
6282
|
connection,
|
|
5389
6283
|
fetchImpl: this.fetchImpl,
|
|
6284
|
+
timing,
|
|
5390
6285
|
onResponse: (response) => {
|
|
5391
|
-
this.
|
|
6286
|
+
this.headerTimeoutStreaks.delete(options.sessionId ?? GLOBAL_HEADER_TIMEOUT_KEY);
|
|
5392
6287
|
responseMetadata = {
|
|
5393
6288
|
protocol,
|
|
5394
6289
|
headers: responseIdentifiers(response.headers)
|
|
5395
6290
|
};
|
|
5396
6291
|
trace.record("response", {
|
|
5397
6292
|
protocol,
|
|
5398
|
-
endpoint: protocol
|
|
6293
|
+
endpoint: protocolEndpoint(protocol, connection.apiBase),
|
|
5399
6294
|
status: response.status,
|
|
5400
6295
|
attempt: attemptNumber,
|
|
5401
6296
|
accountsTried: tried.size,
|
|
@@ -5419,15 +6314,26 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5419
6314
|
sessionId: options.sessionId,
|
|
5420
6315
|
...requestFingerprint(protocol, body)
|
|
5421
6316
|
});
|
|
5422
|
-
|
|
6317
|
+
let serialized = serializedBodies.get(body);
|
|
6318
|
+
if (serialized === void 0) {
|
|
6319
|
+
const finishSerialization = timing.phase("serialization");
|
|
6320
|
+
try {
|
|
6321
|
+
serialized = JSON.stringify(body, (_key, value) => sanitizeSurrogates(value));
|
|
6322
|
+
} finally {
|
|
6323
|
+
finishSerialization();
|
|
6324
|
+
}
|
|
6325
|
+
serializedBodies.set(body, serialized);
|
|
6326
|
+
}
|
|
6327
|
+
const attempt = await connectGenerate(connectDeps, apiKey, protocol, serialized);
|
|
5423
6328
|
if ("response" in attempt) {
|
|
5424
6329
|
connected = attempt;
|
|
5425
6330
|
break;
|
|
5426
6331
|
}
|
|
5427
6332
|
const providerError = parseProviderError(attempt.errText);
|
|
5428
|
-
if (protocol
|
|
6333
|
+
if (protocol !== "cli" && isUpgradeRequiredError(attempt.status, attempt.errText, providerError)) {
|
|
6334
|
+
const wasMessages = protocol === "messages";
|
|
5429
6335
|
protocol = "cli";
|
|
5430
|
-
this.rememberProtocol(apiKey,
|
|
6336
|
+
if (!wasMessages) this.rememberProtocol(connection.apiBase, apiKey, "cli");
|
|
5431
6337
|
requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true ? options.model : void 0);
|
|
5432
6338
|
body = await buildBody("cli");
|
|
5433
6339
|
continue;
|
|
@@ -5445,15 +6351,24 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5445
6351
|
});
|
|
5446
6352
|
if (next !== void 0 && !tried.has(next)) {
|
|
5447
6353
|
apiKey = next;
|
|
6354
|
+
const nextProtocol = this.resolveProtocol(connection.apiBase, apiKey, options.model);
|
|
6355
|
+
if (nextProtocol !== protocol) {
|
|
6356
|
+
protocol = nextProtocol;
|
|
6357
|
+
requestOptions = withSurfaceOffload(options, this.surfaceOffload, REQUEST_IMAGE_BUDGETS[0], connection.offloadSeenImagesForCache === true && protocol === "cli" ? options.model : void 0);
|
|
6358
|
+
body = await buildBody(protocol);
|
|
6359
|
+
}
|
|
5448
6360
|
continue;
|
|
5449
6361
|
}
|
|
5450
6362
|
}
|
|
5451
|
-
if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled")
|
|
6363
|
+
if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled")) {
|
|
5452
6364
|
const reset = rejection.resetAtMs;
|
|
5453
|
-
const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
|
|
6365
|
+
const wait = reset === void 0 ? attempt.retryAfterMs ?? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
|
|
5454
6366
|
const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
|
|
5455
6367
|
const window = rejection.reason === "rate-limit";
|
|
5456
|
-
throw bilingual("RATE_LIMIT", (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}`),
|
|
6368
|
+
throw bilingual(window ? "RATE_LIMIT" : THROTTLED_CODE, (window ? "llm-commandcode: the Command Code account is rate limited" : "llm-commandcode: the Command Code account is rate limited (429) — the provider did not report an exhausted usage window") + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`), (window ? "当前 Command Code 账户已被限流" : "当前 Command Code 账户被限流(429),服务商未报告用量窗口用尽") + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), {
|
|
6369
|
+
status: attempt.status,
|
|
6370
|
+
...wait > 0 && wait <= 9e5 ? { providerRetryAfterMs: wait } : {}
|
|
6371
|
+
});
|
|
5457
6372
|
}
|
|
5458
6373
|
throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs, providerError, contextBudget.sizePressure);
|
|
5459
6374
|
}
|
|
@@ -5464,7 +6379,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5464
6379
|
});
|
|
5465
6380
|
trace.close();
|
|
5466
6381
|
if (options.signal?.aborted) throw error;
|
|
5467
|
-
throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs), responseMetadata);
|
|
6382
|
+
throw failureWithRequestId(this.gradeHeaderTimeout(error, protocol, body, connection.requestTimeoutMs, options.sessionId), responseMetadata);
|
|
5468
6383
|
}
|
|
5469
6384
|
if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
|
|
5470
6385
|
const { response, cleanup } = connected;
|
|
@@ -5544,6 +6459,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5544
6459
|
break;
|
|
5545
6460
|
}
|
|
5546
6461
|
const text = decoder.decode(value, { stream: true });
|
|
6462
|
+
if (value.byteLength > 0) timing.first("firstByteMs");
|
|
5547
6463
|
buffer += text;
|
|
5548
6464
|
chunkCount += 1;
|
|
5549
6465
|
totalBytes += value.byteLength;
|
|
@@ -5581,9 +6497,13 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5581
6497
|
});
|
|
5582
6498
|
endRecorded = true;
|
|
5583
6499
|
if (failure !== void 0) throw failure;
|
|
5584
|
-
|
|
6500
|
+
const replayBlocks = asm.replayBlocks;
|
|
6501
|
+
yield hasResponseIdentifiers(responseMetadata) || replayBlocks.length > 0 ? {
|
|
5585
6502
|
...finish,
|
|
5586
|
-
replayState: {
|
|
6503
|
+
replayState: {
|
|
6504
|
+
response: responseMetadata,
|
|
6505
|
+
...replayBlocks.length > 0 ? { blocks: replayBlocks } : {}
|
|
6506
|
+
}
|
|
5587
6507
|
} : finish;
|
|
5588
6508
|
} else {
|
|
5589
6509
|
const elapsed = Date.now() - streamStartedAt;
|
|
@@ -5627,23 +6547,47 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
5627
6547
|
}
|
|
5628
6548
|
}
|
|
5629
6549
|
};
|
|
6550
|
+
/**
|
|
6551
|
+
* Unwrap one level of a double-encoded provider message.
|
|
6552
|
+
*
|
|
6553
|
+
* The Messages endpoint sometimes reports a rejection with `error.message`
|
|
6554
|
+
* holding a whole JSON document rather than the sentence
|
|
6555
|
+
* (`{"type":"error","error":{"type":"invalid_request_error","message":"{\"type\":
|
|
6556
|
+
* \"error\",…}"},"request_id":"req_…"}`, measured 2026-09-29). Showing that blob
|
|
6557
|
+
* to the user hides the actual refusal, so one nesting level is peeled; the
|
|
6558
|
+
* outer text is returned unchanged when it is not JSON or carries no message.
|
|
6559
|
+
*/
|
|
6560
|
+
function unwrapProviderMessage(raw) {
|
|
6561
|
+
if (raw === void 0) return void 0;
|
|
6562
|
+
const trimmed = raw.trim();
|
|
6563
|
+
if (!trimmed.startsWith("{")) return raw;
|
|
6564
|
+
try {
|
|
6565
|
+
const inner = JSON.parse(trimmed);
|
|
6566
|
+
if (!isRecord(inner)) return raw;
|
|
6567
|
+
return stringValue(((isRecord(inner.error) ? inner.error : void 0) ?? inner).message) ?? raw;
|
|
6568
|
+
} catch {
|
|
6569
|
+
return raw;
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
5630
6572
|
/** Parse a rejected body once, retaining the nested/root distinction. */
|
|
5631
6573
|
function parseProviderError(errText) {
|
|
5632
6574
|
try {
|
|
5633
6575
|
const parsed = JSON.parse(errText);
|
|
5634
|
-
if (!isRecord(parsed)) return {};
|
|
6576
|
+
if (!isRecord(parsed)) return { message: errText };
|
|
5635
6577
|
const nested = isRecord(parsed.error) ? parsed.error : void 0;
|
|
5636
6578
|
const body = nested ?? parsed;
|
|
6579
|
+
const rawMessage = stringValue(body.message);
|
|
5637
6580
|
return {
|
|
5638
6581
|
root: parsed,
|
|
5639
6582
|
nested,
|
|
5640
6583
|
code: stringValue(body.code),
|
|
5641
6584
|
type: stringValue(body.type),
|
|
5642
|
-
|
|
6585
|
+
rawMessage,
|
|
6586
|
+
message: unwrapProviderMessage(rawMessage) ?? errText,
|
|
5643
6587
|
rateLimit: isRecord(body.rateLimit) ? body.rateLimit : void 0
|
|
5644
6588
|
};
|
|
5645
6589
|
} catch {
|
|
5646
|
-
return {};
|
|
6590
|
+
return { message: errText };
|
|
5647
6591
|
}
|
|
5648
6592
|
}
|
|
5649
6593
|
function generateHttpError(status, errText, retryAfterMs, parsed = parseProviderError(errText), sizePressure = false) {
|
|
@@ -5651,10 +6595,14 @@ function generateHttpError(status, errText, retryAfterMs, parsed = parseProvider
|
|
|
5651
6595
|
const providerDetail = parsed.nested === void 0 ? "" : [
|
|
5652
6596
|
parsed.code,
|
|
5653
6597
|
parsed.type,
|
|
5654
|
-
parsed.message
|
|
6598
|
+
parsed.rawMessage ?? parsed.message
|
|
5655
6599
|
].filter((part) => part !== void 0 && part !== "").join(" ");
|
|
5656
6600
|
const detail = providerCode ?? `HTTP ${status}`;
|
|
5657
6601
|
const overflowDetail = providerDetail !== "" ? providerDetail : errText.slice(0, 500);
|
|
6602
|
+
if (status < 500 && OUTPUT_LIMIT_REJECTION.test(overflowDetail)) {
|
|
6603
|
+
const stated = /max_tokens:\s*\d+\s*>\s*\d+/.exec(overflowDetail)?.[0] ?? (parsed.message !== void 0 && parsed.message.length <= 200 ? parsed.message : void 0);
|
|
6604
|
+
return bilingual("PROVIDER_HTTP_ERROR", `Command Code API error ${status}: this request's max_tokens is not valid for this model` + (stated ? ` (${stated})` : "") + " — the session is not oversized, so compacting it will not help; lower \"Max output tokens\" or pick a model with a larger output ceiling", `Command Code API 返回 ${status}:本次请求的 max_tokens 对该模型不合法` + (stated ? `(${stated})` : "") + "——会话内容并未超长,压缩上下文无效;请调低「最大输出 token」或改用输出上限更大的模型", { status });
|
|
6605
|
+
}
|
|
5658
6606
|
if (status < 500 && isContextOverflowDetail(overflowDetail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, `Command Code API error ${status}: the request exceeds the model's context window — this session is being compacted and the request retried`, `Command Code API 返回 ${status}:请求内容超出模型上下文窗口——正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, { status });
|
|
5659
6607
|
if (status === 400 && sizePressure && AMBIGUOUS_SIZE_REJECTION.test(providerDetail) && /\bprovider_error\b/i.test(providerDetail)) return bilingual(CONTEXT_WINDOW_EXCEEDED_CODE, "Command Code API error 400: the provider could not complete a request near the model context limit — compacting the session and retrying", "Command Code API 返回 400:请求接近模型上下文上限,服务商无法完成——正在压缩上下文后重试", { status });
|
|
5660
6608
|
if (status === 422 && isZdrNoProviders(providerCode, providerDetail, errText)) return bilingual("PROVIDER_HTTP_ERROR", `Command Code API error 422 (${detail}): zero data retention is enforced but no ZDR-capable upstream is available for this model — turn the ZDR setting off (or use another model)`, "Command Code API 返回 422:已开启零数据保留(ZDR),但该模型当前没有可用的 ZDR 上游——请关闭 ZDR 开关,或改用其他模型", { status: 422 });
|
|
@@ -5678,7 +6626,7 @@ function generateHttpError(status, errText, retryAfterMs, parsed = parseProvider
|
|
|
5678
6626
|
* rejection repeats identically on every attempt.
|
|
5679
6627
|
*/
|
|
5680
6628
|
function httpErrorCode(status) {
|
|
5681
|
-
if (status === 429) return
|
|
6629
|
+
if (status === 429) return THROTTLED_CODE;
|
|
5682
6630
|
if (status === 408) return "TIMEOUT";
|
|
5683
6631
|
if (status >= 500) return "SERVER";
|
|
5684
6632
|
return "PROVIDER_HTTP_ERROR";
|
|
@@ -5898,8 +6846,195 @@ function handleOpenAIEvent(asm, event) {
|
|
|
5898
6846
|
return chunks;
|
|
5899
6847
|
}
|
|
5900
6848
|
/** Dispatch one parsed stream event to the active transport's handler. */
|
|
6849
|
+
/**
|
|
6850
|
+
* Messages `usage` → the harness meter.
|
|
6851
|
+
*
|
|
6852
|
+
* The field names are the Anthropic ones, and unlike Chat Completions
|
|
6853
|
+
* `input_tokens` already excludes cache reads and writes, so it is mapped
|
|
6854
|
+
* as-is rather than reduced (the shape `dsh-llm-deepseek` implements).
|
|
6855
|
+
* Measured 2026-09-29: `{ input_tokens, cache_creation_input_tokens,
|
|
6856
|
+
* cache_read_input_tokens, output_tokens, output_tokens_details:
|
|
6857
|
+
* { thinking_tokens } }`, plus gateway additions the meter ignores.
|
|
6858
|
+
*/
|
|
6859
|
+
function mapMessagesUsage(raw) {
|
|
6860
|
+
if (!isRecord(raw)) return void 0;
|
|
6861
|
+
const inputTokens = numberValue(raw.input_tokens) ?? 0;
|
|
6862
|
+
const outputTokens = numberValue(raw.output_tokens) ?? 0;
|
|
6863
|
+
const cacheRead = numberValue(raw.cache_read_input_tokens) ?? 0;
|
|
6864
|
+
const cacheWrite = numberValue(raw.cache_creation_input_tokens) ?? 0;
|
|
6865
|
+
const reasoningTokens = numberValue((isRecord(raw.output_tokens_details) ? raw.output_tokens_details : void 0)?.thinking_tokens);
|
|
6866
|
+
const usage = {
|
|
6867
|
+
inputTokens: Math.max(0, inputTokens),
|
|
6868
|
+
outputTokens,
|
|
6869
|
+
cacheReadTokens: cacheRead
|
|
6870
|
+
};
|
|
6871
|
+
if (cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
|
|
6872
|
+
if (reasoningTokens !== void 0) usage.reasoningTokens = reasoningTokens;
|
|
6873
|
+
return usage;
|
|
6874
|
+
}
|
|
6875
|
+
/** The harness block type one Messages content block maps to. */
|
|
6876
|
+
function messagesBlockType(native) {
|
|
6877
|
+
if (native.type === "text") return "text";
|
|
6878
|
+
if (native.type === "thinking") return "reasoning";
|
|
6879
|
+
if (native.type === "tool_use") return "tool-call";
|
|
6880
|
+
}
|
|
6881
|
+
/**
|
|
6882
|
+
* Handle one Messages-transport stream event, appending harness StreamChunks.
|
|
6883
|
+
*
|
|
6884
|
+
* The event sequence is the Anthropic one measured on 2026-09-29:
|
|
6885
|
+
* `message_start`, then per content block `content_block_start` /
|
|
6886
|
+
* `content_block_delta`* / `content_block_stop`, then `message_delta`
|
|
6887
|
+
* (carrying `stop_reason` and the final usage) and `message_stop`. A `ping`
|
|
6888
|
+
* keepalive interleaves freely and carries no content. The terminal chunks are
|
|
6889
|
+
* NOT emitted here: `stream()` holds the success finish until it has validated
|
|
6890
|
+
* the assembled answer, and it attaches the replay envelope.
|
|
6891
|
+
*/
|
|
6892
|
+
function handleMessagesEvent(asm, event) {
|
|
6893
|
+
const chunks = [];
|
|
6894
|
+
if (!isRecord(event)) return chunks;
|
|
6895
|
+
switch (event.type) {
|
|
6896
|
+
case "message_start": {
|
|
6897
|
+
const usage = mapMessagesUsage((isRecord(event.message) ? event.message : void 0)?.usage);
|
|
6898
|
+
if (usage) chunks.push({
|
|
6899
|
+
type: "usage",
|
|
6900
|
+
usage
|
|
6901
|
+
});
|
|
6902
|
+
break;
|
|
6903
|
+
}
|
|
6904
|
+
case "content_block_start": {
|
|
6905
|
+
const wireIndex = numberValue(event.index);
|
|
6906
|
+
const native = isRecord(event.content_block) ? event.content_block : void 0;
|
|
6907
|
+
const type = native ? messagesBlockType(native) : void 0;
|
|
6908
|
+
if (wireIndex === void 0 || type === void 0) break;
|
|
6909
|
+
const chunkIndex = asm.nextIndex++;
|
|
6910
|
+
const state = {
|
|
6911
|
+
type,
|
|
6912
|
+
text: stringValue(native?.text) ?? "",
|
|
6913
|
+
signature: stringValue(native?.signature) ?? "",
|
|
6914
|
+
json: "",
|
|
6915
|
+
id: stringValue(native?.id) ?? "",
|
|
6916
|
+
name: stringValue(native?.name) ?? "",
|
|
6917
|
+
chunkIndex
|
|
6918
|
+
};
|
|
6919
|
+
asm.messagesBlocks.set(wireIndex, state);
|
|
6920
|
+
asm.replayBlocks[chunkIndex] = { type };
|
|
6921
|
+
chunks.push({
|
|
6922
|
+
type: "block-start",
|
|
6923
|
+
index: chunkIndex,
|
|
6924
|
+
blockType: type
|
|
6925
|
+
});
|
|
6926
|
+
if (type === "tool-call" && state.id !== "") chunks.push({
|
|
6927
|
+
type: "tool-call-delta",
|
|
6928
|
+
index: chunkIndex,
|
|
6929
|
+
id: ToolCallId(state.id),
|
|
6930
|
+
name: state.name,
|
|
6931
|
+
argumentsDelta: ""
|
|
6932
|
+
});
|
|
6933
|
+
break;
|
|
6934
|
+
}
|
|
6935
|
+
case "content_block_delta": {
|
|
6936
|
+
const wireIndex = numberValue(event.index);
|
|
6937
|
+
const state = wireIndex === void 0 ? void 0 : asm.messagesBlocks.get(wireIndex);
|
|
6938
|
+
const delta = isRecord(event.delta) ? event.delta : void 0;
|
|
6939
|
+
if (!state || !delta) break;
|
|
6940
|
+
const index = state.chunkIndex;
|
|
6941
|
+
if (delta.type === "text_delta" && state.type === "text") {
|
|
6942
|
+
const text = stringValue(delta.text) ?? "";
|
|
6943
|
+
state.text += text;
|
|
6944
|
+
if (text.trim() !== "") asm.sawContent = true;
|
|
6945
|
+
chunks.push({
|
|
6946
|
+
type: "text-delta",
|
|
6947
|
+
index,
|
|
6948
|
+
text
|
|
6949
|
+
});
|
|
6950
|
+
} else if (delta.type === "thinking_delta" && state.type === "reasoning") {
|
|
6951
|
+
const text = stringValue(delta.thinking) ?? "";
|
|
6952
|
+
state.text += text;
|
|
6953
|
+
chunks.push({
|
|
6954
|
+
type: "reasoning-delta",
|
|
6955
|
+
index,
|
|
6956
|
+
text
|
|
6957
|
+
});
|
|
6958
|
+
} else if (delta.type === "signature_delta" && state.type === "reasoning") {
|
|
6959
|
+
state.signature += stringValue(delta.signature) ?? "";
|
|
6960
|
+
const entry = asm.replayBlocks[index];
|
|
6961
|
+
if (entry) entry.signature = state.signature;
|
|
6962
|
+
} else if (delta.type === "input_json_delta" && state.type === "tool-call") {
|
|
6963
|
+
const fragment = stringValue(delta.partial_json) ?? "";
|
|
6964
|
+
state.json += fragment;
|
|
6965
|
+
chunks.push({
|
|
6966
|
+
type: "tool-call-delta",
|
|
6967
|
+
index,
|
|
6968
|
+
id: ToolCallId(state.id),
|
|
6969
|
+
name: state.name,
|
|
6970
|
+
argumentsDelta: fragment
|
|
6971
|
+
});
|
|
6972
|
+
}
|
|
6973
|
+
break;
|
|
6974
|
+
}
|
|
6975
|
+
case "content_block_stop": {
|
|
6976
|
+
const wireIndex = numberValue(event.index);
|
|
6977
|
+
if (wireIndex === void 0) break;
|
|
6978
|
+
const state = asm.messagesBlocks.get(wireIndex);
|
|
6979
|
+
if (!state) break;
|
|
6980
|
+
asm.messagesBlocks.delete(wireIndex);
|
|
6981
|
+
const index = state.chunkIndex;
|
|
6982
|
+
if (state.type === "text") {
|
|
6983
|
+
if (state.text.trim() !== "") asm.sawContent = true;
|
|
6984
|
+
chunks.push({
|
|
6985
|
+
type: "block-end",
|
|
6986
|
+
index,
|
|
6987
|
+
block: {
|
|
6988
|
+
type: "text",
|
|
6989
|
+
text: state.text
|
|
6990
|
+
}
|
|
6991
|
+
});
|
|
6992
|
+
} else if (state.type === "reasoning") chunks.push({
|
|
6993
|
+
type: "block-end",
|
|
6994
|
+
index,
|
|
6995
|
+
block: {
|
|
6996
|
+
type: "reasoning",
|
|
6997
|
+
text: state.text
|
|
6998
|
+
}
|
|
6999
|
+
});
|
|
7000
|
+
else {
|
|
7001
|
+
if (state.id !== "") asm.sawContent = true;
|
|
7002
|
+
chunks.push({
|
|
7003
|
+
type: "block-end",
|
|
7004
|
+
index,
|
|
7005
|
+
block: {
|
|
7006
|
+
type: "tool-call",
|
|
7007
|
+
id: ToolCallId(state.id),
|
|
7008
|
+
name: state.name,
|
|
7009
|
+
arguments: state.json || "{}"
|
|
7010
|
+
}
|
|
7011
|
+
});
|
|
7012
|
+
}
|
|
7013
|
+
break;
|
|
7014
|
+
}
|
|
7015
|
+
case "message_delta": {
|
|
7016
|
+
const reason = stringValue((isRecord(event.delta) ? event.delta : void 0)?.stop_reason);
|
|
7017
|
+
if (reason !== void 0) asm.finishReason = reason;
|
|
7018
|
+
const usage = mapMessagesUsage(event.usage);
|
|
7019
|
+
if (usage) chunks.push({
|
|
7020
|
+
type: "usage",
|
|
7021
|
+
usage
|
|
7022
|
+
});
|
|
7023
|
+
chunks.push({
|
|
7024
|
+
type: "finish",
|
|
7025
|
+
reason: mapFinishReason(reason)
|
|
7026
|
+
});
|
|
7027
|
+
break;
|
|
7028
|
+
}
|
|
7029
|
+
case "message_stop":
|
|
7030
|
+
case "ping": break;
|
|
7031
|
+
case "error": throw streamErrorToLlmError(event.error ?? event, stringValue(event.message));
|
|
7032
|
+
}
|
|
7033
|
+
return chunks;
|
|
7034
|
+
}
|
|
5901
7035
|
function handleEvent(asm, protocol, event) {
|
|
5902
|
-
|
|
7036
|
+
if (protocol === "cli") return handleCliEvent(asm, event);
|
|
7037
|
+
return protocol === "messages" ? handleMessagesEvent(asm, event) : handleOpenAIEvent(asm, event);
|
|
5903
7038
|
}
|
|
5904
7039
|
/**
|
|
5905
7040
|
* Parse an HTTP `Retry-After` value (delay-seconds or an HTTP-date) into
|
|
@@ -5928,7 +7063,7 @@ function parseRetryAfterMs(value, now = Date.now()) {
|
|
|
5928
7063
|
* one function serves both — a new reason cannot drift between them.
|
|
5929
7064
|
*/
|
|
5930
7065
|
function mapFinishReason(reason) {
|
|
5931
|
-
if (reason === "tool-calls" || reason === "tool_calls") return { kind: "tool-calls" };
|
|
7066
|
+
if (reason === "tool-calls" || reason === "tool_calls" || reason === "tool_use") return { kind: "tool-calls" };
|
|
5932
7067
|
if (reason === "length" || reason === "max_tokens" || reason === "max-tokens" || reason === "max_output_tokens") return { kind: "max-tokens" };
|
|
5933
7068
|
return { kind: "stop" };
|
|
5934
7069
|
}
|
|
@@ -7886,7 +9021,7 @@ function transportBudgetMessage(failureMessage, maxRetries) {
|
|
|
7886
9021
|
* Mark one schema field volatile.
|
|
7887
9022
|
*
|
|
7888
9023
|
* `.volatile()` is unconditional: it is a schemastery 3.18.3 feature and the
|
|
7889
|
-
* only supported engine (`dsh 0.2.0-rc.
|
|
9024
|
+
* only supported engine (`dsh 0.2.0-rc.2`) pins `~3.18.4`, so the method is
|
|
7890
9025
|
* always present. It is reached through a structural member so the caller's
|
|
7891
9026
|
* declared field type survives — the mark changes what the schema PARSES to (a
|
|
7892
9027
|
* live reference), not the plugin-facing `Config` shape.
|
|
@@ -8120,10 +9255,13 @@ function apply(ctx, config) {
|
|
|
8120
9255
|
return Math.min(Math.trunc(value), 50);
|
|
8121
9256
|
};
|
|
8122
9257
|
const sessionAgents = /* @__PURE__ */ new WeakMap();
|
|
8123
|
-
ctx.on("agent/request-error", async ({ agent, failure, signal }, next) => {
|
|
8124
|
-
if (signal.aborted) return next();
|
|
9258
|
+
ctx.on("agent/request-error", async ({ agent, provider, failure, signal }, next) => {
|
|
9259
|
+
if (signal.aborted || provider !== "commandcode") return next();
|
|
8125
9260
|
const session = Reflect.get(agent, "session");
|
|
8126
9261
|
if (typeof session === "object" && session !== null) sessionAgents.set(session, agent);
|
|
9262
|
+
const transient = absorbTransientFailure(agent, failure.code);
|
|
9263
|
+
if (transient === "exhausted") throw new Error(transientBudgetMessage(failure.code, failure.message));
|
|
9264
|
+
if (transient === "retry") return next();
|
|
8127
9265
|
const decision = absorbTransportFailure(agent, failure.code, transportMaxRetries());
|
|
8128
9266
|
if (decision === "ignored") return next();
|
|
8129
9267
|
if (decision === "retry") return { kind: "retry" };
|
|
@@ -8139,10 +9277,16 @@ function apply(ctx, config) {
|
|
|
8139
9277
|
}
|
|
8140
9278
|
if (action !== "reset") return;
|
|
8141
9279
|
const agent = sessionAgents.get(session);
|
|
8142
|
-
if (agent !== void 0)
|
|
9280
|
+
if (agent !== void 0) {
|
|
9281
|
+
resetTransportFailures(agent);
|
|
9282
|
+
resetTransientFailures(agent);
|
|
9283
|
+
}
|
|
8143
9284
|
});
|
|
8144
9285
|
ctx.on("agent/status", ({ agent, status }) => {
|
|
8145
|
-
if (status === "idle")
|
|
9286
|
+
if (status === "idle") {
|
|
9287
|
+
resetTransportFailures(agent);
|
|
9288
|
+
resetTransientFailures(agent);
|
|
9289
|
+
}
|
|
8146
9290
|
});
|
|
8147
9291
|
const usageReports = async () => {
|
|
8148
9292
|
const described = await pool.describeAccounts();
|