@mars-sea/dsh-commandcode-provider 0.11.8 → 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 +39 -0
- package/icon.svg +5 -0
- package/lib/client.js +470 -22
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +93 -5
- package/lib/index.js +702 -182
- 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";
|
|
@@ -2652,6 +2652,181 @@ function scaleBy(size, factor) {
|
|
|
2652
2652
|
};
|
|
2653
2653
|
}
|
|
2654
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
|
|
2655
2830
|
//#region src/adapter.ts
|
|
2656
2831
|
/**
|
|
2657
2832
|
* DeepSeek Harness LLM adapter for the Command Code Provider API.
|
|
@@ -2767,15 +2942,21 @@ function periodEndValue(value) {
|
|
|
2767
2942
|
* Terminal stream-error markers from the official CLI (`Xw` in command-code's
|
|
2768
2943
|
* cli.mjs): these always mean "retrying cannot succeed", so the adapter must
|
|
2769
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).
|
|
2770
2951
|
*/
|
|
2771
2952
|
const TERMINAL_STREAM_ERROR_MARKERS = [
|
|
2772
|
-
"
|
|
2773
|
-
"
|
|
2953
|
+
"premium credits exhausted",
|
|
2954
|
+
"model not in plan",
|
|
2774
2955
|
"insufficient credits"
|
|
2775
2956
|
];
|
|
2776
2957
|
function hasTerminalStreamMarker(message) {
|
|
2777
|
-
const
|
|
2778
|
-
return TERMINAL_STREAM_ERROR_MARKERS.some((marker) =>
|
|
2958
|
+
const normalized = message.toLowerCase().replaceAll("_", " ");
|
|
2959
|
+
return TERMINAL_STREAM_ERROR_MARKERS.some((marker) => normalized.includes(marker));
|
|
2779
2960
|
}
|
|
2780
2961
|
/**
|
|
2781
2962
|
* Context-window wording the provider uses to reject a request that outgrew
|
|
@@ -2814,7 +2995,10 @@ function isContextOverflowDetail(detail) {
|
|
|
2814
2995
|
* (issue #39).
|
|
2815
2996
|
* - Credits/plan wording is terminal, so an exhausted balance or a model
|
|
2816
2997
|
* outside the plan is not retried as if it were a transient rate limit.
|
|
2817
|
-
* - 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`.
|
|
2818
3002
|
*/
|
|
2819
3003
|
function streamErrorToLlmError(value, fallbackMessage) {
|
|
2820
3004
|
const record = isRecord(value) ? value : void 0;
|
|
@@ -2829,7 +3013,7 @@ function streamErrorToLlmError(value, fallbackMessage) {
|
|
|
2829
3013
|
const statusOption = statusCode !== void 0 ? { status: statusCode } : void 0;
|
|
2830
3014
|
if (isContextOverflowDetail(detail)) return new LlmError(`Command Code stream error: ${message};Command Code API 拒绝了这次请求:内容超出模型上下文窗口。正在压缩上下文后重试;如仍失败,请新建会话或减少上下文`, CONTEXT_WINDOW_EXCEEDED_CODE, statusOption);
|
|
2831
3015
|
const terminal = hasTerminalStreamMarker(detail);
|
|
2832
|
-
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);
|
|
2833
3017
|
return new LlmError(`Command Code stream error: ${message}`, "SERVER", statusOption);
|
|
2834
3018
|
}
|
|
2835
3019
|
function recordOrEmpty(value) {
|
|
@@ -3033,42 +3217,32 @@ async function writeModelsCache(cachePath, models) {
|
|
|
3033
3217
|
}
|
|
3034
3218
|
}
|
|
3035
3219
|
/**
|
|
3036
|
-
*
|
|
3037
|
-
*
|
|
3038
|
-
*
|
|
3039
|
-
*
|
|
3040
|
-
*
|
|
3041
|
-
*
|
|
3042
|
-
* field outright (`dsh-llm-deepseek` never reads it). A third-party plugin that
|
|
3043
|
-
* assembles its own history can therefore omit it and run correctly everywhere
|
|
3044
|
-
* else — dsh-mneme's memory pipeline did — which made this adapter, the first
|
|
3045
|
-
* to actually read the field, the first to crash on such a message with
|
|
3046
|
-
* `TypeError: Cannot read properties of undefined (reading 'kind')` in the
|
|
3047
|
-
* serialization phase, 0–8 ms in, before any request left the machine and with
|
|
3048
|
-
* nothing in the error naming the message or the producer (issue #47).
|
|
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.
|
|
3049
3226
|
*
|
|
3050
|
-
*
|
|
3051
|
-
*
|
|
3052
|
-
*
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
* instead of sending them.
|
|
3066
|
-
*/
|
|
3067
|
-
function isToolResultMessage(message) {
|
|
3068
|
-
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;
|
|
3069
3242
|
const kind = message.source?.kind;
|
|
3070
|
-
if (kind !== void 0
|
|
3071
|
-
|
|
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;
|
|
3072
3246
|
}
|
|
3073
3247
|
/**
|
|
3074
3248
|
* Collect the tool calls that have a paired tool result, plus each call's
|
|
@@ -3081,12 +3255,13 @@ function pairedToolCalls(messages) {
|
|
|
3081
3255
|
const callIds = /* @__PURE__ */ new Set();
|
|
3082
3256
|
const names = /* @__PURE__ */ new Map();
|
|
3083
3257
|
const resultIds = /* @__PURE__ */ new Set();
|
|
3084
|
-
for (const message of messages)
|
|
3085
|
-
|
|
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") {
|
|
3086
3262
|
callIds.add(block.id);
|
|
3087
3263
|
names.set(block.id, block.name);
|
|
3088
3264
|
}
|
|
3089
|
-
if (block.type === "tool-result") resultIds.add(block.toolCallId);
|
|
3090
3265
|
}
|
|
3091
3266
|
return {
|
|
3092
3267
|
ids: new Set([...callIds].filter((id) => resultIds.has(id))),
|
|
@@ -3394,7 +3569,8 @@ async function messagesToCC(messages, readImage) {
|
|
|
3394
3569
|
};
|
|
3395
3570
|
for (const message of messages) {
|
|
3396
3571
|
if (message.role === "system") continue;
|
|
3397
|
-
|
|
3572
|
+
const result = toolResultOf(message);
|
|
3573
|
+
if (message.role === "user" && !result) {
|
|
3398
3574
|
flushPendingImages();
|
|
3399
3575
|
const parts = [];
|
|
3400
3576
|
for (const block of message.content) {
|
|
@@ -3437,18 +3613,17 @@ async function messagesToCC(messages, readImage) {
|
|
|
3437
3613
|
});
|
|
3438
3614
|
continue;
|
|
3439
3615
|
}
|
|
3440
|
-
if (
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
const
|
|
3444
|
-
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;
|
|
3445
3620
|
out.push({
|
|
3446
3621
|
role: "tool",
|
|
3447
3622
|
content: [{
|
|
3448
3623
|
type: "tool-result",
|
|
3449
3624
|
toolCallId: wireToolCallId,
|
|
3450
|
-
toolName: toolNames.get(
|
|
3451
|
-
output:
|
|
3625
|
+
toolName: toolNames.get(result.toolCallId) || "unknown",
|
|
3626
|
+
output: result.isError ? {
|
|
3452
3627
|
type: "error-text",
|
|
3453
3628
|
value: toolResultTextForWire(media)
|
|
3454
3629
|
} : {
|
|
@@ -3506,7 +3681,8 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
3506
3681
|
};
|
|
3507
3682
|
for (const message of messages) {
|
|
3508
3683
|
if (message.role === "system") continue;
|
|
3509
|
-
|
|
3684
|
+
const result = toolResultOf(message);
|
|
3685
|
+
if (message.role === "user" && !result) {
|
|
3510
3686
|
flushPendingImages();
|
|
3511
3687
|
const parts = [];
|
|
3512
3688
|
for (const block of message.content) {
|
|
@@ -3552,11 +3728,10 @@ async function messagesToOpenAI(messages, readImage) {
|
|
|
3552
3728
|
out.push(assistant);
|
|
3553
3729
|
continue;
|
|
3554
3730
|
}
|
|
3555
|
-
if (
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
const
|
|
3559
|
-
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;
|
|
3560
3735
|
out.push({
|
|
3561
3736
|
role: "tool",
|
|
3562
3737
|
tool_call_id: wireToolCallId,
|
|
@@ -3817,6 +3992,7 @@ function createBlockAssembler() {
|
|
|
3817
3992
|
reasoningIndex: -1,
|
|
3818
3993
|
reasoningContent: "",
|
|
3819
3994
|
sawContent: false,
|
|
3995
|
+
finishReason: void 0,
|
|
3820
3996
|
openAiToolCalls: []
|
|
3821
3997
|
};
|
|
3822
3998
|
}
|
|
@@ -4036,8 +4212,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4036
4212
|
/** The headers every authenticated account endpoint shares. */
|
|
4037
4213
|
async accountHeaders(apiKey) {
|
|
4038
4214
|
const connection = this.deps.options();
|
|
4215
|
+
const raw = apiKey ?? await this.deps.resolveApiKey(connection);
|
|
4216
|
+
const named = connection.apiKeyEnv;
|
|
4039
4217
|
return {
|
|
4040
|
-
Authorization: `Bearer ${
|
|
4218
|
+
Authorization: `Bearer ${assertUsableApiKey(raw, "llm-commandcode", typeof named === "string" && named !== "" ? named : "the stored credential")}`,
|
|
4041
4219
|
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
4042
4220
|
"x-cli-environment": "production",
|
|
4043
4221
|
...attributionHeaders()
|
|
@@ -4049,11 +4227,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4049
4227
|
* failure accounting: the billing probe fails open silently, the usage
|
|
4050
4228
|
* report books failures per endpoint. Non-2xx and non-record bodies come
|
|
4051
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.
|
|
4052
4235
|
*/
|
|
4053
|
-
async fetchEndpointJson(url, headers) {
|
|
4236
|
+
async fetchEndpointJson(url, headers, timeoutMs = MODELS_TIMEOUT_MS) {
|
|
4054
4237
|
const response = await this.fetchImpl(url, {
|
|
4055
4238
|
headers,
|
|
4056
|
-
signal: AbortSignal.timeout(
|
|
4239
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
4057
4240
|
});
|
|
4058
4241
|
if (!response.ok) return { status: response.status };
|
|
4059
4242
|
const parsed = await response.json();
|
|
@@ -4211,15 +4394,39 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4211
4394
|
* view. Requires a usable API key (throws `MISSING_CREDENTIAL` otherwise).
|
|
4212
4395
|
* Pass `apiKey` to report on a specific account of a multi-account pool;
|
|
4213
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.
|
|
4214
4411
|
*/
|
|
4215
4412
|
async getUsage(apiKey) {
|
|
4216
|
-
const
|
|
4217
|
-
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
|
+
}
|
|
4218
4425
|
const failures = [];
|
|
4219
4426
|
const failedStatuses = [];
|
|
4220
4427
|
const getJson = async (path) => {
|
|
4221
4428
|
try {
|
|
4222
|
-
const { status, record } = await this.fetchEndpointJson(`${base}${path}`, headers);
|
|
4429
|
+
const { status, record } = await this.fetchEndpointJson(`${base}${path}`, headers, connection.requestTimeoutMs);
|
|
4223
4430
|
if (record === void 0) {
|
|
4224
4431
|
failures.push(`${path}: HTTP ${status}`);
|
|
4225
4432
|
failedStatuses.push(status);
|
|
@@ -4305,7 +4512,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4305
4512
|
reported = true;
|
|
4306
4513
|
if (!window.exceeded) continue;
|
|
4307
4514
|
exceeded = true;
|
|
4308
|
-
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);
|
|
4309
4516
|
}
|
|
4310
4517
|
if (!reported) return void 0;
|
|
4311
4518
|
return {
|
|
@@ -4413,6 +4620,19 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4413
4620
|
const reader = response.body.getReader();
|
|
4414
4621
|
const decoder = new TextDecoder();
|
|
4415
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
|
+
});
|
|
4416
4636
|
let idleTimer;
|
|
4417
4637
|
let idleFired = false;
|
|
4418
4638
|
const armIdle = () => {
|
|
@@ -4429,9 +4649,16 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4429
4649
|
}
|
|
4430
4650
|
};
|
|
4431
4651
|
const asm = createBlockAssembler();
|
|
4432
|
-
|
|
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
|
+
}
|
|
4433
4661
|
try {
|
|
4434
|
-
let finished = false;
|
|
4435
4662
|
for (;;) {
|
|
4436
4663
|
let read;
|
|
4437
4664
|
armIdle();
|
|
@@ -4439,44 +4666,104 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
4439
4666
|
read = await reader.read();
|
|
4440
4667
|
} catch (error) {
|
|
4441
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
|
+
});
|
|
4442
4675
|
throw new LlmError(`Command Code API stream from ${connection.apiBase} failed while reading: ${errorChain(error)};Command Code API 流式响应中途断开——网络波动所致,重试通常可恢复`, "TRANSPORT", { cause: error });
|
|
4443
4676
|
} finally {
|
|
4444
4677
|
clearIdle();
|
|
4445
4678
|
}
|
|
4446
4679
|
const { done, value } = read;
|
|
4447
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
|
+
});
|
|
4448
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");
|
|
4449
|
-
if (buffer.trim())
|
|
4450
|
-
yield chunk;
|
|
4451
|
-
if (chunk.type === "finish") finished = true;
|
|
4452
|
-
}
|
|
4691
|
+
if (buffer.trim()) yield* handle(parseStreamEventLine(buffer));
|
|
4453
4692
|
break;
|
|
4454
4693
|
}
|
|
4455
|
-
|
|
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
|
+
});
|
|
4456
4704
|
const lines = buffer.split("\n");
|
|
4457
4705
|
buffer = lines.pop() ?? "";
|
|
4458
|
-
for (const line of lines)
|
|
4459
|
-
|
|
4460
|
-
for (const chunk of chunks) {
|
|
4461
|
-
yield chunk;
|
|
4462
|
-
if (chunk.type === "finish") finished = true;
|
|
4463
|
-
}
|
|
4464
|
-
}
|
|
4465
|
-
if (finished) break;
|
|
4706
|
+
for (const line of lines) yield* handle(parseStreamEventLine(line));
|
|
4707
|
+
if (finish !== void 0) break;
|
|
4466
4708
|
}
|
|
4467
|
-
if (
|
|
4468
|
-
|
|
4469
|
-
yield* closeReasoning(asm);
|
|
4470
|
-
if (protocol === "openai") yield* emitOpenAiToolCalls(asm);
|
|
4471
|
-
if (!asm.sawContent) throw new LlmError("Command Code returned an empty response;Command Code 返回了空响应,重试通常可恢复", "EMPTY_RESPONSE");
|
|
4709
|
+
if (usage !== void 0) {
|
|
4710
|
+
usageEmitted = true;
|
|
4472
4711
|
yield {
|
|
4473
|
-
type: "
|
|
4474
|
-
|
|
4712
|
+
type: "usage",
|
|
4713
|
+
usage
|
|
4475
4714
|
};
|
|
4476
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;
|
|
4477
4763
|
} finally {
|
|
4478
4764
|
clearIdle();
|
|
4479
4765
|
cleanup();
|
|
4766
|
+
trace.close();
|
|
4480
4767
|
await reader.cancel().catch(() => void 0);
|
|
4481
4768
|
reader.releaseLock();
|
|
4482
4769
|
}
|
|
@@ -4567,7 +4854,7 @@ function handleCliEvent(asm, event) {
|
|
|
4567
4854
|
}
|
|
4568
4855
|
const delta = stringValue(event.text) ?? "";
|
|
4569
4856
|
asm.textContent += delta;
|
|
4570
|
-
asm.sawContent = true;
|
|
4857
|
+
if (delta.trim() !== "") asm.sawContent = true;
|
|
4571
4858
|
chunks.push({
|
|
4572
4859
|
type: "text-delta",
|
|
4573
4860
|
index: asm.textIndex,
|
|
@@ -4630,6 +4917,7 @@ function handleCliEvent(asm, event) {
|
|
|
4630
4917
|
break;
|
|
4631
4918
|
}
|
|
4632
4919
|
case "finish": {
|
|
4920
|
+
asm.finishReason = stringValue(event.finishReason);
|
|
4633
4921
|
chunks.push(...closeText(asm), ...closeReasoning(asm));
|
|
4634
4922
|
const usage = isRecord(event.totalUsage) ? event.totalUsage : void 0;
|
|
4635
4923
|
if (usage) {
|
|
@@ -4643,6 +4931,8 @@ function handleCliEvent(asm, event) {
|
|
|
4643
4931
|
cacheReadTokens: cacheRead,
|
|
4644
4932
|
cacheWriteTokens: cacheWrite
|
|
4645
4933
|
};
|
|
4934
|
+
const reasoningTokens = numberValue((isRecord(usage.outputTokenDetails) ? usage.outputTokenDetails : void 0)?.reasoningTokens) ?? numberValue(usage.reasoningTokens);
|
|
4935
|
+
if (reasoningTokens !== void 0) tokenUsage.reasoningTokens = reasoningTokens;
|
|
4646
4936
|
chunks.push({
|
|
4647
4937
|
type: "usage",
|
|
4648
4938
|
usage: tokenUsage
|
|
@@ -4707,39 +4997,38 @@ function handleOpenAIEvent(asm, event) {
|
|
|
4707
4997
|
});
|
|
4708
4998
|
}
|
|
4709
4999
|
asm.textContent += contentDelta;
|
|
4710
|
-
asm.sawContent = true;
|
|
5000
|
+
if (contentDelta.trim() !== "") asm.sawContent = true;
|
|
4711
5001
|
chunks.push({
|
|
4712
5002
|
type: "text-delta",
|
|
4713
5003
|
index: asm.textIndex,
|
|
4714
5004
|
text: contentDelta
|
|
4715
5005
|
});
|
|
4716
5006
|
}
|
|
4717
|
-
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;
|
|
4718
5009
|
asm.sawContent = true;
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
if (name !== void 0 && existing.name === "") existing.name = name;
|
|
4738
|
-
if (argDelta !== void 0) existing.arguments += argDelta;
|
|
4739
|
-
}
|
|
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;
|
|
4740
5028
|
}
|
|
4741
5029
|
}
|
|
4742
5030
|
if (choice.finish_reason !== void 0 && choice.finish_reason !== null) {
|
|
5031
|
+
asm.finishReason = stringValue(choice.finish_reason);
|
|
4743
5032
|
chunks.push(...closeText(asm), ...closeReasoning(asm), ...emitOpenAiToolCalls(asm));
|
|
4744
5033
|
if (event.usage !== void 0) chunks.push({
|
|
4745
5034
|
type: "usage",
|
|
@@ -4787,6 +5076,13 @@ function mapFinishReason(reason) {
|
|
|
4787
5076
|
if (reason === "length" || reason === "max_tokens" || reason === "max-tokens" || reason === "max_output_tokens") return { kind: "max-tokens" };
|
|
4788
5077
|
return { kind: "stop" };
|
|
4789
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
|
+
}
|
|
4790
5086
|
/**
|
|
4791
5087
|
* True when a Provider API pre-stream rejection is Command Code's Go-plan
|
|
4792
5088
|
* gate (`upgrade_required`). Only this exact class of rejection should fall
|
|
@@ -4831,8 +5127,25 @@ function mapFinishReason(reason) {
|
|
|
4831
5127
|
* or a model outside its plan (`MODEL_NOT_IN_PLAN`). Rotating must NOT mark
|
|
4832
5128
|
* the account: the fact is about the model or the balance, it can change
|
|
4833
5129
|
* without the key changing, and a `:free` model can still be served by an
|
|
4834
|
-
* 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.
|
|
4835
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
|
+
]);
|
|
4836
5149
|
function classifyAccountRejection(status, errText) {
|
|
4837
5150
|
if (status === 429 || readProviderErrorCode(errText)?.toUpperCase() === "RATE_LIMITED") {
|
|
4838
5151
|
const evidence = readWindowLimitEvidence(errText);
|
|
@@ -4843,10 +5156,11 @@ function classifyAccountRejection(status, errText) {
|
|
|
4843
5156
|
};
|
|
4844
5157
|
}
|
|
4845
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" };
|
|
4846
5161
|
if (status < 400 || status >= 500) return void 0;
|
|
4847
5162
|
const lower = errText.toLowerCase().replaceAll("_", " ");
|
|
4848
|
-
|
|
4849
|
-
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" };
|
|
4850
5164
|
}
|
|
4851
5165
|
/** The machine-readable `error.code`/`error.type` of a pre-stream failure body. */
|
|
4852
5166
|
function readProviderErrorCode(errText) {
|
|
@@ -4860,6 +5174,16 @@ function readProviderErrorCode(errText) {
|
|
|
4860
5174
|
}
|
|
4861
5175
|
}
|
|
4862
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
|
+
/**
|
|
4863
5187
|
* The usage-window evidence carried by a rejected request, mirroring the CLI's
|
|
4864
5188
|
* `parseWindowLimitError` → `resolveWindowLabel`/`extractResetAtMs` pair.
|
|
4865
5189
|
*
|
|
@@ -4893,7 +5217,8 @@ function readWindowLimitEvidence(errText) {
|
|
|
4893
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;
|
|
4894
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];
|
|
4895
5219
|
const stamped = stamp === void 0 ? void 0 : Date.parse(stamp);
|
|
4896
|
-
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;
|
|
4897
5222
|
return {
|
|
4898
5223
|
...window === void 0 ? {} : { window },
|
|
4899
5224
|
...resetAtMs === void 0 ? {} : { resetAtMs }
|
|
@@ -5693,7 +6018,10 @@ function isCallbackCredentials(value) {
|
|
|
5693
6018
|
/**
|
|
5694
6019
|
* One browser-login attempt machine. Single-flight by design: `begin()` while
|
|
5695
6020
|
* waiting returns the live attempt's status instead of starting a second one;
|
|
5696
|
-
* 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.
|
|
5697
6025
|
*/
|
|
5698
6026
|
var CommandCodeLoginFlow = class {
|
|
5699
6027
|
deps;
|
|
@@ -5711,6 +6039,17 @@ var CommandCodeLoginFlow = class {
|
|
|
5711
6039
|
* the user cancelled and flipping the page back to success.
|
|
5712
6040
|
*/
|
|
5713
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;
|
|
5714
6053
|
disposed = false;
|
|
5715
6054
|
constructor(deps) {
|
|
5716
6055
|
this.deps = deps;
|
|
@@ -5732,6 +6071,22 @@ var CommandCodeLoginFlow = class {
|
|
|
5732
6071
|
async begin() {
|
|
5733
6072
|
if (this.disposed) throw new Error("login flow has been disposed");
|
|
5734
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() {
|
|
5735
6090
|
this.teardown();
|
|
5736
6091
|
const attempt = ++this.attemptSeq;
|
|
5737
6092
|
const port = await this.findPort();
|
|
@@ -5743,6 +6098,16 @@ var CommandCodeLoginFlow = class {
|
|
|
5743
6098
|
};
|
|
5744
6099
|
});
|
|
5745
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
|
+
}
|
|
5746
6111
|
const apiBase = this.readApiBase();
|
|
5747
6112
|
this.setStatus({
|
|
5748
6113
|
state: "waiting",
|
|
@@ -5764,19 +6129,29 @@ var CommandCodeLoginFlow = class {
|
|
|
5764
6129
|
settled.then((credentials) => this.complete(attempt, credentials), (failure) => this.failFrom(attempt, failure));
|
|
5765
6130
|
return this.statusValue;
|
|
5766
6131
|
}
|
|
5767
|
-
/**
|
|
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
|
+
*/
|
|
5768
6138
|
cancel() {
|
|
5769
|
-
if (this.disposed
|
|
5770
|
-
this.
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
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;
|
|
5775
6149
|
}
|
|
5776
6150
|
/** Stop everything; a waiting attempt ends cancelled. Idempotent. */
|
|
5777
6151
|
dispose() {
|
|
5778
6152
|
if (this.disposed) return;
|
|
5779
6153
|
this.disposed = true;
|
|
6154
|
+
this.cancelPending = false;
|
|
5780
6155
|
const wasWaiting = this.statusValue.state === "waiting";
|
|
5781
6156
|
this.teardown();
|
|
5782
6157
|
if (wasWaiting) this.setStatus({
|
|
@@ -5812,8 +6187,12 @@ var CommandCodeLoginFlow = class {
|
|
|
5812
6187
|
const server = createServer((request, response) => this.handleCallback(request, response, expectedState));
|
|
5813
6188
|
this.server = server;
|
|
5814
6189
|
server.once("error", (error) => {
|
|
5815
|
-
if (this.server !== server)
|
|
6190
|
+
if (this.server !== server) {
|
|
6191
|
+
closeServer(server);
|
|
6192
|
+
return;
|
|
6193
|
+
}
|
|
5816
6194
|
this.server = void 0;
|
|
6195
|
+
closeServer(server);
|
|
5817
6196
|
const tagged = new LoginSettleError("error", `Could not bind the login callback server on port ${port}: ${error.code ?? error.message}`);
|
|
5818
6197
|
if (binding) {
|
|
5819
6198
|
binding = false;
|
|
@@ -5982,11 +6361,17 @@ var CommandCodeLoginFlow = class {
|
|
|
5982
6361
|
/** Close the server and watchdog without touching the published status. */
|
|
5983
6362
|
teardown() {
|
|
5984
6363
|
this.clearTimer();
|
|
5985
|
-
this.server
|
|
6364
|
+
closeServer(this.server);
|
|
5986
6365
|
this.server = void 0;
|
|
5987
6366
|
this.settle = void 0;
|
|
5988
6367
|
}
|
|
5989
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
|
+
}
|
|
5990
6375
|
/** A tagged settle failure carrying the stable copy reason. */
|
|
5991
6376
|
var LoginSettleError = class extends Error {
|
|
5992
6377
|
reason;
|
|
@@ -6060,7 +6445,8 @@ function selectCommandCodeSearchProvider(web, enable) {
|
|
|
6060
6445
|
function commandCodeSearchSelection() {
|
|
6061
6446
|
return {
|
|
6062
6447
|
owner: false,
|
|
6063
|
-
displaced: void 0
|
|
6448
|
+
displaced: void 0,
|
|
6449
|
+
preexisting: false
|
|
6064
6450
|
};
|
|
6065
6451
|
}
|
|
6066
6452
|
/**
|
|
@@ -6078,8 +6464,14 @@ function commandCodeSearchSelection() {
|
|
|
6078
6464
|
* auto-select — already says what the user wants.
|
|
6079
6465
|
* - When the field already reads `commandcode` at first touch (e.g. a
|
|
6080
6466
|
* surviving runtime the plugin did not set, or a manual
|
|
6081
|
-
* `searchProvider: commandcode` pin), `displaced` stays undefined
|
|
6082
|
-
* 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`.
|
|
6083
6475
|
*
|
|
6084
6476
|
* Never throws: like the low-level rewrite, a hardened runtime shape degrades
|
|
6085
6477
|
* to registered-but-unselected.
|
|
@@ -6093,14 +6485,16 @@ function applyCommandCodeSearchSelection(web, state, enable) {
|
|
|
6093
6485
|
return;
|
|
6094
6486
|
}
|
|
6095
6487
|
const prior = field.searchProviderId;
|
|
6096
|
-
state.
|
|
6488
|
+
state.preexisting = prior === COMMANDCODE_SEARCH_PROVIDER_ID;
|
|
6489
|
+
state.displaced = state.preexisting ? void 0 : prior;
|
|
6097
6490
|
field.searchProviderId = COMMANDCODE_SEARCH_PROVIDER_ID;
|
|
6098
6491
|
state.owner = true;
|
|
6099
6492
|
return;
|
|
6100
6493
|
}
|
|
6101
6494
|
if (state.owner) {
|
|
6102
6495
|
state.owner = false;
|
|
6103
|
-
field.searchProviderId = state.displaced;
|
|
6496
|
+
if (!state.preexisting) field.searchProviderId = state.displaced;
|
|
6497
|
+
state.preexisting = false;
|
|
6104
6498
|
return;
|
|
6105
6499
|
}
|
|
6106
6500
|
} catch {}
|
|
@@ -6402,8 +6796,8 @@ function buildCommandCodeTuiSection(deps) {
|
|
|
6402
6796
|
label: "API key",
|
|
6403
6797
|
descriptions: { zh: "API 密钥" },
|
|
6404
6798
|
secret: { ref },
|
|
6405
|
-
hint: `Stored in the credential store as ${ref}, never in
|
|
6406
|
-
hintDescriptions: { zh: `保存在凭据库(${ref}
|
|
6799
|
+
hint: `Stored in the credential store as ${ref}, never in configuration files.`,
|
|
6800
|
+
hintDescriptions: { zh: `保存在凭据库(${ref}),不会写入配置文件。` }
|
|
6407
6801
|
},
|
|
6408
6802
|
{
|
|
6409
6803
|
path: ["apiBase"],
|
|
@@ -6681,6 +7075,67 @@ function transportBudgetMessage(failureMessage, maxRetries) {
|
|
|
6681
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`;
|
|
6682
7076
|
}
|
|
6683
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
|
|
6684
7139
|
//#region src/index.ts
|
|
6685
7140
|
/**
|
|
6686
7141
|
* dsh-commandcode-provider — DeepSeek Harness LLM provider plugin for Command
|
|
@@ -6690,9 +7145,10 @@ function transportBudgetMessage(failureMessage, maxRetries) {
|
|
|
6690
7145
|
* the configurable-provider directory, so the web Models page shows a
|
|
6691
7146
|
* "Command Code" card with an API-key field and the model picker lists the
|
|
6692
7147
|
* live Command Code model catalog. Connection facts resolve per request over
|
|
6693
|
-
* the
|
|
6694
|
-
*
|
|
6695
|
-
*
|
|
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.
|
|
6696
7152
|
*
|
|
6697
7153
|
* ```yaml
|
|
6698
7154
|
* - id: llm-commandcode
|
|
@@ -6717,41 +7173,84 @@ const DEFAULT_API_KEY_ENV = "COMMANDCODE_API_KEY";
|
|
|
6717
7173
|
const PROVIDER = "commandcode";
|
|
6718
7174
|
/** Default models cache path (mirrors the pi plugin's on-disk cache). */
|
|
6719
7175
|
const DEFAULT_MODELS_CACHE_PATH = join(homedir(), ".commandcode", "models-cache.json");
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
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());
|
|
6755
7254
|
/**
|
|
6756
7255
|
* The one explicit resolve step from raw config to validated connection
|
|
6757
7256
|
* facts. Programmatic construction may bypass Schemastery normalization, so
|
|
@@ -6785,7 +7284,8 @@ function readModelVisibility(raw) {
|
|
|
6785
7284
|
}
|
|
6786
7285
|
function apply(ctx, config) {
|
|
6787
7286
|
installCostProjection(ctx);
|
|
6788
|
-
let
|
|
7287
|
+
let source = () => config;
|
|
7288
|
+
const current = () => unwrapVolatileConfig(source());
|
|
6789
7289
|
let lastRaw;
|
|
6790
7290
|
let lastGood;
|
|
6791
7291
|
const options = () => {
|
|
@@ -6998,18 +7498,38 @@ function apply(ctx, config) {
|
|
|
6998
7498
|
}, "dsh-commandcode-provider: tui settings handle");
|
|
6999
7499
|
});
|
|
7000
7500
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
7001
|
-
settingsCtx.settings
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
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");
|
|
7010
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");
|
|
7011
7531
|
}
|
|
7012
7532
|
//#endregion
|
|
7013
|
-
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 };
|
|
7014
7534
|
|
|
7015
7535
|
//# sourceMappingURL=index.js.map
|