@mars-sea/dsh-commandcode-provider 0.11.7 → 0.11.9
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 +45 -0
- package/icon.svg +5 -0
- package/lib/client.js +470 -22
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +99 -7
- package/lib/index.js +786 -185
- package/lib/index.js.map +1 -1
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +43 -38
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { homedir, tmpdir } from "node:os";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
@@ -6,7 +6,7 @@ import * as dshLlm from "@deepseek-ai/dsh-llm";
|
|
|
6
6
|
import { CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, assertUsableApiKey, attributionHeaders, errorChain, isContextWindowExceededError, offloadedImageText, resolveRetryPolicy, textOnlyImageText } from "@deepseek-ai/dsh-llm";
|
|
7
7
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
8
8
|
import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
|
|
9
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
10
10
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
11
11
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
12
12
|
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
@@ -203,6 +203,11 @@ const KNOWN_EFFORTS = {
|
|
|
203
203
|
"high"
|
|
204
204
|
],
|
|
205
205
|
"sakana/fugu-ultra": ["high", "xhigh"],
|
|
206
|
+
"stepfun/Step-5-Preview": [
|
|
207
|
+
"low",
|
|
208
|
+
"medium",
|
|
209
|
+
"high"
|
|
210
|
+
],
|
|
206
211
|
"tencent/hy4-preview": [
|
|
207
212
|
"low",
|
|
208
213
|
"medium",
|
|
@@ -219,6 +224,12 @@ const KNOWN_EFFORTS = {
|
|
|
219
224
|
"high",
|
|
220
225
|
"xhigh"
|
|
221
226
|
],
|
|
227
|
+
"xai/grok-4.7": [
|
|
228
|
+
"low",
|
|
229
|
+
"medium",
|
|
230
|
+
"high",
|
|
231
|
+
"xhigh"
|
|
232
|
+
],
|
|
222
233
|
"z-ai/glm-5.3-flash": [
|
|
223
234
|
"low",
|
|
224
235
|
"high",
|
|
@@ -340,11 +351,16 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
340
351
|
"moonshotai/Kimi-K3",
|
|
341
352
|
"sakana/fugu-ultra",
|
|
342
353
|
"stepfun/Step-3.7-Flash",
|
|
354
|
+
"stepfun/Step-5-Preview",
|
|
343
355
|
"thinkingmachines/inkling",
|
|
344
356
|
"thinkingmachines/inkling-small",
|
|
345
357
|
"xai/grok-4.5",
|
|
346
358
|
"xai/grok-4.6",
|
|
359
|
+
"xai/grok-4.7",
|
|
347
360
|
"xiaomi/mimo-v2.5",
|
|
361
|
+
"xiaomi/mimo-v2.6-flash",
|
|
362
|
+
"xiaomi/mimo-v2.6-pro",
|
|
363
|
+
"xiaomi/mimo-v2.6-pro-ultraspeed",
|
|
348
364
|
"z-ai/glm-5.3-flash",
|
|
349
365
|
"z-ai/glm-5.3-flashx"
|
|
350
366
|
]);
|
|
@@ -458,7 +474,11 @@ function requiresMessagesEndpoint(modelId) {
|
|
|
458
474
|
* free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
|
|
459
475
|
* added `deepseek/deepseek-v4.1-flash` (Go); command-code@1.56.0 added
|
|
460
476
|
* `Qwen/Qwen3.8-Omni-Flash` (Go); command-code@1.57.0 added
|
|
461
|
-
* `z-ai/glm-5.3-flashx` (Go).
|
|
477
|
+
* `z-ai/glm-5.3-flashx` (Go); command-code@1.59.0 added `xai/grok-4.7` (GOAT)
|
|
478
|
+
* and 1.60.0 added `stepfun/Step-5-Preview` (Go); command-code@1.62.0 added the
|
|
479
|
+
* MiMo V2.6 family — `xiaomi/mimo-v2.6-flash` + `xiaomi/mimo-v2.6-pro` (Go) and
|
|
480
|
+
* `xiaomi/mimo-v2.6-pro-ultraspeed` (GOAT) — so the 1.58.0 -> 1.62.0 window's
|
|
481
|
+
* only tier changes are additions and the superset chain still holds.
|
|
462
482
|
*
|
|
463
483
|
* The Provider API exposes no plan metadata, so this snapshot is the source of
|
|
464
484
|
* truth for the picker's plan annotation — it answers "which plan do I need to
|
|
@@ -501,6 +521,7 @@ const KNOWN_PLANS = {
|
|
|
501
521
|
"poolside/laguna-s-2.1-free": "go",
|
|
502
522
|
"stepfun/Step-3.5-Flash": "go",
|
|
503
523
|
"stepfun/Step-3.7-Flash": "go",
|
|
524
|
+
"stepfun/Step-5-Preview": "go",
|
|
504
525
|
"tencent/hy3-paid": "go",
|
|
505
526
|
"tencent/hy4-preview": "go",
|
|
506
527
|
"thinkingmachines/inkling": "go",
|
|
@@ -508,6 +529,8 @@ const KNOWN_PLANS = {
|
|
|
508
529
|
"xai/grok-4.5": "go",
|
|
509
530
|
"xiaomi/mimo-v2.5": "go",
|
|
510
531
|
"xiaomi/mimo-v2.5-pro": "go",
|
|
532
|
+
"xiaomi/mimo-v2.6-flash": "go",
|
|
533
|
+
"xiaomi/mimo-v2.6-pro": "go",
|
|
511
534
|
"z-ai/glm-5.3-flash": "go",
|
|
512
535
|
"z-ai/glm-5.3-flashx": "go",
|
|
513
536
|
"zai-org/GLM-5": "go",
|
|
@@ -521,6 +544,8 @@ const KNOWN_PLANS = {
|
|
|
521
544
|
"meta/muse-spark-1.2": "goat",
|
|
522
545
|
"meta/muse-spark-1.3": "goat",
|
|
523
546
|
"xai/grok-4.6": "goat",
|
|
547
|
+
"xai/grok-4.7": "goat",
|
|
548
|
+
"xiaomi/mimo-v2.6-pro-ultraspeed": "goat",
|
|
524
549
|
"claude-haiku-4-5-20251001": "pro",
|
|
525
550
|
"claude-sonnet-4-6": "pro",
|
|
526
551
|
"claude-sonnet-5": "pro",
|
|
@@ -691,6 +716,10 @@ const KNOWN_DEALS = {
|
|
|
691
716
|
"MiniMaxAI/MiniMax-M3": { label: "50% off" },
|
|
692
717
|
"xiaomi/mimo-v2.5-pro": { label: "99% off" },
|
|
693
718
|
"xiaomi/mimo-v2.5": { label: "98% off" },
|
|
719
|
+
"xai/grok-4.7": {
|
|
720
|
+
label: "40% off",
|
|
721
|
+
expiresAt: "2026-09-27T23:59:59.999Z"
|
|
722
|
+
},
|
|
694
723
|
"poolside/laguna-s-2.1-free": {
|
|
695
724
|
label: "FREE",
|
|
696
725
|
free: true
|
|
@@ -1273,6 +1302,26 @@ const MODEL_PRICE_ROWS = [
|
|
|
1273
1302
|
1
|
|
1274
1303
|
] }]
|
|
1275
1304
|
},
|
|
1305
|
+
{
|
|
1306
|
+
id: "grok-4.7",
|
|
1307
|
+
rates: [
|
|
1308
|
+
1.2,
|
|
1309
|
+
3.6,
|
|
1310
|
+
.3
|
|
1311
|
+
],
|
|
1312
|
+
contextTiers: [{
|
|
1313
|
+
"maxContext": 2e5,
|
|
1314
|
+
"rates": [
|
|
1315
|
+
1.2,
|
|
1316
|
+
3.6,
|
|
1317
|
+
.3
|
|
1318
|
+
]
|
|
1319
|
+
}, { "rates": [
|
|
1320
|
+
2.4,
|
|
1321
|
+
7.2,
|
|
1322
|
+
.6
|
|
1323
|
+
] }]
|
|
1324
|
+
},
|
|
1276
1325
|
{
|
|
1277
1326
|
id: "inkling",
|
|
1278
1327
|
rates: [
|
|
@@ -1353,6 +1402,30 @@ const MODEL_PRICE_ROWS = [
|
|
|
1353
1402
|
.0036
|
|
1354
1403
|
]
|
|
1355
1404
|
},
|
|
1405
|
+
{
|
|
1406
|
+
id: "mimo-v2.6-flash",
|
|
1407
|
+
rates: [
|
|
1408
|
+
.14,
|
|
1409
|
+
.28,
|
|
1410
|
+
.0028
|
|
1411
|
+
]
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
id: "mimo-v2.6-pro",
|
|
1415
|
+
rates: [
|
|
1416
|
+
.435,
|
|
1417
|
+
.87,
|
|
1418
|
+
.0036
|
|
1419
|
+
]
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
id: "mimo-v2.6-pro-ultraspeed",
|
|
1423
|
+
rates: [
|
|
1424
|
+
4.35,
|
|
1425
|
+
8.7,
|
|
1426
|
+
.036
|
|
1427
|
+
]
|
|
1428
|
+
},
|
|
1356
1429
|
{
|
|
1357
1430
|
id: "minimax-m2.5",
|
|
1358
1431
|
rates: [
|
|
@@ -1578,6 +1651,14 @@ const MODEL_PRICE_ROWS = [
|
|
|
1578
1651
|
.04
|
|
1579
1652
|
]
|
|
1580
1653
|
},
|
|
1654
|
+
{
|
|
1655
|
+
id: "step-5-preview",
|
|
1656
|
+
rates: [
|
|
1657
|
+
1,
|
|
1658
|
+
2.7,
|
|
1659
|
+
.05
|
|
1660
|
+
]
|
|
1661
|
+
},
|
|
1581
1662
|
{
|
|
1582
1663
|
id: "tencent/hy3-paid",
|
|
1583
1664
|
rates: [
|
|
@@ -2571,6 +2652,181 @@ function scaleBy(size, factor) {
|
|
|
2571
2652
|
};
|
|
2572
2653
|
}
|
|
2573
2654
|
//#endregion
|
|
2655
|
+
//#region src/stream-trace.ts
|
|
2656
|
+
/**
|
|
2657
|
+
* Opt-in raw-stream trace for the Command Code transports.
|
|
2658
|
+
*
|
|
2659
|
+
* Why this exists: this route's failure mode of record is a response that ENDS
|
|
2660
|
+
* EARLY. The gateway, an intermediary proxy, or the network closes the socket
|
|
2661
|
+
* mid-generation, so the adapter's read loop sees `done` without ever seeing
|
|
2662
|
+
* the transport's terminal event. From inside the adapter those two endings are
|
|
2663
|
+
* indistinguishable — the provider cut the stream, a proxy timed out, or the
|
|
2664
|
+
* socket was dropped — and the only artifact that separates them is the raw
|
|
2665
|
+
* wire: which events actually arrived, when the last one landed, how long the
|
|
2666
|
+
* socket then sat silent, and how it ended. Without that, a truncated answer is
|
|
2667
|
+
* a report with no evidence attached.
|
|
2668
|
+
*
|
|
2669
|
+
* Enable it per process:
|
|
2670
|
+
*
|
|
2671
|
+
* ```sh
|
|
2672
|
+
* DSH_COMMANDCODE_TRACE=/tmp/cc-stream.jsonl dsh web
|
|
2673
|
+
* DSH_COMMANDCODE_TRACE=1 dsh web # -> $TMPDIR/dsh-commandcode-stream.jsonl
|
|
2674
|
+
* ```
|
|
2675
|
+
*
|
|
2676
|
+
* Any value that is not a flag is a path, and the explicit off-spellings
|
|
2677
|
+
* (`0`, `false`, `no`, `off`) are recognized as "off" rather than being taken
|
|
2678
|
+
* for a file name.
|
|
2679
|
+
*
|
|
2680
|
+
* Every model request appends one JSONL record per raw chunk plus lifecycle
|
|
2681
|
+
* records (`response`, `eof`, `idle-timeout`, `read-error`, `end`,
|
|
2682
|
+
* `stream-close`), each stamped with `at` — milliseconds since that request's
|
|
2683
|
+
* first byte. The chunk records carry the provider's own payload verbatim
|
|
2684
|
+
* (bounded per chunk), because a truncated answer is exactly what is being
|
|
2685
|
+
* diagnosed: treat the file as conversation content, not as a log safe to
|
|
2686
|
+
* share. It never carries the request body, an API key, or any request header.
|
|
2687
|
+
*
|
|
2688
|
+
* Deliberate trade-offs:
|
|
2689
|
+
* - OFF costs one `process.env` read per request.
|
|
2690
|
+
* - ON appends synchronously, one file open per chunk. The trace must survive
|
|
2691
|
+
* a host killed mid-diagnosis, and the microseconds it costs are irrelevant
|
|
2692
|
+
* beside the seconds-scale silences it is there to measure.
|
|
2693
|
+
* - A trace that cannot be written turns itself off instead of failing the
|
|
2694
|
+
* request it is observing: a diagnostic must never become the outage.
|
|
2695
|
+
*
|
|
2696
|
+
* @module
|
|
2697
|
+
*/
|
|
2698
|
+
/** Environment variable that turns the trace on. */
|
|
2699
|
+
const STREAM_TRACE_ENV = "DSH_COMMANDCODE_TRACE";
|
|
2700
|
+
/** File used when the variable is set to a bare flag instead of a path. */
|
|
2701
|
+
const STREAM_TRACE_DEFAULT_FILE = "dsh-commandcode-stream.jsonl";
|
|
2702
|
+
/** One raw chunk is recorded verbatim up to this many characters. */
|
|
2703
|
+
const STREAM_TRACE_MAX_TEXT = 16384;
|
|
2704
|
+
/** Values of {@link STREAM_TRACE_ENV} that mean "trace to the default file". */
|
|
2705
|
+
const FLAG_VALUES = /* @__PURE__ */ new Set([
|
|
2706
|
+
"1",
|
|
2707
|
+
"true",
|
|
2708
|
+
"yes",
|
|
2709
|
+
"on"
|
|
2710
|
+
]);
|
|
2711
|
+
/**
|
|
2712
|
+
* Values that explicitly mean OFF.
|
|
2713
|
+
*
|
|
2714
|
+
* A user turning the trace off writes `false` as readily as they write `1` to
|
|
2715
|
+
* turn it on, and without this set every one of these spellings fell through to
|
|
2716
|
+
* "…so it must be a path" — the trace switched ON and wrote the conversation to
|
|
2717
|
+
* a file NAMED `false` (or `0`, or `off`) in the process's working directory.
|
|
2718
|
+
*/
|
|
2719
|
+
const OFF_VALUES = /* @__PURE__ */ new Set([
|
|
2720
|
+
"0",
|
|
2721
|
+
"false",
|
|
2722
|
+
"no",
|
|
2723
|
+
"off"
|
|
2724
|
+
]);
|
|
2725
|
+
/** The inert trace used whenever the switch is off. */
|
|
2726
|
+
const OFF = {
|
|
2727
|
+
enabled: false,
|
|
2728
|
+
record: () => void 0,
|
|
2729
|
+
close: () => void 0
|
|
2730
|
+
};
|
|
2731
|
+
/**
|
|
2732
|
+
* Resolve the trace file for this process.
|
|
2733
|
+
*
|
|
2734
|
+
* @param raw - the variable's raw value; defaults to the live environment.
|
|
2735
|
+
* @returns the file to append to, or undefined when the trace is off.
|
|
2736
|
+
*/
|
|
2737
|
+
function resolveStreamTracePath(raw = process.env[STREAM_TRACE_ENV]) {
|
|
2738
|
+
const value = raw?.trim();
|
|
2739
|
+
if (value === void 0 || value === "") return void 0;
|
|
2740
|
+
const lower = value.toLowerCase();
|
|
2741
|
+
if (OFF_VALUES.has(lower)) return void 0;
|
|
2742
|
+
if (FLAG_VALUES.has(lower)) return join(tmpdir(), STREAM_TRACE_DEFAULT_FILE);
|
|
2743
|
+
return value;
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Clamp one recorded payload so a single chunk cannot consume the whole cap.
|
|
2747
|
+
*
|
|
2748
|
+
* @param text - the text to record.
|
|
2749
|
+
* @param max - characters to keep, defaulting to {@link STREAM_TRACE_MAX_TEXT}.
|
|
2750
|
+
* @returns `text` unchanged, or its head plus a marker naming what was dropped.
|
|
2751
|
+
*/
|
|
2752
|
+
function boundTraceText(text, max = STREAM_TRACE_MAX_TEXT) {
|
|
2753
|
+
if (text.length <= max) return text;
|
|
2754
|
+
return `${text.slice(0, max)}…[+${text.length - max} chars]`;
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Open one request's trace.
|
|
2758
|
+
*
|
|
2759
|
+
* @param scope - what the records describe (the `provider/model` route).
|
|
2760
|
+
* @param options - `path` overrides the environment (tests pass a temp file);
|
|
2761
|
+
* `now` overrides the clock the `at` stamps are measured with.
|
|
2762
|
+
* @returns a writer that never throws and never fails the request.
|
|
2763
|
+
*/
|
|
2764
|
+
function openStreamTrace(scope, options = {}) {
|
|
2765
|
+
const path = options.path ?? resolveStreamTracePath();
|
|
2766
|
+
if (path === void 0) return OFF;
|
|
2767
|
+
const now = options.now ?? Date.now;
|
|
2768
|
+
const startedAt = now();
|
|
2769
|
+
let written = 0;
|
|
2770
|
+
let stopped = false;
|
|
2771
|
+
let capped = false;
|
|
2772
|
+
const terminals = /* @__PURE__ */ new Set();
|
|
2773
|
+
try {
|
|
2774
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2775
|
+
} catch {
|
|
2776
|
+
return OFF;
|
|
2777
|
+
}
|
|
2778
|
+
const write = (record) => {
|
|
2779
|
+
if (stopped) return;
|
|
2780
|
+
const terminal = record.event === "end" || record.event === "stream-close";
|
|
2781
|
+
if (capped && !terminal) return;
|
|
2782
|
+
if (terminal && terminals.has(String(record.event))) return;
|
|
2783
|
+
let line;
|
|
2784
|
+
try {
|
|
2785
|
+
line = `${JSON.stringify({
|
|
2786
|
+
at: now() - startedAt,
|
|
2787
|
+
scope,
|
|
2788
|
+
...record
|
|
2789
|
+
})}\n`;
|
|
2790
|
+
} catch {
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
if (terminal) {
|
|
2794
|
+
if (Buffer.byteLength(line) > 16384) return;
|
|
2795
|
+
terminals.add(String(record.event));
|
|
2796
|
+
} else if (written + Buffer.byteLength(line) > 4194304) {
|
|
2797
|
+
capped = true;
|
|
2798
|
+
line = `${JSON.stringify({
|
|
2799
|
+
at: now() - startedAt,
|
|
2800
|
+
scope,
|
|
2801
|
+
event: "trace-capped",
|
|
2802
|
+
bytes: written
|
|
2803
|
+
})}\n`;
|
|
2804
|
+
}
|
|
2805
|
+
try {
|
|
2806
|
+
appendFileSync(path, line);
|
|
2807
|
+
written += Buffer.byteLength(line);
|
|
2808
|
+
} catch {
|
|
2809
|
+
stopped = true;
|
|
2810
|
+
}
|
|
2811
|
+
};
|
|
2812
|
+
write({ event: "stream-open" });
|
|
2813
|
+
return {
|
|
2814
|
+
enabled: true,
|
|
2815
|
+
record(event, fields) {
|
|
2816
|
+
write(fields === void 0 ? { event } : {
|
|
2817
|
+
event,
|
|
2818
|
+
...fields
|
|
2819
|
+
});
|
|
2820
|
+
},
|
|
2821
|
+
close() {
|
|
2822
|
+
write({
|
|
2823
|
+
event: "stream-close",
|
|
2824
|
+
bytes: written
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
//#endregion
|
|
2574
2830
|
//#region src/adapter.ts
|
|
2575
2831
|
/**
|
|
2576
2832
|
* DeepSeek Harness LLM adapter for the Command Code Provider API.
|
|
@@ -2580,7 +2836,7 @@ function scaleBy(size, factor) {
|
|
|
2580
2836
|
* and API key or subscription, and Command Code's terms apply.
|
|
2581
2837
|
*
|
|
2582
2838
|
* Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.4;
|
|
2583
|
-
* re-verified against command-code@1.
|
|
2839
|
+
* re-verified against command-code@1.62.0 — endpoints, request shape, and
|
|
2584
2840
|
* stream events unchanged):
|
|
2585
2841
|
* POST {apiBase}/alpha/generate
|
|
2586
2842
|
* body: { config, memory, taste, skills, params: { model, messages, tools,
|
|
@@ -2593,7 +2849,7 @@ function scaleBy(size, factor) {
|
|
|
2593
2849
|
* per-request options thunk and an API-key resolver from the plugin entry
|
|
2594
2850
|
* (src/index.ts), so a settings change reaches the very next request.
|
|
2595
2851
|
*/
|
|
2596
|
-
const COMMAND_CODE_CLI_VERSION = "1.
|
|
2852
|
+
const COMMAND_CODE_CLI_VERSION = "1.62.0";
|
|
2597
2853
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
2598
2854
|
const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
|
|
2599
2855
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
@@ -2686,15 +2942,21 @@ function periodEndValue(value) {
|
|
|
2686
2942
|
* Terminal stream-error markers from the official CLI (`Xw` in command-code's
|
|
2687
2943
|
* cli.mjs): these always mean "retrying cannot succeed", so the adapter must
|
|
2688
2944
|
* not classify them as transient server errors.
|
|
2945
|
+
*
|
|
2946
|
+
* Written with spaces rather than the CLI's underscores, because
|
|
2947
|
+
* {@link hasTerminalStreamMarker} normalizes the separator before matching:
|
|
2948
|
+
* a JSON body usually carries `premium credits exhausted` while the CLI's own
|
|
2949
|
+
* list is written `premium_credits_exhausted`, and both are the same refusal
|
|
2950
|
+
* (the pre-stream classifier normalizes identically).
|
|
2689
2951
|
*/
|
|
2690
2952
|
const TERMINAL_STREAM_ERROR_MARKERS = [
|
|
2691
|
-
"
|
|
2692
|
-
"
|
|
2953
|
+
"premium credits exhausted",
|
|
2954
|
+
"model not in plan",
|
|
2693
2955
|
"insufficient credits"
|
|
2694
2956
|
];
|
|
2695
2957
|
function hasTerminalStreamMarker(message) {
|
|
2696
|
-
const
|
|
2697
|
-
return TERMINAL_STREAM_ERROR_MARKERS.some((marker) =>
|
|
2958
|
+
const normalized = message.toLowerCase().replaceAll("_", " ");
|
|
2959
|
+
return TERMINAL_STREAM_ERROR_MARKERS.some((marker) => normalized.includes(marker));
|
|
2698
2960
|
}
|
|
2699
2961
|
/**
|
|
2700
2962
|
* Context-window wording the provider uses to reject a request that outgrew
|
|
@@ -2733,7 +2995,10 @@ function isContextOverflowDetail(detail) {
|
|
|
2733
2995
|
* (issue #39).
|
|
2734
2996
|
* - Credits/plan wording is terminal, so an exhausted balance or a model
|
|
2735
2997
|
* outside the plan is not retried as if it were a transient rate limit.
|
|
2736
|
-
* - Only then does the status/`isRetryable` pair decide
|
|
2998
|
+
* - Only then does the status/`isRetryable` pair decide — and an explicit
|
|
2999
|
+
* refusal (`isRetryable: false`) or a terminal marker OUTRANKS a status, so a
|
|
3000
|
+
* 5xx carrying "insufficient credits" stays terminal instead of entering the
|
|
3001
|
+
* retry cadence as `SERVER`.
|
|
2737
3002
|
*/
|
|
2738
3003
|
function streamErrorToLlmError(value, fallbackMessage) {
|
|
2739
3004
|
const record = isRecord(value) ? value : void 0;
|
|
@@ -2748,7 +3013,7 @@ function streamErrorToLlmError(value, fallbackMessage) {
|
|
|
2748
3013
|
const statusOption = statusCode !== void 0 ? { status: statusCode } : void 0;
|
|
2749
3014
|
if (isContextOverflowDetail(detail)) return new LlmError(`Command Code stream error: ${message};Command Code API 拒绝了这次请求:内容超出模型上下文窗口。正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, CONTEXT_WINDOW_EXCEEDED_CODE, statusOption);
|
|
2750
3015
|
const terminal = hasTerminalStreamMarker(detail);
|
|
2751
|
-
if (!(isRetryable === true || (statusCode !== void 0 ? statusCode !== void 0 && (statusCode === 429 || statusCode >= 500) :
|
|
3016
|
+
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);
|
|
2752
3017
|
return new LlmError(`Command Code stream error: ${message}`, "SERVER", statusOption);
|
|
2753
3018
|
}
|
|
2754
3019
|
function recordOrEmpty(value) {
|
|
@@ -2952,42 +3217,32 @@ async function writeModelsCache(cachePath, models) {
|
|
|
2952
3217
|
}
|
|
2953
3218
|
}
|
|
2954
3219
|
/**
|
|
2955
|
-
*
|
|
2956
|
-
*
|
|
3220
|
+
* >=0.1.7 puts toolCallId/isError on a role:'tool' message and carries raw
|
|
3221
|
+
* content blocks. Older engines wrap those fields in the first tool-result
|
|
3222
|
+
* block of a role:'user' message. Read structurally so the bundle still builds
|
|
3223
|
+
* and loads against older peers. Pairing and emission MUST use this same view:
|
|
3224
|
+
* otherwise a dropped result either drops its call too (making the model
|
|
3225
|
+
* forget completed work), or leaves an unanswered call on the wire.
|
|
2957
3226
|
*
|
|
2958
|
-
*
|
|
2959
|
-
*
|
|
2960
|
-
*
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
* looks like — the tag answers *who produced this*.
|
|
2974
|
-
* - An ABSENT tag falls back to the content shape. A `user` message whose
|
|
2975
|
-
* first block is a `tool-result` is a tool result by the harness's own
|
|
2976
|
-
* definition (`ToolResultMessage` is exactly that shape with a
|
|
2977
|
-
* `ToolMessageSource`), and the fallback is not cosmetic: `pairedToolCalls()`
|
|
2978
|
-
* counts a `tool-result` block from ANY message, so classifying such a
|
|
2979
|
-
* message as a plain user message would drop it at emission while its call
|
|
2980
|
-
* still counted as paired — leaving the assistant's tool call unanswered on
|
|
2981
|
-
* the wire, which the gateway rejects outright. FIRST block, not "any block":
|
|
2982
|
-
* the tool-result branch reads `content[0]` and skips anything else, so
|
|
2983
|
-
* claiming a mixed message as a tool result would discard its other blocks
|
|
2984
|
-
* instead of sending them.
|
|
2985
|
-
*/
|
|
2986
|
-
function isToolResultMessage(message) {
|
|
2987
|
-
if (message.role !== "user") return false;
|
|
3227
|
+
* For the legacy envelope a present source tag remains authoritative; an
|
|
3228
|
+
* absent tag falls back to the first block (issue #47). The modern role is
|
|
3229
|
+
* itself the discriminator, as it is for the Harness's own serializers.
|
|
3230
|
+
*/
|
|
3231
|
+
function toolResultOf(message) {
|
|
3232
|
+
const current = message;
|
|
3233
|
+
if (current.role === "tool") {
|
|
3234
|
+
if (typeof current.toolCallId !== "string" || current.toolCallId === "") return void 0;
|
|
3235
|
+
return {
|
|
3236
|
+
toolCallId: current.toolCallId,
|
|
3237
|
+
isError: current.isError ?? false,
|
|
3238
|
+
content: current.content
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
if (message.role !== "user") return void 0;
|
|
2988
3242
|
const kind = message.source?.kind;
|
|
2989
|
-
if (kind !== void 0
|
|
2990
|
-
|
|
3243
|
+
if (kind !== void 0 && kind !== "tool") return void 0;
|
|
3244
|
+
const block = message.content[0];
|
|
3245
|
+
return block?.type === "tool-result" ? block : void 0;
|
|
2991
3246
|
}
|
|
2992
3247
|
/**
|
|
2993
3248
|
* Collect the tool calls that have a paired tool result, plus each call's
|
|
@@ -3000,12 +3255,13 @@ function pairedToolCalls(messages) {
|
|
|
3000
3255
|
const callIds = /* @__PURE__ */ new Set();
|
|
3001
3256
|
const names = /* @__PURE__ */ new Map();
|
|
3002
3257
|
const resultIds = /* @__PURE__ */ new Set();
|
|
3003
|
-
for (const message of messages)
|
|
3004
|
-
|
|
3258
|
+
for (const message of messages) {
|
|
3259
|
+
const result = toolResultOf(message);
|
|
3260
|
+
if (result) resultIds.add(result.toolCallId);
|
|
3261
|
+
for (const block of message.content) if (message.role === "assistant" && block.type === "tool-call") {
|
|
3005
3262
|
callIds.add(block.id);
|
|
3006
3263
|
names.set(block.id, block.name);
|
|
3007
3264
|
}
|
|
3008
|
-
if (block.type === "tool-result") resultIds.add(block.toolCallId);
|
|
3009
3265
|
}
|
|
3010
3266
|
return {
|
|
3011
3267
|
ids: new Set([...callIds].filter((id) => resultIds.has(id))),
|
|
@@ -3313,7 +3569,8 @@ async function messagesToCC(messages, readImage) {
|
|
|
3313
3569
|
};
|
|
3314
3570
|
for (const message of messages) {
|
|
3315
3571
|
if (message.role === "system") continue;
|
|
3316
|
-
|
|
3572
|
+
const result = toolResultOf(message);
|
|
3573
|
+
if (message.role === "user" && !result) {
|
|
3317
3574
|
flushPendingImages();
|
|
3318
3575
|
const parts = [];
|
|
3319
3576
|
for (const block of message.content) {
|
|
@@ -3356,18 +3613,17 @@ async function messagesToCC(messages, readImage) {
|
|
|
3356
3613
|
});
|
|
3357
3614
|
continue;
|
|
3358
3615
|
}
|
|
3359
|
-
if (
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
const
|
|
3363
|
-
const wireToolCallId = wireIds.get(block.toolCallId) ?? block.toolCallId;
|
|
3616
|
+
if (result) {
|
|
3617
|
+
if (!paired.has(result.toolCallId)) continue;
|
|
3618
|
+
const media = toolResultMedia(result);
|
|
3619
|
+
const wireToolCallId = wireIds.get(result.toolCallId) ?? result.toolCallId;
|
|
3364
3620
|
out.push({
|
|
3365
3621
|
role: "tool",
|
|
3366
3622
|
content: [{
|
|
3367
3623
|
type: "tool-result",
|
|
3368
3624
|
toolCallId: wireToolCallId,
|
|
3369
|
-
toolName: toolNames.get(
|
|
3370
|
-
output:
|
|
3625
|
+
toolName: toolNames.get(result.toolCallId) || "unknown",
|
|
3626
|
+
output: result.isError ? {
|
|
3371
3627
|
type: "error-text",
|
|
3372
3628
|
value: toolResultTextForWire(media)
|
|
3373
3629
|
} : {
|
|
@@ -3425,7 +3681,8 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
3425
3681
|
};
|
|
3426
3682
|
for (const message of messages) {
|
|
3427
3683
|
if (message.role === "system") continue;
|
|
3428
|
-
|
|
3684
|
+
const result = toolResultOf(message);
|
|
3685
|
+
if (message.role === "user" && !result) {
|
|
3429
3686
|
flushPendingImages();
|
|
3430
3687
|
const parts = [];
|
|
3431
3688
|
for (const block of message.content) {
|
|
@@ -3471,11 +3728,10 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
3471
3728
|
out.push(assistant);
|
|
3472
3729
|
continue;
|
|
3473
3730
|
}
|
|
3474
|
-
if (
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
const
|
|
3478
|
-
const wireToolCallId = wireIds.get(block.toolCallId) ?? block.toolCallId;
|
|
3731
|
+
if (result) {
|
|
3732
|
+
if (!paired.has(result.toolCallId)) continue;
|
|
3733
|
+
const media = toolResultMedia(result);
|
|
3734
|
+
const wireToolCallId = wireIds.get(result.toolCallId) ?? result.toolCallId;
|
|
3479
3735
|
out.push({
|
|
3480
3736
|
role: "tool",
|
|
3481
3737
|
tool_call_id: wireToolCallId,
|
|
@@ -3736,6 +3992,7 @@ function createBlockAssembler() {
|
|
|
3736
3992
|
reasoningIndex: -1,
|
|
3737
3993
|
reasoningContent: "",
|
|
3738
3994
|
sawContent: false,
|
|
3995
|
+
finishReason: void 0,
|
|
3739
3996
|
openAiToolCalls: []
|
|
3740
3997
|
};
|
|
3741
3998
|
}
|
|
@@ -3955,8 +4212,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
3955
4212
|
/** The headers every authenticated account endpoint shares. */
|
|
3956
4213
|
async accountHeaders(apiKey) {
|
|
3957
4214
|
const connection = this.deps.options();
|
|
4215
|
+
const raw = apiKey ?? await this.deps.resolveApiKey(connection);
|
|
4216
|
+
const named = connection.apiKeyEnv;
|
|
3958
4217
|
return {
|
|
3959
|
-
Authorization: `Bearer ${
|
|
4218
|
+
Authorization: `Bearer ${assertUsableApiKey(raw, "llm-commandcode", typeof named === "string" && named !== "" ? named : "the stored credential")}`,
|
|
3960
4219
|
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
3961
4220
|
"x-cli-environment": "production",
|
|
3962
4221
|
...attributionHeaders()
|
|
@@ -3968,11 +4227,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
3968
4227
|
* failure accounting: the billing probe fails open silently, the usage
|
|
3969
4228
|
* report books failures per endpoint. Non-2xx and non-record bodies come
|
|
3970
4229
|
* back without a record; only a transport throw propagates to the caller.
|
|
4230
|
+
*
|
|
4231
|
+
* `timeoutMs` is the caller's budget for this one request. The default is the
|
|
4232
|
+
* CATALOG probe's short cap, which fits the picker's fail-open reads; the
|
|
4233
|
+
* usage report deliberately passes the connection's own request budget so an
|
|
4234
|
+
* account query is not held to a stricter (and invisible) limit than chat.
|
|
3971
4235
|
*/
|
|
3972
|
-
async fetchEndpointJson(url, headers) {
|
|
4236
|
+
async fetchEndpointJson(url, headers, timeoutMs = MODELS_TIMEOUT_MS) {
|
|
3973
4237
|
const response = await this.fetchImpl(url, {
|
|
3974
4238
|
headers,
|
|
3975
|
-
signal: AbortSignal.timeout(
|
|
4239
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
3976
4240
|
});
|
|
3977
4241
|
if (!response.ok) return { status: response.status };
|
|
3978
4242
|
const parsed = await response.json();
|
|
@@ -4130,15 +4394,39 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4130
4394
|
* view. Requires a usable API key (throws `MISSING_CREDENTIAL` otherwise).
|
|
4131
4395
|
* Pass `apiKey` to report on a specific account of a multi-account pool;
|
|
4132
4396
|
* the default resolves the currently active account.
|
|
4397
|
+
*
|
|
4398
|
+
* Two failure modes are NOT "the network is down", and the report must say
|
|
4399
|
+
* so rather than let {@link classifyTotalFailure} blame the connection:
|
|
4400
|
+
*
|
|
4401
|
+
* - A key that no HTTP header can carry (a stray newline from a paste, a
|
|
4402
|
+
* full-width character) makes `fetch` throw a `TypeError` **before any
|
|
4403
|
+
* I/O**, for every endpoint at once. The chat path refuses such a key
|
|
4404
|
+
* through `assertUsableApiKey`; this path must too, and it answers
|
|
4405
|
+
* `blocked: 'invalid-key'` instead of four phantom transport failures.
|
|
4406
|
+
* - The four endpoints get the SAME per-request budget as a chat call
|
|
4407
|
+
* (`connection.requestTimeoutMs`, default 60 s), not the catalog's 10 s
|
|
4408
|
+
* probe budget: on a slow link a 10 s cap made every account query time
|
|
4409
|
+
* out while chat kept working, which is exactly a "check your network"
|
|
4410
|
+
* banner that never clears.
|
|
4133
4411
|
*/
|
|
4134
4412
|
async getUsage(apiKey) {
|
|
4135
|
-
const
|
|
4136
|
-
const
|
|
4413
|
+
const connection = this.deps.options();
|
|
4414
|
+
const base = connection.apiBase;
|
|
4415
|
+
let headers;
|
|
4416
|
+
try {
|
|
4417
|
+
headers = await this.accountHeaders(apiKey);
|
|
4418
|
+
} catch (error) {
|
|
4419
|
+
if (error instanceof LlmError && error.code === "INVALID_CREDENTIAL") return {
|
|
4420
|
+
failures: [error.message],
|
|
4421
|
+
blocked: "invalid-key"
|
|
4422
|
+
};
|
|
4423
|
+
throw error;
|
|
4424
|
+
}
|
|
4137
4425
|
const failures = [];
|
|
4138
4426
|
const failedStatuses = [];
|
|
4139
4427
|
const getJson = async (path) => {
|
|
4140
4428
|
try {
|
|
4141
|
-
const { status, record } = await this.fetchEndpointJson(`${base}${path}`, headers);
|
|
4429
|
+
const { status, record } = await this.fetchEndpointJson(`${base}${path}`, headers, connection.requestTimeoutMs);
|
|
4142
4430
|
if (record === void 0) {
|
|
4143
4431
|
failures.push(`${path}: HTTP ${status}`);
|
|
4144
4432
|
failedStatuses.push(status);
|
|
@@ -4224,7 +4512,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4224
4512
|
reported = true;
|
|
4225
4513
|
if (!window.exceeded) continue;
|
|
4226
4514
|
exceeded = true;
|
|
4227
|
-
resetAt = Math.max(resetAt, window.resetAt);
|
|
4515
|
+
if (window.resetAt > 0 && window.resetAt - Date.now() <= MAX_TRUSTED_RESET_MS) resetAt = Math.max(resetAt, window.resetAt);
|
|
4228
4516
|
}
|
|
4229
4517
|
if (!reported) return void 0;
|
|
4230
4518
|
return {
|
|
@@ -4332,6 +4620,19 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4332
4620
|
const reader = response.body.getReader();
|
|
4333
4621
|
const decoder = new TextDecoder();
|
|
4334
4622
|
let buffer = "";
|
|
4623
|
+
const trace = openStreamTrace(`${options.provider}/${options.model}`);
|
|
4624
|
+
const streamStartedAt = Date.now();
|
|
4625
|
+
let chunkCount = 0;
|
|
4626
|
+
let totalBytes = 0;
|
|
4627
|
+
let lastChunkAt = streamStartedAt;
|
|
4628
|
+
trace.record("response", {
|
|
4629
|
+
protocol,
|
|
4630
|
+
endpoint: protocol === "cli" ? `${connection.apiBase}/alpha/generate` : `${connection.apiBase}/provider/v1/chat/completions`,
|
|
4631
|
+
status: response.status,
|
|
4632
|
+
attempts: tried.size,
|
|
4633
|
+
maxTokens,
|
|
4634
|
+
...reasoningEffort === void 0 ? {} : { reasoningEffort }
|
|
4635
|
+
});
|
|
4335
4636
|
let idleTimer;
|
|
4336
4637
|
let idleFired = false;
|
|
4337
4638
|
const armIdle = () => {
|
|
@@ -4348,9 +4649,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4348
4649
|
}
|
|
4349
4650
|
};
|
|
4350
4651
|
const asm = createBlockAssembler();
|
|
4351
|
-
|
|
4652
|
+
let finish;
|
|
4653
|
+
let usage;
|
|
4654
|
+
let usageEmitted = false;
|
|
4655
|
+
let endRecorded = false;
|
|
4656
|
+
function* handle(event) {
|
|
4657
|
+
for (const chunk of handleEvent(asm, protocol, event)) if (chunk.type === "finish") finish = chunk;
|
|
4658
|
+
else if (chunk.type === "usage") usage = chunk.usage;
|
|
4659
|
+
else yield chunk;
|
|
4660
|
+
}
|
|
4352
4661
|
try {
|
|
4353
|
-
let finished = false;
|
|
4354
4662
|
for (;;) {
|
|
4355
4663
|
let read;
|
|
4356
4664
|
armIdle();
|
|
@@ -4358,44 +4666,104 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4358
4666
|
read = await reader.read();
|
|
4359
4667
|
} catch (error) {
|
|
4360
4668
|
if (options.signal?.aborted) throw error;
|
|
4669
|
+
trace.record("read-error", {
|
|
4670
|
+
chunks: chunkCount,
|
|
4671
|
+
bytes: totalBytes,
|
|
4672
|
+
silentMs: Date.now() - lastChunkAt,
|
|
4673
|
+
message: errorChain(error)
|
|
4674
|
+
});
|
|
4361
4675
|
throw new LlmError(`Command Code API stream from ${connection.apiBase} failed while reading: ${errorChain(error)};Command Code API 流式响应中途断开——网络波动所致,重试通常可恢复`, "TRANSPORT", { cause: error });
|
|
4362
4676
|
} finally {
|
|
4363
4677
|
clearIdle();
|
|
4364
4678
|
}
|
|
4365
4679
|
const { done, value } = read;
|
|
4366
4680
|
if (done) {
|
|
4681
|
+
trace.record("eof", {
|
|
4682
|
+
chunks: chunkCount,
|
|
4683
|
+
bytes: totalBytes,
|
|
4684
|
+
silentMs: Date.now() - lastChunkAt,
|
|
4685
|
+
totalMs: Date.now() - streamStartedAt,
|
|
4686
|
+
finishSeen: finish !== void 0,
|
|
4687
|
+
idleFired,
|
|
4688
|
+
buffered: buffer.length
|
|
4689
|
+
});
|
|
4367
4690
|
if (idleFired) throw new LlmError(`Command Code API stream from ${connection.apiBase} was idle for ${connection.streamIdleTimeoutMs}ms (no events) and was treated as a dead connection;Command Code API 流式响应已 ${connection.streamIdleTimeoutMs} 毫秒无任何事件,被判定为死连接——长思考模型可在设置中调大流空闲超时`, "TIMEOUT");
|
|
4368
|
-
if (buffer.trim())
|
|
4369
|
-
yield chunk;
|
|
4370
|
-
if (chunk.type === "finish") finished = true;
|
|
4371
|
-
}
|
|
4691
|
+
if (buffer.trim()) yield* handle(parseStreamEventLine(buffer));
|
|
4372
4692
|
break;
|
|
4373
4693
|
}
|
|
4374
|
-
|
|
4694
|
+
const text = decoder.decode(value, { stream: true });
|
|
4695
|
+
buffer += text;
|
|
4696
|
+
chunkCount += 1;
|
|
4697
|
+
totalBytes += value.byteLength;
|
|
4698
|
+
lastChunkAt = Date.now();
|
|
4699
|
+
trace.record("chunk", {
|
|
4700
|
+
n: chunkCount,
|
|
4701
|
+
bytes: value.byteLength,
|
|
4702
|
+
text: boundTraceText(text)
|
|
4703
|
+
});
|
|
4375
4704
|
const lines = buffer.split("\n");
|
|
4376
4705
|
buffer = lines.pop() ?? "";
|
|
4377
|
-
for (const line of lines)
|
|
4378
|
-
|
|
4379
|
-
for (const chunk of chunks) {
|
|
4380
|
-
yield chunk;
|
|
4381
|
-
if (chunk.type === "finish") finished = true;
|
|
4382
|
-
}
|
|
4383
|
-
}
|
|
4384
|
-
if (finished) break;
|
|
4706
|
+
for (const line of lines) yield* handle(parseStreamEventLine(line));
|
|
4707
|
+
if (finish !== void 0) break;
|
|
4385
4708
|
}
|
|
4386
|
-
if (
|
|
4387
|
-
|
|
4388
|
-
yield* closeReasoning(asm);
|
|
4389
|
-
if (protocol === "openai") yield* emitOpenAiToolCalls(asm);
|
|
4390
|
-
if (!asm.sawContent) throw new LlmError("Command Code returned an empty response;Command Code 返回了空响应,重试通常可恢复", "EMPTY_RESPONSE");
|
|
4709
|
+
if (usage !== void 0) {
|
|
4710
|
+
usageEmitted = true;
|
|
4391
4711
|
yield {
|
|
4392
|
-
type: "
|
|
4393
|
-
|
|
4712
|
+
type: "usage",
|
|
4713
|
+
usage
|
|
4394
4714
|
};
|
|
4395
4715
|
}
|
|
4716
|
+
if (finish !== void 0) {
|
|
4717
|
+
const failure = asm.sawContent ? void 0 : emptyCompletionError(asm.finishReason, maxTokens, usage);
|
|
4718
|
+
trace.record("end", {
|
|
4719
|
+
outcome: failure === void 0 ? "finished" : failure.code === "OUTPUT_TOKEN_LIMIT" ? "output-token-limit" : "empty",
|
|
4720
|
+
finishReason: asm.finishReason,
|
|
4721
|
+
...failure === void 0 ? {} : { code: failure.code },
|
|
4722
|
+
maxTokens,
|
|
4723
|
+
...usage === void 0 ? {} : { usage },
|
|
4724
|
+
chunks: chunkCount,
|
|
4725
|
+
bytes: totalBytes,
|
|
4726
|
+
totalMs: Date.now() - streamStartedAt,
|
|
4727
|
+
sawContent: asm.sawContent
|
|
4728
|
+
});
|
|
4729
|
+
endRecorded = true;
|
|
4730
|
+
if (failure !== void 0) throw failure;
|
|
4731
|
+
yield finish;
|
|
4732
|
+
} else {
|
|
4733
|
+
const elapsed = Date.now() - streamStartedAt;
|
|
4734
|
+
const detail = `${chunkCount} chunk(s), ${totalBytes} bytes, ${elapsed}ms`;
|
|
4735
|
+
trace.record("end", {
|
|
4736
|
+
outcome: asm.sawContent ? "stream-closed" : "empty",
|
|
4737
|
+
chunks: chunkCount,
|
|
4738
|
+
bytes: totalBytes,
|
|
4739
|
+
totalMs: elapsed,
|
|
4740
|
+
sawContent: asm.sawContent
|
|
4741
|
+
});
|
|
4742
|
+
endRecorded = true;
|
|
4743
|
+
if (!asm.sawContent) throw new LlmError("Command Code returned an empty response;Command Code 返回了空响应,重试通常可恢复", "EMPTY_RESPONSE");
|
|
4744
|
+
throw new LlmError(`Command Code API stream from ${connection.apiBase} ended before its finish event (${detail}) — the response was closed mid-generation, so this is not the model stopping by itself;Command Code API 流式响应在结束事件之前被关闭(生成中途断流),并非模型主动结束——请重试;若频繁出现,可用 ${STREAM_TRACE_ENV}=<文件路径> 抓取原始流以定位断流原因`, "STREAM_CLOSED");
|
|
4745
|
+
}
|
|
4746
|
+
} catch (error) {
|
|
4747
|
+
if (!usageEmitted && usage !== void 0) yield {
|
|
4748
|
+
type: "usage",
|
|
4749
|
+
usage
|
|
4750
|
+
};
|
|
4751
|
+
if (!endRecorded) trace.record("end", {
|
|
4752
|
+
outcome: options.signal?.aborted ? "aborted" : "error",
|
|
4753
|
+
code: error instanceof LlmError ? error.code : "unknown",
|
|
4754
|
+
finishReason: asm.finishReason,
|
|
4755
|
+
maxTokens,
|
|
4756
|
+
...usage === void 0 ? {} : { usage },
|
|
4757
|
+
sawContent: asm.sawContent,
|
|
4758
|
+
chunks: chunkCount,
|
|
4759
|
+
bytes: totalBytes,
|
|
4760
|
+
totalMs: Date.now() - streamStartedAt
|
|
4761
|
+
});
|
|
4762
|
+
throw error;
|
|
4396
4763
|
} finally {
|
|
4397
4764
|
clearIdle();
|
|
4398
4765
|
cleanup();
|
|
4766
|
+
trace.close();
|
|
4399
4767
|
await reader.cancel().catch(() => void 0);
|
|
4400
4768
|
reader.releaseLock();
|
|
4401
4769
|
}
|
|
@@ -4486,7 +4854,7 @@ function handleCliEvent(asm, event) {
|
|
|
4486
4854
|
}
|
|
4487
4855
|
const delta = stringValue(event.text) ?? "";
|
|
4488
4856
|
asm.textContent += delta;
|
|
4489
|
-
asm.sawContent = true;
|
|
4857
|
+
if (delta.trim() !== "") asm.sawContent = true;
|
|
4490
4858
|
chunks.push({
|
|
4491
4859
|
type: "text-delta",
|
|
4492
4860
|
index: asm.textIndex,
|
|
@@ -4549,6 +4917,7 @@ function handleCliEvent(asm, event) {
|
|
|
4549
4917
|
break;
|
|
4550
4918
|
}
|
|
4551
4919
|
case "finish": {
|
|
4920
|
+
asm.finishReason = stringValue(event.finishReason);
|
|
4552
4921
|
chunks.push(...closeText(asm), ...closeReasoning(asm));
|
|
4553
4922
|
const usage = isRecord(event.totalUsage) ? event.totalUsage : void 0;
|
|
4554
4923
|
if (usage) {
|
|
@@ -4562,6 +4931,8 @@ function handleCliEvent(asm, event) {
|
|
|
4562
4931
|
cacheReadTokens: cacheRead,
|
|
4563
4932
|
cacheWriteTokens: cacheWrite
|
|
4564
4933
|
};
|
|
4934
|
+
const reasoningTokens = numberValue((isRecord(usage.outputTokenDetails) ? usage.outputTokenDetails : void 0)?.reasoningTokens) ?? numberValue(usage.reasoningTokens);
|
|
4935
|
+
if (reasoningTokens !== void 0) tokenUsage.reasoningTokens = reasoningTokens;
|
|
4565
4936
|
chunks.push({
|
|
4566
4937
|
type: "usage",
|
|
4567
4938
|
usage: tokenUsage
|
|
@@ -4626,39 +4997,38 @@ function handleOpenAIEvent(asm, event) {
|
|
|
4626
4997
|
});
|
|
4627
4998
|
}
|
|
4628
4999
|
asm.textContent += contentDelta;
|
|
4629
|
-
asm.sawContent = true;
|
|
5000
|
+
if (contentDelta.trim() !== "") asm.sawContent = true;
|
|
4630
5001
|
chunks.push({
|
|
4631
5002
|
type: "text-delta",
|
|
4632
5003
|
index: asm.textIndex,
|
|
4633
5004
|
text: contentDelta
|
|
4634
5005
|
});
|
|
4635
5006
|
}
|
|
4636
|
-
if (Array.isArray(delta.tool_calls)) {
|
|
5007
|
+
if (Array.isArray(delta.tool_calls)) for (const rawCall of delta.tool_calls) {
|
|
5008
|
+
if (!isRecord(rawCall)) continue;
|
|
4637
5009
|
asm.sawContent = true;
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
if (name !== void 0 && existing.name === "") existing.name = name;
|
|
4657
|
-
if (argDelta !== void 0) existing.arguments += argDelta;
|
|
4658
|
-
}
|
|
5010
|
+
const callIndex = numberValue(rawCall.index) ?? 0;
|
|
5011
|
+
let existing = asm.openAiToolCalls.find((call) => call.index === callIndex);
|
|
5012
|
+
const fn = isRecord(rawCall.function) ? rawCall.function : void 0;
|
|
5013
|
+
const id = stringValue(rawCall.id);
|
|
5014
|
+
const name = fn === void 0 ? void 0 : stringValue(fn.name);
|
|
5015
|
+
const argDelta = fn === void 0 ? void 0 : stringValue(fn.arguments) ?? (fn.arguments === void 0 ? "" : JSON.stringify(fn.arguments));
|
|
5016
|
+
if (!existing) {
|
|
5017
|
+
existing = {
|
|
5018
|
+
index: callIndex,
|
|
5019
|
+
name: name ?? "",
|
|
5020
|
+
arguments: argDelta ?? ""
|
|
5021
|
+
};
|
|
5022
|
+
if (id !== void 0) existing.id = id;
|
|
5023
|
+
asm.openAiToolCalls.push(existing);
|
|
5024
|
+
} else {
|
|
5025
|
+
if (id !== void 0 && existing.id === void 0) existing.id = id;
|
|
5026
|
+
if (name !== void 0 && existing.name === "") existing.name = name;
|
|
5027
|
+
if (argDelta !== void 0) existing.arguments += argDelta;
|
|
4659
5028
|
}
|
|
4660
5029
|
}
|
|
4661
5030
|
if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
|
|
5031
|
+
asm.finishReason = stringValue(choice.finish_reason);
|
|
4662
5032
|
chunks.push(...closeText(asm), ...closeReasoning(asm), ...emitOpenAiToolCalls(asm));
|
|
4663
5033
|
if (event.usage !== void 0) chunks.push({
|
|
4664
5034
|
type: "usage",
|
|
@@ -4706,6 +5076,13 @@ function mapFinishReason(reason) {
|
|
|
4706
5076
|
if (reason === "length" || reason === "max_tokens" || reason === "max-tokens" || reason === "max_output_tokens") return { kind: "max-tokens" };
|
|
4707
5077
|
return { kind: "stop" };
|
|
4708
5078
|
}
|
|
5079
|
+
/** A terminal marker without text or tools is a failed attempt, not a completed turn. */
|
|
5080
|
+
function emptyCompletionError(reason, maxTokens, usage) {
|
|
5081
|
+
const detail = `finish_reason=${reason ?? "unknown"}, max_tokens=${maxTokens}, outputTokens=${usage?.outputTokens ?? "unknown"}, reasoningTokens=${usage?.reasoningTokens ?? "unknown"}`;
|
|
5082
|
+
if (mapFinishReason(reason).kind === "max-tokens") return new LlmError(`Command Code reached the output token limit without producing answer text or a tool call (${detail});Command Code 已达到输出 token 上限,但没有生成正文或工具调用;请调整思考强度,或在模型和接口上限内增加输出预算`, "OUTPUT_TOKEN_LIMIT");
|
|
5083
|
+
if (reason === "content_filter" || reason === "content-filter") return new LlmError(`Command Code filtered the response (${detail});Command Code 拦截了响应,未生成正文或工具调用`, "CONTENT_FILTER");
|
|
5084
|
+
return new LlmError(`Command Code finished without producing answer text or a tool call (${detail});Command Code 返回了空响应(可能只有思考内容),重试通常可恢复`, "EMPTY_RESPONSE");
|
|
5085
|
+
}
|
|
4709
5086
|
/**
|
|
4710
5087
|
* True when a Provider API pre-stream rejection is Command Code's Go-plan
|
|
4711
5088
|
* gate (`upgrade_required`). Only this exact class of rejection should fall
|
|
@@ -4750,8 +5127,25 @@ function mapFinishReason(reason) {
|
|
|
4750
5127
|
* or a model outside its plan (`MODEL_NOT_IN_PLAN`). Rotating must NOT mark
|
|
4751
5128
|
* the account: the fact is about the model or the balance, it can change
|
|
4752
5129
|
* without the key changing, and a `:free` model can still be served by an
|
|
4753
|
-
* account with an empty balance.
|
|
5130
|
+
* account with an empty balance. These codes are recognized on EVERY
|
|
5131
|
+
* status — a 5xx that proxies the provider's own credits/plan body is
|
|
5132
|
+
* still an account fact, and only the prose scan is confined to 4xx.
|
|
4754
5133
|
*/
|
|
5134
|
+
/**
|
|
5135
|
+
* The provider's structured "this account cannot serve this request" codes.
|
|
5136
|
+
*
|
|
5137
|
+
* Status-independent on purpose, mirroring the CLI's classifier, which reads
|
|
5138
|
+
* the code before any status guard. A 5xx that carries one of these must not
|
|
5139
|
+
* be retried as a transient provider failure: the same request against the
|
|
5140
|
+
* same account cannot succeed, so the caller rotates past it (without marking
|
|
5141
|
+
* the key) instead of replaying it on the harness's retry cadence.
|
|
5142
|
+
*/
|
|
5143
|
+
const ACCOUNT_UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
|
|
5144
|
+
"USAGE_EXCEEDED",
|
|
5145
|
+
"INSUFFICIENT_CREDITS",
|
|
5146
|
+
"PREMIUM_CREDITS_EXHAUSTED",
|
|
5147
|
+
"MODEL_NOT_IN_PLAN"
|
|
5148
|
+
]);
|
|
4755
5149
|
function classifyAccountRejection(status, errText) {
|
|
4756
5150
|
if (status === 429 || readProviderErrorCode(errText)?.toUpperCase() === "RATE_LIMITED") {
|
|
4757
5151
|
const evidence = readWindowLimitEvidence(errText);
|
|
@@ -4762,10 +5156,11 @@ function classifyAccountRejection(status, errText) {
|
|
|
4762
5156
|
};
|
|
4763
5157
|
}
|
|
4764
5158
|
if (status === 401) return { reason: "invalid-credential" };
|
|
5159
|
+
const code = readProviderErrorCode(errText)?.toUpperCase();
|
|
5160
|
+
if (code !== void 0 && ACCOUNT_UNAVAILABLE_CODES.has(code)) return { reason: "unavailable" };
|
|
4765
5161
|
if (status < 400 || status >= 500) return void 0;
|
|
4766
5162
|
const lower = errText.toLowerCase().replaceAll("_", " ");
|
|
4767
|
-
|
|
4768
|
-
if (code === "USAGE_EXCEEDED" || code === "INSUFFICIENT_CREDITS" || code === "PREMIUM_CREDITS_EXHAUSTED" || code === "MODEL_NOT_IN_PLAN" || lower.includes("insufficient credits") || lower.includes("premium credits exhausted") || lower.includes("model not in plan") || /insufficient (credit|balance)/.test(lower) || /out of credit/.test(lower) || /not (available|included)[^.]{0,40}plan/.test(lower) || lower.includes("upgrade your plan")) return { reason: "unavailable" };
|
|
5163
|
+
if (lower.includes("insufficient credits") || lower.includes("premium credits exhausted") || lower.includes("model not in plan") || /insufficient (credit|balance)/.test(lower) || /out of credit/.test(lower) || /not (available|included)[^.]{0,40}plan/.test(lower) || lower.includes("upgrade your plan")) return { reason: "unavailable" };
|
|
4769
5164
|
}
|
|
4770
5165
|
/** The machine-readable `error.code`/`error.type` of a pre-stream failure body. */
|
|
4771
5166
|
function readProviderErrorCode(errText) {
|
|
@@ -4779,6 +5174,16 @@ function readProviderErrorCode(errText) {
|
|
|
4779
5174
|
}
|
|
4780
5175
|
}
|
|
4781
5176
|
/**
|
|
5177
|
+
* How far ahead a provider-published reset may be and still be trusted.
|
|
5178
|
+
*
|
|
5179
|
+
* Generous on purpose: the weekly window is the longest this provider meters,
|
|
5180
|
+
* and the value is a plausibility bound rather than a policy. Beyond it the
|
|
5181
|
+
* reset is dropped rather than clamped, which leaves the account's mark
|
|
5182
|
+
* `unknown` — the state the pool's probe pass re-checks — instead of a
|
|
5183
|
+
* cooldown that never expires.
|
|
5184
|
+
*/
|
|
5185
|
+
const MAX_TRUSTED_RESET_MS = 2592e6;
|
|
5186
|
+
/**
|
|
4782
5187
|
* The usage-window evidence carried by a rejected request, mirroring the CLI's
|
|
4783
5188
|
* `parseWindowLimitError` → `resolveWindowLabel`/`extractResetAtMs` pair.
|
|
4784
5189
|
*
|
|
@@ -4812,7 +5217,8 @@ function readWindowLimitEvidence(errText) {
|
|
|
4812
5217
|
const window = named === "fiveHour" || named === "weekly" || named === "daily" ? named : /usage limit for your plan/i.test(message) || /usage limit for your plan/i.test(errText) ? /weekly/i.test(message) ? "weekly" : "fiveHour" : void 0;
|
|
4813
5218
|
const stamp = /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(message)?.[1] ?? /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(errText)?.[1];
|
|
4814
5219
|
const stamped = stamp === void 0 ? void 0 : Date.parse(stamp);
|
|
4815
|
-
const
|
|
5220
|
+
const published = seconds !== void 0 && seconds > 0 ? Math.round(seconds * 1e3) : stamped !== void 0 && !Number.isNaN(stamped) ? stamped : void 0;
|
|
5221
|
+
const resetAtMs = published !== void 0 && Number.isFinite(published) && published - Date.now() <= MAX_TRUSTED_RESET_MS ? published : void 0;
|
|
4816
5222
|
return {
|
|
4817
5223
|
...window === void 0 ? {} : { window },
|
|
4818
5224
|
...resetAtMs === void 0 ? {} : { resetAtMs }
|
|
@@ -5612,7 +6018,10 @@ function isCallbackCredentials(value) {
|
|
|
5612
6018
|
/**
|
|
5613
6019
|
* One browser-login attempt machine. Single-flight by design: `begin()` while
|
|
5614
6020
|
* waiting returns the live attempt's status instead of starting a second one;
|
|
5615
|
-
* a
|
|
6021
|
+
* a start that is still binding its port is REJOINED through
|
|
6022
|
+
* {@link CommandCodeLoginFlow.begin}'s in-flight promise, because the status
|
|
6023
|
+
* face cannot say `waiting` until that bind settles; a terminal state makes the
|
|
6024
|
+
* next `begin()` start fresh.
|
|
5616
6025
|
*/
|
|
5617
6026
|
var CommandCodeLoginFlow = class {
|
|
5618
6027
|
deps;
|
|
@@ -5630,6 +6039,17 @@ var CommandCodeLoginFlow = class {
|
|
|
5630
6039
|
* the user cancelled and flipping the page back to success.
|
|
5631
6040
|
*/
|
|
5632
6041
|
attemptSeq = 0;
|
|
6042
|
+
/**
|
|
6043
|
+
* The start currently binding a port, if any. Every `begin()` that arrives
|
|
6044
|
+
* before it settles joins it: two independent starts would each bind their
|
|
6045
|
+
* own loopback server, and only the LAST one is reachable by
|
|
6046
|
+
* {@link CommandCodeLoginFlow.teardown} — the orphan keeps listening and
|
|
6047
|
+
* answering `/callback` for the process's lifetime, and ten of them exhaust
|
|
6048
|
+
* the port window so browser login dies until the Host restarts.
|
|
6049
|
+
*/
|
|
6050
|
+
starting;
|
|
6051
|
+
/** A `cancel()` that arrived while a start was still binding (see {@link CommandCodeLoginFlow.cancel}). */
|
|
6052
|
+
cancelPending = false;
|
|
5633
6053
|
disposed = false;
|
|
5634
6054
|
constructor(deps) {
|
|
5635
6055
|
this.deps = deps;
|
|
@@ -5651,6 +6071,22 @@ var CommandCodeLoginFlow = class {
|
|
|
5651
6071
|
async begin() {
|
|
5652
6072
|
if (this.disposed) throw new Error("login flow has been disposed");
|
|
5653
6073
|
if (this.statusValue.state === "waiting" && this.server !== void 0) return this.statusValue;
|
|
6074
|
+
if (this.starting !== void 0) return this.starting;
|
|
6075
|
+
this.cancelPending = false;
|
|
6076
|
+
const starting = this.startAttempt();
|
|
6077
|
+
this.starting = starting;
|
|
6078
|
+
try {
|
|
6079
|
+
return await starting;
|
|
6080
|
+
} finally {
|
|
6081
|
+
if (this.starting === starting) this.starting = void 0;
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
/**
|
|
6085
|
+
* The binding half of {@link CommandCodeLoginFlow.begin}: one attempt, one
|
|
6086
|
+
* server, one published `waiting` status. Callers reach it only through
|
|
6087
|
+
* `begin()`'s single-flight fence.
|
|
6088
|
+
*/
|
|
6089
|
+
async startAttempt() {
|
|
5654
6090
|
this.teardown();
|
|
5655
6091
|
const attempt = ++this.attemptSeq;
|
|
5656
6092
|
const port = await this.findPort();
|
|
@@ -5662,6 +6098,16 @@ var CommandCodeLoginFlow = class {
|
|
|
5662
6098
|
};
|
|
5663
6099
|
});
|
|
5664
6100
|
await this.bindServer(port, expectedState);
|
|
6101
|
+
if (this.disposed || this.cancelPending) {
|
|
6102
|
+
const cancelled = !this.disposed;
|
|
6103
|
+
this.cancelPending = false;
|
|
6104
|
+
this.teardown();
|
|
6105
|
+
if (cancelled) this.setStatus({
|
|
6106
|
+
state: "failed",
|
|
6107
|
+
reason: "cancelled"
|
|
6108
|
+
});
|
|
6109
|
+
return this.statusValue;
|
|
6110
|
+
}
|
|
5665
6111
|
const apiBase = this.readApiBase();
|
|
5666
6112
|
this.setStatus({
|
|
5667
6113
|
state: "waiting",
|
|
@@ -5683,19 +6129,29 @@ var CommandCodeLoginFlow = class {
|
|
|
5683
6129
|
settled.then((credentials) => this.complete(attempt, credentials), (failure) => this.failFrom(attempt, failure));
|
|
5684
6130
|
return this.statusValue;
|
|
5685
6131
|
}
|
|
5686
|
-
/**
|
|
6132
|
+
/**
|
|
6133
|
+
* Cancel a waiting attempt; terminal states are untouched. A start that is
|
|
6134
|
+
* still binding a port is flagged instead of ignored — it cannot be torn
|
|
6135
|
+
* down from here (its server does not exist yet), so `startAttempt` observes
|
|
6136
|
+
* the flag once the bind settles and retires it.
|
|
6137
|
+
*/
|
|
5687
6138
|
cancel() {
|
|
5688
|
-
if (this.disposed
|
|
5689
|
-
this.
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
6139
|
+
if (this.disposed) return;
|
|
6140
|
+
if (this.statusValue.state === "waiting") {
|
|
6141
|
+
this.teardown();
|
|
6142
|
+
this.setStatus({
|
|
6143
|
+
state: "failed",
|
|
6144
|
+
reason: "cancelled"
|
|
6145
|
+
});
|
|
6146
|
+
return;
|
|
6147
|
+
}
|
|
6148
|
+
if (this.starting !== void 0) this.cancelPending = true;
|
|
5694
6149
|
}
|
|
5695
6150
|
/** Stop everything; a waiting attempt ends cancelled. Idempotent. */
|
|
5696
6151
|
dispose() {
|
|
5697
6152
|
if (this.disposed) return;
|
|
5698
6153
|
this.disposed = true;
|
|
6154
|
+
this.cancelPending = false;
|
|
5699
6155
|
const wasWaiting = this.statusValue.state === "waiting";
|
|
5700
6156
|
this.teardown();
|
|
5701
6157
|
if (wasWaiting) this.setStatus({
|
|
@@ -5731,8 +6187,12 @@ var CommandCodeLoginFlow = class {
|
|
|
5731
6187
|
const server = createServer((request, response) => this.handleCallback(request, response, expectedState));
|
|
5732
6188
|
this.server = server;
|
|
5733
6189
|
server.once("error", (error) => {
|
|
5734
|
-
if (this.server !== server)
|
|
6190
|
+
if (this.server !== server) {
|
|
6191
|
+
closeServer(server);
|
|
6192
|
+
return;
|
|
6193
|
+
}
|
|
5735
6194
|
this.server = void 0;
|
|
6195
|
+
closeServer(server);
|
|
5736
6196
|
const tagged = new LoginSettleError("error", `Could not bind the login callback server on port ${port}: ${error.code ?? error.message}`);
|
|
5737
6197
|
if (binding) {
|
|
5738
6198
|
binding = false;
|
|
@@ -5901,11 +6361,17 @@ var CommandCodeLoginFlow = class {
|
|
|
5901
6361
|
/** Close the server and watchdog without touching the published status. */
|
|
5902
6362
|
teardown() {
|
|
5903
6363
|
this.clearTimer();
|
|
5904
|
-
this.server
|
|
6364
|
+
closeServer(this.server);
|
|
5905
6365
|
this.server = void 0;
|
|
5906
6366
|
this.settle = void 0;
|
|
5907
6367
|
}
|
|
5908
6368
|
};
|
|
6369
|
+
/** Close one loopback server without letting "not running"/"already closed" escape. */
|
|
6370
|
+
function closeServer(server) {
|
|
6371
|
+
try {
|
|
6372
|
+
server?.close();
|
|
6373
|
+
} catch {}
|
|
6374
|
+
}
|
|
5909
6375
|
/** A tagged settle failure carrying the stable copy reason. */
|
|
5910
6376
|
var LoginSettleError = class extends Error {
|
|
5911
6377
|
reason;
|
|
@@ -5979,7 +6445,8 @@ function selectCommandCodeSearchProvider(web, enable) {
|
|
|
5979
6445
|
function commandCodeSearchSelection() {
|
|
5980
6446
|
return {
|
|
5981
6447
|
owner: false,
|
|
5982
|
-
displaced: void 0
|
|
6448
|
+
displaced: void 0,
|
|
6449
|
+
preexisting: false
|
|
5983
6450
|
};
|
|
5984
6451
|
}
|
|
5985
6452
|
/**
|
|
@@ -5997,8 +6464,14 @@ function commandCodeSearchSelection() {
|
|
|
5997
6464
|
* auto-select — already says what the user wants.
|
|
5998
6465
|
* - When the field already reads `commandcode` at first touch (e.g. a
|
|
5999
6466
|
* surviving runtime the plugin did not set, or a manual
|
|
6000
|
-
* `searchProvider: commandcode` pin), `displaced` stays undefined
|
|
6001
|
-
* later disable is a no-op rather than a guess
|
|
6467
|
+
* `searchProvider: commandcode` pin), `displaced` stays undefined and
|
|
6468
|
+
* `preexisting` is set, so the later disable is a no-op rather than a guess
|
|
6469
|
+
* at the factory default. A "no-op" means the field is left ALONE: writing
|
|
6470
|
+
* that `undefined` back would destroy the user's own pin, and dsh-web reads
|
|
6471
|
+
* a cleared `searchProviderId` as auto-select — where a second usable
|
|
6472
|
+
* provider (the shipped `deepseek-official` is usable whenever a DeepSeek
|
|
6473
|
+
* key resolves) makes EVERY later search throw
|
|
6474
|
+
* `WEB_PROVIDER_AMBIGUOUS`.
|
|
6002
6475
|
*
|
|
6003
6476
|
* Never throws: like the low-level rewrite, a hardened runtime shape degrades
|
|
6004
6477
|
* to registered-but-unselected.
|
|
@@ -6012,14 +6485,16 @@ function applyCommandCodeSearchSelection(web, state, enable) {
|
|
|
6012
6485
|
return;
|
|
6013
6486
|
}
|
|
6014
6487
|
const prior = field.searchProviderId;
|
|
6015
|
-
state.
|
|
6488
|
+
state.preexisting = prior === COMMANDCODE_SEARCH_PROVIDER_ID;
|
|
6489
|
+
state.displaced = state.preexisting ? void 0 : prior;
|
|
6016
6490
|
field.searchProviderId = COMMANDCODE_SEARCH_PROVIDER_ID;
|
|
6017
6491
|
state.owner = true;
|
|
6018
6492
|
return;
|
|
6019
6493
|
}
|
|
6020
6494
|
if (state.owner) {
|
|
6021
6495
|
state.owner = false;
|
|
6022
|
-
field.searchProviderId = state.displaced;
|
|
6496
|
+
if (!state.preexisting) field.searchProviderId = state.displaced;
|
|
6497
|
+
state.preexisting = false;
|
|
6023
6498
|
return;
|
|
6024
6499
|
}
|
|
6025
6500
|
} catch {}
|
|
@@ -6321,8 +6796,8 @@ function buildCommandCodeTuiSection(deps) {
|
|
|
6321
6796
|
label: "API key",
|
|
6322
6797
|
descriptions: { zh: "API 密钥" },
|
|
6323
6798
|
secret: { ref },
|
|
6324
|
-
hint: `Stored in the credential store as ${ref}, never in
|
|
6325
|
-
hintDescriptions: { zh: `保存在凭据库(${ref}
|
|
6799
|
+
hint: `Stored in the credential store as ${ref}, never in configuration files.`,
|
|
6800
|
+
hintDescriptions: { zh: `保存在凭据库(${ref}),不会写入配置文件。` }
|
|
6326
6801
|
},
|
|
6327
6802
|
{
|
|
6328
6803
|
path: ["apiBase"],
|
|
@@ -6600,6 +7075,67 @@ function transportBudgetMessage(failureMessage, maxRetries) {
|
|
|
6600
7075
|
return `${failureMessage};Command Code API 连接在连续重试后仍失败——已停止重试以免整轮卡死。插件已自动重试 ${maxRetries} 次。通常说明当前网络到 api.commandcode.ai 的连接不通,常见原因是代理未生效:DSH 只读取环境变量 HTTPS_PROXY/HTTP_PROXY(系统代理/PAC 不会被读取),请确认启动 DSH 的终端里已导出代理,或先关闭代理直连后重试。网络恢复后直接重发即可 — Command Code API transport still failed after ${maxRetries} automatic retries, which were stopped so the turn could not stall for minutes. This is a connectivity problem between this machine and api.commandcode.ai, not a context-window or request-size rejection. Common cause: the proxy is not reaching the harness — DSH reads HTTPS_PROXY/HTTP_PROXY from the environment only (a Windows system proxy/PAC setting is not consulted), so export it in the launching shell, or turn the proxy off and retry. Resend when the network recovers; the budget is per request and has been reset`;
|
|
6601
7076
|
}
|
|
6602
7077
|
//#endregion
|
|
7078
|
+
//#region src/config-volatile.ts
|
|
7079
|
+
/**
|
|
7080
|
+
* Mark one schema field volatile when the installed schemastery supports it.
|
|
7081
|
+
* @param schema - The field schema (any generation).
|
|
7082
|
+
* @returns The schema carrying `meta.volatile`, or the schema itself on an
|
|
7083
|
+
* engine whose schemastery predates `.volatile()`.
|
|
7084
|
+
*/
|
|
7085
|
+
function markVolatile(schema) {
|
|
7086
|
+
const candidate = schema;
|
|
7087
|
+
if (typeof candidate.volatile !== "function") return schema;
|
|
7088
|
+
return candidate.volatile();
|
|
7089
|
+
}
|
|
7090
|
+
/**
|
|
7091
|
+
* Mark every field of a Config field dict volatile, except the named
|
|
7092
|
+
* composition-only secrets (`apiKey` here, which no settings surface writes).
|
|
7093
|
+
*
|
|
7094
|
+
* The result is a NEW dict of schema instances: callers that also need an
|
|
7095
|
+
* UNMARKED schema for the legacy settings generation must build the fields
|
|
7096
|
+
* twice (`configFields()`), never share one dict — `.volatile()` clones per
|
|
7097
|
+
* call, and a shared instance would carry the mark into the legacy schema.
|
|
7098
|
+
*
|
|
7099
|
+
* @param fields - Freshly built field schemas.
|
|
7100
|
+
* @param skip - Field names that must stay unmarked.
|
|
7101
|
+
* @returns The field dict with every non-skipped field marked.
|
|
7102
|
+
*/
|
|
7103
|
+
function markVolatileFields(fields, skip = []) {
|
|
7104
|
+
return Object.fromEntries(Object.entries(fields).map(([name, schema]) => [name, skip.includes(name) ? schema : markVolatile(schema)]));
|
|
7105
|
+
}
|
|
7106
|
+
/**
|
|
7107
|
+
* Whether a resolved config value is a volatile reference.
|
|
7108
|
+
*
|
|
7109
|
+
* Duck-typed on the two properties `createVolatile` produces (frozen object
|
|
7110
|
+
* with a `get` method) rather than cosmokit's private `write` symbol, because
|
|
7111
|
+
* cosmokit is not a peer of this bundle. Config values are plain parsed JSON
|
|
7112
|
+
* (or `!!jsExpr` wrappers), which are never frozen, so the test cannot
|
|
7113
|
+
* misfire on an ordinary field.
|
|
7114
|
+
*/
|
|
7115
|
+
function isVolatileRef(value) {
|
|
7116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.isFrozen(value) && typeof value.get === "function";
|
|
7117
|
+
}
|
|
7118
|
+
/**
|
|
7119
|
+
* Read one config object as PLAIN values across generations.
|
|
7120
|
+
*
|
|
7121
|
+
* Each top-level field found to be a volatile reference is read through
|
|
7122
|
+
* `.get()` — freshly on every call, so a live update lands on the very next
|
|
7123
|
+
* read. When no field is a reference the input object is returned as-is,
|
|
7124
|
+
* keeping reference identity stable for identity-keyed memos on engines that
|
|
7125
|
+
* hand `apply()` plain config.
|
|
7126
|
+
*/
|
|
7127
|
+
function unwrapVolatileConfig(config) {
|
|
7128
|
+
const source = config;
|
|
7129
|
+
if (typeof source !== "object" || source === null) return config;
|
|
7130
|
+
let found = false;
|
|
7131
|
+
const plain = {};
|
|
7132
|
+
for (const [field, value] of Object.entries(source)) if (isVolatileRef(value)) {
|
|
7133
|
+
plain[field] = value.get();
|
|
7134
|
+
found = true;
|
|
7135
|
+
} else plain[field] = value;
|
|
7136
|
+
return found ? plain : config;
|
|
7137
|
+
}
|
|
7138
|
+
//#endregion
|
|
6603
7139
|
//#region src/index.ts
|
|
6604
7140
|
/**
|
|
6605
7141
|
* dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
|
|
@@ -6609,9 +7145,10 @@ function transportBudgetMessage(failureMessage, maxRetries) {
|
|
|
6609
7145
|
* the configurable-provider directory, so the web Models page shows a
|
|
6610
7146
|
* "Command Code" card with an API-key field and the model picker lists the
|
|
6611
7147
|
* live Command Code model catalog. Connection facts resolve per request over
|
|
6612
|
-
* the
|
|
6613
|
-
*
|
|
6614
|
-
*
|
|
7148
|
+
* the plugin's live config — the `llm-commandcode` settings section through
|
|
7149
|
+
* dsh 0.1.6, profile Config (volatile fields, unwrapped per read) from 0.1.7
|
|
7150
|
+
* — and the credential seam, so a changed key, endpoint, or cache path
|
|
7151
|
+
* reaches the next request without a restart.
|
|
6615
7152
|
*
|
|
6616
7153
|
* ```yaml
|
|
6617
7154
|
* - id: llm-commandcode
|
|
@@ -6636,41 +7173,84 @@ const DEFAULT_API_KEY_ENV = "COMMANDCODE_API_KEY";
|
|
|
6636
7173
|
const PROVIDER = "commandcode";
|
|
6637
7174
|
/** Default models cache path (mirrors the pi plugin's on-disk cache). */
|
|
6638
7175
|
const DEFAULT_MODELS_CACHE_PATH = join(homedir(), ".commandcode", "models-cache.json");
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
7176
|
+
/**
|
|
7177
|
+
* The Config schema's fields, freshly built on every call.
|
|
7178
|
+
*
|
|
7179
|
+
* A FACTORY, not a shared dict, and that is load-bearing: the exported
|
|
7180
|
+
* {@link Config} marks every field volatile while {@link LegacySettingsSchema}
|
|
7181
|
+
* must carry no mark at all, and schemastery's schema instances are
|
|
7182
|
+
* single-purpose objects — sharing one dict would leak the marks into the
|
|
7183
|
+
* legacy registration (see `config-volatile.ts` for why that is a boot-time
|
|
7184
|
+
* `ValidationError` on ≤0.1.6 engines whose schemastery resolved to ≥3.18.3).
|
|
7185
|
+
*
|
|
7186
|
+
* @returns One fresh schema instance per Config field.
|
|
7187
|
+
*/
|
|
7188
|
+
function configFields() {
|
|
7189
|
+
return {
|
|
7190
|
+
apiKeyEnv: z.string().role("credential-ref").default(DEFAULT_API_KEY_ENV),
|
|
7191
|
+
apiKey: z.string().role("secret"),
|
|
7192
|
+
apiBase: z.string(),
|
|
7193
|
+
workingDir: z.string(),
|
|
7194
|
+
modelsCachePath: z.string(),
|
|
7195
|
+
requestTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
|
|
7196
|
+
streamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
|
|
7197
|
+
transportMaxRetries: z.number().min(0).max(50),
|
|
7198
|
+
filterModelsByPlan: z.boolean(),
|
|
7199
|
+
visibleModels: z.array(z.string()),
|
|
7200
|
+
/**
|
|
7201
|
+
* Per-model visibility overrides for the terminal settings page's checkbox
|
|
7202
|
+
* list, keyed by catalog id. dsh-TUI addresses a staged edit by its field
|
|
7203
|
+
* PATH, so two checkboxes sharing one path would overwrite each other's
|
|
7204
|
+
* draft and the section's last model would decide every write; a map gives
|
|
7205
|
+
* each checkbox a path of its own. An id listed here wins over
|
|
7206
|
+
* {@link Config.visibleModels}; ids absent here keep following it.
|
|
7207
|
+
*/
|
|
7208
|
+
modelVisibility: z.dict(z.boolean()),
|
|
7209
|
+
webSearch: z.boolean().default(true),
|
|
7210
|
+
showSidebarQuota: z.boolean().default(false),
|
|
7211
|
+
accounts: z.array(z.object({
|
|
7212
|
+
label: z.string(),
|
|
7213
|
+
apiKeyEnv: z.string().role("credential-ref"),
|
|
7214
|
+
/** Literal key for one extra slot; see the top-level `apiKey` secret note. */
|
|
7215
|
+
apiKey: z.string().role("secret")
|
|
7216
|
+
})),
|
|
7217
|
+
activeAccount: z.string(),
|
|
7218
|
+
modelAccountRules: z.array(z.object({
|
|
7219
|
+
models: z.array(z.string()),
|
|
7220
|
+
account: z.string()
|
|
7221
|
+
})),
|
|
7222
|
+
lang: z.string().pattern(/^(zh|en)$/).default("zh")
|
|
7223
|
+
};
|
|
7224
|
+
}
|
|
7225
|
+
/**
|
|
7226
|
+
* The 0.1.7-generation schema: every field volatile except the
|
|
7227
|
+
* composition-only `apiKey` secret.
|
|
7228
|
+
*
|
|
7229
|
+
* dsh 0.1.7's settings forms are projected from the schema's `meta.volatile`
|
|
7230
|
+
* nodes, so an unmarked field would be invisible to AND unwritable from the
|
|
7231
|
+
* settings page and refused by form-edit path validation — and on that
|
|
7232
|
+
* generation the loader hands `apply()` a live reference per marked field,
|
|
7233
|
+
* committing later writes without remounting this fiber (the
|
|
7234
|
+
* `loader/volatile-update` listener at the bottom of `apply` covers the facts
|
|
7235
|
+
* that are not re-derived per read). The mark is inert on engines whose
|
|
7236
|
+
* schemastery predates `.volatile()`.
|
|
7237
|
+
*/
|
|
7238
|
+
const Config = z.object(markVolatileFields(configFields(), ["apiKey"]));
|
|
7239
|
+
/**
|
|
7240
|
+
* The ≤0.1.6-generation registration schema: the same fields, NO volatile
|
|
7241
|
+
* marks.
|
|
7242
|
+
*
|
|
7243
|
+
* The legacy `installSection(owner, ns, schema, entry, hooks)` re-validates the
|
|
7244
|
+
* `entry` it is handed (`register()` → `resolve()` → `schema(mergeLayers(base,
|
|
7245
|
+
* section))`) and its `describe()` structuredClones that same `entry`. Since
|
|
7246
|
+
* schemastery ≥3.18.3 creates references at parse time on EVERY generation —
|
|
7247
|
+
* `dsh-settings` through 0.1.6 declares `schemastery: ^3.18.2`, so old engines
|
|
7248
|
+
* freshly installed today resolve 3.18.3 — a marked schema plus the raw
|
|
7249
|
+
* reference-carrying `config` is a boot-time `ValidationError` there. This
|
|
7250
|
+
* schema is therefore unmarked, and `apply` pairs it with
|
|
7251
|
+
* `unwrapVolatileConfig(config)`.
|
|
7252
|
+
*/
|
|
7253
|
+
const LegacySettingsSchema = z.object(configFields());
|
|
6674
7254
|
/**
|
|
6675
7255
|
* The one explicit resolve step from raw config to validated connection
|
|
6676
7256
|
* facts. Programmatic construction may bypass Schemastery normalization, so
|
|
@@ -6704,7 +7284,8 @@ function readModelVisibility(raw) {
|
|
|
6704
7284
|
}
|
|
6705
7285
|
function apply(ctx, config) {
|
|
6706
7286
|
installCostProjection(ctx);
|
|
6707
|
-
let
|
|
7287
|
+
let source = () => config;
|
|
7288
|
+
const current = () => unwrapVolatileConfig(source());
|
|
6708
7289
|
let lastRaw;
|
|
6709
7290
|
let lastGood;
|
|
6710
7291
|
const options = () => {
|
|
@@ -6917,18 +7498,38 @@ function apply(ctx, config) {
|
|
|
6917
7498
|
}, "dsh-commandcode-provider: tui settings handle");
|
|
6918
7499
|
});
|
|
6919
7500
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
6920
|
-
settingsCtx.settings
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
7501
|
+
const service = settingsCtx.settings;
|
|
7502
|
+
if (typeof service.installSection === "function") {
|
|
7503
|
+
service.installSection(ctx, NS, LegacySettingsSchema, unwrapVolatileConfig(config), {
|
|
7504
|
+
setSource: (next) => {
|
|
7505
|
+
source = next;
|
|
7506
|
+
},
|
|
7507
|
+
onChange: () => {
|
|
7508
|
+
applySearchSelection(current().webSearch ?? true);
|
|
7509
|
+
refreshTuiSettings?.();
|
|
7510
|
+
}
|
|
7511
|
+
});
|
|
7512
|
+
return;
|
|
7513
|
+
}
|
|
7514
|
+
if (typeof service.configure === "function") settingsCtx.effect(() => {
|
|
7515
|
+
const dispose = service.configure?.({ auto: false }, ctx.fiber);
|
|
7516
|
+
return () => {
|
|
7517
|
+
dispose?.();
|
|
7518
|
+
};
|
|
7519
|
+
}, "dsh-commandcode-provider: settings auto-form policy");
|
|
6929
7520
|
});
|
|
7521
|
+
const loaderEvents = ctx;
|
|
7522
|
+
ctx.effect(() => {
|
|
7523
|
+
const off = loaderEvents.on("loader/volatile-update", () => {
|
|
7524
|
+
applySearchSelection(current().webSearch ?? true);
|
|
7525
|
+
refreshTuiSettings?.();
|
|
7526
|
+
});
|
|
7527
|
+
return () => {
|
|
7528
|
+
if (typeof off === "function") off();
|
|
7529
|
+
};
|
|
7530
|
+
}, "dsh-commandcode-provider: volatile config updates");
|
|
6930
7531
|
}
|
|
6931
7532
|
//#endregion
|
|
6932
|
-
export { ACTIVE_ACCOUNT_AUTO, BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LANG_AUTO, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommandCodeSearchSelection, applyCommandCodeTuiSettings, applyCommands, applyUsageRemote, buildCommandAuthUrl, buildCommandCodeTuiSection, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, selectCommandCodeSearchProvider, studioBaseForApiBase, subscriptionPlanInfo, usageReportSchema, validateCommandApiKey };
|
|
7533
|
+
export { ACTIVE_ACCOUNT_AUTO, BILLING_ACCESS_TTL_MS, COMMANDCODE_SEARCH_PROVIDER_ID, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, CommandCodeLoginFlow, CommandCodeSearchProvider, CommandCodeUsageService, Config, DEFAULT_API_BASE, DEFAULT_GENERATE_MAX_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, DEFAULT_MODELS_CACHE_PATH, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_WEB_SEARCH_PROVIDER_ID, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, LANG_AUTO, LOGIN_ALLOWED_ORIGINS, LOGIN_BEGIN_ENDPOINT, LOGIN_BODY_LIMIT_BYTES, LOGIN_CANCEL_ENDPOINT, LOGIN_MAX_PORT_ATTEMPTS, LOGIN_START_PORT, LOGIN_STATUS_ENDPOINT, LOGIN_TIMEOUT_MS, LegacySettingsSchema, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommandCodeSearchSelection, applyCommandCodeTuiSettings, applyCommands, applyUsageRemote, buildCommandAuthUrl, buildCommandCodeTuiSection, capabilityDescription, commandCodeSearchSelection, commandDefinition, compareByPlan, dealLabel, formatContext, inject, loginStatusSchema, matchModelRule, modelVisibleInPlan, name, parseLoginStatus, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectAccountForModel, selectActiveAccount, selectCommandCodeSearchProvider, studioBaseForApiBase, subscriptionPlanInfo, usageReportSchema, validateCommandApiKey };
|
|
6933
7534
|
|
|
6934
7535
|
//# sourceMappingURL=index.js.map
|