@tokenbuddy/tb-admin 1.0.15 → 1.0.28
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 +323 -14
- 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 +3 -0
- package/dist/src/server-cmd.d.ts.map +1 -1
- package/dist/src/server-cmd.js +32 -9
- package/dist/src/server-cmd.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 +123 -63
- package/dist/src/ui-actions.js.map +1 -1
- package/dist/src/ui-command.js +1 -1
- package/dist/src/ui-command.js.map +1 -1
- package/dist/src/ui-server.d.ts +0 -1
- package/dist/src/ui-server.d.ts.map +1 -1
- package/dist/src/ui-server.js +25 -9
- package/dist/src/ui-server.js.map +1 -1
- package/dist/src/ui-state.d.ts +7 -1
- package/dist/src/ui-state.d.ts.map +1 -1
- package/dist/src/ui-state.js +55 -24
- 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 +371 -46
- package/dist/src/ui-static.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +367 -14
- package/src/client.ts +13 -8
- package/src/display-format.ts +398 -0
- package/src/server-cmd.ts +35 -9
- package/src/ui-actions.ts +129 -72
- package/src/ui-command.ts +1 -1
- package/src/ui-server.ts +24 -10
- package/src/ui-state.ts +64 -25
- package/src/ui-static.ts +374 -46
- package/tests/admin.test.ts +590 -41
package/src/ui-actions.ts
CHANGED
|
@@ -84,17 +84,20 @@ export class UiActions {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
public async updateSellerConfig(id: string, patch: ConfigPatch): Promise<UiActionResult> {
|
|
87
|
-
const { profileName, config } = await this.state.rawSellerConfig(id);
|
|
88
|
-
if (!profileName) {
|
|
89
|
-
throw new Error(`seller \`${id}\` has no local profile for seller-config put`);
|
|
90
|
-
}
|
|
87
|
+
const { entry, profileName, config } = await this.state.rawSellerConfig(id);
|
|
91
88
|
const next = mergeSellerConfigPatch(config, patch);
|
|
92
89
|
return this.withTempYaml(next, async (filePath) => {
|
|
93
|
-
const
|
|
90
|
+
const validateArgs = profileName
|
|
91
|
+
? profileArgs(this.options, profileName, ["seller-config", "validate", "--file", filePath])
|
|
92
|
+
: sellerOperatorArgs(this.options, entry, ["seller-config", "validate", "--file", filePath]);
|
|
93
|
+
const validate = await this.runAdmin(validateArgs, 30000);
|
|
94
94
|
if (!validate.ok) {
|
|
95
95
|
return validate;
|
|
96
96
|
}
|
|
97
|
-
|
|
97
|
+
const putArgs = profileName
|
|
98
|
+
? profileArgs(this.options, profileName, ["seller-config", "put", "--file", filePath])
|
|
99
|
+
: sellerOperatorArgs(this.options, entry, ["seller-config", "put", "--file", filePath]);
|
|
100
|
+
return this.runAdmin(putArgs, 30000);
|
|
98
101
|
});
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -210,15 +213,7 @@ export class UiActions {
|
|
|
210
213
|
});
|
|
211
214
|
if (configPut.ok) {
|
|
212
215
|
report({ stepId: "refresh_models", status: "running", title: "Refresh upstream models", message: `Refreshing model catalog from ${sellerOperatorUrl(appName)}.` });
|
|
213
|
-
modelsRefresh = await this.
|
|
214
|
-
{
|
|
215
|
-
...this.options,
|
|
216
|
-
profile: undefined,
|
|
217
|
-
url: sellerOperatorUrl(appName),
|
|
218
|
-
token: operatorSecret
|
|
219
|
-
},
|
|
220
|
-
["upstreams", "refresh", "--auto-models"]
|
|
221
|
-
), 30000);
|
|
216
|
+
modelsRefresh = await this.refreshSellerModelsWithRetry(appName, operatorSecret, report);
|
|
222
217
|
report({
|
|
223
218
|
stepId: "refresh_models",
|
|
224
219
|
status: modelsRefresh.ok ? "succeeded" : "failed",
|
|
@@ -239,6 +234,12 @@ export class UiActions {
|
|
|
239
234
|
enabled: Boolean(result.ok && !normalizedRequest.dryRun && readiness?.ok && configPut?.ok && modelsRefresh?.ok),
|
|
240
235
|
report
|
|
241
236
|
});
|
|
237
|
+
if (registryPublish?.ok) {
|
|
238
|
+
this.options.configManager.setProfile(appName, {
|
|
239
|
+
url: sellerOperatorUrl(appName),
|
|
240
|
+
token: operatorSecret
|
|
241
|
+
});
|
|
242
|
+
}
|
|
242
243
|
return {
|
|
243
244
|
result,
|
|
244
245
|
configValidation,
|
|
@@ -258,18 +259,15 @@ export class UiActions {
|
|
|
258
259
|
if (!target) {
|
|
259
260
|
throw new Error(`seller \`${id}\` not found in bootstrap registry`);
|
|
260
261
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
return this.runAdmin(globalArgs(this.options, ["bootstrap", "sellers", "put", "--file", filePath]), 30000);
|
|
272
|
-
});
|
|
262
|
+
return this.runAdmin(globalArgs(this.options, [
|
|
263
|
+
"bootstrap",
|
|
264
|
+
"sellers",
|
|
265
|
+
"status",
|
|
266
|
+
target.id,
|
|
267
|
+
status,
|
|
268
|
+
"--expect-version",
|
|
269
|
+
String(registry.version)
|
|
270
|
+
]), 30000);
|
|
273
271
|
}
|
|
274
272
|
|
|
275
273
|
public async deleteDeployment(id: string, confirm: boolean): Promise<UiActionResult> {
|
|
@@ -365,6 +363,43 @@ export class UiActions {
|
|
|
365
363
|
return failed;
|
|
366
364
|
}
|
|
367
365
|
|
|
366
|
+
private async refreshSellerModelsWithRetry(appName: string, operatorSecret: string, report: UiActionProgressReporter): Promise<UiActionResult> {
|
|
367
|
+
const maxAttempts = 6;
|
|
368
|
+
const baseDelayMs = 3000;
|
|
369
|
+
let last: UiActionResult | undefined;
|
|
370
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
371
|
+
if (attempt > 1) {
|
|
372
|
+
report({
|
|
373
|
+
stepId: "refresh_models",
|
|
374
|
+
status: "running",
|
|
375
|
+
title: "Refresh upstream models",
|
|
376
|
+
message: `Retrying model catalog refresh (${attempt}/${maxAttempts}).`
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
last = await this.runAdmin(globalArgs(
|
|
380
|
+
{
|
|
381
|
+
...this.options,
|
|
382
|
+
profile: undefined,
|
|
383
|
+
url: sellerOperatorUrl(appName),
|
|
384
|
+
token: operatorSecret
|
|
385
|
+
},
|
|
386
|
+
["upstreams", "refresh", "--auto-models"]
|
|
387
|
+
), 45000);
|
|
388
|
+
if (last.ok) {
|
|
389
|
+
return last;
|
|
390
|
+
}
|
|
391
|
+
if (attempt < maxAttempts) {
|
|
392
|
+
await sleep(baseDelayMs * attempt);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return last || {
|
|
396
|
+
ok: false,
|
|
397
|
+
stdout: "",
|
|
398
|
+
stderr: "model catalog refresh did not run",
|
|
399
|
+
command: ["tb-admin", "upstreams", "refresh", "--auto-models"]
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
368
403
|
private async publishCreatedSellerRegistryEntry(options: {
|
|
369
404
|
registry: SellerRegistryDocument;
|
|
370
405
|
request: CreateSellerRequest;
|
|
@@ -387,15 +422,16 @@ export class UiActions {
|
|
|
387
422
|
report({
|
|
388
423
|
stepId: "publish_registry",
|
|
389
424
|
status: "running",
|
|
390
|
-
title: "
|
|
391
|
-
message: `
|
|
425
|
+
title: "Update bootstrap registry",
|
|
426
|
+
message: `Adding ${request.sellerName} to bootstrap registry.`
|
|
392
427
|
});
|
|
393
428
|
|
|
394
|
-
let
|
|
429
|
+
let entry: SellerRegistryEntry;
|
|
395
430
|
try {
|
|
396
431
|
const upstreams = await this.fetchSellerOperatorJson(appName, operatorSecret, "/operator/admin/upstreams");
|
|
397
432
|
const service = await this.fetchSellerOperatorJson(appName, operatorSecret, "/operator/admin/service");
|
|
398
|
-
|
|
433
|
+
const manifest = await this.fetchSellerOperatorJsonOptional(appName, operatorSecret, "/manifest");
|
|
434
|
+
entry = registryEntryFromCreatedSeller(request, appName, upstreams, service, manifest);
|
|
399
435
|
} catch (err: any) {
|
|
400
436
|
const failed = {
|
|
401
437
|
ok: false,
|
|
@@ -406,30 +442,27 @@ export class UiActions {
|
|
|
406
442
|
report({
|
|
407
443
|
stepId: "publish_registry",
|
|
408
444
|
status: "failed",
|
|
409
|
-
title: "
|
|
445
|
+
title: "Update bootstrap registry",
|
|
410
446
|
message: "Failed to read seller metadata for bootstrap registry.",
|
|
411
447
|
result: failed
|
|
412
448
|
});
|
|
413
449
|
return failed;
|
|
414
450
|
}
|
|
415
|
-
return this.withTempJson(
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
return validate;
|
|
426
|
-
}
|
|
427
|
-
const put = await this.runAdmin(globalArgs(this.options, ["bootstrap", "sellers", "put", "--file", filePath]), 30000);
|
|
451
|
+
return this.withTempJson(entry, async (filePath) => {
|
|
452
|
+
const put = await this.runAdmin(globalArgs(this.options, [
|
|
453
|
+
"bootstrap",
|
|
454
|
+
"sellers",
|
|
455
|
+
"add",
|
|
456
|
+
"--file",
|
|
457
|
+
filePath,
|
|
458
|
+
"--expect-version",
|
|
459
|
+
String(registry.version)
|
|
460
|
+
]), 30000);
|
|
428
461
|
report({
|
|
429
462
|
stepId: "publish_registry",
|
|
430
463
|
status: put.ok ? "succeeded" : "failed",
|
|
431
|
-
title: "
|
|
432
|
-
message: put.ok ? "Bootstrap registry was
|
|
464
|
+
title: "Update bootstrap registry",
|
|
465
|
+
message: put.ok ? "Bootstrap registry entry was added. Run registry publish to update R2." : "Bootstrap registry update failed.",
|
|
433
466
|
result: put
|
|
434
467
|
});
|
|
435
468
|
return put;
|
|
@@ -446,47 +479,44 @@ export class UiActions {
|
|
|
446
479
|
}
|
|
447
480
|
return {};
|
|
448
481
|
}
|
|
449
|
-
}
|
|
450
482
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const entry = registryEntryFromCreatedSeller(request, appName, upstreams, service);
|
|
459
|
-
const sellers = registry.sellers.filter((seller) => {
|
|
460
|
-
return seller.id !== entry.id && seller.name !== entry.name && seller.app !== entry.app;
|
|
461
|
-
});
|
|
462
|
-
return {
|
|
463
|
-
...registry,
|
|
464
|
-
version: Number(registry.version || 0) + 1,
|
|
465
|
-
updatedAt: new Date().toISOString(),
|
|
466
|
-
sellers: [...sellers, entry]
|
|
467
|
-
};
|
|
483
|
+
private async fetchSellerOperatorJsonOptional(appName: string, operatorSecret: string, pathName: string): Promise<Record<string, unknown>> {
|
|
484
|
+
try {
|
|
485
|
+
return await this.fetchSellerOperatorJson(appName, operatorSecret, pathName);
|
|
486
|
+
} catch {
|
|
487
|
+
return {};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
468
490
|
}
|
|
469
491
|
|
|
470
492
|
function registryEntryFromCreatedSeller(
|
|
471
493
|
request: CreateSellerRequest,
|
|
472
494
|
appName: string,
|
|
473
495
|
upstreams: Record<string, unknown>,
|
|
474
|
-
service: Record<string, unknown
|
|
496
|
+
service: Record<string, unknown>,
|
|
497
|
+
manifest: Record<string, unknown>
|
|
475
498
|
): SellerRegistryEntry {
|
|
476
|
-
const
|
|
499
|
+
const wrappedUpstreams = upstreamPayload(upstreams);
|
|
500
|
+
const models = modelIdsFrom(upstreams.models)
|
|
501
|
+
|| modelIdsFrom(wrappedUpstreams?.models)
|
|
502
|
+
|| modelIdsFrom(service.models)
|
|
503
|
+
|| modelIdsFrom(manifest.models);
|
|
477
504
|
if (!models || models.length === 0) {
|
|
478
505
|
throw new Error("seller operator upstreams did not return models for bootstrap registry");
|
|
479
506
|
}
|
|
480
507
|
const supportedProtocols = supportedProtocolsFrom(service.supportedProtocols)
|
|
481
508
|
|| supportedProtocolsFrom(upstreams.supportedProtocols)
|
|
509
|
+
|| supportedProtocolsFrom(wrappedUpstreams?.supportedProtocols)
|
|
510
|
+
|| supportedProtocolsFrom(manifest.supportedProtocols)
|
|
511
|
+
|| supportedProtocolsFrom(manifest.supported_protocols)
|
|
482
512
|
|| ["chat_completions"];
|
|
483
513
|
const paymentMethods = paymentMethodsFromRequest(request);
|
|
484
514
|
return {
|
|
485
|
-
id:
|
|
486
|
-
name:
|
|
515
|
+
id: appName,
|
|
516
|
+
name: appName,
|
|
487
517
|
app: appName,
|
|
488
518
|
url: sellerOperatorUrl(appName),
|
|
489
|
-
status: "
|
|
519
|
+
status: "active",
|
|
490
520
|
region: stringValue(request.region),
|
|
491
521
|
modelsCount: models.length || undefined,
|
|
492
522
|
sampleModels: models.slice(0, 5),
|
|
@@ -513,6 +543,14 @@ function modelIdsFrom(value: unknown): string[] | undefined {
|
|
|
513
543
|
}).filter((entry): entry is string => Boolean(entry))));
|
|
514
544
|
}
|
|
515
545
|
|
|
546
|
+
function upstreamPayload(value: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
547
|
+
const wrapped = value.upstreams;
|
|
548
|
+
if (Array.isArray(wrapped)) {
|
|
549
|
+
return objectValue(wrapped[0]);
|
|
550
|
+
}
|
|
551
|
+
return objectValue(wrapped);
|
|
552
|
+
}
|
|
553
|
+
|
|
516
554
|
function supportedProtocolsFrom(value: unknown): string[] | undefined {
|
|
517
555
|
if (!Array.isArray(value)) {
|
|
518
556
|
return undefined;
|
|
@@ -564,10 +602,14 @@ async function defaultFetchJson(url: string, init?: RequestInit): Promise<unknow
|
|
|
564
602
|
|
|
565
603
|
function adminEntryPath(): string {
|
|
566
604
|
const current = process.argv[1] || "";
|
|
567
|
-
if (current.
|
|
605
|
+
if ((path.basename(current) === "tb-admin" || path.basename(current) === "tb-admin.js") && fs.existsSync(current)) {
|
|
568
606
|
return current;
|
|
569
607
|
}
|
|
570
|
-
|
|
608
|
+
const candidates = [
|
|
609
|
+
path.resolve(process.cwd(), "packages/admin-cli/bin/tb-admin.js"),
|
|
610
|
+
path.resolve(process.cwd(), "bin/tb-admin.js")
|
|
611
|
+
];
|
|
612
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[candidates.length - 1];
|
|
571
613
|
}
|
|
572
614
|
|
|
573
615
|
function globalArgs(options: UiActionsOptions, args: string[]): string[] {
|
|
@@ -592,6 +634,19 @@ function profileArgs(options: UiActionsOptions, profileName: string, args: strin
|
|
|
592
634
|
return globalArgs({ ...options, profile: profileName, url: undefined, token: undefined }, args);
|
|
593
635
|
}
|
|
594
636
|
|
|
637
|
+
function sellerOperatorArgs(options: UiActionsOptions, entry: SellerRegistryEntry, args: string[]): string[] {
|
|
638
|
+
const operatorSecret = options.configManager.getSellerProvider("fly")?.operator_secret;
|
|
639
|
+
if (!operatorSecret) {
|
|
640
|
+
throw new Error(`seller \`${entry.id}\` has no local profile and seller_providers.fly.operator_secret is not configured`);
|
|
641
|
+
}
|
|
642
|
+
return globalArgs({
|
|
643
|
+
...options,
|
|
644
|
+
profile: undefined,
|
|
645
|
+
url: entry.url,
|
|
646
|
+
token: operatorSecret
|
|
647
|
+
}, args);
|
|
648
|
+
}
|
|
649
|
+
|
|
595
650
|
function mergeSellerConfigPatch(config: Record<string, unknown>, patch: ConfigPatch): Record<string, unknown> {
|
|
596
651
|
const next = { ...config };
|
|
597
652
|
copyIfPresent(next, patch, "upstreamUrl");
|
|
@@ -672,6 +727,8 @@ function initialSellerConfig(request: CreateSellerRequest, masked: boolean): Rec
|
|
|
672
727
|
const upstreamRechargeUrl = balanceProbeRechargeUrl(request);
|
|
673
728
|
const paymentConfig = paymentConfigFromRequest(request, masked);
|
|
674
729
|
return {
|
|
730
|
+
sellerId: request.app || request.sellerName,
|
|
731
|
+
manifestVersion: "manifest.v1",
|
|
675
732
|
upstreamUrl: request.upstreamUrl,
|
|
676
733
|
upstreamApiKey: masked ? "********" : request.upstreamApiKey,
|
|
677
734
|
upstreamWebsite: request.upstreamWebsite,
|
package/src/ui-command.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function bindAdminUiCommand(program: Command, configManager: ConfigManage
|
|
|
18
18
|
openBrowser: Boolean(options.open),
|
|
19
19
|
configManager,
|
|
20
20
|
configPath: rootOptions.config,
|
|
21
|
-
profile: rootOptions.profile || process.env.TOKENBUDDY_ADMIN_PROFILE,
|
|
21
|
+
profile: rootOptions.profile || process.env.TOKENBUDDY_ADMIN_PROFILE || "bootstrap",
|
|
22
22
|
url: rootOptions.url || process.env.TOKENBUDDY_ADMIN_URL,
|
|
23
23
|
token: rootOptions.token || process.env.TOKENBUDDY_ADMIN_TOKEN
|
|
24
24
|
});
|
package/src/ui-server.ts
CHANGED
|
@@ -23,7 +23,6 @@ export interface AdminUiServerOptions {
|
|
|
23
23
|
export interface StartedAdminUiServer {
|
|
24
24
|
server: http.Server;
|
|
25
25
|
url: string;
|
|
26
|
-
sessionToken: string;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
type UiJobStatus = "running" | "succeeded" | "failed";
|
|
@@ -44,13 +43,12 @@ export async function startAdminUiServer(options: AdminUiServerOptions): Promise
|
|
|
44
43
|
assertSafeHost(options.host);
|
|
45
44
|
assertSafePort(options.port);
|
|
46
45
|
|
|
47
|
-
const sessionToken = randomBytes(24).toString("hex");
|
|
48
46
|
const state = new AdminUiState(options);
|
|
49
47
|
const actions = new UiActions(options);
|
|
50
48
|
const jobs = new Map<string, UiJob>();
|
|
51
49
|
const server = http.createServer(async (req, res) => {
|
|
52
50
|
try {
|
|
53
|
-
await routeRequest(req, res,
|
|
51
|
+
await routeRequest(req, res, state, actions, jobs);
|
|
54
52
|
} catch (err: any) {
|
|
55
53
|
sendJson(res, 500, { error: err.message || "internal error" });
|
|
56
54
|
}
|
|
@@ -66,17 +64,16 @@ export async function startAdminUiServer(options: AdminUiServerOptions): Promise
|
|
|
66
64
|
|
|
67
65
|
const address = server.address();
|
|
68
66
|
const actualPort = typeof address === "object" && address ? address.port : options.port;
|
|
69
|
-
const url = `http://${options.host}:${actualPort}
|
|
67
|
+
const url = `http://${options.host}:${actualPort}/`;
|
|
70
68
|
if (options.openBrowser) {
|
|
71
69
|
openLocalBrowser(url).catch(() => undefined);
|
|
72
70
|
}
|
|
73
|
-
return { server, url
|
|
71
|
+
return { server, url };
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
async function routeRequest(
|
|
77
75
|
req: http.IncomingMessage,
|
|
78
76
|
res: http.ServerResponse,
|
|
79
|
-
sessionToken: string,
|
|
80
77
|
state: AdminUiState,
|
|
81
78
|
actions: UiActions,
|
|
82
79
|
jobs: Map<string, UiJob>
|
|
@@ -91,8 +88,8 @@ async function routeRequest(
|
|
|
91
88
|
res.end();
|
|
92
89
|
return;
|
|
93
90
|
}
|
|
94
|
-
if (!
|
|
95
|
-
sendJson(res,
|
|
91
|
+
if (parsed.pathname.startsWith("/api/") && !isValidUiOrigin(req)) {
|
|
92
|
+
sendJson(res, 403, { error: "invalid UI origin" });
|
|
96
93
|
return;
|
|
97
94
|
}
|
|
98
95
|
|
|
@@ -252,8 +249,25 @@ function pruneJobs(jobs: Map<string, UiJob>): void {
|
|
|
252
249
|
}
|
|
253
250
|
}
|
|
254
251
|
|
|
255
|
-
function
|
|
256
|
-
|
|
252
|
+
function isValidUiOrigin(req: http.IncomingMessage): boolean {
|
|
253
|
+
const origin = headerValue(req.headers.origin);
|
|
254
|
+
if (!origin) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
const host = headerValue(req.headers.host);
|
|
258
|
+
if (!host) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const parsed = new URL(origin);
|
|
263
|
+
return parsed.protocol === "http:" && parsed.host === host;
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function headerValue(value: string | string[] | undefined): string | undefined {
|
|
270
|
+
return Array.isArray(value) ? value[0] : value;
|
|
257
271
|
}
|
|
258
272
|
|
|
259
273
|
function sendHtml(res: http.ServerResponse, html: string): void {
|
package/src/ui-state.ts
CHANGED
|
@@ -23,7 +23,8 @@ export interface SellerRow {
|
|
|
23
23
|
region?: string;
|
|
24
24
|
upstreamDomain: string;
|
|
25
25
|
upstreamStatus: UpstreamStatus;
|
|
26
|
-
|
|
26
|
+
upstreamBalanceUsdMicros?: number;
|
|
27
|
+
upstreamBalanceCurrency?: string;
|
|
27
28
|
upstreamBalanceSource?: string;
|
|
28
29
|
upstreamBalanceFetchedAt?: string;
|
|
29
30
|
upstreamBalanceError?: string;
|
|
@@ -34,7 +35,10 @@ export interface SellerRow {
|
|
|
34
35
|
ttftMs?: number;
|
|
35
36
|
avgInferenceMs?: number;
|
|
36
37
|
lastInferenceMs?: number;
|
|
38
|
+
avgTokensPerSecond?: number;
|
|
39
|
+
lastTokensPerSecond?: number;
|
|
37
40
|
latencySamples?: number;
|
|
41
|
+
lastSwitchAt?: string;
|
|
38
42
|
modelsCount?: number;
|
|
39
43
|
specs?: {
|
|
40
44
|
memoryGb?: number;
|
|
@@ -52,6 +56,7 @@ export interface SellerModelRow {
|
|
|
52
56
|
outputPrice?: string;
|
|
53
57
|
ttftMs?: number;
|
|
54
58
|
avgInferenceMs?: number;
|
|
59
|
+
avgTokensPerSecond?: number;
|
|
55
60
|
latencySamples?: number;
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -126,6 +131,7 @@ export interface AdminUiStateOptions {
|
|
|
126
131
|
interface ProfileMatch {
|
|
127
132
|
name?: string;
|
|
128
133
|
profile?: AdminProfile;
|
|
134
|
+
localProfile?: boolean;
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
interface SellerSnapshot {
|
|
@@ -232,7 +238,7 @@ export class AdminUiState {
|
|
|
232
238
|
upstreamUrl: stringValue(config.upstreamUrl || upstreams.upstreamUrl),
|
|
233
239
|
upstreamApiKeyMasked: maskApiKey(config.upstreamApiKey || upstreams.upstreamApiKey),
|
|
234
240
|
upstreamStatus: snapshot.row.upstreamStatus,
|
|
235
|
-
upstreamBalance: snapshot.
|
|
241
|
+
upstreamBalance: balanceString(snapshot.balance),
|
|
236
242
|
upstreamBalanceSource: snapshot.row.upstreamBalanceSource,
|
|
237
243
|
upstreamBalanceFetchedAt: snapshot.row.upstreamBalanceFetchedAt,
|
|
238
244
|
upstreamBalanceError: snapshot.row.upstreamBalanceError,
|
|
@@ -262,9 +268,8 @@ export class AdminUiState {
|
|
|
262
268
|
if (!match.profile) {
|
|
263
269
|
throw new Error(`seller \`${entry.id}\` has no matching local admin profile`);
|
|
264
270
|
}
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
return { entry, profileName: match.name, config: response.config || response };
|
|
271
|
+
const response = await this.fetchSellerAdminJson(match.profile, "/operator/admin/config");
|
|
272
|
+
return { entry, profileName: match.localProfile ? match.name : undefined, config: response.config || response };
|
|
268
273
|
}
|
|
269
274
|
|
|
270
275
|
public async fetchRegistry(): Promise<SellerRegistryDocument> {
|
|
@@ -286,25 +291,27 @@ export class AdminUiState {
|
|
|
286
291
|
if (this.options.url) {
|
|
287
292
|
return {
|
|
288
293
|
name: target,
|
|
289
|
-
profile: this.options.token ? { url: this.options.url, token: this.options.token } : undefined
|
|
294
|
+
profile: this.options.token ? { url: this.options.url, token: this.options.token } : undefined,
|
|
295
|
+
localProfile: false
|
|
290
296
|
};
|
|
291
297
|
}
|
|
292
298
|
const config = this.configManager.load();
|
|
293
299
|
const name = target || config.default_profile || Object.keys(config.profiles)[0];
|
|
294
300
|
return {
|
|
295
301
|
name,
|
|
296
|
-
profile: name ? config.profiles[name] : undefined
|
|
302
|
+
profile: name ? config.profiles[name] : undefined,
|
|
303
|
+
localProfile: Boolean(name && config.profiles[name])
|
|
297
304
|
};
|
|
298
305
|
}
|
|
299
306
|
|
|
300
307
|
private matchSellerProfile(entry: SellerRegistryEntry): ProfileMatch {
|
|
301
308
|
const config = this.configManager.load();
|
|
302
309
|
if (entry.profile && config.profiles[entry.profile]) {
|
|
303
|
-
return { name: entry.profile, profile: config.profiles[entry.profile] };
|
|
310
|
+
return { name: entry.profile, profile: config.profiles[entry.profile], localProfile: true };
|
|
304
311
|
}
|
|
305
312
|
const byUrl = Object.entries(config.profiles).find(([, profile]) => trimSlash(profile.url) === trimSlash(entry.url));
|
|
306
313
|
if (byUrl) {
|
|
307
|
-
return { name: byUrl[0], profile: byUrl[1] };
|
|
314
|
+
return { name: byUrl[0], profile: byUrl[1], localProfile: true };
|
|
308
315
|
}
|
|
309
316
|
const entryHost = hostName(entry.url);
|
|
310
317
|
const near = Object.entries(config.profiles).find(([name, profile]) => {
|
|
@@ -312,7 +319,18 @@ export class AdminUiState {
|
|
|
312
319
|
return [entry.id, entry.app, entryHost].some((value) => value && haystack.includes(value.toLowerCase()));
|
|
313
320
|
});
|
|
314
321
|
if (near) {
|
|
315
|
-
return { name: near[0], profile: near[1] };
|
|
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
|
+
};
|
|
316
334
|
}
|
|
317
335
|
return {};
|
|
318
336
|
}
|
|
@@ -330,13 +348,12 @@ export class AdminUiState {
|
|
|
330
348
|
};
|
|
331
349
|
}
|
|
332
350
|
|
|
333
|
-
const client = new AdminClient(match.profile.url, match.profile.token);
|
|
334
351
|
try {
|
|
335
352
|
const [status, service, upstreams, config] = await Promise.all([
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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 }))
|
|
340
357
|
]);
|
|
341
358
|
const configDocument = config?.config || config || {};
|
|
342
359
|
const balance = await this.balanceSnapshot(configDocument, upstreams);
|
|
@@ -359,6 +376,15 @@ export class AdminUiState {
|
|
|
359
376
|
}
|
|
360
377
|
}
|
|
361
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
|
+
|
|
362
388
|
private async balanceSnapshot(config: any, upstreams: any): Promise<BalanceSnapshot | undefined> {
|
|
363
389
|
if (config?.error) {
|
|
364
390
|
return undefined;
|
|
@@ -419,40 +445,45 @@ function mergeSellerRow(
|
|
|
419
445
|
config: any,
|
|
420
446
|
balance: BalanceSnapshot | undefined
|
|
421
447
|
): SellerRow {
|
|
448
|
+
const normalizedUpstreams = upstreamDocument(upstreams);
|
|
422
449
|
const capacity = status?.capacity || service?.capacity || {};
|
|
423
|
-
const upstreamUrl = stringValue(config?.upstreamUrl ||
|
|
450
|
+
const upstreamUrl = stringValue(config?.upstreamUrl || normalizedUpstreams?.upstreamUrl || service?.upstreamUrl) || entry.url;
|
|
424
451
|
const error = firstError(status, service, upstreams);
|
|
425
452
|
return {
|
|
426
453
|
...base,
|
|
427
454
|
nodeStatus: error ? "unknown" : nodeStatus(status?.status || entry.status),
|
|
428
455
|
upstreamDomain: hostName(upstreamUrl) || base.upstreamDomain,
|
|
429
|
-
upstreamStatus: upstreamStatus(status?.upstream?.status ||
|
|
430
|
-
discountRatio: numberValue(config?.discountRatio ??
|
|
456
|
+
upstreamStatus: upstreamStatus(status?.upstream?.status || normalizedUpstreams?.status),
|
|
457
|
+
discountRatio: numberValue(config?.discountRatio ?? normalizedUpstreams?.discountRatio),
|
|
431
458
|
capacityUsed: numberValue(capacity.activeConnections),
|
|
432
459
|
capacityLimit: numberValue(capacity.maxConnections),
|
|
433
460
|
ttftMs: numberValue(status?.latency?.ttftMs),
|
|
434
461
|
avgInferenceMs: numberValue(status?.latency?.avgInferenceMs),
|
|
435
462
|
lastInferenceMs: numberValue(status?.latency?.lastInferenceMs),
|
|
463
|
+
avgTokensPerSecond: numberValue(status?.latency?.avgTokensPerSecond),
|
|
464
|
+
lastTokensPerSecond: numberValue(status?.latency?.lastTokensPerSecond),
|
|
436
465
|
latencySamples: numberValue(status?.latency?.sampleCount),
|
|
437
|
-
|
|
466
|
+
upstreamBalanceUsdMicros: balance && Number.isFinite(balance.amountUsdMicros ?? NaN) ? (balance.amountUsdMicros as number) : undefined,
|
|
467
|
+
upstreamBalanceCurrency: typeof balance?.currency === "string" ? balance.currency : undefined,
|
|
438
468
|
upstreamBalanceSource: balance?.source,
|
|
439
469
|
upstreamBalanceFetchedAt: balance ? new Date(balance.fetchedAt).toISOString() : undefined,
|
|
440
470
|
upstreamBalanceError: balance?.error?.message,
|
|
441
|
-
upstreamRechargeUrl: stringValue(config?.upstreamBalanceProbe?.rechargeUrl || config?.upstreamRechargeUrl ||
|
|
442
|
-
modelsCount: numberValue(service?.modelsCount ??
|
|
471
|
+
upstreamRechargeUrl: stringValue(config?.upstreamBalanceProbe?.rechargeUrl || config?.upstreamRechargeUrl || normalizedUpstreams?.upstreamRechargeUrl),
|
|
472
|
+
modelsCount: numberValue(service?.modelsCount ?? normalizedUpstreams?.models?.length ?? base.modelsCount),
|
|
443
473
|
specs: {
|
|
444
474
|
...base.specs,
|
|
445
475
|
region: entry.region,
|
|
446
|
-
modelsCount: numberValue(service?.modelsCount ??
|
|
476
|
+
modelsCount: numberValue(service?.modelsCount ?? normalizedUpstreams?.models?.length ?? base.modelsCount)
|
|
447
477
|
},
|
|
448
478
|
error
|
|
449
479
|
};
|
|
450
480
|
}
|
|
451
481
|
|
|
452
482
|
function modelRows(upstreams: any, config: any, status: any): SellerModelRow[] {
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
-
|
|
483
|
+
const normalizedUpstreams = upstreamDocument(upstreams);
|
|
484
|
+
const aliases = config.modelAliases || normalizedUpstreams.modelAliases || {};
|
|
485
|
+
const models = Array.isArray(normalizedUpstreams.models)
|
|
486
|
+
? normalizedUpstreams.models
|
|
456
487
|
: Array.isArray(config.models)
|
|
457
488
|
? config.models
|
|
458
489
|
: [];
|
|
@@ -465,11 +496,19 @@ function modelRows(upstreams: any, config: any, status: any): SellerModelRow[] {
|
|
|
465
496
|
outputPrice: priceString(model.outputPriceMicrosPer1m),
|
|
466
497
|
ttftMs: numberValue(model.ttftMs ?? status?.latency?.ttftMs),
|
|
467
498
|
avgInferenceMs: numberValue(model.avgInferenceMs ?? status?.latency?.avgInferenceMs),
|
|
499
|
+
avgTokensPerSecond: numberValue(model.avgTokensPerSecond ?? status?.latency?.avgTokensPerSecond),
|
|
468
500
|
latencySamples: numberValue(model.latencySamples ?? status?.latency?.sampleCount)
|
|
469
501
|
};
|
|
470
502
|
});
|
|
471
503
|
}
|
|
472
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
|
+
|
|
473
512
|
function registryStatus(value: unknown): RegistryStatus {
|
|
474
513
|
const normalized = String(value || "unknown").toLowerCase();
|
|
475
514
|
if (normalized === "active" || normalized === "draining" || normalized === "offline" || normalized === "pending") {
|