@mars-sea/dsh-commandcode-provider 0.4.1 → 0.5.0
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 +23 -0
- package/README.md +28 -2
- package/README.zh-CN.md +28 -2
- package/lib/client.js +556 -97
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +236 -23
- package/lib/index.js +510 -100
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
|
18
18
|
* community-maintained integration; you need your own Command Code account
|
|
19
19
|
* and API key or subscription, and Command Code's terms apply.
|
|
20
20
|
*
|
|
21
|
-
* Wire protocol (reverse-engineered by the pi plugin, command-code@1.
|
|
21
|
+
* Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.1):
|
|
22
22
|
* POST {apiBase}/alpha/generate
|
|
23
23
|
* body: { config, memory, taste, skills, params: { model, messages, tools,
|
|
24
24
|
* system, max_tokens, temperature, stream, reasoning_effort? }, threadId }
|
|
@@ -36,6 +36,11 @@ const KNOWN_EFFORTS = {
|
|
|
36
36
|
"medium",
|
|
37
37
|
"xhigh"
|
|
38
38
|
],
|
|
39
|
+
"Qwen/Qwen3.8-27B": [
|
|
40
|
+
"low",
|
|
41
|
+
"medium",
|
|
42
|
+
"xhigh"
|
|
43
|
+
],
|
|
39
44
|
"claude-fable-5": [
|
|
40
45
|
"low",
|
|
41
46
|
"medium",
|
|
@@ -188,6 +193,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
188
193
|
"Qwen/Qwen3.6-Plus",
|
|
189
194
|
"Qwen/Qwen3.7-Flash",
|
|
190
195
|
"Qwen/Qwen3.7-Plus",
|
|
196
|
+
"Qwen/Qwen3.8-27B",
|
|
191
197
|
"Qwen/Qwen3.8-Max",
|
|
192
198
|
"claude-fable-5",
|
|
193
199
|
"claude-haiku-4-5-20251001",
|
|
@@ -224,7 +230,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
224
230
|
"xiaomi/mimo-v2.5"
|
|
225
231
|
]);
|
|
226
232
|
/**
|
|
227
|
-
* Models the official CLI's model table (`ZA` in command-code@1.
|
|
233
|
+
* Models the official CLI's model table (`ZA` in command-code@1.28.1) marks
|
|
228
234
|
* `reasoning:!0` but defines no selectable `reasoning_effort` levels — they
|
|
229
235
|
* think automatically, with Command Code driving the depth. This is the
|
|
230
236
|
* authoritative "thinks, effort not adjustable" set: `KNOWN_EFFORTS` (which
|
|
@@ -232,7 +238,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
232
238
|
* effort levels, and this snapshot is not surfaced in the picker's compact
|
|
233
239
|
* description — it exists for programmatic consumers.
|
|
234
240
|
*
|
|
235
|
-
* Source: the command-code@1.
|
|
241
|
+
* Source: the command-code@1.28.1 bundled model table (dist/cli.mjs, the `ZA`
|
|
236
242
|
* object), cross-checked with https://commandcode.ai/docs/reference/cli/models.
|
|
237
243
|
* Keep in sync via the dsh-commandcode-upstream skill.
|
|
238
244
|
*/
|
|
@@ -281,6 +287,7 @@ const KNOWN_PLANS = {
|
|
|
281
287
|
"Qwen/Qwen3.7-Flash": "go",
|
|
282
288
|
"Qwen/Qwen3.7-Max": "go",
|
|
283
289
|
"Qwen/Qwen3.7-Plus": "go",
|
|
290
|
+
"Qwen/Qwen3.8-27B": "go",
|
|
284
291
|
"Qwen/Qwen3.8-Max": "go",
|
|
285
292
|
"deepseek/deepseek-v4-flash": "go",
|
|
286
293
|
"deepseek/deepseek-v4-pro": "go",
|
|
@@ -362,7 +369,7 @@ function compareByPlan(a, b) {
|
|
|
362
369
|
}
|
|
363
370
|
/**
|
|
364
371
|
* Subscription plan table, synced from the official CLI bundle's plan maps
|
|
365
|
-
* (`Nn`/`$n` in command-code@1.
|
|
372
|
+
* (`Nn`/`$n` in command-code@1.28.1 `dist/cli.mjs`): subscription `planId`
|
|
366
373
|
* prefix → display name and the plan's monthly credit total. This is the
|
|
367
374
|
* account's own subscription (from `/alpha/billing/subscriptions`) — distinct
|
|
368
375
|
* from {@link KNOWN_PLANS}, which maps catalog models to their minimum tier.
|
|
@@ -492,7 +499,7 @@ function peakPricingLabel(modelId, now = Date.now()) {
|
|
|
492
499
|
if (state === void 0) return void 0;
|
|
493
500
|
return state === "peak" ? "Peak" : "Half";
|
|
494
501
|
}
|
|
495
|
-
const COMMAND_CODE_CLI_VERSION = "1.
|
|
502
|
+
const COMMAND_CODE_CLI_VERSION = "1.28.1";
|
|
496
503
|
const DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
497
504
|
const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
|
|
498
505
|
const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
|
|
@@ -685,14 +692,28 @@ async function writeModelsCache(cachePath, models) {
|
|
|
685
692
|
await rm(tmp, { force: true }).catch(() => void 0);
|
|
686
693
|
}
|
|
687
694
|
}
|
|
688
|
-
|
|
695
|
+
/**
|
|
696
|
+
* Collect the tool calls that have a paired tool result, plus each call's
|
|
697
|
+
* name. The name map feeds the `toolName` of replayed tool results: some
|
|
698
|
+
* backends (e.g. Google Gemini `functionResponse`) reject a result whose
|
|
699
|
+
* function name is empty, so the real name must round-trip (the official
|
|
700
|
+
* CLI does the same via its `tool_use_id -> toolName` map).
|
|
701
|
+
*/
|
|
702
|
+
function pairedToolCalls(messages) {
|
|
689
703
|
const callIds = /* @__PURE__ */ new Set();
|
|
704
|
+
const names = /* @__PURE__ */ new Map();
|
|
690
705
|
const resultIds = /* @__PURE__ */ new Set();
|
|
691
706
|
for (const message of messages) for (const block of message.content) {
|
|
692
|
-
if (message.role === "assistant" && block.type === "tool-call")
|
|
707
|
+
if (message.role === "assistant" && block.type === "tool-call") {
|
|
708
|
+
callIds.add(block.id);
|
|
709
|
+
names.set(block.id, block.name);
|
|
710
|
+
}
|
|
693
711
|
if (block.type === "tool-result") resultIds.add(block.toolCallId);
|
|
694
712
|
}
|
|
695
|
-
return
|
|
713
|
+
return {
|
|
714
|
+
ids: new Set([...callIds].filter((id) => resultIds.has(id))),
|
|
715
|
+
names
|
|
716
|
+
};
|
|
696
717
|
}
|
|
697
718
|
function blockText(block) {
|
|
698
719
|
return block.type === "text" || block.type === "reasoning" ? block.text : "";
|
|
@@ -723,7 +744,7 @@ async function imageToCommandCode(ref, readImage) {
|
|
|
723
744
|
}
|
|
724
745
|
async function messagesToCC(messages, readImage) {
|
|
725
746
|
const out = [];
|
|
726
|
-
const paired =
|
|
747
|
+
const { ids: paired, names: toolNames } = pairedToolCalls(messages);
|
|
727
748
|
for (const message of messages) {
|
|
728
749
|
if (message.role === "system") continue;
|
|
729
750
|
if (message.role === "user" && message.source.kind !== "tool") {
|
|
@@ -770,7 +791,7 @@ async function messagesToCC(messages, readImage) {
|
|
|
770
791
|
content: [{
|
|
771
792
|
type: "tool-result",
|
|
772
793
|
toolCallId: block.toolCallId,
|
|
773
|
-
toolName: "",
|
|
794
|
+
toolName: toolNames.get(block.toolCallId) || "unknown",
|
|
774
795
|
output: block.isError ? {
|
|
775
796
|
type: "error-text",
|
|
776
797
|
value: toolResultText(block)
|
|
@@ -789,8 +810,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
789
810
|
catalog = [];
|
|
790
811
|
fetchImpl;
|
|
791
812
|
resolveAttachments;
|
|
792
|
-
billingAccess;
|
|
793
|
-
billingAccessInflight;
|
|
813
|
+
billingAccess = /* @__PURE__ */ new Map();
|
|
814
|
+
billingAccessInflight = /* @__PURE__ */ new Map();
|
|
794
815
|
constructor(deps) {
|
|
795
816
|
super();
|
|
796
817
|
this.deps = deps;
|
|
@@ -863,10 +884,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
863
884
|
};
|
|
864
885
|
}
|
|
865
886
|
/** The headers every authenticated account endpoint shares. */
|
|
866
|
-
async accountHeaders() {
|
|
887
|
+
async accountHeaders(apiKey) {
|
|
867
888
|
const connection = this.deps.options();
|
|
868
889
|
return {
|
|
869
|
-
Authorization: `Bearer ${await this.deps.resolveApiKey(connection)}`,
|
|
890
|
+
Authorization: `Bearer ${apiKey ?? await this.deps.resolveApiKey(connection)}`,
|
|
870
891
|
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
871
892
|
"x-cli-environment": "production",
|
|
872
893
|
...attributionHeaders()
|
|
@@ -878,18 +899,27 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
878
899
|
* `undefined` means "unknown — show everything" (fail-open).
|
|
879
900
|
*/
|
|
880
901
|
async loadBillingAccess() {
|
|
881
|
-
|
|
902
|
+
let apiKey;
|
|
903
|
+
try {
|
|
904
|
+
apiKey = await this.deps.resolveApiKey(this.deps.options());
|
|
905
|
+
} catch {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const cached = this.billingAccess.get(apiKey);
|
|
882
909
|
if (cached !== void 0 && Date.now() - cached.at < 3e5) return cached.value;
|
|
883
|
-
|
|
884
|
-
|
|
910
|
+
const existing = this.billingAccessInflight.get(apiKey);
|
|
911
|
+
if (existing !== void 0) return existing;
|
|
912
|
+
const inflight = this.fetchBillingAccess(apiKey).then((value) => {
|
|
913
|
+
this.billingAccess.set(apiKey, {
|
|
885
914
|
value,
|
|
886
915
|
at: Date.now()
|
|
887
|
-
};
|
|
916
|
+
});
|
|
888
917
|
return value;
|
|
889
918
|
}).finally(() => {
|
|
890
|
-
this.billingAccessInflight
|
|
919
|
+
this.billingAccessInflight.delete(apiKey);
|
|
891
920
|
});
|
|
892
|
-
|
|
921
|
+
this.billingAccessInflight.set(apiKey, inflight);
|
|
922
|
+
return inflight;
|
|
893
923
|
}
|
|
894
924
|
/**
|
|
895
925
|
* The billing facts behind the picker's plan filter, mirroring the CLI's
|
|
@@ -900,10 +930,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
900
930
|
* fallback (the CLI stamps plan identity from it too). Any failure resolves
|
|
901
931
|
* to `undefined` (fail-open) rather than breaking the picker.
|
|
902
932
|
*/
|
|
903
|
-
async fetchBillingAccess() {
|
|
933
|
+
async fetchBillingAccess(apiKey) {
|
|
904
934
|
try {
|
|
905
935
|
const connection = this.deps.options();
|
|
906
|
-
const headers = await this.accountHeaders();
|
|
936
|
+
const headers = await this.accountHeaders(apiKey);
|
|
907
937
|
const base = connection.apiBase;
|
|
908
938
|
const getJson = async (path) => {
|
|
909
939
|
const response = await this.fetchImpl(`${base}${path}`, {
|
|
@@ -941,10 +971,12 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
941
971
|
* Each endpoint degrades independently: a failed one lands in `failures`
|
|
942
972
|
* while the rest still report, so a transient outage never blanks the whole
|
|
943
973
|
* view. Requires a usable API key (throws `MISSING_CREDENTIAL` otherwise).
|
|
974
|
+
* Pass `apiKey` to report on a specific account of a multi-account pool;
|
|
975
|
+
* the default resolves the currently active account.
|
|
944
976
|
*/
|
|
945
|
-
async getUsage() {
|
|
977
|
+
async getUsage(apiKey) {
|
|
946
978
|
const base = this.deps.options().apiBase;
|
|
947
|
-
const headers = await this.accountHeaders();
|
|
979
|
+
const headers = await this.accountHeaders(apiKey);
|
|
948
980
|
const failures = [];
|
|
949
981
|
const getJson = async (path) => {
|
|
950
982
|
try {
|
|
@@ -1022,6 +1054,35 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
1022
1054
|
}
|
|
1023
1055
|
return report;
|
|
1024
1056
|
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Probe one account's five-hour window from `/alpha/billing/credits`. The
|
|
1059
|
+
* multi-account pool calls this when every account is marked exhausted: an
|
|
1060
|
+
* account whose window no longer reports `exceeded` is revived, and the
|
|
1061
|
+
* `resetAt` values feed the "earliest reset" error message. Returns
|
|
1062
|
+
* `undefined` when the probe itself failed (transport, non-200, or a
|
|
1063
|
+
* payload without window limits) — a failed probe never changes pool state.
|
|
1064
|
+
*/
|
|
1065
|
+
async probeFiveHourWindow(apiKey) {
|
|
1066
|
+
try {
|
|
1067
|
+
const connection = this.deps.options();
|
|
1068
|
+
const response = await this.fetchImpl(`${connection.apiBase}/alpha/billing/credits`, {
|
|
1069
|
+
headers: await this.accountHeaders(apiKey),
|
|
1070
|
+
signal: AbortSignal.timeout(MODELS_TIMEOUT_MS)
|
|
1071
|
+
});
|
|
1072
|
+
if (!response.ok) return void 0;
|
|
1073
|
+
const parsed = await response.json();
|
|
1074
|
+
if (!isRecord(parsed)) return void 0;
|
|
1075
|
+
const windowLimits = isRecord(parsed.windowLimits) ? parsed.windowLimits : void 0;
|
|
1076
|
+
const fiveHour = windowLimits && isRecord(windowLimits.fiveHour) ? windowLimits.fiveHour : void 0;
|
|
1077
|
+
if (fiveHour === void 0) return void 0;
|
|
1078
|
+
return {
|
|
1079
|
+
exceeded: fiveHour.exceeded === true,
|
|
1080
|
+
resetAt: numberValue(fiveHour.resetAt) ?? 0
|
|
1081
|
+
};
|
|
1082
|
+
} catch {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1025
1086
|
async *stream(options) {
|
|
1026
1087
|
if (options.stop?.length) throw new LlmError("Command Code adapter does not support stop sequences", "UNSUPPORTED_OPTION");
|
|
1027
1088
|
const hasImages = options.messages.some(hasImageContent);
|
|
@@ -1033,7 +1094,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
1033
1094
|
readImage = (ref) => attachments.readImage(ref).then((stored) => stored.data);
|
|
1034
1095
|
}
|
|
1035
1096
|
const connection = this.deps.options();
|
|
1036
|
-
|
|
1097
|
+
let apiKey = await this.deps.resolveApiKey(connection);
|
|
1037
1098
|
const modelMax = this.catalog.find((m) => m.id === options.model)?.maxTokens ?? 65536;
|
|
1038
1099
|
const maxTokens = Math.min(options.maxTokens ?? modelMax, modelMax, DEFAULT_GENERATE_MAX_TOKENS);
|
|
1039
1100
|
const effort = options.reasoningEffort;
|
|
@@ -1072,58 +1133,83 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
1072
1133
|
},
|
|
1073
1134
|
threadId: randomUUID()
|
|
1074
1135
|
};
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
if (options.signal
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1095
|
-
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
1096
|
-
"x-cli-environment": "production",
|
|
1097
|
-
"x-project-slug": projectSlugFromPath(connection.workingDir),
|
|
1098
|
-
"x-taste-learning": "true",
|
|
1099
|
-
"x-co-flag": "false",
|
|
1100
|
-
...attributionHeaders()
|
|
1101
|
-
},
|
|
1102
|
-
body: JSON.stringify(body),
|
|
1103
|
-
signal: connectAbort.signal
|
|
1104
|
-
});
|
|
1105
|
-
clearTimeout(connectTimer);
|
|
1106
|
-
} catch (error) {
|
|
1107
|
-
clearTimeout(connectTimer);
|
|
1108
|
-
if (options.signal) options.signal.removeEventListener("abort", onCallerAbort);
|
|
1109
|
-
if (options.signal?.aborted) throw error;
|
|
1110
|
-
if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw new LlmError(`Command Code API request to ${connection.apiBase}/alpha/generate did not respond within ${connection.requestTimeoutMs}ms: ${errorChain(error)}`, "TIMEOUT", { cause: error });
|
|
1111
|
-
throw new LlmError(`Command Code API request to ${connection.apiBase}/alpha/generate failed: ${errorChain(error)}`, "TRANSPORT", { cause: error });
|
|
1112
|
-
}
|
|
1113
|
-
if (!response.ok) {
|
|
1114
|
-
if (options.signal) options.signal.removeEventListener("abort", onCallerAbort);
|
|
1115
|
-
const errText = await response.text().catch(() => "");
|
|
1116
|
-
let providerCode;
|
|
1136
|
+
const connect = async (key) => {
|
|
1137
|
+
const connectAbort = new AbortController();
|
|
1138
|
+
let connectTimedOut = false;
|
|
1139
|
+
const connectTimer = setTimeout(() => {
|
|
1140
|
+
connectTimedOut = true;
|
|
1141
|
+
connectAbort.abort(new DOMException(`Command Code API request to ${connection.apiBase}/alpha/generate did not respond within ${connection.requestTimeoutMs}ms`, "TimeoutError"));
|
|
1142
|
+
}, connection.requestTimeoutMs);
|
|
1143
|
+
const onCallerAbort = () => {
|
|
1144
|
+
connectAbort.abort(options.signal?.reason);
|
|
1145
|
+
};
|
|
1146
|
+
if (options.signal) {
|
|
1147
|
+
if (options.signal.aborted) onCallerAbort();
|
|
1148
|
+
else options.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
1149
|
+
}
|
|
1150
|
+
const cleanup = () => {
|
|
1151
|
+
clearTimeout(connectTimer);
|
|
1152
|
+
if (options.signal) options.signal.removeEventListener("abort", onCallerAbort);
|
|
1153
|
+
};
|
|
1154
|
+
let response;
|
|
1117
1155
|
try {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1156
|
+
response = await this.fetchImpl(`${connection.apiBase}/alpha/generate`, {
|
|
1157
|
+
method: "POST",
|
|
1158
|
+
headers: {
|
|
1159
|
+
"Content-Type": "application/json",
|
|
1160
|
+
Authorization: `Bearer ${key}`,
|
|
1161
|
+
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
1162
|
+
"x-cli-environment": "production",
|
|
1163
|
+
"x-project-slug": projectSlugFromPath(connection.workingDir),
|
|
1164
|
+
"x-taste-learning": "true",
|
|
1165
|
+
"x-co-flag": "false",
|
|
1166
|
+
...attributionHeaders()
|
|
1167
|
+
},
|
|
1168
|
+
body: JSON.stringify(body),
|
|
1169
|
+
signal: connectAbort.signal
|
|
1170
|
+
});
|
|
1171
|
+
clearTimeout(connectTimer);
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
cleanup();
|
|
1174
|
+
if (options.signal?.aborted) throw error;
|
|
1175
|
+
if (connectTimedOut || error instanceof DOMException && error.name === "TimeoutError") throw new LlmError(`Command Code API request to ${connection.apiBase}/alpha/generate did not respond within ${connection.requestTimeoutMs}ms: ${errorChain(error)}`, "TIMEOUT", { cause: error });
|
|
1176
|
+
throw new LlmError(`Command Code API request to ${connection.apiBase}/alpha/generate failed: ${errorChain(error)}`, "TRANSPORT", { cause: error });
|
|
1177
|
+
}
|
|
1178
|
+
if (!response.ok) {
|
|
1179
|
+
const errText = await response.text().catch(() => "");
|
|
1180
|
+
cleanup();
|
|
1181
|
+
return {
|
|
1182
|
+
status: response.status,
|
|
1183
|
+
errText
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
response,
|
|
1188
|
+
cleanup
|
|
1189
|
+
};
|
|
1190
|
+
};
|
|
1191
|
+
const tried = /* @__PURE__ */ new Set();
|
|
1192
|
+
let connected;
|
|
1193
|
+
for (;;) {
|
|
1194
|
+
tried.add(apiKey);
|
|
1195
|
+
const attempt = await connect(apiKey);
|
|
1196
|
+
if ("response" in attempt) {
|
|
1197
|
+
connected = attempt;
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
const rotate = this.deps.rotateApiKey;
|
|
1201
|
+
if ((attempt.status === 429 || attempt.status === 401) && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
|
|
1202
|
+
const next = await rotate(apiKey, attempt.status === 429 ? "rate-limit" : "invalid-credential", connection);
|
|
1203
|
+
if (next !== void 0 && !tried.has(next)) {
|
|
1204
|
+
apiKey = next;
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
throw generateHttpError(attempt.status, attempt.errText);
|
|
1124
1209
|
}
|
|
1210
|
+
const { response, cleanup } = connected;
|
|
1125
1211
|
if (!response.body) {
|
|
1126
|
-
|
|
1212
|
+
cleanup();
|
|
1127
1213
|
throw new LlmError("Command Code API returned no response body", "PROVIDER_PROTOCOL_ERROR");
|
|
1128
1214
|
}
|
|
1129
1215
|
const reader = response.body.getReader();
|
|
@@ -1334,18 +1420,220 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
1334
1420
|
}
|
|
1335
1421
|
} finally {
|
|
1336
1422
|
clearIdle();
|
|
1337
|
-
|
|
1423
|
+
cleanup();
|
|
1338
1424
|
await reader.cancel().catch(() => void 0);
|
|
1339
1425
|
reader.releaseLock();
|
|
1340
1426
|
}
|
|
1341
1427
|
}
|
|
1342
1428
|
};
|
|
1429
|
+
/** Hard cap on account rotations within one request (one attempt per distinct key). */
|
|
1430
|
+
const MAX_ACCOUNT_ROTATIONS = 16;
|
|
1431
|
+
/**
|
|
1432
|
+
* Map a pre-stream generate HTTP failure onto a stable LlmError. Command
|
|
1433
|
+
* Code folds several business rejections into 403 (plan limits, CLI version,
|
|
1434
|
+
* model access): prefer the machine-readable `error.code` when present; the
|
|
1435
|
+
* status alone cannot distinguish them.
|
|
1436
|
+
*/
|
|
1437
|
+
function generateHttpError(status, errText) {
|
|
1438
|
+
let providerCode;
|
|
1439
|
+
try {
|
|
1440
|
+
const parsed = JSON.parse(errText);
|
|
1441
|
+
if (isRecord(parsed) && isRecord(parsed.error)) providerCode = stringValue(parsed.error.code);
|
|
1442
|
+
} catch {}
|
|
1443
|
+
const detail = providerCode ?? `HTTP ${status}`;
|
|
1444
|
+
if (status === 401) return new LlmError(`Command Code API error 401 (${detail}): the API key is missing or invalid — check the key stored for COMMANDCODE_API_KEY (Models page) or the auth file`, "INVALID_CREDENTIAL", { status: 401 });
|
|
1445
|
+
return new LlmError(`Command Code API error ${status}${detail === `HTTP ${status}` ? "" : ` (${detail})`}: ${errText.slice(0, 500)}`, status === 429 ? "RATE_LIMIT" : "PROVIDER_HTTP_ERROR", { status });
|
|
1446
|
+
}
|
|
1343
1447
|
function mapFinishReason(reason) {
|
|
1344
1448
|
if (reason === "tool-calls") return { kind: "tool-calls" };
|
|
1345
1449
|
if (reason === "length" || reason === "max_tokens" || reason === "max-tokens" || reason === "max_output_tokens") return { kind: "max-tokens" };
|
|
1346
1450
|
return { kind: "stop" };
|
|
1347
1451
|
}
|
|
1348
1452
|
//#endregion
|
|
1453
|
+
//#region src/accounts.ts
|
|
1454
|
+
/**
|
|
1455
|
+
* Multi-account pool for the Command Code provider (host side).
|
|
1456
|
+
*
|
|
1457
|
+
* One Command Code subscription (e.g. the Go plan's 5-hour window) is
|
|
1458
|
+
* metered; a user with several subscriptions wants a request that hits one
|
|
1459
|
+
* account's limit to continue on the next account without a visible failure.
|
|
1460
|
+
* This module owns that rotation:
|
|
1461
|
+
*
|
|
1462
|
+
* - {@link CommandCodeAccountPool.resolveKey} hands out the first account
|
|
1463
|
+
* whose key is not currently marked exhausted, resolving each slot's key
|
|
1464
|
+
* lazily (literal config key → credential seam → launch environment → the
|
|
1465
|
+
* official CLI auth file for the default slot only).
|
|
1466
|
+
* - {@link CommandCodeAccountPool.markRejected} records a 429 (rate limit,
|
|
1467
|
+
* window unknown) or 401 (invalid key, disabled until the config changes)
|
|
1468
|
+
* against the exact API key, so several slots sharing one key share one
|
|
1469
|
+
* state.
|
|
1470
|
+
* - When every account is marked, the pool probes each key's
|
|
1471
|
+
* `/alpha/billing/credits` window limits (through the injected
|
|
1472
|
+
* {@link CommandCodeAccountPoolDeps.probeWindow}): an account whose window
|
|
1473
|
+
* no longer reports `exceeded` is revived, otherwise the pool throws a
|
|
1474
|
+
* `RATE_LIMIT` error naming the earliest reset time.
|
|
1475
|
+
*
|
|
1476
|
+
* The pool is deliberately cordis-free (like the adapter): every host fact
|
|
1477
|
+
* arrives through injected thunks, so node tests can drive it directly.
|
|
1478
|
+
*
|
|
1479
|
+
* @module dsh-commandcode-provider/accounts
|
|
1480
|
+
*/
|
|
1481
|
+
/** A labeled, human-readable clock reading for error messages. */
|
|
1482
|
+
function clockLabel(ms) {
|
|
1483
|
+
return new Date(ms).toLocaleString();
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Whether an account with this rotation state can serve a request right now.
|
|
1487
|
+
* `undefined` (never rejected) is usable; a cooldown becomes usable again
|
|
1488
|
+
* once its reset time passes; `unknown` (429, reset unprobed) and
|
|
1489
|
+
* `disabled` (401) are not.
|
|
1490
|
+
*/
|
|
1491
|
+
function accountUsable(state) {
|
|
1492
|
+
if (state === void 0) return true;
|
|
1493
|
+
if (state.kind === "cooldown") return state.until > 0 && Date.now() >= state.until;
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Pick the account that should serve now: the manually preferred slot when it
|
|
1498
|
+
* is usable, otherwise the first usable account in rotation order; undefined
|
|
1499
|
+
* when no account is usable. Shared by the pool (request path) and the plugin
|
|
1500
|
+
* entry (the usage view's active badge) so both always agree.
|
|
1501
|
+
*/
|
|
1502
|
+
function selectActiveAccount(accounts, preferredId) {
|
|
1503
|
+
const usable = accounts.filter((account) => accountUsable(account.state));
|
|
1504
|
+
if (preferredId !== void 0) {
|
|
1505
|
+
const preferred = usable.find((account) => account.slot.id === preferredId);
|
|
1506
|
+
if (preferred !== void 0) return preferred;
|
|
1507
|
+
}
|
|
1508
|
+
return usable[0];
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* The account pool. Rotation state is keyed by API key (never logged), so two
|
|
1512
|
+
* slots resolving to the same credential share one mark, and a key changed in
|
|
1513
|
+
* the credentials service starts with a clean slate.
|
|
1514
|
+
*/
|
|
1515
|
+
var CommandCodeAccountPool = class {
|
|
1516
|
+
deps;
|
|
1517
|
+
/** Rotation state by API key. */
|
|
1518
|
+
states = /* @__PURE__ */ new Map();
|
|
1519
|
+
constructor(deps) {
|
|
1520
|
+
this.deps = deps;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Resolve every slot's key, deduplicated by key (first slot wins). Slots
|
|
1524
|
+
* without any resolvable key are omitted — they still appear in the
|
|
1525
|
+
* settings page as unconfigured, they just cannot serve requests.
|
|
1526
|
+
*/
|
|
1527
|
+
async resolvedAccounts() {
|
|
1528
|
+
const out = [];
|
|
1529
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1530
|
+
for (const slot of this.deps.slots()) {
|
|
1531
|
+
const key = await this.resolveSlotKey(slot);
|
|
1532
|
+
if (key === void 0 || seen.has(key)) continue;
|
|
1533
|
+
seen.add(key);
|
|
1534
|
+
out.push({
|
|
1535
|
+
slot,
|
|
1536
|
+
key,
|
|
1537
|
+
state: this.states.get(key)
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
return out;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Every slot paired with its resolved key and rotation state — NOT
|
|
1544
|
+
* deduplicated: two slots sharing one credential both appear (the usage
|
|
1545
|
+
* view reports them individually), while slots without any resolvable key
|
|
1546
|
+
* are omitted. The serving path uses {@link resolvedAccounts} instead.
|
|
1547
|
+
*/
|
|
1548
|
+
async describeAccounts() {
|
|
1549
|
+
const out = [];
|
|
1550
|
+
for (const slot of this.deps.slots()) {
|
|
1551
|
+
const key = await this.resolveSlotKey(slot);
|
|
1552
|
+
if (key === void 0) continue;
|
|
1553
|
+
out.push({
|
|
1554
|
+
slot,
|
|
1555
|
+
key,
|
|
1556
|
+
state: this.states.get(key)
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
return out;
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Hand out the first usable account's key (the manually preferred account
|
|
1563
|
+
* when usable, else rotation order). Returns `undefined` when no account
|
|
1564
|
+
* resolves any key at all (the caller then reports the missing credential).
|
|
1565
|
+
* Throws `RATE_LIMIT` — naming the earliest window reset — or
|
|
1566
|
+
* `INVALID_CREDENTIAL` when accounts exist but none can serve.
|
|
1567
|
+
*
|
|
1568
|
+
* `options.exclude` skips one key during the probe-revival pass: the
|
|
1569
|
+
* rotation hook excludes the just-rejected key so a probe that clears its
|
|
1570
|
+
* window cannot re-offer the same key within the same request (the adapter
|
|
1571
|
+
* refuses already-tried keys; the next request picks the revived key up).
|
|
1572
|
+
*/
|
|
1573
|
+
async resolveKey(options) {
|
|
1574
|
+
const accounts = await this.resolvedAccounts();
|
|
1575
|
+
if (accounts.length === 0) return;
|
|
1576
|
+
const chosen = selectActiveAccount(accounts, this.deps.preferredId?.());
|
|
1577
|
+
if (chosen !== void 0) return this.pick(chosen);
|
|
1578
|
+
await Promise.all(accounts.map(async (account) => {
|
|
1579
|
+
if (account.state?.kind === "disabled") return;
|
|
1580
|
+
if (options?.exclude !== void 0 && account.key === options.exclude) return;
|
|
1581
|
+
const probe = await this.deps.probeWindow(account.key);
|
|
1582
|
+
if (probe === void 0) return;
|
|
1583
|
+
if (!probe.exceeded) this.states.delete(account.key);
|
|
1584
|
+
else this.states.set(account.key, {
|
|
1585
|
+
kind: "cooldown",
|
|
1586
|
+
reason: account.state?.reason ?? "rate limited (429)",
|
|
1587
|
+
until: probe.resetAt
|
|
1588
|
+
});
|
|
1589
|
+
}));
|
|
1590
|
+
const revived = selectActiveAccount(await this.resolvedAccounts(), this.deps.preferredId?.());
|
|
1591
|
+
if (revived !== void 0) return this.pick(revived);
|
|
1592
|
+
const latest = await this.resolvedAccounts();
|
|
1593
|
+
if (latest.filter((account) => account.state?.kind === "disabled").length === latest.length) throw new LlmError(`llm-commandcode: every configured Command Code account (${latest.length}) was rejected with 401 — check the stored API keys (Models page / settings) or the auth file`, "INVALID_CREDENTIAL");
|
|
1594
|
+
const resets = latest.map((account) => account.state).filter((state) => state !== void 0 && state.kind === "cooldown" && state.until > 0).map((state) => state.until);
|
|
1595
|
+
const earliest = resets.length > 0 ? Math.min(...resets) : 0;
|
|
1596
|
+
throw new LlmError(`llm-commandcode: all ${latest.length} Command Code account(s) have exhausted their usage window` + (earliest > 0 ? `; the earliest window resets at ${clockLabel(earliest)}` : "") + " — requests will succeed again after the reset (or add another account)", "RATE_LIMIT");
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Record a rejection against one key. `rate-limit` (429) marks the key
|
|
1600
|
+
* exhausted with an unknown reset (probed lazily at the next resolution
|
|
1601
|
+
* once every account is marked); `invalid-credential` (401) disables the
|
|
1602
|
+
* key until the stored credential changes.
|
|
1603
|
+
*/
|
|
1604
|
+
markRejected(apiKey, rejection) {
|
|
1605
|
+
if (rejection === "invalid-credential") this.states.set(apiKey, {
|
|
1606
|
+
kind: "disabled",
|
|
1607
|
+
reason: "invalid API key (401)",
|
|
1608
|
+
until: 0
|
|
1609
|
+
});
|
|
1610
|
+
else this.states.set(apiKey, {
|
|
1611
|
+
kind: "unknown",
|
|
1612
|
+
reason: "rate limited (429)",
|
|
1613
|
+
until: 0
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
/** One account's key: literal → credential seam → auth file (default slot). */
|
|
1617
|
+
async resolveSlotKey(slot) {
|
|
1618
|
+
if (slot.literal !== void 0 && slot.literal !== "") return slot.literal;
|
|
1619
|
+
if (slot.ref !== void 0) {
|
|
1620
|
+
const hit = await this.deps.resolveRef(slot.ref);
|
|
1621
|
+
if (hit !== void 0 && hit !== "") return hit;
|
|
1622
|
+
}
|
|
1623
|
+
if (slot.allowAuthFile) {
|
|
1624
|
+
const fromFile = this.deps.authFileKey();
|
|
1625
|
+
if (fromFile !== void 0 && fromFile !== "") return fromFile;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
/** Hand out the chosen account's key. */
|
|
1629
|
+
pick(account) {
|
|
1630
|
+
return {
|
|
1631
|
+
key: account.key,
|
|
1632
|
+
slot: account.slot
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
//#endregion
|
|
1349
1637
|
//#region src/commands.ts
|
|
1350
1638
|
/** Format a dollar amount. */
|
|
1351
1639
|
function money(value) {
|
|
@@ -1378,11 +1666,18 @@ function bar(used, cap) {
|
|
|
1378
1666
|
const filled = Math.round(ratio * 10);
|
|
1379
1667
|
return "█".repeat(filled) + "░".repeat(10 - filled);
|
|
1380
1668
|
}
|
|
1669
|
+
/** Render one account's rotation mark / cooldown as a short badge. */
|
|
1670
|
+
function markLabel(entry) {
|
|
1671
|
+
if (entry.mark === "invalid-credential") return " ⛔ 密钥无效";
|
|
1672
|
+
if (entry.cooldownUntil > 0) return ` ⏳ 限额冷却中,重置 ${resetLabel(entry.cooldownUntil)}`;
|
|
1673
|
+
if (entry.mark === "rate-limit") return " ⏳ 已达限额(等待窗口探测)";
|
|
1674
|
+
return "";
|
|
1675
|
+
}
|
|
1381
1676
|
/** Render the usage report as a structured, aligned, bar-chart text view. */
|
|
1382
|
-
function renderReport(report) {
|
|
1677
|
+
function renderReport(report, title) {
|
|
1383
1678
|
const lines = [];
|
|
1384
1679
|
const account = report.account ? ` (${report.account.userName || report.account.name})` : "";
|
|
1385
|
-
lines.push(`📊 Command Code 用量${account}`, "");
|
|
1680
|
+
lines.push(title ?? `📊 Command Code 用量${account}`, "");
|
|
1386
1681
|
if (report.plan && report.plan.name !== "") {
|
|
1387
1682
|
const p = report.plan;
|
|
1388
1683
|
const status = p.status !== "" && p.status !== "active" ? ` (${p.status})` : "";
|
|
@@ -1411,6 +1706,18 @@ function commandDefinition(deps) {
|
|
|
1411
1706
|
input: { hint: "[status]" },
|
|
1412
1707
|
handler: async () => {
|
|
1413
1708
|
try {
|
|
1709
|
+
if (deps.reports !== void 0) {
|
|
1710
|
+
const { accounts } = await deps.reports();
|
|
1711
|
+
return {
|
|
1712
|
+
kind: "success",
|
|
1713
|
+
text: accounts.map((entry) => {
|
|
1714
|
+
const badges = `${entry.active ? " ✅ 当前使用" : ""}${markLabel(entry)}`;
|
|
1715
|
+
const title = `📊 ${entry.label}${badges}`;
|
|
1716
|
+
if (!entry.configured) return `${title}\n\n (未配置 API 密钥)`;
|
|
1717
|
+
return renderReport(entry.report, title);
|
|
1718
|
+
}).join("\n\n────────────────────\n\n")
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1414
1721
|
return {
|
|
1415
1722
|
kind: "success",
|
|
1416
1723
|
text: renderReport(await adapter.getUsage())
|
|
@@ -1527,12 +1834,31 @@ function parseUsageReport(value) {
|
|
|
1527
1834
|
}
|
|
1528
1835
|
return report;
|
|
1529
1836
|
}
|
|
1837
|
+
/** Parse one untrusted boundary value into a {@link CommandCodeAccountUsage}. */
|
|
1838
|
+
function parseAccountUsage(value) {
|
|
1839
|
+
const source = record(value, "account");
|
|
1840
|
+
return {
|
|
1841
|
+
id: stringField(source, "id", "account.id"),
|
|
1842
|
+
label: stringField(source, "label", "account.label"),
|
|
1843
|
+
configured: booleanField(source, "configured", "account.configured"),
|
|
1844
|
+
active: booleanField(source, "active", "account.active"),
|
|
1845
|
+
mark: stringField(source, "mark", "account.mark"),
|
|
1846
|
+
cooldownUntil: numberField(source, "cooldownUntil", "account.cooldownUntil"),
|
|
1847
|
+
report: parseUsageReport(source.report)
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
/** Parse the wire result into a {@link CommandCodeAccountsReport}. */
|
|
1851
|
+
function parseAccountsReport(value) {
|
|
1852
|
+
const accounts = record(value, "result").accounts;
|
|
1853
|
+
if (!Array.isArray(accounts)) reject("accounts");
|
|
1854
|
+
return { accounts: accounts.map(parseAccountUsage) };
|
|
1855
|
+
}
|
|
1530
1856
|
/**
|
|
1531
1857
|
* The strict result codec both halves attach to the descriptor. Hand-rolled:
|
|
1532
1858
|
* the client bundle may not require a schema library, and `TypertSchema` is
|
|
1533
1859
|
* deliberately minimal so one `parse` function satisfies it.
|
|
1534
1860
|
*/
|
|
1535
|
-
const usageReportSchema = { parse:
|
|
1861
|
+
const usageReportSchema = { parse: parseAccountsReport };
|
|
1536
1862
|
/** The Host-face contribution registered on `ctx.typert`. */
|
|
1537
1863
|
const USAGE_HOST_CONTRIBUTION = {
|
|
1538
1864
|
package: USAGE_REMOTE_PACKAGE,
|
|
@@ -1547,7 +1873,7 @@ const USAGE_HOST_CONTRIBUTION = {
|
|
|
1547
1873
|
parameters: [],
|
|
1548
1874
|
result: {
|
|
1549
1875
|
mode: "strict",
|
|
1550
|
-
typeSymbol: `${USAGE_REMOTE_PACKAGE}#
|
|
1876
|
+
typeSymbol: `${USAGE_REMOTE_PACKAGE}#CommandCodeAccountsReport`,
|
|
1551
1877
|
schema: usageReportSchema
|
|
1552
1878
|
}
|
|
1553
1879
|
}]
|
|
@@ -1568,13 +1894,24 @@ var CommandCodeUsageService = class extends TypertRemoteService {
|
|
|
1568
1894
|
this.deps = deps;
|
|
1569
1895
|
}
|
|
1570
1896
|
/**
|
|
1571
|
-
* Account, usage, and credit state for the settings page's account card
|
|
1572
|
-
*
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
1897
|
+
* Account, usage, and credit state for the settings page's account card —
|
|
1898
|
+
* one entry per pool account when the plugin entry wired `reports`, a
|
|
1899
|
+
* single default-account entry otherwise. Degrades per endpoint like the
|
|
1900
|
+
* `/commandcode` command (failures land in `report.failures`); throws
|
|
1901
|
+
* `MISSING_CREDENTIAL` when no key resolves, which the Gateway folds into
|
|
1902
|
+
* the failure branch the page renders as a hint.
|
|
1575
1903
|
*/
|
|
1576
1904
|
async report() {
|
|
1577
|
-
return this.deps.
|
|
1905
|
+
if (this.deps.reports !== void 0) return this.deps.reports();
|
|
1906
|
+
return { accounts: [{
|
|
1907
|
+
id: "default",
|
|
1908
|
+
label: "Default",
|
|
1909
|
+
configured: true,
|
|
1910
|
+
active: true,
|
|
1911
|
+
mark: "",
|
|
1912
|
+
cooldownUntil: 0,
|
|
1913
|
+
report: await this.deps.adapter.getUsage()
|
|
1914
|
+
}] };
|
|
1578
1915
|
}
|
|
1579
1916
|
};
|
|
1580
1917
|
/**
|
|
@@ -1634,7 +1971,13 @@ const Config = z.object({
|
|
|
1634
1971
|
modelsCachePath: z.string(),
|
|
1635
1972
|
requestTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
|
|
1636
1973
|
streamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
|
|
1637
|
-
filterModelsByPlan: z.boolean()
|
|
1974
|
+
filterModelsByPlan: z.boolean(),
|
|
1975
|
+
accounts: z.array(z.object({
|
|
1976
|
+
label: z.string(),
|
|
1977
|
+
apiKeyEnv: z.string().role("credential-ref"),
|
|
1978
|
+
apiKey: z.string()
|
|
1979
|
+
})),
|
|
1980
|
+
activeAccount: z.string()
|
|
1638
1981
|
});
|
|
1639
1982
|
/**
|
|
1640
1983
|
* The one explicit resolve step from raw config to validated connection
|
|
@@ -1666,25 +2009,60 @@ function apply(ctx, config) {
|
|
|
1666
2009
|
return next;
|
|
1667
2010
|
};
|
|
1668
2011
|
options();
|
|
2012
|
+
const slots = () => {
|
|
2013
|
+
const raw = current();
|
|
2014
|
+
const list = [{
|
|
2015
|
+
id: "default",
|
|
2016
|
+
label: "Default",
|
|
2017
|
+
ref: credentialRef(raw.apiKeyEnv ?? DEFAULT_API_KEY_ENV),
|
|
2018
|
+
literal: raw.apiKey,
|
|
2019
|
+
allowAuthFile: true
|
|
2020
|
+
}];
|
|
2021
|
+
for (const [index, account] of (raw.accounts ?? []).entries()) {
|
|
2022
|
+
const refName = typeof account.apiKeyEnv === "string" && account.apiKeyEnv.trim() !== "" ? account.apiKeyEnv.trim() : void 0;
|
|
2023
|
+
const literal = typeof account.apiKey === "string" && account.apiKey !== "" ? account.apiKey : void 0;
|
|
2024
|
+
if (refName === void 0 && literal === void 0) continue;
|
|
2025
|
+
list.push({
|
|
2026
|
+
id: refName ?? `account-${index + 2}`,
|
|
2027
|
+
label: typeof account.label === "string" && account.label.trim() !== "" ? account.label.trim() : `Account ${index + 2}`,
|
|
2028
|
+
ref: refName === void 0 ? void 0 : credentialRef(refName),
|
|
2029
|
+
literal,
|
|
2030
|
+
allowAuthFile: false
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
return list;
|
|
2034
|
+
};
|
|
2035
|
+
const preferredId = () => {
|
|
2036
|
+
const raw = current().activeAccount;
|
|
2037
|
+
return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
|
|
2038
|
+
};
|
|
2039
|
+
const resolveRef = async (ref) => {
|
|
2040
|
+
const credentials = ctx.get("credentials");
|
|
2041
|
+
if (credentials !== void 0) return (await credentials.resolve(ref))?.value;
|
|
2042
|
+
const ambient = launchEnvironmentOf(ctx).get(ref);
|
|
2043
|
+
return ambient !== void 0 && ambient.value.length > 0 ? ambient.value : void 0;
|
|
2044
|
+
};
|
|
2045
|
+
const pool = new CommandCodeAccountPool({
|
|
2046
|
+
slots,
|
|
2047
|
+
resolveRef,
|
|
2048
|
+
authFileKey: resolveAuthFileApiKey,
|
|
2049
|
+
probeWindow: (apiKey) => adapter.probeFiveHourWindow(apiKey),
|
|
2050
|
+
preferredId
|
|
2051
|
+
});
|
|
1669
2052
|
const resolveApiKey = async (connection) => {
|
|
1670
|
-
const
|
|
1671
|
-
if (
|
|
2053
|
+
const resolved = await pool.resolveKey();
|
|
2054
|
+
if (resolved !== void 0) return assertUsableApiKey(resolved.key, "llm-commandcode", resolved.slot.ref ?? `${resolved.slot.label} (config.apiKey)`);
|
|
1672
2055
|
const ref = connection.apiKeyEnv;
|
|
1673
|
-
const credentials = ctx.get("credentials");
|
|
1674
|
-
if (credentials !== void 0) {
|
|
1675
|
-
const hit = await credentials.resolve(ref);
|
|
1676
|
-
if (hit !== void 0) return assertUsableApiKey(hit.value, "llm-commandcode", ref);
|
|
1677
|
-
} else {
|
|
1678
|
-
const ambient = launchEnvironmentOf(ctx).get(ref);
|
|
1679
|
-
if (ambient !== void 0 && ambient.value.length > 0) return assertUsableApiKey(ambient.value, "llm-commandcode", ref);
|
|
1680
|
-
}
|
|
1681
|
-
const authFileKey = resolveAuthFileApiKey();
|
|
1682
|
-
if (authFileKey) return assertUsableApiKey(authFileKey, "llm-commandcode", "~/.commandcode/auth.json");
|
|
1683
2056
|
throw new LlmError(`llm-commandcode: no API key for provider route "${PROVIDER}"; store ${ref} through the credentials service (the web Models page writes it), export it in the launching environment, set config.apiKey, or run \`command-code login\` to write ~/.commandcode/auth.json`, "MISSING_CREDENTIAL");
|
|
1684
2057
|
};
|
|
1685
2058
|
const adapter = new CommandCodeAdapter({
|
|
1686
2059
|
options,
|
|
1687
2060
|
resolveApiKey,
|
|
2061
|
+
rotateApiKey: async (rejectedKey, rejection) => {
|
|
2062
|
+
pool.markRejected(rejectedKey, rejection);
|
|
2063
|
+
const resolved = await pool.resolveKey({ exclude: rejectedKey });
|
|
2064
|
+
return resolved === void 0 ? void 0 : assertUsableApiKey(resolved.key, "llm-commandcode", resolved.slot.ref ?? `${resolved.slot.label} (config.apiKey)`);
|
|
2065
|
+
},
|
|
1688
2066
|
resolveAttachments: () => {
|
|
1689
2067
|
const attachments = ctx.get("attachments");
|
|
1690
2068
|
return attachments === void 0 ? void 0 : attachments;
|
|
@@ -1697,10 +2075,42 @@ function apply(ctx, config) {
|
|
|
1697
2075
|
settingsPath: []
|
|
1698
2076
|
}]);
|
|
1699
2077
|
ctx.llm.registerAdapter([PROVIDER], adapter);
|
|
2078
|
+
const usageReports = async () => {
|
|
2079
|
+
const described = await pool.describeAccounts();
|
|
2080
|
+
const byId = new Map(described.map((account) => [account.slot.id, account]));
|
|
2081
|
+
const active = selectActiveAccount(await pool.resolvedAccounts(), preferredId());
|
|
2082
|
+
return { accounts: await Promise.all(slots().map(async (slot) => {
|
|
2083
|
+
const account = byId.get(slot.id);
|
|
2084
|
+
let report;
|
|
2085
|
+
if (account === void 0) report = { failures: [] };
|
|
2086
|
+
else try {
|
|
2087
|
+
report = await adapter.getUsage(account.key);
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
report = { failures: [error instanceof Error ? error.message : String(error)] };
|
|
2090
|
+
}
|
|
2091
|
+
const state = account?.state;
|
|
2092
|
+
const usable = accountUsable(state);
|
|
2093
|
+
return {
|
|
2094
|
+
id: slot.id,
|
|
2095
|
+
label: slot.label,
|
|
2096
|
+
configured: account !== void 0,
|
|
2097
|
+
active: account !== void 0 && active?.slot.id === slot.id,
|
|
2098
|
+
mark: usable ? "" : state?.kind === "disabled" ? "invalid-credential" : "rate-limit",
|
|
2099
|
+
cooldownUntil: !usable && state?.kind === "cooldown" ? state.until : 0,
|
|
2100
|
+
report
|
|
2101
|
+
};
|
|
2102
|
+
})) };
|
|
2103
|
+
};
|
|
1700
2104
|
ctx.inject(["commands"], (commandCtx) => {
|
|
1701
|
-
applyCommands(commandCtx, {
|
|
2105
|
+
applyCommands(commandCtx, {
|
|
2106
|
+
adapter,
|
|
2107
|
+
reports: usageReports
|
|
2108
|
+
});
|
|
2109
|
+
});
|
|
2110
|
+
applyUsageRemote(ctx, {
|
|
2111
|
+
adapter,
|
|
2112
|
+
reports: usageReports
|
|
1702
2113
|
});
|
|
1703
|
-
applyUsageRemote(ctx, { adapter });
|
|
1704
2114
|
installSettingsSection(ctx, NS, Config, config, {
|
|
1705
2115
|
setSource: (source) => {
|
|
1706
2116
|
current = source;
|
|
@@ -1709,6 +2119,6 @@ function apply(ctx, config) {
|
|
|
1709
2119
|
});
|
|
1710
2120
|
}
|
|
1711
2121
|
//#endregion
|
|
1712
|
-
export { BILLING_ACCESS_TTL_MS, COMMAND_CODE_CLI_VERSION, CommandCodeAdapter, 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, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, apply, applyCommands, applyUsageRemote, capabilityDescription, commandDefinition, compareByPlan, dealLabel, formatContext, inject, modelVisibleInPlan, name, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, subscriptionPlanInfo, usageReportSchema };
|
|
2122
|
+
export { BILLING_ACCESS_TTL_MS, COMMAND_CODE_CLI_VERSION, CommandCodeAccountPool, CommandCodeAdapter, 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, KNOWN_DEALS, KNOWN_EFFORTS, KNOWN_IMAGE_MODELS, KNOWN_PEAK_PRICING, KNOWN_PLANS, KNOWN_SUBSCRIPTION_PLANS, KNOWN_THINKING_MODELS, PLAN_LABELS, PLAN_ORDER, PROVIDER, USAGE_REPORT_ENDPOINT, accountUsable, apply, applyCommands, applyUsageRemote, capabilityDescription, commandDefinition, compareByPlan, dealLabel, formatContext, inject, modelVisibleInPlan, name, peakPricingLabel, peakPricingState, planLabel, projectSlugFromPath, resolveAdapterOptions, resolveAuthFileApiKey, selectActiveAccount, subscriptionPlanInfo, usageReportSchema };
|
|
1713
2123
|
|
|
1714
2124
|
//# sourceMappingURL=index.js.map
|