@tokenbuddy/tokenbuddy 1.0.12 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -19
- 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 -27
- 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 +446 -34
- /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,14 +515,7 @@ function opencodeConfig(home: string, proxyUrl: string, config: ProviderRuntimeC
|
|
|
469
515
|
: {};
|
|
470
516
|
providers.tokenbuddy = {
|
|
471
517
|
name: "TokenBuddy",
|
|
472
|
-
|
|
473
|
-
// desktop 加载 custom provider 时报 ProviderModelNotFoundError(schema
|
|
474
|
-
// mismatch —— SDK 是 dynamic model 但 opencode 期望静态 languageModel 映射)。
|
|
475
|
-
// 1.0.11 改动对 opencode 用户实际跑不通,回滚到 chat completions。
|
|
476
|
-
// 备注:buyer 端 /v1/responses 协议支持仍然完整(之前已验证
|
|
477
|
-
// buyer→tbs-719577→code.shoestravel.xin 200 + 真实 LLM 响应)。等
|
|
478
|
-
// opencode 1.15+ 支持 openai-responses custom provider 再切默认。
|
|
479
|
-
npm: "@ai-sdk/openai",
|
|
518
|
+
npm: "@ai-sdk/openai-compatible",
|
|
480
519
|
options: {
|
|
481
520
|
apiKey: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
482
521
|
baseURL: openAiBaseUrl(proxyUrl),
|
|
@@ -496,6 +535,25 @@ function opencodeConfig(home: string, proxyUrl: string, config: ProviderRuntimeC
|
|
|
496
535
|
return [makeChange("opencode", configPath, "configure OpenCode provider for TokenBuddy proxy", jsonContent(current))];
|
|
497
536
|
}
|
|
498
537
|
|
|
538
|
+
function isOpencodeTokenBuddyConfigured(filePath: string): boolean {
|
|
539
|
+
const current = readJsonObject(filePath);
|
|
540
|
+
const tokenbuddy = readObjectField(readObjectField(current, "provider"), "tokenbuddy");
|
|
541
|
+
const options = readObjectField(tokenbuddy, "options");
|
|
542
|
+
if (!tokenbuddy || !options) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return tokenbuddy.npm === "@ai-sdk/openai-compatible" &&
|
|
547
|
+
options.apiKey === PROXY_ACCESS_TOKEN_PLACEHOLDER &&
|
|
548
|
+
typeof options.baseURL === "string" &&
|
|
549
|
+
options.baseURL.includes("127.0.0.1") &&
|
|
550
|
+
options.baseURL.endsWith("/v1") &&
|
|
551
|
+
typeof current.model === "string" &&
|
|
552
|
+
current.model.startsWith("tokenbuddy/") &&
|
|
553
|
+
typeof current.small_model === "string" &&
|
|
554
|
+
current.small_model.startsWith("tokenbuddy/");
|
|
555
|
+
}
|
|
556
|
+
|
|
499
557
|
function hermesConfig(home: string, proxyUrl: string, config: ProviderRuntimeConfig): ProviderFileChange[] {
|
|
500
558
|
const model = pickConfiguredModel(config);
|
|
501
559
|
const configPath = path.join(home, ".hermes", "settings.json");
|
|
@@ -557,9 +615,10 @@ const PROVIDERS: ProviderDefinition[] = [
|
|
|
557
615
|
name: "OpenCode",
|
|
558
616
|
commandName: "opencode",
|
|
559
617
|
configPath: (home) => path.join(home, ".config", "opencode", "opencode.json"),
|
|
618
|
+
isConfigured: isOpencodeTokenBuddyConfigured,
|
|
560
619
|
changes: opencodeConfig,
|
|
561
620
|
modelSelectionKind: "single-model",
|
|
562
|
-
protocolPreference: "
|
|
621
|
+
protocolPreference: "chat_completions",
|
|
563
622
|
},
|
|
564
623
|
{
|
|
565
624
|
id: "hermes",
|
|
@@ -584,14 +643,22 @@ function getProviderDefinition(providerId: ProviderId): ProviderDefinition {
|
|
|
584
643
|
return provider;
|
|
585
644
|
}
|
|
586
645
|
|
|
646
|
+
/**
|
|
647
|
+
* 探测所有 SUPPORTED_PROVIDER_IDS:检查可执行文件、配置文件、原生 hints 目录。
|
|
648
|
+
* 状态机:`configured`(已存在配置且被识别) / `installed`(可执行/配置存在但未配置) / `missing`(都未找到)。
|
|
649
|
+
*
|
|
650
|
+
* @param options 探测选项
|
|
651
|
+
* @returns 探测结果列表
|
|
652
|
+
*/
|
|
587
653
|
export function detectProviders(options: ProviderDetectOptions = {}): ProviderCandidate[] {
|
|
588
654
|
const home = resolveHome(options.home);
|
|
589
655
|
return PROVIDERS.map((provider) => {
|
|
590
656
|
const configPath = provider.configPath(home);
|
|
591
|
-
const
|
|
657
|
+
const configExists = fs.existsSync(configPath);
|
|
658
|
+
const configured = configExists && (provider.isConfigured?.(configPath, home) ?? true);
|
|
592
659
|
const executablePath = provider.commandName ? resolveExecutable(provider.commandName) : undefined;
|
|
593
660
|
const observedPaths = provider.observedPaths?.(home).filter((entry) => fs.existsSync(entry)) || [];
|
|
594
|
-
const installed = Boolean(executablePath) || observedPaths.length > 0;
|
|
661
|
+
const installed = Boolean(executablePath) || observedPaths.length > 0 || configExists;
|
|
595
662
|
const status: ProviderCandidate["status"] = configured
|
|
596
663
|
? "configured"
|
|
597
664
|
: installed
|
|
@@ -626,14 +693,33 @@ export function detectProviders(options: ProviderDetectOptions = {}): ProviderCa
|
|
|
626
693
|
});
|
|
627
694
|
}
|
|
628
695
|
|
|
696
|
+
/**
|
|
697
|
+
* 返回 provider 默认的协议偏好(未声明则返回 `undefined`,由运行时探测)。
|
|
698
|
+
*
|
|
699
|
+
* @param providerId provider ID
|
|
700
|
+
* @returns 协议偏好
|
|
701
|
+
*/
|
|
629
702
|
export function getProviderProtocolPreference(providerId: ProviderId): ProtocolPreference | undefined {
|
|
630
703
|
return getProviderDefinition(providerId).protocolPreference;
|
|
631
704
|
}
|
|
632
705
|
|
|
706
|
+
/**
|
|
707
|
+
* 返回 provider 的 model selection kind(决定 init 走 single-model 还是 claude-role-mapping 流程)。
|
|
708
|
+
*
|
|
709
|
+
* @param providerId provider ID
|
|
710
|
+
* @returns selection kind
|
|
711
|
+
*/
|
|
633
712
|
export function getProviderModelSelectionKind(providerId: ProviderId): ModelSelectionKind {
|
|
634
713
|
return getProviderDefinition(providerId).modelSelectionKind;
|
|
635
714
|
}
|
|
636
715
|
|
|
716
|
+
/**
|
|
717
|
+
* 预览 provider 安装会产生的文件变更(不实际写盘)。
|
|
718
|
+
* 给 `tb init` 在用户确认前展示"将要改哪些文件 / 改什么内容"。
|
|
719
|
+
*
|
|
720
|
+
* @param options 安装选项
|
|
721
|
+
* @returns 计划中的文件变更列表
|
|
722
|
+
*/
|
|
637
723
|
export function previewProviderInstall(options: ProviderInstallOptions): ProviderFileChange[] {
|
|
638
724
|
const home = resolveHome(options.home);
|
|
639
725
|
const providerIds = assertProviderIds(options.providers);
|
|
@@ -647,6 +733,14 @@ export function previewProviderInstall(options: ProviderInstallOptions): Provide
|
|
|
647
733
|
});
|
|
648
734
|
}
|
|
649
735
|
|
|
736
|
+
/**
|
|
737
|
+
* 实际写盘 provider 配置。
|
|
738
|
+
* 同时把每个 provider 的原始文件快照写入 `store`(用于 `tb rollback`)。
|
|
739
|
+
*
|
|
740
|
+
* @param options 安装选项
|
|
741
|
+
* @param store buyer store(用于快照 + runtime config 持久化)
|
|
742
|
+
* @returns 实际写盘结果列表
|
|
743
|
+
*/
|
|
650
744
|
export function applyProviderInstall(options: ProviderInstallOptions, store: BuyerStore): ProviderApplyResult[] {
|
|
651
745
|
const providerIds = assertProviderIds(options.providers);
|
|
652
746
|
const changes = previewProviderInstall(options);
|
|
@@ -673,10 +767,6 @@ export function applyProviderInstall(options: ProviderInstallOptions, store: Buy
|
|
|
673
767
|
store.saveProviderRuntimeConfig(providerId, runtimeConfig);
|
|
674
768
|
}
|
|
675
769
|
|
|
676
|
-
if (options.sellerRouting) {
|
|
677
|
-
store.saveDaemonRuntimeConfig(ROUTING_CONFIG_KEY, options.sellerRouting);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
770
|
const applied: ProviderApplyResult[] = [];
|
|
681
771
|
for (const change of changes) {
|
|
682
772
|
const dir = path.dirname(change.path);
|
|
@@ -693,6 +783,15 @@ export function applyProviderInstall(options: ProviderInstallOptions, store: Buy
|
|
|
693
783
|
return applied;
|
|
694
784
|
}
|
|
695
785
|
|
|
786
|
+
/**
|
|
787
|
+
* 回滚 provider 安装。
|
|
788
|
+
* 从 `store` 读取安装前的快照,恢复原文件(如果快照里有原始内容)。
|
|
789
|
+
* 没有快照的 provider 标记为 `missing_snapshot`。
|
|
790
|
+
*
|
|
791
|
+
* @param options 回滚选项
|
|
792
|
+
* @param store buyer store
|
|
793
|
+
* @returns 回滚结果列表
|
|
794
|
+
*/
|
|
696
795
|
export function rollbackProviderInstall(options: ProviderRollbackOptions, store: BuyerStore): ProviderRollbackResult[] {
|
|
697
796
|
const providerIds = assertProviderIds(options.providers);
|
|
698
797
|
const results: ProviderRollbackResult[] = [];
|
|
@@ -718,6 +817,5 @@ export function rollbackProviderInstall(options: ProviderRollbackOptions, store:
|
|
|
718
817
|
store.removeProviderInstallSnapshot(providerId);
|
|
719
818
|
store.removeProviderRuntimeConfig(providerId);
|
|
720
819
|
}
|
|
721
|
-
store.removeDaemonRuntimeConfig(ROUTING_CONFIG_KEY);
|
|
722
820
|
return results;
|
|
723
821
|
}
|
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
|
|