@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.
- 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
package/src/seller-catalog.ts
DELETED
|
@@ -1,505 +0,0 @@
|
|
|
1
|
-
import { createModuleLogger } from "@tokenbuddy/logging";
|
|
2
|
-
import * as crypto from "crypto";
|
|
3
|
-
import {
|
|
4
|
-
shouldVerifyRegistry,
|
|
5
|
-
signatureUrlForRegistryUrl,
|
|
6
|
-
verifyTrustedRegistrySignature
|
|
7
|
-
} from "./registry-trust.js";
|
|
8
|
-
|
|
9
|
-
const logger = createModuleLogger("tb-proxyd");
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* buyer 端协议偏好(用于按协议过滤 catalog)。`messages` 是 anthropic 协议的简称。
|
|
13
|
-
*/
|
|
14
|
-
export type ProtocolPreference = "chat_completions" | "responses" | "messages" | "images_generations";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* wallet-bootstrap `/registry/sellers` 里的 seller 描述。
|
|
18
|
-
* v1.2 起 `models` 是 buyer model-index 路由的权威依据;缺省会被报告为
|
|
19
|
-
* `models_refresh.seller_missing_models` 事件。
|
|
20
|
-
*/
|
|
21
|
-
export interface RegistrySeller {
|
|
22
|
-
/** seller 全局唯一 ID(也作为 token class) */
|
|
23
|
-
id: string;
|
|
24
|
-
/** 人类可读名称 */
|
|
25
|
-
name?: string;
|
|
26
|
-
/** registry 发布状态;只有 `active` 参与 buyer 自动路由,缺省兼容旧 registry 为可用 */
|
|
27
|
-
status?: string;
|
|
28
|
-
/** seller 服务的公网 URL(去掉尾部 `/`) */
|
|
29
|
-
url: string;
|
|
30
|
-
/** seller 支持的协议列表(包含 `anthropic_messages` 时内部 alias 到 `messages`) */
|
|
31
|
-
supportedProtocols?: string[];
|
|
32
|
-
/** seller 支持的支付方式 */
|
|
33
|
-
paymentMethods?: string[];
|
|
34
|
-
/**
|
|
35
|
-
* v1.2: authoritative model list for buyer-side model-index routing.
|
|
36
|
-
* Optional at the buyer boundary for backward compatibility with older
|
|
37
|
-
* registry payloads, but the upstream wallet-bootstrap registry schema
|
|
38
|
-
* requires it. Missing entries are reported via `models_refresh.seller_missing_models`.
|
|
39
|
-
*/
|
|
40
|
-
models?: string[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* `/registry/sellers` 响应的最小形状(去掉了 buyer 不关心的 metadata)。
|
|
45
|
-
*/
|
|
46
|
-
export interface SellerRegistryDocument {
|
|
47
|
-
/** schema 版本号 */
|
|
48
|
-
version: number;
|
|
49
|
-
/** 默认 seller ID(buyer 未指定时回落的目标) */
|
|
50
|
-
defaultSeller?: string;
|
|
51
|
-
/** seller 列表 */
|
|
52
|
-
sellers: RegistrySeller[];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface SellerRegistryTrustMetadata {
|
|
56
|
-
registryUrl: string;
|
|
57
|
-
registrySha256: string;
|
|
58
|
-
verified: boolean;
|
|
59
|
-
signatureUrl?: string;
|
|
60
|
-
signature?: string;
|
|
61
|
-
signingKeyId?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface FetchedSellerRegistry {
|
|
65
|
-
registry: SellerRegistryDocument;
|
|
66
|
-
registryJson: string;
|
|
67
|
-
trust: SellerRegistryTrustMetadata;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Buyer 自动路由 / 模型目录可见性门禁。
|
|
72
|
-
* 新 registry 会显式写 `status`,只有 `active` 参与 buyer 可见路径;
|
|
73
|
-
* 旧 registry 缺省 status 时按历史行为保留可用,避免升级后隐藏存量节点。
|
|
74
|
-
*
|
|
75
|
-
* @param seller registry seller
|
|
76
|
-
* @returns seller 是否应进入 buyer 路由和模型目录
|
|
77
|
-
*/
|
|
78
|
-
export function isBuyerVisibleRegistrySeller(seller: RegistrySeller): boolean {
|
|
79
|
-
const status = seller.status?.trim().toLowerCase();
|
|
80
|
-
return !status || status === "active";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 单个 seller 的 `/manifest` 响应,兼容 snake_case 与 camelCase 字段。
|
|
85
|
-
*/
|
|
86
|
-
export interface SellerManifest {
|
|
87
|
-
/** manifest schema 版本(camelCase) */
|
|
88
|
-
manifestVersion?: string;
|
|
89
|
-
/** manifest schema 版本(snake_case 兼容) */
|
|
90
|
-
manifest_version?: string;
|
|
91
|
-
/** seller 实例 ID(camelCase) */
|
|
92
|
-
sellerId?: string;
|
|
93
|
-
/** seller 实例 ID(snake_case 兼容) */
|
|
94
|
-
seller_id?: string;
|
|
95
|
-
/** seller 支持的协议(camelCase) */
|
|
96
|
-
supportedProtocols?: string[];
|
|
97
|
-
/** seller 支持的协议(snake_case 兼容) */
|
|
98
|
-
supported_protocols?: string[];
|
|
99
|
-
/** seller 支持的支付方式(camelCase) */
|
|
100
|
-
paymentMethods?: string[];
|
|
101
|
-
/** seller 支持的支付方式(snake_case 兼容) */
|
|
102
|
-
payment_methods?: string[];
|
|
103
|
-
/** 模型清单(含价格) */
|
|
104
|
-
models?: ManifestModelRecord[];
|
|
105
|
-
/** 选择策略块(折扣等) */
|
|
106
|
-
selection?: {
|
|
107
|
-
/** 折扣系数(camelCase) */
|
|
108
|
-
discountRatio?: number;
|
|
109
|
-
/** 折扣系数(snake_case 兼容) */
|
|
110
|
-
discount_ratio?: number;
|
|
111
|
-
/** 服务手续费系数(camelCase) */
|
|
112
|
-
serviceFeeRatio?: number;
|
|
113
|
-
/** 服务手续费系数(snake_case 兼容) */
|
|
114
|
-
service_fee_ratio?: number;
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* `/manifest` 响应里单个模型记录(兼容 snake_case)。
|
|
120
|
-
*/
|
|
121
|
-
export interface ManifestModelRecord {
|
|
122
|
-
/** 模型 ID */
|
|
123
|
-
id: string;
|
|
124
|
-
/** 输入价格 USD micros/1M(camelCase) */
|
|
125
|
-
inputPriceMicrosPer1m?: number;
|
|
126
|
-
/** 输出价格 USD micros/1M(camelCase) */
|
|
127
|
-
outputPriceMicrosPer1m?: number;
|
|
128
|
-
/** 输入价格 USD micros/1M(snake_case 兼容) */
|
|
129
|
-
input_price_micros_per_1m?: number;
|
|
130
|
-
/** 输出价格 USD micros/1M(snake_case 兼容) */
|
|
131
|
-
output_price_micros_per_1m?: number;
|
|
132
|
-
/** 其它扩展字段(保留以便透传) */
|
|
133
|
-
[key: string]: unknown;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* 聚合后的模型目录条目(catalog 渲染 / provider install 选择的最小可消费形状)。
|
|
138
|
-
*/
|
|
139
|
-
export interface ModelCatalogEntry {
|
|
140
|
-
/** 模型 ID */
|
|
141
|
-
id: string;
|
|
142
|
-
/** 所属 seller ID */
|
|
143
|
-
sellerId: string;
|
|
144
|
-
/** seller 名称(可选) */
|
|
145
|
-
sellerName?: string;
|
|
146
|
-
/** seller 公网 URL(去尾 `/`) */
|
|
147
|
-
sellerUrl: string;
|
|
148
|
-
/** seller 支持的协议 */
|
|
149
|
-
supportedProtocols: string[];
|
|
150
|
-
/** seller 支持的支付方式 */
|
|
151
|
-
paymentMethods: string[];
|
|
152
|
-
/** 输入价格 USD micros/1M */
|
|
153
|
-
inputPriceMicrosPer1m?: number;
|
|
154
|
-
/** 输出价格 USD micros/1M */
|
|
155
|
-
outputPriceMicrosPer1m?: number;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* seller 目录条目(聚合 seller 元信息 + manifest 拉取结果)。
|
|
160
|
-
* 用于 `tb doctor` 和 CLI 表格展示。
|
|
161
|
-
*/
|
|
162
|
-
export interface SellerCatalogEntry {
|
|
163
|
-
/** seller ID */
|
|
164
|
-
id: string;
|
|
165
|
-
/** 人类可读名称 */
|
|
166
|
-
name?: string;
|
|
167
|
-
/** seller 公网 URL(去尾 `/`) */
|
|
168
|
-
url: string;
|
|
169
|
-
/** 当前状态(`active` / `error` / `manifest_unavailable`) */
|
|
170
|
-
status: string;
|
|
171
|
-
/** manifest 报告的 sellerId(可能与 id 不同,跨 namespace 时有用) */
|
|
172
|
-
manifestSellerId?: string;
|
|
173
|
-
/** 折扣系数(来自 manifest.selection) */
|
|
174
|
-
discountRatio?: number;
|
|
175
|
-
/** 服务手续费系数(来自 manifest.selection) */
|
|
176
|
-
serviceFeeRatio?: number;
|
|
177
|
-
/** 最近一次 TTFT(毫秒),来自本地 seller pool 运行时指标 */
|
|
178
|
-
ttftMs?: number;
|
|
179
|
-
/** 最近 10 分钟窗口内的平均输出吞吐(tokens/s),来自本地 seller pool 运行时指标 */
|
|
180
|
-
avgTokensPerSecond?: number;
|
|
181
|
-
/** 模型数(来自 manifest) */
|
|
182
|
-
modelCount?: number;
|
|
183
|
-
/** seller 支持的协议(manifest > registry fallback) */
|
|
184
|
-
supportedProtocols?: string[];
|
|
185
|
-
/** seller 支持的支付方式(manifest > registry fallback) */
|
|
186
|
-
paymentMethods?: string[];
|
|
187
|
-
/** manifest 拉取失败时的错误消息 */
|
|
188
|
-
errorMessage?: string;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* `discoverSellerBackedModels` 的返回结果。
|
|
193
|
-
* 包含聚合后的 `models[]` 和 `sellers[]`,供 provider install / catalog 渲染消费。
|
|
194
|
-
*/
|
|
195
|
-
export interface SellerCatalogResult {
|
|
196
|
-
/** 拉取的 registry URL(去尾 `/`) */
|
|
197
|
-
registryUrl: string;
|
|
198
|
-
/** registry schema 版本 */
|
|
199
|
-
version: number;
|
|
200
|
-
/** 默认 seller ID(来自 registry) */
|
|
201
|
-
defaultSeller?: string;
|
|
202
|
-
/** fetched registry document */
|
|
203
|
-
registry?: SellerRegistryDocument;
|
|
204
|
-
/** fetched registry bytes used for hashing/signature verification */
|
|
205
|
-
registryJson?: string;
|
|
206
|
-
/** 模型目录条目列表(去重后) */
|
|
207
|
-
models: ModelCatalogEntry[];
|
|
208
|
-
/** seller 元信息列表(拉取 manifest 后的快照) */
|
|
209
|
-
sellers: SellerCatalogEntry[];
|
|
210
|
-
/** registry trust metadata for the fetched registry document */
|
|
211
|
-
registryTrust?: SellerRegistryTrustMetadata;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* 去掉 seller URL 尾部的 `/`,避免路径拼接时出现 `//v1/messages`。
|
|
216
|
-
*
|
|
217
|
-
* @param seller registry seller
|
|
218
|
-
* @returns 规范化后的 URL
|
|
219
|
-
*/
|
|
220
|
-
export function normalizeSellerUrl(seller: RegistrySeller): string {
|
|
221
|
-
return seller.url.replace(/\/+$/, "");
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* 解析 seller 实际支持的协议列表。
|
|
226
|
-
* 优先级:manifest.camelCase > manifest.snake_case > registry fallback。
|
|
227
|
-
* 包含 `anthropic_messages` 时内部 alias 到 `messages`(更短、内部统一用简称)。
|
|
228
|
-
*
|
|
229
|
-
* @param manifest seller 的 `/manifest` 响应
|
|
230
|
-
* @param seller 兜底用的 registry seller
|
|
231
|
-
* @returns 归一化后的协议列表
|
|
232
|
-
*/
|
|
233
|
-
export function manifestProtocols(manifest: SellerManifest, seller: RegistrySeller): string[] {
|
|
234
|
-
const protocols = manifest.supportedProtocols || manifest.supported_protocols || seller.supportedProtocols || [];
|
|
235
|
-
return protocols.includes("anthropic_messages") && !protocols.includes("messages")
|
|
236
|
-
? [...protocols, "messages"]
|
|
237
|
-
: protocols;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* 解析 seller 支持的支付方式。
|
|
242
|
-
* 优先级:manifest.camelCase > manifest.snake_case > registry fallback。
|
|
243
|
-
*
|
|
244
|
-
* @param manifest seller 的 `/manifest` 响应
|
|
245
|
-
* @param seller 兜底用的 registry seller
|
|
246
|
-
* @returns 支付方式列表
|
|
247
|
-
*/
|
|
248
|
-
export function manifestPaymentMethods(manifest: SellerManifest, seller: RegistrySeller): string[] {
|
|
249
|
-
return manifest.paymentMethods || manifest.payment_methods || seller.paymentMethods || [];
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* 从 manifest 抽取所有模型 ID,过滤非字符串与空串、trim 空白。
|
|
254
|
-
*
|
|
255
|
-
* @param manifest seller 的 `/manifest` 响应
|
|
256
|
-
* @returns 清理后的模型 ID 列表(保序)
|
|
257
|
-
*/
|
|
258
|
-
export function manifestModelIds(manifest: SellerManifest): string[] {
|
|
259
|
-
return (manifest.models || [])
|
|
260
|
-
.map((model) => model.id)
|
|
261
|
-
.filter((id): id is string => typeof id === "string" && id.trim().length > 0)
|
|
262
|
-
.map((id) => id.trim());
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function numericPriceField(value: unknown): number | undefined {
|
|
266
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function manifestModels(manifest: SellerManifest): ManifestModelRecord[] {
|
|
270
|
-
return (manifest.models || [])
|
|
271
|
-
.filter((model): model is ManifestModelRecord => Boolean(model?.id && typeof model.id === "string"));
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* v1.2 §18.9:bootstrap 的 `/registry/sellers` 返回 413 + `X-TokenBuddy-Registry-Too-Large: 1` 时抛出的错误。
|
|
276
|
-
* daemon 在 `TokenbuddyDaemon.fetchRegistry` 捕获并回退到上次成功快照,保证 buyer 仍可路由。
|
|
277
|
-
*/
|
|
278
|
-
export class RegistryTooLargeError extends Error {
|
|
279
|
-
readonly status: number;
|
|
280
|
-
readonly sizeBytes: number;
|
|
281
|
-
readonly sellerCount: number;
|
|
282
|
-
readonly maxBytes: number;
|
|
283
|
-
constructor(detail: { status: number; sizeBytes: number; sellerCount: number; maxBytes: number }) {
|
|
284
|
-
super(`registry response exceeds ${detail.maxBytes} bytes (got ${detail.sizeBytes}, ${detail.sellerCount} sellers, status ${detail.status})`);
|
|
285
|
-
this.name = "RegistryTooLargeError";
|
|
286
|
-
this.status = detail.status;
|
|
287
|
-
this.sizeBytes = detail.sizeBytes;
|
|
288
|
-
this.sellerCount = detail.sellerCount;
|
|
289
|
-
this.maxBytes = detail.maxBytes;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* 拉取 `/registry/sellers` 响应。
|
|
295
|
-
* - HTTP 413:抛 `RegistryTooLargeError`(带 `sizeBytes` / `sellerCount` / `maxBytes` 元信息),让 daemon 决定是否回退快照。
|
|
296
|
-
* - 其它非 2xx:抛通用 Error。
|
|
297
|
-
* - 2xx 但缺 `sellers` 数组:抛 Error。
|
|
298
|
-
*
|
|
299
|
-
* @param registryUrl wallet-bootstrap 的 `/registry/sellers` 完整 URL
|
|
300
|
-
* @returns 解析后的注册表文档
|
|
301
|
-
* @throws RegistryTooLargeError / Error
|
|
302
|
-
*/
|
|
303
|
-
export async function fetchSellerRegistry(registryUrl: string): Promise<SellerRegistryDocument> {
|
|
304
|
-
return (await fetchSellerRegistryWithTrust(registryUrl)).registry;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export async function fetchSellerRegistryWithTrust(registryUrl: string): Promise<FetchedSellerRegistry> {
|
|
308
|
-
const response = await fetch(registryUrl);
|
|
309
|
-
if (response.status === 413) {
|
|
310
|
-
// v1.2 §18.9: parse the structured 413 body so the caller can
|
|
311
|
-
// decide whether to fall back to a stale snapshot.
|
|
312
|
-
let sizeBytes = 0;
|
|
313
|
-
let sellerCount = 0;
|
|
314
|
-
let maxBytes = 0;
|
|
315
|
-
try {
|
|
316
|
-
const body = (await response.json()) as { sizeBytes?: number; sellerCount?: number; maxBytes?: number };
|
|
317
|
-
sizeBytes = body.sizeBytes ?? 0;
|
|
318
|
-
sellerCount = body.sellerCount ?? 0;
|
|
319
|
-
maxBytes = body.maxBytes ?? 0;
|
|
320
|
-
} catch {
|
|
321
|
-
// Fall through with zeroes; the error message still carries the
|
|
322
|
-
// status code which is enough for the daemon's stale-cache
|
|
323
|
-
// branch to fire.
|
|
324
|
-
}
|
|
325
|
-
throw new RegistryTooLargeError({ status: response.status, sizeBytes, sellerCount, maxBytes });
|
|
326
|
-
}
|
|
327
|
-
if (!response.ok) {
|
|
328
|
-
throw new Error(`registry returned ${response.status}`);
|
|
329
|
-
}
|
|
330
|
-
const text = await response.text();
|
|
331
|
-
const trust: SellerRegistryTrustMetadata = {
|
|
332
|
-
registryUrl,
|
|
333
|
-
registrySha256: crypto.createHash("sha256").update(text).digest("hex"),
|
|
334
|
-
verified: false
|
|
335
|
-
};
|
|
336
|
-
if (shouldVerifyRegistry(registryUrl)) {
|
|
337
|
-
const signatureUrl = signatureUrlForRegistryUrl(registryUrl);
|
|
338
|
-
const signatureResponse = await fetch(signatureUrl);
|
|
339
|
-
if (!signatureResponse.ok) {
|
|
340
|
-
logger.warn("registry.signature.invalid", "registry signature fetch failed", {
|
|
341
|
-
registryUrl,
|
|
342
|
-
signatureUrl,
|
|
343
|
-
status: signatureResponse.status
|
|
344
|
-
});
|
|
345
|
-
throw new Error(`registry signature returned ${signatureResponse.status}`);
|
|
346
|
-
}
|
|
347
|
-
const signature = (await signatureResponse.text()).trim();
|
|
348
|
-
const signingKeyId = verifyTrustedRegistrySignature(text, signature);
|
|
349
|
-
trust.verified = true;
|
|
350
|
-
trust.signatureUrl = signatureUrl;
|
|
351
|
-
trust.signature = signature;
|
|
352
|
-
trust.signingKeyId = signingKeyId;
|
|
353
|
-
logger.info("registry.signature.verified", "registry signature verified", {
|
|
354
|
-
registryUrl,
|
|
355
|
-
signingKeyId
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
const data = JSON.parse(text) as SellerRegistryDocument;
|
|
359
|
-
if (!data || !Array.isArray(data.sellers)) {
|
|
360
|
-
throw new Error("registry response missing sellers");
|
|
361
|
-
}
|
|
362
|
-
return { registry: data, registryJson: text, trust };
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* 拉取单个 seller 的 `/manifest` 响应。
|
|
367
|
-
*
|
|
368
|
-
* @param seller registry seller
|
|
369
|
-
* @returns 解析后的 manifest
|
|
370
|
-
* @throws Error 非 2xx
|
|
371
|
-
*/
|
|
372
|
-
export async function fetchSellerManifest(seller: RegistrySeller): Promise<SellerManifest> {
|
|
373
|
-
const response = await fetch(`${normalizeSellerUrl(seller)}/manifest`);
|
|
374
|
-
if (!response.ok) {
|
|
375
|
-
throw new Error(`manifest returned ${response.status}`);
|
|
376
|
-
}
|
|
377
|
-
return await response.json() as SellerManifest;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* 端到端发现 seller-backed 模型目录:
|
|
382
|
-
* 1. 拉取 `/registry/sellers`。
|
|
383
|
-
* 2. 并发拉取每个 seller 的 `/manifest`(失败转成 `errorMessage` 字段,不阻断整体)。
|
|
384
|
-
* 3. 合并 model 列表、按 `(modelId, sellerId)` 去重。
|
|
385
|
-
* 4. 输出 `SellerCatalogResult`(含聚合的 `models[]` 与 `sellers[]`)。
|
|
386
|
-
*
|
|
387
|
-
* @param registryUrl wallet-bootstrap 注册表 URL
|
|
388
|
-
* @returns 聚合后的目录
|
|
389
|
-
*/
|
|
390
|
-
export async function discoverSellerBackedModels(registryUrl: string): Promise<SellerCatalogResult> {
|
|
391
|
-
const fetched = await fetchSellerRegistryWithTrust(registryUrl);
|
|
392
|
-
const registry = fetched.registry;
|
|
393
|
-
const visibleSellers = registry.sellers.filter(isBuyerVisibleRegistrySeller);
|
|
394
|
-
const sellerResults = await Promise.all(visibleSellers.map(async (seller) => {
|
|
395
|
-
try {
|
|
396
|
-
const manifest = await fetchSellerManifest(seller);
|
|
397
|
-
const protocols = manifestProtocols(manifest, seller);
|
|
398
|
-
const paymentMethods = manifestPaymentMethods(manifest, seller);
|
|
399
|
-
const models = manifestModels(manifest).map((model) => ({
|
|
400
|
-
id: model.id.trim(),
|
|
401
|
-
sellerId: seller.id,
|
|
402
|
-
sellerName: seller.name,
|
|
403
|
-
sellerUrl: seller.url,
|
|
404
|
-
supportedProtocols: protocols,
|
|
405
|
-
paymentMethods,
|
|
406
|
-
inputPriceMicrosPer1m: numericPriceField(model.inputPriceMicrosPer1m) ?? numericPriceField(model.input_price_micros_per_1m),
|
|
407
|
-
outputPriceMicrosPer1m: numericPriceField(model.outputPriceMicrosPer1m) ?? numericPriceField(model.output_price_micros_per_1m),
|
|
408
|
-
}));
|
|
409
|
-
return {
|
|
410
|
-
seller: {
|
|
411
|
-
id: seller.id,
|
|
412
|
-
name: seller.name,
|
|
413
|
-
url: seller.url,
|
|
414
|
-
status: "ok",
|
|
415
|
-
manifestSellerId: manifest.sellerId || manifest.seller_id || seller.id,
|
|
416
|
-
discountRatio: manifest.selection?.discountRatio ?? manifest.selection?.discount_ratio,
|
|
417
|
-
serviceFeeRatio: manifest.selection?.serviceFeeRatio ?? manifest.selection?.service_fee_ratio,
|
|
418
|
-
modelCount: models.length,
|
|
419
|
-
supportedProtocols: protocols,
|
|
420
|
-
paymentMethods,
|
|
421
|
-
},
|
|
422
|
-
models
|
|
423
|
-
};
|
|
424
|
-
} catch (error: unknown) {
|
|
425
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
426
|
-
logger.warn("models.refresh.seller_failed", "seller manifest refresh failed", {
|
|
427
|
-
sellerId: seller.id,
|
|
428
|
-
errorMessage
|
|
429
|
-
});
|
|
430
|
-
return {
|
|
431
|
-
seller: {
|
|
432
|
-
id: seller.id,
|
|
433
|
-
name: seller.name,
|
|
434
|
-
url: seller.url,
|
|
435
|
-
status: "failed",
|
|
436
|
-
errorMessage
|
|
437
|
-
},
|
|
438
|
-
models: [] as ModelCatalogEntry[]
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
}));
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
registryUrl,
|
|
445
|
-
version: registry.version,
|
|
446
|
-
defaultSeller: registry.defaultSeller,
|
|
447
|
-
registry,
|
|
448
|
-
registryJson: fetched.registryJson,
|
|
449
|
-
models: sellerResults.flatMap((entry) => entry.models),
|
|
450
|
-
sellers: sellerResults.map((entry) => entry.seller),
|
|
451
|
-
registryTrust: fetched.trust
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* 按协议过滤 catalog 条目。
|
|
457
|
-
*
|
|
458
|
-
* @param models 目录条目
|
|
459
|
-
* @param protocol 目标协议
|
|
460
|
-
* @returns 仅包含该协议的条目
|
|
461
|
-
*/
|
|
462
|
-
export function filterCatalogByProtocol(
|
|
463
|
-
models: ModelCatalogEntry[],
|
|
464
|
-
protocol: ProtocolPreference
|
|
465
|
-
): ModelCatalogEntry[] {
|
|
466
|
-
return models.filter((entry) => entry.supportedProtocols.includes(protocol));
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* 按 seller ID 过滤 catalog 条目。
|
|
471
|
-
* `sellerId` 缺省时返回原列表。
|
|
472
|
-
*
|
|
473
|
-
* @param models 目录条目
|
|
474
|
-
* @param sellerId 目标 seller ID
|
|
475
|
-
* @returns 仅包含该 seller 的条目
|
|
476
|
-
*/
|
|
477
|
-
export function filterCatalogBySeller(
|
|
478
|
-
models: ModelCatalogEntry[],
|
|
479
|
-
sellerId: string | undefined
|
|
480
|
-
): ModelCatalogEntry[] {
|
|
481
|
-
if (!sellerId) {
|
|
482
|
-
return models;
|
|
483
|
-
}
|
|
484
|
-
return models.filter((entry) => entry.sellerId === sellerId);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* 按 `(sellerId, modelId)` 去重 catalog 条目,保留首次出现的顺序。
|
|
489
|
-
*
|
|
490
|
-
* @param models 目录条目
|
|
491
|
-
* @returns 去重后的条目
|
|
492
|
-
*/
|
|
493
|
-
export function dedupeCatalogEntries(models: ModelCatalogEntry[]): ModelCatalogEntry[] {
|
|
494
|
-
const seen = new Set<string>();
|
|
495
|
-
const output: ModelCatalogEntry[] = [];
|
|
496
|
-
for (const entry of models) {
|
|
497
|
-
const key = `${entry.sellerId}:${entry.id}`;
|
|
498
|
-
if (seen.has(key)) {
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
seen.add(key);
|
|
502
|
-
output.push(entry);
|
|
503
|
-
}
|
|
504
|
-
return output;
|
|
505
|
-
}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { createModuleLogger } from "@tokenbuddy/logging";
|
|
2
|
-
|
|
3
|
-
const logger = createModuleLogger("tb-proxyd:seller-concurrency");
|
|
4
|
-
|
|
5
|
-
const DEFAULT_MAX_IN_FLIGHT_PER_SELLER = 2;
|
|
6
|
-
const DEFAULT_LEASE_TTL_MS = 185_000;
|
|
7
|
-
|
|
8
|
-
export interface SellerConcurrencyLimiterOptions {
|
|
9
|
-
enabled?: boolean;
|
|
10
|
-
maxInFlightPerSeller?: number;
|
|
11
|
-
leaseTtlMs?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface SellerConcurrencyLease {
|
|
15
|
-
sellerId: string;
|
|
16
|
-
activeCount: number;
|
|
17
|
-
maxInFlight: number;
|
|
18
|
-
refresh(): void;
|
|
19
|
-
release(): void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface SellerConcurrencySnapshot {
|
|
23
|
-
enabled: boolean;
|
|
24
|
-
maxInFlightPerSeller: number;
|
|
25
|
-
leaseTtlMs: number;
|
|
26
|
-
active: Array<{ sellerId: string; activeCount: number }>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class SellerConcurrencyLimiter {
|
|
30
|
-
private readonly enabled: boolean;
|
|
31
|
-
private readonly maxInFlightPerSeller: number;
|
|
32
|
-
private readonly leaseTtlMs: number;
|
|
33
|
-
private readonly active = new Map<string, number>();
|
|
34
|
-
|
|
35
|
-
constructor(options: SellerConcurrencyLimiterOptions = {}) {
|
|
36
|
-
this.enabled = options.enabled === true;
|
|
37
|
-
this.maxInFlightPerSeller = positiveIntegerOrDefault(
|
|
38
|
-
options.maxInFlightPerSeller,
|
|
39
|
-
DEFAULT_MAX_IN_FLIGHT_PER_SELLER
|
|
40
|
-
);
|
|
41
|
-
this.leaseTtlMs = positiveIntegerOrDefault(options.leaseTtlMs, DEFAULT_LEASE_TTL_MS);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
isEnabled(): boolean {
|
|
45
|
-
return this.enabled;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
tryAcquire(sellerId: string, context: { requestId?: string; modelId?: string; endpoint?: string } = {}): SellerConcurrencyLease | undefined {
|
|
49
|
-
if (!this.enabled) {
|
|
50
|
-
return {
|
|
51
|
-
sellerId,
|
|
52
|
-
activeCount: 0,
|
|
53
|
-
maxInFlight: this.maxInFlightPerSeller,
|
|
54
|
-
refresh: () => undefined,
|
|
55
|
-
release: () => undefined
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const activeCount = this.active.get(sellerId) ?? 0;
|
|
60
|
-
if (activeCount >= this.maxInFlightPerSeller) {
|
|
61
|
-
logger.info("seller_concurrency.local_full", "seller local concurrency limit reached", {
|
|
62
|
-
sellerId,
|
|
63
|
-
requestId: context.requestId,
|
|
64
|
-
model: context.modelId,
|
|
65
|
-
endpoint: context.endpoint,
|
|
66
|
-
activeCount,
|
|
67
|
-
maxInFlight: this.maxInFlightPerSeller
|
|
68
|
-
});
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const nextCount = activeCount + 1;
|
|
73
|
-
this.active.set(sellerId, nextCount);
|
|
74
|
-
logger.info("seller_concurrency.lease_acquired", "seller local concurrency lease acquired", {
|
|
75
|
-
sellerId,
|
|
76
|
-
requestId: context.requestId,
|
|
77
|
-
model: context.modelId,
|
|
78
|
-
endpoint: context.endpoint,
|
|
79
|
-
activeCount: nextCount,
|
|
80
|
-
maxInFlight: this.maxInFlightPerSeller
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
let released = false;
|
|
84
|
-
let timer: NodeJS.Timeout | undefined;
|
|
85
|
-
const scheduleExpiry = (): void => {
|
|
86
|
-
timer = setTimeout(() => {
|
|
87
|
-
if (released) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
logger.warn("seller_concurrency.lease_expired", "seller local concurrency lease expired", {
|
|
91
|
-
sellerId,
|
|
92
|
-
requestId: context.requestId,
|
|
93
|
-
model: context.modelId,
|
|
94
|
-
endpoint: context.endpoint,
|
|
95
|
-
leaseTtlMs: this.leaseTtlMs
|
|
96
|
-
});
|
|
97
|
-
release();
|
|
98
|
-
}, this.leaseTtlMs);
|
|
99
|
-
timer.unref?.();
|
|
100
|
-
};
|
|
101
|
-
const refresh = (): void => {
|
|
102
|
-
if (released) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (timer) {
|
|
106
|
-
clearTimeout(timer);
|
|
107
|
-
}
|
|
108
|
-
scheduleExpiry();
|
|
109
|
-
};
|
|
110
|
-
scheduleExpiry();
|
|
111
|
-
|
|
112
|
-
const release = (): void => {
|
|
113
|
-
if (released) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
released = true;
|
|
117
|
-
if (timer) {
|
|
118
|
-
clearTimeout(timer);
|
|
119
|
-
timer = undefined;
|
|
120
|
-
}
|
|
121
|
-
const current = this.active.get(sellerId) ?? 0;
|
|
122
|
-
const remaining = Math.max(0, current - 1);
|
|
123
|
-
if (remaining === 0) {
|
|
124
|
-
this.active.delete(sellerId);
|
|
125
|
-
} else {
|
|
126
|
-
this.active.set(sellerId, remaining);
|
|
127
|
-
}
|
|
128
|
-
logger.info("seller_concurrency.lease_released", "seller local concurrency lease released", {
|
|
129
|
-
sellerId,
|
|
130
|
-
requestId: context.requestId,
|
|
131
|
-
model: context.modelId,
|
|
132
|
-
endpoint: context.endpoint,
|
|
133
|
-
activeCount: remaining,
|
|
134
|
-
maxInFlight: this.maxInFlightPerSeller
|
|
135
|
-
});
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
sellerId,
|
|
140
|
-
activeCount: nextCount,
|
|
141
|
-
maxInFlight: this.maxInFlightPerSeller,
|
|
142
|
-
refresh,
|
|
143
|
-
release
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
snapshot(): SellerConcurrencySnapshot {
|
|
148
|
-
return {
|
|
149
|
-
enabled: this.enabled,
|
|
150
|
-
maxInFlightPerSeller: this.maxInFlightPerSeller,
|
|
151
|
-
leaseTtlMs: this.leaseTtlMs,
|
|
152
|
-
active: Array.from(this.active.entries())
|
|
153
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
154
|
-
.map(([sellerId, activeCount]) => ({ sellerId, activeCount }))
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function positiveIntegerOrDefault(value: number | undefined, fallback: number): number {
|
|
160
|
-
return Number.isInteger(value) && (value as number) > 0 ? value as number : fallback;
|
|
161
|
-
}
|