@tokenbuddy/tokenbuddy 1.0.11 → 1.0.13
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/dist/src/buyer-store.d.ts +61 -0
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +12 -0
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts +47 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +287 -63
- package/dist/src/cli.js.map +1 -1
- package/dist/src/credit-tracker.d.ts +26 -0
- package/dist/src/credit-tracker.d.ts.map +1 -1
- package/dist/src/credit-tracker.js +8 -0
- package/dist/src/credit-tracker.js.map +1 -1
- package/dist/src/daemon.d.ts +29 -3
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +292 -65
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-clawtip-wallet.d.ts +25 -0
- package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -1
- package/dist/src/doctor-clawtip-wallet.js +13 -0
- package/dist/src/doctor-clawtip-wallet.js.map +1 -1
- package/dist/src/doctor-diagnostics.d.ts +63 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -1
- package/dist/src/doctor-diagnostics.js +39 -1
- package/dist/src/doctor-diagnostics.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/init-clawtip-activation.d.ts +103 -0
- package/dist/src/init-clawtip-activation.d.ts.map +1 -1
- package/dist/src/init-clawtip-activation.js +60 -0
- package/dist/src/init-clawtip-activation.js.map +1 -1
- package/dist/src/init-payment-options.d.ts +124 -0
- package/dist/src/init-payment-options.d.ts.map +1 -1
- package/dist/src/init-payment-options.js +68 -0
- package/dist/src/init-payment-options.js.map +1 -1
- package/dist/src/model-index.d.ts +9 -0
- package/dist/src/model-index.d.ts.map +1 -1
- package/dist/src/model-index.js.map +1 -1
- package/dist/src/prewarm-cache.d.ts +89 -0
- package/dist/src/prewarm-cache.d.ts.map +1 -1
- package/dist/src/prewarm-cache.js +14 -1
- package/dist/src/prewarm-cache.js.map +1 -1
- package/dist/src/prewarm-scheduler.d.ts +62 -3
- package/dist/src/prewarm-scheduler.d.ts.map +1 -1
- package/dist/src/prewarm-scheduler.js +39 -8
- package/dist/src/prewarm-scheduler.js.map +1 -1
- package/dist/src/provider-install.d.ts +89 -3
- package/dist/src/provider-install.d.ts.map +1 -1
- package/dist/src/provider-install.js +77 -17
- package/dist/src/provider-install.js.map +1 -1
- package/dist/src/route-failover.d.ts +48 -0
- package/dist/src/route-failover.d.ts.map +1 -1
- package/dist/src/route-failover.js.map +1 -1
- package/dist/src/seller-catalog.d.ts +158 -10
- package/dist/src/seller-catalog.d.ts.map +1 -1
- package/dist/src/seller-catalog.js +79 -5
- package/dist/src/seller-catalog.js.map +1 -1
- package/dist/src/seller-metadata-cache.d.ts +29 -0
- package/dist/src/seller-metadata-cache.d.ts.map +1 -0
- package/dist/src/seller-metadata-cache.js +71 -0
- package/dist/src/seller-metadata-cache.js.map +1 -0
- package/dist/src/seller-pool.d.ts +71 -0
- package/dist/src/seller-pool.d.ts.map +1 -1
- package/dist/src/seller-pool.js +6 -1
- package/dist/src/seller-pool.js.map +1 -1
- package/dist/src/seller-route-planner.d.ts +118 -0
- package/dist/src/seller-route-planner.d.ts.map +1 -0
- package/dist/src/seller-route-planner.js +160 -0
- package/dist/src/seller-route-planner.js.map +1 -0
- package/dist/src/seller-routing-config.d.ts +69 -0
- package/dist/src/seller-routing-config.d.ts.map +1 -0
- package/dist/src/seller-routing-config.js +164 -0
- package/dist/src/seller-routing-config.js.map +1 -0
- package/dist/src/seller-routing-strategy.d.ts +118 -0
- package/dist/src/seller-routing-strategy.d.ts.map +1 -0
- package/dist/src/seller-routing-strategy.js +183 -0
- package/dist/src/seller-routing-strategy.js.map +1 -0
- package/dist/src/stream-failover.d.ts +23 -0
- package/dist/src/stream-failover.d.ts.map +1 -1
- package/dist/src/stream-failover.js +4 -0
- package/dist/src/stream-failover.js.map +1 -1
- package/dist/src/tb-proxyd.js +7 -21
- package/dist/src/tb-proxyd.js.map +1 -1
- package/dist/src/terminal-detect.d.ts +51 -0
- package/dist/src/terminal-detect.d.ts.map +1 -1
- package/dist/src/terminal-detect.js +42 -0
- package/dist/src/terminal-detect.js.map +1 -1
- package/dist/src/terminal-image.d.ts +41 -0
- package/dist/src/terminal-image.d.ts.map +1 -1
- package/dist/src/terminal-image.js +15 -0
- package/dist/src/terminal-image.js.map +1 -1
- package/package.json +1 -1
- package/src/buyer-store.ts +61 -0
- package/src/cli.ts +330 -68
- package/src/credit-tracker.ts +26 -0
- package/src/daemon.ts +363 -72
- package/src/doctor-clawtip-wallet.ts +25 -0
- package/src/doctor-diagnostics.ts +63 -1
- package/src/index.ts +4 -0
- package/src/init-clawtip-activation.ts +103 -0
- package/src/init-payment-options.ts +124 -0
- package/src/model-index.ts +9 -0
- package/src/prewarm-cache.ts +99 -1
- package/src/prewarm-scheduler.ts +97 -12
- package/src/provider-install.ts +125 -25
- package/src/route-failover.ts +48 -0
- package/src/seller-catalog.ts +158 -12
- package/src/seller-metadata-cache.ts +91 -0
- package/src/seller-pool.ts +77 -1
- package/src/seller-route-planner.ts +323 -0
- package/src/seller-routing-config.ts +198 -0
- package/src/seller-routing-strategy.ts +316 -0
- package/src/stream-failover.ts +23 -0
- package/src/tb-proxyd.ts +7 -23
- package/src/terminal-detect.ts +51 -0
- package/src/terminal-image.ts +41 -0
- package/tests/cli-routing.test.ts +287 -0
- package/tests/daemon-classify.test.ts +431 -0
- package/tests/daemon-roles.test.ts +92 -0
- package/tests/seller-catalog-utilities.test.ts +70 -0
- package/tests/seller-metadata-cache.test.ts +89 -0
- package/tests/seller-route-planner.test.ts +150 -0
- package/tests/seller-routing-config.test.ts +111 -0
- package/tests/seller-routing-strategy.test.ts +166 -0
- package/tests/tokenbuddy.test.ts +447 -33
- /package/{src → tests}/credit-tracker.test.ts +0 -0
- /package/{src → tests}/model-index.test.ts +0 -0
- /package/{src → tests}/prewarm-cache.test.ts +0 -0
- /package/{src → tests}/prewarm-scheduler.test.ts +0 -0
- /package/{src → tests}/route-failover.test.ts +0 -0
- /package/{src → tests}/seller-catalog-413.test.ts +0 -0
- /package/{src → tests}/seller-pool.test.ts +0 -0
- /package/{src → tests}/stream-failover.test.ts +0 -0
- /package/{src → tests}/thousand-seller.test.ts +0 -0
package/dist/src/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as p from "@clack/prompts";
|
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import * as os from "os";
|
|
6
|
-
import {
|
|
6
|
+
import { execFileSync, spawn } from "child_process";
|
|
7
7
|
import Table from "cli-table3";
|
|
8
8
|
import { BuyerStore } from "./buyer-store.js";
|
|
9
9
|
import { applyProviderInstall, detectProviders, getProviderModelSelectionKind, getProviderProtocolPreference, } from "./provider-install.js";
|
|
@@ -11,6 +11,7 @@ import { createModuleLogger } from "@tokenbuddy/logging";
|
|
|
11
11
|
import * as crypto from "crypto";
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
13
|
import { discoverSellerBackedModels, filterCatalogByProtocol, filterCatalogBySeller, } from "./seller-catalog.js";
|
|
14
|
+
import { assertSellerRoutingConfig, defaultSellerRoutingConfig, normalizeSellerRoutingConfig, parseSellerIdList, ROUTING_CONFIG_KEY, } from "./seller-routing-config.js";
|
|
14
15
|
import { collectDoctorDiagnostics, collectDoctorModelsSummary, printDoctorProviders, printDoctorModelsSummary, readDoctorProviders, renderDoctorDiagnosticsProgressively, } from "./doctor-diagnostics.js";
|
|
15
16
|
import { buildInitSuccessMessage, buildInitTerminalSelectionState, buildInstalledTerminalMessage, INIT_PAYMENT_OPTIONS, inspectClawtipWalletReadiness, inspectOpenClawWalletConfig, noteInitComingSoonPayments, OTHER_TERMINAL_OPTION, validateInitTerminalSelection, } from "./init-payment-options.js";
|
|
16
17
|
import { checkOpenClawRuntime, readClawtipPayCredential, startClawtipWalletBootstrap, waitForClawtipActivationConfirmation, } from "./init-clawtip-activation.js";
|
|
@@ -19,6 +20,11 @@ import { displayTerminalImage } from "./terminal-image.js";
|
|
|
19
20
|
import qrcode from "qrcode-terminal";
|
|
20
21
|
const CONTROL_PORT = 17820;
|
|
21
22
|
const PROXY_PORT = 17821;
|
|
23
|
+
const TOKENBUDDY_LAUNCHD_LABEL = "com.tokenbuddy.proxyd";
|
|
24
|
+
const STALE_TOKENBUDDY_LAUNCHD_LABELS = [
|
|
25
|
+
"com.tokenbuddy.tb-proxyd",
|
|
26
|
+
"homebrew.mxcl.tokenbuddy",
|
|
27
|
+
];
|
|
22
28
|
const logger = createModuleLogger("tokenbuddy-cli");
|
|
23
29
|
const SUPPORTED_PAYMENT_METHODS = ["mock", "clawtip"];
|
|
24
30
|
function isSupportedPaymentMethod(method) {
|
|
@@ -101,6 +107,85 @@ function defaultProxydLogPath(kind) {
|
|
|
101
107
|
function tbProxydScriptPath() {
|
|
102
108
|
return path.resolve(currentModuleDir(), "./tb-proxyd.js");
|
|
103
109
|
}
|
|
110
|
+
function escapeXmlText(value) {
|
|
111
|
+
return value
|
|
112
|
+
.replace(/&/g, "&")
|
|
113
|
+
.replace(/</g, "<")
|
|
114
|
+
.replace(/>/g, ">");
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 构造 macOS launchd plist 的 XML 文本,用于 `launchctl load` 注册 `tb-proxyd`。
|
|
118
|
+
* 已转义所有插值字段,避免 XML 注入;env 块只输出非空项。
|
|
119
|
+
*
|
|
120
|
+
* @param options plist 字段
|
|
121
|
+
* @returns 完整的 plist XML 字符串
|
|
122
|
+
*/
|
|
123
|
+
export function buildLaunchdPlistContent(options) {
|
|
124
|
+
const env = {
|
|
125
|
+
TB_PROXYD_CONTROL_PORT: String(options.controlPort),
|
|
126
|
+
TB_PROXYD_PROXY_PORT: String(options.proxyPort),
|
|
127
|
+
TB_PROXYD_SELLER_REGISTRY_URL: options.sellerRegistryUrl,
|
|
128
|
+
};
|
|
129
|
+
if (options.clawtipProofCommand?.trim()) {
|
|
130
|
+
env.TB_PROXYD_CLAWTIP_PROOF_COMMAND = options.clawtipProofCommand.trim();
|
|
131
|
+
}
|
|
132
|
+
if (options.clawtipProofTimeoutMs !== undefined) {
|
|
133
|
+
env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS = String(options.clawtipProofTimeoutMs);
|
|
134
|
+
}
|
|
135
|
+
const envEntries = Object.entries(env)
|
|
136
|
+
.map(([key, value]) => ` <key>${escapeXmlText(key)}</key>\n <string>${escapeXmlText(value)}</string>`)
|
|
137
|
+
.join("\n");
|
|
138
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
139
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
140
|
+
<plist version="1.0">
|
|
141
|
+
<dict>
|
|
142
|
+
<key>Label</key>
|
|
143
|
+
<string>${escapeXmlText(options.label)}</string>
|
|
144
|
+
<key>ProgramArguments</key>
|
|
145
|
+
<array>
|
|
146
|
+
<string>${escapeXmlText(options.nodePath)}</string>
|
|
147
|
+
<string>${escapeXmlText(options.scriptPath)}</string>
|
|
148
|
+
</array>
|
|
149
|
+
<key>EnvironmentVariables</key>
|
|
150
|
+
<dict>
|
|
151
|
+
${envEntries}
|
|
152
|
+
</dict>
|
|
153
|
+
<key>RunAtLoad</key>
|
|
154
|
+
<true/>
|
|
155
|
+
<key>KeepAlive</key>
|
|
156
|
+
<true/>
|
|
157
|
+
<key>StandardOutPath</key>
|
|
158
|
+
<string>${escapeXmlText(options.stdoutPath)}</string>
|
|
159
|
+
<key>StandardErrorPath</key>
|
|
160
|
+
<string>${escapeXmlText(options.stderrPath)}</string>
|
|
161
|
+
</dict>
|
|
162
|
+
</plist>`;
|
|
163
|
+
}
|
|
164
|
+
function launchdUserDomain() {
|
|
165
|
+
if (typeof process.getuid === "function") {
|
|
166
|
+
return `gui/${process.getuid()}`;
|
|
167
|
+
}
|
|
168
|
+
return "gui/501";
|
|
169
|
+
}
|
|
170
|
+
function runLaunchctl(args, ignoreFailure = false) {
|
|
171
|
+
try {
|
|
172
|
+
execFileSync("launchctl", args, { stdio: "ignore" });
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (!ignoreFailure) {
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function installLaunchAgent(plistPath, label) {
|
|
181
|
+
const domain = launchdUserDomain();
|
|
182
|
+
for (const staleLabel of STALE_TOKENBUDDY_LAUNCHD_LABELS) {
|
|
183
|
+
runLaunchctl(["bootout", `${domain}/${staleLabel}`], true);
|
|
184
|
+
}
|
|
185
|
+
runLaunchctl(["bootout", `${domain}/${label}`], true);
|
|
186
|
+
runLaunchctl(["bootstrap", domain, plistPath]);
|
|
187
|
+
runLaunchctl(["kickstart", "-k", `${domain}/${label}`]);
|
|
188
|
+
}
|
|
104
189
|
async function repairDaemon(controlPort) {
|
|
105
190
|
const existing = await probeDaemonStatus(controlPort);
|
|
106
191
|
if (existing.running) {
|
|
@@ -159,7 +244,7 @@ function rootActionName(command) {
|
|
|
159
244
|
}
|
|
160
245
|
function commandRequiresDaemon(command) {
|
|
161
246
|
const rootName = rootActionName(command);
|
|
162
|
-
return rootName !== "doctor" && rootName !== "init";
|
|
247
|
+
return rootName !== "doctor" && rootName !== "init" && rootName !== "routing";
|
|
163
248
|
}
|
|
164
249
|
async function enforceDaemonGate(command) {
|
|
165
250
|
if (!commandRequiresDaemon(command)) {
|
|
@@ -236,6 +321,14 @@ function printPaymentList(payments, asJson) {
|
|
|
236
321
|
console.log("=== TokenBuddy Payment Methods ===");
|
|
237
322
|
console.log(table.toString());
|
|
238
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* 调 wallet-bootstrap 的 `/payments/clawtip/bootstrap` 端点,拿到激活支付参数。
|
|
326
|
+
* 校验:HTTP 200、订单字段齐全、`payTo` 不是占位符。
|
|
327
|
+
*
|
|
328
|
+
* @param bootstrapUrl wallet-bootstrap 服务 base URL
|
|
329
|
+
* @returns bootstrap 响应(含 `payment` 字段)
|
|
330
|
+
* @throws Error 任何校验失败
|
|
331
|
+
*/
|
|
239
332
|
export async function fetchClawtipBootstrap(bootstrapUrl) {
|
|
240
333
|
const response = await fetch(`${bootstrapUrl.replace(/\/+$/, "")}/payments/clawtip/bootstrap`, {
|
|
241
334
|
method: "POST",
|
|
@@ -259,6 +352,15 @@ export async function fetchClawtipBootstrap(bootstrapUrl) {
|
|
|
259
352
|
body.payment.resourceUrl = normalizeClawtipBootstrapResourceUrl(bootstrapUrl, body.payment.resourceUrl);
|
|
260
353
|
return body;
|
|
261
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* 修正 Clawtip bootstrap 返回的 `resourceUrl`。
|
|
357
|
+
* 早期 bootstrap 返回 `/registry/sellers` 这种占位 URL,把它替换成当前 bootstrap URL 的 path,
|
|
358
|
+
* 让 buyer 正确指向 wallet-bootstrap。
|
|
359
|
+
*
|
|
360
|
+
* @param bootstrapUrl wallet-bootstrap base URL
|
|
361
|
+
* @param resourceUrl bootstrap 响应中的 `resourceUrl` 字段
|
|
362
|
+
* @returns 修正后的 resource URL(无法解析时返回原值)
|
|
363
|
+
*/
|
|
262
364
|
export function normalizeClawtipBootstrapResourceUrl(bootstrapUrl, resourceUrl) {
|
|
263
365
|
try {
|
|
264
366
|
const bootstrap = new URL(bootstrapUrl);
|
|
@@ -318,50 +420,137 @@ async function promptSellerRoutingPreference(catalog) {
|
|
|
318
420
|
message: "Choose seller routing mode for tb-proxyd:",
|
|
319
421
|
options: [
|
|
320
422
|
{
|
|
321
|
-
value: "
|
|
322
|
-
label: "Auto",
|
|
323
|
-
hint: "
|
|
423
|
+
value: "fullAuto",
|
|
424
|
+
label: "Full Auto",
|
|
425
|
+
hint: "Use all compatible sellers and rank them by the selected scorer.",
|
|
324
426
|
},
|
|
325
427
|
{
|
|
326
428
|
value: "fixed",
|
|
327
429
|
label: "Fixed Seller",
|
|
328
430
|
hint: "Pin tb-proxyd to one seller and only use models from that seller.",
|
|
329
431
|
},
|
|
432
|
+
{
|
|
433
|
+
value: "fixedSet",
|
|
434
|
+
label: "Fixed Seller Set",
|
|
435
|
+
hint: "Use only a selected seller pool and rank within that pool.",
|
|
436
|
+
},
|
|
330
437
|
],
|
|
331
438
|
});
|
|
332
439
|
if (typeof mode !== "string") {
|
|
333
440
|
throw new Error("seller routing selection was cancelled");
|
|
334
441
|
}
|
|
335
|
-
if (mode === "
|
|
336
|
-
return { mode };
|
|
337
|
-
}
|
|
338
|
-
if (healthySellers.length === 0) {
|
|
442
|
+
if ((mode === "fixed" || mode === "fixedSet") && healthySellers.length === 0) {
|
|
339
443
|
throw new Error("no healthy sellers available for fixed routing");
|
|
340
444
|
}
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
445
|
+
const scorer = await promptSellerRoutingScorer();
|
|
446
|
+
if (mode === "fullAuto") {
|
|
447
|
+
return { mode, scorer };
|
|
448
|
+
}
|
|
449
|
+
if (mode === "fixed") {
|
|
450
|
+
const sellerId = await p.select({
|
|
451
|
+
message: "Choose the seller to pin tb-proxyd to:",
|
|
452
|
+
options: sellerChoices(healthySellers),
|
|
453
|
+
});
|
|
454
|
+
if (typeof sellerId !== "string") {
|
|
455
|
+
throw new Error("fixed seller selection was cancelled");
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
mode,
|
|
459
|
+
sellerId,
|
|
460
|
+
scorer,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const sellerIds = await p.multiselect({
|
|
464
|
+
message: "Choose the seller pool for fixedSet routing:",
|
|
465
|
+
options: sellerChoices(healthySellers),
|
|
466
|
+
required: true
|
|
467
|
+
});
|
|
468
|
+
if (!Array.isArray(sellerIds)) {
|
|
469
|
+
throw new Error("fixedSet seller selection was cancelled");
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
mode,
|
|
473
|
+
sellerIds,
|
|
474
|
+
scorer,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
async function promptSellerRoutingScorer() {
|
|
478
|
+
const scorer = await p.select({
|
|
479
|
+
message: "Choose seller ranking preference:",
|
|
480
|
+
options: [
|
|
481
|
+
{ value: "balanced", label: "Balanced", hint: "Balance health, latency, and discount." },
|
|
482
|
+
{ value: "speed", label: "Speed", hint: "Prefer healthier, faster sellers." },
|
|
483
|
+
{ value: "discount", label: "Discount", hint: "Prefer lower discount ratio first." },
|
|
484
|
+
],
|
|
355
485
|
});
|
|
356
|
-
if (typeof
|
|
357
|
-
throw new Error("
|
|
486
|
+
if (typeof scorer !== "string") {
|
|
487
|
+
throw new Error("seller scorer selection was cancelled");
|
|
488
|
+
}
|
|
489
|
+
return scorer;
|
|
490
|
+
}
|
|
491
|
+
function sellerChoices(sellers) {
|
|
492
|
+
return sellers.map((seller) => ({
|
|
493
|
+
value: seller.id,
|
|
494
|
+
label: seller.name ? `${seller.name} (${seller.id})` : seller.id,
|
|
495
|
+
hint: [
|
|
496
|
+
seller.discountRatio != null ? `discount x${seller.discountRatio}` : null,
|
|
497
|
+
seller.modelCount != null ? `${seller.modelCount} models` : null,
|
|
498
|
+
seller.supportedProtocols?.length ? seller.supportedProtocols.join(",") : null,
|
|
499
|
+
seller.paymentMethods?.length ? seller.paymentMethods.join(",") : null,
|
|
500
|
+
]
|
|
501
|
+
.filter(Boolean)
|
|
502
|
+
.join(" · ") || seller.url,
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
function buildRoutingConfigFromOptions(modeValue, options) {
|
|
506
|
+
const mode = parseRoutingModeValue(modeValue);
|
|
507
|
+
const scorer = parseRoutingScorerValue(options.scorer || "balanced");
|
|
508
|
+
if (mode === "fixed") {
|
|
509
|
+
return {
|
|
510
|
+
mode,
|
|
511
|
+
sellerId: options.seller?.trim(),
|
|
512
|
+
scorer
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (mode === "fixedSet") {
|
|
516
|
+
return {
|
|
517
|
+
mode,
|
|
518
|
+
sellerIds: parseSellerIdList(options.sellerSet || ""),
|
|
519
|
+
scorer
|
|
520
|
+
};
|
|
358
521
|
}
|
|
359
522
|
return {
|
|
360
523
|
mode,
|
|
361
|
-
|
|
524
|
+
scorer
|
|
362
525
|
};
|
|
363
526
|
}
|
|
364
|
-
|
|
527
|
+
function parseRoutingModeValue(value) {
|
|
528
|
+
if (value === "fixed" || value === "fixedSet" || value === "fullAuto") {
|
|
529
|
+
return value;
|
|
530
|
+
}
|
|
531
|
+
throw new Error("routing mode must be fixed, fixedSet, or fullAuto");
|
|
532
|
+
}
|
|
533
|
+
function parseRoutingScorerValue(value) {
|
|
534
|
+
if (value === "balanced" || value === "speed" || value === "discount") {
|
|
535
|
+
return value;
|
|
536
|
+
}
|
|
537
|
+
throw new Error("routing scorer must be balanced, speed, or discount");
|
|
538
|
+
}
|
|
539
|
+
function printRoutingConfig(config, updatedAt) {
|
|
540
|
+
console.log("=== TokenBuddy Seller Routing ===");
|
|
541
|
+
console.log(`Mode: ${config.mode}`);
|
|
542
|
+
console.log(`Scorer: ${config.scorer}`);
|
|
543
|
+
if (config.mode === "fixed") {
|
|
544
|
+
console.log(`Seller: ${config.sellerId || "(not configured)"}`);
|
|
545
|
+
}
|
|
546
|
+
if (config.mode === "fixedSet") {
|
|
547
|
+
console.log(`Seller Set: ${config.sellerIds?.join(",") || "(empty)"}`);
|
|
548
|
+
}
|
|
549
|
+
if (updatedAt) {
|
|
550
|
+
console.log(`Updated: ${updatedAt}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
async function promptSingleModelSelection(providerId, models) {
|
|
365
554
|
const protocolPreference = getProviderProtocolPreference(providerId);
|
|
366
555
|
const protocolFiltered = protocolPreference
|
|
367
556
|
? filterCatalogByProtocol(models, protocolPreference)
|
|
@@ -385,12 +574,10 @@ async function promptSingleModelSelection(providerId, models, sellerRouting) {
|
|
|
385
574
|
if (typeof selectedModel !== "string") {
|
|
386
575
|
throw new Error(`default model selection was cancelled for ${providerId}`);
|
|
387
576
|
}
|
|
388
|
-
const selectedEntry = protocolFiltered.find((entry) => entry.id === selectedModel);
|
|
389
577
|
return {
|
|
390
578
|
selectionKind: "single-model",
|
|
391
579
|
protocolPreference,
|
|
392
580
|
defaultModel: selectedModel,
|
|
393
|
-
sellerId: sellerRouting.mode === "fixed" ? selectedEntry?.sellerId : undefined,
|
|
394
581
|
};
|
|
395
582
|
}
|
|
396
583
|
function defaultClaudeDisplayName(modelId) {
|
|
@@ -484,7 +671,9 @@ async function promptClaudeCodeModelSelection(models) {
|
|
|
484
671
|
async function promptProviderSelections(providerIds, catalog, sellerRouting) {
|
|
485
672
|
const baseModels = sellerRouting.mode === "fixed"
|
|
486
673
|
? filterCatalogBySeller(catalog.models, sellerRouting.sellerId)
|
|
487
|
-
:
|
|
674
|
+
: sellerRouting.mode === "fixedSet"
|
|
675
|
+
? catalog.models.filter((model) => sellerRouting.sellerIds?.includes(model.sellerId))
|
|
676
|
+
: catalog.models;
|
|
488
677
|
const selections = {};
|
|
489
678
|
for (const providerId of providerIds) {
|
|
490
679
|
const selectionKind = getProviderModelSelectionKind(providerId);
|
|
@@ -492,10 +681,16 @@ async function promptProviderSelections(providerIds, catalog, sellerRouting) {
|
|
|
492
681
|
selections[providerId] = await promptClaudeCodeModelSelection(baseModels);
|
|
493
682
|
continue;
|
|
494
683
|
}
|
|
495
|
-
selections[providerId] = await promptSingleModelSelection(providerId, baseModels
|
|
684
|
+
selections[providerId] = await promptSingleModelSelection(providerId, baseModels);
|
|
496
685
|
}
|
|
497
686
|
return selections;
|
|
498
687
|
}
|
|
688
|
+
/**
|
|
689
|
+
* 构造 commander program,绑定所有 `tb` 子命令(doctor / init / config / provider 等)。
|
|
690
|
+
* 在 `preAction` 钩子里做 daemon gate:非 daemon-only 命令跳过,其余命令需要本地 `tb-proxyd` 在跑。
|
|
691
|
+
*
|
|
692
|
+
* @returns 配置完整的 commander Command 实例
|
|
693
|
+
*/
|
|
499
694
|
export function buildCli() {
|
|
500
695
|
const program = new Command();
|
|
501
696
|
program
|
|
@@ -789,6 +984,45 @@ export function buildCli() {
|
|
|
789
984
|
store.close();
|
|
790
985
|
}
|
|
791
986
|
});
|
|
987
|
+
// 3. tb routing
|
|
988
|
+
const routing = program.command("routing").description("Manage buyer-side seller routing strategy");
|
|
989
|
+
routing
|
|
990
|
+
.command("show")
|
|
991
|
+
.description("Show the configured seller routing strategy")
|
|
992
|
+
.option("--json", "Output routing config as JSON")
|
|
993
|
+
.action(async (options) => {
|
|
994
|
+
const store = openBuyerStore();
|
|
995
|
+
try {
|
|
996
|
+
const record = store.getDaemonRuntimeConfig(ROUTING_CONFIG_KEY);
|
|
997
|
+
const config = record ? normalizeSellerRoutingConfig(record.config) : defaultSellerRoutingConfig();
|
|
998
|
+
if (options.json) {
|
|
999
|
+
console.log(JSON.stringify({ routing: config, updatedAt: record?.updatedAt }, null, 2));
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
printRoutingConfig(config, record?.updatedAt);
|
|
1003
|
+
}
|
|
1004
|
+
finally {
|
|
1005
|
+
store.close();
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
routing
|
|
1009
|
+
.command("set <mode>")
|
|
1010
|
+
.description("Set seller routing strategy: fullAuto, fixed, or fixedSet")
|
|
1011
|
+
.option("--seller <id>", "Seller id for fixed routing")
|
|
1012
|
+
.option("--seller-set <ids>", "Comma-separated seller ids for fixedSet routing")
|
|
1013
|
+
.option("--scorer <scorer>", "Ranking scorer: balanced, speed, or discount", "balanced")
|
|
1014
|
+
.action(async (mode, options) => {
|
|
1015
|
+
const config = buildRoutingConfigFromOptions(mode, options);
|
|
1016
|
+
assertSellerRoutingConfig(config);
|
|
1017
|
+
const store = openBuyerStore();
|
|
1018
|
+
try {
|
|
1019
|
+
store.saveDaemonRuntimeConfig(ROUTING_CONFIG_KEY, config);
|
|
1020
|
+
}
|
|
1021
|
+
finally {
|
|
1022
|
+
store.close();
|
|
1023
|
+
}
|
|
1024
|
+
printRoutingConfig(config);
|
|
1025
|
+
});
|
|
792
1026
|
// 3. tb models
|
|
793
1027
|
program
|
|
794
1028
|
.command("models")
|
|
@@ -874,7 +1108,7 @@ export function buildCli() {
|
|
|
874
1108
|
}
|
|
875
1109
|
if (selectedProviders.length > 0) {
|
|
876
1110
|
spinner.start("Fetching seller-backed model catalog...");
|
|
877
|
-
const proxyUrl = `http://127.0.0.1:${
|
|
1111
|
+
const proxyUrl = `http://127.0.0.1:${configuredProxyPort()}`;
|
|
878
1112
|
const registryUrl = sellerRegistryUrlForInit();
|
|
879
1113
|
let catalog;
|
|
880
1114
|
try {
|
|
@@ -900,11 +1134,11 @@ export function buildCli() {
|
|
|
900
1134
|
spinner.start("Configuring proxy routing in selected terminals...");
|
|
901
1135
|
const store = openBuyerStore();
|
|
902
1136
|
try {
|
|
1137
|
+
store.saveDaemonRuntimeConfig(ROUTING_CONFIG_KEY, sellerRouting);
|
|
903
1138
|
applyProviderInstall({
|
|
904
1139
|
providers: providerIds,
|
|
905
1140
|
proxyUrl,
|
|
906
1141
|
providerSelections,
|
|
907
|
-
sellerRouting,
|
|
908
1142
|
}, store);
|
|
909
1143
|
}
|
|
910
1144
|
finally {
|
|
@@ -1147,38 +1381,28 @@ export function buildCli() {
|
|
|
1147
1381
|
const plistDir = path.join(home, "Library", "LaunchAgents");
|
|
1148
1382
|
if (!fs.existsSync(plistDir))
|
|
1149
1383
|
fs.mkdirSync(plistDir, { recursive: true });
|
|
1150
|
-
const plistPath = path.join(plistDir,
|
|
1151
|
-
|
|
1152
|
-
const
|
|
1384
|
+
const plistPath = path.join(plistDir, `${TOKENBUDDY_LAUNCHD_LABEL}.plist`);
|
|
1385
|
+
const controlPort = configuredControlPort();
|
|
1386
|
+
const proxyPort = configuredProxyPort();
|
|
1387
|
+
const sellerRegistryUrl = sellerRegistryUrlForInit();
|
|
1388
|
+
const nodePath = process.execPath;
|
|
1153
1389
|
const scriptPath = tbProxydScriptPath();
|
|
1154
|
-
const plistContent =
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
<true/>
|
|
1169
|
-
<key>StandardOutPath</key>
|
|
1170
|
-
<string>${path.join(home, ".tokenbuddy-store", "tb-proxyd.stdout.log")}</string>
|
|
1171
|
-
<key>StandardErrorPath</key>
|
|
1172
|
-
<string>${path.join(home, ".tokenbuddy-store", "tb-proxyd.stderr.log")}</string>
|
|
1173
|
-
</dict>
|
|
1174
|
-
</plist>`;
|
|
1390
|
+
const plistContent = buildLaunchdPlistContent({
|
|
1391
|
+
label: TOKENBUDDY_LAUNCHD_LABEL,
|
|
1392
|
+
nodePath,
|
|
1393
|
+
scriptPath,
|
|
1394
|
+
stdoutPath: path.join(home, ".tokenbuddy-store", "tb-proxyd.stdout.log"),
|
|
1395
|
+
stderrPath: path.join(home, ".tokenbuddy-store", "tb-proxyd.stderr.log"),
|
|
1396
|
+
controlPort,
|
|
1397
|
+
proxyPort,
|
|
1398
|
+
sellerRegistryUrl,
|
|
1399
|
+
clawtipProofCommand: process.env.TB_PROXYD_CLAWTIP_PROOF_COMMAND,
|
|
1400
|
+
clawtipProofTimeoutMs: process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS
|
|
1401
|
+
? Number(process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS)
|
|
1402
|
+
: undefined,
|
|
1403
|
+
});
|
|
1175
1404
|
fs.writeFileSync(plistPath, plistContent, "utf8");
|
|
1176
|
-
|
|
1177
|
-
try {
|
|
1178
|
-
execSync(`launchctl unload ${plistPath}`, { stdio: "ignore" });
|
|
1179
|
-
}
|
|
1180
|
-
catch { }
|
|
1181
|
-
execSync(`launchctl load ${plistPath}`);
|
|
1405
|
+
installLaunchAgent(plistPath, TOKENBUDDY_LAUNCHD_LABEL);
|
|
1182
1406
|
spinner.stop("LaunchAgent daemon successfully registered and started! 🚀");
|
|
1183
1407
|
setupSummaryLines.push("Background tb-proxyd launchd service installed.");
|
|
1184
1408
|
}
|