@tokenbuddy/tb-admin 1.0.14 → 1.0.27
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/bootstrap-registry.d.ts +1 -0
- package/dist/src/bootstrap-registry.d.ts.map +1 -1
- package/dist/src/bootstrap-registry.js.map +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +294 -13
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +12 -3
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +12 -8
- package/dist/src/client.js.map +1 -1
- package/dist/src/display-format.d.ts +39 -0
- package/dist/src/display-format.d.ts.map +1 -0
- package/dist/src/display-format.js +354 -0
- package/dist/src/display-format.js.map +1 -0
- package/dist/src/server-cmd.d.ts +25 -1
- package/dist/src/server-cmd.d.ts.map +1 -1
- package/dist/src/server-cmd.js +116 -16
- package/dist/src/server-cmd.js.map +1 -1
- package/dist/src/ui-actions.d.ts +90 -0
- package/dist/src/ui-actions.d.ts.map +1 -0
- package/dist/src/ui-actions.js +823 -0
- package/dist/src/ui-actions.js.map +1 -0
- package/dist/src/ui-command.d.ts +4 -0
- package/dist/src/ui-command.d.ts.map +1 -0
- package/dist/src/ui-command.js +37 -0
- package/dist/src/ui-command.js.map +1 -0
- package/dist/src/ui-server.d.ts +22 -0
- package/dist/src/ui-server.d.ts.map +1 -0
- package/dist/src/ui-server.js +261 -0
- package/dist/src/ui-server.js.map +1 -0
- package/dist/src/ui-state.d.ts +140 -0
- package/dist/src/ui-state.d.ts.map +1 -0
- package/dist/src/ui-state.js +438 -0
- package/dist/src/ui-state.js.map +1 -0
- package/dist/src/ui-static.d.ts +2 -0
- package/dist/src/ui-static.d.ts.map +1 -0
- package/dist/src/ui-static.js +469 -0
- package/dist/src/ui-static.js.map +1 -0
- package/dist/src/upstream-balance-probe.d.ts +41 -0
- package/dist/src/upstream-balance-probe.d.ts.map +1 -0
- package/dist/src/upstream-balance-probe.js +379 -0
- package/dist/src/upstream-balance-probe.js.map +1 -0
- package/package.json +1 -1
- package/src/bootstrap-registry.ts +1 -0
- package/src/cli.ts +335 -13
- package/src/client.ts +13 -8
- package/src/display-format.ts +398 -0
- package/src/server-cmd.ts +145 -20
- package/src/ui-actions.ts +958 -0
- package/src/ui-command.ts +39 -0
- package/src/ui-server.ts +322 -0
- package/src/ui-state.ts +614 -0
- package/src/ui-static.ts +472 -0
- package/src/upstream-balance-probe.ts +505 -0
- package/tests/admin.test.ts +1404 -2
package/src/ui-state.ts
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { AdminClient } from "./client.js";
|
|
2
|
+
import { ConfigManager, type AdminProfile } from "./config.js";
|
|
3
|
+
import type { SellerRegistryDocument, SellerRegistryEntry } from "./bootstrap-registry.js";
|
|
4
|
+
import {
|
|
5
|
+
BalanceProbeCache,
|
|
6
|
+
type BalanceSnapshot,
|
|
7
|
+
probeUpstreamBalance
|
|
8
|
+
} from "./upstream-balance-probe.js";
|
|
9
|
+
|
|
10
|
+
export type RegistryStatus = "active" | "draining" | "offline" | "pending" | "unknown";
|
|
11
|
+
export type NodeStatus = RegistryStatus | "busy_capacity" | "auth_unknown";
|
|
12
|
+
export type UpstreamStatus = "healthy" | "degraded" | "unhealthy" | "unknown";
|
|
13
|
+
|
|
14
|
+
export interface SellerRow {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
app?: string;
|
|
19
|
+
profile?: string;
|
|
20
|
+
url: string;
|
|
21
|
+
registryStatus: RegistryStatus;
|
|
22
|
+
nodeStatus: NodeStatus;
|
|
23
|
+
region?: string;
|
|
24
|
+
upstreamDomain: string;
|
|
25
|
+
upstreamStatus: UpstreamStatus;
|
|
26
|
+
upstreamBalanceUsdMicros?: number;
|
|
27
|
+
upstreamBalanceCurrency?: string;
|
|
28
|
+
upstreamBalanceSource?: string;
|
|
29
|
+
upstreamBalanceFetchedAt?: string;
|
|
30
|
+
upstreamBalanceError?: string;
|
|
31
|
+
upstreamRechargeUrl?: string;
|
|
32
|
+
discountRatio?: number;
|
|
33
|
+
capacityUsed?: number;
|
|
34
|
+
capacityLimit?: number;
|
|
35
|
+
ttftMs?: number;
|
|
36
|
+
avgInferenceMs?: number;
|
|
37
|
+
lastInferenceMs?: number;
|
|
38
|
+
avgTokensPerSecond?: number;
|
|
39
|
+
lastTokensPerSecond?: number;
|
|
40
|
+
latencySamples?: number;
|
|
41
|
+
lastSwitchAt?: string;
|
|
42
|
+
modelsCount?: number;
|
|
43
|
+
specs?: {
|
|
44
|
+
memoryGb?: number;
|
|
45
|
+
machines?: number;
|
|
46
|
+
region?: string;
|
|
47
|
+
modelsCount?: number;
|
|
48
|
+
};
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SellerModelRow {
|
|
53
|
+
upstreamModel: string;
|
|
54
|
+
billingModel: string;
|
|
55
|
+
inputPrice?: string;
|
|
56
|
+
outputPrice?: string;
|
|
57
|
+
ttftMs?: number;
|
|
58
|
+
avgInferenceMs?: number;
|
|
59
|
+
avgTokensPerSecond?: number;
|
|
60
|
+
latencySamples?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SellerDetail {
|
|
64
|
+
row: SellerRow;
|
|
65
|
+
configuration: {
|
|
66
|
+
registryStatus: string;
|
|
67
|
+
region?: string;
|
|
68
|
+
upstreamUrl?: string;
|
|
69
|
+
upstreamApiKeyMasked?: string;
|
|
70
|
+
upstreamStatus: UpstreamStatus;
|
|
71
|
+
upstreamBalance?: string;
|
|
72
|
+
upstreamBalanceSource?: string;
|
|
73
|
+
upstreamBalanceFetchedAt?: string;
|
|
74
|
+
upstreamBalanceError?: string;
|
|
75
|
+
upstreamBalanceProbeTemplate?: string;
|
|
76
|
+
upstreamBalanceProbeUrl?: string;
|
|
77
|
+
upstreamBalanceProbeUserId?: string;
|
|
78
|
+
upstreamBalanceProbeRechargeUrl?: string;
|
|
79
|
+
upstreamBalanceUrl?: string;
|
|
80
|
+
upstreamUserId?: string;
|
|
81
|
+
upstreamRechargeUrl?: string;
|
|
82
|
+
markupRatio?: number;
|
|
83
|
+
discountRatio?: number;
|
|
84
|
+
maxConnections?: number;
|
|
85
|
+
maxQueueDepth?: number;
|
|
86
|
+
};
|
|
87
|
+
models: SellerModelRow[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface BootstrapSummary {
|
|
91
|
+
status: "available" | "unavailable";
|
|
92
|
+
url?: string;
|
|
93
|
+
profile?: string;
|
|
94
|
+
registryVersion?: number;
|
|
95
|
+
registryUpdatedAt?: string;
|
|
96
|
+
sellerEntries: number;
|
|
97
|
+
defaultSeller?: string;
|
|
98
|
+
regions: string[];
|
|
99
|
+
error?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface BootstrapConfigSummary {
|
|
103
|
+
status: "available" | "unavailable";
|
|
104
|
+
sellerRegistryPath?: string;
|
|
105
|
+
bindHost?: string;
|
|
106
|
+
bindPort?: number;
|
|
107
|
+
allowLocalSellerUrls?: boolean;
|
|
108
|
+
clawtip?: {
|
|
109
|
+
payToMasked?: string;
|
|
110
|
+
sm4KeyMasked?: string;
|
|
111
|
+
skillSlug?: string;
|
|
112
|
+
skillId?: string;
|
|
113
|
+
description?: string;
|
|
114
|
+
resourceUrl?: string;
|
|
115
|
+
activationFeeFen?: number;
|
|
116
|
+
microsPerFen?: number;
|
|
117
|
+
};
|
|
118
|
+
error?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface AdminUiStateOptions {
|
|
122
|
+
configManager: ConfigManager;
|
|
123
|
+
configPath?: string;
|
|
124
|
+
profile?: string;
|
|
125
|
+
url?: string;
|
|
126
|
+
token?: string;
|
|
127
|
+
fetchJson?: (url: string, init?: RequestInit) => Promise<unknown>;
|
|
128
|
+
balanceFetch?: typeof fetch;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface ProfileMatch {
|
|
132
|
+
name?: string;
|
|
133
|
+
profile?: AdminProfile;
|
|
134
|
+
localProfile?: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface SellerSnapshot {
|
|
138
|
+
row: SellerRow;
|
|
139
|
+
status?: any;
|
|
140
|
+
service?: any;
|
|
141
|
+
upstreams?: any;
|
|
142
|
+
config?: any;
|
|
143
|
+
balance?: BalanceSnapshot;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export class AdminUiState {
|
|
147
|
+
private readonly configManager: ConfigManager;
|
|
148
|
+
private readonly options: AdminUiStateOptions;
|
|
149
|
+
private readonly fetchJson: (url: string, init?: RequestInit) => Promise<unknown>;
|
|
150
|
+
private readonly balanceCache = new BalanceProbeCache();
|
|
151
|
+
|
|
152
|
+
constructor(options: AdminUiStateOptions) {
|
|
153
|
+
this.options = options;
|
|
154
|
+
this.configManager = options.configPath ? new ConfigManager(options.configPath) : options.configManager;
|
|
155
|
+
this.fetchJson = options.fetchJson || defaultFetchJson;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public async bootstrap(): Promise<BootstrapSummary> {
|
|
159
|
+
try {
|
|
160
|
+
const document = await this.fetchRegistry();
|
|
161
|
+
const profile = this.activeBootstrapProfile();
|
|
162
|
+
return {
|
|
163
|
+
status: "available",
|
|
164
|
+
url: profile.profile?.url || this.options.url,
|
|
165
|
+
profile: profile.name,
|
|
166
|
+
registryVersion: document.version,
|
|
167
|
+
registryUpdatedAt: document.updatedAt,
|
|
168
|
+
sellerEntries: document.sellers.length,
|
|
169
|
+
defaultSeller: document.defaultSeller,
|
|
170
|
+
regions: Array.from(new Set(document.sellers.map((seller) => seller.region).filter((region): region is string => Boolean(region)))).sort()
|
|
171
|
+
};
|
|
172
|
+
} catch (err: any) {
|
|
173
|
+
const profile = this.activeBootstrapProfile();
|
|
174
|
+
return {
|
|
175
|
+
status: "unavailable",
|
|
176
|
+
url: profile.profile?.url || this.options.url,
|
|
177
|
+
profile: profile.name,
|
|
178
|
+
sellerEntries: 0,
|
|
179
|
+
regions: [],
|
|
180
|
+
error: err.message
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public async bootstrapConfig(): Promise<BootstrapConfigSummary> {
|
|
186
|
+
try {
|
|
187
|
+
const profile = this.activeBootstrapProfile();
|
|
188
|
+
if (!profile.profile) {
|
|
189
|
+
throw new Error("No bootstrap profile found. Configure an admin profile or pass --url and --token.");
|
|
190
|
+
}
|
|
191
|
+
const client = new AdminClient(profile.profile.url, profile.profile.token);
|
|
192
|
+
const config = await client.get("/operator/config");
|
|
193
|
+
return {
|
|
194
|
+
status: "available",
|
|
195
|
+
sellerRegistryPath: stringValue(config.sellerRegistryPath),
|
|
196
|
+
bindHost: stringValue(config.bind?.host),
|
|
197
|
+
bindPort: numberValue(config.bind?.port),
|
|
198
|
+
allowLocalSellerUrls: Boolean(config.allowLocalSellerUrls),
|
|
199
|
+
clawtip: {
|
|
200
|
+
payToMasked: maskSecret(config.clawtip?.payTo),
|
|
201
|
+
sm4KeyMasked: maskSecret(config.clawtip?.sm4KeyBase64),
|
|
202
|
+
skillSlug: stringValue(config.clawtip?.skillSlug),
|
|
203
|
+
skillId: stringValue(config.clawtip?.skillId),
|
|
204
|
+
description: stringValue(config.clawtip?.description),
|
|
205
|
+
resourceUrl: stringValue(config.clawtip?.resourceUrl),
|
|
206
|
+
activationFeeFen: numberValue(config.clawtip?.activationFeeFen),
|
|
207
|
+
microsPerFen: numberValue(config.clawtip?.microsPerFen)
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
} catch (err: any) {
|
|
211
|
+
return {
|
|
212
|
+
status: "unavailable",
|
|
213
|
+
error: err.message
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public async sellers(): Promise<SellerRow[]> {
|
|
219
|
+
const document = await this.fetchRegistry();
|
|
220
|
+
const snapshots = await Promise.all(document.sellers.map((seller) => this.sellerSnapshot(seller)));
|
|
221
|
+
return snapshots.map((snapshot) => snapshot.row);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public async sellerDetail(id: string): Promise<SellerDetail> {
|
|
225
|
+
const document = await this.fetchRegistry();
|
|
226
|
+
const entry = document.sellers.find((seller) => seller.id === id || seller.name === id || seller.app === id);
|
|
227
|
+
if (!entry) {
|
|
228
|
+
throw new Error(`seller \`${id}\` not found in bootstrap registry`);
|
|
229
|
+
}
|
|
230
|
+
const snapshot = await this.sellerSnapshot(entry);
|
|
231
|
+
const config = snapshot.config?.config || snapshot.config || {};
|
|
232
|
+
const upstreams = snapshot.upstreams || {};
|
|
233
|
+
return {
|
|
234
|
+
row: snapshot.row,
|
|
235
|
+
configuration: {
|
|
236
|
+
registryStatus: snapshot.row.registryStatus,
|
|
237
|
+
region: entry.region,
|
|
238
|
+
upstreamUrl: stringValue(config.upstreamUrl || upstreams.upstreamUrl),
|
|
239
|
+
upstreamApiKeyMasked: maskApiKey(config.upstreamApiKey || upstreams.upstreamApiKey),
|
|
240
|
+
upstreamStatus: snapshot.row.upstreamStatus,
|
|
241
|
+
upstreamBalance: balanceString(snapshot.balance),
|
|
242
|
+
upstreamBalanceSource: snapshot.row.upstreamBalanceSource,
|
|
243
|
+
upstreamBalanceFetchedAt: snapshot.row.upstreamBalanceFetchedAt,
|
|
244
|
+
upstreamBalanceError: snapshot.row.upstreamBalanceError,
|
|
245
|
+
upstreamBalanceProbeTemplate: stringValue(config.upstreamBalanceProbe?.template),
|
|
246
|
+
upstreamBalanceProbeUrl: stringValue(config.upstreamBalanceProbe?.url || config.upstreamBalanceUrl),
|
|
247
|
+
upstreamBalanceProbeUserId: stringValue(config.upstreamBalanceProbe?.userId || config.upstreamUserId),
|
|
248
|
+
upstreamBalanceProbeRechargeUrl: stringValue(config.upstreamBalanceProbe?.rechargeUrl || config.upstreamRechargeUrl),
|
|
249
|
+
upstreamBalanceUrl: stringValue(config.upstreamBalanceUrl),
|
|
250
|
+
upstreamUserId: stringValue(config.upstreamUserId),
|
|
251
|
+
upstreamRechargeUrl: snapshot.row.upstreamRechargeUrl,
|
|
252
|
+
markupRatio: numberValue(config.markupRatio ?? upstreams.markupRatio),
|
|
253
|
+
discountRatio: numberValue(config.discountRatio ?? upstreams.discountRatio),
|
|
254
|
+
maxConnections: numberValue(config.maxConnections ?? snapshot.service?.capacity?.maxConnections ?? snapshot.status?.capacity?.maxConnections),
|
|
255
|
+
maxQueueDepth: numberValue(config.maxQueueDepth ?? snapshot.service?.capacity?.maxQueueDepth ?? snapshot.status?.capacity?.maxQueueDepth)
|
|
256
|
+
},
|
|
257
|
+
models: modelRows(upstreams, config, snapshot.status)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public async rawSellerConfig(id: string): Promise<{ entry: SellerRegistryEntry; profileName?: string; config: any }> {
|
|
262
|
+
const document = await this.fetchRegistry();
|
|
263
|
+
const entry = document.sellers.find((seller) => seller.id === id || seller.name === id || seller.app === id);
|
|
264
|
+
if (!entry) {
|
|
265
|
+
throw new Error(`seller \`${id}\` not found in bootstrap registry`);
|
|
266
|
+
}
|
|
267
|
+
const match = this.matchSellerProfile(entry);
|
|
268
|
+
if (!match.profile) {
|
|
269
|
+
throw new Error(`seller \`${entry.id}\` has no matching local admin profile`);
|
|
270
|
+
}
|
|
271
|
+
const response = await this.fetchSellerAdminJson(match.profile, "/operator/admin/config");
|
|
272
|
+
return { entry, profileName: match.localProfile ? match.name : undefined, config: response.config || response };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public async fetchRegistry(): Promise<SellerRegistryDocument> {
|
|
276
|
+
const profile = this.activeBootstrapProfile();
|
|
277
|
+
const baseUrl = this.options.url || profile.profile?.url;
|
|
278
|
+
if (!baseUrl) {
|
|
279
|
+
throw new Error("No bootstrap profile found. Configure an admin profile or pass --url.");
|
|
280
|
+
}
|
|
281
|
+
const document = await this.fetchJson(`${trimSlash(baseUrl)}/registry/sellers`) as SellerRegistryDocument;
|
|
282
|
+
if (!document || !Array.isArray(document.sellers)) {
|
|
283
|
+
throw new Error("bootstrap registry response did not include sellers");
|
|
284
|
+
}
|
|
285
|
+
return document;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private activeBootstrapProfile(): ProfileMatch {
|
|
289
|
+
const envProfile = process.env.TOKENBUDDY_ADMIN_PROFILE;
|
|
290
|
+
const target = this.options.profile || envProfile;
|
|
291
|
+
if (this.options.url) {
|
|
292
|
+
return {
|
|
293
|
+
name: target,
|
|
294
|
+
profile: this.options.token ? { url: this.options.url, token: this.options.token } : undefined,
|
|
295
|
+
localProfile: false
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const config = this.configManager.load();
|
|
299
|
+
const name = target || config.default_profile || Object.keys(config.profiles)[0];
|
|
300
|
+
return {
|
|
301
|
+
name,
|
|
302
|
+
profile: name ? config.profiles[name] : undefined,
|
|
303
|
+
localProfile: Boolean(name && config.profiles[name])
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private matchSellerProfile(entry: SellerRegistryEntry): ProfileMatch {
|
|
308
|
+
const config = this.configManager.load();
|
|
309
|
+
if (entry.profile && config.profiles[entry.profile]) {
|
|
310
|
+
return { name: entry.profile, profile: config.profiles[entry.profile], localProfile: true };
|
|
311
|
+
}
|
|
312
|
+
const byUrl = Object.entries(config.profiles).find(([, profile]) => trimSlash(profile.url) === trimSlash(entry.url));
|
|
313
|
+
if (byUrl) {
|
|
314
|
+
return { name: byUrl[0], profile: byUrl[1], localProfile: true };
|
|
315
|
+
}
|
|
316
|
+
const entryHost = hostName(entry.url);
|
|
317
|
+
const near = Object.entries(config.profiles).find(([name, profile]) => {
|
|
318
|
+
const haystack = `${name} ${profile.url} ${hostName(profile.url)}`.toLowerCase();
|
|
319
|
+
return [entry.id, entry.app, entryHost].some((value) => value && haystack.includes(value.toLowerCase()));
|
|
320
|
+
});
|
|
321
|
+
if (near) {
|
|
322
|
+
return { name: near[0], profile: near[1], localProfile: true };
|
|
323
|
+
}
|
|
324
|
+
const provider = this.configManager.getSellerProvider("fly");
|
|
325
|
+
if (provider?.operator_secret) {
|
|
326
|
+
return {
|
|
327
|
+
name: entry.app || entry.id,
|
|
328
|
+
profile: {
|
|
329
|
+
url: entry.url,
|
|
330
|
+
token: provider.operator_secret
|
|
331
|
+
},
|
|
332
|
+
localProfile: false
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
return {};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private async sellerSnapshot(entry: SellerRegistryEntry): Promise<SellerSnapshot> {
|
|
339
|
+
const match = this.matchSellerProfile(entry);
|
|
340
|
+
const baseRow = baseSellerRow(entry, match.name);
|
|
341
|
+
if (!match.profile) {
|
|
342
|
+
return {
|
|
343
|
+
row: {
|
|
344
|
+
...baseRow,
|
|
345
|
+
nodeStatus: "auth_unknown",
|
|
346
|
+
error: "No matching local admin profile"
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const [status, service, upstreams, config] = await Promise.all([
|
|
353
|
+
this.fetchSellerAdminJson(match.profile, "/operator/status").catch((err: any) => ({ error: err.message })),
|
|
354
|
+
this.fetchSellerAdminJson(match.profile, "/operator/admin/service").catch((err: any) => ({ error: err.message })),
|
|
355
|
+
this.fetchSellerAdminJson(match.profile, "/operator/admin/upstreams").catch((err: any) => ({ error: err.message })),
|
|
356
|
+
this.fetchSellerAdminJson(match.profile, "/operator/admin/config").catch((err: any) => ({ error: err.message }))
|
|
357
|
+
]);
|
|
358
|
+
const configDocument = config?.config || config || {};
|
|
359
|
+
const balance = await this.balanceSnapshot(configDocument, upstreams);
|
|
360
|
+
return {
|
|
361
|
+
status,
|
|
362
|
+
service,
|
|
363
|
+
upstreams,
|
|
364
|
+
config,
|
|
365
|
+
balance,
|
|
366
|
+
row: mergeSellerRow(baseRow, entry, status, service, upstreams, configDocument, balance)
|
|
367
|
+
};
|
|
368
|
+
} catch (err: any) {
|
|
369
|
+
return {
|
|
370
|
+
row: {
|
|
371
|
+
...baseRow,
|
|
372
|
+
nodeStatus: "unknown",
|
|
373
|
+
error: err.message
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private async fetchSellerAdminJson(profile: AdminProfile, pathName: string): Promise<any> {
|
|
380
|
+
return this.fetchJson(`${trimSlash(profile.url)}${pathName}`, {
|
|
381
|
+
headers: {
|
|
382
|
+
"Content-Type": "application/json",
|
|
383
|
+
Authorization: `Bearer ${profile.token}`
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private async balanceSnapshot(config: any, upstreams: any): Promise<BalanceSnapshot | undefined> {
|
|
389
|
+
if (config?.error) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
if (stringValue(config.upstreamBalanceProbe?.template) === "none") {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
return probeUpstreamBalance({
|
|
396
|
+
upstreamUrl: stringValue(config.upstreamUrl || upstreams?.upstreamUrl),
|
|
397
|
+
upstreamBalanceUrl: stringValue(config.upstreamBalanceUrl || upstreams?.upstreamBalanceUrl),
|
|
398
|
+
upstreamApiKey: stringValue(config.upstreamApiKey),
|
|
399
|
+
upstreamUserId: stringValue(config.upstreamUserId),
|
|
400
|
+
upstreamBalanceProbe: objectValue(config.upstreamBalanceProbe)
|
|
401
|
+
}, {
|
|
402
|
+
fetch: this.options.balanceFetch,
|
|
403
|
+
cache: this.balanceCache
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function defaultFetchJson(url: string, init?: RequestInit): Promise<unknown> {
|
|
409
|
+
const response = await fetch(url, init);
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
const text = await response.text();
|
|
412
|
+
throw new Error(`HTTP Error ${response.status}: ${text || response.statusText}`);
|
|
413
|
+
}
|
|
414
|
+
const text = await response.text();
|
|
415
|
+
return text ? JSON.parse(text) : {};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function baseSellerRow(entry: SellerRegistryEntry, profile?: string): SellerRow {
|
|
419
|
+
return {
|
|
420
|
+
id: entry.id,
|
|
421
|
+
name: entry.id,
|
|
422
|
+
description: entry.name && entry.name !== entry.id ? entry.name : undefined,
|
|
423
|
+
app: entry.app,
|
|
424
|
+
profile: profile || entry.profile,
|
|
425
|
+
url: entry.url,
|
|
426
|
+
registryStatus: registryStatus(entry.status),
|
|
427
|
+
nodeStatus: registryStatus(entry.status),
|
|
428
|
+
region: entry.region,
|
|
429
|
+
upstreamDomain: hostName(entry.url) || "unknown",
|
|
430
|
+
upstreamStatus: "unknown",
|
|
431
|
+
modelsCount: entry.modelsCount ?? entry.models?.length ?? entry.sampleModels?.length,
|
|
432
|
+
specs: {
|
|
433
|
+
region: entry.region,
|
|
434
|
+
modelsCount: entry.modelsCount ?? entry.models?.length ?? entry.sampleModels?.length
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function mergeSellerRow(
|
|
440
|
+
base: SellerRow,
|
|
441
|
+
entry: SellerRegistryEntry,
|
|
442
|
+
status: any,
|
|
443
|
+
service: any,
|
|
444
|
+
upstreams: any,
|
|
445
|
+
config: any,
|
|
446
|
+
balance: BalanceSnapshot | undefined
|
|
447
|
+
): SellerRow {
|
|
448
|
+
const normalizedUpstreams = upstreamDocument(upstreams);
|
|
449
|
+
const capacity = status?.capacity || service?.capacity || {};
|
|
450
|
+
const upstreamUrl = stringValue(config?.upstreamUrl || normalizedUpstreams?.upstreamUrl || service?.upstreamUrl) || entry.url;
|
|
451
|
+
const error = firstError(status, service, upstreams);
|
|
452
|
+
return {
|
|
453
|
+
...base,
|
|
454
|
+
nodeStatus: error ? "unknown" : nodeStatus(status?.status || entry.status),
|
|
455
|
+
upstreamDomain: hostName(upstreamUrl) || base.upstreamDomain,
|
|
456
|
+
upstreamStatus: upstreamStatus(status?.upstream?.status || normalizedUpstreams?.status),
|
|
457
|
+
discountRatio: numberValue(config?.discountRatio ?? normalizedUpstreams?.discountRatio),
|
|
458
|
+
capacityUsed: numberValue(capacity.activeConnections),
|
|
459
|
+
capacityLimit: numberValue(capacity.maxConnections),
|
|
460
|
+
ttftMs: numberValue(status?.latency?.ttftMs),
|
|
461
|
+
avgInferenceMs: numberValue(status?.latency?.avgInferenceMs),
|
|
462
|
+
lastInferenceMs: numberValue(status?.latency?.lastInferenceMs),
|
|
463
|
+
avgTokensPerSecond: numberValue(status?.latency?.avgTokensPerSecond),
|
|
464
|
+
lastTokensPerSecond: numberValue(status?.latency?.lastTokensPerSecond),
|
|
465
|
+
latencySamples: numberValue(status?.latency?.sampleCount),
|
|
466
|
+
upstreamBalanceUsdMicros: balance && Number.isFinite(balance.amountUsdMicros ?? NaN) ? (balance.amountUsdMicros as number) : undefined,
|
|
467
|
+
upstreamBalanceCurrency: typeof balance?.currency === "string" ? balance.currency : undefined,
|
|
468
|
+
upstreamBalanceSource: balance?.source,
|
|
469
|
+
upstreamBalanceFetchedAt: balance ? new Date(balance.fetchedAt).toISOString() : undefined,
|
|
470
|
+
upstreamBalanceError: balance?.error?.message,
|
|
471
|
+
upstreamRechargeUrl: stringValue(config?.upstreamBalanceProbe?.rechargeUrl || config?.upstreamRechargeUrl || normalizedUpstreams?.upstreamRechargeUrl),
|
|
472
|
+
modelsCount: numberValue(service?.modelsCount ?? normalizedUpstreams?.models?.length ?? base.modelsCount),
|
|
473
|
+
specs: {
|
|
474
|
+
...base.specs,
|
|
475
|
+
region: entry.region,
|
|
476
|
+
modelsCount: numberValue(service?.modelsCount ?? normalizedUpstreams?.models?.length ?? base.modelsCount)
|
|
477
|
+
},
|
|
478
|
+
error
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function modelRows(upstreams: any, config: any, status: any): SellerModelRow[] {
|
|
483
|
+
const normalizedUpstreams = upstreamDocument(upstreams);
|
|
484
|
+
const aliases = config.modelAliases || normalizedUpstreams.modelAliases || {};
|
|
485
|
+
const models = Array.isArray(normalizedUpstreams.models)
|
|
486
|
+
? normalizedUpstreams.models
|
|
487
|
+
: Array.isArray(config.models)
|
|
488
|
+
? config.models
|
|
489
|
+
: [];
|
|
490
|
+
return models.map((model: any) => {
|
|
491
|
+
const id = stringValue(model.id || model.name) || "unknown";
|
|
492
|
+
return {
|
|
493
|
+
upstreamModel: id,
|
|
494
|
+
billingModel: stringValue(aliases[id]) || id,
|
|
495
|
+
inputPrice: priceString(model.inputPriceMicrosPer1m),
|
|
496
|
+
outputPrice: priceString(model.outputPriceMicrosPer1m),
|
|
497
|
+
ttftMs: numberValue(model.ttftMs ?? status?.latency?.ttftMs),
|
|
498
|
+
avgInferenceMs: numberValue(model.avgInferenceMs ?? status?.latency?.avgInferenceMs),
|
|
499
|
+
avgTokensPerSecond: numberValue(model.avgTokensPerSecond ?? status?.latency?.avgTokensPerSecond),
|
|
500
|
+
latencySamples: numberValue(model.latencySamples ?? status?.latency?.sampleCount)
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function upstreamDocument(value: any): any {
|
|
506
|
+
if (Array.isArray(value?.upstreams)) {
|
|
507
|
+
return objectValue(value.upstreams[0]) || {};
|
|
508
|
+
}
|
|
509
|
+
return objectValue(value?.upstreams) || value || {};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function registryStatus(value: unknown): RegistryStatus {
|
|
513
|
+
const normalized = String(value || "unknown").toLowerCase();
|
|
514
|
+
if (normalized === "active" || normalized === "draining" || normalized === "offline" || normalized === "pending") {
|
|
515
|
+
return normalized;
|
|
516
|
+
}
|
|
517
|
+
return "unknown";
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function nodeStatus(value: unknown): NodeStatus {
|
|
521
|
+
const normalized = String(value || "unknown").toLowerCase();
|
|
522
|
+
if (normalized === "active" || normalized === "busy_capacity" || normalized === "draining" || normalized === "pending" || normalized === "offline") {
|
|
523
|
+
return normalized;
|
|
524
|
+
}
|
|
525
|
+
if (normalized === "healthy") {
|
|
526
|
+
return "active";
|
|
527
|
+
}
|
|
528
|
+
return "unknown";
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function upstreamStatus(value: unknown): UpstreamStatus {
|
|
532
|
+
const normalized = String(value || "unknown").toLowerCase();
|
|
533
|
+
if (normalized === "healthy" || normalized === "degraded" || normalized === "unhealthy") {
|
|
534
|
+
return normalized;
|
|
535
|
+
}
|
|
536
|
+
return "unknown";
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function maskApiKey(value: unknown): string | undefined {
|
|
540
|
+
const normalized = String(value || "").trim();
|
|
541
|
+
if (!normalized) {
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
const tail = normalized.replace(/\s+/g, "").slice(-4);
|
|
545
|
+
return tail ? `**** **** **** ${tail}` : "****";
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function maskSecret(value: unknown): string | undefined {
|
|
549
|
+
const normalized = String(value || "").trim();
|
|
550
|
+
if (!normalized) {
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
const compact = normalized.replace(/\s+/g, "");
|
|
554
|
+
if (compact.length <= 8) {
|
|
555
|
+
return "********";
|
|
556
|
+
}
|
|
557
|
+
return `${compact.slice(0, 4)}...${compact.slice(-4)}`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function trimSlash(value: string): string {
|
|
561
|
+
return value.replace(/\/+$/, "");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function hostName(value: string | undefined): string {
|
|
565
|
+
if (!value) {
|
|
566
|
+
return "";
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
return new URL(value).hostname.replace(/^www\./, "");
|
|
570
|
+
} catch {
|
|
571
|
+
return "";
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function numberValue(value: unknown): number | undefined {
|
|
576
|
+
const parsed = Number(value);
|
|
577
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function stringValue(value: unknown): string | undefined {
|
|
581
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function objectValue(value: unknown): Record<string, unknown> | undefined {
|
|
585
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
586
|
+
? value as Record<string, unknown>
|
|
587
|
+
: undefined;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function priceString(value: unknown): string | undefined {
|
|
591
|
+
const parsed = numberValue(value);
|
|
592
|
+
if (parsed === undefined) {
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
return `$${(parsed / 1000000).toFixed(4)}/1M`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function balanceString(balance: BalanceSnapshot | undefined): string | undefined {
|
|
599
|
+
if (!balance) {
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
if (balance.rawAmount === null) {
|
|
603
|
+
return balance.error?.message;
|
|
604
|
+
}
|
|
605
|
+
const amount = Math.abs(balance.rawAmount) >= 100
|
|
606
|
+
? balance.rawAmount.toFixed(0)
|
|
607
|
+
: balance.rawAmount.toFixed(2);
|
|
608
|
+
return `${balance.currency || "USD"} ${amount}`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function firstError(...values: any[]): string | undefined {
|
|
612
|
+
const hit = values.find((value) => value?.error);
|
|
613
|
+
return hit?.error;
|
|
614
|
+
}
|