@tokenbuddy/tokenbuddy 1.0.35 → 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.
Files changed (143) hide show
  1. package/dist/src/buyer-store.d.ts +6 -1
  2. package/dist/src/buyer-store.js +43 -4
  3. package/dist/src/cli.js +2 -2
  4. package/dist/src/daemon.d.ts +12 -0
  5. package/dist/src/daemon.js +791 -61
  6. package/dist/src/doctor-diagnostics.js +1 -6
  7. package/dist/src/provider-install.d.ts +2 -2
  8. package/dist/src/provider-install.js +248 -2
  9. package/dist/src/seller-catalog.d.ts +21 -0
  10. package/dist/src/seller-catalog.js +17 -0
  11. package/dist/src/seller-route-planner.d.ts +4 -1
  12. package/dist/src/seller-route-planner.js +3 -0
  13. package/dist/src/seller-routing-strategy.d.ts +3 -0
  14. package/dist/src/terminal-detect.d.ts +1 -1
  15. package/dist/src/terminal-detect.js +3 -2
  16. package/package.json +15 -2
  17. package/static/ui/assets/index-Djfl9tw5.js +271 -0
  18. package/static/ui/assets/index-DkfztCkn.css +1 -0
  19. package/static/ui/index.html +2 -2
  20. package/dist/src/buyer-store.d.ts.map +0 -1
  21. package/dist/src/buyer-store.js.map +0 -1
  22. package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
  23. package/dist/src/clawtip-bootstrap.js.map +0 -1
  24. package/dist/src/cli.d.ts.map +0 -1
  25. package/dist/src/cli.js.map +0 -1
  26. package/dist/src/credit-tracker.d.ts.map +0 -1
  27. package/dist/src/credit-tracker.js.map +0 -1
  28. package/dist/src/daemon.d.ts.map +0 -1
  29. package/dist/src/daemon.js.map +0 -1
  30. package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
  31. package/dist/src/doctor-clawtip-wallet.js.map +0 -1
  32. package/dist/src/doctor-diagnostics.d.ts.map +0 -1
  33. package/dist/src/doctor-diagnostics.js.map +0 -1
  34. package/dist/src/index.d.ts.map +0 -1
  35. package/dist/src/index.js.map +0 -1
  36. package/dist/src/init-clawtip-activation.d.ts.map +0 -1
  37. package/dist/src/init-clawtip-activation.js.map +0 -1
  38. package/dist/src/init-payment-options.d.ts.map +0 -1
  39. package/dist/src/init-payment-options.js.map +0 -1
  40. package/dist/src/init-setup.d.ts.map +0 -1
  41. package/dist/src/init-setup.js.map +0 -1
  42. package/dist/src/model-index.d.ts.map +0 -1
  43. package/dist/src/model-index.js.map +0 -1
  44. package/dist/src/package-update.d.ts.map +0 -1
  45. package/dist/src/package-update.js.map +0 -1
  46. package/dist/src/prewarm-cache.d.ts.map +0 -1
  47. package/dist/src/prewarm-cache.js.map +0 -1
  48. package/dist/src/prewarm-scheduler.d.ts.map +0 -1
  49. package/dist/src/prewarm-scheduler.js.map +0 -1
  50. package/dist/src/provider-install.d.ts.map +0 -1
  51. package/dist/src/provider-install.js.map +0 -1
  52. package/dist/src/provider-routing-config.d.ts.map +0 -1
  53. package/dist/src/provider-routing-config.js.map +0 -1
  54. package/dist/src/registry-trust.d.ts.map +0 -1
  55. package/dist/src/registry-trust.js.map +0 -1
  56. package/dist/src/route-failover.d.ts.map +0 -1
  57. package/dist/src/route-failover.js.map +0 -1
  58. package/dist/src/seller-catalog.d.ts.map +0 -1
  59. package/dist/src/seller-catalog.js.map +0 -1
  60. package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
  61. package/dist/src/seller-concurrency-limiter.js.map +0 -1
  62. package/dist/src/seller-metadata-cache.d.ts.map +0 -1
  63. package/dist/src/seller-metadata-cache.js.map +0 -1
  64. package/dist/src/seller-pool.d.ts.map +0 -1
  65. package/dist/src/seller-pool.js.map +0 -1
  66. package/dist/src/seller-route-planner.d.ts.map +0 -1
  67. package/dist/src/seller-route-planner.js.map +0 -1
  68. package/dist/src/seller-routing-config.d.ts.map +0 -1
  69. package/dist/src/seller-routing-config.js.map +0 -1
  70. package/dist/src/seller-routing-strategy.d.ts.map +0 -1
  71. package/dist/src/seller-routing-strategy.js.map +0 -1
  72. package/dist/src/stream-failover.d.ts.map +0 -1
  73. package/dist/src/stream-failover.js.map +0 -1
  74. package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
  75. package/dist/src/tb-clawtip-proof.js.map +0 -1
  76. package/dist/src/tb-proxyd.d.ts.map +0 -1
  77. package/dist/src/tb-proxyd.js.map +0 -1
  78. package/dist/src/terminal-detect.d.ts.map +0 -1
  79. package/dist/src/terminal-detect.js.map +0 -1
  80. package/dist/src/terminal-image.d.ts.map +0 -1
  81. package/dist/src/terminal-image.js.map +0 -1
  82. package/src/buyer-store.ts +0 -1090
  83. package/src/clawtip-bootstrap.ts +0 -65
  84. package/src/cli.ts +0 -2243
  85. package/src/credit-tracker.ts +0 -295
  86. package/src/daemon.ts +0 -5475
  87. package/src/doctor-clawtip-wallet.ts +0 -95
  88. package/src/doctor-diagnostics.ts +0 -1026
  89. package/src/index.ts +0 -16
  90. package/src/init-clawtip-activation.ts +0 -695
  91. package/src/init-payment-options.ts +0 -373
  92. package/src/init-setup.ts +0 -165
  93. package/src/model-index.ts +0 -278
  94. package/src/package-update.ts +0 -311
  95. package/src/prewarm-cache.ts +0 -485
  96. package/src/prewarm-scheduler.ts +0 -675
  97. package/src/provider-install.ts +0 -1006
  98. package/src/provider-routing-config.ts +0 -410
  99. package/src/registry-trust.ts +0 -51
  100. package/src/route-failover.ts +0 -304
  101. package/src/seller-catalog.ts +0 -505
  102. package/src/seller-concurrency-limiter.ts +0 -161
  103. package/src/seller-metadata-cache.ts +0 -91
  104. package/src/seller-pool.ts +0 -557
  105. package/src/seller-route-planner.ts +0 -513
  106. package/src/seller-routing-config.ts +0 -211
  107. package/src/seller-routing-strategy.ts +0 -362
  108. package/src/stream-failover.ts +0 -152
  109. package/src/tb-clawtip-proof.ts +0 -28
  110. package/src/tb-proxyd.ts +0 -101
  111. package/src/terminal-detect.ts +0 -333
  112. package/src/terminal-image.ts +0 -228
  113. package/static/ui/assets/index-0MVXD7bH.css +0 -1
  114. package/static/ui/assets/index-BVbeDEwq.js +0 -271
  115. package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
  116. package/tests/cli-routing.test.ts +0 -363
  117. package/tests/control-plane-ui-endpoints.test.ts +0 -1630
  118. package/tests/credit-tracker.test.ts +0 -165
  119. package/tests/daemon-413-fallback.test.ts +0 -92
  120. package/tests/daemon-classify.test.ts +0 -452
  121. package/tests/daemon-roles.test.ts +0 -92
  122. package/tests/daemon-trusted-registry-cache.test.ts +0 -132
  123. package/tests/e2e.test.ts +0 -366
  124. package/tests/image-generation-e2e.test.ts +0 -230
  125. package/tests/model-index.test.ts +0 -198
  126. package/tests/package-update.test.ts +0 -147
  127. package/tests/prewarm-cache.test.ts +0 -296
  128. package/tests/prewarm-scheduler.test.ts +0 -367
  129. package/tests/provider-routing-config.test.ts +0 -150
  130. package/tests/registry-trust.test.ts +0 -28
  131. package/tests/route-failover.test.ts +0 -222
  132. package/tests/seller-catalog-413.test.ts +0 -120
  133. package/tests/seller-catalog-utilities.test.ts +0 -124
  134. package/tests/seller-concurrency-limiter.test.ts +0 -83
  135. package/tests/seller-metadata-cache.test.ts +0 -89
  136. package/tests/seller-pool.test.ts +0 -365
  137. package/tests/seller-route-planner.test.ts +0 -312
  138. package/tests/seller-routing-config.test.ts +0 -124
  139. package/tests/seller-routing-strategy.test.ts +0 -167
  140. package/tests/stream-failover.test.ts +0 -52
  141. package/tests/thousand-seller.test.ts +0 -151
  142. package/tests/tokenbuddy.test.ts +0 -4043
  143. 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
- if (ratio === 0)
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 = "TOKENBUDDY_PROXY";
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 = "TOKENBUDDY_PROXY";
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
- results.push({ providerId, path: "", action: "missing_snapshot" });
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`(占位 `TOKENBUDDY_PROXY`)/
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
- const PLACEHOLDER_API_KEY = "TOKENBUDDY_PROXY";
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`(占位 `TOKENBUDDY_PROXY`)/
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.35",
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.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",