@tokenbuddy/tb-admin 1.0.32 → 1.0.34
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/cli.d.ts.map +1 -1
- package/dist/src/cli.js +29 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.js +3 -3
- package/dist/src/client.js.map +1 -1
- package/dist/src/seller.d.ts +40 -1
- package/dist/src/seller.d.ts.map +1 -1
- package/dist/src/seller.js +132 -2
- package/dist/src/seller.js.map +1 -1
- package/dist/src/ui-actions.d.ts +2 -0
- package/dist/src/ui-actions.d.ts.map +1 -1
- package/dist/src/ui-actions.js +8 -6
- package/dist/src/ui-actions.js.map +1 -1
- package/dist/src/ui-command.d.ts +1 -0
- package/dist/src/ui-command.d.ts.map +1 -1
- package/dist/src/ui-command.js +7 -2
- package/dist/src/ui-command.js.map +1 -1
- package/dist/src/ui-server.d.ts.map +1 -1
- package/dist/src/ui-server.js +29 -8
- package/dist/src/ui-server.js.map +1 -1
- package/dist/src/ui-state.d.ts +29 -0
- package/dist/src/ui-state.d.ts.map +1 -1
- package/dist/src/ui-state.js +455 -111
- package/dist/src/ui-state.js.map +1 -1
- package/dist/src/ui-static.d.ts.map +1 -1
- package/dist/src/ui-static.js +262 -143
- package/dist/src/ui-static.js.map +1 -1
- package/dist/src/upstream-balance-probe.d.ts +2 -40
- package/dist/src/upstream-balance-probe.d.ts.map +1 -1
- package/dist/src/upstream-balance-probe.js +1 -378
- package/dist/src/upstream-balance-probe.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +32 -1
- package/src/client.ts +3 -4
- package/src/seller.ts +179 -3
- package/src/ui-actions.ts +10 -6
- package/src/ui-command.ts +7 -2
- package/src/ui-server.ts +30 -8
- package/src/ui-state.ts +533 -111
- package/src/ui-static.ts +262 -143
- package/src/upstream-balance-probe.ts +13 -505
- package/tests/admin.test.ts +472 -36
- package/tests/seller.test.ts +84 -3
- package/tests/ui-state-fleet.test.ts +272 -3
- package/tests/ui-static-row.test.ts +273 -8
- package/tests/vendor-cli.test.ts +45 -1
package/src/ui-state.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
export type RegistryStatus = "active" | "draining" | "offline" | "pending" | "unknown";
|
|
11
11
|
export type NodeStatus = RegistryStatus | "busy_capacity" | "auth_unknown";
|
|
12
12
|
export type UpstreamStatus = "healthy" | "degraded" | "unhealthy" | "unknown";
|
|
13
|
+
export type PublishStatus = "checking" | "published" | "unpublished" | "registry_only" | "unknown";
|
|
14
|
+
export type DetailStatus = "pending" | "queued" | "loading" | "fresh" | "stale" | "error" | "skipped";
|
|
13
15
|
/**
|
|
14
16
|
* Step 13 v1.1: dataSource 标记一行 seller 在 fly.io apps list vs
|
|
15
17
|
* registry /registry/sellers 两个独立查询里的出现情况. UI 据此决定
|
|
@@ -39,6 +41,10 @@ export interface SellerRow {
|
|
|
39
41
|
discountRatio?: number;
|
|
40
42
|
capacityUsed?: number;
|
|
41
43
|
capacityLimit?: number;
|
|
44
|
+
resourceCpuPercent?: number;
|
|
45
|
+
resourceMemoryPercent?: number;
|
|
46
|
+
resourceMemoryRssMb?: number;
|
|
47
|
+
resourceMemoryLimitMb?: number;
|
|
42
48
|
ttftMs?: number;
|
|
43
49
|
avgInferenceMs?: number;
|
|
44
50
|
lastInferenceMs?: number;
|
|
@@ -48,9 +54,15 @@ export interface SellerRow {
|
|
|
48
54
|
lastSwitchAt?: string;
|
|
49
55
|
modelsCount?: number;
|
|
50
56
|
specs?: {
|
|
57
|
+
cpuKind?: string;
|
|
58
|
+
cpuCores?: number;
|
|
59
|
+
memoryMb?: number;
|
|
51
60
|
memoryGb?: number;
|
|
52
61
|
machines?: number;
|
|
62
|
+
runningMachines?: number;
|
|
63
|
+
volumeGb?: number;
|
|
53
64
|
region?: string;
|
|
65
|
+
regions?: string[];
|
|
54
66
|
modelsCount?: number;
|
|
55
67
|
};
|
|
56
68
|
error?: string;
|
|
@@ -80,6 +92,10 @@ export interface SellerRow {
|
|
|
80
92
|
* fly-only 行一定有值; both 行有值; registry-only 行 undefined.
|
|
81
93
|
*/
|
|
82
94
|
flyApp?: { name: string; status?: string; owner?: string; latestDeployAt?: string };
|
|
95
|
+
publishStatus?: PublishStatus;
|
|
96
|
+
detailStatus?: DetailStatus;
|
|
97
|
+
detailUpdatedAt?: string;
|
|
98
|
+
detailNextRefreshAt?: string;
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
export interface SellerModelRow {
|
|
@@ -165,6 +181,7 @@ export interface AdminUiStateOptions {
|
|
|
165
181
|
* 返回的 SellerAppJson 形如 { name, status, owner, latestDeployAt }.
|
|
166
182
|
*/
|
|
167
183
|
flyApps?: () => Promise<import("./seller.js").SellerAppJson[]>;
|
|
184
|
+
flyMachineSpecs?: (appName: string) => Promise<import("./seller.js").SellerMachineSpecs | undefined>;
|
|
168
185
|
}
|
|
169
186
|
|
|
170
187
|
interface ProfileMatch {
|
|
@@ -182,11 +199,18 @@ interface SellerSnapshot {
|
|
|
182
199
|
balance?: BalanceSnapshot;
|
|
183
200
|
}
|
|
184
201
|
|
|
202
|
+
interface SellerTarget {
|
|
203
|
+
entry: SellerRegistryEntry;
|
|
204
|
+
flyApp?: import("./seller.js").SellerAppJson;
|
|
205
|
+
dataSource: SellerDataSource;
|
|
206
|
+
}
|
|
207
|
+
|
|
185
208
|
export class AdminUiState {
|
|
186
209
|
private readonly configManager: ConfigManager;
|
|
187
210
|
private readonly options: AdminUiStateOptions;
|
|
188
211
|
private readonly fetchJson: (url: string, init?: RequestInit) => Promise<unknown>;
|
|
189
212
|
private readonly balanceCache = new BalanceProbeCache();
|
|
213
|
+
private readonly machineSpecsCache = new Map<string, import("./seller.js").SellerMachineSpecs | undefined>();
|
|
190
214
|
|
|
191
215
|
constructor(options: AdminUiStateOptions) {
|
|
192
216
|
this.options = options;
|
|
@@ -267,7 +291,7 @@ export class AdminUiState {
|
|
|
267
291
|
this.fetchFlyApps().catch((err: any) => {
|
|
268
292
|
return { __error: err.message } as any;
|
|
269
293
|
}),
|
|
270
|
-
this.
|
|
294
|
+
this.fetchManagedRegistry().catch((err: any) => {
|
|
271
295
|
return { __error: err.message, sellers: [] } as any;
|
|
272
296
|
})
|
|
273
297
|
]);
|
|
@@ -300,22 +324,33 @@ export class AdminUiState {
|
|
|
300
324
|
const rows: SellerRow[] = [];
|
|
301
325
|
const consumedFly = new Set<string>();
|
|
302
326
|
const consumedRegistry = new Set<string>();
|
|
327
|
+
const specsByApp = await this.fetchFlyMachineSpecsForApps(Array.from(flyByName.keys()));
|
|
328
|
+
const registryTargets: Array<{
|
|
329
|
+
entry: SellerRegistryEntry;
|
|
330
|
+
flyMatch?: import("./seller.js").SellerAppJson;
|
|
331
|
+
dataSource: SellerDataSource;
|
|
332
|
+
}> = [];
|
|
303
333
|
|
|
304
334
|
// Phase 1: registry first (因为有 id + url + 详细 metadata)
|
|
305
335
|
for (const entry of registryDoc.sellers || []) {
|
|
306
|
-
const flyMatch =
|
|
336
|
+
const flyMatch = findFlyAppForEntry(flyByName, entry);
|
|
307
337
|
const dataSource: SellerDataSource = flyMatch ? "both" : "registry";
|
|
308
338
|
if (flyMatch) {
|
|
309
339
|
consumedFly.add(flyMatch.name);
|
|
310
340
|
}
|
|
311
341
|
consumedRegistry.add(entry.id);
|
|
312
|
-
|
|
313
|
-
rows.push(snapshot.row);
|
|
342
|
+
registryTargets.push({ entry, flyMatch, dataSource });
|
|
314
343
|
}
|
|
344
|
+
rows.push(...await Promise.all(
|
|
345
|
+
registryTargets.map(async ({ entry, flyMatch, dataSource }) => {
|
|
346
|
+
const snapshot = await this.sellerSnapshot(entry, flyMatch, dataSource, { balanceTimeoutMs: 8000, machineSpecs: flyMatch ? specsByApp.get(flyMatch.name) : undefined });
|
|
347
|
+
return snapshot.row;
|
|
348
|
+
})
|
|
349
|
+
));
|
|
315
350
|
|
|
316
351
|
// Phase 2: fly-only apps (registry 没有)
|
|
317
|
-
|
|
318
|
-
|
|
352
|
+
const flyOnlyApps = Array.from(flyByName.values()).filter((app) => !consumedFly.has(app.name));
|
|
353
|
+
rows.push(...await Promise.all(flyOnlyApps.map(async (app) => {
|
|
319
354
|
const stubEntry: SellerRegistryEntry = {
|
|
320
355
|
id: app.name,
|
|
321
356
|
name: app.name,
|
|
@@ -325,20 +360,80 @@ export class AdminUiState {
|
|
|
325
360
|
paymentMethods: [],
|
|
326
361
|
models: []
|
|
327
362
|
};
|
|
328
|
-
const snapshot = await this.sellerSnapshot(stubEntry, app, "fly");
|
|
329
|
-
|
|
363
|
+
const snapshot = await this.sellerSnapshot(stubEntry, app, "fly", { includeOperator: true, balanceTimeoutMs: 8000, machineSpecs: specsByApp.get(app.name) });
|
|
364
|
+
return snapshot.row;
|
|
365
|
+
})));
|
|
366
|
+
|
|
367
|
+
return rows;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
public async sellerRegistryRows(): Promise<SellerRow[]> {
|
|
371
|
+
const registryDoc = await this.fetchManagedRegistry();
|
|
372
|
+
return (registryDoc.sellers || []).map((entry) => {
|
|
373
|
+
const match = this.matchSellerProfile(entry);
|
|
374
|
+
return {
|
|
375
|
+
...baseSellerRow(entry, match.name, "registry"),
|
|
376
|
+
publishStatus: "checking",
|
|
377
|
+
detailStatus: "pending"
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
public async sellerInventory(): Promise<SellerRow[]> {
|
|
383
|
+
const registryDoc = await this.fetchManagedRegistry();
|
|
384
|
+
const flyApps = await this.fetchFlyApps().catch(() => []);
|
|
385
|
+
const flyByName = new Map<string, import("./seller.js").SellerAppJson>();
|
|
386
|
+
for (const app of flyApps) {
|
|
387
|
+
if (app?.name) {
|
|
388
|
+
flyByName.set(app.name, app);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const specsByApp = await this.fetchFlyMachineSpecsForApps(Array.from(flyByName.keys()));
|
|
392
|
+
const rows: SellerRow[] = [];
|
|
393
|
+
const consumedFly = new Set<string>();
|
|
394
|
+
|
|
395
|
+
for (const entry of registryDoc.sellers || []) {
|
|
396
|
+
const flyMatch = findFlyAppForEntry(flyByName, entry);
|
|
397
|
+
const dataSource: SellerDataSource = flyMatch ? "both" : "registry";
|
|
398
|
+
if (flyMatch) {
|
|
399
|
+
consumedFly.add(flyMatch.name);
|
|
400
|
+
}
|
|
401
|
+
const match = this.matchSellerProfile(entry);
|
|
402
|
+
const row = baseSellerRow(entry, match.name, dataSource, flyMatch, flyMatch ? specsByApp.get(flyMatch.name) : undefined);
|
|
403
|
+
row.publishStatus = flyMatch ? "published" : "registry_only";
|
|
404
|
+
row.detailStatus = dataSource === "registry" ? "skipped" : "pending";
|
|
405
|
+
if (dataSource === "registry") {
|
|
406
|
+
row.registryAlert = true;
|
|
407
|
+
row.alertReason = "registry 收录了但 fly app 失踪 — 严重事故, 立即下线";
|
|
408
|
+
row.removeHint = "立即下线 (registry-only)";
|
|
409
|
+
}
|
|
410
|
+
rows.push(row);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const flyOnlyApps = Array.from(flyByName.values()).filter((app) => !consumedFly.has(app.name));
|
|
414
|
+
for (const app of flyOnlyApps) {
|
|
415
|
+
const stubEntry = sellerEntryFromFlyApp(app);
|
|
416
|
+
const row = baseSellerRow(stubEntry, undefined, "fly", app, specsByApp.get(app.name));
|
|
417
|
+
row.publishStatus = "unpublished";
|
|
418
|
+
row.detailStatus = "pending";
|
|
419
|
+
row.publishHint = "未发布 — 可申请发布 (走 vendor-bootstrap stage)";
|
|
420
|
+
rows.push(row);
|
|
330
421
|
}
|
|
331
422
|
|
|
332
423
|
return rows;
|
|
333
424
|
}
|
|
334
425
|
|
|
426
|
+
public async refreshSellerRows(rows: SellerRow[]): Promise<SellerRow[]> {
|
|
427
|
+
return await Promise.all(rows.map((row) => this.refreshSellerRow(row)));
|
|
428
|
+
}
|
|
429
|
+
|
|
335
430
|
/**
|
|
336
431
|
* Step 13 v1.1: 拉 flyctl apps list --json. 默认走 seller.ts 真实
|
|
337
432
|
* flyctl spawn, 测试或受限环境可注入 options.flyApps closure.
|
|
338
433
|
*/
|
|
339
434
|
private async fetchFlyApps(): Promise<import("./seller.js").SellerAppJson[]> {
|
|
340
435
|
if (this.options.flyApps) {
|
|
341
|
-
return await this.options.flyApps();
|
|
436
|
+
return (await this.options.flyApps()).filter((app) => isSellerFlyAppName(app.name));
|
|
342
437
|
}
|
|
343
438
|
// 默认路径: 走 SellerCommandRunner.ls(true) 真 spawn flyctl.
|
|
344
439
|
// 避免在 ui-state.ts 里 import 整个 seller module 引入循环依赖,
|
|
@@ -356,7 +451,7 @@ export class AdminUiState {
|
|
|
356
451
|
const runner = new mod.SellerCommandRunner(this.configManager);
|
|
357
452
|
const result = await runner.ls(true);
|
|
358
453
|
if (result && typeof result === "object" && "apps" in result) {
|
|
359
|
-
return (result as { apps: import("./seller.js").SellerAppJson[] }).apps;
|
|
454
|
+
return (result as { apps: import("./seller.js").SellerAppJson[] }).apps.filter((app) => isSellerFlyAppName(app.name));
|
|
360
455
|
}
|
|
361
456
|
return [];
|
|
362
457
|
} catch (err: any) {
|
|
@@ -364,36 +459,42 @@ export class AdminUiState {
|
|
|
364
459
|
}
|
|
365
460
|
}
|
|
366
461
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const document = await this.fetchRegistry();
|
|
371
|
-
let entry = document.sellers.find((seller) => seller.id === id || seller.name === id || seller.app === id);
|
|
372
|
-
let dataSource: SellerDataSource = "registry";
|
|
373
|
-
let flyApp: import("./seller.js").SellerAppJson | undefined;
|
|
374
|
-
if (!entry) {
|
|
375
|
-
const flyApps = await this.fetchFlyApps().catch(() => []);
|
|
376
|
-
const flyMatch = flyApps.find((app) => app.name === id || app.name === `tb-seller-${id}` || app.name === id.replace(/^tb-seller-/, ""));
|
|
377
|
-
if (!flyMatch) {
|
|
378
|
-
throw new Error(`seller \`${id}\` not found in fly.io apps or bootstrap registry`);
|
|
379
|
-
}
|
|
380
|
-
entry = {
|
|
381
|
-
id: flyMatch.name,
|
|
382
|
-
name: flyMatch.name,
|
|
383
|
-
app: flyMatch.name,
|
|
384
|
-
url: `https://${flyMatch.name}.fly.dev`,
|
|
385
|
-
supportedProtocols: [],
|
|
386
|
-
paymentMethods: [],
|
|
387
|
-
models: []
|
|
388
|
-
};
|
|
389
|
-
flyApp = flyMatch;
|
|
390
|
-
dataSource = "fly";
|
|
391
|
-
} else {
|
|
392
|
-
const flyApps = await this.fetchFlyApps().catch(() => []);
|
|
393
|
-
flyApp = entry.app ? flyApps.find((app) => app.name === entry!.app) : undefined;
|
|
394
|
-
dataSource = flyApp ? "both" : "registry";
|
|
462
|
+
private async fetchFlyMachineSpecsForApps(appNames: string[]): Promise<Map<string, import("./seller.js").SellerMachineSpecs | undefined>> {
|
|
463
|
+
if (!this.options.flyMachineSpecs && this.options.flyApps) {
|
|
464
|
+
return new Map(appNames.map((appName) => [appName, undefined]));
|
|
395
465
|
}
|
|
396
|
-
const
|
|
466
|
+
const entries: Array<readonly [string, import("./seller.js").SellerMachineSpecs | undefined]> = [];
|
|
467
|
+
for (const appName of appNames) {
|
|
468
|
+
entries.push([appName, await this.fetchFlyMachineSpecs(appName)] as const);
|
|
469
|
+
}
|
|
470
|
+
return new Map(entries);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private async fetchFlyMachineSpecs(appName: string): Promise<import("./seller.js").SellerMachineSpecs | undefined> {
|
|
474
|
+
if (this.machineSpecsCache.has(appName)) {
|
|
475
|
+
return this.machineSpecsCache.get(appName);
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const specs = this.options.flyMachineSpecs
|
|
479
|
+
? await this.options.flyMachineSpecs(appName)
|
|
480
|
+
: await this.defaultFlyMachineSpecs(appName);
|
|
481
|
+
this.machineSpecsCache.set(appName, specs);
|
|
482
|
+
return specs;
|
|
483
|
+
} catch {
|
|
484
|
+
this.machineSpecsCache.set(appName, undefined);
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private async defaultFlyMachineSpecs(appName: string): Promise<import("./seller.js").SellerMachineSpecs | undefined> {
|
|
490
|
+
const mod = await import("./seller.js");
|
|
491
|
+
const runner = new mod.SellerCommandRunner(this.configManager);
|
|
492
|
+
return runner.machineSpecs(appName);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
public async sellerDetail(id: string): Promise<SellerDetail> {
|
|
496
|
+
const { entry, flyApp, dataSource } = await this.resolveSellerTarget(id);
|
|
497
|
+
const snapshot = await this.sellerSnapshot(entry, flyApp, dataSource, { includeOperator: true, balanceTimeoutMs: 8000 });
|
|
397
498
|
const config = snapshot.config?.config || snapshot.config || {};
|
|
398
499
|
const upstreams = snapshot.upstreams || {};
|
|
399
500
|
return {
|
|
@@ -424,12 +525,75 @@ export class AdminUiState {
|
|
|
424
525
|
};
|
|
425
526
|
}
|
|
426
527
|
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
528
|
+
private async refreshSellerRow(row: SellerRow): Promise<SellerRow> {
|
|
529
|
+
const dataSource = row.dataSource || "both";
|
|
530
|
+
if (dataSource === "registry") {
|
|
531
|
+
return { ...row, nodeStatus: "unknown", detailStatus: "skipped", detailUpdatedAt: new Date().toISOString() };
|
|
532
|
+
}
|
|
533
|
+
const entry = sellerEntryFromRow(row);
|
|
534
|
+
const match = this.matchSellerProfile(entry);
|
|
535
|
+
const manifestPromise = this.probeManifest(entry.url);
|
|
536
|
+
if (!match.profile) {
|
|
537
|
+
const manifestOk = await manifestPromise;
|
|
538
|
+
return {
|
|
539
|
+
...row,
|
|
540
|
+
nodeStatus: manifestOk ? "active" : "unknown",
|
|
541
|
+
detailStatus: manifestOk ? "fresh" : "error",
|
|
542
|
+
detailUpdatedAt: new Date().toISOString(),
|
|
543
|
+
error: manifestOk ? row.error : (row.error || "No matching local admin profile; /manifest probe failed")
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const [manifestOk, status, upstreams, config] = await Promise.all([
|
|
548
|
+
manifestPromise,
|
|
549
|
+
this.fetchSellerAdminJson(match.profile, "/operator/status").catch((err: any) => ({ error: err.message })),
|
|
550
|
+
this.fetchSellerAdminJson(match.profile, "/operator/admin/upstreams").catch((err: any) => ({ error: err.message })),
|
|
551
|
+
this.fetchSellerAdminJson(match.profile, "/operator/admin/config").catch((err: any) => ({ error: err.message }))
|
|
552
|
+
]);
|
|
553
|
+
const balance = await this.operatorBalanceSnapshot(match.profile, 8000).catch(() => unavailableBalanceSnapshot("seller balance endpoint unavailable"));
|
|
554
|
+
const configDocument = config?.config || config || {};
|
|
555
|
+
const normalizedUpstreams = upstreamDocument(upstreams);
|
|
556
|
+
const upstreamUrl = stringValue(configDocument.upstreamUrl || normalizedUpstreams?.upstreamUrl || status?.upstream?.url || status?.upstreamUrl);
|
|
557
|
+
if (status?.error) {
|
|
558
|
+
return {
|
|
559
|
+
...row,
|
|
560
|
+
nodeStatus: manifestOk ? "active" : "unknown",
|
|
561
|
+
upstreamDomain: upstreamUrl ? (hostName(upstreamUrl) || row.upstreamDomain) : row.upstreamDomain,
|
|
562
|
+
upstreamStatus: upstreamStatus(normalizedUpstreams?.status || row.upstreamStatus),
|
|
563
|
+
discountRatio: numberValue(configDocument.discountRatio ?? normalizedUpstreams?.discountRatio) ?? row.discountRatio,
|
|
564
|
+
modelsCount: numberValue(normalizedUpstreams?.models?.length ?? row.modelsCount) ?? row.modelsCount,
|
|
565
|
+
...balanceFields(balance, row),
|
|
566
|
+
detailStatus: "error",
|
|
567
|
+
detailUpdatedAt: new Date().toISOString(),
|
|
568
|
+
error: status.error
|
|
569
|
+
};
|
|
432
570
|
}
|
|
571
|
+
const capacity = status?.capacity || {};
|
|
572
|
+
return {
|
|
573
|
+
...row,
|
|
574
|
+
nodeStatus: manifestOk ? "active" : nodeStatus(status?.status || row.nodeStatus),
|
|
575
|
+
upstreamDomain: upstreamUrl ? (hostName(upstreamUrl) || row.upstreamDomain) : row.upstreamDomain,
|
|
576
|
+
upstreamStatus: upstreamStatus(status?.upstream?.status || normalizedUpstreams?.status || row.upstreamStatus),
|
|
577
|
+
discountRatio: numberValue(configDocument.discountRatio ?? normalizedUpstreams?.discountRatio) ?? row.discountRatio,
|
|
578
|
+
capacityUsed: numberValue(capacity.activeConnections) ?? row.capacityUsed,
|
|
579
|
+
capacityLimit: numberValue(capacity.maxConnections) ?? row.capacityLimit,
|
|
580
|
+
...runtimeUsageFields(status?.runtime, row),
|
|
581
|
+
ttftMs: numberValue(status?.latency?.ttftMs) ?? row.ttftMs,
|
|
582
|
+
avgInferenceMs: numberValue(status?.latency?.avgInferenceMs) ?? row.avgInferenceMs,
|
|
583
|
+
lastInferenceMs: numberValue(status?.latency?.lastInferenceMs) ?? row.lastInferenceMs,
|
|
584
|
+
avgTokensPerSecond: numberValue(status?.latency?.avgTokensPerSecond) ?? row.avgTokensPerSecond,
|
|
585
|
+
lastTokensPerSecond: numberValue(status?.latency?.lastTokensPerSecond) ?? row.lastTokensPerSecond,
|
|
586
|
+
latencySamples: numberValue(status?.latency?.sampleCount) ?? row.latencySamples,
|
|
587
|
+
modelsCount: numberValue(normalizedUpstreams?.models?.length ?? row.modelsCount) ?? row.modelsCount,
|
|
588
|
+
...balanceFields(balance, row),
|
|
589
|
+
detailStatus: "fresh",
|
|
590
|
+
detailUpdatedAt: new Date().toISOString(),
|
|
591
|
+
error: undefined
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
public async rawSellerConfig(id: string): Promise<{ entry: SellerRegistryEntry; profileName?: string; config: any }> {
|
|
596
|
+
const { entry } = await this.resolveSellerTarget(id);
|
|
433
597
|
const match = this.matchSellerProfile(entry);
|
|
434
598
|
if (!match.profile) {
|
|
435
599
|
throw new Error(`seller \`${entry.id}\` has no matching local admin profile`);
|
|
@@ -438,17 +602,76 @@ export class AdminUiState {
|
|
|
438
602
|
return { entry, profileName: match.localProfile ? match.name : undefined, config: response.config || response };
|
|
439
603
|
}
|
|
440
604
|
|
|
605
|
+
private async resolveSellerTarget(id: string): Promise<SellerTarget> {
|
|
606
|
+
let document: SellerRegistryDocument | undefined;
|
|
607
|
+
let registryError: unknown;
|
|
608
|
+
try {
|
|
609
|
+
document = await this.fetchManagedRegistry();
|
|
610
|
+
} catch (err) {
|
|
611
|
+
registryError = err;
|
|
612
|
+
document = { version: 0, sellers: [] };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const flyApps = await this.fetchFlyApps().catch(() => []);
|
|
616
|
+
const flyByName = new Map(flyApps.map((app) => [app.name, app]));
|
|
617
|
+
const entry = document.sellers.find((seller) => seller.id === id || seller.name === id || seller.app === id);
|
|
618
|
+
if (entry) {
|
|
619
|
+
const flyApp = findFlyAppForEntry(flyByName, entry);
|
|
620
|
+
return { entry, flyApp, dataSource: flyApp ? "both" : "registry" };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const flyApp = flyApps.find((app) => app.name === id || app.name === `tb-seller-${id}` || app.name === id.replace(/^tb-seller-/, ""));
|
|
624
|
+
if (flyApp) {
|
|
625
|
+
return { entry: sellerEntryFromFlyApp(flyApp), flyApp, dataSource: "fly" };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (registryError) {
|
|
629
|
+
throw registryError;
|
|
630
|
+
}
|
|
631
|
+
throw new Error(`seller \`${id}\` not found in fly.io apps or bootstrap registry`);
|
|
632
|
+
}
|
|
633
|
+
|
|
441
634
|
public async fetchRegistry(): Promise<SellerRegistryDocument> {
|
|
442
635
|
const profile = this.activeBootstrapProfile();
|
|
443
636
|
const baseUrl = this.options.url || profile.profile?.url;
|
|
444
637
|
if (!baseUrl) {
|
|
445
638
|
throw new Error("No bootstrap profile found. Configure an admin profile or pass --url.");
|
|
446
639
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
640
|
+
return normalizeRegistryDocument(
|
|
641
|
+
await this.fetchBootstrapJson(`${trimSlash(baseUrl)}/registry/sellers`),
|
|
642
|
+
"bootstrap registry response"
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
public async fetchManagedRegistry(): Promise<SellerRegistryDocument> {
|
|
647
|
+
const profile = this.activeBootstrapProfile();
|
|
648
|
+
const baseUrl = this.options.url || profile.profile?.url;
|
|
649
|
+
const token = this.options.token || profile.profile?.token;
|
|
650
|
+
if (!baseUrl) {
|
|
651
|
+
throw new Error("No bootstrap profile found. Configure an admin profile or pass --url.");
|
|
652
|
+
}
|
|
653
|
+
if (!token) {
|
|
654
|
+
return this.fetchRegistry();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
const managed = normalizeRegistryDocument(
|
|
659
|
+
await this.fetchBootstrapJson(`${trimSlash(baseUrl)}/platform/sellers`, {
|
|
660
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
661
|
+
}),
|
|
662
|
+
"platform sellers response"
|
|
663
|
+
);
|
|
664
|
+
const publicDoc = await this.fetchRegistry().catch(() => undefined);
|
|
665
|
+
if (!publicDoc) {
|
|
666
|
+
return managed;
|
|
667
|
+
}
|
|
668
|
+
return mergeRegistryDocuments(publicDoc, managed);
|
|
669
|
+
} catch (err: any) {
|
|
670
|
+
if (isUnavailablePlatformSellersEndpoint(err)) {
|
|
671
|
+
return this.fetchRegistry();
|
|
672
|
+
}
|
|
673
|
+
throw err;
|
|
450
674
|
}
|
|
451
|
-
return document;
|
|
452
675
|
}
|
|
453
676
|
|
|
454
677
|
public activeBootstrapProfile(): ProfileMatch {
|
|
@@ -511,10 +734,11 @@ export class AdminUiState {
|
|
|
511
734
|
private async sellerSnapshot(
|
|
512
735
|
entry: SellerRegistryEntry,
|
|
513
736
|
flyApp: import("./seller.js").SellerAppJson | undefined,
|
|
514
|
-
dataSource: SellerDataSource
|
|
737
|
+
dataSource: SellerDataSource,
|
|
738
|
+
options: { includeOperator?: boolean; balanceTimeoutMs?: number; machineSpecs?: import("./seller.js").SellerMachineSpecs } = {}
|
|
515
739
|
): Promise<SellerSnapshot> {
|
|
516
740
|
const match = this.matchSellerProfile(entry);
|
|
517
|
-
const baseRow = baseSellerRow(entry, match.name, dataSource, flyApp);
|
|
741
|
+
const baseRow = baseSellerRow(entry, match.name, dataSource, flyApp, options.machineSpecs);
|
|
518
742
|
|
|
519
743
|
// Step 13: 立即下线 / Apply 按钮 hint 文案
|
|
520
744
|
if (dataSource === "registry") {
|
|
@@ -525,86 +749,82 @@ export class AdminUiState {
|
|
|
525
749
|
baseRow.publishHint = "未发布 — 可申请发布 (走 vendor-bootstrap stage)";
|
|
526
750
|
}
|
|
527
751
|
|
|
528
|
-
if (dataSource === "
|
|
529
|
-
// fly-only 行没 registry entry, 也可能没 profile. 仍然跑 manifest
|
|
530
|
-
// probe (entry.url 来自 flyApp 推断 https://<name>.fly.dev), 但
|
|
531
|
-
// 没 profile 时只算 "unknown" (灰点) — 因为 vendor 没法管控一个
|
|
532
|
-
// 还没发布的 instance, 不该 throw auth_unknown 把 UI 卡死.
|
|
533
|
-
const manifestOk = await this.probeManifest(entry.url);
|
|
752
|
+
if (dataSource === "registry") {
|
|
534
753
|
return {
|
|
535
754
|
row: {
|
|
536
755
|
...baseRow,
|
|
537
|
-
nodeStatus:
|
|
538
|
-
error: manifestOk ? undefined : "未发布到 registry, /manifest 探测失败 (不视为事故)"
|
|
756
|
+
nodeStatus: "unknown"
|
|
539
757
|
}
|
|
540
758
|
};
|
|
541
759
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
760
|
+
|
|
761
|
+
let manifestOk = false;
|
|
762
|
+
const manifestPromise = this.probeManifest(entry.url).then((ok) => {
|
|
763
|
+
manifestOk = ok;
|
|
764
|
+
return ok;
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
if (dataSource === "fly" && (!options.includeOperator || !match.profile)) {
|
|
768
|
+
// fly-only 行没 registry entry, 也可能没 profile. 仍然跑 manifest
|
|
769
|
+
// probe (entry.url 来自 flyApp 推断 https://<name>.fly.dev), 但
|
|
770
|
+
// 没 profile 时只算 "unknown" (灰点) — 因为 vendor 没法管控一个
|
|
771
|
+
// 还没发布的 instance, 不该 throw auth_unknown 把 UI 卡死.
|
|
772
|
+
manifestOk = await manifestPromise;
|
|
545
773
|
return {
|
|
546
774
|
row: {
|
|
547
775
|
...baseRow,
|
|
548
|
-
nodeStatus: "unknown",
|
|
549
|
-
error: "
|
|
776
|
+
nodeStatus: manifestOk ? "active" : "unknown",
|
|
777
|
+
error: manifestOk ? undefined : "未发布到 registry, /manifest 探测失败 (不视为事故)"
|
|
550
778
|
}
|
|
551
779
|
};
|
|
552
780
|
}
|
|
553
781
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (dataSource === "registry") {
|
|
782
|
+
if (!match.profile) {
|
|
783
|
+
manifestOk = await manifestPromise;
|
|
557
784
|
return {
|
|
558
785
|
row: {
|
|
559
786
|
...baseRow,
|
|
560
|
-
nodeStatus: "unknown"
|
|
787
|
+
nodeStatus: manifestOk ? "active" : "unknown",
|
|
788
|
+
error: manifestOk ? "No matching local admin profile" : "No matching local admin profile; /manifest probe failed"
|
|
561
789
|
}
|
|
562
790
|
};
|
|
563
791
|
}
|
|
564
|
-
|
|
565
792
|
try {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const manifestOk = await this.probeManifest(entry.url);
|
|
569
|
-
if (manifestOk) {
|
|
570
|
-
return {
|
|
571
|
-
row: {
|
|
572
|
-
...baseRow,
|
|
573
|
-
nodeStatus: "active"
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
// /manifest 失败: 试 /operator/status (老路径, 需要 vendor profile)
|
|
578
|
-
if (!match.profile) {
|
|
579
|
-
return {
|
|
580
|
-
row: {
|
|
581
|
-
...baseRow,
|
|
582
|
-
nodeStatus: "auth_unknown",
|
|
583
|
-
error: "No matching local admin profile (post /manifest fallback)"
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
const [status, service, upstreams, config] = await Promise.all([
|
|
793
|
+
const [, status, service, upstreams, config] = await Promise.all([
|
|
794
|
+
manifestPromise,
|
|
588
795
|
this.fetchSellerAdminJson(match.profile, "/operator/status").catch((err: any) => ({ error: err.message })),
|
|
589
796
|
this.fetchSellerAdminJson(match.profile, "/operator/admin/service").catch((err: any) => ({ error: err.message })),
|
|
590
797
|
this.fetchSellerAdminJson(match.profile, "/operator/admin/upstreams").catch((err: any) => ({ error: err.message })),
|
|
591
798
|
this.fetchSellerAdminJson(match.profile, "/operator/admin/config").catch((err: any) => ({ error: err.message }))
|
|
592
799
|
]);
|
|
593
800
|
const configDocument = config?.config || config || {};
|
|
594
|
-
const balance = await this.balanceSnapshot(configDocument, upstreams);
|
|
801
|
+
const balance = await this.balanceSnapshot(match.profile, configDocument, upstreams, options.balanceTimeoutMs);
|
|
595
802
|
return {
|
|
596
803
|
status,
|
|
597
804
|
service,
|
|
598
805
|
upstreams,
|
|
599
806
|
config,
|
|
600
807
|
balance,
|
|
601
|
-
row: mergeSellerRow(
|
|
808
|
+
row: mergeSellerRow(
|
|
809
|
+
{
|
|
810
|
+
...baseRow,
|
|
811
|
+
nodeStatus: manifestOk ? "active" : baseRow.nodeStatus
|
|
812
|
+
},
|
|
813
|
+
entry,
|
|
814
|
+
status,
|
|
815
|
+
service,
|
|
816
|
+
upstreams,
|
|
817
|
+
configDocument,
|
|
818
|
+
balance,
|
|
819
|
+
manifestOk
|
|
820
|
+
)
|
|
602
821
|
};
|
|
603
822
|
} catch (err: any) {
|
|
823
|
+
await manifestPromise.catch(() => false);
|
|
604
824
|
return {
|
|
605
825
|
row: {
|
|
606
826
|
...baseRow,
|
|
607
|
-
nodeStatus: "unknown",
|
|
827
|
+
nodeStatus: manifestOk ? "active" : "unknown",
|
|
608
828
|
error: err.message
|
|
609
829
|
}
|
|
610
830
|
};
|
|
@@ -632,22 +852,58 @@ export class AdminUiState {
|
|
|
632
852
|
}
|
|
633
853
|
}
|
|
634
854
|
|
|
635
|
-
private async fetchSellerAdminJson(profile: AdminProfile, pathName: string): Promise<any> {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
855
|
+
private async fetchSellerAdminJson(profile: AdminProfile, pathName: string, options: { timeoutMs?: number } = {}): Promise<any> {
|
|
856
|
+
const controller = options.timeoutMs ? new AbortController() : undefined;
|
|
857
|
+
const timer = controller ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
|
858
|
+
try {
|
|
859
|
+
return await this.fetchJson(`${trimSlash(profile.url)}${pathName}`, {
|
|
860
|
+
headers: {
|
|
861
|
+
"Content-Type": "application/json",
|
|
862
|
+
Authorization: `Bearer ${profile.token}`
|
|
863
|
+
},
|
|
864
|
+
...(controller ? { signal: controller.signal } : {})
|
|
865
|
+
});
|
|
866
|
+
} finally {
|
|
867
|
+
if (timer) {
|
|
868
|
+
clearTimeout(timer);
|
|
640
869
|
}
|
|
641
|
-
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
private async fetchBootstrapJson(url: string, init?: RequestInit): Promise<unknown> {
|
|
874
|
+
const controller = new AbortController();
|
|
875
|
+
const timer = setTimeout(() => controller.abort(), 10000);
|
|
876
|
+
try {
|
|
877
|
+
return await this.fetchJson(url, { ...init, signal: controller.signal });
|
|
878
|
+
} finally {
|
|
879
|
+
clearTimeout(timer);
|
|
880
|
+
}
|
|
642
881
|
}
|
|
643
882
|
|
|
644
|
-
private async balanceSnapshot(config: any, upstreams: any): Promise<BalanceSnapshot | undefined> {
|
|
883
|
+
private async balanceSnapshot(profile: AdminProfile, config: any, upstreams: any, timeoutMs?: number): Promise<BalanceSnapshot | undefined> {
|
|
645
884
|
if (config?.error) {
|
|
646
885
|
return undefined;
|
|
647
886
|
}
|
|
648
887
|
if (stringValue(config.upstreamBalanceProbe?.template) === "none") {
|
|
649
888
|
return undefined;
|
|
650
889
|
}
|
|
890
|
+
const operatorBalance = await this.operatorBalanceSnapshot(profile, timeoutMs).catch(() => undefined);
|
|
891
|
+
if (operatorBalance) {
|
|
892
|
+
return operatorBalance;
|
|
893
|
+
}
|
|
894
|
+
if (isRedactedConfigSecret(config.upstreamApiKey)) {
|
|
895
|
+
return {
|
|
896
|
+
rawAmount: null,
|
|
897
|
+
amountUsdMicros: null,
|
|
898
|
+
currency: null,
|
|
899
|
+
source: "unknown",
|
|
900
|
+
fetchedAt: Date.now(),
|
|
901
|
+
error: {
|
|
902
|
+
httpStatus: 0,
|
|
903
|
+
message: "seller balance endpoint unavailable"
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
}
|
|
651
907
|
return probeUpstreamBalance({
|
|
652
908
|
upstreamUrl: stringValue(config.upstreamUrl || upstreams?.upstreamUrl),
|
|
653
909
|
upstreamBalanceUrl: stringValue(config.upstreamBalanceUrl || upstreams?.upstreamBalanceUrl),
|
|
@@ -656,20 +912,32 @@ export class AdminUiState {
|
|
|
656
912
|
upstreamBalanceProbe: objectValue(config.upstreamBalanceProbe)
|
|
657
913
|
}, {
|
|
658
914
|
fetch: this.options.balanceFetch,
|
|
659
|
-
cache: this.balanceCache
|
|
915
|
+
cache: this.balanceCache,
|
|
916
|
+
timeoutMs
|
|
660
917
|
});
|
|
661
918
|
}
|
|
919
|
+
|
|
920
|
+
private async operatorBalanceSnapshot(profile: AdminProfile, timeoutMs?: number): Promise<BalanceSnapshot | undefined> {
|
|
921
|
+
return await this.fetchSellerAdminJson(profile, "/operator/admin/upstream-balance", { timeoutMs })
|
|
922
|
+
.then((response) => {
|
|
923
|
+
const responseObject = objectValue(response);
|
|
924
|
+
const balance = objectValue(responseObject?.balance) || responseObject;
|
|
925
|
+
return balance as BalanceSnapshot | undefined;
|
|
926
|
+
});
|
|
927
|
+
}
|
|
662
928
|
}
|
|
663
929
|
|
|
664
930
|
async function defaultFetchJson(url: string, init?: RequestInit): Promise<unknown> {
|
|
665
931
|
// Step 13 v1.1: 3s timeout 避免坏 host / DNS 卡死整个 admin web.
|
|
666
|
-
const controller = new AbortController();
|
|
667
|
-
const timer = setTimeout(() => controller.abort(), 3000);
|
|
932
|
+
const controller = init?.signal ? undefined : new AbortController();
|
|
933
|
+
const timer = controller ? setTimeout(() => controller.abort(), 3000) : undefined;
|
|
668
934
|
let response: Response;
|
|
669
935
|
try {
|
|
670
|
-
response = await fetch(url, { ...init, signal: controller.signal });
|
|
936
|
+
response = await fetch(url, controller ? { ...init, signal: controller.signal } : init);
|
|
671
937
|
} finally {
|
|
672
|
-
|
|
938
|
+
if (timer) {
|
|
939
|
+
clearTimeout(timer);
|
|
940
|
+
}
|
|
673
941
|
}
|
|
674
942
|
if (!response.ok) {
|
|
675
943
|
const text = await response.text();
|
|
@@ -683,8 +951,10 @@ function baseSellerRow(
|
|
|
683
951
|
entry: SellerRegistryEntry,
|
|
684
952
|
profile?: string,
|
|
685
953
|
dataSource: SellerDataSource = "registry",
|
|
686
|
-
flyApp?: import("./seller.js").SellerAppJson
|
|
954
|
+
flyApp?: import("./seller.js").SellerAppJson,
|
|
955
|
+
machineSpecs?: import("./seller.js").SellerMachineSpecs
|
|
687
956
|
): SellerRow {
|
|
957
|
+
const primaryRegion = entry.region || machineSpecs?.regions?.[0];
|
|
688
958
|
return {
|
|
689
959
|
id: entry.id,
|
|
690
960
|
name: entry.id,
|
|
@@ -696,15 +966,25 @@ function baseSellerRow(
|
|
|
696
966
|
// Step 13 v1.1: 绿点 base 改 unknown, 真正值由 sellerSnapshot
|
|
697
967
|
// 拿 /manifest 200 决定. 老逻辑直接复用 entry.status 是错的.
|
|
698
968
|
nodeStatus: "unknown",
|
|
699
|
-
region:
|
|
969
|
+
region: primaryRegion,
|
|
700
970
|
upstreamDomain: hostName(entry.url) || "unknown",
|
|
701
971
|
upstreamStatus: "unknown",
|
|
702
972
|
modelsCount: entry.modelsCount ?? entry.models?.length ?? entry.sampleModels?.length,
|
|
703
973
|
specs: {
|
|
704
|
-
|
|
974
|
+
cpuKind: machineSpecs?.cpuKind,
|
|
975
|
+
cpuCores: machineSpecs?.cpuCores,
|
|
976
|
+
memoryMb: machineSpecs?.memoryMb,
|
|
977
|
+
memoryGb: machineSpecs?.memoryMb ? Number((machineSpecs.memoryMb / 1024).toFixed(2)) : undefined,
|
|
978
|
+
machines: machineSpecs?.machines,
|
|
979
|
+
runningMachines: machineSpecs?.runningMachines,
|
|
980
|
+
volumeGb: machineSpecs?.volumeGb,
|
|
981
|
+
region: primaryRegion,
|
|
982
|
+
regions: machineSpecs?.regions,
|
|
705
983
|
modelsCount: entry.modelsCount ?? entry.models?.length ?? entry.sampleModels?.length
|
|
706
984
|
},
|
|
707
985
|
dataSource,
|
|
986
|
+
publishStatus: dataSource === "both" ? "published" : dataSource === "fly" ? "unpublished" : "unknown",
|
|
987
|
+
detailStatus: "pending",
|
|
708
988
|
flyApp: flyApp ? {
|
|
709
989
|
name: flyApp.name,
|
|
710
990
|
status: flyApp.status,
|
|
@@ -714,6 +994,54 @@ function baseSellerRow(
|
|
|
714
994
|
};
|
|
715
995
|
}
|
|
716
996
|
|
|
997
|
+
function isSellerFlyAppName(name: string | undefined): boolean {
|
|
998
|
+
return Boolean(name && (name.startsWith("tbs-") || name.startsWith("tb-seller-")) && name !== "tb-seller");
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function sellerEntryFromFlyApp(app: import("./seller.js").SellerAppJson): SellerRegistryEntry {
|
|
1002
|
+
return {
|
|
1003
|
+
id: app.name,
|
|
1004
|
+
name: app.name,
|
|
1005
|
+
app: app.name,
|
|
1006
|
+
url: `https://${app.name}.fly.dev`,
|
|
1007
|
+
supportedProtocols: [],
|
|
1008
|
+
paymentMethods: [],
|
|
1009
|
+
models: []
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function findFlyAppForEntry(
|
|
1014
|
+
flyByName: Map<string, import("./seller.js").SellerAppJson>,
|
|
1015
|
+
entry: SellerRegistryEntry
|
|
1016
|
+
): import("./seller.js").SellerAppJson | undefined {
|
|
1017
|
+
for (const key of [entry.app, entry.id, entry.name]) {
|
|
1018
|
+
const normalized = stringValue(key);
|
|
1019
|
+
if (!normalized) {
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
const match = flyByName.get(normalized);
|
|
1023
|
+
if (match) {
|
|
1024
|
+
return match;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
return undefined;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function sellerEntryFromRow(row: SellerRow): SellerRegistryEntry {
|
|
1031
|
+
return {
|
|
1032
|
+
id: row.id,
|
|
1033
|
+
name: row.name,
|
|
1034
|
+
app: row.app || row.flyApp?.name || row.id,
|
|
1035
|
+
url: row.url,
|
|
1036
|
+
status: row.registryStatus,
|
|
1037
|
+
region: row.region,
|
|
1038
|
+
modelsCount: row.modelsCount,
|
|
1039
|
+
supportedProtocols: [],
|
|
1040
|
+
paymentMethods: [],
|
|
1041
|
+
models: []
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
717
1045
|
function mergeSellerRow(
|
|
718
1046
|
base: SellerRow,
|
|
719
1047
|
entry: SellerRegistryEntry,
|
|
@@ -721,7 +1049,8 @@ function mergeSellerRow(
|
|
|
721
1049
|
service: any,
|
|
722
1050
|
upstreams: any,
|
|
723
1051
|
config: any,
|
|
724
|
-
balance: BalanceSnapshot | undefined
|
|
1052
|
+
balance: BalanceSnapshot | undefined,
|
|
1053
|
+
manifestOk = false
|
|
725
1054
|
): SellerRow {
|
|
726
1055
|
const normalizedUpstreams = upstreamDocument(upstreams);
|
|
727
1056
|
const capacity = status?.capacity || service?.capacity || {};
|
|
@@ -729,12 +1058,13 @@ function mergeSellerRow(
|
|
|
729
1058
|
const error = firstError(status, service, upstreams);
|
|
730
1059
|
return {
|
|
731
1060
|
...base,
|
|
732
|
-
nodeStatus: error ? "unknown" : nodeStatus(status?.status || entry.status),
|
|
1061
|
+
nodeStatus: error ? (manifestOk ? "active" : "unknown") : (manifestOk ? "active" : nodeStatus(status?.status || entry.status)),
|
|
733
1062
|
upstreamDomain: hostName(upstreamUrl) || base.upstreamDomain,
|
|
734
1063
|
upstreamStatus: upstreamStatus(status?.upstream?.status || normalizedUpstreams?.status),
|
|
735
1064
|
discountRatio: numberValue(config?.discountRatio ?? normalizedUpstreams?.discountRatio),
|
|
736
1065
|
capacityUsed: numberValue(capacity.activeConnections),
|
|
737
1066
|
capacityLimit: numberValue(capacity.maxConnections),
|
|
1067
|
+
...runtimeUsageFields(status?.runtime, base),
|
|
738
1068
|
ttftMs: numberValue(status?.latency?.ttftMs),
|
|
739
1069
|
avgInferenceMs: numberValue(status?.latency?.avgInferenceMs),
|
|
740
1070
|
lastInferenceMs: numberValue(status?.latency?.lastInferenceMs),
|
|
@@ -750,13 +1080,50 @@ function mergeSellerRow(
|
|
|
750
1080
|
modelsCount: numberValue(service?.modelsCount ?? normalizedUpstreams?.models?.length ?? base.modelsCount),
|
|
751
1081
|
specs: {
|
|
752
1082
|
...base.specs,
|
|
753
|
-
region: entry.region,
|
|
1083
|
+
region: entry.region || base.specs?.region,
|
|
754
1084
|
modelsCount: numberValue(service?.modelsCount ?? normalizedUpstreams?.models?.length ?? base.modelsCount)
|
|
755
1085
|
},
|
|
756
1086
|
error
|
|
757
1087
|
};
|
|
758
1088
|
}
|
|
759
1089
|
|
|
1090
|
+
function runtimeUsageFields(runtime: any, fallback: SellerRow): Partial<SellerRow> {
|
|
1091
|
+
return {
|
|
1092
|
+
resourceCpuPercent: numberValue(runtime?.cpuPercent) ?? fallback.resourceCpuPercent,
|
|
1093
|
+
resourceMemoryPercent: numberValue(runtime?.memoryPercent) ?? fallback.resourceMemoryPercent,
|
|
1094
|
+
resourceMemoryRssMb: numberValue(runtime?.memoryRssMb) ?? fallback.resourceMemoryRssMb,
|
|
1095
|
+
resourceMemoryLimitMb: numberValue(runtime?.memoryLimitMb) ?? fallback.resourceMemoryLimitMb
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function balanceFields(balance: BalanceSnapshot | undefined, fallback: SellerRow): Partial<SellerRow> {
|
|
1100
|
+
if (!balance) {
|
|
1101
|
+
return {};
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
upstreamBalanceUsdMicros: Number.isFinite(balance.amountUsdMicros ?? NaN) ? (balance.amountUsdMicros as number) : undefined,
|
|
1105
|
+
upstreamBalanceCurrency: typeof balance.currency === "string" ? balance.currency : undefined,
|
|
1106
|
+
upstreamBalanceSource: balance.source,
|
|
1107
|
+
upstreamBalanceFetchedAt: new Date(balance.fetchedAt).toISOString(),
|
|
1108
|
+
upstreamBalanceError: balance.error?.message,
|
|
1109
|
+
upstreamRechargeUrl: fallback.upstreamRechargeUrl
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function unavailableBalanceSnapshot(message: string): BalanceSnapshot {
|
|
1114
|
+
return {
|
|
1115
|
+
rawAmount: null,
|
|
1116
|
+
amountUsdMicros: null,
|
|
1117
|
+
currency: null,
|
|
1118
|
+
source: "unknown",
|
|
1119
|
+
fetchedAt: Date.now(),
|
|
1120
|
+
error: {
|
|
1121
|
+
httpStatus: 0,
|
|
1122
|
+
message
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
|
|
760
1127
|
function modelRows(upstreams: any, config: any, status: any): SellerModelRow[] {
|
|
761
1128
|
const normalizedUpstreams = upstreamDocument(upstreams);
|
|
762
1129
|
const aliases = config.modelAliases || normalizedUpstreams.modelAliases || {};
|
|
@@ -819,6 +1186,9 @@ export function maskApiKey(value: unknown): string | undefined {
|
|
|
819
1186
|
if (!normalized) {
|
|
820
1187
|
return undefined;
|
|
821
1188
|
}
|
|
1189
|
+
if (isRedactedConfigSecret(normalized)) {
|
|
1190
|
+
return "configured";
|
|
1191
|
+
}
|
|
822
1192
|
const tail = normalized.replace(/\s+/g, "").slice(-4);
|
|
823
1193
|
return tail ? `**** **** **** ${tail}` : "****";
|
|
824
1194
|
}
|
|
@@ -865,6 +1235,10 @@ function objectValue(value: unknown): Record<string, unknown> | undefined {
|
|
|
865
1235
|
: undefined;
|
|
866
1236
|
}
|
|
867
1237
|
|
|
1238
|
+
function isRedactedConfigSecret(value: unknown): boolean {
|
|
1239
|
+
return value === "[redacted]";
|
|
1240
|
+
}
|
|
1241
|
+
|
|
868
1242
|
function priceString(value: unknown): string | undefined {
|
|
869
1243
|
const parsed = numberValue(value);
|
|
870
1244
|
if (parsed === undefined) {
|
|
@@ -890,3 +1264,51 @@ function firstError(...values: any[]): string | undefined {
|
|
|
890
1264
|
const hit = values.find((value) => value?.error);
|
|
891
1265
|
return hit?.error;
|
|
892
1266
|
}
|
|
1267
|
+
|
|
1268
|
+
function normalizeRegistryDocument(value: unknown, label: string): SellerRegistryDocument {
|
|
1269
|
+
if (!value || typeof value !== "object" || !Array.isArray((value as { sellers?: unknown }).sellers)) {
|
|
1270
|
+
throw new Error(`${label} did not include sellers`);
|
|
1271
|
+
}
|
|
1272
|
+
const document = value as Partial<SellerRegistryDocument> & { sellers: SellerRegistryEntry[] };
|
|
1273
|
+
return {
|
|
1274
|
+
version: numberValue(document.version) ?? 0,
|
|
1275
|
+
updatedAt: stringValue(document.updatedAt),
|
|
1276
|
+
purpose: stringValue(document.purpose),
|
|
1277
|
+
defaultSeller: stringValue(document.defaultSeller),
|
|
1278
|
+
notes: Array.isArray(document.notes) ? document.notes.map((note) => stringValue(note)).filter((note): note is string => Boolean(note)) : undefined,
|
|
1279
|
+
sellers: document.sellers
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function mergeRegistryDocuments(publicDoc: SellerRegistryDocument, managedDoc: SellerRegistryDocument): SellerRegistryDocument {
|
|
1284
|
+
const sellers = publicDoc.sellers.map((seller) => ({ ...seller }));
|
|
1285
|
+
for (const managed of managedDoc.sellers) {
|
|
1286
|
+
const index = sellers.findIndex((seller) => sameSellerEntry(seller, managed));
|
|
1287
|
+
if (index >= 0) {
|
|
1288
|
+
sellers[index] = { ...sellers[index], ...managed };
|
|
1289
|
+
} else {
|
|
1290
|
+
sellers.push(managed);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
version: publicDoc.version || managedDoc.version,
|
|
1295
|
+
updatedAt: publicDoc.updatedAt || managedDoc.updatedAt,
|
|
1296
|
+
purpose: publicDoc.purpose || managedDoc.purpose,
|
|
1297
|
+
defaultSeller: publicDoc.defaultSeller || managedDoc.defaultSeller,
|
|
1298
|
+
notes: publicDoc.notes || managedDoc.notes,
|
|
1299
|
+
sellers
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function sameSellerEntry(a: SellerRegistryEntry, b: SellerRegistryEntry): boolean {
|
|
1304
|
+
const aKeys = new Set([a.id, a.app, a.name].map((value) => stringValue(value)).filter((value): value is string => Boolean(value)));
|
|
1305
|
+
return [b.id, b.app, b.name].some((value) => {
|
|
1306
|
+
const normalized = stringValue(value);
|
|
1307
|
+
return normalized ? aKeys.has(normalized) : false;
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function isUnavailablePlatformSellersEndpoint(err: unknown): boolean {
|
|
1312
|
+
const message = err instanceof Error ? err.message : String(err || "");
|
|
1313
|
+
return /HTTP Error (401|403|404)|Cannot GET \/platform\/sellers|vendor_auth|not found/i.test(message);
|
|
1314
|
+
}
|