@tokenbuddy/tokenbuddy 1.0.36 → 1.0.38

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 (146) hide show
  1. package/dist/src/buyer-store.d.ts +7 -2
  2. package/dist/src/buyer-store.js +46 -7
  3. package/dist/src/cli.d.ts +1 -0
  4. package/dist/src/cli.js +15 -7
  5. package/dist/src/daemon.d.ts +12 -0
  6. package/dist/src/daemon.js +791 -61
  7. package/dist/src/doctor-diagnostics.js +1 -6
  8. package/dist/src/provider-install.d.ts +2 -2
  9. package/dist/src/provider-install.js +248 -2
  10. package/dist/src/seller-catalog.d.ts +21 -0
  11. package/dist/src/seller-catalog.js +17 -0
  12. package/dist/src/seller-route-planner.d.ts +4 -1
  13. package/dist/src/seller-route-planner.js +3 -0
  14. package/dist/src/seller-routing-strategy.d.ts +3 -0
  15. package/dist/src/terminal-detect.d.ts +1 -1
  16. package/dist/src/terminal-detect.js +3 -2
  17. package/dist/src/workdir.d.ts +10 -0
  18. package/dist/src/workdir.js +26 -0
  19. package/package.json +15 -2
  20. package/static/ui/assets/index-Djfl9tw5.js +271 -0
  21. package/static/ui/assets/index-DkfztCkn.css +1 -0
  22. package/static/ui/index.html +2 -2
  23. package/dist/src/buyer-store.d.ts.map +0 -1
  24. package/dist/src/buyer-store.js.map +0 -1
  25. package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
  26. package/dist/src/clawtip-bootstrap.js.map +0 -1
  27. package/dist/src/cli.d.ts.map +0 -1
  28. package/dist/src/cli.js.map +0 -1
  29. package/dist/src/credit-tracker.d.ts.map +0 -1
  30. package/dist/src/credit-tracker.js.map +0 -1
  31. package/dist/src/daemon.d.ts.map +0 -1
  32. package/dist/src/daemon.js.map +0 -1
  33. package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
  34. package/dist/src/doctor-clawtip-wallet.js.map +0 -1
  35. package/dist/src/doctor-diagnostics.d.ts.map +0 -1
  36. package/dist/src/doctor-diagnostics.js.map +0 -1
  37. package/dist/src/index.d.ts.map +0 -1
  38. package/dist/src/index.js.map +0 -1
  39. package/dist/src/init-clawtip-activation.d.ts.map +0 -1
  40. package/dist/src/init-clawtip-activation.js.map +0 -1
  41. package/dist/src/init-payment-options.d.ts.map +0 -1
  42. package/dist/src/init-payment-options.js.map +0 -1
  43. package/dist/src/init-setup.d.ts.map +0 -1
  44. package/dist/src/init-setup.js.map +0 -1
  45. package/dist/src/model-index.d.ts.map +0 -1
  46. package/dist/src/model-index.js.map +0 -1
  47. package/dist/src/package-update.d.ts.map +0 -1
  48. package/dist/src/package-update.js.map +0 -1
  49. package/dist/src/prewarm-cache.d.ts.map +0 -1
  50. package/dist/src/prewarm-cache.js.map +0 -1
  51. package/dist/src/prewarm-scheduler.d.ts.map +0 -1
  52. package/dist/src/prewarm-scheduler.js.map +0 -1
  53. package/dist/src/provider-install.d.ts.map +0 -1
  54. package/dist/src/provider-install.js.map +0 -1
  55. package/dist/src/provider-routing-config.d.ts.map +0 -1
  56. package/dist/src/provider-routing-config.js.map +0 -1
  57. package/dist/src/registry-trust.d.ts.map +0 -1
  58. package/dist/src/registry-trust.js.map +0 -1
  59. package/dist/src/route-failover.d.ts.map +0 -1
  60. package/dist/src/route-failover.js.map +0 -1
  61. package/dist/src/seller-catalog.d.ts.map +0 -1
  62. package/dist/src/seller-catalog.js.map +0 -1
  63. package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
  64. package/dist/src/seller-concurrency-limiter.js.map +0 -1
  65. package/dist/src/seller-metadata-cache.d.ts.map +0 -1
  66. package/dist/src/seller-metadata-cache.js.map +0 -1
  67. package/dist/src/seller-pool.d.ts.map +0 -1
  68. package/dist/src/seller-pool.js.map +0 -1
  69. package/dist/src/seller-route-planner.d.ts.map +0 -1
  70. package/dist/src/seller-route-planner.js.map +0 -1
  71. package/dist/src/seller-routing-config.d.ts.map +0 -1
  72. package/dist/src/seller-routing-config.js.map +0 -1
  73. package/dist/src/seller-routing-strategy.d.ts.map +0 -1
  74. package/dist/src/seller-routing-strategy.js.map +0 -1
  75. package/dist/src/stream-failover.d.ts.map +0 -1
  76. package/dist/src/stream-failover.js.map +0 -1
  77. package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
  78. package/dist/src/tb-clawtip-proof.js.map +0 -1
  79. package/dist/src/tb-proxyd.d.ts.map +0 -1
  80. package/dist/src/tb-proxyd.js.map +0 -1
  81. package/dist/src/terminal-detect.d.ts.map +0 -1
  82. package/dist/src/terminal-detect.js.map +0 -1
  83. package/dist/src/terminal-image.d.ts.map +0 -1
  84. package/dist/src/terminal-image.js.map +0 -1
  85. package/src/buyer-store.ts +0 -1090
  86. package/src/clawtip-bootstrap.ts +0 -65
  87. package/src/cli.ts +0 -2243
  88. package/src/credit-tracker.ts +0 -295
  89. package/src/daemon.ts +0 -5475
  90. package/src/doctor-clawtip-wallet.ts +0 -95
  91. package/src/doctor-diagnostics.ts +0 -1026
  92. package/src/index.ts +0 -16
  93. package/src/init-clawtip-activation.ts +0 -695
  94. package/src/init-payment-options.ts +0 -373
  95. package/src/init-setup.ts +0 -165
  96. package/src/model-index.ts +0 -278
  97. package/src/package-update.ts +0 -311
  98. package/src/prewarm-cache.ts +0 -485
  99. package/src/prewarm-scheduler.ts +0 -675
  100. package/src/provider-install.ts +0 -1006
  101. package/src/provider-routing-config.ts +0 -410
  102. package/src/registry-trust.ts +0 -51
  103. package/src/route-failover.ts +0 -304
  104. package/src/seller-catalog.ts +0 -505
  105. package/src/seller-concurrency-limiter.ts +0 -161
  106. package/src/seller-metadata-cache.ts +0 -91
  107. package/src/seller-pool.ts +0 -557
  108. package/src/seller-route-planner.ts +0 -513
  109. package/src/seller-routing-config.ts +0 -211
  110. package/src/seller-routing-strategy.ts +0 -362
  111. package/src/stream-failover.ts +0 -152
  112. package/src/tb-clawtip-proof.ts +0 -28
  113. package/src/tb-proxyd.ts +0 -101
  114. package/src/terminal-detect.ts +0 -333
  115. package/src/terminal-image.ts +0 -228
  116. package/static/ui/assets/index-0MVXD7bH.css +0 -1
  117. package/static/ui/assets/index-BVbeDEwq.js +0 -271
  118. package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
  119. package/tests/cli-routing.test.ts +0 -363
  120. package/tests/control-plane-ui-endpoints.test.ts +0 -1630
  121. package/tests/credit-tracker.test.ts +0 -165
  122. package/tests/daemon-413-fallback.test.ts +0 -92
  123. package/tests/daemon-classify.test.ts +0 -452
  124. package/tests/daemon-roles.test.ts +0 -92
  125. package/tests/daemon-trusted-registry-cache.test.ts +0 -132
  126. package/tests/e2e.test.ts +0 -366
  127. package/tests/image-generation-e2e.test.ts +0 -230
  128. package/tests/model-index.test.ts +0 -198
  129. package/tests/package-update.test.ts +0 -147
  130. package/tests/prewarm-cache.test.ts +0 -296
  131. package/tests/prewarm-scheduler.test.ts +0 -367
  132. package/tests/provider-routing-config.test.ts +0 -150
  133. package/tests/registry-trust.test.ts +0 -28
  134. package/tests/route-failover.test.ts +0 -222
  135. package/tests/seller-catalog-413.test.ts +0 -120
  136. package/tests/seller-catalog-utilities.test.ts +0 -124
  137. package/tests/seller-concurrency-limiter.test.ts +0 -83
  138. package/tests/seller-metadata-cache.test.ts +0 -89
  139. package/tests/seller-pool.test.ts +0 -365
  140. package/tests/seller-route-planner.test.ts +0 -312
  141. package/tests/seller-routing-config.test.ts +0 -124
  142. package/tests/seller-routing-strategy.test.ts +0 -167
  143. package/tests/stream-failover.test.ts +0 -52
  144. package/tests/thousand-seller.test.ts +0 -151
  145. package/tests/tokenbuddy.test.ts +0 -4043
  146. package/tsconfig.json +0 -8
