@tokenbuddy/tokenbuddy 1.0.12 → 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.
Files changed (135) hide show
  1. package/dist/src/buyer-store.d.ts +61 -0
  2. package/dist/src/buyer-store.d.ts.map +1 -1
  3. package/dist/src/buyer-store.js +12 -0
  4. package/dist/src/buyer-store.js.map +1 -1
  5. package/dist/src/cli.d.ts +47 -0
  6. package/dist/src/cli.d.ts.map +1 -1
  7. package/dist/src/cli.js +287 -63
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/credit-tracker.d.ts +26 -0
  10. package/dist/src/credit-tracker.d.ts.map +1 -1
  11. package/dist/src/credit-tracker.js +8 -0
  12. package/dist/src/credit-tracker.js.map +1 -1
  13. package/dist/src/daemon.d.ts +29 -3
  14. package/dist/src/daemon.d.ts.map +1 -1
  15. package/dist/src/daemon.js +292 -65
  16. package/dist/src/daemon.js.map +1 -1
  17. package/dist/src/doctor-clawtip-wallet.d.ts +25 -0
  18. package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -1
  19. package/dist/src/doctor-clawtip-wallet.js +13 -0
  20. package/dist/src/doctor-clawtip-wallet.js.map +1 -1
  21. package/dist/src/doctor-diagnostics.d.ts +63 -0
  22. package/dist/src/doctor-diagnostics.d.ts.map +1 -1
  23. package/dist/src/doctor-diagnostics.js +39 -1
  24. package/dist/src/doctor-diagnostics.js.map +1 -1
  25. package/dist/src/index.d.ts +4 -0
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/index.js +4 -0
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/init-clawtip-activation.d.ts +103 -0
  30. package/dist/src/init-clawtip-activation.d.ts.map +1 -1
  31. package/dist/src/init-clawtip-activation.js +60 -0
  32. package/dist/src/init-clawtip-activation.js.map +1 -1
  33. package/dist/src/init-payment-options.d.ts +124 -0
  34. package/dist/src/init-payment-options.d.ts.map +1 -1
  35. package/dist/src/init-payment-options.js +68 -0
  36. package/dist/src/init-payment-options.js.map +1 -1
  37. package/dist/src/model-index.d.ts +9 -0
  38. package/dist/src/model-index.d.ts.map +1 -1
  39. package/dist/src/model-index.js.map +1 -1
  40. package/dist/src/prewarm-cache.d.ts +89 -0
  41. package/dist/src/prewarm-cache.d.ts.map +1 -1
  42. package/dist/src/prewarm-cache.js +14 -1
  43. package/dist/src/prewarm-cache.js.map +1 -1
  44. package/dist/src/prewarm-scheduler.d.ts +62 -3
  45. package/dist/src/prewarm-scheduler.d.ts.map +1 -1
  46. package/dist/src/prewarm-scheduler.js +39 -8
  47. package/dist/src/prewarm-scheduler.js.map +1 -1
  48. package/dist/src/provider-install.d.ts +89 -3
  49. package/dist/src/provider-install.d.ts.map +1 -1
  50. package/dist/src/provider-install.js +77 -19
  51. package/dist/src/provider-install.js.map +1 -1
  52. package/dist/src/route-failover.d.ts +48 -0
  53. package/dist/src/route-failover.d.ts.map +1 -1
  54. package/dist/src/route-failover.js.map +1 -1
  55. package/dist/src/seller-catalog.d.ts +158 -10
  56. package/dist/src/seller-catalog.d.ts.map +1 -1
  57. package/dist/src/seller-catalog.js +79 -5
  58. package/dist/src/seller-catalog.js.map +1 -1
  59. package/dist/src/seller-metadata-cache.d.ts +29 -0
  60. package/dist/src/seller-metadata-cache.d.ts.map +1 -0
  61. package/dist/src/seller-metadata-cache.js +71 -0
  62. package/dist/src/seller-metadata-cache.js.map +1 -0
  63. package/dist/src/seller-pool.d.ts +71 -0
  64. package/dist/src/seller-pool.d.ts.map +1 -1
  65. package/dist/src/seller-pool.js +6 -1
  66. package/dist/src/seller-pool.js.map +1 -1
  67. package/dist/src/seller-route-planner.d.ts +118 -0
  68. package/dist/src/seller-route-planner.d.ts.map +1 -0
  69. package/dist/src/seller-route-planner.js +160 -0
  70. package/dist/src/seller-route-planner.js.map +1 -0
  71. package/dist/src/seller-routing-config.d.ts +69 -0
  72. package/dist/src/seller-routing-config.d.ts.map +1 -0
  73. package/dist/src/seller-routing-config.js +164 -0
  74. package/dist/src/seller-routing-config.js.map +1 -0
  75. package/dist/src/seller-routing-strategy.d.ts +118 -0
  76. package/dist/src/seller-routing-strategy.d.ts.map +1 -0
  77. package/dist/src/seller-routing-strategy.js +183 -0
  78. package/dist/src/seller-routing-strategy.js.map +1 -0
  79. package/dist/src/stream-failover.d.ts +23 -0
  80. package/dist/src/stream-failover.d.ts.map +1 -1
  81. package/dist/src/stream-failover.js +4 -0
  82. package/dist/src/stream-failover.js.map +1 -1
  83. package/dist/src/tb-proxyd.js +7 -21
  84. package/dist/src/tb-proxyd.js.map +1 -1
  85. package/dist/src/terminal-detect.d.ts +51 -0
  86. package/dist/src/terminal-detect.d.ts.map +1 -1
  87. package/dist/src/terminal-detect.js +42 -0
  88. package/dist/src/terminal-detect.js.map +1 -1
  89. package/dist/src/terminal-image.d.ts +41 -0
  90. package/dist/src/terminal-image.d.ts.map +1 -1
  91. package/dist/src/terminal-image.js +15 -0
  92. package/dist/src/terminal-image.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/buyer-store.ts +61 -0
  95. package/src/cli.ts +330 -68
  96. package/src/credit-tracker.ts +26 -0
  97. package/src/daemon.ts +363 -72
  98. package/src/doctor-clawtip-wallet.ts +25 -0
  99. package/src/doctor-diagnostics.ts +63 -1
  100. package/src/index.ts +4 -0
  101. package/src/init-clawtip-activation.ts +103 -0
  102. package/src/init-payment-options.ts +124 -0
  103. package/src/model-index.ts +9 -0
  104. package/src/prewarm-cache.ts +99 -1
  105. package/src/prewarm-scheduler.ts +97 -12
  106. package/src/provider-install.ts +125 -27
  107. package/src/route-failover.ts +48 -0
  108. package/src/seller-catalog.ts +158 -12
  109. package/src/seller-metadata-cache.ts +91 -0
  110. package/src/seller-pool.ts +77 -1
  111. package/src/seller-route-planner.ts +323 -0
  112. package/src/seller-routing-config.ts +198 -0
  113. package/src/seller-routing-strategy.ts +316 -0
  114. package/src/stream-failover.ts +23 -0
  115. package/src/tb-proxyd.ts +7 -23
  116. package/src/terminal-detect.ts +51 -0
  117. package/src/terminal-image.ts +41 -0
  118. package/tests/cli-routing.test.ts +287 -0
  119. package/tests/daemon-classify.test.ts +431 -0
  120. package/tests/daemon-roles.test.ts +92 -0
  121. package/tests/seller-catalog-utilities.test.ts +70 -0
  122. package/tests/seller-metadata-cache.test.ts +89 -0
  123. package/tests/seller-route-planner.test.ts +150 -0
  124. package/tests/seller-routing-config.test.ts +111 -0
  125. package/tests/seller-routing-strategy.test.ts +166 -0
  126. package/tests/tokenbuddy.test.ts +446 -34
  127. /package/{src → tests}/credit-tracker.test.ts +0 -0
  128. /package/{src → tests}/model-index.test.ts +0 -0
  129. /package/{src → tests}/prewarm-cache.test.ts +0 -0
  130. /package/{src → tests}/prewarm-scheduler.test.ts +0 -0
  131. /package/{src → tests}/route-failover.test.ts +0 -0
  132. /package/{src → tests}/seller-catalog-413.test.ts +0 -0
  133. /package/{src → tests}/seller-pool.test.ts +0 -0
  134. /package/{src → tests}/stream-failover.test.ts +0 -0
  135. /package/{src → tests}/thousand-seller.test.ts +0 -0
