@mars-sea/dsh-commandcode-provider 0.4.2 → 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 +17 -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 +491 -95
- 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;
|
|
@@ -803,8 +810,8 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
803
810
|
catalog = [];
|
|
804
811
|
fetchImpl;
|
|
805
812
|
resolveAttachments;
|
|
806
|
-
billingAccess;
|
|
807
|
-
billingAccessInflight;
|
|
813
|
+
billingAccess = /* @__PURE__ */ new Map();
|
|
814
|
+
billingAccessInflight = /* @__PURE__ */ new Map();
|
|
808
815
|
constructor(deps) {
|
|
809
816
|
super();
|
|
810
817
|
this.deps = deps;
|
|
@@ -877,10 +884,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
877
884
|
};
|
|
878
885
|
}
|
|
879
886
|
/** The headers every authenticated account endpoint shares. */
|
|
880
|
-
async accountHeaders() {
|
|
887
|
+
async accountHeaders(apiKey) {
|
|
881
888
|
const connection = this.deps.options();
|
|
882
889
|
return {
|
|
883
|
-
Authorization: `Bearer ${await this.deps.resolveApiKey(connection)}`,
|
|
890
|
+
Authorization: `Bearer ${apiKey ?? await this.deps.resolveApiKey(connection)}`,
|
|
884
891
|
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
885
892
|
"x-cli-environment": "production",
|
|
886
893
|
...attributionHeaders()
|
|
@@ -892,18 +899,27 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
892
899
|
* `undefined` means "unknown — show everything" (fail-open).
|
|
893
900
|
*/
|
|
894
901
|
async loadBillingAccess() {
|
|
895
|
-
|
|
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);
|
|
896
909
|
if (cached !== void 0 && Date.now() - cached.at < 3e5) return cached.value;
|
|
897
|
-
|
|
898
|
-
|
|
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, {
|
|
899
914
|
value,
|
|
900
915
|
at: Date.now()
|
|
901
|
-
};
|
|
916
|
+
});
|
|
902
917
|
return value;
|
|
903
918
|
}).finally(() => {
|
|
904
|
-
this.billingAccessInflight
|
|
919
|
+
this.billingAccessInflight.delete(apiKey);
|
|
905
920
|
});
|
|
906
|
-
|
|
921
|
+
this.billingAccessInflight.set(apiKey, inflight);
|
|
922
|
+
return inflight;
|
|
907
923
|
}
|
|
908
924
|
/**
|
|
909
925
|
* The billing facts behind the picker's plan filter, mirroring the CLI's
|
|
@@ -914,10 +930,10 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
914
930
|
* fallback (the CLI stamps plan identity from it too). Any failure resolves
|
|
915
931
|
* to `undefined` (fail-open) rather than breaking the picker.
|
|
916
932
|
*/
|
|
917
|
-
async fetchBillingAccess() {
|
|
933
|
+
async fetchBillingAccess(apiKey) {
|
|
918
934
|
try {
|
|
919
935
|
const connection = this.deps.options();
|
|
920
|
-
const headers = await this.accountHeaders();
|
|
936
|
+
const headers = await this.accountHeaders(apiKey);
|
|
921
937
|
const base = connection.apiBase;
|
|
922
938
|
const getJson = async (path) => {
|
|
923
939
|
const response = await this.fetchImpl(`${base}${path}`, {
|
|
@@ -955,10 +971,12 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
955
971
|
* Each endpoint degrades independently: a failed one lands in `failures`
|
|
956
972
|
* while the rest still report, so a transient outage never blanks the whole
|
|
957
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.
|
|
958
976
|
*/
|
|
959
|
-
async getUsage() {
|
|
977
|
+
async getUsage(apiKey) {
|
|
960
978
|
const base = this.deps.options().apiBase;
|
|
961
|
-
const headers = await this.accountHeaders();
|
|
979
|
+
const headers = await this.accountHeaders(apiKey);
|
|
962
980
|
const failures = [];
|
|
963
981
|
const getJson = async (path) => {
|
|
964
982
|
try {
|
|
@@ -1036,6 +1054,35 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
1036
1054
|
}
|
|
1037
1055
|
return report;
|
|
1038
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
|
+
}
|
|
1039
1086
|
async *stream(options) {
|
|
1040
1087
|
if (options.stop?.length) throw new LlmError("Command Code adapter does not support stop sequences", "UNSUPPORTED_OPTION");
|
|
1041
1088
|
const hasImages = options.messages.some(hasImageContent);
|
|
@@ -1047,7 +1094,7 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
1047
1094
|
readImage = (ref) => attachments.readImage(ref).then((stored) => stored.data);
|
|
1048
1095
|
}
|
|
1049
1096
|
const connection = this.deps.options();
|
|
1050
|
-
|
|
1097
|
+
let apiKey = await this.deps.resolveApiKey(connection);
|
|
1051
1098
|
const modelMax = this.catalog.find((m) => m.id === options.model)?.maxTokens ?? 65536;
|
|
1052
1099
|
const maxTokens = Math.min(options.maxTokens ?? modelMax, modelMax, DEFAULT_GENERATE_MAX_TOKENS);
|
|
1053
1100
|
const effort = options.reasoningEffort;
|
|
@@ -1086,58 +1133,83 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
1086
1133
|
},
|
|
1087
1134
|
threadId: randomUUID()
|
|
1088
1135
|
};
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if (options.signal
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1109
|
-
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
|
1110
|
-
"x-cli-environment": "production",
|
|
1111
|
-
"x-project-slug": projectSlugFromPath(connection.workingDir),
|
|
1112
|
-
"x-taste-learning": "true",
|
|
1113
|
-
"x-co-flag": "false",
|
|
1114
|
-
...attributionHeaders()
|
|
1115
|
-
},
|
|
1116
|
-
body: JSON.stringify(body),
|
|
1117
|
-
signal: connectAbort.signal
|
|
1118
|
-
});
|
|
1119
|
-
clearTimeout(connectTimer);
|
|
1120
|
-
} catch (error) {
|
|
1121
|
-
clearTimeout(connectTimer);
|
|
1122
|
-
if (options.signal) options.signal.removeEventListener("abort", onCallerAbort);
|
|
1123
|
-
if (options.signal?.aborted) throw error;
|
|
1124
|
-
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 });
|
|
1125
|
-
throw new LlmError(`Command Code API request to ${connection.apiBase}/alpha/generate failed: ${errorChain(error)}`, "TRANSPORT", { cause: error });
|
|
1126
|
-
}
|
|
1127
|
-
if (!response.ok) {
|
|
1128
|
-
if (options.signal) options.signal.removeEventListener("abort", onCallerAbort);
|
|
1129
|
-
const errText = await response.text().catch(() => "");
|
|
1130
|
-
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;
|
|
1131
1155
|
try {
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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);
|
|
1138
1209
|
}
|
|
1210
|
+
const { response, cleanup } = connected;
|
|
1139
1211
|
if (!response.body) {
|
|
1140
|
-
|
|
1212
|
+
cleanup();
|
|
1141
1213
|
throw new LlmError("Command Code API returned no response body", "PROVIDER_PROTOCOL_ERROR");
|
|
1142
1214
|
}
|
|
1143
1215
|
const reader = response.body.getReader();
|
|
@@ -1348,18 +1420,220 @@ var CommandCodeAdapter = class extends LlmAdapter {
|
|
|
1348
1420
|
}
|
|
1349
1421
|
} finally {
|
|
1350
1422
|
clearIdle();
|
|
1351
|
-
|
|
1423
|
+
cleanup();
|
|
1352
1424
|
await reader.cancel().catch(() => void 0);
|
|
1353
1425
|
reader.releaseLock();
|
|
1354
1426
|
}
|
|
1355
1427
|
}
|
|
1356
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
|
+
}
|
|
1357
1447
|
function mapFinishReason(reason) {
|
|
1358
1448
|
if (reason === "tool-calls") return { kind: "tool-calls" };
|
|
1359
1449
|
if (reason === "length" || reason === "max_tokens" || reason === "max-tokens" || reason === "max_output_tokens") return { kind: "max-tokens" };
|
|
1360
1450
|
return { kind: "stop" };
|
|
1361
1451
|
}
|
|
1362
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
|
|
1363
1637
|
//#region src/commands.ts
|
|
1364
1638
|
/** Format a dollar amount. */
|
|
1365
1639
|
function money(value) {
|
|
@@ -1392,11 +1666,18 @@ function bar(used, cap) {
|
|
|
1392
1666
|
const filled = Math.round(ratio * 10);
|
|
1393
1667
|
return "█".repeat(filled) + "░".repeat(10 - filled);
|
|
1394
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
|
+
}
|
|
1395
1676
|
/** Render the usage report as a structured, aligned, bar-chart text view. */
|
|
1396
|
-
function renderReport(report) {
|
|
1677
|
+
function renderReport(report, title) {
|
|
1397
1678
|
const lines = [];
|
|
1398
1679
|
const account = report.account ? ` (${report.account.userName || report.account.name})` : "";
|
|
1399
|
-
lines.push(`📊 Command Code 用量${account}`, "");
|
|
1680
|
+
lines.push(title ?? `📊 Command Code 用量${account}`, "");
|
|
1400
1681
|
if (report.plan && report.plan.name !== "") {
|
|
1401
1682
|
const p = report.plan;
|
|
1402
1683
|
const status = p.status !== "" && p.status !== "active" ? ` (${p.status})` : "";
|
|
@@ -1425,6 +1706,18 @@ function commandDefinition(deps) {
|
|
|
1425
1706
|
input: { hint: "[status]" },
|
|
1426
1707
|
handler: async () => {
|
|
1427
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
|
+
}
|
|
1428
1721
|
return {
|
|
1429
1722
|
kind: "success",
|
|
1430
1723
|
text: renderReport(await adapter.getUsage())
|
|
@@ -1541,12 +1834,31 @@ function parseUsageReport(value) {
|
|
|
1541
1834
|
}
|
|
1542
1835
|
return report;
|
|
1543
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
|
+
}
|
|
1544
1856
|
/**
|
|
1545
1857
|
* The strict result codec both halves attach to the descriptor. Hand-rolled:
|
|
1546
1858
|
* the client bundle may not require a schema library, and `TypertSchema` is
|
|
1547
1859
|
* deliberately minimal so one `parse` function satisfies it.
|
|
1548
1860
|
*/
|
|
1549
|
-
const usageReportSchema = { parse:
|
|
1861
|
+
const usageReportSchema = { parse: parseAccountsReport };
|
|
1550
1862
|
/** The Host-face contribution registered on `ctx.typert`. */
|
|
1551
1863
|
const USAGE_HOST_CONTRIBUTION = {
|
|
1552
1864
|
package: USAGE_REMOTE_PACKAGE,
|
|
@@ -1561,7 +1873,7 @@ const USAGE_HOST_CONTRIBUTION = {
|
|
|
1561
1873
|
parameters: [],
|
|
1562
1874
|
result: {
|
|
1563
1875
|
mode: "strict",
|
|
1564
|
-
typeSymbol: `${USAGE_REMOTE_PACKAGE}#
|
|
1876
|
+
typeSymbol: `${USAGE_REMOTE_PACKAGE}#CommandCodeAccountsReport`,
|
|
1565
1877
|
schema: usageReportSchema
|
|
1566
1878
|
}
|
|
1567
1879
|
}]
|
|
@@ -1582,13 +1894,24 @@ var CommandCodeUsageService = class extends TypertRemoteService {
|
|
|
1582
1894
|
this.deps = deps;
|
|
1583
1895
|
}
|
|
1584
1896
|
/**
|
|
1585
|
-
* Account, usage, and credit state for the settings page's account card
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
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.
|
|
1589
1903
|
*/
|
|
1590
1904
|
async report() {
|
|
1591
|
-
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
|
+
}] };
|
|
1592
1915
|
}
|
|
1593
1916
|
};
|
|
1594
1917
|
/**
|
|
@@ -1648,7 +1971,13 @@ const Config = z.object({
|
|
|
1648
1971
|
modelsCachePath: z.string(),
|
|
1649
1972
|
requestTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
|
|
1650
1973
|
streamIdleTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS),
|
|
1651
|
-
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()
|
|
1652
1981
|
});
|
|
1653
1982
|
/**
|
|
1654
1983
|
* The one explicit resolve step from raw config to validated connection
|
|
@@ -1680,25 +2009,60 @@ function apply(ctx, config) {
|
|
|
1680
2009
|
return next;
|
|
1681
2010
|
};
|
|
1682
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
|
+
});
|
|
1683
2052
|
const resolveApiKey = async (connection) => {
|
|
1684
|
-
const
|
|
1685
|
-
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)`);
|
|
1686
2055
|
const ref = connection.apiKeyEnv;
|
|
1687
|
-
const credentials = ctx.get("credentials");
|
|
1688
|
-
if (credentials !== void 0) {
|
|
1689
|
-
const hit = await credentials.resolve(ref);
|
|
1690
|
-
if (hit !== void 0) return assertUsableApiKey(hit.value, "llm-commandcode", ref);
|
|
1691
|
-
} else {
|
|
1692
|
-
const ambient = launchEnvironmentOf(ctx).get(ref);
|
|
1693
|
-
if (ambient !== void 0 && ambient.value.length > 0) return assertUsableApiKey(ambient.value, "llm-commandcode", ref);
|
|
1694
|
-
}
|
|
1695
|
-
const authFileKey = resolveAuthFileApiKey();
|
|
1696
|
-
if (authFileKey) return assertUsableApiKey(authFileKey, "llm-commandcode", "~/.commandcode/auth.json");
|
|
1697
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");
|
|
1698
2057
|
};
|
|
1699
2058
|
const adapter = new CommandCodeAdapter({
|
|
1700
2059
|
options,
|
|
1701
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
|
+
},
|
|
1702
2066
|
resolveAttachments: () => {
|
|
1703
2067
|
const attachments = ctx.get("attachments");
|
|
1704
2068
|
return attachments === void 0 ? void 0 : attachments;
|
|
@@ -1711,10 +2075,42 @@ function apply(ctx, config) {
|
|
|
1711
2075
|
settingsPath: []
|
|
1712
2076
|
}]);
|
|
1713
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
|
+
};
|
|
1714
2104
|
ctx.inject(["commands"], (commandCtx) => {
|
|
1715
|
-
applyCommands(commandCtx, {
|
|
2105
|
+
applyCommands(commandCtx, {
|
|
2106
|
+
adapter,
|
|
2107
|
+
reports: usageReports
|
|
2108
|
+
});
|
|
2109
|
+
});
|
|
2110
|
+
applyUsageRemote(ctx, {
|
|
2111
|
+
adapter,
|
|
2112
|
+
reports: usageReports
|
|
1716
2113
|
});
|
|
1717
|
-
applyUsageRemote(ctx, { adapter });
|
|
1718
2114
|
installSettingsSection(ctx, NS, Config, config, {
|
|
1719
2115
|
setSource: (source) => {
|
|
1720
2116
|
current = source;
|
|
@@ -1723,6 +2119,6 @@ function apply(ctx, config) {
|
|
|
1723
2119
|
});
|
|
1724
2120
|
}
|
|
1725
2121
|
//#endregion
|
|
1726
|
-
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 };
|
|
1727
2123
|
|
|
1728
2124
|
//# sourceMappingURL=index.js.map
|