@tokenbuddy/tokenbuddy 1.0.11 → 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.
- package/dist/src/buyer-store.d.ts +61 -0
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +12 -0
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts +47 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +287 -63
- package/dist/src/cli.js.map +1 -1
- package/dist/src/credit-tracker.d.ts +26 -0
- package/dist/src/credit-tracker.d.ts.map +1 -1
- package/dist/src/credit-tracker.js +8 -0
- package/dist/src/credit-tracker.js.map +1 -1
- package/dist/src/daemon.d.ts +29 -3
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +292 -65
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-clawtip-wallet.d.ts +25 -0
- package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -1
- package/dist/src/doctor-clawtip-wallet.js +13 -0
- package/dist/src/doctor-clawtip-wallet.js.map +1 -1
- package/dist/src/doctor-diagnostics.d.ts +63 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -1
- package/dist/src/doctor-diagnostics.js +39 -1
- package/dist/src/doctor-diagnostics.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/init-clawtip-activation.d.ts +103 -0
- package/dist/src/init-clawtip-activation.d.ts.map +1 -1
- package/dist/src/init-clawtip-activation.js +60 -0
- package/dist/src/init-clawtip-activation.js.map +1 -1
- package/dist/src/init-payment-options.d.ts +124 -0
- package/dist/src/init-payment-options.d.ts.map +1 -1
- package/dist/src/init-payment-options.js +68 -0
- package/dist/src/init-payment-options.js.map +1 -1
- package/dist/src/model-index.d.ts +9 -0
- package/dist/src/model-index.d.ts.map +1 -1
- package/dist/src/model-index.js.map +1 -1
- package/dist/src/prewarm-cache.d.ts +89 -0
- package/dist/src/prewarm-cache.d.ts.map +1 -1
- package/dist/src/prewarm-cache.js +14 -1
- package/dist/src/prewarm-cache.js.map +1 -1
- package/dist/src/prewarm-scheduler.d.ts +62 -3
- package/dist/src/prewarm-scheduler.d.ts.map +1 -1
- package/dist/src/prewarm-scheduler.js +39 -8
- package/dist/src/prewarm-scheduler.js.map +1 -1
- package/dist/src/provider-install.d.ts +89 -3
- package/dist/src/provider-install.d.ts.map +1 -1
- package/dist/src/provider-install.js +77 -17
- package/dist/src/provider-install.js.map +1 -1
- package/dist/src/route-failover.d.ts +48 -0
- package/dist/src/route-failover.d.ts.map +1 -1
- package/dist/src/route-failover.js.map +1 -1
- package/dist/src/seller-catalog.d.ts +158 -10
- package/dist/src/seller-catalog.d.ts.map +1 -1
- package/dist/src/seller-catalog.js +79 -5
- package/dist/src/seller-catalog.js.map +1 -1
- package/dist/src/seller-metadata-cache.d.ts +29 -0
- package/dist/src/seller-metadata-cache.d.ts.map +1 -0
- package/dist/src/seller-metadata-cache.js +71 -0
- package/dist/src/seller-metadata-cache.js.map +1 -0
- package/dist/src/seller-pool.d.ts +71 -0
- package/dist/src/seller-pool.d.ts.map +1 -1
- package/dist/src/seller-pool.js +6 -1
- package/dist/src/seller-pool.js.map +1 -1
- package/dist/src/seller-route-planner.d.ts +118 -0
- package/dist/src/seller-route-planner.d.ts.map +1 -0
- package/dist/src/seller-route-planner.js +160 -0
- package/dist/src/seller-route-planner.js.map +1 -0
- package/dist/src/seller-routing-config.d.ts +69 -0
- package/dist/src/seller-routing-config.d.ts.map +1 -0
- package/dist/src/seller-routing-config.js +164 -0
- package/dist/src/seller-routing-config.js.map +1 -0
- package/dist/src/seller-routing-strategy.d.ts +118 -0
- package/dist/src/seller-routing-strategy.d.ts.map +1 -0
- package/dist/src/seller-routing-strategy.js +183 -0
- package/dist/src/seller-routing-strategy.js.map +1 -0
- package/dist/src/stream-failover.d.ts +23 -0
- package/dist/src/stream-failover.d.ts.map +1 -1
- package/dist/src/stream-failover.js +4 -0
- package/dist/src/stream-failover.js.map +1 -1
- package/dist/src/tb-proxyd.js +7 -21
- package/dist/src/tb-proxyd.js.map +1 -1
- package/dist/src/terminal-detect.d.ts +51 -0
- package/dist/src/terminal-detect.d.ts.map +1 -1
- package/dist/src/terminal-detect.js +42 -0
- package/dist/src/terminal-detect.js.map +1 -1
- package/dist/src/terminal-image.d.ts +41 -0
- package/dist/src/terminal-image.d.ts.map +1 -1
- package/dist/src/terminal-image.js +15 -0
- package/dist/src/terminal-image.js.map +1 -1
- package/package.json +1 -1
- package/src/buyer-store.ts +61 -0
- package/src/cli.ts +330 -68
- package/src/credit-tracker.ts +26 -0
- package/src/daemon.ts +363 -72
- package/src/doctor-clawtip-wallet.ts +25 -0
- package/src/doctor-diagnostics.ts +63 -1
- package/src/index.ts +4 -0
- package/src/init-clawtip-activation.ts +103 -0
- package/src/init-payment-options.ts +124 -0
- package/src/model-index.ts +9 -0
- package/src/prewarm-cache.ts +99 -1
- package/src/prewarm-scheduler.ts +97 -12
- package/src/provider-install.ts +125 -25
- package/src/route-failover.ts +48 -0
- package/src/seller-catalog.ts +158 -12
- package/src/seller-metadata-cache.ts +91 -0
- package/src/seller-pool.ts +77 -1
- package/src/seller-route-planner.ts +323 -0
- package/src/seller-routing-config.ts +198 -0
- package/src/seller-routing-strategy.ts +316 -0
- package/src/stream-failover.ts +23 -0
- package/src/tb-proxyd.ts +7 -23
- package/src/terminal-detect.ts +51 -0
- package/src/terminal-image.ts +41 -0
- package/tests/cli-routing.test.ts +287 -0
- package/tests/daemon-classify.test.ts +431 -0
- package/tests/daemon-roles.test.ts +92 -0
- package/tests/seller-catalog-utilities.test.ts +70 -0
- package/tests/seller-metadata-cache.test.ts +89 -0
- package/tests/seller-route-planner.test.ts +150 -0
- package/tests/seller-routing-config.test.ts +111 -0
- package/tests/seller-routing-strategy.test.ts +166 -0
- package/tests/tokenbuddy.test.ts +447 -33
- /package/{src → tests}/credit-tracker.test.ts +0 -0
- /package/{src → tests}/model-index.test.ts +0 -0
- /package/{src → tests}/prewarm-cache.test.ts +0 -0
- /package/{src → tests}/prewarm-scheduler.test.ts +0 -0
- /package/{src → tests}/route-failover.test.ts +0 -0
- /package/{src → tests}/seller-catalog-413.test.ts +0 -0
- /package/{src → tests}/seller-pool.test.ts +0 -0
- /package/{src → tests}/stream-failover.test.ts +0 -0
- /package/{src → tests}/thousand-seller.test.ts +0 -0
package/src/provider-install.ts
CHANGED
|
@@ -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,12 +515,7 @@ function opencodeConfig(home: string, proxyUrl: string, config: ProviderRuntimeC
|
|
|
469
515
|
: {};
|
|
470
516
|
providers.tokenbuddy = {
|
|
471
517
|
name: "TokenBuddy",
|
|
472
|
-
|
|
473
|
-
// 验证:之前默认 @ai-sdk/openai(chat completions)也能 work,但 Responses API
|
|
474
|
-
// 才是上游(code.shoestravel.xin 等)原生支持的 SSE 事件链,type 字段更标准
|
|
475
|
-
// (response.created / response.output_text.delta / response.completed),
|
|
476
|
-
// 让 buyer 端 SseUsageExtractor 能稳定 parse usage 字段。
|
|
477
|
-
npm: "@ai-sdk/openai-responses",
|
|
518
|
+
npm: "@ai-sdk/openai-compatible",
|
|
478
519
|
options: {
|
|
479
520
|
apiKey: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
480
521
|
baseURL: openAiBaseUrl(proxyUrl),
|
|
@@ -494,6 +535,25 @@ function opencodeConfig(home: string, proxyUrl: string, config: ProviderRuntimeC
|
|
|
494
535
|
return [makeChange("opencode", configPath, "configure OpenCode provider for TokenBuddy proxy", jsonContent(current))];
|
|
495
536
|
}
|
|
496
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
|
+
|
|
497
557
|
function hermesConfig(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[] {
|
|
498
558
|
const model = pickConfiguredModel(config);
|
|
499
559
|
const configPath = path.join(home, ".hermes", "settings.json");
|
|
@@ -555,9 +615,10 @@ const PROVIDERS: ProviderDefinition[] = [
|
|
|
555
615
|
name: "OpenCode",
|
|
556
616
|
commandName: "opencode",
|
|
557
617
|
configPath: (home) => path.join(home, ".config", "opencode", "opencode.json"),
|
|
618
|
+
isConfigured: isOpencodeTokenBuddyConfigured,
|
|
558
619
|
changes: opencodeConfig,
|
|
559
620
|
modelSelectionKind: "single-model",
|
|
560
|
-
protocolPreference: "
|
|
621
|
+
protocolPreference: "chat_completions",
|
|
561
622
|
},
|
|
562
623
|
{
|
|
563
624
|
id: "hermes",
|
|
@@ -582,14 +643,22 @@ function getProviderDefinition(providerId: ProviderId): ProviderDefinition {
|
|
|
582
643
|
return provider;
|
|
583
644
|
}
|
|
584
645
|
|
|
646
|
+
/**
|
|
647
|
+
* 探测所有 SUPPORTED_PROVIDER_IDS:检查可执行文件、配置文件、原生 hints 目录。
|
|
648
|
+
* 状态机:`configured`(已存在配置且被识别) / `installed`(可执行/配置存在但未配置) / `missing`(都未找到)。
|
|
649
|
+
*
|
|
650
|
+
* @param options 探测选项
|
|
651
|
+
* @returns 探测结果列表
|
|
652
|
+
*/
|
|
585
653
|
export function detectProviders(options: ProviderDetectOptions = {}): ProviderCandidate[] {
|
|
586
654
|
const home = resolveHome(options.home);
|
|
587
655
|
return PROVIDERS.map((provider) => {
|
|
588
656
|
const configPath = provider.configPath(home);
|
|
589
|
-
const
|
|
657
|
+
const configExists = fs.existsSync(configPath);
|
|
658
|
+
const configured = configExists && (provider.isConfigured?.(configPath, home) ?? true);
|
|
590
659
|
const executablePath = provider.commandName ? resolveExecutable(provider.commandName) : undefined;
|
|
591
660
|
const observedPaths = provider.observedPaths?.(home).filter((entry) => fs.existsSync(entry)) || [];
|
|
592
|
-
const installed = Boolean(executablePath) || observedPaths.length > 0;
|
|
661
|
+
const installed = Boolean(executablePath) || observedPaths.length > 0 || configExists;
|
|
593
662
|
const status: ProviderCandidate["status"] = configured
|
|
594
663
|
? "configured"
|
|
595
664
|
: installed
|
|
@@ -624,14 +693,33 @@ export function detectProviders(options: ProviderDetectOptions = {}): ProviderCa
|
|
|
624
693
|
});
|
|
625
694
|
}
|
|
626
695
|
|
|
696
|
+
/**
|
|
697
|
+
* 返回 provider 默认的协议偏好(未声明则返回 `undefined`,由运行时探测)。
|
|
698
|
+
*
|
|
699
|
+
* @param providerId provider ID
|
|
700
|
+
* @returns 协议偏好
|
|
701
|
+
*/
|
|
627
702
|
export function getProviderProtocolPreference(providerId: ProviderId): ProtocolPreference | undefined {
|
|
628
703
|
return getProviderDefinition(providerId).protocolPreference;
|
|
629
704
|
}
|
|
630
705
|
|
|
706
|
+
/**
|
|
707
|
+
* 返回 provider 的 model selection kind(决定 init 走 single-model 还是 claude-role-mapping 流程)。
|
|
708
|
+
*
|
|
709
|
+
* @param providerId provider ID
|
|
710
|
+
* @returns selection kind
|
|
711
|
+
*/
|
|
631
712
|
export function getProviderModelSelectionKind(providerId: ProviderId): ModelSelectionKind {
|
|
632
713
|
return getProviderDefinition(providerId).modelSelectionKind;
|
|
633
714
|
}
|
|
634
715
|
|
|
716
|
+
/**
|
|
717
|
+
* 预览 provider 安装会产生的文件变更(不实际写盘)。
|
|
718
|
+
* 给 `tb init` 在用户确认前展示"将要改哪些文件 / 改什么内容"。
|
|
719
|
+
*
|
|
720
|
+
* @param options 安装选项
|
|
721
|
+
* @returns 计划中的文件变更列表
|
|
722
|
+
*/
|
|
635
723
|
export function previewProviderInstall(options: ProviderInstallOptions): ProviderFileChange[] {
|
|
636
724
|
const home = resolveHome(options.home);
|
|
637
725
|
const providerIds = assertProviderIds(options.providers);
|
|
@@ -645,6 +733,14 @@ export function previewProviderInstall(options: ProviderInstallOptions): Provide
|
|
|
645
733
|
});
|
|
646
734
|
}
|
|
647
735
|
|
|
736
|
+
/**
|
|
737
|
+
* 实际写盘 provider 配置。
|
|
738
|
+
* 同时把每个 provider 的原始文件快照写入 `store`(用于 `tb rollback`)。
|
|
739
|
+
*
|
|
740
|
+
* @param options 安装选项
|
|
741
|
+
* @param store buyer store(用于快照 + runtime config 持久化)
|
|
742
|
+
* @returns 实际写盘结果列表
|
|
743
|
+
*/
|
|
648
744
|
export function applyProviderInstall(options: ProviderInstallOptions, store: BuyerStore): ProviderApplyResult[] {
|
|
649
745
|
const providerIds = assertProviderIds(options.providers);
|
|
650
746
|
const changes = previewProviderInstall(options);
|
|
@@ -671,10 +767,6 @@ export function applyProviderInstall(options: ProviderInstallOptions, store: Buy
|
|
|
671
767
|
store.saveProviderRuntimeConfig(providerId, runtimeConfig);
|
|
672
768
|
}
|
|
673
769
|
|
|
674
|
-
if (options.sellerRouting) {
|
|
675
|
-
store.saveDaemonRuntimeConfig(ROUTING_CONFIG_KEY, options.sellerRouting);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
770
|
const applied: ProviderApplyResult[] = [];
|
|
679
771
|
for (const change of changes) {
|
|
680
772
|
const dir = path.dirname(change.path);
|
|
@@ -691,6 +783,15 @@ export function applyProviderInstall(options: ProviderInstallOptions, store: Buy
|
|
|
691
783
|
return applied;
|
|
692
784
|
}
|
|
693
785
|
|
|
786
|
+
/**
|
|
787
|
+
* 回滚 provider 安装。
|
|
788
|
+
* 从 `store` 读取安装前的快照,恢复原文件(如果快照里有原始内容)。
|
|
789
|
+
* 没有快照的 provider 标记为 `missing_snapshot`。
|
|
790
|
+
*
|
|
791
|
+
* @param options 回滚选项
|
|
792
|
+
* @param store buyer store
|
|
793
|
+
* @returns 回滚结果列表
|
|
794
|
+
*/
|
|
694
795
|
export function rollbackProviderInstall(options: ProviderRollbackOptions, store: BuyerStore): ProviderRollbackResult[] {
|
|
695
796
|
const providerIds = assertProviderIds(options.providers);
|
|
696
797
|
const results: ProviderRollbackResult[] = [];
|
|
@@ -716,6 +817,5 @@ export function rollbackProviderInstall(options: ProviderRollbackOptions, store:
|
|
|
716
817
|
store.removeProviderInstallSnapshot(providerId);
|
|
717
818
|
store.removeProviderRuntimeConfig(providerId);
|
|
718
819
|
}
|
|
719
|
-
store.removeDaemonRuntimeConfig(ROUTING_CONFIG_KEY);
|
|
720
820
|
return results;
|
|
721
821
|
}
|
package/src/route-failover.ts
CHANGED
|
@@ -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
|
|