@tokenbuddy/tb-admin 1.0.15 → 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.
Files changed (43) hide show
  1. package/dist/src/cli.d.ts.map +1 -1
  2. package/dist/src/cli.js +286 -13
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/client.d.ts +12 -3
  5. package/dist/src/client.d.ts.map +1 -1
  6. package/dist/src/client.js +12 -8
  7. package/dist/src/client.js.map +1 -1
  8. package/dist/src/display-format.d.ts +39 -0
  9. package/dist/src/display-format.d.ts.map +1 -0
  10. package/dist/src/display-format.js +354 -0
  11. package/dist/src/display-format.js.map +1 -0
  12. package/dist/src/server-cmd.d.ts +3 -0
  13. package/dist/src/server-cmd.d.ts.map +1 -1
  14. package/dist/src/server-cmd.js +32 -9
  15. package/dist/src/server-cmd.js.map +1 -1
  16. package/dist/src/ui-actions.d.ts +2 -0
  17. package/dist/src/ui-actions.d.ts.map +1 -1
  18. package/dist/src/ui-actions.js +123 -63
  19. package/dist/src/ui-actions.js.map +1 -1
  20. package/dist/src/ui-command.js +1 -1
  21. package/dist/src/ui-command.js.map +1 -1
  22. package/dist/src/ui-server.d.ts +0 -1
  23. package/dist/src/ui-server.d.ts.map +1 -1
  24. package/dist/src/ui-server.js +25 -9
  25. package/dist/src/ui-server.js.map +1 -1
  26. package/dist/src/ui-state.d.ts +7 -1
  27. package/dist/src/ui-state.d.ts.map +1 -1
  28. package/dist/src/ui-state.js +55 -24
  29. package/dist/src/ui-state.js.map +1 -1
  30. package/dist/src/ui-static.d.ts.map +1 -1
  31. package/dist/src/ui-static.js +372 -47
  32. package/dist/src/ui-static.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/cli.ts +326 -13
  35. package/src/client.ts +13 -8
  36. package/src/display-format.ts +398 -0
  37. package/src/server-cmd.ts +35 -9
  38. package/src/ui-actions.ts +129 -72
  39. package/src/ui-command.ts +1 -1
  40. package/src/ui-server.ts +24 -10
  41. package/src/ui-state.ts +64 -25
  42. package/src/ui-static.ts +375 -47
  43. package/tests/admin.test.ts +573 -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 validate = await this.runAdmin(profileArgs(this.options, profileName, ["seller-config", "validate", "--file", filePath]), 30000);
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
- return this.runAdmin(profileArgs(this.options, profileName, ["seller-config", "put", "--file", filePath]), 30000);
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.runAdmin(globalArgs(
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
- const next = {
262
- ...registry,
263
- updatedAt: new Date().toISOString(),
264
- sellers: registry.sellers.map((seller) => seller === target ? { ...seller, status } : seller)
265
- };
266
- return this.withTempJson(next, async (filePath) => {
267
- const validate = await this.runAdmin(globalArgs(this.options, ["bootstrap", "sellers", "validate", "--file", filePath]), 30000);
268
- if (!validate.ok) {
269
- return validate;
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: "Publish bootstrap registry",
391
- message: `Publishing ${request.sellerName} to bootstrap registry.`
425
+ title: "Update bootstrap registry",
426
+ message: `Adding ${request.sellerName} to bootstrap registry.`
392
427
  });
393
428
 
394
- let next: SellerRegistryDocument;
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
- next = nextRegistryDocument(registry, request, appName, upstreams, service);
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: "Publish bootstrap registry",
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(next, async (filePath) => {
416
- const validate = await this.runAdmin(globalArgs(this.options, ["bootstrap", "sellers", "validate", "--file", filePath]), 30000);
417
- if (!validate.ok) {
418
- report({
419
- stepId: "publish_registry",
420
- status: "failed",
421
- title: "Publish bootstrap registry",
422
- message: "Generated bootstrap registry entry failed validation.",
423
- result: validate
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: "Publish bootstrap registry",
432
- message: put.ok ? "Bootstrap registry was published with the new seller." : "Bootstrap registry publish failed.",
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
- function nextRegistryDocument(
452
- registry: SellerRegistryDocument,
453
- request: CreateSellerRequest,
454
- appName: string,
455
- upstreams: Record<string, unknown>,
456
- service: Record<string, unknown>
457
- ): SellerRegistryDocument {
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 models = modelIdsFrom(upstreams.models) || modelIdsFrom(service.models);
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: request.sellerName,
486
- name: request.sellerName,
515
+ id: appName,
516
+ name: appName,
487
517
  app: appName,
488
518
  url: sellerOperatorUrl(appName),
489
- status: "pending",
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.endsWith("tb-admin.js")) {
605
+ if ((path.basename(current) === "tb-admin" || path.basename(current) === "tb-admin.js") && fs.existsSync(current)) {
568
606
  return current;
569
607
  }
570
- return path.resolve(process.cwd(), "packages/admin-cli/bin/tb-admin.js");
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, sessionToken, state, actions, jobs);
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}/?session=${sessionToken}`;
67
+ const url = `http://${options.host}:${actualPort}/`;
70
68
  if (options.openBrowser) {
71
69
  openLocalBrowser(url).catch(() => undefined);
72
70
  }
73
- return { server, url, sessionToken };
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 (!isValidSession(req, sessionToken)) {
95
- sendJson(res, 401, { error: "invalid UI session" });
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 isValidSession(req: http.IncomingMessage, sessionToken: string): boolean {
256
- return req.headers["x-tokenbuddy-ui-session"] === sessionToken;
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
- upstreamBalance?: string;
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.row.upstreamBalance,
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 client = new AdminClient(match.profile.url, match.profile.token);
266
- const response = await client.get("/operator/admin/config");
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
- client.get("/operator/status").catch((err: any) => ({ error: err.message })),
337
- client.get("/operator/admin/service").catch((err: any) => ({ error: err.message })),
338
- client.get("/operator/admin/upstreams").catch((err: any) => ({ error: err.message })),
339
- client.get("/operator/admin/config").catch((err: any) => ({ error: err.message }))
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 || upstreams?.upstreamUrl || service?.upstreamUrl) || entry.url;
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 || upstreams?.status),
430
- discountRatio: numberValue(config?.discountRatio ?? upstreams?.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
- upstreamBalance: balanceString(balance),
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 || upstreams?.upstreamRechargeUrl),
442
- modelsCount: numberValue(service?.modelsCount ?? upstreams?.models?.length ?? base.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 ?? upstreams?.models?.length ?? base.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 aliases = config.modelAliases || upstreams.modelAliases || {};
454
- const models = Array.isArray(upstreams.models)
455
- ? upstreams.models
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") {