@tokenbuddy/tokenbuddy 1.0.36 → 1.0.37
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 +6 -1
- package/dist/src/buyer-store.js +43 -4
- package/dist/src/cli.js +2 -2
- package/dist/src/daemon.d.ts +12 -0
- package/dist/src/daemon.js +791 -61
- package/dist/src/doctor-diagnostics.js +1 -6
- package/dist/src/provider-install.d.ts +2 -2
- package/dist/src/provider-install.js +248 -2
- package/dist/src/seller-catalog.d.ts +21 -0
- package/dist/src/seller-catalog.js +17 -0
- package/dist/src/seller-route-planner.d.ts +4 -1
- package/dist/src/seller-route-planner.js +3 -0
- package/dist/src/seller-routing-strategy.d.ts +3 -0
- package/dist/src/terminal-detect.d.ts +1 -1
- package/dist/src/terminal-detect.js +3 -2
- package/package.json +15 -2
- package/static/ui/assets/index-Djfl9tw5.js +271 -0
- package/static/ui/assets/index-DkfztCkn.css +1 -0
- package/static/ui/index.html +2 -2
- package/dist/src/buyer-store.d.ts.map +0 -1
- package/dist/src/buyer-store.js.map +0 -1
- package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
- package/dist/src/clawtip-bootstrap.js.map +0 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/credit-tracker.d.ts.map +0 -1
- package/dist/src/credit-tracker.js.map +0 -1
- package/dist/src/daemon.d.ts.map +0 -1
- package/dist/src/daemon.js.map +0 -1
- package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
- package/dist/src/doctor-clawtip-wallet.js.map +0 -1
- package/dist/src/doctor-diagnostics.d.ts.map +0 -1
- package/dist/src/doctor-diagnostics.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/init-clawtip-activation.d.ts.map +0 -1
- package/dist/src/init-clawtip-activation.js.map +0 -1
- package/dist/src/init-payment-options.d.ts.map +0 -1
- package/dist/src/init-payment-options.js.map +0 -1
- package/dist/src/init-setup.d.ts.map +0 -1
- package/dist/src/init-setup.js.map +0 -1
- package/dist/src/model-index.d.ts.map +0 -1
- package/dist/src/model-index.js.map +0 -1
- package/dist/src/package-update.d.ts.map +0 -1
- package/dist/src/package-update.js.map +0 -1
- package/dist/src/prewarm-cache.d.ts.map +0 -1
- package/dist/src/prewarm-cache.js.map +0 -1
- package/dist/src/prewarm-scheduler.d.ts.map +0 -1
- package/dist/src/prewarm-scheduler.js.map +0 -1
- package/dist/src/provider-install.d.ts.map +0 -1
- package/dist/src/provider-install.js.map +0 -1
- package/dist/src/provider-routing-config.d.ts.map +0 -1
- package/dist/src/provider-routing-config.js.map +0 -1
- package/dist/src/registry-trust.d.ts.map +0 -1
- package/dist/src/registry-trust.js.map +0 -1
- package/dist/src/route-failover.d.ts.map +0 -1
- package/dist/src/route-failover.js.map +0 -1
- package/dist/src/seller-catalog.d.ts.map +0 -1
- package/dist/src/seller-catalog.js.map +0 -1
- package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
- package/dist/src/seller-concurrency-limiter.js.map +0 -1
- package/dist/src/seller-metadata-cache.d.ts.map +0 -1
- package/dist/src/seller-metadata-cache.js.map +0 -1
- package/dist/src/seller-pool.d.ts.map +0 -1
- package/dist/src/seller-pool.js.map +0 -1
- package/dist/src/seller-route-planner.d.ts.map +0 -1
- package/dist/src/seller-route-planner.js.map +0 -1
- package/dist/src/seller-routing-config.d.ts.map +0 -1
- package/dist/src/seller-routing-config.js.map +0 -1
- package/dist/src/seller-routing-strategy.d.ts.map +0 -1
- package/dist/src/seller-routing-strategy.js.map +0 -1
- package/dist/src/stream-failover.d.ts.map +0 -1
- package/dist/src/stream-failover.js.map +0 -1
- package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
- package/dist/src/tb-clawtip-proof.js.map +0 -1
- package/dist/src/tb-proxyd.d.ts.map +0 -1
- package/dist/src/tb-proxyd.js.map +0 -1
- package/dist/src/terminal-detect.d.ts.map +0 -1
- package/dist/src/terminal-detect.js.map +0 -1
- package/dist/src/terminal-image.d.ts.map +0 -1
- package/dist/src/terminal-image.js.map +0 -1
- package/src/buyer-store.ts +0 -1090
- package/src/clawtip-bootstrap.ts +0 -65
- package/src/cli.ts +0 -2243
- package/src/credit-tracker.ts +0 -295
- package/src/daemon.ts +0 -5475
- package/src/doctor-clawtip-wallet.ts +0 -95
- package/src/doctor-diagnostics.ts +0 -1026
- package/src/index.ts +0 -16
- package/src/init-clawtip-activation.ts +0 -695
- package/src/init-payment-options.ts +0 -373
- package/src/init-setup.ts +0 -165
- package/src/model-index.ts +0 -278
- package/src/package-update.ts +0 -311
- package/src/prewarm-cache.ts +0 -485
- package/src/prewarm-scheduler.ts +0 -675
- package/src/provider-install.ts +0 -1006
- package/src/provider-routing-config.ts +0 -410
- package/src/registry-trust.ts +0 -51
- package/src/route-failover.ts +0 -304
- package/src/seller-catalog.ts +0 -505
- package/src/seller-concurrency-limiter.ts +0 -161
- package/src/seller-metadata-cache.ts +0 -91
- package/src/seller-pool.ts +0 -557
- package/src/seller-route-planner.ts +0 -513
- package/src/seller-routing-config.ts +0 -211
- package/src/seller-routing-strategy.ts +0 -362
- package/src/stream-failover.ts +0 -152
- package/src/tb-clawtip-proof.ts +0 -28
- package/src/tb-proxyd.ts +0 -101
- package/src/terminal-detect.ts +0 -333
- package/src/terminal-image.ts +0 -228
- package/static/ui/assets/index-0MVXD7bH.css +0 -1
- package/static/ui/assets/index-BVbeDEwq.js +0 -271
- package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
- package/tests/cli-routing.test.ts +0 -363
- package/tests/control-plane-ui-endpoints.test.ts +0 -1630
- package/tests/credit-tracker.test.ts +0 -165
- package/tests/daemon-413-fallback.test.ts +0 -92
- package/tests/daemon-classify.test.ts +0 -452
- package/tests/daemon-roles.test.ts +0 -92
- package/tests/daemon-trusted-registry-cache.test.ts +0 -132
- package/tests/e2e.test.ts +0 -366
- package/tests/image-generation-e2e.test.ts +0 -230
- package/tests/model-index.test.ts +0 -198
- package/tests/package-update.test.ts +0 -147
- package/tests/prewarm-cache.test.ts +0 -296
- package/tests/prewarm-scheduler.test.ts +0 -367
- package/tests/provider-routing-config.test.ts +0 -150
- package/tests/registry-trust.test.ts +0 -28
- package/tests/route-failover.test.ts +0 -222
- package/tests/seller-catalog-413.test.ts +0 -120
- package/tests/seller-catalog-utilities.test.ts +0 -124
- package/tests/seller-concurrency-limiter.test.ts +0 -83
- package/tests/seller-metadata-cache.test.ts +0 -89
- package/tests/seller-pool.test.ts +0 -365
- package/tests/seller-route-planner.test.ts +0 -312
- package/tests/seller-routing-config.test.ts +0 -124
- package/tests/seller-routing-strategy.test.ts +0 -167
- package/tests/stream-failover.test.ts +0 -52
- package/tests/thousand-seller.test.ts +0 -151
- package/tests/tokenbuddy.test.ts +0 -4043
- package/tsconfig.json +0 -8
|
@@ -133,12 +133,7 @@ function discountRatioFromSeller(seller) {
|
|
|
133
133
|
}
|
|
134
134
|
function formatDiscountRatio(value) {
|
|
135
135
|
const ratio = Math.max(0, value);
|
|
136
|
-
|
|
137
|
-
return "免费";
|
|
138
|
-
if (Math.abs(ratio - 1) < 0.0001)
|
|
139
|
-
return "原价";
|
|
140
|
-
const folded = Math.round(ratio * 100) / 10;
|
|
141
|
-
return `${Number.isInteger(folded) ? String(folded) : folded.toFixed(1)}折`;
|
|
136
|
+
return String(ratio);
|
|
142
137
|
}
|
|
143
138
|
function formatUsdPer1m(microsPer1m) {
|
|
144
139
|
const usd = microsPer1m / 1_000_000;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BuyerStore } from "./buyer-store.js";
|
|
2
2
|
import { ProtocolPreference } from "./seller-catalog.js";
|
|
3
|
-
export declare const PROXY_ACCESS_TOKEN_PLACEHOLDER = "
|
|
3
|
+
export declare const PROXY_ACCESS_TOKEN_PLACEHOLDER = "tbp_local_17821_d7f4c9a2b8e1";
|
|
4
4
|
export declare const SUPPORTED_PROVIDER_IDS: readonly ["codex", "claude-code", "claude-desktop", "openclaw", "opencode", "hermes"];
|
|
5
5
|
export type ProviderId = typeof SUPPORTED_PROVIDER_IDS[number];
|
|
6
6
|
export type ModelSelectionKind = "single-model" | "claude-role-mapping";
|
|
@@ -111,7 +111,7 @@ export interface ProviderApplyResult {
|
|
|
111
111
|
export interface ProviderRollbackResult {
|
|
112
112
|
providerId: ProviderId;
|
|
113
113
|
path: string;
|
|
114
|
-
action: "restored" | "removed" | "missing_snapshot";
|
|
114
|
+
action: "restored" | "removed" | "cleaned" | "missing_snapshot";
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
117
|
* 探测所有 SUPPORTED_PROVIDER_IDS:检查可执行文件、配置文件、原生 hints 目录。
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
export const PROXY_ACCESS_TOKEN_PLACEHOLDER = "
|
|
4
|
+
export const PROXY_ACCESS_TOKEN_PLACEHOLDER = "tbp_local_17821_d7f4c9a2b8e1";
|
|
5
5
|
const DESKTOP_PROFILE_ID = "00000000-0000-4000-8000-000000178210";
|
|
6
6
|
const CLAUDE_ONE_M_MARKER = "[1M]";
|
|
7
7
|
const CLAUDE_CLIENT_HAIKU_MODEL = "claude-haiku-4-5";
|
|
8
8
|
const CLAUDE_CLIENT_SONNET_MODEL = "claude-sonnet-4-6";
|
|
9
9
|
const CLAUDE_CLIENT_OPUS_MODEL = "claude-opus-4-7";
|
|
10
|
+
const CLAUDE_CODE_TOKENBUDDY_ENV_KEYS = [
|
|
11
|
+
"ANTHROPIC_BASE_URL",
|
|
12
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
13
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
14
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME",
|
|
15
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
16
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
|
|
17
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
18
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
|
|
19
|
+
"ANTHROPIC_MODEL",
|
|
20
|
+
];
|
|
10
21
|
export const SUPPORTED_PROVIDER_IDS = [
|
|
11
22
|
"codex",
|
|
12
23
|
"claude-code",
|
|
@@ -174,6 +185,27 @@ function replaceTopLevelYamlSection(existing, sectionName, sectionBody) {
|
|
|
174
185
|
...lines.slice(sectionEnd),
|
|
175
186
|
].join("\n").replace(/\n*$/, "")}\n`;
|
|
176
187
|
}
|
|
188
|
+
function removeTopLevelYamlSection(existing, sectionName) {
|
|
189
|
+
const lines = existing.split(/\r?\n/);
|
|
190
|
+
const sectionStart = lines.findIndex((line) => {
|
|
191
|
+
return line === `${sectionName}:` || line.startsWith(`${sectionName}: `);
|
|
192
|
+
});
|
|
193
|
+
if (sectionStart < 0) {
|
|
194
|
+
return existing;
|
|
195
|
+
}
|
|
196
|
+
let sectionEnd = sectionStart + 1;
|
|
197
|
+
while (sectionEnd < lines.length) {
|
|
198
|
+
const line = lines[sectionEnd];
|
|
199
|
+
if (line.trim() && !line.startsWith(" ") && !line.startsWith("\t")) {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
sectionEnd += 1;
|
|
203
|
+
}
|
|
204
|
+
return `${[
|
|
205
|
+
...lines.slice(0, sectionStart),
|
|
206
|
+
...lines.slice(sectionEnd),
|
|
207
|
+
].join("\n").replace(/\n*$/, "")}\n`;
|
|
208
|
+
}
|
|
177
209
|
function readObjectField(value, key) {
|
|
178
210
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
179
211
|
return undefined;
|
|
@@ -183,6 +215,13 @@ function readObjectField(value, key) {
|
|
|
183
215
|
? field
|
|
184
216
|
: undefined;
|
|
185
217
|
}
|
|
218
|
+
function removeObjectKey(value, key) {
|
|
219
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
delete value[key];
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
186
225
|
function jsonContent(value) {
|
|
187
226
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
188
227
|
}
|
|
@@ -368,6 +407,45 @@ function claudeCodeConfig(home, proxyUrl, config) {
|
|
|
368
407
|
makeChange("claude-code", configPath, "configure Anthropic proxy env for Claude Code", jsonContent(current)),
|
|
369
408
|
];
|
|
370
409
|
}
|
|
410
|
+
function isClaudeCodeTokenBuddyConfigured(filePath) {
|
|
411
|
+
const env = readObjectField(readJsonObject(filePath), "env");
|
|
412
|
+
if (!env) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
return env.ANTHROPIC_AUTH_TOKEN === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
|
|
416
|
+
typeof env.ANTHROPIC_BASE_URL === "string" &&
|
|
417
|
+
env.ANTHROPIC_BASE_URL.trim().length > 0;
|
|
418
|
+
}
|
|
419
|
+
function cleanupClaudeCodeConfig(home) {
|
|
420
|
+
const configPath = path.join(home, ".claude", "settings.json");
|
|
421
|
+
if (!fs.existsSync(configPath) || !isClaudeCodeTokenBuddyConfigured(configPath)) {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
const current = readJsonObject(configPath);
|
|
425
|
+
const env = readObjectField(current, "env");
|
|
426
|
+
if (!env) {
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
let changed = false;
|
|
430
|
+
for (const key of CLAUDE_CODE_TOKENBUDDY_ENV_KEYS) {
|
|
431
|
+
changed = removeObjectKey(env, key) || changed;
|
|
432
|
+
}
|
|
433
|
+
if (!changed) {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
if (Object.keys(env).length > 0) {
|
|
437
|
+
current.env = env;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
delete current.env;
|
|
441
|
+
}
|
|
442
|
+
if (Object.keys(current).length === 0) {
|
|
443
|
+
fs.rmSync(configPath, { force: true });
|
|
444
|
+
return [{ providerId: "claude-code", path: configPath, action: "removed" }];
|
|
445
|
+
}
|
|
446
|
+
fs.writeFileSync(configPath, jsonContent(current), "utf8");
|
|
447
|
+
return [{ providerId: "claude-code", path: configPath, action: "cleaned" }];
|
|
448
|
+
}
|
|
371
449
|
function claudeDesktopConfig(home, proxyUrl, config) {
|
|
372
450
|
const model = pickConfiguredModel(config);
|
|
373
451
|
const configDir = path.join(home, "Library", "Application Support", "Claude");
|
|
@@ -403,6 +481,85 @@ function claudeDesktopConfig(home, proxyUrl, config) {
|
|
|
403
481
|
makeChange("claude-desktop", metaPath, "select TokenBuddy Claude Desktop profile", jsonContent(meta)),
|
|
404
482
|
];
|
|
405
483
|
}
|
|
484
|
+
function claudeDesktopPaths(home) {
|
|
485
|
+
const configDir = path.join(home, "Library", "Application Support", "Claude");
|
|
486
|
+
const threepDir = path.join(home, "Library", "Application Support", "Claude-3p");
|
|
487
|
+
const libraryPath = path.join(threepDir, "configLibrary");
|
|
488
|
+
return {
|
|
489
|
+
configPath: path.join(configDir, "claude_desktop_config.json"),
|
|
490
|
+
threepConfigPath: path.join(threepDir, "claude_desktop_config.json"),
|
|
491
|
+
profilePath: path.join(libraryPath, `${DESKTOP_PROFILE_ID}.json`),
|
|
492
|
+
metaPath: path.join(libraryPath, "_meta.json"),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function isClaudeDesktopProfileTokenBuddyConfigured(profilePath) {
|
|
496
|
+
const profile = readJsonObject(profilePath);
|
|
497
|
+
return profile.inferenceGatewayApiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
|
|
498
|
+
profile.inferenceProvider === "gateway" &&
|
|
499
|
+
typeof profile.inferenceGatewayBaseUrl === "string" &&
|
|
500
|
+
profile.inferenceGatewayBaseUrl.trim().length > 0;
|
|
501
|
+
}
|
|
502
|
+
function isClaudeDesktopTokenBuddyConfigured(_filePath, home) {
|
|
503
|
+
const paths = claudeDesktopPaths(home);
|
|
504
|
+
const meta = readJsonObject(paths.metaPath);
|
|
505
|
+
return meta.appliedId === DESKTOP_PROFILE_ID &&
|
|
506
|
+
isClaudeDesktopProfileTokenBuddyConfigured(paths.profilePath);
|
|
507
|
+
}
|
|
508
|
+
function cleanupClaudeDesktopConfig(home) {
|
|
509
|
+
const paths = claudeDesktopPaths(home);
|
|
510
|
+
if (!isClaudeDesktopTokenBuddyConfigured(paths.configPath, home)) {
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
const results = [];
|
|
514
|
+
const primary = readJsonObject(paths.configPath);
|
|
515
|
+
if (primary.deploymentMode === "3p") {
|
|
516
|
+
delete primary.deploymentMode;
|
|
517
|
+
if (Object.keys(primary).length > 0) {
|
|
518
|
+
fs.writeFileSync(paths.configPath, jsonContent(primary), "utf8");
|
|
519
|
+
results.push({ providerId: "claude-desktop", path: paths.configPath, action: "cleaned" });
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
fs.rmSync(paths.configPath, { force: true });
|
|
523
|
+
results.push({ providerId: "claude-desktop", path: paths.configPath, action: "removed" });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const threep = readJsonObject(paths.threepConfigPath);
|
|
527
|
+
if (threep.deploymentMode === "3p") {
|
|
528
|
+
delete threep.deploymentMode;
|
|
529
|
+
if (Object.keys(threep).length > 0) {
|
|
530
|
+
fs.writeFileSync(paths.threepConfigPath, jsonContent(threep), "utf8");
|
|
531
|
+
results.push({ providerId: "claude-desktop", path: paths.threepConfigPath, action: "cleaned" });
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
fs.rmSync(paths.threepConfigPath, { force: true });
|
|
535
|
+
results.push({ providerId: "claude-desktop", path: paths.threepConfigPath, action: "removed" });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (fs.existsSync(paths.profilePath)) {
|
|
539
|
+
fs.rmSync(paths.profilePath, { force: true });
|
|
540
|
+
results.push({ providerId: "claude-desktop", path: paths.profilePath, action: "removed" });
|
|
541
|
+
}
|
|
542
|
+
const meta = readJsonObject(paths.metaPath);
|
|
543
|
+
let changedMeta = false;
|
|
544
|
+
if (meta.appliedId === DESKTOP_PROFILE_ID) {
|
|
545
|
+
delete meta.appliedId;
|
|
546
|
+
changedMeta = true;
|
|
547
|
+
}
|
|
548
|
+
if (Array.isArray(meta.entries)) {
|
|
549
|
+
const nextEntries = meta.entries.filter((entry) => {
|
|
550
|
+
return !(isPlainRecord(entry) && entry.id === DESKTOP_PROFILE_ID);
|
|
551
|
+
});
|
|
552
|
+
if (nextEntries.length !== meta.entries.length) {
|
|
553
|
+
meta.entries = nextEntries;
|
|
554
|
+
changedMeta = true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (changedMeta) {
|
|
558
|
+
fs.writeFileSync(paths.metaPath, jsonContent(meta), "utf8");
|
|
559
|
+
results.push({ providerId: "claude-desktop", path: paths.metaPath, action: "cleaned" });
|
|
560
|
+
}
|
|
561
|
+
return results;
|
|
562
|
+
}
|
|
406
563
|
function openclawConfig(home, proxyUrl, config) {
|
|
407
564
|
const model = pickConfiguredModel(config);
|
|
408
565
|
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
|
@@ -454,6 +611,33 @@ function isOpenclawTokenBuddyConfigured(filePath) {
|
|
|
454
611
|
typeof defaultModel === "string" &&
|
|
455
612
|
defaultModel.startsWith("tokenbuddy/");
|
|
456
613
|
}
|
|
614
|
+
function cleanupOpenclawConfig(home) {
|
|
615
|
+
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
|
616
|
+
if (!fs.existsSync(configPath) || !isOpenclawTokenBuddyConfigured(configPath)) {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
const current = readJsonObject(configPath);
|
|
620
|
+
const models = readObjectField(current, "models");
|
|
621
|
+
const providers = readObjectField(models, "providers");
|
|
622
|
+
const agents = readObjectField(current, "agents");
|
|
623
|
+
const defaults = readObjectField(agents, "defaults");
|
|
624
|
+
let changed = false;
|
|
625
|
+
if (providers) {
|
|
626
|
+
changed = removeObjectKey(providers, "tokenbuddy") || changed;
|
|
627
|
+
if (models && Object.keys(providers).length === 0) {
|
|
628
|
+
delete models.providers;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (defaults && typeof defaults.model === "string" && defaults.model.startsWith("tokenbuddy/")) {
|
|
632
|
+
delete defaults.model;
|
|
633
|
+
changed = true;
|
|
634
|
+
}
|
|
635
|
+
if (!changed) {
|
|
636
|
+
return [];
|
|
637
|
+
}
|
|
638
|
+
fs.writeFileSync(configPath, jsonContent(current), "utf8");
|
|
639
|
+
return [{ providerId: "openclaw", path: configPath, action: "cleaned" }];
|
|
640
|
+
}
|
|
457
641
|
function openAiBaseUrl(proxyUrl) {
|
|
458
642
|
const normalized = proxyUrl.replace(/\/+$/, "");
|
|
459
643
|
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
@@ -503,6 +687,38 @@ function isOpencodeTokenBuddyConfigured(filePath) {
|
|
|
503
687
|
typeof current.small_model === "string" &&
|
|
504
688
|
current.small_model.startsWith("tokenbuddy/");
|
|
505
689
|
}
|
|
690
|
+
function cleanupOpencodeConfig(home) {
|
|
691
|
+
const configPath = path.join(home, ".config", "opencode", "opencode.json");
|
|
692
|
+
if (!fs.existsSync(configPath) || !isOpencodeTokenBuddyConfigured(configPath)) {
|
|
693
|
+
return [];
|
|
694
|
+
}
|
|
695
|
+
const current = readJsonObject(configPath);
|
|
696
|
+
const providers = readObjectField(current, "provider");
|
|
697
|
+
let changed = false;
|
|
698
|
+
if (providers) {
|
|
699
|
+
changed = removeObjectKey(providers, "tokenbuddy") || changed;
|
|
700
|
+
if (Object.keys(providers).length === 0) {
|
|
701
|
+
delete current.provider;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (typeof current.model === "string" && current.model.startsWith("tokenbuddy/")) {
|
|
705
|
+
delete current.model;
|
|
706
|
+
changed = true;
|
|
707
|
+
}
|
|
708
|
+
if (typeof current.small_model === "string" && current.small_model.startsWith("tokenbuddy/")) {
|
|
709
|
+
delete current.small_model;
|
|
710
|
+
changed = true;
|
|
711
|
+
}
|
|
712
|
+
if (!changed) {
|
|
713
|
+
return [];
|
|
714
|
+
}
|
|
715
|
+
if (Object.keys(current).length === 0) {
|
|
716
|
+
fs.rmSync(configPath, { force: true });
|
|
717
|
+
return [{ providerId: "opencode", path: configPath, action: "removed" }];
|
|
718
|
+
}
|
|
719
|
+
fs.writeFileSync(configPath, jsonContent(current), "utf8");
|
|
720
|
+
return [{ providerId: "opencode", path: configPath, action: "cleaned" }];
|
|
721
|
+
}
|
|
506
722
|
function hermesConfig(home, proxyUrl, config) {
|
|
507
723
|
const model = pickConfiguredModel(config);
|
|
508
724
|
const configPath = path.join(home, ".hermes", "config.yaml");
|
|
@@ -535,6 +751,20 @@ function isHermesTokenBuddyConfigured(filePath) {
|
|
|
535
751
|
typeof modelConfig.default === "string" &&
|
|
536
752
|
modelConfig.default.length > 0;
|
|
537
753
|
}
|
|
754
|
+
function cleanupHermesConfig(home) {
|
|
755
|
+
const configPath = path.join(home, ".hermes", "config.yaml");
|
|
756
|
+
if (!fs.existsSync(configPath) || !isHermesTokenBuddyConfigured(configPath)) {
|
|
757
|
+
return [];
|
|
758
|
+
}
|
|
759
|
+
const existing = readText(configPath) || "";
|
|
760
|
+
const next = removeTopLevelYamlSection(existing, "model");
|
|
761
|
+
if (next.trim()) {
|
|
762
|
+
fs.writeFileSync(configPath, next, "utf8");
|
|
763
|
+
return [{ providerId: "hermes", path: configPath, action: "cleaned" }];
|
|
764
|
+
}
|
|
765
|
+
fs.rmSync(configPath, { force: true });
|
|
766
|
+
return [{ providerId: "hermes", path: configPath, action: "removed" }];
|
|
767
|
+
}
|
|
538
768
|
const PROVIDERS = [
|
|
539
769
|
{
|
|
540
770
|
id: "codex",
|
|
@@ -550,7 +780,9 @@ const PROVIDERS = [
|
|
|
550
780
|
name: "Claude Code CLI",
|
|
551
781
|
commandName: "claude",
|
|
552
782
|
configPath: (home) => path.join(home, ".claude", "settings.json"),
|
|
783
|
+
isConfigured: isClaudeCodeTokenBuddyConfigured,
|
|
553
784
|
changes: claudeCodeConfig,
|
|
785
|
+
cleanup: cleanupClaudeCodeConfig,
|
|
554
786
|
modelSelectionKind: "claude-role-mapping",
|
|
555
787
|
protocolPreference: "messages",
|
|
556
788
|
},
|
|
@@ -558,7 +790,9 @@ const PROVIDERS = [
|
|
|
558
790
|
id: "claude-desktop",
|
|
559
791
|
name: "Claude Desktop App",
|
|
560
792
|
configPath: (home) => path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
793
|
+
isConfigured: isClaudeDesktopTokenBuddyConfigured,
|
|
561
794
|
changes: claudeDesktopConfig,
|
|
795
|
+
cleanup: cleanupClaudeDesktopConfig,
|
|
562
796
|
modelSelectionKind: "single-model",
|
|
563
797
|
protocolPreference: "messages",
|
|
564
798
|
},
|
|
@@ -573,6 +807,7 @@ const PROVIDERS = [
|
|
|
573
807
|
path.join(home, ".openclaw", "config.json"),
|
|
574
808
|
],
|
|
575
809
|
changes: openclawConfig,
|
|
810
|
+
cleanup: cleanupOpenclawConfig,
|
|
576
811
|
modelSelectionKind: "single-model",
|
|
577
812
|
protocolPreference: "chat_completions",
|
|
578
813
|
},
|
|
@@ -583,6 +818,7 @@ const PROVIDERS = [
|
|
|
583
818
|
configPath: (home) => path.join(home, ".config", "opencode", "opencode.json"),
|
|
584
819
|
isConfigured: isOpencodeTokenBuddyConfigured,
|
|
585
820
|
changes: opencodeConfig,
|
|
821
|
+
cleanup: cleanupOpencodeConfig,
|
|
586
822
|
modelSelectionKind: "single-model",
|
|
587
823
|
protocolPreference: "chat_completions",
|
|
588
824
|
},
|
|
@@ -597,6 +833,7 @@ const PROVIDERS = [
|
|
|
597
833
|
path.join(home, ".hermes", "auth.json"),
|
|
598
834
|
],
|
|
599
835
|
changes: hermesConfig,
|
|
836
|
+
cleanup: cleanupHermesConfig,
|
|
600
837
|
modelSelectionKind: "single-model",
|
|
601
838
|
protocolPreference: "chat_completions",
|
|
602
839
|
},
|
|
@@ -752,12 +989,21 @@ export function applyProviderInstall(options, store) {
|
|
|
752
989
|
* @returns 回滚结果列表
|
|
753
990
|
*/
|
|
754
991
|
export function rollbackProviderInstall(options, store) {
|
|
992
|
+
const home = resolveHome(options.home);
|
|
755
993
|
const providerIds = assertProviderIds(options.providers);
|
|
756
994
|
const results = [];
|
|
757
995
|
for (const providerId of providerIds) {
|
|
996
|
+
const provider = getProviderDefinition(providerId);
|
|
758
997
|
const snapshot = store.getProviderInstallSnapshot(providerId);
|
|
759
998
|
if (!snapshot) {
|
|
760
|
-
|
|
999
|
+
const cleanupResults = provider.cleanup?.(home) ?? [];
|
|
1000
|
+
if (cleanupResults.length > 0) {
|
|
1001
|
+
results.push(...cleanupResults);
|
|
1002
|
+
}
|
|
1003
|
+
else {
|
|
1004
|
+
results.push({ providerId, path: "", action: "missing_snapshot" });
|
|
1005
|
+
}
|
|
1006
|
+
store.removeProviderRuntimeConfig(providerId);
|
|
761
1007
|
continue;
|
|
762
1008
|
}
|
|
763
1009
|
for (const file of snapshot.files) {
|
|
@@ -94,6 +94,22 @@ export interface SellerManifest {
|
|
|
94
94
|
/** 服务手续费系数(snake_case 兼容) */
|
|
95
95
|
service_fee_ratio?: number;
|
|
96
96
|
};
|
|
97
|
+
/** 上游能力探测快照(camelCase) */
|
|
98
|
+
upstreamCapabilities?: {
|
|
99
|
+
checkedAt?: string;
|
|
100
|
+
};
|
|
101
|
+
/** 上游能力探测快照(snake_case 兼容) */
|
|
102
|
+
upstream_capabilities?: {
|
|
103
|
+
checked_at?: string;
|
|
104
|
+
};
|
|
105
|
+
/** 上游模型目录刷新元数据(camelCase) */
|
|
106
|
+
upstreamMetadata?: {
|
|
107
|
+
lastCheckedAt?: string;
|
|
108
|
+
};
|
|
109
|
+
/** 上游模型目录刷新元数据(snake_case 兼容) */
|
|
110
|
+
upstream_metadata?: {
|
|
111
|
+
last_checked_at?: string;
|
|
112
|
+
};
|
|
97
113
|
}
|
|
98
114
|
/**
|
|
99
115
|
* `/manifest` 响应里单个模型记录(兼容 snake_case)。
|
|
@@ -133,6 +149,7 @@ export interface ModelCatalogEntry {
|
|
|
133
149
|
/** 输出价格 USD micros/1M */
|
|
134
150
|
outputPriceMicrosPer1m?: number;
|
|
135
151
|
}
|
|
152
|
+
export type RouteState = "ok" | "degraded" | "error" | "cooldown" | "full" | "unknown";
|
|
136
153
|
/**
|
|
137
154
|
* seller 目录条目(聚合 seller 元信息 + manifest 拉取结果)。
|
|
138
155
|
* 用于 `tb doctor` 和 CLI 表格展示。
|
|
@@ -146,6 +163,8 @@ export interface SellerCatalogEntry {
|
|
|
146
163
|
url: string;
|
|
147
164
|
/** 当前状态(`active` / `error` / `manifest_unavailable`) */
|
|
148
165
|
status: string;
|
|
166
|
+
/** 控制面统一路由状态,供 UI 直接展示和排序 */
|
|
167
|
+
routeState?: RouteState;
|
|
149
168
|
/** manifest 报告的 sellerId(可能与 id 不同,跨 namespace 时有用) */
|
|
150
169
|
manifestSellerId?: string;
|
|
151
170
|
/** 折扣系数(来自 manifest.selection) */
|
|
@@ -156,6 +175,8 @@ export interface SellerCatalogEntry {
|
|
|
156
175
|
ttftMs?: number;
|
|
157
176
|
/** 最近 10 分钟窗口内的平均输出吞吐(tokens/s),来自本地 seller pool 运行时指标 */
|
|
158
177
|
avgTokensPerSecond?: number;
|
|
178
|
+
/** seller 最近一次上游模型/能力巡检时间,来自 manifest 真实字段 */
|
|
179
|
+
lastModelInspectionAt?: string;
|
|
159
180
|
/** 模型数(来自 manifest) */
|
|
160
181
|
modelCount?: number;
|
|
161
182
|
/** seller 支持的协议(manifest > registry fallback) */
|
|
@@ -68,6 +68,20 @@ function manifestModels(manifest) {
|
|
|
68
68
|
return (manifest.models || [])
|
|
69
69
|
.filter((model) => Boolean(model?.id && typeof model.id === "string"));
|
|
70
70
|
}
|
|
71
|
+
function manifestLastModelInspectionAt(manifest) {
|
|
72
|
+
const value = manifest.upstreamCapabilities?.checkedAt
|
|
73
|
+
?? manifest.upstream_capabilities?.checked_at
|
|
74
|
+
?? manifest.upstreamMetadata?.lastCheckedAt
|
|
75
|
+
?? manifest.upstream_metadata?.last_checked_at;
|
|
76
|
+
if (typeof value !== "string") {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const trimmed = value.trim();
|
|
80
|
+
if (!trimmed) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return Number.isNaN(Date.parse(trimmed)) ? undefined : trimmed;
|
|
84
|
+
}
|
|
71
85
|
/**
|
|
72
86
|
* v1.2 §18.9:bootstrap 的 `/registry/sellers` 返回 413 + `X-TokenBuddy-Registry-Too-Large: 1` 时抛出的错误。
|
|
73
87
|
* daemon 在 `TokenbuddyDaemon.fetchRegistry` 捕获并回退到上次成功快照,保证 buyer 仍可路由。
|
|
@@ -206,9 +220,11 @@ export async function discoverSellerBackedModels(registryUrl) {
|
|
|
206
220
|
name: seller.name,
|
|
207
221
|
url: seller.url,
|
|
208
222
|
status: "ok",
|
|
223
|
+
routeState: "ok",
|
|
209
224
|
manifestSellerId: manifest.sellerId || manifest.seller_id || seller.id,
|
|
210
225
|
discountRatio: manifest.selection?.discountRatio ?? manifest.selection?.discount_ratio,
|
|
211
226
|
serviceFeeRatio: manifest.selection?.serviceFeeRatio ?? manifest.selection?.service_fee_ratio,
|
|
227
|
+
lastModelInspectionAt: manifestLastModelInspectionAt(manifest),
|
|
212
228
|
modelCount: models.length,
|
|
213
229
|
supportedProtocols: protocols,
|
|
214
230
|
paymentMethods,
|
|
@@ -228,6 +244,7 @@ export async function discoverSellerBackedModels(registryUrl) {
|
|
|
228
244
|
name: seller.name,
|
|
229
245
|
url: seller.url,
|
|
230
246
|
status: "failed",
|
|
247
|
+
routeState: "error",
|
|
231
248
|
errorMessage
|
|
232
249
|
},
|
|
233
250
|
models: []
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RegistrySeller } from "./seller-catalog.js";
|
|
1
|
+
import { type RegistrySeller, type RouteState } from "./seller-catalog.js";
|
|
2
2
|
import { type SellerRoutingPlan, type SellerRoutingStrategyConfig } from "./seller-routing-strategy.js";
|
|
3
3
|
/**
|
|
4
4
|
* `planSellerRouteSet` 候选来源:走了 prewarm cache 还是回退到 registry 顺序。
|
|
@@ -29,6 +29,8 @@ export interface SellerRouteMetric {
|
|
|
29
29
|
circuit?: SellerCircuitState;
|
|
30
30
|
/** 临时容量避让截止时间;大于当前时间时直接剔除候选 */
|
|
31
31
|
capacityBlockedUntil?: number;
|
|
32
|
+
/** 控制面统一路由状态,供 UI 解释候选状态 */
|
|
33
|
+
routeState?: RouteState;
|
|
32
34
|
/** 当前 `tb-proxyd` 进程内该 seller 的活跃 lease 数。 */
|
|
33
35
|
localConcurrencyActive?: number;
|
|
34
36
|
/** 当前 `tb-proxyd` 进程内该 seller 的最大活跃 lease 数。 */
|
|
@@ -102,6 +104,7 @@ export interface PlannedSellerRoute {
|
|
|
102
104
|
avgInferenceMs?: number;
|
|
103
105
|
avgTokensPerSecond?: number;
|
|
104
106
|
discountRatio?: number;
|
|
107
|
+
routeState?: RouteState;
|
|
105
108
|
/** 在 registry 里的声明顺序(0-based,tie-breaker) */
|
|
106
109
|
registryOrder: number;
|
|
107
110
|
};
|
|
@@ -32,6 +32,7 @@ export function planSellerRouteSet(input) {
|
|
|
32
32
|
avgInferenceMs: candidate.avgInferenceMs,
|
|
33
33
|
avgTokensPerSecond: candidate.avgTokensPerSecond,
|
|
34
34
|
discountRatio: candidate.discountRatio,
|
|
35
|
+
routeState: candidate.routeState,
|
|
35
36
|
registryOrder: candidate.registryOrder
|
|
36
37
|
}
|
|
37
38
|
};
|
|
@@ -137,6 +138,7 @@ function buildCandidate(input) {
|
|
|
137
138
|
avgInferenceMs: input.metric?.avgInferenceMs,
|
|
138
139
|
avgTokensPerSecond: input.metric?.avgTokensPerSecond,
|
|
139
140
|
discountRatio: input.metric?.discountRatio,
|
|
141
|
+
routeState: input.metric?.routeState,
|
|
140
142
|
registryOrder: input.registryOrder
|
|
141
143
|
};
|
|
142
144
|
}
|
|
@@ -228,6 +230,7 @@ function mergeMetric(metric, prewarm) {
|
|
|
228
230
|
avgInferenceMs: metric?.avgInferenceMs ?? prewarm.avgInferenceMs,
|
|
229
231
|
avgTokensPerSecond: metric?.avgTokensPerSecond ?? prewarm.avgTokensPerSecond,
|
|
230
232
|
discountRatio: metric?.discountRatio,
|
|
233
|
+
routeState: metric?.routeState,
|
|
231
234
|
circuit: metric?.circuit,
|
|
232
235
|
capacityBlockedUntil: metric?.capacityBlockedUntil,
|
|
233
236
|
localConcurrencyActive: metric?.localConcurrencyActive,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RouteState } from "./seller-catalog.js";
|
|
1
2
|
/**
|
|
2
3
|
* seller 路由模式:
|
|
3
4
|
* - `fixed`:强制使用单个 seller(`sellerId`)
|
|
@@ -57,6 +58,8 @@ export interface RoutingCandidate {
|
|
|
57
58
|
avgTokensPerSecond?: number;
|
|
58
59
|
/** 折扣系数 0-1,可选;缺省视为"无折扣信息" */
|
|
59
60
|
discountRatio?: number;
|
|
61
|
+
/** 控制面统一路由状态,供 preview 透传给 UI */
|
|
62
|
+
routeState?: RouteState;
|
|
60
63
|
/** 上游状态,可选 */
|
|
61
64
|
upstreamStatus?: "healthy" | "degraded" | "unhealthy" | "unknown";
|
|
62
65
|
/** 上游错误类名,可选 */
|
|
@@ -34,7 +34,7 @@ export declare function detectTerminals(): TerminalCandidate[];
|
|
|
34
34
|
/**
|
|
35
35
|
* Safely rewrite Claude Code settings to route requests through our proxy.
|
|
36
36
|
*
|
|
37
|
-
* 写入 `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN`(占位 `
|
|
37
|
+
* 写入 `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN`(占位 `tbp_local_17821_d7f4c9a2b8e1`)/
|
|
38
38
|
* `ANTHROPIC_MODEL` / `ANTHROPIC_DEFAULT_SONNET_MODEL`。失败时仅打印日志,
|
|
39
39
|
* 不会抛出异常;调用方无需 try/catch。
|
|
40
40
|
*
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as os from "os";
|
|
4
|
-
|
|
4
|
+
import { PROXY_ACCESS_TOKEN_PLACEHOLDER } from "./provider-install.js";
|
|
5
|
+
const PLACEHOLDER_API_KEY = PROXY_ACCESS_TOKEN_PLACEHOLDER;
|
|
5
6
|
const DESKTOP_PROFILE_ID = "00000000-0000-4000-8000-000000178210";
|
|
6
7
|
/**
|
|
7
8
|
* 获取当前用户的 home 目录,作为拼装 terminal 配置路径的基准。
|
|
@@ -76,7 +77,7 @@ export function detectTerminals() {
|
|
|
76
77
|
/**
|
|
77
78
|
* Safely rewrite Claude Code settings to route requests through our proxy.
|
|
78
79
|
*
|
|
79
|
-
* 写入 `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN`(占位 `
|
|
80
|
+
* 写入 `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN`(占位 `tbp_local_17821_d7f4c9a2b8e1`)/
|
|
80
81
|
* `ANTHROPIC_MODEL` / `ANTHROPIC_DEFAULT_SONNET_MODEL`。失败时仅打印日志,
|
|
81
82
|
* 不会抛出异常;调用方无需 try/catch。
|
|
82
83
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenbuddy/tokenbuddy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
4
4
|
"description": "TokenBuddy Client CLI and Daemon",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -10,12 +10,25 @@
|
|
|
10
10
|
"tb-proxyd": "bin/tb-proxyd.js",
|
|
11
11
|
"tb-clawtip-proof": "bin/tb-clawtip-proof.js"
|
|
12
12
|
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/*.js",
|
|
15
|
+
"dist/src/**/*.js",
|
|
16
|
+
"dist/src/**/*.d.ts",
|
|
17
|
+
"static/clawtip/",
|
|
18
|
+
"static/ui/index.html",
|
|
19
|
+
"static/ui/manifest.webmanifest",
|
|
20
|
+
"static/ui/sw.js",
|
|
21
|
+
"static/ui/assets/*.css",
|
|
22
|
+
"static/ui/assets/*.js",
|
|
23
|
+
"static/ui/icons/",
|
|
24
|
+
"static/ui/tool-logos/"
|
|
25
|
+
],
|
|
13
26
|
"scripts": {
|
|
14
27
|
"build": "tsc"
|
|
15
28
|
},
|
|
16
29
|
"dependencies": {
|
|
17
30
|
"@clack/prompts": "^0.7.0",
|
|
18
|
-
"@tokenbuddy/contracts": "^1.0.
|
|
31
|
+
"@tokenbuddy/contracts": "^1.0.37",
|
|
19
32
|
"@tokenbuddy/logging": "^1.0.0",
|
|
20
33
|
"cli-table3": "^0.6.4",
|
|
21
34
|
"commander": "^12.0.0",
|