@@ -7,7 +7,6 @@ import {
7
7
  } from "./buyer-store.js";
8
8
  import {
9
9
  ProtocolPreference,
10
- SellerRoutingPreference,
11
10
  } from "./seller-catalog.js";
12
11
 
13
12
  export const PROXY_ACCESS_TOKEN_PLACEHOLDER = "TOKENBUDDY_PROXY";
@@ -16,7 +15,6 @@ const CLAUDE_ONE_M_MARKER = "[1M]";
16
15
  const CLAUDE_CLIENT_HAIKU_MODEL = "claude-haiku-4-5";
17
16
  const CLAUDE_CLIENT_SONNET_MODEL = "claude-sonnet-4-6";
18
17
  const CLAUDE_CLIENT_OPUS_MODEL = "claude-opus-4-7";
19
- const ROUTING_CONFIG_KEY = "routing";
20
18
 
21
19
  export const SUPPORTED_PROVIDER_IDS = [
22
20
  "codex",
@@ -30,23 +28,41 @@ export const SUPPORTED_PROVIDER_IDS = [
30
28
  export type ProviderId = typeof SUPPORTED_PROVIDER_IDS[number];
31
29
  export type ModelSelectionKind = "single-model" | "claude-role-mapping";
32
30
 
31
+ /**
32
+ * provider 探测选项。
33
+ */
33
34
  export interface ProviderDetectOptions {
35
+ /** 用户 home 目录,默认 `os.homedir()`;测试可注入临时目录 */
34
36
  home?: string;
35
37
  }
36
38
 
39
+ /**
40
+ * 单模型 provider 的 runtime config(适用于 codex / opencode / hermes / openclaw 等)。
41
+ * `defaultModel` 是 buyer 端默认转发到的上游模型 ID。
42
+ */
37
43
  export interface SingleModelProviderRuntimeConfig {
38
44
  selectionKind: "single-model";
39
45
  protocolPreference?: ProtocolPreference;
40
46
  defaultModel: string;
41
- sellerId?: string;
42
47
  }
43
48
 
49
+ /**
50
+ * Claude Code role 绑定:把 buyer 端的 `haiku/sonnet/opus` 角色映射到具体上游模型。
51
+ * `declareOneM` 用于在 UI 上声明 1M context(仅展示用途,不参与计费)。
52
+ */
44
53
  export interface ClaudeRoleBinding {
54
+ /** 实际转发到 seller 的上游模型 ID */
45
55
  upstreamModel: string;
56
+ /** UI 展示名(可选) */
46
57
  displayName?: string;
58
+ /** 是否在配置里标注 1M context(Claude Code 1M marker) */
47
59
  declareOneM?: boolean;
48
60
  }
49
61
 
62
+ /**
63
+ * Claude Code provider 的 model mapping 配置。
64
+ * 三个 role 都可选;缺失时回退到 `fallbackModel`。
65
+ */
50
66
  export interface ClaudeCodeModelMappingConfig {
51
67
  selectionKind: "claude-role-mapping";
52
68
  protocolPreference?: ProtocolPreference;
@@ -58,24 +74,40 @@ export interface ClaudeCodeModelMappingConfig {
58
74
  };
59
75
  }
60
76
 
77
+ /**
78
+ * 任何 provider 的 runtime config 联合类型。
79
+ * 通过 `selectionKind` 字段做类型守卫。
80
+ */
61
81
  export type ProviderRuntimeConfig =
62
82
  | SingleModelProviderRuntimeConfig
63
83
  | ClaudeCodeModelMappingConfig;
64
84
 
85
+ /**
86
+ * `tb init` 期间为每个 provider 收集的最终选择(按 provider id 索引)。
87
+ */
65
88
  export type ProviderSelections = Partial<Record<ProviderId, ProviderRuntimeConfig>>;
66
89
 
90
+ /**
91
+ * `applyProviderInstall` / `previewProviderInstall` 的输入。
92
+ * 必填 `providers`(要安装的 provider ID 列表)和 `proxyUrl`(buyer 代理 URL)。
93
+ */
67
94
  export interface ProviderInstallOptions extends ProviderDetectOptions {
68
95
  providers: string[];
69
96
  proxyUrl: string;
70
97
  model?: string;
71
98
  providerSelections?: ProviderSelections;
72
- sellerRouting?: SellerRoutingPreference;
73
99
  }
74
100
 
101
+ /**
102
+ * `rollbackProviderInstall` 的输入。
103
+ */
75
104
  export interface ProviderRollbackOptions extends ProviderDetectOptions {
76
105
  providers: string[];
77
106
  }
78
107
 
108
+ /**
109
+ * provider 探测结果(供 `tb doctor` / install 流程使用)。
110
+ */
79
111
  export interface ProviderCandidate {
80
112
  id: ProviderId;
81
113
  name: string;
@@ -89,6 +121,9 @@ export interface ProviderCandidate {
89
121
  reason: string;
90
122
  }
91
123
 
124
+ /**
125
+ * provider 安装时产生的单文件变更(preview 阶段用)。
126
+ */
92
127
  export interface ProviderFileChange {
93
128
  providerId: ProviderId;
94
129
  path: string;
@@ -98,12 +133,18 @@ export interface ProviderFileChange {
98
133
  content: string;
99
134
  }
100
135
 
136
+ /**
137
+ * `applyProviderInstall` 的结果(每文件一行)。
138
+ */
101
139
  export interface ProviderApplyResult {
102
140
  providerId: ProviderId;
103
141
  path: string;
104
142
  action: "created" | "updated";
105
143
  }
106
144
 
145
+ /**
146
+ * `rollbackProviderInstall` 的结果(每文件一行)。
147
+ */
107
148
  export interface ProviderRollbackResult {
108
149
  providerId: ProviderId;
109
150
  path: string;
@@ -116,6 +157,7 @@ interface ProviderDefinition {
116
157
  configPath(home: string): string;
117
158
  commandName?: string;
118
159
  observedPaths?(home: string): string[];
160
+ isConfigured?(filePath: string, home: string): boolean;
119
161
  changes(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[];
120
162
  modelSelectionKind: ModelSelectionKind;
121
163
  protocolPreference?: ProtocolPreference;
@@ -167,6 +209,16 @@ function readJsonObject(filePath: string): Record<string, unknown> {
167
209
  }
168
210
  }
169
211
 
212
+ function readObjectField(value: unknown, key: string): Record<string, unknown> | undefined {
213
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
214
+ return undefined;
215
+ }
216
+ const field = (value as Record<string, unknown>)[key];
217
+ return field && typeof field === "object" && !Array.isArray(field)
218
+ ? field as Record<string, unknown>
219
+ : undefined;
220
+ }
221
+
170
222
  function jsonContent(value: unknown): string {
171
223
  return `${JSON.stringify(value, null, 2)}\n`;
172
224
  }
@@ -243,13 +295,11 @@ function makeChange(providerId: ProviderId, filePath: string, summary: string, c
243
295
  function makeSingleModelRuntimeConfig(
244
296
  provider: ProviderDefinition,
245
297
  model: string,
246
- sellerId?: string,
247
298
  ): SingleModelProviderRuntimeConfig {
248
299
  return {
249
300
  selectionKind: "single-model",
250
301
  protocolPreference: provider.protocolPreference,
251
302
  defaultModel: model,
252
- sellerId,
253
303
  };
254
304
  }
255
305
 
@@ -275,11 +325,7 @@ function resolveProviderRuntimeConfig(
275
325
  if (!model) {
276
326
  throw new Error(`model is required for provider ${provider.id}`);
277
327
  }
278
- return makeSingleModelRuntimeConfig(
279
- provider,
280
- model,
281
- options.sellerRouting?.mode === "fixed" ? options.sellerRouting.sellerId : undefined,
282
- );
328
+ return makeSingleModelRuntimeConfig(provider, model);
283
329
  }
284
330
 
285
331
  function codexConfig(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[] {
@@ -469,14 +515,7 @@ function opencodeConfig(home: string, proxyUrl: string, config: ProviderRuntimeC
469
515
  : {};
470
516
  providers.tokenbuddy = {
471
517
  name: "TokenBuddy",
472
- // v1.0.12 修复:v1.0.11 改成 @ai-sdk/openai-responses 后,opencode 1.14.28
473
- // desktop 加载 custom provider 时报 ProviderModelNotFoundError(schema
474
- // mismatch —— SDK 是 dynamic model 但 opencode 期望静态 languageModel 映射)。
475
- // 1.0.11 改动对 opencode 用户实际跑不通,回滚到 chat completions。
476
- // 备注:buyer 端 /v1/responses 协议支持仍然完整(之前已验证
477
- // buyer→tbs-719577→code.shoestravel.xin 200 + 真实 LLM 响应)。等
478
- // opencode 1.15+ 支持 openai-responses custom provider 再切默认。
479
- npm: "@ai-sdk/openai",
518
+ npm: "@ai-sdk/openai-compatible",
480
519
  options: {
481
520
  apiKey: PROXY_ACCESS_TOKEN_PLACEHOLDER,
482
521
  baseURL: openAiBaseUrl(proxyUrl),
@@ -496,6 +535,25 @@ function opencodeConfig(home: string, proxyUrl: string, config: ProviderRuntimeC
496
535
  return [makeChange("opencode", configPath, "configure OpenCode provider for TokenBuddy proxy", jsonContent(current))];
497
536
  }
498
537
 
538
+ function isOpencodeTokenBuddyConfigured(filePath: string): boolean {
539
+ const current = readJsonObject(filePath);
540
+ const tokenbuddy = readObjectField(readObjectField(current, "provider"), "tokenbuddy");
541
+ const options = readObjectField(tokenbuddy, "options");
542
+ if (!tokenbuddy || !options) {
543
+ return false;
544
+ }
545
+
546
+ return tokenbuddy.npm === "@ai-sdk/openai-compatible" &&
547
+ options.apiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
548
+ typeof options.baseURL === "string" &&
549
+ options.baseURL.includes("127.0.0.1") &&
550
+ options.baseURL.endsWith("/v1") &&
551
+ typeof current.model === "string" &&
552
+ current.model.startsWith("tokenbuddy/") &&
553
+ typeof current.small_model === "string" &&
554
+ current.small_model.startsWith("tokenbuddy/");
555
+ }
556
+
499
557
  function hermesConfig(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[] {
500
558
  const model = pickConfiguredModel(config);
501
559
  const configPath = path.join(home, ".hermes", "settings.json");
@@ -557,9 +615,10 @@ const PROVIDERS: ProviderDefinition[] = [
557
615
  name: "OpenCode",
558
616
  commandName: "opencode",
559
617
  configPath: (home) => path.join(home, ".config", "opencode", "opencode.json"),
618
+ isConfigured: isOpencodeTokenBuddyConfigured,
560
619
  changes: opencodeConfig,
561
620
  modelSelectionKind: "single-model",
562
- protocolPreference: "responses",
621
+ protocolPreference: "chat_completions",
563
622
  },
564
623
  {
565
624
  id: "hermes",
@@ -584,14 +643,22 @@ function getProviderDefinition(providerId: ProviderId): ProviderDefinition {
584
643
  return provider;
585
644
  }
586
645
 
646
+ /**
647
+ * 探测所有 SUPPORTED_PROVIDER_IDS:检查可执行文件、配置文件、原生 hints 目录。
648
+ * 状态机:`configured`(已存在配置且被识别) / `installed`(可执行/配置存在但未配置) / `missing`(都未找到)。
649
+ *
650
+ * @param options 探测选项
651
+ * @returns 探测结果列表
652
+ */
587
653
  export function detectProviders(options: ProviderDetectOptions = {}): ProviderCandidate[] {
588
654
  const home = resolveHome(options.home);
589
655
  return PROVIDERS.map((provider) => {
590
656
  const configPath = provider.configPath(home);
591
- const configured = fs.existsSync(configPath);
657
+ const configExists = fs.existsSync(configPath);
658
+ const configured = configExists && (provider.isConfigured?.(configPath, home) ?? true);
592
659
  const executablePath = provider.commandName ? resolveExecutable(provider.commandName) : undefined;
593
660
  const observedPaths = provider.observedPaths?.(home).filter((entry) => fs.existsSync(entry)) || [];
594
- const installed = Boolean(executablePath) || observedPaths.length > 0;
661
+ const installed = Boolean(executablePath) || observedPaths.length > 0 || configExists;
595
662
  const status: ProviderCandidate["status"] = configured
596
663
  ? "configured"
597
664
  : installed
@@ -626,14 +693,33 @@ export function detectProviders(options: ProviderDetectOptions = {}): ProviderCa
626
693
  });
627
694
  }
628
695
 
696
+ /**
697
+ * 返回 provider 默认的协议偏好(未声明则返回 `undefined`,由运行时探测)。
698
+ *
699
+ * @param providerId provider ID
700
+ * @returns 协议偏好
701
+ */
629
702
  export function getProviderProtocolPreference(providerId: ProviderId): ProtocolPreference | undefined {
630
703
  return getProviderDefinition(providerId).protocolPreference;
631
704
  }
632
705
 
706
+ /**
707
+ * 返回 provider 的 model selection kind(决定 init 走 single-model 还是 claude-role-mapping 流程)。
708
+ *
709
+ * @param providerId provider ID
710
+ * @returns selection kind
711
+ */
633
712
  export function getProviderModelSelectionKind(providerId: ProviderId): ModelSelectionKind {
634
713
  return getProviderDefinition(providerId).modelSelectionKind;
635
714
  }
636
715
 
716
+ /**
717
+ * 预览 provider 安装会产生的文件变更(不实际写盘)。
718
+ * 给 `tb init` 在用户确认前展示"将要改哪些文件 / 改什么内容"。
719
+ *
720
+ * @param options 安装选项
721
+ * @returns 计划中的文件变更列表
722
+ */
637
723
  export function previewProviderInstall(options: ProviderInstallOptions): ProviderFileChange[] {
638
724
  const home = resolveHome(options.home);
639
725
  const providerIds = assertProviderIds(options.providers);
@@ -647,6 +733,14 @@ export function previewProviderInstall(options: ProviderInstallOptions): Provide
647
733
  });
648
734
  }
649
735
 
736
+ /**
737
+ * 实际写盘 provider 配置。
738
+ * 同时把每个 provider 的原始文件快照写入 `store`(用于 `tb rollback`)。
739
+ *
740
+ * @param options 安装选项
741
+ * @param store buyer store(用于快照 + runtime config 持久化)
742
+ * @returns 实际写盘结果列表
743
+ */
650
744
  export function applyProviderInstall(options: ProviderInstallOptions, store: BuyerStore): ProviderApplyResult[] {
651
745
  const providerIds = assertProviderIds(options.providers);
652
746
  const changes = previewProviderInstall(options);
@@ -673,10 +767,6 @@ export function applyProviderInstall(options: ProviderInstallOptions, store: Buy
673
767
  store.saveProviderRuntimeConfig(providerId, runtimeConfig);
674
768
  }
675
769
 
676
- if (options.sellerRouting) {
677
- store.saveDaemonRuntimeConfig(ROUTING_CONFIG_KEY, options.sellerRouting);
678
- }
679
-
680
770
  const applied: ProviderApplyResult[] = [];
681
771
  for (const change of changes) {
682
772
  const dir = path.dirname(change.path);
@@ -693,6 +783,15 @@ export function applyProviderInstall(options: ProviderInstallOptions, store: Buy
693
783
  return applied;
694
784
  }
695
785
 
786
+ /**
787
+ * 回滚 provider 安装。
788
+ * 从 `store` 读取安装前的快照,恢复原文件(如果快照里有原始内容)。
789
+ * 没有快照的 provider 标记为 `missing_snapshot`。
790
+ *
791
+ * @param options 回滚选项
792
+ * @param store buyer store
793
+ * @returns 回滚结果列表
794
+ */
696
795
  export function rollbackProviderInstall(options: ProviderRollbackOptions, store: BuyerStore): ProviderRollbackResult[] {
697
796
  const providerIds = assertProviderIds(options.providers);
698
797
  const results: ProviderRollbackResult[] = [];
@@ -718,6 +817,5 @@ export function rollbackProviderInstall(options: ProviderRollbackOptions, store:
718
817
  store.removeProviderInstallSnapshot(providerId);
719
818
  store.removeProviderRuntimeConfig(providerId);
720
819
  }
721
- store.removeDaemonRuntimeConfig(ROUTING_CONFIG_KEY);
722
820
  return results;
723
821
  }
@@ -5,42 +5,90 @@ import type { CreditTracker } from "./credit-tracker.js";
5
5
 
6
6
  const logger = createModuleLogger("tb-proxyd:route-failover");
7
7
 
8
+ /**
9
+ * 路由失败后的下一步动作(控制器收到 `FailoverDecision` 后据此执行)。
10
+ * - `retry_same_seller`:在同 seller 上做 jittered backoff 重试
11
+ * - `failover_next`:从池里取出下一个候选 seller
12
+ * - `fail_fast`:立刻向上抛错,不再尝试其它候选
13
+ * - `abort`:候选已耗尽,放弃本轮
14
+ */
8
15
  export type RouteAction = "retry_same_seller" | "failover_next" | "fail_fast" | "abort";
9
16
 
17
+ /**
18
+ * 一次失败后 `RouteFailover.decide()` 返回的结构化决策。
19
+ * 控制器根据 `action` 决定是否重试/切换/终止,并把 `wastedCreditMicros`
20
+ * 上报到 doctor 面板。
21
+ */
10
22
  export interface FailoverDecision {
23
+ /** 控制器要执行的下一步动作 */
11
24
  action: RouteAction;
25
+ /** 仅当 `action === "retry_same_seller"` 时使用,表示 backoff 后多久重试(毫秒) */
12
26
  retryDelayMs?: number;
27
+ /** 决策理由,结构化但可读,供日志 / doctor 渲染 */
13
28
  reason: string;
29
+ /** 本次失败后被废弃的 credit(USD micros),供 doctor 统计 */
14
30
  wastedCreditMicros?: number;
31
+ /** 失败发生在 fresh-purchase 窗口内(刚买不到 N 秒),用于触发软重试保护 */
15
32
  freshPurchase: boolean;
33
+ /** 在切走这个 seller 之前已经尝试的次数(含本次失败) */
16
34
  retryAttemptsBeforeFailover: number;
35
+ /** 当次会话是否已超出 auto-purchase 预算;true 时不再触发新一轮 buy */
17
36
  budgetExceeded: boolean;
18
37
  }
19
38
 
39
+ /**
40
+ * 一次路由选择的最小可执行单元:池里的某个 entry + 关联的 registry 描述。
41
+ * `routeIndex` 仅用于日志排版,控制器自己维护尝试序号。
42
+ */
20
43
  export interface RouteCandidate {
44
+ /** 在本批候选中的序号(0-based),仅作日志可读性使用 */
21
45
  routeIndex: number;
46
+ /** 池里的 entry(包含 circuit / healthScore 等运行时状态) */
22
47
  entry: PoolEntry;
48
+ /** registry 里的原始 seller 描述 */
23
49
  registrySeller: RegistrySeller;
50
+ /** seller 全局 ID(同时也是 token class) */
24
51
  sellerId: string;
52
+ /** 去掉尾部斜杠后的 seller URL */
25
53
  url: string;
26
54
  }
27
55
 
56
+ /**
57
+ * 控制器调用 `decide()` 时传入的失败上下文。
58
+ * 描述"这次失败发生在谁身上、是什么错误、第几次尝试"。
59
+ */
28
60
  export interface DecideContext {
61
+ /** 失败时正在调用的 seller ID */
29
62
  sellerId: string;
63
+ /** HTTP status(如有),用于区分 4xx/5xx */
30
64
  status?: number;
65
+ /** 错误分类,由调用方在边界层归一化为 `FailureKind` */
31
66
  errorKind: FailureKind;
67
+ /** 人类可读错误描述(不携带敏感字段),用于日志/doctor */
32
68
  errorMessage?: string;
69
+ /** 当前是该 seller 的第几次尝试(1-based) */
33
70
  attempt: number;
34
71
  }
35
72
 
73
+ /**
74
+ * 构造 `RouteFailover` 所需的依赖与可调参数。
75
+ * 默认值见 `DEFAULTS`:软重试 2 次,jitter 100-400ms。
76
+ */
36
77
  export interface RouteFailoverOptions {
78
+ /** 共享的 seller pool,控制器只读 + 委托 recordSuccess/recordFailure */
37
79
  pool: SellerPool;
80
+ /** 共享的 credit tracker,用于 wasted 计算与 auto-purchase 预算 */
38
81
  creditTracker: CreditTracker;
39
82
  // v1.1 / v1.2 design defaults; exposed for tests.
83
+ /** 软重试次数上限,默认 2 */
40
84
  softRetryAttempts?: number; // default 2
85
+ /** 软重试 backoff jitter 下界(毫秒),默认 100 */
41
86
  softRetryJitterMinMs?: number; // default 100
87
+ /** 软重试 backoff jitter 上界(毫秒),默认 400 */
42
88
  softRetryJitterMaxMs?: number; // default 400
89
+ /** 注入的随机源(测试用),默认 `Math.random` */
43
90
  random?: () => number;
91
+ /** 注入的时钟(测试用),默认 `Date.now` */
44
92
  now?: () => number;
45
93
  }
46
94