@@ -1,513 +0,0 @@
1
- import { isBuyerVisibleRegistrySeller, type RegistrySeller } from "./seller-catalog.js";
2
- import {
3
- planSellerRoutes,
4
- type RoutingCandidate,
5
- type SellerRoutingPlan,
6
- type SellerRoutingStrategyConfig
7
- } from "./seller-routing-strategy.js";
8
-
9
- /**
10
- * `planSellerRouteSet` 候选来源:走了 prewarm cache 还是回退到 registry 顺序。
11
- */
12
- export type SellerRouteSource = "prewarm_cache" | "registry_fallback";
13
- /** 路由层使用的熔断状态枚举,与 `seller-pool.CircuitState` 语义一致。 */
14
- export type SellerCircuitState = "closed" | "half_open" | "open";
15
-
16
- /**
17
- * 路由规划器在合并多个数据源时使用的"实时指标"维度。
18
- * 由 `SellerPool` 快照聚合;可空字段表示该维度暂未采集。
19
- */
20
- export interface SellerRouteMetric {
21
- /** seller ID */
22
- sellerId: string;
23
- /** 综合健康分 0-100,可选 */
24
- healthScore?: number;
25
- /** 平均延迟(毫秒),可选 */
26
- avgLatencyMs?: number;
27
- /** TTFT(毫秒),可选 */
28
- ttftMs?: number;
29
- /** 平均推理延迟(毫秒),可选 */
30
- avgInferenceMs?: number;
31
- /** 最近 10 分钟窗口内的平均输出吞吐(tokens/s),可选 */
32
- avgTokensPerSecond?: number;
33
- /** 折扣系数(0-1),可选;缺省时 scoring 视为"无折扣信息" */
34
- discountRatio?: number;
35
- /** 当前熔断状态,可选;`open` 的 seller 直接被剔除候选 */
36
- circuit?: SellerCircuitState;
37
- /** 临时容量避让截止时间;大于当前时间时直接剔除候选 */
38
- capacityBlockedUntil?: number;
39
- /** 当前 `tb-proxyd` 进程内该 seller 的活跃 lease 数。 */
40
- localConcurrencyActive?: number;
41
- /** 当前 `tb-proxyd` 进程内该 seller 的最大活跃 lease 数。 */
42
- localConcurrencyLimit?: number;
43
- }
44
-
45
- /**
46
- * 路由规划器视角的 prewarm 候选:比 `PrewarmCandidate` 少调度内部状态,只保留打分需要的字段。
47
- */
48
- export interface SellerRoutePrewarmCandidate {
49
- /** seller ID */
50
- sellerId: string;
51
- /** seller URL(去尾部斜杠) */
52
- url: string;
53
- /** 综合健康分,可选 */
54
- healthScore?: number;
55
- /** 平均延迟(毫秒),可选 */
56
- avgLatencyMs?: number;
57
- /** TTFT(毫秒),可选 */
58
- ttftMs?: number;
59
- /** 平均推理延迟(毫秒),可选 */
60
- avgInferenceMs?: number;
61
- /** 最近 10 分钟窗口内的平均输出吞吐(tokens/s),可选 */
62
- avgTokensPerSecond?: number;
63
- }
64
-
65
- /**
66
- * 路由规划器视角的 seller 元数据:折扣系数和 manifest 刷新时间。
67
- * 用于在 `discount` 排序时把"无折扣"和"折扣已知"区分开。
68
- */
69
- export interface SellerRouteMetadata {
70
- sellerId: string;
71
- discountRatio?: number;
72
- manifestVersion?: string;
73
- lastRefreshAt?: number;
74
- source: "manifest_selection";
75
- errorMessage?: string;
76
- }
77
-
78
- /**
79
- * `planSellerRouteSet()` 的入参:把"模型 + 协议 + 支付" + 三类数据源(registry / prewarm / 指标)+ 路由策略打包传入。
80
- */
81
- export interface SellerRoutePlannerInput {
82
- /** 目标模型 ID */
83
- modelId: string;
84
- /** 协议偏好(`chat_completions` / `responses` / `messages`) */
85
- protocol: string;
86
- /** 支付方式(`mock` / `clawtip`) */
87
- paymentMethod: string;
88
- /** seller 注册表快照 */
89
- registrySellers: RegistrySeller[];
90
- /** buyer 路由策略配置(fixed / fixedSet / fullAuto + 评分器) */
91
- routing: SellerRoutingStrategyConfig;
92
- /** 来自 prewarm cache 的候选(可选;提供时优先使用) */
93
- prewarmCandidates?: SellerRoutePrewarmCandidate[];
94
- /** seller 实时指标(可选;`circuit=open` 的 seller 被剔除) */
95
- sellerMetrics?: SellerRouteMetric[];
96
- /** 当前时间戳,用于判断容量避让窗口;默认 `Date.now()` */
97
- now?: number;
98
- }
99
-
100
- /**
101
- * 单条规划后的路由:含 seller 描述、URL、聚合后的指标。
102
- */
103
- export interface PlannedSellerRoute {
104
- /** seller 注册表描述(用于后续转发) */
105
- seller: RegistrySeller;
106
- /** seller URL(去尾部斜杠) */
107
- url: string;
108
- /** 聚合指标:健康分、平均延迟、折扣、registry 声明顺序 */
109
- metrics: {
110
- healthScore?: number;
111
- avgLatencyMs?: number;
112
- ttftMs?: number;
113
- avgInferenceMs?: number;
114
- avgTokensPerSecond?: number;
115
- discountRatio?: number;
116
- /** 在 registry 里的声明顺序(0-based,tie-breaker) */
117
- registryOrder: number;
118
- };
119
- }
120
-
121
- /**
122
- * `planSellerRouteSet()` 的返回结果:路由列表 + 来源 + 决策原因 + 模式 / 评分器。
123
- */
124
- export interface SellerRoutePlan {
125
- /** 已排好序的可选 seller 列表(按路由策略打分) */
126
- routes: PlannedSellerRoute[];
127
- /** 候选来源:`prewarm_cache` 或 `registry_fallback` */
128
- source: SellerRouteSource;
129
- /** 候选来源的细化原因(用于诊断 / 稳定事件断言) */
130
- sourceReason: string;
131
- /** 路由策略输出的整体决策原因 */
132
- reason: string;
133
- /** 路由模式(`fixed` / `fixedSet` / `fullAuto`) */
134
- mode: SellerRoutingPlan["mode"];
135
- /** 使用的评分器(`speed` / `discount` / `balanced`) */
136
- scorer: SellerRoutingPlan["scorer"];
137
- /** 候选总数(prewarm 阶段剔除了不支持模型 / 协议 / 支付的 seller 之前) */
138
- candidateCount: number;
139
- /** 候选构造与排除摘要,用于日志解释本轮决策为什么只剩这些 seller。 */
140
- diagnostics: SellerRouteDiagnostics;
141
- }
142
-
143
- export interface SellerRouteDiagnostics {
144
- registryVisibleCount: number;
145
- prewarmCandidateCount: number;
146
- prewarmUsableCount: number;
147
- prewarmMissingSellerIds: string[];
148
- prewarmBlockedSellerIds: string[];
149
- prewarmIncompatibleSellerIds: string[];
150
- sourceCandidateCount: number;
151
- blockedOpenCircuitCount: number;
152
- blockedCapacityCount: number;
153
- blockedLocalConcurrencyCount: number;
154
- blockedSellerIds: string[];
155
- incompatibleCount: number;
156
- incompatibleSellerIds: string[];
157
- }
158
-
159
- interface CandidateSourceResult {
160
- source: SellerRouteSource;
161
- sourceReason: string;
162
- candidates: RoutingCandidate[];
163
- incompatibleSellerIds: string[];
164
- prewarmDiagnostics: PrewarmSourceDiagnostics;
165
- }
166
-
167
- interface PrewarmSourceDiagnostics {
168
- usableCount: number;
169
- missingSellerIds: string[];
170
- blockedSellerIds: string[];
171
- incompatibleSellerIds: string[];
172
- }
173
-
174
- interface IndexedSeller {
175
- seller: RegistrySeller;
176
- registryOrder: number;
177
- }
178
-
179
- interface MetricIndex {
180
- bySellerId: Map<string, SellerRouteMetric>;
181
- blockedSellerIds: Set<string>;
182
- blockedOpenCircuitSellerIds: Set<string>;
183
- blockedCapacitySellerIds: Set<string>;
184
- blockedLocalConcurrencySellerIds: Set<string>;
185
- }
186
-
187
- /**
188
- * 给定 (model, protocol, payment) + 三类数据源,规划出有序的 seller 路由列表。
189
- * 决策流程:
190
- * 1. 索引 registry sellers(按声明顺序)。
191
- * 2. 索引 `sellerMetrics`,把 `circuit=open` 的 seller 标记为剔除。
192
- * 3. 优先从 `prewarmCandidates` 构造候选;都不可用时回退 registry 顺序。
193
- * 4. 调 `planSellerRoutes`(`seller-routing-strategy`)做策略评分。
194
- * 5. 把 `RoutingCandidate` 转回 `PlannedSellerRoute`,加上 `registryOrder`。
195
- *
196
- * @param input 规划入参
197
- * @returns 规划后的路由计划
198
- */
199
- export function planSellerRouteSet(input: SellerRoutePlannerInput): SellerRoutePlan {
200
- const indexed = indexRegistrySellers(input.registrySellers);
201
- const metrics = indexMetrics(input.sellerMetrics, input.now ?? Date.now());
202
- const source = chooseCandidateSource(input, indexed, metrics);
203
- const strategyPlan = planSellerRoutes(source.candidates, input.routing);
204
- const routes = strategyPlan.routes.map((candidate) => {
205
- const seller = indexed.bySellerId.get(candidate.sellerId)?.seller;
206
- if (!seller) {
207
- throw new Error(`planned seller ${candidate.sellerId} is missing from registry index`);
208
- }
209
- return {
210
- seller,
211
- url: candidate.url,
212
- metrics: {
213
- healthScore: candidate.healthScore,
214
- avgLatencyMs: candidate.avgLatencyMs,
215
- ttftMs: candidate.ttftMs,
216
- avgInferenceMs: candidate.avgInferenceMs,
217
- avgTokensPerSecond: candidate.avgTokensPerSecond,
218
- discountRatio: candidate.discountRatio,
219
- registryOrder: candidate.registryOrder
220
- }
221
- };
222
- });
223
-
224
- return {
225
- routes,
226
- source: source.source,
227
- sourceReason: source.sourceReason,
228
- reason: strategyPlan.reason,
229
- mode: strategyPlan.mode,
230
- scorer: strategyPlan.scorer,
231
- candidateCount: source.candidates.length,
232
- diagnostics: buildDiagnostics(input, indexed, metrics, source)
233
- };
234
- }
235
-
236
- function chooseCandidateSource(
237
- input: SellerRoutePlannerInput,
238
- indexed: ReturnType<typeof indexRegistrySellers>,
239
- metrics: MetricIndex
240
- ): CandidateSourceResult {
241
- const prewarm = input.prewarmCandidates ?? [];
242
- let prewarmDiagnostics: PrewarmSourceDiagnostics = emptyPrewarmDiagnostics();
243
- const prewarmBySellerId = new Map(prewarm.map((candidate) => [candidate.sellerId, candidate]));
244
- if (prewarm.length > 0) {
245
- const missingSellerIds: string[] = [];
246
- const blockedSellerIds: string[] = [];
247
- const prewarmCandidatesBeforeCompatibility = prewarm
248
- .map((candidate) => {
249
- const indexedSeller = indexed.bySellerId.get(candidate.sellerId);
250
- if (!indexedSeller) {
251
- missingSellerIds.push(candidate.sellerId);
252
- return undefined;
253
- }
254
- if (metrics.blockedSellerIds.has(indexedSeller.seller.id)) {
255
- blockedSellerIds.push(candidate.sellerId);
256
- return undefined;
257
- }
258
- return buildCandidate({
259
- seller: indexedSeller.seller,
260
- registryOrder: indexedSeller.registryOrder,
261
- modelId: input.modelId,
262
- protocol: input.protocol,
263
- paymentMethod: input.paymentMethod,
264
- metric: mergeMetric(metrics.bySellerId.get(candidate.sellerId), candidate)
265
- });
266
- })
267
- .filter((candidate): candidate is RoutingCandidate => Boolean(candidate));
268
- const prewarmCandidates = prewarmCandidatesBeforeCompatibility.filter(isSelectableCandidate);
269
- prewarmDiagnostics = {
270
- usableCount: prewarmCandidates.length,
271
- missingSellerIds,
272
- blockedSellerIds,
273
- incompatibleSellerIds: incompatibleSellerIds(prewarmCandidatesBeforeCompatibility)
274
- };
275
-
276
- if (prewarmCandidates.length > 0) {
277
- const registryCandidatesBeforeCompatibility = buildRegistryCandidates({
278
- input,
279
- indexed,
280
- metrics,
281
- prewarmBySellerId
282
- });
283
- return {
284
- source: "prewarm_cache",
285
- sourceReason: "prewarm_metrics_merged_with_registry",
286
- candidates: registryCandidatesBeforeCompatibility.filter(isSelectableCandidate),
287
- incompatibleSellerIds: incompatibleSellerIds(registryCandidatesBeforeCompatibility),
288
- prewarmDiagnostics
289
- };
290
- }
291
- }
292
-
293
- const registryCandidatesBeforeCompatibility = buildRegistryCandidates({
294
- input,
295
- indexed,
296
- metrics,
297
- prewarmBySellerId
298
- });
299
-
300
- return {
301
- source: "registry_fallback",
302
- sourceReason: prewarm.length > 0 ? "prewarm_no_compatible_candidates" : "prewarm_missing",
303
- candidates: registryCandidatesBeforeCompatibility.filter(isSelectableCandidate),
304
- incompatibleSellerIds: incompatibleSellerIds(registryCandidatesBeforeCompatibility),
305
- prewarmDiagnostics
306
- };
307
- }
308
-
309
- function buildRegistryCandidates(input: {
310
- input: SellerRoutePlannerInput;
311
- indexed: ReturnType<typeof indexRegistrySellers>;
312
- metrics: MetricIndex;
313
- prewarmBySellerId: Map<string, SellerRoutePrewarmCandidate>;
314
- }): RoutingCandidate[] {
315
- return input.indexed.ordered
316
- .filter((entry) => !input.metrics.blockedSellerIds.has(entry.seller.id))
317
- .map((entry) => buildCandidate({
318
- seller: entry.seller,
319
- registryOrder: entry.registryOrder,
320
- modelId: input.input.modelId,
321
- protocol: input.input.protocol,
322
- paymentMethod: input.input.paymentMethod,
323
- metric: mergeOptionalMetric(
324
- input.metrics.bySellerId.get(entry.seller.id),
325
- input.prewarmBySellerId.get(entry.seller.id)
326
- )
327
- }));
328
- }
329
-
330
- function buildCandidate(input: {
331
- seller: RegistrySeller;
332
- registryOrder: number;
333
- modelId: string;
334
- protocol: string;
335
- paymentMethod: string;
336
- metric?: SellerRouteMetric;
337
- }): RoutingCandidate {
338
- return {
339
- sellerId: input.seller.id,
340
- url: trimTrailingSlashes(input.seller.url),
341
- supportsModel: sellerSupportsModel(input.seller, input.modelId),
342
- supportsProtocol: sellerSupportsProtocol(input.seller, input.protocol),
343
- supportsPayment: sellerSupportsPayment(input.seller, input.paymentMethod),
344
- healthScore: input.metric?.healthScore,
345
- avgLatencyMs: input.metric?.avgLatencyMs,
346
- ttftMs: input.metric?.ttftMs,
347
- avgInferenceMs: input.metric?.avgInferenceMs,
348
- avgTokensPerSecond: input.metric?.avgTokensPerSecond,
349
- discountRatio: input.metric?.discountRatio,
350
- registryOrder: input.registryOrder
351
- };
352
- }
353
-
354
- function isSelectableCandidate(candidate: RoutingCandidate): boolean {
355
- return candidate.supportsModel && candidate.supportsProtocol && candidate.supportsPayment;
356
- }
357
-
358
- function indexRegistrySellers(sellers: RegistrySeller[]): {
359
- ordered: IndexedSeller[];
360
- bySellerId: Map<string, IndexedSeller>;
361
- } {
362
- const ordered = sellers
363
- .filter((seller) => Boolean(seller?.id && seller.url))
364
- .filter((seller) => isBuyerVisibleRegistrySeller(seller))
365
- .map((seller, registryOrder) => ({ seller, registryOrder }));
366
- return {
367
- ordered,
368
- bySellerId: new Map(ordered.map((entry) => [entry.seller.id, entry]))
369
- };
370
- }
371
-
372
- function indexMetrics(metrics: SellerRouteMetric[] | undefined, now: number): MetricIndex {
373
- const blockedOpenCircuitSellerIds = new Set((metrics ?? [])
374
- .filter((metric) => metric.circuit === "open")
375
- .map((metric) => metric.sellerId));
376
- const blockedCapacitySellerIds = new Set((metrics ?? [])
377
- .filter((metric) => isCapacityBlocked(metric, now))
378
- .map((metric) => metric.sellerId));
379
- const blockedLocalConcurrencySellerIds = new Set((metrics ?? [])
380
- .filter(isLocalConcurrencyBlocked)
381
- .map((metric) => metric.sellerId));
382
- const blockedSellerIds = new Set([
383
- ...blockedOpenCircuitSellerIds,
384
- ...blockedCapacitySellerIds,
385
- ...blockedLocalConcurrencySellerIds
386
- ]);
387
- return {
388
- bySellerId: new Map((metrics ?? [])
389
- .filter((metric) => !blockedSellerIds.has(metric.sellerId))
390
- .map((metric) => [metric.sellerId, metric])),
391
- blockedSellerIds,
392
- blockedOpenCircuitSellerIds,
393
- blockedCapacitySellerIds,
394
- blockedLocalConcurrencySellerIds
395
- };
396
- }
397
-
398
- function buildDiagnostics(
399
- input: SellerRoutePlannerInput,
400
- indexed: ReturnType<typeof indexRegistrySellers>,
401
- metrics: MetricIndex,
402
- source: CandidateSourceResult
403
- ): SellerRouteDiagnostics {
404
- const visibleOpenBlocked = Array.from(metrics.blockedOpenCircuitSellerIds)
405
- .filter((sellerId) => indexed.bySellerId.has(sellerId));
406
- const visibleCapacityBlocked = Array.from(metrics.blockedCapacitySellerIds)
407
- .filter((sellerId) => indexed.bySellerId.has(sellerId));
408
- const visibleLocalConcurrencyBlocked = Array.from(metrics.blockedLocalConcurrencySellerIds)
409
- .filter((sellerId) => indexed.bySellerId.has(sellerId));
410
- const visibleBlockedSellerIds = Array.from(new Set([
411
- ...visibleOpenBlocked,
412
- ...visibleCapacityBlocked,
413
- ...visibleLocalConcurrencyBlocked
414
- ])).sort();
415
- return {
416
- registryVisibleCount: indexed.ordered.length,
417
- prewarmCandidateCount: input.prewarmCandidates?.length ?? 0,
418
- prewarmUsableCount: source.prewarmDiagnostics.usableCount,
419
- prewarmMissingSellerIds: [...source.prewarmDiagnostics.missingSellerIds].sort(),
420
- prewarmBlockedSellerIds: [...source.prewarmDiagnostics.blockedSellerIds].sort(),
421
- prewarmIncompatibleSellerIds: [...source.prewarmDiagnostics.incompatibleSellerIds].sort(),
422
- sourceCandidateCount: source.candidates.length,
423
- blockedOpenCircuitCount: visibleOpenBlocked.length,
424
- blockedCapacityCount: visibleCapacityBlocked.length,
425
- blockedLocalConcurrencyCount: visibleLocalConcurrencyBlocked.length,
426
- blockedSellerIds: visibleBlockedSellerIds,
427
- incompatibleCount: source.incompatibleSellerIds.length,
428
- incompatibleSellerIds: [...source.incompatibleSellerIds].sort()
429
- };
430
- }
431
-
432
- function incompatibleSellerIds(candidates: RoutingCandidate[]): string[] {
433
- return candidates
434
- .filter((candidate) => !isSelectableCandidate(candidate))
435
- .map((candidate) => candidate.sellerId);
436
- }
437
-
438
- function emptyPrewarmDiagnostics(): PrewarmSourceDiagnostics {
439
- return {
440
- usableCount: 0,
441
- missingSellerIds: [],
442
- blockedSellerIds: [],
443
- incompatibleSellerIds: []
444
- };
445
- }
446
-
447
- function mergeMetric(
448
- metric: SellerRouteMetric | undefined,
449
- prewarm: SellerRoutePrewarmCandidate
450
- ): SellerRouteMetric {
451
- return {
452
- sellerId: prewarm.sellerId,
453
- healthScore: prewarm.healthScore ?? metric?.healthScore,
454
- avgLatencyMs: prewarm.avgLatencyMs ?? metric?.avgLatencyMs,
455
- ttftMs: metric?.ttftMs ?? prewarm.ttftMs,
456
- avgInferenceMs: metric?.avgInferenceMs ?? prewarm.avgInferenceMs,
457
- avgTokensPerSecond: metric?.avgTokensPerSecond ?? prewarm.avgTokensPerSecond,
458
- discountRatio: metric?.discountRatio,
459
- circuit: metric?.circuit,
460
- capacityBlockedUntil: metric?.capacityBlockedUntil,
461
- localConcurrencyActive: metric?.localConcurrencyActive,
462
- localConcurrencyLimit: metric?.localConcurrencyLimit
463
- };
464
- }
465
-
466
- function mergeOptionalMetric(
467
- metric: SellerRouteMetric | undefined,
468
- prewarm: SellerRoutePrewarmCandidate | undefined
469
- ): SellerRouteMetric | undefined {
470
- if (!prewarm) {
471
- return metric;
472
- }
473
- return mergeMetric(metric, prewarm);
474
- }
475
-
476
- function isCapacityBlocked(metric: SellerRouteMetric, now: number): boolean {
477
- return Number.isFinite(metric.capacityBlockedUntil) && (metric.capacityBlockedUntil as number) > now;
478
- }
479
-
480
- function isLocalConcurrencyBlocked(metric: SellerRouteMetric): boolean {
481
- return Number.isFinite(metric.localConcurrencyActive) &&
482
- Number.isFinite(metric.localConcurrencyLimit) &&
483
- (metric.localConcurrencyActive as number) >= (metric.localConcurrencyLimit as number);
484
- }
485
-
486
- function sellerSupportsModel(seller: RegistrySeller, modelId: string): boolean {
487
- const normalized = normalizeLookupValue(modelId);
488
- return (seller.models ?? []).some((model) => normalizeLookupValue(model) === normalized);
489
- }
490
-
491
- function sellerSupportsProtocol(seller: RegistrySeller, protocol: string): boolean {
492
- const normalized = normalizeLookupValue(protocol);
493
- return protocolAliases(seller.supportedProtocols ?? []).some((entry) => normalizeLookupValue(entry) === normalized);
494
- }
495
-
496
- function sellerSupportsPayment(seller: RegistrySeller, paymentMethod: string): boolean {
497
- const normalized = normalizeLookupValue(paymentMethod);
498
- return (seller.paymentMethods ?? []).some((entry) => normalizeLookupValue(entry) === normalized);
499
- }
500
-
501
- function protocolAliases(protocols: string[]): string[] {
502
- return protocols.includes("anthropic_messages") && !protocols.includes("messages")
503
- ? [...protocols, "messages"]
504
- : protocols;
505
- }
506
-
507
- function normalizeLookupValue(value: string): string {
508
- return value.trim().toLowerCase();
509
- }
510
-
511
- function trimTrailingSlashes(value: string): string {
512
- return value.replace(/\/+$/, "");
513
- }