@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
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
SellerRoutingMode,
|
|
3
|
-
SellerRoutingScorer,
|
|
4
|
-
SellerRoutingStrategyConfig
|
|
5
|
-
} from "./seller-routing-strategy.js";
|
|
6
|
-
|
|
7
|
-
/** buyer 配置存盘时使用的顶层 key,对应 `BuyerStore.set("routing", ...)` */
|
|
8
|
-
export const ROUTING_CONFIG_KEY = "routing";
|
|
9
|
-
/** 缺省 scorer:健康 + 速度 + 折扣三方面均衡 */
|
|
10
|
-
export const DEFAULT_ROUTING_SCORER: SellerRoutingScorer = "balanced";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* buyer 端的 seller 路由配置,继承策略层 `SellerRoutingStrategyConfig`,
|
|
14
|
-
* 在此基础上强制 `mode` 和 `scorer` 都必须存在(策略层允许省略)。
|
|
15
|
-
*/
|
|
16
|
-
export interface BuyerSellerRoutingConfig extends SellerRoutingStrategyConfig {
|
|
17
|
-
/** 路由模式:`fixed` / `fixedSet` / `fullAuto` */
|
|
18
|
-
mode: SellerRoutingMode;
|
|
19
|
-
/** 评分器:`speed` / `discount` / `balanced` */
|
|
20
|
-
scorer: SellerRoutingScorer;
|
|
21
|
-
/** fixed 模式下按模型固定 seller;缺省时回退到全局 sellerId。 */
|
|
22
|
-
fixedByModel?: Record<string, string>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 返回 buyer 默认的 seller 路由配置:`fullAuto` + `balanced`。
|
|
27
|
-
* 用于首次启动没有持久化配置时的兜底。
|
|
28
|
-
*
|
|
29
|
-
* @returns buyer 默认路由配置
|
|
30
|
-
*/
|
|
31
|
-
export function defaultSellerRoutingConfig(): BuyerSellerRoutingConfig {
|
|
32
|
-
return {
|
|
33
|
-
mode: "fullAuto",
|
|
34
|
-
scorer: DEFAULT_ROUTING_SCORER
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 把任意 `unknown`(通常来自 buyer-store 读盘或外部输入)归一化为合法的
|
|
40
|
-
* `BuyerSellerRoutingConfig`。非法 `mode` / `scorer` 抛错,缺字段则回退到默认。
|
|
41
|
-
*
|
|
42
|
-
* @param value 任意输入值(对象 / undefined / 字符串等)
|
|
43
|
-
* @returns 归一化后的路由配置
|
|
44
|
-
* @throws 当 `value.mode` 是非 `fixed` / `fixedSet` / `fullAuto` 时
|
|
45
|
-
* @throws 当 `value.scorer` 是非 `speed` / `discount` / `balanced` 时
|
|
46
|
-
*/
|
|
47
|
-
export function normalizeSellerRoutingConfig(value: unknown): BuyerSellerRoutingConfig {
|
|
48
|
-
if (!isObject(value)) {
|
|
49
|
-
return defaultSellerRoutingConfig();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const mode = readMode(value.mode);
|
|
53
|
-
const scorer = readScorer(value.scorer);
|
|
54
|
-
if (mode === "fixed") {
|
|
55
|
-
return {
|
|
56
|
-
mode,
|
|
57
|
-
sellerId: readOptionalString(value.sellerId),
|
|
58
|
-
fixedByModel: readFixedByModel(value.fixedByModel),
|
|
59
|
-
scorer
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
if (mode === "fixedSet") {
|
|
63
|
-
return {
|
|
64
|
-
mode,
|
|
65
|
-
sellerIds: readSellerIds(value.sellerIds),
|
|
66
|
-
scorer
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
mode: "fullAuto",
|
|
71
|
-
scorer
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 从 `process.env`(可注入)里解析出 `TB_PROXYD_ROUTING_*` 覆盖项。
|
|
77
|
-
* 至少有一个变量非空时才返回 `Partial<BuyerSellerRoutingConfig>`,否则返回 `undefined`。
|
|
78
|
-
* 当 `mode` 未指定时,会根据 `sellerId` / `sellerIds` 是否存在自动选择 `fixed` / `fixedSet` / `fullAuto`。
|
|
79
|
-
*
|
|
80
|
-
* @param env 进程环境变量(默认 `process.env`,测试可注入)
|
|
81
|
-
* @returns 解析出的覆盖项;无任何相关变量时返回 `undefined`
|
|
82
|
-
* @throws 当 `TB_PROXYD_ROUTING_MODE` 或 `TB_PROXYD_ROUTING_SCORER` 取值非法时
|
|
83
|
-
*/
|
|
84
|
-
export function parseSellerRoutingEnv(env: NodeJS.ProcessEnv = process.env): Partial<BuyerSellerRoutingConfig> | undefined {
|
|
85
|
-
const modeRaw = env.TB_PROXYD_ROUTING_MODE?.trim();
|
|
86
|
-
const scorerRaw = env.TB_PROXYD_ROUTING_SCORER?.trim();
|
|
87
|
-
const sellerId = env.TB_PROXYD_ROUTING_SELLER_ID?.trim();
|
|
88
|
-
const sellerIdsRaw = env.TB_PROXYD_ROUTING_SELLER_IDS?.trim();
|
|
89
|
-
|
|
90
|
-
if (!modeRaw && !scorerRaw && !sellerId && !sellerIdsRaw) {
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const mode = modeRaw
|
|
95
|
-
? readMode(modeRaw)
|
|
96
|
-
: sellerId
|
|
97
|
-
? "fixed"
|
|
98
|
-
: sellerIdsRaw
|
|
99
|
-
? "fixedSet"
|
|
100
|
-
: "fullAuto";
|
|
101
|
-
const scorer = scorerRaw ? readScorer(scorerRaw) : DEFAULT_ROUTING_SCORER;
|
|
102
|
-
|
|
103
|
-
if (mode === "fixed") {
|
|
104
|
-
return { mode, scorer, sellerId: sellerId || undefined };
|
|
105
|
-
}
|
|
106
|
-
if (mode === "fixedSet") {
|
|
107
|
-
return { mode, scorer, sellerIds: parseSellerIdList(sellerIdsRaw || "") };
|
|
108
|
-
}
|
|
109
|
-
return { mode: "fullAuto", scorer };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 合并"buyer-store 持久化值"和"运行时 override":override 字段优先,
|
|
114
|
-
* 但合并结果会再次走 `normalizeSellerRoutingConfig` 校验。
|
|
115
|
-
*
|
|
116
|
-
* @param stored buyer-store 读出的原始值(可能是 undefined 或非法对象)
|
|
117
|
-
* @param override 运行时 override(CLI / 临时开关)
|
|
118
|
-
* @returns 合并并归一化后的配置
|
|
119
|
-
* @throws 当合并结果包含非法 `mode` / `scorer` 时
|
|
120
|
-
*/
|
|
121
|
-
export function mergeSellerRoutingConfig(
|
|
122
|
-
stored: unknown,
|
|
123
|
-
override?: Partial<BuyerSellerRoutingConfig>
|
|
124
|
-
): BuyerSellerRoutingConfig {
|
|
125
|
-
const base = normalizeSellerRoutingConfig(stored);
|
|
126
|
-
if (!override) {
|
|
127
|
-
return base;
|
|
128
|
-
}
|
|
129
|
-
return normalizeSellerRoutingConfig({
|
|
130
|
-
...base,
|
|
131
|
-
...override
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* 把逗号分隔的 seller ID 列表解析为去重、trim、过滤空串后的字符串数组。
|
|
137
|
-
*
|
|
138
|
-
* @param value 原始字符串,例:`"a,b,b, c,,"`
|
|
139
|
-
* @returns 去重后的 seller ID 列表
|
|
140
|
-
*/
|
|
141
|
-
export function parseSellerIdList(value: string): string[] {
|
|
142
|
-
return value
|
|
143
|
-
.split(",")
|
|
144
|
-
.map((entry) => entry.trim())
|
|
145
|
-
.filter((entry, index, all) => entry.length > 0 && all.indexOf(entry) === index);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* 校验 `BuyerSellerRoutingConfig` 是否满足当前模式的必要字段。
|
|
150
|
-
* `fixed` 模式必须有非空 `sellerId`;`fixedSet` 模式必须有非空 `sellerIds`。
|
|
151
|
-
*
|
|
152
|
-
* @param config 待校验的路由配置
|
|
153
|
-
* @throws 当 `fixed` 模式缺少 `sellerId` 时
|
|
154
|
-
* @throws 当 `fixedSet` 模式缺少 `sellerIds` 时
|
|
155
|
-
*/
|
|
156
|
-
export function assertSellerRoutingConfig(config: BuyerSellerRoutingConfig): void {
|
|
157
|
-
if (config.mode === "fixed" && !config.sellerId?.trim() && Object.keys(config.fixedByModel ?? {}).length === 0) {
|
|
158
|
-
throw new Error("fixed routing requires --seller <sellerId>");
|
|
159
|
-
}
|
|
160
|
-
if (config.mode === "fixedSet" && (!config.sellerIds || config.sellerIds.length === 0)) {
|
|
161
|
-
throw new Error("fixedSet routing requires --seller-set <sellerId[,sellerId...]>");
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function readMode(value: unknown): SellerRoutingMode {
|
|
166
|
-
if (value === "fixed" || value === "fixedSet" || value === "fullAuto") {
|
|
167
|
-
return value;
|
|
168
|
-
}
|
|
169
|
-
throw new Error("seller routing mode must be fixed, fixedSet, or fullAuto");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function readScorer(value: unknown): SellerRoutingScorer {
|
|
173
|
-
if (value === undefined || value === null || value === "") {
|
|
174
|
-
return DEFAULT_ROUTING_SCORER;
|
|
175
|
-
}
|
|
176
|
-
if (value === "speed" || value === "discount" || value === "balanced") {
|
|
177
|
-
return value;
|
|
178
|
-
}
|
|
179
|
-
throw new Error("seller routing scorer must be speed, discount, or balanced");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function readOptionalString(value: unknown): string | undefined {
|
|
183
|
-
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function readSellerIds(value: unknown): string[] {
|
|
187
|
-
if (Array.isArray(value)) {
|
|
188
|
-
return value
|
|
189
|
-
.filter((entry): entry is string => typeof entry === "string")
|
|
190
|
-
.map((entry) => entry.trim())
|
|
191
|
-
.filter((entry, index, all) => entry.length > 0 && all.indexOf(entry) === index);
|
|
192
|
-
}
|
|
193
|
-
if (typeof value === "string") {
|
|
194
|
-
return parseSellerIdList(value);
|
|
195
|
-
}
|
|
196
|
-
return [];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function readFixedByModel(value: unknown): Record<string, string> | undefined {
|
|
200
|
-
if (!isObject(value)) {
|
|
201
|
-
return undefined;
|
|
202
|
-
}
|
|
203
|
-
const entries = Object.entries(value)
|
|
204
|
-
.map(([modelId, sellerId]) => [modelId.trim(), typeof sellerId === "string" ? sellerId.trim() : ""] as const)
|
|
205
|
-
.filter(([modelId, sellerId]) => modelId.length > 0 && sellerId.length > 0);
|
|
206
|
-
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
210
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
211
|
-
}
|
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* seller 路由模式:
|
|
3
|
-
* - `fixed`:强制使用单个 seller(`sellerId`)
|
|
4
|
-
* - `fixedSet`:在指定 seller 集合内挑选
|
|
5
|
-
* - `fullAuto`:根据指标和 scorer 自动排序
|
|
6
|
-
*/
|
|
7
|
-
export type SellerRoutingMode = "fixed" | "fixedSet" | "fullAuto";
|
|
8
|
-
/**
|
|
9
|
-
* 评分器:决定如何把候选的健康/延迟/折扣分折算成总分。
|
|
10
|
-
* - `speed`:TTFT / 输出吞吐优先
|
|
11
|
-
* - `discount`:折扣系数优先
|
|
12
|
-
* - `balanced`:三方面加权均衡
|
|
13
|
-
*/
|
|
14
|
-
export type SellerRoutingScorer = "speed" | "discount" | "balanced";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* seller 路由策略配置:模式 + 可选的目标 seller 列表 + 可选评分器。
|
|
18
|
-
* 策略层不强制 `scorer` 必填(缺省走 `balanced`);buyer 配置层
|
|
19
|
-
* `BuyerSellerRoutingConfig` 会进一步收紧。
|
|
20
|
-
*/
|
|
21
|
-
export interface SellerRoutingStrategyConfig {
|
|
22
|
-
/** 路由模式 */
|
|
23
|
-
mode: SellerRoutingMode;
|
|
24
|
-
/** `fixed` 模式下唯一允许的 seller ID */
|
|
25
|
-
sellerId?: string;
|
|
26
|
-
/** `fixedSet` 模式下的候选 seller ID 列表(去重) */
|
|
27
|
-
sellerIds?: string[];
|
|
28
|
-
/** 评分器;省略时按 `balanced` 处理 */
|
|
29
|
-
scorer?: SellerRoutingScorer;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 路由规划阶段的中间数据结构:聚合了"兼容性 + 健康画像 + 折扣"。
|
|
34
|
-
* `supportsModel/Protocol/Payment` 由 `sellerSupports*` 系列函数计算,
|
|
35
|
-
* 任一为 `false` 的 candidate 都会被策略层过滤。
|
|
36
|
-
*/
|
|
37
|
-
export interface RoutingCandidate {
|
|
38
|
-
/** seller ID */
|
|
39
|
-
sellerId: string;
|
|
40
|
-
/** 去掉尾部斜杠的 seller URL */
|
|
41
|
-
url: string;
|
|
42
|
-
/** 是否在 seller 的 `models` 列表里 */
|
|
43
|
-
supportsModel: boolean;
|
|
44
|
-
/** 是否在 seller 的 `supportedProtocols` 列表里(含 `messages` alias) */
|
|
45
|
-
supportsProtocol: boolean;
|
|
46
|
-
/** 是否在 seller 的 `paymentMethods` 列表里 */
|
|
47
|
-
supportsPayment: boolean;
|
|
48
|
-
/** 综合健康分 0-100,可选 */
|
|
49
|
-
healthScore?: number;
|
|
50
|
-
/** 平均延迟(毫秒),可选 */
|
|
51
|
-
avgLatencyMs?: number;
|
|
52
|
-
/** health probe 延迟(毫秒),可选 */
|
|
53
|
-
healthProbeLatencyMs?: number;
|
|
54
|
-
/** TTFT(毫秒),可选 */
|
|
55
|
-
ttftMs?: number;
|
|
56
|
-
/** 平均推理延迟(毫秒),可选 */
|
|
57
|
-
avgInferenceMs?: number;
|
|
58
|
-
/** 最近 10 分钟窗口内的平均输出吞吐(tokens/s),可选 */
|
|
59
|
-
avgTokensPerSecond?: number;
|
|
60
|
-
/** 折扣系数 0-1,可选;缺省视为"无折扣信息" */
|
|
61
|
-
discountRatio?: number;
|
|
62
|
-
/** 上游状态,可选 */
|
|
63
|
-
upstreamStatus?: "healthy" | "degraded" | "unhealthy" | "unknown";
|
|
64
|
-
/** 上游错误类名,可选 */
|
|
65
|
-
upstreamErrorClass?: string;
|
|
66
|
-
/** 在 registry 里的声明顺序(0-based),用于打分平局时的 deterministic 兜底 */
|
|
67
|
-
registryOrder: number;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* `planSellerRoutes()` 的输出:选中的候选列表(已按 scorer 排序)+ 决策溯源。
|
|
72
|
-
*/
|
|
73
|
-
export interface SellerRoutingPlan {
|
|
74
|
-
/** 已按策略排序的候选 seller 列表 */
|
|
75
|
-
routes: RoutingCandidate[];
|
|
76
|
-
/** 路由模式(来自 config) */
|
|
77
|
-
mode: SellerRoutingMode;
|
|
78
|
-
/** 实际生效的评分器(config 缺省时为 `balanced`) */
|
|
79
|
-
scorer: SellerRoutingScorer;
|
|
80
|
-
/** 决策原因,例:`fullAuto:balanced:routes_3`、`fixed_seller_not_compatible` */
|
|
81
|
-
reason: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* 单个 candidate 在某 scorer 下的打分拆解:总分 + 各维度分量 + 缺失的输入。
|
|
86
|
-
* 用于路由诊断 / `tb doctor` 面板展示。
|
|
87
|
-
*/
|
|
88
|
-
export interface CandidateScoreBreakdown {
|
|
89
|
-
/** 评分器 */
|
|
90
|
-
scorer: SellerRoutingScorer;
|
|
91
|
-
/** 总分(按 scorer 权重加和) */
|
|
92
|
-
totalScore: number;
|
|
93
|
-
/** 健康分量(仅 `speed` / `balanced` 有意义) */
|
|
94
|
-
healthComponent?: number;
|
|
95
|
-
/** TTFT 分量(仅 `speed` / `balanced` 有意义) */
|
|
96
|
-
ttftComponent?: number;
|
|
97
|
-
/** 输出吞吐分量(仅 `speed` / `balanced` 有意义) */
|
|
98
|
-
avgTokensPerSecondComponent?: number;
|
|
99
|
-
/** 折扣分量(仅 `discount` / `balanced` 有意义) */
|
|
100
|
-
discountComponent?: number;
|
|
101
|
-
/** 打分时缺失的输入项;缺越多则越说明"无依据" */
|
|
102
|
-
missingInputs: Array<"healthScore" | "ttftMs" | "avgTokensPerSecond" | "discountRatio">;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
type SortableCandidate = RoutingCandidate & { score: number };
|
|
106
|
-
|
|
107
|
-
const DEFAULT_SCORER: SellerRoutingScorer = "balanced";
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 根据 `config.mode` 和 `config.scorer` 把候选 seller 过滤、排序并返回路由计划。
|
|
111
|
-
* - `fixed` / `fixedSet` 模式只保留命中的 seller(命中为空时返回空 routes + 决策原因)
|
|
112
|
-
* - `fullAuto` 模式使用所有兼容候选
|
|
113
|
-
* - 排序时按 scorer 计算分数,分数相同时按 `registryOrder` 兜底
|
|
114
|
-
*
|
|
115
|
-
* @param candidates 候选列表(已包含兼容性布尔位)
|
|
116
|
-
* @param config 路由策略配置
|
|
117
|
-
* @returns 路由计划,含 routes / mode / scorer / reason
|
|
118
|
-
*/
|
|
119
|
-
export function planSellerRoutes(
|
|
120
|
-
candidates: RoutingCandidate[],
|
|
121
|
-
config: SellerRoutingStrategyConfig
|
|
122
|
-
): SellerRoutingPlan {
|
|
123
|
-
const scorer = config.scorer ?? DEFAULT_SCORER;
|
|
124
|
-
const compatible = candidates.filter(isCompatibleCandidate);
|
|
125
|
-
const selected = selectCandidates(compatible, config);
|
|
126
|
-
|
|
127
|
-
if (selected.reason !== "ok") {
|
|
128
|
-
return {
|
|
129
|
-
routes: [],
|
|
130
|
-
mode: config.mode,
|
|
131
|
-
scorer,
|
|
132
|
-
reason: selected.reason
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
routes: sortCandidates(selected.candidates, scorer),
|
|
138
|
-
mode: config.mode,
|
|
139
|
-
scorer,
|
|
140
|
-
reason: routeReason(config.mode, scorer, selected.candidates.length)
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function selectCandidates(
|
|
145
|
-
compatible: RoutingCandidate[],
|
|
146
|
-
config: SellerRoutingStrategyConfig
|
|
147
|
-
): { candidates: RoutingCandidate[]; reason: string } {
|
|
148
|
-
if (config.mode === "fixed") {
|
|
149
|
-
const sellerId = normalizeSellerId(config.sellerId);
|
|
150
|
-
if (!sellerId) {
|
|
151
|
-
return { candidates: [], reason: "fixed_seller_missing" };
|
|
152
|
-
}
|
|
153
|
-
const matched = compatible.filter((candidate) => normalizeSellerId(candidate.sellerId) === sellerId);
|
|
154
|
-
return matched.length > 0
|
|
155
|
-
? { candidates: matched, reason: "ok" }
|
|
156
|
-
: { candidates: [], reason: "fixed_seller_not_compatible" };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (config.mode === "fixedSet") {
|
|
160
|
-
const sellerIds = normalizedSellerIdSet(config.sellerIds);
|
|
161
|
-
if (sellerIds.size === 0) {
|
|
162
|
-
return { candidates: [], reason: "fixed_set_empty" };
|
|
163
|
-
}
|
|
164
|
-
const matched = compatible.filter((candidate) => sellerIds.has(normalizeSellerId(candidate.sellerId)));
|
|
165
|
-
return matched.length > 0
|
|
166
|
-
? { candidates: matched, reason: "ok" }
|
|
167
|
-
: { candidates: [], reason: "fixed_set_no_compatible_seller" };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (config.mode === "fullAuto") {
|
|
171
|
-
return compatible.length > 0
|
|
172
|
-
? { candidates: compatible, reason: "ok" }
|
|
173
|
-
: { candidates: [], reason: "no_compatible_seller" };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return { candidates: [], reason: "unsupported_routing_mode" };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function isCompatibleCandidate(candidate: RoutingCandidate): boolean {
|
|
180
|
-
return candidate.supportsModel && candidate.supportsProtocol && candidate.supportsPayment;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function normalizedSellerIdSet(ids: string[] | undefined): Set<string> {
|
|
184
|
-
return new Set((ids ?? []).map(normalizeSellerId).filter(Boolean));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function normalizeSellerId(id: string | undefined): string {
|
|
188
|
-
return id?.trim().toLowerCase() ?? "";
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function sortCandidates(candidates: RoutingCandidate[], scorer: SellerRoutingScorer): RoutingCandidate[] {
|
|
192
|
-
return candidates
|
|
193
|
-
.map((candidate) => ({ ...candidate, score: scoreCandidateBreakdown(candidate, scorer).totalScore }))
|
|
194
|
-
.sort((a, b) => compareCandidates(a, b, scorer))
|
|
195
|
-
.map(({ score: _score, ...candidate }) => candidate);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function compareCandidates(a: SortableCandidate, b: SortableCandidate, scorer: SellerRoutingScorer): number {
|
|
199
|
-
const scoreDiff = b.score - a.score;
|
|
200
|
-
if (scoreDiff !== 0) {
|
|
201
|
-
return scoreDiff;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (scorer === "speed") {
|
|
205
|
-
return compareFiniteAsc(effectiveTtftMs(a), effectiveTtftMs(b))
|
|
206
|
-
|| compareFiniteDesc(a.avgTokensPerSecond, b.avgTokensPerSecond)
|
|
207
|
-
|| compareFiniteDesc(a.healthScore, b.healthScore)
|
|
208
|
-
|| compareRegistryOrder(a, b);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (scorer === "discount") {
|
|
212
|
-
return compareFiniteAsc(a.discountRatio, b.discountRatio)
|
|
213
|
-
|| compareFiniteDesc(a.healthScore, b.healthScore)
|
|
214
|
-
|| compareRegistryOrder(a, b);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return compareRegistryOrder(a, b);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* 计算单个 candidate 在指定 scorer 下的完整打分拆解(含各维度分量和缺失项)。
|
|
222
|
-
* 不会修改输入 candidate,常用于 doctor 面板和调试日志。
|
|
223
|
-
*
|
|
224
|
-
* 权重设计理由:
|
|
225
|
-
* - **Speed 评分器**:TTFT 65% + Tok/s 25% + Health 10%
|
|
226
|
-
* - TTFT 占主导是因为首 token 延迟直接影响用户感知的响应速度
|
|
227
|
-
* - Tok/s 次要,因为大多数场景下吞吐差异不如延迟显著
|
|
228
|
-
* - Health 最低,只作为平局时的兜底
|
|
229
|
-
*
|
|
230
|
-
* - **Discount 评分器**:折扣 100%
|
|
231
|
-
* - 纯成本优先,忽略性能指标
|
|
232
|
-
* - 平局时按健康分和注册顺序兜底
|
|
233
|
-
*
|
|
234
|
-
* - **Balanced 评分器**:Health 35% + TTFT 20% + Tok/s 20% + Discount 25%
|
|
235
|
-
* - Health 占比最高是为了避免选择不稳定的 seller
|
|
236
|
-
* - 速度(TTFT + Tok/s)合计 40%,与折扣 25% 形成平衡
|
|
237
|
-
* - 这是默认评分器,适合大多数生产场景
|
|
238
|
-
*
|
|
239
|
-
* @param candidate 待打分的候选
|
|
240
|
-
* @param scorer 评分器:`speed` / `discount` / `balanced`
|
|
241
|
-
* @returns 打分拆解
|
|
242
|
-
*/
|
|
243
|
-
export function scoreCandidateBreakdown(candidate: RoutingCandidate, scorer: SellerRoutingScorer): CandidateScoreBreakdown {
|
|
244
|
-
const missingInputs = missingScoreInputs(candidate);
|
|
245
|
-
if (scorer === "speed") {
|
|
246
|
-
const ttftComponent = latencyScore(effectiveTtftMs(candidate)) * 0.65;
|
|
247
|
-
const avgTokensPerSecondComponent = tokensPerSecondScore(candidate.avgTokensPerSecond) * 0.25;
|
|
248
|
-
const healthComponent = finiteOr(candidate.healthScore, 0) * 0.1;
|
|
249
|
-
return {
|
|
250
|
-
scorer,
|
|
251
|
-
totalScore: ttftComponent + avgTokensPerSecondComponent + healthComponent,
|
|
252
|
-
healthComponent,
|
|
253
|
-
ttftComponent,
|
|
254
|
-
avgTokensPerSecondComponent,
|
|
255
|
-
missingInputs
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (scorer === "discount") {
|
|
260
|
-
const totalScore = -finiteOr(candidate.discountRatio, Number.POSITIVE_INFINITY);
|
|
261
|
-
return {
|
|
262
|
-
scorer,
|
|
263
|
-
totalScore,
|
|
264
|
-
discountComponent: totalScore,
|
|
265
|
-
missingInputs
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const healthComponent = finiteOr(candidate.healthScore, 0) * 0.35;
|
|
270
|
-
const ttftComponent = latencyScore(effectiveTtftMs(candidate)) * 0.2;
|
|
271
|
-
const avgTokensPerSecondComponent = tokensPerSecondScore(candidate.avgTokensPerSecond) * 0.2;
|
|
272
|
-
const discountComponent = discountScore(candidate.discountRatio) * 0.25;
|
|
273
|
-
return {
|
|
274
|
-
scorer,
|
|
275
|
-
totalScore: healthComponent + ttftComponent + avgTokensPerSecondComponent + discountComponent,
|
|
276
|
-
healthComponent,
|
|
277
|
-
ttftComponent,
|
|
278
|
-
avgTokensPerSecondComponent,
|
|
279
|
-
discountComponent,
|
|
280
|
-
missingInputs
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* 将延迟(毫秒)转换为 0-100 分数。
|
|
286
|
-
* 公式:`100 - latency_ms / 10`
|
|
287
|
-
*
|
|
288
|
-
* 设计理由:
|
|
289
|
-
* - 0ms → 100分,1000ms → 0分(线性递减)
|
|
290
|
-
* - 除以 10 的比例来自经验:100ms 是"优秀"延迟,1000ms 是"不可接受"延迟
|
|
291
|
-
* - 线性公式的局限:100ms vs 200ms 的差异被等同于 900ms vs 1000ms
|
|
292
|
-
* 实际用户体验可能不是线性的,未来可考虑对数或分段函数
|
|
293
|
-
*
|
|
294
|
-
* @param latencyMs 延迟(毫秒),undefined 或非有限值返回 0 分
|
|
295
|
-
* @returns 0-100 分数
|
|
296
|
-
*/
|
|
297
|
-
function latencyScore(latencyMs: number | undefined): number {
|
|
298
|
-
if (!Number.isFinite(latencyMs)) {
|
|
299
|
-
return 0;
|
|
300
|
-
}
|
|
301
|
-
return Math.max(0, 100 - Math.max(0, latencyMs as number) / 10);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* 将输出吞吐(tokens/s)转换为 0-100 分数。
|
|
306
|
-
* 公式:直接取值并限制在 0-100 范围内
|
|
307
|
-
*
|
|
308
|
-
* 设计理由:
|
|
309
|
-
* - 假设大多数 seller 的吞吐在 0-100 tok/s 范围内
|
|
310
|
-
* - 100 tok/s 及以上都得满分(上界问题)
|
|
311
|
-
* - 这种线性映射的局限:无法区分 100 tok/s 和 200 tok/s 的差异
|
|
312
|
-
* 未来可考虑对数缩放,例如 `100 * log(1 + value) / log(101)`
|
|
313
|
-
*
|
|
314
|
-
* @param value 吞吐(tokens/s),undefined 或非有限值返回 0 分
|
|
315
|
-
* @returns 0-100 分数
|
|
316
|
-
*/
|
|
317
|
-
function tokensPerSecondScore(value: number | undefined): number {
|
|
318
|
-
if (!Number.isFinite(value)) {
|
|
319
|
-
return 0;
|
|
320
|
-
}
|
|
321
|
-
return Math.max(0, Math.min(100, value as number));
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function discountScore(discountRatio: number | undefined): number {
|
|
325
|
-
if (!Number.isFinite(discountRatio)) {
|
|
326
|
-
return 0;
|
|
327
|
-
}
|
|
328
|
-
return Math.max(0, 100 * (1 - Math.max(0, discountRatio as number)));
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function finiteOr(value: number | undefined, fallback: number): number {
|
|
332
|
-
return Number.isFinite(value) ? value as number : fallback;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function compareFiniteAsc(a: number | undefined, b: number | undefined): number {
|
|
336
|
-
return finiteOr(a, Number.POSITIVE_INFINITY) - finiteOr(b, Number.POSITIVE_INFINITY);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function compareFiniteDesc(a: number | undefined, b: number | undefined): number {
|
|
340
|
-
return finiteOr(b, Number.NEGATIVE_INFINITY) - finiteOr(a, Number.NEGATIVE_INFINITY);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function effectiveTtftMs(candidate: RoutingCandidate): number | undefined {
|
|
344
|
-
return candidate.ttftMs ?? candidate.healthProbeLatencyMs ?? candidate.avgLatencyMs;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function compareRegistryOrder(a: RoutingCandidate, b: RoutingCandidate): number {
|
|
348
|
-
return a.registryOrder - b.registryOrder;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function routeReason(mode: SellerRoutingMode, scorer: SellerRoutingScorer, count: number): string {
|
|
352
|
-
return `${mode}:${scorer}:routes_${count}`;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function missingScoreInputs(candidate: RoutingCandidate): CandidateScoreBreakdown["missingInputs"] {
|
|
356
|
-
const missing: CandidateScoreBreakdown["missingInputs"] = [];
|
|
357
|
-
if (!Number.isFinite(candidate.healthScore)) missing.push("healthScore");
|
|
358
|
-
if (!Number.isFinite(candidate.ttftMs)) missing.push("ttftMs");
|
|
359
|
-
if (!Number.isFinite(candidate.avgTokensPerSecond)) missing.push("avgTokensPerSecond");
|
|
360
|
-
if (!Number.isFinite(candidate.discountRatio)) missing.push("discountRatio");
|
|
361
|
-
return missing;
|
|
362
|
-
}
|