@tokenbuddy/tokenbuddy 1.0.36 → 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,1026 +0,0 @@
|
|
|
1
|
-
import { BuyerStore } from "./buyer-store.js";
|
|
2
|
-
import Table from "cli-table3";
|
|
3
|
-
import {
|
|
4
|
-
detectProviders,
|
|
5
|
-
PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
6
|
-
SUPPORTED_PROVIDER_IDS,
|
|
7
|
-
type ProviderCandidate,
|
|
8
|
-
type ProviderId,
|
|
9
|
-
type ProviderRuntimeConfig,
|
|
10
|
-
} from "./provider-install.js";
|
|
11
|
-
import {
|
|
12
|
-
discoverSellerBackedModels,
|
|
13
|
-
type ModelCatalogEntry,
|
|
14
|
-
type SellerCatalogEntry,
|
|
15
|
-
} from "./seller-catalog.js";
|
|
16
|
-
import {
|
|
17
|
-
printDoctorClawtipWallet,
|
|
18
|
-
readDoctorClawtipWallet,
|
|
19
|
-
type DoctorClawtipWalletSummary,
|
|
20
|
-
} from "./doctor-clawtip-wallet.js";
|
|
21
|
-
import type { PublicManualProviderConfig } from "./provider-routing-config.js";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* `tb doctor` 输出里 provider 行的形状。
|
|
25
|
-
* 在 `ProviderCandidate` 之上叠加 buyer 端持久化的 runtime config(如果存在)。
|
|
26
|
-
*/
|
|
27
|
-
export interface DoctorProviderView extends ProviderCandidate {
|
|
28
|
-
/** 持久化的 provider runtime config(opencode 的 `defaultModel` 等) */
|
|
29
|
-
runtimeConfig?: ProviderRuntimeConfig;
|
|
30
|
-
/** runtime config 的最近更新时间 */
|
|
31
|
-
runtimeConfigUpdatedAt?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* `tb doctor` 输出里 seller 行的形状。
|
|
36
|
-
* 字段命名与 `RegistrySeller` 对齐,但额外包含状态、错误信息、manifest sellerId 等。
|
|
37
|
-
*/
|
|
38
|
-
export interface DoctorSellerEntry {
|
|
39
|
-
id: string;
|
|
40
|
-
name?: string;
|
|
41
|
-
url: string;
|
|
42
|
-
status: string;
|
|
43
|
-
discountRatio?: number;
|
|
44
|
-
supportedProtocols?: string[];
|
|
45
|
-
paymentMethods?: string[];
|
|
46
|
-
manifestSellerId?: string;
|
|
47
|
-
modelCount?: number;
|
|
48
|
-
errorMessage?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* `tb doctor` 完整诊断结果,由 `collectDoctorDiagnostics` 返回。
|
|
53
|
-
*/
|
|
54
|
-
export interface DoctorDiagnostics {
|
|
55
|
-
/** 控制面 / 代理端点可达性 */
|
|
56
|
-
access: DoctorAccessSummary;
|
|
57
|
-
/** Clawtip 钱包信息 */
|
|
58
|
-
clawtipWallet: DoctorClawtipWalletSummary;
|
|
59
|
-
/** buyer 本地 Manual provider 状态 */
|
|
60
|
-
manualProviders: DoctorManualProvidersSummary;
|
|
61
|
-
/** 模型目录汇总 */
|
|
62
|
-
models: DoctorModelsSummary;
|
|
63
|
-
/** provider 安装 / 配置状态 */
|
|
64
|
-
providers: DoctorProviderView[];
|
|
65
|
-
/** seller 注册表 + 探测结果 */
|
|
66
|
-
sellers: DoctorSellersSummary;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
interface RemoteJsonResult<T> {
|
|
70
|
-
url: string;
|
|
71
|
-
available: boolean;
|
|
72
|
-
statusCode?: number;
|
|
73
|
-
data?: T;
|
|
74
|
-
error?: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
interface DoctorEndpointStatus {
|
|
78
|
-
id: string;
|
|
79
|
-
name: string;
|
|
80
|
-
url: string;
|
|
81
|
-
probeUrl?: string;
|
|
82
|
-
available: boolean;
|
|
83
|
-
requiresToken: boolean;
|
|
84
|
-
token?: string;
|
|
85
|
-
error?: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
interface DoctorSellerResponse {
|
|
89
|
-
registryUrl?: string;
|
|
90
|
-
version?: number;
|
|
91
|
-
defaultSeller?: string;
|
|
92
|
-
sellers: DoctorSellerEntry[];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
interface DoctorRegistryDocument {
|
|
96
|
-
version?: number;
|
|
97
|
-
defaultSeller?: string;
|
|
98
|
-
sellers: Array<{
|
|
99
|
-
id: string;
|
|
100
|
-
name?: string;
|
|
101
|
-
url: string;
|
|
102
|
-
supportedProtocols?: string[];
|
|
103
|
-
paymentMethods?: string[];
|
|
104
|
-
}>;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
interface DoctorModelsResponse {
|
|
108
|
-
object?: string;
|
|
109
|
-
registryUrl?: string;
|
|
110
|
-
data: ModelCatalogEntry[];
|
|
111
|
-
sellers?: DoctorSellerEntry[];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
interface DoctorManualProvidersResponse {
|
|
115
|
-
providers: PublicManualProviderConfig[];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* `tb doctor` 模型表的单行:按 model id 聚合 seller 的折扣 / 价格区间。
|
|
120
|
-
*/
|
|
121
|
-
export interface DoctorModelSummaryEntry {
|
|
122
|
-
id: string;
|
|
123
|
-
sellerCount: number;
|
|
124
|
-
localProviderCount: number;
|
|
125
|
-
sourceRange: string;
|
|
126
|
-
discountMin?: number;
|
|
127
|
-
discountMax?: number;
|
|
128
|
-
discountRange: string;
|
|
129
|
-
inputPriceMinMicrosPer1m?: number;
|
|
130
|
-
inputPriceMaxMicrosPer1m?: number;
|
|
131
|
-
outputPriceMinMicrosPer1m?: number;
|
|
132
|
-
outputPriceMaxMicrosPer1m?: number;
|
|
133
|
-
priceRange: string;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
interface DoctorFetchResults {
|
|
137
|
-
healthResult: RemoteJsonResult<Record<string, unknown>>;
|
|
138
|
-
sellersResult: RemoteJsonResult<DoctorSellerResponse>;
|
|
139
|
-
modelsResult: RemoteJsonResult<DoctorModelsResponse>;
|
|
140
|
-
manualProvidersResult: RemoteJsonResult<DoctorManualProvidersResponse>;
|
|
141
|
-
proxyModelsResult: RemoteJsonResult<{ object?: string; data?: Array<{ id?: string }> }>;
|
|
142
|
-
registryResult: RemoteJsonResult<DoctorRegistryDocument>;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
interface DoctorFetchPromises {
|
|
146
|
-
healthResult: Promise<RemoteJsonResult<Record<string, unknown>>>;
|
|
147
|
-
sellersResult: Promise<RemoteJsonResult<DoctorSellerResponse>>;
|
|
148
|
-
modelsResult: Promise<RemoteJsonResult<DoctorModelsResponse>>;
|
|
149
|
-
manualProvidersResult: Promise<RemoteJsonResult<DoctorManualProvidersResponse>>;
|
|
150
|
-
proxyModelsResult: Promise<RemoteJsonResult<{ object?: string; data?: Array<{ id?: string }> }>>;
|
|
151
|
-
registryResult: Promise<RemoteJsonResult<DoctorRegistryDocument>>;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
interface DoctorAccessAvailability {
|
|
155
|
-
sellersAvailable?: boolean;
|
|
156
|
-
sellersError?: string;
|
|
157
|
-
modelsAvailable?: boolean;
|
|
158
|
-
modelsError?: string;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
interface DoctorRenderOptions {
|
|
162
|
-
controlPort: number;
|
|
163
|
-
proxyPort: number;
|
|
164
|
-
daemonRunning: boolean;
|
|
165
|
-
daemonError?: string;
|
|
166
|
-
sellerRegistryUrl?: string;
|
|
167
|
-
providers: DoctorProviderView[];
|
|
168
|
-
writeLine?: (line: string) => void;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
interface DoctorCollectOptions {
|
|
172
|
-
controlPort: number;
|
|
173
|
-
proxyPort: number;
|
|
174
|
-
daemonRunning: boolean;
|
|
175
|
-
daemonError?: string;
|
|
176
|
-
sellerRegistryUrl?: string;
|
|
177
|
-
providers: DoctorProviderView[];
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
type DoctorAccessSummary = {
|
|
181
|
-
controlBaseUrl: string;
|
|
182
|
-
proxyBaseUrl: string;
|
|
183
|
-
openAiBaseUrl: string;
|
|
184
|
-
anthropicBaseUrl: string;
|
|
185
|
-
token: string;
|
|
186
|
-
endpoints: DoctorEndpointStatus[];
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
type DoctorSellersSummary = {
|
|
190
|
-
available: boolean;
|
|
191
|
-
registryUrl?: string;
|
|
192
|
-
version?: number;
|
|
193
|
-
defaultSeller?: string;
|
|
194
|
-
sellers: DoctorSellerEntry[];
|
|
195
|
-
error?: string;
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
export type DoctorManualProvidersSummary = {
|
|
199
|
-
available: boolean;
|
|
200
|
-
count: number;
|
|
201
|
-
configuredKeyCount: number;
|
|
202
|
-
providers: PublicManualProviderConfig[];
|
|
203
|
-
error?: string;
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* `tb doctor` 模型目录汇总:原始 `data` + 聚合后的 `grouped`(按 model id 分组)。
|
|
208
|
-
*/
|
|
209
|
-
export type DoctorModelsSummary = {
|
|
210
|
-
available: boolean;
|
|
211
|
-
count: number;
|
|
212
|
-
uniqueCount: number;
|
|
213
|
-
registryUrl?: string;
|
|
214
|
-
data: ModelCatalogEntry[];
|
|
215
|
-
grouped: DoctorModelSummaryEntry[];
|
|
216
|
-
sellers: DoctorSellerEntry[];
|
|
217
|
-
error?: string;
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
function parseJsonText(text: string): unknown {
|
|
221
|
-
if (!text.trim()) {
|
|
222
|
-
return undefined;
|
|
223
|
-
}
|
|
224
|
-
return JSON.parse(text) as unknown;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function extractRemoteError(payload: unknown, fallback: string): string {
|
|
228
|
-
if (payload && typeof payload === "object") {
|
|
229
|
-
if ("error" in payload) {
|
|
230
|
-
const error = (payload as { error?: unknown }).error;
|
|
231
|
-
if (typeof error === "string" && error.trim()) {
|
|
232
|
-
return error;
|
|
233
|
-
}
|
|
234
|
-
if (error && typeof error === "object" && "message" in error && typeof error.message === "string" && error.message.trim()) {
|
|
235
|
-
return error.message;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if ("message" in payload && typeof (payload as { message?: unknown }).message === "string" && (payload as { message: string }).message.trim()) {
|
|
239
|
-
return (payload as { message: string }).message;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return fallback;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function fetchJsonDocument<T>(url: string): Promise<RemoteJsonResult<T>> {
|
|
246
|
-
try {
|
|
247
|
-
const response = await fetch(url);
|
|
248
|
-
const text = await response.text();
|
|
249
|
-
const payload = text ? parseJsonText(text) : undefined;
|
|
250
|
-
if (!response.ok) {
|
|
251
|
-
return {
|
|
252
|
-
url,
|
|
253
|
-
available: false,
|
|
254
|
-
statusCode: response.status,
|
|
255
|
-
error: extractRemoteError(payload, `HTTP ${response.status}`),
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
return {
|
|
259
|
-
url,
|
|
260
|
-
available: true,
|
|
261
|
-
statusCode: response.status,
|
|
262
|
-
data: payload as T,
|
|
263
|
-
};
|
|
264
|
-
} catch (error: unknown) {
|
|
265
|
-
return {
|
|
266
|
-
url,
|
|
267
|
-
available: false,
|
|
268
|
-
error: error instanceof Error ? error.message : String(error),
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function providerRuntimeSummary(runtimeConfig?: ProviderRuntimeConfig): string | undefined {
|
|
274
|
-
if (!runtimeConfig) {
|
|
275
|
-
return undefined;
|
|
276
|
-
}
|
|
277
|
-
if (runtimeConfig.selectionKind === "single-model") {
|
|
278
|
-
return [
|
|
279
|
-
runtimeConfig.protocolPreference,
|
|
280
|
-
runtimeConfig.defaultModel,
|
|
281
|
-
].filter(Boolean).join(" · ");
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const bindings = [
|
|
285
|
-
runtimeConfig.roles.haiku?.upstreamModel ? `haiku=${runtimeConfig.roles.haiku.upstreamModel}` : undefined,
|
|
286
|
-
runtimeConfig.roles.sonnet?.upstreamModel ? `sonnet=${runtimeConfig.roles.sonnet.upstreamModel}` : undefined,
|
|
287
|
-
runtimeConfig.roles.opus?.upstreamModel ? `opus=${runtimeConfig.roles.opus.upstreamModel}` : undefined,
|
|
288
|
-
runtimeConfig.fallbackModel ? `fallback=${runtimeConfig.fallbackModel}` : undefined,
|
|
289
|
-
].filter(Boolean);
|
|
290
|
-
return [runtimeConfig.protocolPreference, ...bindings].join(" · ");
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function mergeDoctorSellerEntries(
|
|
294
|
-
configuredSellers: DoctorSellerEntry[],
|
|
295
|
-
probedSellers: DoctorSellerEntry[],
|
|
296
|
-
): DoctorSellerEntry[] {
|
|
297
|
-
const configuredById = new Map(configuredSellers.map((seller) => [seller.id, seller]));
|
|
298
|
-
const merged: DoctorSellerEntry[] = [];
|
|
299
|
-
|
|
300
|
-
for (const seller of probedSellers) {
|
|
301
|
-
const configured = configuredById.get(seller.id);
|
|
302
|
-
merged.push({
|
|
303
|
-
...configured,
|
|
304
|
-
...seller,
|
|
305
|
-
discountRatio: seller.discountRatio ?? configured?.discountRatio,
|
|
306
|
-
supportedProtocols: seller.supportedProtocols || configured?.supportedProtocols,
|
|
307
|
-
paymentMethods: seller.paymentMethods || configured?.paymentMethods,
|
|
308
|
-
modelCount: seller.modelCount ?? configured?.modelCount,
|
|
309
|
-
errorMessage: seller.errorMessage || configured?.errorMessage,
|
|
310
|
-
});
|
|
311
|
-
configuredById.delete(seller.id);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
for (const seller of configuredById.values()) {
|
|
315
|
-
merged.push(seller);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return merged;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function registrySellersToDoctorEntries(registry?: DoctorRegistryDocument): DoctorSellerEntry[] {
|
|
322
|
-
if (!registry?.sellers) {
|
|
323
|
-
return [];
|
|
324
|
-
}
|
|
325
|
-
return registry.sellers.map((seller) => ({
|
|
326
|
-
id: seller.id,
|
|
327
|
-
name: seller.name,
|
|
328
|
-
url: seller.url,
|
|
329
|
-
status: "configured",
|
|
330
|
-
supportedProtocols: seller.supportedProtocols || [],
|
|
331
|
-
paymentMethods: seller.paymentMethods || [],
|
|
332
|
-
}));
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function sellerCatalogEntriesToDoctorEntries(sellers: SellerCatalogEntry[]): DoctorSellerEntry[] {
|
|
336
|
-
return sellers.map((seller) => ({
|
|
337
|
-
id: seller.id,
|
|
338
|
-
name: seller.name,
|
|
339
|
-
url: seller.url,
|
|
340
|
-
status: seller.status,
|
|
341
|
-
discountRatio: seller.discountRatio,
|
|
342
|
-
supportedProtocols: seller.supportedProtocols,
|
|
343
|
-
paymentMethods: seller.paymentMethods,
|
|
344
|
-
manifestSellerId: seller.manifestSellerId,
|
|
345
|
-
modelCount: seller.modelCount,
|
|
346
|
-
errorMessage: seller.errorMessage,
|
|
347
|
-
}));
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function discountRatioFromSeller(seller: DoctorSellerEntry): number | undefined {
|
|
351
|
-
if (typeof seller.discountRatio === "number") {
|
|
352
|
-
return seller.discountRatio;
|
|
353
|
-
}
|
|
354
|
-
const match = seller.name?.match(/([0-9]+(?:\.[0-9]+)?)\s+discount/i);
|
|
355
|
-
if (!match) {
|
|
356
|
-
return undefined;
|
|
357
|
-
}
|
|
358
|
-
const parsed = Number(match[1]);
|
|
359
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function formatDiscountRatio(value: number): string {
|
|
363
|
-
const ratio = Math.max(0, value);
|
|
364
|
-
if (ratio === 0) return "免费";
|
|
365
|
-
if (Math.abs(ratio - 1) < 0.0001) return "原价";
|
|
366
|
-
const folded = Math.round(ratio * 100) / 10;
|
|
367
|
-
return `${Number.isInteger(folded) ? String(folded) : folded.toFixed(1)}折`;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function formatUsdPer1m(microsPer1m: number): string {
|
|
371
|
-
const usd = microsPer1m / 1_000_000;
|
|
372
|
-
return `$${usd.toFixed(2).replace(/\.?0+$/, "")}`;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function formatPriceRange(minMicros?: number, maxMicros?: number): string {
|
|
376
|
-
if (minMicros == null || maxMicros == null) {
|
|
377
|
-
return "-";
|
|
378
|
-
}
|
|
379
|
-
if (minMicros === maxMicros) {
|
|
380
|
-
return formatUsdPer1m(minMicros);
|
|
381
|
-
}
|
|
382
|
-
return `${formatUsdPer1m(minMicros)}~${formatUsdPer1m(maxMicros)}`;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function formatModelSourceRange(marketplaceCount: number, localProviderCount: number): string {
|
|
386
|
-
const parts = [
|
|
387
|
-
marketplaceCount > 0 ? `${marketplaceCount} marketplace` : undefined,
|
|
388
|
-
localProviderCount > 0 ? `${localProviderCount} local` : undefined,
|
|
389
|
-
].filter(Boolean);
|
|
390
|
-
return parts.length > 0 ? parts.join(" + ") : "-";
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function modelsHaveExplicitPriceData(models: ModelCatalogEntry[]): boolean {
|
|
394
|
-
return models.some((model) =>
|
|
395
|
-
typeof model.inputPriceMicrosPer1m === "number" ||
|
|
396
|
-
typeof model.outputPriceMicrosPer1m === "number"
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function buildDoctorModelSummaryEntries(
|
|
401
|
-
models: ModelCatalogEntry[],
|
|
402
|
-
sellers: DoctorSellerEntry[],
|
|
403
|
-
): DoctorModelSummaryEntry[] {
|
|
404
|
-
const grouped = new Map<string, {
|
|
405
|
-
sellerIds: Set<string>;
|
|
406
|
-
localProviderIds: Set<string>;
|
|
407
|
-
discounts: number[];
|
|
408
|
-
inputPrices: number[];
|
|
409
|
-
outputPrices: number[];
|
|
410
|
-
}>();
|
|
411
|
-
const discountBySellerId = new Map(
|
|
412
|
-
sellers
|
|
413
|
-
.map((seller) => [seller.id, discountRatioFromSeller(seller)] as const)
|
|
414
|
-
.filter((entry): entry is readonly [string, number] => typeof entry[1] === "number")
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
for (const model of models) {
|
|
418
|
-
const entry = grouped.get(model.id) || {
|
|
419
|
-
sellerIds: new Set<string>(),
|
|
420
|
-
localProviderIds: new Set<string>(),
|
|
421
|
-
discounts: [],
|
|
422
|
-
inputPrices: [],
|
|
423
|
-
outputPrices: [],
|
|
424
|
-
};
|
|
425
|
-
if (!entry.sellerIds.has(model.sellerId)) {
|
|
426
|
-
entry.sellerIds.add(model.sellerId);
|
|
427
|
-
if (model.paymentMethods.includes("provider_key")) {
|
|
428
|
-
entry.localProviderIds.add(model.sellerId);
|
|
429
|
-
}
|
|
430
|
-
const discount = discountBySellerId.get(model.sellerId);
|
|
431
|
-
if (typeof discount === "number") {
|
|
432
|
-
entry.discounts.push(discount);
|
|
433
|
-
}
|
|
434
|
-
if (typeof model.inputPriceMicrosPer1m === "number") {
|
|
435
|
-
entry.inputPrices.push(model.inputPriceMicrosPer1m);
|
|
436
|
-
}
|
|
437
|
-
if (typeof model.outputPriceMicrosPer1m === "number") {
|
|
438
|
-
entry.outputPrices.push(model.outputPriceMicrosPer1m);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
grouped.set(model.id, entry);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return Array.from(grouped.entries())
|
|
445
|
-
.map(([id, entry]) => {
|
|
446
|
-
const marketplaceCount = Math.max(0, entry.sellerIds.size - entry.localProviderIds.size);
|
|
447
|
-
const discountMin = entry.discounts.length > 0 ? Math.min(...entry.discounts) : undefined;
|
|
448
|
-
const discountMax = entry.discounts.length > 0 ? Math.max(...entry.discounts) : undefined;
|
|
449
|
-
const inputPriceMinMicrosPer1m = entry.inputPrices.length > 0 ? Math.min(...entry.inputPrices) : undefined;
|
|
450
|
-
const inputPriceMaxMicrosPer1m = entry.inputPrices.length > 0 ? Math.max(...entry.inputPrices) : undefined;
|
|
451
|
-
const outputPriceMinMicrosPer1m = entry.outputPrices.length > 0 ? Math.min(...entry.outputPrices) : undefined;
|
|
452
|
-
const outputPriceMaxMicrosPer1m = entry.outputPrices.length > 0 ? Math.max(...entry.outputPrices) : undefined;
|
|
453
|
-
const discountRange = discountMin == null || discountMax == null
|
|
454
|
-
? "-"
|
|
455
|
-
: discountMin === discountMax
|
|
456
|
-
? formatDiscountRatio(discountMin)
|
|
457
|
-
: `${formatDiscountRatio(discountMin)}~${formatDiscountRatio(discountMax)}`;
|
|
458
|
-
const priceRange = inputPriceMinMicrosPer1m == null || outputPriceMinMicrosPer1m == null
|
|
459
|
-
? "-"
|
|
460
|
-
: `in ${formatPriceRange(inputPriceMinMicrosPer1m, inputPriceMaxMicrosPer1m)} / out ${formatPriceRange(outputPriceMinMicrosPer1m, outputPriceMaxMicrosPer1m)}`;
|
|
461
|
-
return {
|
|
462
|
-
id,
|
|
463
|
-
sellerCount: entry.sellerIds.size,
|
|
464
|
-
localProviderCount: entry.localProviderIds.size,
|
|
465
|
-
sourceRange: formatModelSourceRange(marketplaceCount, entry.localProviderIds.size),
|
|
466
|
-
discountMin,
|
|
467
|
-
discountMax,
|
|
468
|
-
discountRange,
|
|
469
|
-
inputPriceMinMicrosPer1m,
|
|
470
|
-
inputPriceMaxMicrosPer1m,
|
|
471
|
-
outputPriceMinMicrosPer1m,
|
|
472
|
-
outputPriceMaxMicrosPer1m,
|
|
473
|
-
priceRange,
|
|
474
|
-
};
|
|
475
|
-
})
|
|
476
|
-
.sort((left, right) => left.id.localeCompare(right.id));
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function startDoctorFetches(
|
|
480
|
-
controlPort: number,
|
|
481
|
-
proxyPort: number,
|
|
482
|
-
daemonRunning: boolean,
|
|
483
|
-
daemonError: string | undefined,
|
|
484
|
-
sellerRegistryUrl?: string,
|
|
485
|
-
): DoctorFetchPromises {
|
|
486
|
-
const controlBaseUrl = `http://127.0.0.1:${controlPort}`;
|
|
487
|
-
const openAiBaseUrl = `http://127.0.0.1:${proxyPort}/v1`;
|
|
488
|
-
const unavailableMessage = daemonError || "tb-proxyd is not running";
|
|
489
|
-
|
|
490
|
-
if (!daemonRunning) {
|
|
491
|
-
return {
|
|
492
|
-
healthResult: Promise.resolve({ url: `${controlBaseUrl}/health`, available: false, error: unavailableMessage }),
|
|
493
|
-
sellersResult: Promise.resolve({ url: `${controlBaseUrl}/sellers`, available: false, error: unavailableMessage }),
|
|
494
|
-
modelsResult: Promise.resolve({ url: `${controlBaseUrl}/models`, available: false, error: unavailableMessage }),
|
|
495
|
-
manualProvidersResult: Promise.resolve({ url: `${controlBaseUrl}/routing/manual-providers`, available: false, error: unavailableMessage }),
|
|
496
|
-
proxyModelsResult: Promise.resolve({ url: `${openAiBaseUrl}/models`, available: false, error: unavailableMessage }),
|
|
497
|
-
registryResult: Promise.resolve({ url: sellerRegistryUrl || "", available: false, error: unavailableMessage }),
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return {
|
|
502
|
-
healthResult: fetchJsonDocument<Record<string, unknown>>(`${controlBaseUrl}/health`),
|
|
503
|
-
sellersResult: fetchJsonDocument<DoctorSellerResponse>(`${controlBaseUrl}/sellers`),
|
|
504
|
-
modelsResult: fetchJsonDocument<DoctorModelsResponse>(`${controlBaseUrl}/models`),
|
|
505
|
-
manualProvidersResult: fetchJsonDocument<DoctorManualProvidersResponse>(`${controlBaseUrl}/routing/manual-providers`),
|
|
506
|
-
proxyModelsResult: fetchJsonDocument<{ object?: string; data?: Array<{ id?: string }> }>(`${openAiBaseUrl}/models`),
|
|
507
|
-
registryResult: sellerRegistryUrl
|
|
508
|
-
? fetchJsonDocument<DoctorRegistryDocument>(sellerRegistryUrl)
|
|
509
|
-
: Promise.resolve({ url: "", available: false, error: "registry url unavailable" }),
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
async function resolveDoctorFetches(fetches: DoctorFetchPromises): Promise<DoctorFetchResults> {
|
|
514
|
-
const [
|
|
515
|
-
healthResult,
|
|
516
|
-
sellersResult,
|
|
517
|
-
modelsResult,
|
|
518
|
-
manualProvidersResult,
|
|
519
|
-
proxyModelsResult,
|
|
520
|
-
registryResult,
|
|
521
|
-
] = await Promise.all([
|
|
522
|
-
fetches.healthResult,
|
|
523
|
-
fetches.sellersResult,
|
|
524
|
-
fetches.modelsResult,
|
|
525
|
-
fetches.manualProvidersResult,
|
|
526
|
-
fetches.proxyModelsResult,
|
|
527
|
-
fetches.registryResult,
|
|
528
|
-
]);
|
|
529
|
-
return {
|
|
530
|
-
healthResult,
|
|
531
|
-
sellersResult,
|
|
532
|
-
modelsResult,
|
|
533
|
-
manualProvidersResult,
|
|
534
|
-
proxyModelsResult,
|
|
535
|
-
registryResult,
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
function buildDoctorSellerSummary(
|
|
540
|
-
sellersResult: RemoteJsonResult<DoctorSellerResponse>,
|
|
541
|
-
registryResult: RemoteJsonResult<DoctorRegistryDocument>,
|
|
542
|
-
sellerRegistryUrl?: string,
|
|
543
|
-
probedSellers: DoctorSellerEntry[] = [],
|
|
544
|
-
): DoctorSellersSummary {
|
|
545
|
-
const sellersData = sellersResult.data;
|
|
546
|
-
const configuredSellers = mergeDoctorSellerEntries(
|
|
547
|
-
registrySellersToDoctorEntries(registryResult.data),
|
|
548
|
-
sellersData?.sellers || [],
|
|
549
|
-
);
|
|
550
|
-
const mergedSellers = probedSellers.length > 0
|
|
551
|
-
? mergeDoctorSellerEntries(configuredSellers, probedSellers)
|
|
552
|
-
: configuredSellers;
|
|
553
|
-
|
|
554
|
-
return {
|
|
555
|
-
available: sellersResult.available || registryResult.available || mergedSellers.length > 0,
|
|
556
|
-
registryUrl: sellersData?.registryUrl || sellerRegistryUrl,
|
|
557
|
-
version: sellersData?.version || registryResult.data?.version,
|
|
558
|
-
defaultSeller: sellersData?.defaultSeller || registryResult.data?.defaultSeller,
|
|
559
|
-
sellers: mergedSellers,
|
|
560
|
-
error: sellersResult.error || registryResult.error,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function buildDoctorManualProvidersSummary(
|
|
565
|
-
manualProvidersResult: RemoteJsonResult<DoctorManualProvidersResponse>
|
|
566
|
-
): DoctorManualProvidersSummary {
|
|
567
|
-
const providers = manualProvidersResult.data?.providers || [];
|
|
568
|
-
return {
|
|
569
|
-
available: manualProvidersResult.available,
|
|
570
|
-
count: providers.length,
|
|
571
|
-
configuredKeyCount: providers.filter((provider) => provider.keyRef?.configured).length,
|
|
572
|
-
providers,
|
|
573
|
-
error: manualProvidersResult.error,
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function buildDoctorAccessSummary(
|
|
578
|
-
controlPort: number,
|
|
579
|
-
proxyPort: number,
|
|
580
|
-
healthResult: RemoteJsonResult<Record<string, unknown>>,
|
|
581
|
-
proxyModelsResult: RemoteJsonResult<{ object?: string; data?: Array<{ id?: string }> }>,
|
|
582
|
-
availability: DoctorAccessAvailability = {},
|
|
583
|
-
): DoctorAccessSummary {
|
|
584
|
-
const controlBaseUrl = `http://127.0.0.1:${controlPort}`;
|
|
585
|
-
const proxyBaseUrl = `http://127.0.0.1:${proxyPort}`;
|
|
586
|
-
const openAiBaseUrl = `${proxyBaseUrl}/v1`;
|
|
587
|
-
const anthropicBaseUrl = proxyBaseUrl;
|
|
588
|
-
|
|
589
|
-
return {
|
|
590
|
-
controlBaseUrl,
|
|
591
|
-
proxyBaseUrl,
|
|
592
|
-
openAiBaseUrl,
|
|
593
|
-
anthropicBaseUrl,
|
|
594
|
-
token: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
595
|
-
endpoints: [
|
|
596
|
-
{
|
|
597
|
-
id: "control.health",
|
|
598
|
-
name: "Control Plane Health",
|
|
599
|
-
url: `${controlBaseUrl}/health`,
|
|
600
|
-
available: healthResult.available,
|
|
601
|
-
requiresToken: false,
|
|
602
|
-
error: healthResult.error,
|
|
603
|
-
},
|
|
604
|
-
{
|
|
605
|
-
id: "control.sellers",
|
|
606
|
-
name: "Seller Registry",
|
|
607
|
-
url: `${controlBaseUrl}/sellers`,
|
|
608
|
-
available: availability.sellersAvailable ?? true,
|
|
609
|
-
requiresToken: false,
|
|
610
|
-
error: availability.sellersError,
|
|
611
|
-
},
|
|
612
|
-
{
|
|
613
|
-
id: "control.models",
|
|
614
|
-
name: "Seller-backed Models",
|
|
615
|
-
url: `${controlBaseUrl}/models`,
|
|
616
|
-
available: availability.modelsAvailable ?? true,
|
|
617
|
-
requiresToken: false,
|
|
618
|
-
error: availability.modelsError,
|
|
619
|
-
},
|
|
620
|
-
{
|
|
621
|
-
id: "proxy.openai",
|
|
622
|
-
name: "OpenAI-compatible Proxy",
|
|
623
|
-
url: openAiBaseUrl,
|
|
624
|
-
probeUrl: `${openAiBaseUrl}/models`,
|
|
625
|
-
available: proxyModelsResult.available,
|
|
626
|
-
requiresToken: true,
|
|
627
|
-
token: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
628
|
-
error: proxyModelsResult.error,
|
|
629
|
-
},
|
|
630
|
-
{
|
|
631
|
-
id: "proxy.anthropic",
|
|
632
|
-
name: "Anthropic-compatible Proxy",
|
|
633
|
-
url: anthropicBaseUrl,
|
|
634
|
-
probeUrl: `${openAiBaseUrl}/models`,
|
|
635
|
-
available: proxyModelsResult.available,
|
|
636
|
-
requiresToken: true,
|
|
637
|
-
token: PROXY_ACCESS_TOKEN_PLACEHOLDER,
|
|
638
|
-
error: proxyModelsResult.error,
|
|
639
|
-
},
|
|
640
|
-
],
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function buildDoctorModelsSummary(
|
|
645
|
-
modelsResult: RemoteJsonResult<DoctorModelsResponse>,
|
|
646
|
-
sellers: DoctorSellerEntry[],
|
|
647
|
-
): DoctorModelsSummary {
|
|
648
|
-
const modelsData = modelsResult.data;
|
|
649
|
-
const grouped = buildDoctorModelSummaryEntries(modelsData?.data || [], sellers);
|
|
650
|
-
return {
|
|
651
|
-
available: modelsResult.available,
|
|
652
|
-
count: modelsData?.data?.length || 0,
|
|
653
|
-
uniqueCount: grouped.length,
|
|
654
|
-
registryUrl: modelsData?.registryUrl,
|
|
655
|
-
data: modelsData?.data || [],
|
|
656
|
-
grouped,
|
|
657
|
-
sellers,
|
|
658
|
-
error: modelsResult.error,
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function providerStatusIcon(status: DoctorProviderView["status"]): string {
|
|
663
|
-
if (status === "configured") {
|
|
664
|
-
return "✅";
|
|
665
|
-
}
|
|
666
|
-
if (status === "installed") {
|
|
667
|
-
return "🟡";
|
|
668
|
-
}
|
|
669
|
-
return "🔘";
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
function remoteStatusIcon(available: boolean): string {
|
|
673
|
-
return available ? "✅" : "❌";
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function formatList(values?: string[]): string {
|
|
677
|
-
return values && values.length > 0 ? values.join(", ") : "-";
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function defaultWriter(line: string): void {
|
|
681
|
-
console.log(line);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function printDoctorAccess(access: DoctorAccessSummary, writeLine: (line: string) => void): void {
|
|
685
|
-
writeLine("Access check complete.");
|
|
686
|
-
writeLine(`Proxy token: ${access.token}`);
|
|
687
|
-
for (const endpoint of access.endpoints) {
|
|
688
|
-
writeLine(`${remoteStatusIcon(endpoint.available)} ${endpoint.name}`);
|
|
689
|
-
writeLine(` URL: ${endpoint.url}`);
|
|
690
|
-
if (endpoint.probeUrl) {
|
|
691
|
-
writeLine(` Probe: ${endpoint.probeUrl}`);
|
|
692
|
-
}
|
|
693
|
-
if (endpoint.requiresToken && endpoint.token) {
|
|
694
|
-
writeLine(` Token: ${endpoint.token}`);
|
|
695
|
-
}
|
|
696
|
-
if (!endpoint.available && endpoint.error) {
|
|
697
|
-
writeLine(` Error: ${endpoint.error}`);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function printDoctorSellers(sellers: DoctorSellersSummary, writeLine: (line: string) => void): void {
|
|
703
|
-
writeLine("Seller registry refresh complete.");
|
|
704
|
-
if (!sellers.available) {
|
|
705
|
-
writeLine(`❌ Seller registry unavailable: ${sellers.error || "unknown error"}`);
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
writeLine(`Registry: ${sellers.registryUrl || "-"}`);
|
|
709
|
-
writeLine(`Default seller: ${sellers.defaultSeller || "-"}`);
|
|
710
|
-
for (const seller of sellers.sellers) {
|
|
711
|
-
const label = seller.name ? `${seller.name} (${seller.id})` : seller.id;
|
|
712
|
-
const icon = seller.status === "ok" || seller.status === "configured"
|
|
713
|
-
? "✅"
|
|
714
|
-
: seller.status === "failed"
|
|
715
|
-
? "❌"
|
|
716
|
-
: "🔘";
|
|
717
|
-
writeLine(`${icon} ${label} [${seller.status}]`);
|
|
718
|
-
writeLine(` URL: ${seller.url}`);
|
|
719
|
-
writeLine(` Protocols: ${formatList(seller.supportedProtocols)}`);
|
|
720
|
-
writeLine(` Payments: ${formatList(seller.paymentMethods)}`);
|
|
721
|
-
if (seller.modelCount != null) {
|
|
722
|
-
writeLine(` Models: ${seller.modelCount}`);
|
|
723
|
-
}
|
|
724
|
-
if (seller.errorMessage) {
|
|
725
|
-
writeLine(` Error: ${seller.errorMessage}`);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function printDoctorManualProviders(
|
|
731
|
-
manualProviders: DoctorManualProvidersSummary,
|
|
732
|
-
writeLine: (line: string) => void
|
|
733
|
-
): void {
|
|
734
|
-
writeLine("Manual provider check complete.");
|
|
735
|
-
if (!manualProviders.available) {
|
|
736
|
-
writeLine(`❌ Manual providers unavailable: ${manualProviders.error || "unknown error"}`);
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
writeLine(`Manual providers: ${manualProviders.count}`);
|
|
740
|
-
writeLine(`Configured keys: ${manualProviders.configuredKeyCount}/${manualProviders.count}`);
|
|
741
|
-
for (const provider of manualProviders.providers) {
|
|
742
|
-
const icon = provider.enabled && provider.keyRef?.configured
|
|
743
|
-
? "✅"
|
|
744
|
-
: provider.enabled
|
|
745
|
-
? "⚠️"
|
|
746
|
-
: "🔘";
|
|
747
|
-
const keyRef = provider.keyRef
|
|
748
|
-
? `${provider.keyRef.kind}:${provider.keyRef.name} ${provider.keyRef.configured ? "configured" : "missing"}`
|
|
749
|
-
: "missing";
|
|
750
|
-
writeLine(`${icon} ${provider.name} (${provider.id}) [${provider.enabled ? "enabled" : "disabled"}]`);
|
|
751
|
-
writeLine(` Key: ${keyRef}`);
|
|
752
|
-
writeLine(` Models: ${provider.models.length}`);
|
|
753
|
-
writeLine(` Protocols: ${formatList(provider.supportedProtocols)}`);
|
|
754
|
-
if (provider.lastAccess) {
|
|
755
|
-
writeLine(` Last access: ${provider.lastAccess}`);
|
|
756
|
-
}
|
|
757
|
-
if (provider.status) {
|
|
758
|
-
writeLine(` Status: ${provider.status}`);
|
|
759
|
-
}
|
|
760
|
-
if (provider.errorMessage) {
|
|
761
|
-
writeLine(` Error: ${provider.errorMessage}`);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
/**
|
|
767
|
-
* 打印模型目录汇总(写到 `writeLine`,默认 stdout)。
|
|
768
|
-
*
|
|
769
|
-
* @param models 模型汇总
|
|
770
|
-
* @param writeLine 自定义输出函数(测试可注入)
|
|
771
|
-
*/
|
|
772
|
-
export function printDoctorModelsSummary(
|
|
773
|
-
models: DoctorModelsSummary,
|
|
774
|
-
writeLine: (line: string) => void = defaultWriter,
|
|
775
|
-
): void {
|
|
776
|
-
writeLine("Model catalog refresh complete.");
|
|
777
|
-
if (!models.available) {
|
|
778
|
-
writeLine(`❌ Model catalog unavailable: ${models.error || "unknown error"}`);
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
writeLine(`Unique models: ${models.uniqueCount}`);
|
|
782
|
-
writeLine(`Seller offers: ${models.count}`);
|
|
783
|
-
|
|
784
|
-
const table = new Table({
|
|
785
|
-
head: ["Model ID", "Seller Count", "Sources", "Discount Range", "Price Range"],
|
|
786
|
-
style: {
|
|
787
|
-
head: []
|
|
788
|
-
}
|
|
789
|
-
});
|
|
790
|
-
for (const entry of models.grouped) {
|
|
791
|
-
table.push([
|
|
792
|
-
entry.id,
|
|
793
|
-
String(entry.sellerCount),
|
|
794
|
-
entry.sourceRange,
|
|
795
|
-
entry.discountRange,
|
|
796
|
-
entry.priceRange
|
|
797
|
-
]);
|
|
798
|
-
}
|
|
799
|
-
writeLine(table.toString());
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
/**
|
|
803
|
-
* 读取 buyer 端已知的 provider 列表,把 runtime config(如果有)合并到 `DoctorProviderView`。
|
|
804
|
-
*
|
|
805
|
-
* @returns 每个 provider 的 view(数组)
|
|
806
|
-
*/
|
|
807
|
-
export function readDoctorProviders(): DoctorProviderView[] {
|
|
808
|
-
const store = new BuyerStore();
|
|
809
|
-
try {
|
|
810
|
-
const runtimeConfigByProvider = new Map<ProviderId, { config: ProviderRuntimeConfig; updatedAt: string }>();
|
|
811
|
-
for (const providerId of SUPPORTED_PROVIDER_IDS) {
|
|
812
|
-
const record = store.getProviderRuntimeConfig<ProviderRuntimeConfig>(providerId);
|
|
813
|
-
if (record) {
|
|
814
|
-
runtimeConfigByProvider.set(providerId, {
|
|
815
|
-
config: record.config,
|
|
816
|
-
updatedAt: record.updatedAt,
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
return detectProviders().map((provider) => {
|
|
822
|
-
const runtime = runtimeConfigByProvider.get(provider.id);
|
|
823
|
-
return {
|
|
824
|
-
...provider,
|
|
825
|
-
runtimeConfig: runtime?.config,
|
|
826
|
-
runtimeConfigUpdatedAt: runtime?.updatedAt,
|
|
827
|
-
};
|
|
828
|
-
});
|
|
829
|
-
} finally {
|
|
830
|
-
store.close();
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
/**
|
|
835
|
-
* 打印 provider 列表("Programming Terminals" 段)。
|
|
836
|
-
*
|
|
837
|
-
* @param providers provider view 列表
|
|
838
|
-
* @param writeLine 自定义输出函数
|
|
839
|
-
*/
|
|
840
|
-
export function printDoctorProviders(
|
|
841
|
-
providers: DoctorProviderView[],
|
|
842
|
-
writeLine: (line: string) => void = defaultWriter,
|
|
843
|
-
): void {
|
|
844
|
-
writeLine("\n--- Programming Terminals ---");
|
|
845
|
-
for (const provider of providers) {
|
|
846
|
-
writeLine(`${providerStatusIcon(provider.status)} ${provider.name} [${provider.status}]`);
|
|
847
|
-
if (provider.commandName) {
|
|
848
|
-
writeLine(` Command: ${provider.commandName}${provider.executablePath ? ` -> ${provider.executablePath}` : " (not found in PATH)"}`);
|
|
849
|
-
}
|
|
850
|
-
writeLine(` Config: ${provider.configPath}`);
|
|
851
|
-
if (provider.observedPaths && provider.observedPaths.length > 0) {
|
|
852
|
-
writeLine(` Native hints: ${provider.observedPaths.join(", ")}`);
|
|
853
|
-
}
|
|
854
|
-
const runtimeSummary = providerRuntimeSummary(provider.runtimeConfig);
|
|
855
|
-
if (runtimeSummary) {
|
|
856
|
-
writeLine(` Runtime: ${runtimeSummary}`);
|
|
857
|
-
}
|
|
858
|
-
writeLine(` Notes: ${provider.reason}`);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
/**
|
|
863
|
-
* 收集 `tb doctor` 需要的全部诊断信息。
|
|
864
|
-
* 1. 并发拉取 daemon health / sellers / models / proxy models / registry 五个端点。
|
|
865
|
-
* 2. 合并 registry 配置 + 探测结果 + catalog 拉取结果。
|
|
866
|
-
* 3. 返回完整 `DoctorDiagnostics`(供 JSON 消费者或自定义渲染使用)。
|
|
867
|
-
*
|
|
868
|
-
* @param options 收集选项(端口 / daemon 状态 / registry URL / providers)
|
|
869
|
-
* @returns 完整诊断结果
|
|
870
|
-
*/
|
|
871
|
-
export async function collectDoctorDiagnostics(options: DoctorCollectOptions): Promise<DoctorDiagnostics> {
|
|
872
|
-
const fetches = startDoctorFetches(
|
|
873
|
-
options.controlPort,
|
|
874
|
-
options.proxyPort,
|
|
875
|
-
options.daemonRunning,
|
|
876
|
-
options.daemonError,
|
|
877
|
-
options.sellerRegistryUrl,
|
|
878
|
-
);
|
|
879
|
-
const results = await resolveDoctorFetches(fetches);
|
|
880
|
-
const sellers = buildDoctorSellerSummary(
|
|
881
|
-
results.sellersResult,
|
|
882
|
-
results.registryResult,
|
|
883
|
-
options.sellerRegistryUrl,
|
|
884
|
-
results.modelsResult.data?.sellers || [],
|
|
885
|
-
);
|
|
886
|
-
const models = buildDoctorModelsSummary(results.modelsResult, sellers.sellers);
|
|
887
|
-
const manualProviders = buildDoctorManualProvidersSummary(results.manualProvidersResult);
|
|
888
|
-
|
|
889
|
-
return {
|
|
890
|
-
access: buildDoctorAccessSummary(
|
|
891
|
-
options.controlPort,
|
|
892
|
-
options.proxyPort,
|
|
893
|
-
results.healthResult,
|
|
894
|
-
results.proxyModelsResult,
|
|
895
|
-
{
|
|
896
|
-
sellersAvailable: sellers.available,
|
|
897
|
-
sellersError: sellers.error,
|
|
898
|
-
modelsAvailable: models.available,
|
|
899
|
-
modelsError: models.error,
|
|
900
|
-
},
|
|
901
|
-
),
|
|
902
|
-
clawtipWallet: readDoctorClawtipWallet(),
|
|
903
|
-
manualProviders,
|
|
904
|
-
models,
|
|
905
|
-
providers: options.providers,
|
|
906
|
-
sellers,
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/**
|
|
911
|
-
* 单独收集 `tb models` 需要的模型汇总(不包含 providers)。
|
|
912
|
-
* daemon 拉到的数据缺价格时,回退用 `discoverSellerBackedModels` 重新拉 catalog。
|
|
913
|
-
*
|
|
914
|
-
* @param options 同 `collectDoctorDiagnostics`(去掉 providers)
|
|
915
|
-
* @returns 模型汇总
|
|
916
|
-
*/
|
|
917
|
-
export async function collectDoctorModelsSummary(options: Omit<DoctorCollectOptions, "providers">): Promise<DoctorModelsSummary> {
|
|
918
|
-
const diagnostics = await collectDoctorDiagnostics({
|
|
919
|
-
...options,
|
|
920
|
-
providers: [],
|
|
921
|
-
});
|
|
922
|
-
if (modelsHaveExplicitPriceData(diagnostics.models.data) || !options.sellerRegistryUrl) {
|
|
923
|
-
return diagnostics.models;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
try {
|
|
927
|
-
const catalog = await discoverSellerBackedModels(options.sellerRegistryUrl);
|
|
928
|
-
const sellers = sellerCatalogEntriesToDoctorEntries(catalog.sellers);
|
|
929
|
-
return buildDoctorModelsSummary({
|
|
930
|
-
url: options.sellerRegistryUrl,
|
|
931
|
-
available: true,
|
|
932
|
-
data: {
|
|
933
|
-
object: "list",
|
|
934
|
-
registryUrl: catalog.registryUrl,
|
|
935
|
-
data: catalog.models,
|
|
936
|
-
},
|
|
937
|
-
}, sellers);
|
|
938
|
-
} catch {
|
|
939
|
-
return diagnostics.models;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
/**
|
|
945
|
-
* 渐进式渲染 `tb doctor`:分阶段打印(clawtip → access → sellers → models),边等边输出,避免用户长时间看不到输出。
|
|
946
|
-
*
|
|
947
|
-
* @param options 渲染选项(含写入函数)
|
|
948
|
-
* @returns 完整 `DoctorDiagnostics`
|
|
949
|
-
*/
|
|
950
|
-
export async function renderDoctorDiagnosticsProgressively(options: DoctorRenderOptions): Promise<DoctorDiagnostics> {
|
|
951
|
-
const writeLine = options.writeLine || defaultWriter;
|
|
952
|
-
const clawtipWallet = readDoctorClawtipWallet();
|
|
953
|
-
const fetches = startDoctorFetches(
|
|
954
|
-
options.controlPort,
|
|
955
|
-
options.proxyPort,
|
|
956
|
-
options.daemonRunning,
|
|
957
|
-
options.daemonError,
|
|
958
|
-
options.sellerRegistryUrl,
|
|
959
|
-
);
|
|
960
|
-
|
|
961
|
-
printDoctorClawtipWallet(clawtipWallet, writeLine);
|
|
962
|
-
|
|
963
|
-
writeLine("\n--- Access Interfaces ---");
|
|
964
|
-
writeLine("Checking local control plane and proxy endpoints...");
|
|
965
|
-
const [healthResult, proxyModelsResult] = await Promise.all([
|
|
966
|
-
fetches.healthResult,
|
|
967
|
-
fetches.proxyModelsResult,
|
|
968
|
-
]);
|
|
969
|
-
const access = buildDoctorAccessSummary(
|
|
970
|
-
options.controlPort,
|
|
971
|
-
options.proxyPort,
|
|
972
|
-
healthResult,
|
|
973
|
-
proxyModelsResult,
|
|
974
|
-
);
|
|
975
|
-
printDoctorAccess(access, writeLine);
|
|
976
|
-
|
|
977
|
-
writeLine("\n--- Manual Providers ---");
|
|
978
|
-
writeLine("Reading local custom provider status...");
|
|
979
|
-
const manualProvidersResult = await fetches.manualProvidersResult;
|
|
980
|
-
const manualProviders = buildDoctorManualProvidersSummary(manualProvidersResult);
|
|
981
|
-
printDoctorManualProviders(manualProviders, writeLine);
|
|
982
|
-
|
|
983
|
-
writeLine("\n--- Sellers ---");
|
|
984
|
-
writeLine("Refreshing seller registry...");
|
|
985
|
-
const [sellersResult, registryResult] = await Promise.all([
|
|
986
|
-
fetches.sellersResult,
|
|
987
|
-
fetches.registryResult,
|
|
988
|
-
]);
|
|
989
|
-
const partialSellers = buildDoctorSellerSummary(
|
|
990
|
-
sellersResult,
|
|
991
|
-
registryResult,
|
|
992
|
-
options.sellerRegistryUrl,
|
|
993
|
-
);
|
|
994
|
-
printDoctorSellers(partialSellers, writeLine);
|
|
995
|
-
|
|
996
|
-
const modelsResult = await fetches.modelsResult;
|
|
997
|
-
const finalSellers = buildDoctorSellerSummary(
|
|
998
|
-
sellersResult,
|
|
999
|
-
registryResult,
|
|
1000
|
-
options.sellerRegistryUrl,
|
|
1001
|
-
modelsResult.data?.sellers || [],
|
|
1002
|
-
);
|
|
1003
|
-
const models = buildDoctorModelsSummary(modelsResult, finalSellers.sellers);
|
|
1004
|
-
access.endpoints = buildDoctorAccessSummary(
|
|
1005
|
-
options.controlPort,
|
|
1006
|
-
options.proxyPort,
|
|
1007
|
-
healthResult,
|
|
1008
|
-
proxyModelsResult,
|
|
1009
|
-
{
|
|
1010
|
-
sellersAvailable: finalSellers.available,
|
|
1011
|
-
sellersError: finalSellers.error,
|
|
1012
|
-
modelsAvailable: models.available,
|
|
1013
|
-
modelsError: models.error,
|
|
1014
|
-
},
|
|
1015
|
-
).endpoints;
|
|
1016
|
-
writeLine("\nModel catalog hidden in `tb doctor`. Run `tb models` for the current model summary.");
|
|
1017
|
-
|
|
1018
|
-
return {
|
|
1019
|
-
access,
|
|
1020
|
-
clawtipWallet,
|
|
1021
|
-
manualProviders,
|
|
1022
|
-
models,
|
|
1023
|
-
providers: options.providers,
|
|
1024
|
-
sellers: finalSellers,
|
|
1025
|
-
};
|
|
1026
|
-
}
|