@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.
Files changed (43) hide show
  1. package/dist/src/cli.d.ts.map +1 -1
  2. package/dist/src/cli.js +323 -14
  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 +371 -46
  32. package/dist/src/ui-static.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/cli.ts +367 -14
  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 +374 -46
  43. package/tests/admin.test.ts +590 -41
@@ -3,8 +3,27 @@ import { buildAdminCli } from "../src/cli.js";
3
3
  import { FlyProvider, parseFlyMachineIds, requirePublishedDockerImage } from "../src/server-cmd.js";
4
4
  import { startAdminUiServer } from "../src/ui-server.js";
5
5
  import { AdminUiState } from "../src/ui-state.js";
6
- import { UiActions, type UiActionResult } from "../src/ui-actions.js";
6
+ import { UiActions, type CreateSellerRequest, type UiActionResult } from "../src/ui-actions.js";
7
7
  import { adminUiHtml } from "../src/ui-static.js";
8
+ import {
9
+ formatBalanceAmount,
10
+ formatCount,
11
+ formatDiscountRatio,
12
+ formatDuration,
13
+ formatMoney,
14
+ formatPercent,
15
+ formatPricePair,
16
+ formatSellerCapacity,
17
+ formatSellerStatus,
18
+ formatSellerId,
19
+ formatSpeed,
20
+ formatTimeCompact,
21
+ formatTimeFull,
22
+ formatTimeLedger,
23
+ sellerStatusTone,
24
+ statusTone,
25
+ UNKNOWN_VALUE
26
+ } from "../src/display-format.js";
8
27
  import {
9
28
  BalanceProbeCache,
10
29
  probeUpstreamBalance
@@ -15,8 +34,10 @@ import {
15
34
  import * as fs from "fs";
16
35
  import * as http from "http";
17
36
  import * as path from "path";
37
+ import * as vm from "vm";
18
38
 
19
39
  const TEMP_CONF_PATH = path.resolve(__dirname, "../../data-test/admin-config.json");
40
+ const PACKAGE_JSON = path.resolve(__dirname, "../package.json");
20
41
 
21
42
  describe("Admin CLI Config Profile Management Tests", () => {
22
43
  beforeEach(() => {
@@ -54,6 +75,21 @@ describe("Admin CLI Config Profile Management Tests", () => {
54
75
  expect(defaultP?.url).toBe("http://127.0.0.1:8000");
55
76
  });
56
77
 
78
+ test("tb-admin version follows package version", () => {
79
+ const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
80
+ const program = buildAdminCli(new ConfigManager(TEMP_CONF_PATH));
81
+
82
+ expect(program.version()).toBe(packageJson.version);
83
+ });
84
+
85
+ test("admin UI inline script is valid JavaScript", () => {
86
+ const html = adminUiHtml();
87
+ const scripts = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map((match) => match[1]);
88
+
89
+ expect(scripts.length).toBe(1);
90
+ expect(() => new vm.Script(scripts[0])).not.toThrow();
91
+ });
92
+
57
93
  test("Switch default profiles", () => {
58
94
  const mgr = new ConfigManager(TEMP_CONF_PATH);
59
95
  mgr.setProfile("prod", { url: "http://127.0.0.1:8000", token: "secret-op" });
@@ -81,9 +117,30 @@ describe("Admin CLI Config Profile Management Tests", () => {
81
117
 
82
118
  expect(bootstrap).toBeDefined();
83
119
  expect(sellers).toBeDefined();
84
- expect(sellers?.commands.map((command) => command.name()).sort()).toEqual(["get", "put", "validate"]);
120
+ expect(sellers?.commands.map((command) => command.name()).sort()).toEqual([
121
+ "add",
122
+ "get",
123
+ "list",
124
+ "put",
125
+ "remove",
126
+ "status",
127
+ "update",
128
+ "validate"
129
+ ]);
85
130
  expect(sellers?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--file")).toBe(true);
131
+ expect(sellers?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--force")).toBe(true);
132
+ expect(sellers?.commands.find((command) => command.name() === "add")?.options.some((option) => option.long === "--expect-version")).toBe(true);
133
+ expect(sellers?.commands.find((command) => command.name() === "update")?.options.some((option) => option.long === "--expect-version")).toBe(true);
134
+ expect(sellers?.commands.find((command) => command.name() === "status")?.options.some((option) => option.long === "--expect-version")).toBe(true);
135
+ expect(sellers?.commands.find((command) => command.name() === "remove")?.options.some((option) => option.long === "--expect-version")).toBe(true);
86
136
  expect(sellers?.commands.find((command) => command.name() === "validate")?.options.some((option) => option.long === "--file")).toBe(true);
137
+ const defaultSeller = bootstrap?.commands.find((command) => command.name() === "default-seller");
138
+ const registry = bootstrap?.commands.find((command) => command.name() === "registry");
139
+ expect(defaultSeller?.commands.map((command) => command.name()).sort()).toEqual(["set"]);
140
+ expect(defaultSeller?.commands.find((command) => command.name() === "set")?.options.some((option) => option.long === "--expect-version")).toBe(true);
141
+ expect(registry?.commands.map((command) => command.name()).sort()).toEqual(["diff", "import", "publish", "versions"]);
142
+ expect(registry?.commands.find((command) => command.name() === "import")?.options.some((option) => option.long === "--dry-run")).toBe(true);
143
+ expect(registry?.commands.find((command) => command.name() === "import")?.options.some((option) => option.long === "--force")).toBe(true);
87
144
  expect(sellerConfig).toBeDefined();
88
145
  expect(sellerConfig?.commands.map((command) => command.name()).sort()).toEqual(["get", "put", "validate"]);
89
146
  expect(sellerConfig?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--file")).toBe(true);
@@ -170,6 +227,32 @@ describe("Admin CLI Config Profile Management Tests", () => {
170
227
  expect(commands).toEqual([]);
171
228
  });
172
229
 
230
+ test("FlyProvider passes configured Fly token to flyctl commands", () => {
231
+ const envs: Array<string | undefined> = [];
232
+ const provider = new FlyProvider({ token: "fly-token-value" }, {
233
+ checkFlyctlInstalled: () => true,
234
+ imageInspector: () => ({ ok: true }),
235
+ execSync: (_command, options) => {
236
+ envs.push(options?.env?.FLY_API_TOKEN);
237
+ return "";
238
+ },
239
+ spawnSync: (_command, _args, options) => {
240
+ envs.push(options?.env?.FLY_API_TOKEN);
241
+ return { status: 0, signal: null, stdout: "", stderr: "", pid: 1, output: [] };
242
+ }
243
+ });
244
+
245
+ provider.createSeller({
246
+ name: "tbs-test",
247
+ app: "tbs-test",
248
+ image: "registry.fly.io/tb-seller:latest",
249
+ flyConfig: "deploy/fly.io/fly.tb-seller.toml",
250
+ operatorSecret: "operator-secret"
251
+ });
252
+
253
+ expect(envs).toEqual(["fly-token-value", "fly-token-value", "fly-token-value"]);
254
+ });
255
+
173
256
  test("parseFlyMachineIds reads machine ids and rejects unusable machine lists", () => {
174
257
  expect(parseFlyMachineIds(JSON.stringify([
175
258
  { id: "machine-1" },
@@ -200,7 +283,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
200
283
  expect(() => validateRegistryDocument(badDefault)).toThrow("defaultSeller `missing`");
201
284
  });
202
285
 
203
- test("tb-admin ui server binds loopback, protects APIs with a session, and serves bootstrap data", async () => {
286
+ test("tb-admin ui server binds loopback, rejects cross-origin APIs, and serves bootstrap data", async () => {
204
287
  const mgr = new ConfigManager(TEMP_CONF_PATH);
205
288
  const started = await startAdminUiServer({
206
289
  host: "127.0.0.1",
@@ -222,10 +305,14 @@ describe("Admin CLI Config Profile Management Tests", () => {
222
305
  })
223
306
  });
224
307
  try {
225
- await expect(fetch(`${started.url.replace(/\?.*/, "")}api/bootstrap`).then((res) => res.status)).resolves.toBe(401);
226
- const response = await fetch(`${started.url.replace(/\?.*/, "")}api/bootstrap`, {
227
- headers: { "X-TokenBuddy-Ui-Session": started.sessionToken }
308
+ expect(started.url).not.toContain("session=");
309
+ const blocked = await fetch(`${started.url}api/bootstrap`, {
310
+ headers: { Origin: "http://malicious.example" }
228
311
  });
312
+ expect(blocked.status).toBe(403);
313
+ await expect(blocked.json()).resolves.toMatchObject({ error: "invalid UI origin" });
314
+
315
+ const response = await fetch(`${started.url}api/bootstrap`);
229
316
  await expect(response.json()).resolves.toMatchObject({
230
317
  status: "available",
231
318
  registryVersion: 7,
@@ -256,22 +343,19 @@ describe("Admin CLI Config Profile Management Tests", () => {
256
343
  url: "https://bootstrap.example.test",
257
344
  fetchJson: async (url) => createSellerFetchJson(url),
258
345
  commandRunner: async (args): Promise<UiActionResult> => {
259
- if (isBootstrapSellersValidate(args)) {
346
+ if (isBootstrapSellersAdd(args)) {
260
347
  const filePath = args[args.indexOf("--file") + 1];
261
- const doc = JSON.parse(fs.readFileSync(filePath, "utf8")) as { sellers: Array<{ id: string; status?: string }> };
262
- expect(doc.sellers.find((seller) => seller.id === "tbs-nrt-07")?.status).toBe("pending");
348
+ const entry = JSON.parse(fs.readFileSync(filePath, "utf8")) as { id: string; status?: string };
349
+ expect(entry).toMatchObject({ id: "tbs-nrt-07", status: "active" });
263
350
  }
264
351
  calls.push(args);
265
352
  return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
266
353
  }
267
354
  });
268
355
  try {
269
- const response = await fetch(`${started.url.replace(/\?.*/, "")}api/sellers`, {
356
+ const response = await fetch(`${started.url}api/sellers`, {
270
357
  method: "POST",
271
- headers: {
272
- "Content-Type": "application/json",
273
- "X-TokenBuddy-Ui-Session": started.sessionToken
274
- },
358
+ headers: { "Content-Type": "application/json" },
275
359
  body: JSON.stringify({
276
360
  sellerName: "tbs-nrt-07",
277
361
  app: "tbs-nrt-07",
@@ -305,9 +389,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
305
389
 
306
390
  let job: any;
307
391
  for (let attempt = 0; attempt < 20; attempt += 1) {
308
- const poll = await fetch(`${started.url.replace(/\?.*/, "")}api/jobs/${created.jobId}`, {
309
- headers: { "X-TokenBuddy-Ui-Session": started.sessionToken }
310
- });
392
+ const poll = await fetch(`${started.url}api/jobs/${created.jobId}`);
311
393
  job = await poll.json();
312
394
  if (job.status !== "running") {
313
395
  break;
@@ -336,9 +418,9 @@ describe("Admin CLI Config Profile Management Tests", () => {
336
418
  expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-token-value status"),
337
419
  expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-token-value seller-config put --file"),
338
420
  expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-token-value upstreams refresh --auto-models"),
339
- expect.stringContaining("bootstrap sellers validate --file"),
340
- expect.stringContaining("bootstrap sellers put --file")
421
+ expect.stringContaining("bootstrap sellers add --file"),
341
422
  ]);
423
+ expect(commandLines[5]).toContain("--expect-version 7");
342
424
  expect(commandLines.find((line) => line.includes("seller create tbs-nrt-07"))).toContain("--initial-config");
343
425
  } finally {
344
426
  await new Promise<void>((resolve) => started.server.close(() => resolve()));
@@ -373,7 +455,8 @@ describe("Admin CLI Config Profile Management Tests", () => {
373
455
  lastInferenceMs: 700,
374
456
  latencySamples: 2,
375
457
  upstreamStatus: "healthy",
376
- upstreamBalance: "USD 47.95",
458
+ upstreamBalanceUsdMicros: 47_950_000,
459
+ upstreamBalanceCurrency: "USD",
377
460
  upstreamBalanceSource: "openrouter"
378
461
  });
379
462
 
@@ -394,6 +477,130 @@ describe("Admin CLI Config Profile Management Tests", () => {
394
477
  }
395
478
  });
396
479
 
480
+ test("AdminUiState uses Fly provider operator secret for registry sellers without local profiles", async () => {
481
+ const mgr = new ConfigManager(TEMP_CONF_PATH);
482
+ mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
483
+ const state = new AdminUiState({
484
+ configManager: mgr,
485
+ url: "https://bootstrap.example.test",
486
+ fetchJson: async (url, init) => {
487
+ const pathName = new URL(url).pathname;
488
+ if (pathName === "/registry/sellers") {
489
+ return {
490
+ version: 9,
491
+ sellers: [{
492
+ id: "tbs-openrouter-ai-qecae",
493
+ name: "tbs-openrouter-ai-qecae",
494
+ app: "tbs-openrouter-ai-qecae",
495
+ url: "https://tbs-openrouter-ai-qecae.fly.dev",
496
+ status: "active",
497
+ region: "sin",
498
+ modelsCount: 2,
499
+ supportedProtocols: ["chat_completions"],
500
+ paymentMethods: ["clawtip"]
501
+ }]
502
+ };
503
+ }
504
+ expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
505
+ if (pathName === "/operator/status") {
506
+ return { status: "healthy", upstream: { status: "healthy" }, capacity: { activeConnections: 1, maxConnections: 8 } };
507
+ }
508
+ if (pathName === "/operator/admin/service") {
509
+ return { sellerId: "node-seller-core", modelsCount: 2, capacity: { maxConnections: 8, maxQueueDepth: 4 } };
510
+ }
511
+ if (pathName === "/operator/admin/upstreams") {
512
+ return {
513
+ upstreams: [{
514
+ upstreamUrl: "https://openrouter.ai/api",
515
+ upstreamApiKey: "****abcd",
516
+ models: [{ id: "openai/gpt-5.4" }, { id: "anthropic/claude-opus-4.7" }]
517
+ }]
518
+ };
519
+ }
520
+ if (pathName === "/operator/admin/config") {
521
+ return {
522
+ config: {
523
+ upstreamUrl: "https://openrouter.ai/api",
524
+ upstreamApiKey: "live-secret-abcd",
525
+ upstreamBalanceProbe: { template: "none" },
526
+ markupRatio: 1.2,
527
+ discountRatio: 1,
528
+ maxConnections: 8,
529
+ maxQueueDepth: 4
530
+ }
531
+ };
532
+ }
533
+ throw new Error(`unexpected fetch url ${url}`);
534
+ }
535
+ });
536
+
537
+ const sellers = await state.sellers();
538
+ expect(sellers[0]).toMatchObject({
539
+ id: "tbs-openrouter-ai-qecae",
540
+ name: "tbs-openrouter-ai-qecae",
541
+ nodeStatus: "active",
542
+ upstreamDomain: "openrouter.ai",
543
+ profile: "tbs-openrouter-ai-qecae",
544
+ modelsCount: 2
545
+ });
546
+
547
+ const detail = await state.sellerDetail("tbs-openrouter-ai-qecae");
548
+ expect(detail.configuration.upstreamUrl).toBe("https://openrouter.ai/api");
549
+ expect(detail.configuration.upstreamApiKeyMasked).toBe("**** **** **** abcd");
550
+ expect(detail.models.map((model) => model.upstreamModel)).toEqual(["openai/gpt-5.4", "anthropic/claude-opus-4.7"]);
551
+ });
552
+
553
+ test("UiActions updates new registry seller config without a local profile", async () => {
554
+ const mgr = new ConfigManager(TEMP_CONF_PATH);
555
+ mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
556
+ const calls: string[][] = [];
557
+ const actions = new UiActions({
558
+ configManager: mgr,
559
+ url: "https://bootstrap.example.test",
560
+ fetchJson: async (url, init) => {
561
+ const pathName = new URL(url).pathname;
562
+ if (pathName === "/registry/sellers") {
563
+ return {
564
+ version: 10,
565
+ sellers: [{
566
+ id: "tbs-openrouter-ai-qecae",
567
+ name: "tbs-openrouter-ai-qecae",
568
+ app: "tbs-openrouter-ai-qecae",
569
+ url: "https://tbs-openrouter-ai-qecae.fly.dev",
570
+ status: "active",
571
+ supportedProtocols: ["chat_completions"],
572
+ paymentMethods: ["clawtip"]
573
+ }]
574
+ };
575
+ }
576
+ expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
577
+ if (pathName === "/operator/admin/config") {
578
+ return {
579
+ config: {
580
+ upstreamUrl: "https://openrouter.ai/api",
581
+ upstreamApiKey: "live-secret-abcd",
582
+ maxConnections: 8,
583
+ maxQueueDepth: 4
584
+ }
585
+ };
586
+ }
587
+ throw new Error(`unexpected fetch url ${url}`);
588
+ },
589
+ commandRunner: async (args): Promise<UiActionResult> => {
590
+ calls.push(args);
591
+ return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
592
+ }
593
+ });
594
+
595
+ const result = await actions.updateSellerConfig("tbs-openrouter-ai-qecae", { maxConnections: 12 });
596
+
597
+ expect(result.ok).toBe(true);
598
+ expect(calls.map((args) => args.join(" "))).toEqual([
599
+ expect.stringContaining("--url https://tbs-openrouter-ai-qecae.fly.dev --token operator-secret seller-config validate --file"),
600
+ expect.stringContaining("--url https://tbs-openrouter-ai-qecae.fly.dev --token operator-secret seller-config put --file")
601
+ ]);
602
+ });
603
+
397
604
  test("admin UI seller rows render missing telemetry without unknown data labels", () => {
398
605
  const html = adminUiHtml();
399
606
  expect(html).toContain("class=\"spinner\"");
@@ -407,6 +614,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
407
614
  expect(html).toContain("currentCreateJob");
408
615
  expect(html).toContain("setCreateFormDisabled(true)");
409
616
  expect(html).toContain("setCreateFormDisabled(false)");
617
+ expect(html).toContain("Retry create");
410
618
  expect(html).toContain("data-progress-step");
411
619
  expect(html).toContain("aria-expanded");
412
620
  expect(html).toContain("progress-title");
@@ -414,25 +622,26 @@ describe("Admin CLI Config Profile Management Tests", () => {
414
622
  expect(html).toContain("Hide details");
415
623
  expect(html).toContain("/api/jobs/");
416
624
  expect(html).toContain("function uiErrorMessage");
417
- expect(html).toContain("Admin UI connection lost. Reopen the latest tb-admin UI URL and reload this page.");
418
- expect(html).toContain("Admin UI session expired. Reopen the latest tb-admin UI URL.");
625
+ expect(html).toContain("Admin UI connection lost. Restart tb-admin ui and reload this page.");
626
+ expect(html).toContain("Admin profile authentication failed. Check the configured operator token.");
419
627
  expect(html).toContain("renderCreateJob");
420
628
  expect(html).toContain("pollCreateJob");
421
- expect(html).toContain("Created and published to bootstrap registry.");
629
+ expect(html).toContain("Created and added to bootstrap registry.");
422
630
  expect(html).toContain("upstreamUrl:\"https://openrouter.ai/api/v1\"");
423
631
  expect(html).toContain("loadingSpinner(\"Loading sellers\")");
424
632
  expect(html).toContain("loadingSpinner(\"Loading configuration\")");
425
633
  expect(html).toContain("loadingSpinner(\"Loading models\")");
426
634
  expect(html).toContain("sellerRefreshIntervalMs = 30000");
427
- expect(html).toContain("Last updated: never");
428
635
  expect(html).toContain("function scheduleSellerRefresh()");
429
636
  expect(html).toContain("sellerNextRefreshAt = new Date(Date.now() + sellerRefreshIntervalMs)");
430
637
  expect(html).toContain("sellerRefreshTimer = setTimeout(() => loadSellers(), sellerRefreshIntervalMs)");
431
638
  expect(html).toContain("Next refresh: ");
432
- expect(html).toContain("s ago");
639
+ expect(html).not.toContain("sellerLastUpdated");
640
+ expect(html).not.toContain("Last updated:");
641
+ expect(html).not.toContain("s ago");
433
642
  expect(html).toContain("sellerClockTimer = setInterval(() => updateSellerRefreshMeta(sellerRefreshInFlight), 1000)");
434
643
  expect(html).toContain("function renderSellerRows(rows)");
435
- expect(html).toContain("sellerLastUpdatedAt = new Date()");
644
+ expect(html).toContain("sellerRefreshLoaded = true");
436
645
  expect(html).toContain("readonly-value");
437
646
  expect(html).toContain("--seller-grid");
438
647
  expect(html).toContain("grid-template-columns:var(--seller-grid)");
@@ -440,10 +649,25 @@ describe("Admin CLI Config Profile Management Tests", () => {
440
649
  expect(html).toContain("gap:12px;width:100%;min-width:0");
441
650
  expect(html).toContain("detailFieldsHtml");
442
651
  expect(html).toContain("data-original");
443
- expect(html).toContain("Latency");
444
- expect(html).toContain("avg n/a");
445
- expect(html).toContain("Avg infer");
652
+ // Design-spec header labels (TTFT not Latency, Disc not Discount)
653
+ expect(html).toContain(">TTFT</span>");
654
+ expect(html).toContain(">Disc</span>");
655
+ expect(html).not.toContain(">Latency</span>");
656
+ // Spec-compliant token formats
657
+ expect(html).toContain("tok/s");
658
+ expect(html).toContain("AVG speed");
446
659
  expect(html).toContain("Samples");
660
+ // Spec-compliant unknown-value char (em dash) lives in formatter
661
+ expect(html).toContain("UNKNOWN_VALUE");
662
+ expect(html).toContain("formatDuration");
663
+ expect(html).toContain("formatSellerStatus");
664
+ expect(html).toContain("formatDiscountRatio");
665
+ expect(html).toContain("formatBalanceAmount");
666
+ // No glassmorphism per spec
667
+ expect(html).not.toContain("backdrop-filter");
668
+ // Detail field references
669
+ expect(html).toContain("avgTokensPerSecond");
670
+ expect(html).toContain("lastTokensPerSecond");
447
671
  expect(html).toContain("lastInferenceMs");
448
672
  expect(html).toContain("upstreamBalanceSource");
449
673
  expect(html).toContain("upstreamBalanceFetchedAt");
@@ -452,6 +676,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
452
676
  expect(html).toContain("upstreamBalanceProbeUrl");
453
677
  expect(html).toContain("upstreamBalanceProbeUserId");
454
678
  expect(html).toContain("upstreamBalanceProbeRechargeUrl");
679
+ // Payment tabs
455
680
  expect(html).toContain("paymentMethods");
456
681
  expect(html).toContain("payment-tabs");
457
682
  expect(html).toContain("payment-tab");
@@ -489,7 +714,9 @@ describe("Admin CLI Config Profile Management Tests", () => {
489
714
  expect(html).toContain("clawtipResourceUrl");
490
715
  expect(html).toContain("clawtipActivationFeeFen");
491
716
  expect(html).toContain("clawtipMicrosPerFen");
492
- expect(html).toContain("not reported");
717
+ // Spec uses — (em dash) for unknown, not "not reported" or "n/a"
718
+ expect(html).not.toContain("not reported");
719
+ expect(html).not.toContain("\"n/a\"");
493
720
  expect(html).toContain("aria-label=\"Seller specs\"");
494
721
  expect(html).toContain("<svg viewBox=\"0 0 24 24\"");
495
722
  expect(html).not.toContain(">i</span>");
@@ -500,7 +727,6 @@ describe("Admin CLI Config Profile Management Tests", () => {
500
727
  expect(html).not.toContain("Loading models...");
501
728
  expect(html).not.toContain("<div id=\"createStatus\" class=\"status-line\">Ready</div>");
502
729
  expect(html).not.toContain("not set");
503
- expect(html).not.toContain("tok/s");
504
730
  expect(html).not.toContain(" multiple");
505
731
  });
506
732
 
@@ -729,10 +955,10 @@ describe("Admin CLI Config Profile Management Tests", () => {
729
955
  profile: "bootstrap",
730
956
  fetchJson: async (url) => createSellerFetchJson(url),
731
957
  commandRunner: async (args): Promise<UiActionResult> => {
732
- if (isBootstrapSellersValidate(args)) {
958
+ if (isBootstrapSellersAdd(args)) {
733
959
  const filePath = args[args.indexOf("--file") + 1];
734
- const doc = JSON.parse(fs.readFileSync(filePath, "utf8")) as { sellers: Array<{ id: string; status?: string }> };
735
- expect(doc.sellers.find((seller) => seller.id === "tbs-nrt-07")?.status).toBe("pending");
960
+ const entry = JSON.parse(fs.readFileSync(filePath, "utf8")) as { id: string; status?: string };
961
+ expect(entry).toMatchObject({ id: "tbs-nrt-07", status: "active" });
736
962
  }
737
963
  calls.push(args);
738
964
  return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
@@ -772,6 +998,8 @@ describe("Admin CLI Config Profile Management Tests", () => {
772
998
  expect(response.registryPublish?.ok).toBe(true);
773
999
  expect(response.publishRegistry).toBe("completed");
774
1000
  expect(response.configPreview.upstreamUrl).toBe("https://openrouter.ai/api");
1001
+ expect(response.configPreview.sellerId).toBe("tbs-nrt-07");
1002
+ expect(response.configPreview.manifestVersion).toBe("manifest.v1");
775
1003
  expect(response.configPreview.upstreamBalanceProbe).toEqual({
776
1004
  template: "usage_generic",
777
1005
  url: "https://code.shoestravel.xin/v1/usage",
@@ -798,13 +1026,153 @@ describe("Admin CLI Config Profile Management Tests", () => {
798
1026
  expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret status"),
799
1027
  expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret seller-config put --file"),
800
1028
  expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret upstreams refresh --auto-models"),
801
- expect.stringContaining("bootstrap sellers validate --file"),
802
- expect.stringContaining("bootstrap sellers put --file")
1029
+ expect.stringContaining("bootstrap sellers add --file")
803
1030
  ]);
1031
+ expect(commandLines[5]).toContain("--expect-version 7");
804
1032
  expect(commandLines.find((line) => line.includes("seller create tbs-nrt-07"))).toContain("--initial-config");
1033
+ expect(mgr.getProfile("tbs-nrt-07")).toEqual({
1034
+ url: "https://tbs-nrt-07.fly.dev",
1035
+ token: "operator-secret"
1036
+ });
1037
+ } finally {
1038
+ await fixture.close();
1039
+ }
1040
+ });
1041
+
1042
+ test("UiActions create retries transient upstream model refresh failures", async () => {
1043
+ const fixture = await startFixtureAdminServer();
1044
+ try {
1045
+ const mgr = new ConfigManager(TEMP_CONF_PATH);
1046
+ mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
1047
+ const registry = await createSellerFetchJson(`${fixture.baseUrl}/registry/sellers`);
1048
+ let refreshAttempts = 0;
1049
+ const calls: string[][] = [];
1050
+ const actions = new UiActions({
1051
+ configManager: mgr,
1052
+ url: fixture.baseUrl,
1053
+ fetchJson: async (url) => {
1054
+ const pathName = new URL(url).pathname;
1055
+ if (pathName === "/registry/sellers") {
1056
+ return registry;
1057
+ }
1058
+ return createSellerFetchJson(url);
1059
+ },
1060
+ commandRunner: async (args): Promise<UiActionResult> => {
1061
+ calls.push(args);
1062
+ if (args.includes("upstreams") && args.includes("refresh")) {
1063
+ refreshAttempts += 1;
1064
+ if (refreshAttempts < 3) {
1065
+ return { ok: false, stdout: "", stderr: "Error: Connection failed: fetch failed", command: ["node", "tb-admin", ...args] };
1066
+ }
1067
+ }
1068
+ if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
1069
+ const filePath = args[args.indexOf("--file") + 1];
1070
+ const doc = JSON.parse(fs.readFileSync(filePath, "utf8"));
1071
+ expect(doc).toMatchObject({ id: "tbs-nrt-07" });
1072
+ expect(doc.models).toHaveLength(2);
1073
+ }
1074
+ return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
1075
+ }
1076
+ });
1077
+
1078
+ const response = await actions.createSeller({
1079
+ sellerName: "tbs-nrt-07",
1080
+ app: "tbs-nrt-07",
1081
+ region: "nrt",
1082
+ image: "registry.fly.io/tb-seller:latest",
1083
+ upstreamWebsite: "https://openrouter.ai",
1084
+ upstreamUrl: "https://openrouter.ai/api/v1",
1085
+ upstreamApiKey: "fixture-upstream-key",
1086
+ upstreamBalanceProbeTemplate: "none",
1087
+ maxConnections: 8,
1088
+ maxQueueDepth: 4,
1089
+ markupRatio: 1.2,
1090
+ discountRatio: 1,
1091
+ paymentMethods: ["mock"],
1092
+ flyConfig: "deploy/fly.io/fly.tb-seller.toml"
1093
+ });
1094
+
1095
+ expect(response.modelsRefresh?.ok).toBe(true);
1096
+ expect(response.registryPublish?.ok).toBe(true);
1097
+ expect(refreshAttempts).toBe(3);
1098
+ expect(calls.filter((args) => args.includes("upstreams") && args.includes("refresh"))).toHaveLength(3);
805
1099
  } finally {
806
1100
  await fixture.close();
807
1101
  }
1102
+ }, 30000);
1103
+
1104
+ test("UiActions create publishes registry from wrapped upstream metadata", async () => {
1105
+ const mgr = new ConfigManager(TEMP_CONF_PATH);
1106
+ mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
1107
+ let published: any;
1108
+ const actions = new UiActions({
1109
+ configManager: mgr,
1110
+ url: "https://bootstrap.example.test",
1111
+ fetchJson: async (url) => createSellerFetchJson(url, "wrappedArray"),
1112
+ commandRunner: async (args): Promise<UiActionResult> => {
1113
+ if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
1114
+ const filePath = args[args.indexOf("--file") + 1];
1115
+ published = JSON.parse(fs.readFileSync(filePath, "utf8"));
1116
+ }
1117
+ return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
1118
+ }
1119
+ });
1120
+
1121
+ const response = await actions.createSeller(createSellerRequestFixture({
1122
+ sellerName: "openrouter-ai",
1123
+ app: "tbs-openrouter-ai-qecae"
1124
+ }));
1125
+
1126
+ expect(response.registryPublish?.ok).toBe(true);
1127
+ expect(published).toMatchObject({
1128
+ id: "tbs-openrouter-ai-qecae",
1129
+ name: "tbs-openrouter-ai-qecae",
1130
+ app: "tbs-openrouter-ai-qecae",
1131
+ status: "active",
1132
+ models: ["openai/gpt-5.4", "openai/gpt-5.4-mini"],
1133
+ supportedProtocols: ["chat_completions"]
1134
+ });
1135
+ expect(mgr.getProfile("tbs-openrouter-ai-qecae")).toEqual({
1136
+ url: "https://tbs-openrouter-ai-qecae.fly.dev",
1137
+ token: "operator-secret"
1138
+ });
1139
+ });
1140
+
1141
+ test("UiActions create falls back to public manifest models for registry publish", async () => {
1142
+ const mgr = new ConfigManager(TEMP_CONF_PATH);
1143
+ mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
1144
+ let published: any;
1145
+ const actions = new UiActions({
1146
+ configManager: mgr,
1147
+ url: "https://bootstrap.example.test",
1148
+ fetchJson: async (url) => createSellerFetchJson(url, "manifest"),
1149
+ commandRunner: async (args): Promise<UiActionResult> => {
1150
+ if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
1151
+ const filePath = args[args.indexOf("--file") + 1];
1152
+ published = JSON.parse(fs.readFileSync(filePath, "utf8"));
1153
+ }
1154
+ return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
1155
+ }
1156
+ });
1157
+
1158
+ const response = await actions.createSeller(createSellerRequestFixture({
1159
+ sellerName: "openrouter-ai",
1160
+ app: "tbs-openrouter-ai-qecae"
1161
+ }));
1162
+
1163
+ expect(response.registryPublish?.ok).toBe(true);
1164
+ expect(published).toMatchObject({
1165
+ id: "tbs-openrouter-ai-qecae",
1166
+ name: "tbs-openrouter-ai-qecae",
1167
+ app: "tbs-openrouter-ai-qecae",
1168
+ status: "active",
1169
+ models: ["anthropic/claude-opus-4.7"],
1170
+ supportedProtocols: ["chat_completions"]
1171
+ });
1172
+ expect(mgr.getProfile("tbs-openrouter-ai-qecae")).toEqual({
1173
+ url: "https://tbs-openrouter-ai-qecae.fly.dev",
1174
+ token: "operator-secret"
1175
+ });
808
1176
  });
809
1177
 
810
1178
  test("UiActions create supports mock-only payment without ClawTip parameters", async () => {
@@ -879,12 +1247,13 @@ describe("Admin CLI Config Profile Management Tests", () => {
879
1247
 
880
1248
  test("UiActions subprocess runner uses the tb-admin bin entrypoint", async () => {
881
1249
  const previousArgv = process.argv[1];
882
- process.argv[1] = path.resolve(__dirname, "../dist/src/index.js");
1250
+ process.argv[1] = path.resolve(process.env.HOME || "/tmp", "packages/admin-cli/bin/tb-admin.js");
883
1251
  const { runTbAdmin } = await import("../src/ui-actions.js");
884
1252
  try {
885
1253
  const result = await runTbAdmin(["--help"], 30000);
886
1254
  expect(result.ok).toBe(true);
887
1255
  expect(result.command[1]).toMatch(/packages\/admin-cli\/bin\/tb-admin\.js$/);
1256
+ expect(fs.existsSync(result.command[1])).toBe(true);
888
1257
  expect(result.stdout).toContain("Remote admin CLI");
889
1258
  } finally {
890
1259
  process.argv[1] = previousArgv;
@@ -892,6 +1261,127 @@ describe("Admin CLI Config Profile Management Tests", () => {
892
1261
  });
893
1262
  });
894
1263
 
1264
+ describe("Admin CLI Display Format Spec Compliance", () => {
1265
+ test("formatDuration uses ms under 1s and 2-decimal seconds above 1s", () => {
1266
+ expect(formatDuration(undefined)).toBe(UNKNOWN_VALUE);
1267
+ expect(formatDuration(0)).toBe("0ms");
1268
+ expect(formatDuration(272)).toBe("272ms");
1269
+ expect(formatDuration(999)).toBe("999ms");
1270
+ expect(formatDuration(1000)).toBe("1.00s");
1271
+ expect(formatDuration(3542)).toBe("3.54s");
1272
+ expect(formatDuration(11540)).toBe("11.54s");
1273
+ });
1274
+
1275
+ test("formatMoney uses 4 decimals by default and 6 for tiny ledger amounts", () => {
1276
+ expect(formatMoney(undefined)).toBe(UNKNOWN_VALUE);
1277
+ expect(formatMoney(0)).toBe("$0.0000");
1278
+ expect(formatMoney(138600)).toBe("$0.1386");
1279
+ expect(formatMoney(2_772_200)).toBe("$2.7722");
1280
+ expect(formatMoney(138600, { ledger: true })).toBe("$0.1386");
1281
+ expect(formatMoney(5782, { ledger: true })).toBe("$0.005782");
1282
+ });
1283
+
1284
+ test("formatDiscountRatio converts ratio to percentage per the design spec", () => {
1285
+ expect(formatDiscountRatio(undefined)).toBe(UNKNOWN_VALUE);
1286
+ expect(formatDiscountRatio(0.5)).toBe("50%");
1287
+ expect(formatDiscountRatio(0.01)).toBe("99%");
1288
+ expect(formatDiscountRatio(0.99)).toBe("1%");
1289
+ expect(formatDiscountRatio(1)).toBe("0%");
1290
+ expect(formatDiscountRatio(0)).toBe("100%");
1291
+ });
1292
+
1293
+ test("formatPercent rounds to whole percent", () => {
1294
+ expect(formatPercent(0.123)).toBe("12%");
1295
+ expect(formatPercent(0.987)).toBe("99%");
1296
+ expect(formatPercent(undefined)).toBe(UNKNOWN_VALUE);
1297
+ });
1298
+
1299
+ test("formatPricePair renders $/1M with 4 decimals per spec", () => {
1300
+ expect(formatPricePair(undefined, undefined)).toBe(UNKNOWN_VALUE);
1301
+ expect(formatPricePair(2_500_000, 3_000_000)).toBe("$2.5000 / $3.0000");
1302
+ });
1303
+
1304
+ test("formatCount uses en-US grouping", () => {
1305
+ expect(formatCount(undefined)).toBe(UNKNOWN_VALUE);
1306
+ expect(formatCount(7_529)).toBe("7,529");
1307
+ expect(formatCount(218_700)).toBe("218,700");
1308
+ });
1309
+
1310
+ test("formatTimeCompact emits HH:mm for today and MM/DD HH:mm otherwise", () => {
1311
+ const now = new Date();
1312
+ const todayIso = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 14, 51).toISOString();
1313
+ const yesterdayIso = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 9, 5).toISOString();
1314
+ expect(formatTimeCompact(undefined)).toBe(UNKNOWN_VALUE);
1315
+ expect(formatTimeCompact(todayIso)).toBe("14:51");
1316
+ expect(formatTimeCompact(yesterdayIso)).toMatch(/^\d{2}\/\d{2} 09:05$/);
1317
+ });
1318
+
1319
+ test("formatTimeFull uses YYYY-MM-DD HH:mm:ss for detail panels", () => {
1320
+ expect(formatTimeFull(undefined)).toBe(UNKNOWN_VALUE);
1321
+ expect(formatTimeFull("2026-06-07T00:25:51.000Z")).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
1322
+ });
1323
+
1324
+ test("formatTimeLedger uses YYYY/MM/DD HH:mm:ss for audit tables", () => {
1325
+ expect(formatTimeLedger(undefined)).toBe(UNKNOWN_VALUE);
1326
+ expect(formatTimeLedger("2026-06-07T00:25:51.000Z")).toMatch(/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/);
1327
+ });
1328
+
1329
+ test("formatSellerId trims long tbs ids to 10 chars", () => {
1330
+ expect(formatSellerId(undefined)).toBe(UNKNOWN_VALUE);
1331
+ expect(formatSellerId("tbs-825edb")).toBe("tbs-825edb");
1332
+ expect(formatSellerId("tbs-openrouter-ai-k7p9x")).toBe("tbs-openro");
1333
+ });
1334
+
1335
+ test("formatSpeed renders to one decimal with tok/s suffix", () => {
1336
+ expect(formatSpeed(undefined)).toBe(UNKNOWN_VALUE);
1337
+ expect(formatSpeed(0)).toBe("0.0 tok/s");
1338
+ expect(formatSpeed(11.54)).toBe("11.5 tok/s");
1339
+ });
1340
+
1341
+ test("formatSellerCapacity renders used / limit with en-US grouping", () => {
1342
+ expect(formatSellerCapacity(undefined, undefined)).toBe(UNKNOWN_VALUE);
1343
+ expect(formatSellerCapacity(8, 16)).toBe("8 / 16");
1344
+ expect(formatSellerCapacity(undefined, 16)).toBe(`${UNKNOWN_VALUE} / 16`);
1345
+ expect(formatSellerCapacity(8, undefined)).toBe(`8 / ${UNKNOWN_VALUE}`);
1346
+ });
1347
+
1348
+ test("formatBalanceAmount uses 0 decimals for >=100 and 2 decimals otherwise", () => {
1349
+ expect(formatBalanceAmount(undefined, "USD")).toBe(UNKNOWN_VALUE);
1350
+ expect(formatBalanceAmount(47_950_000, "USD")).toBe("USD 47.95");
1351
+ expect(formatBalanceAmount(479_500_000, "USD")).toBe("USD 480");
1352
+ expect(formatBalanceAmount(50_000_000, "EUR")).toBe("EUR 50.00");
1353
+ });
1354
+
1355
+ test("formatSellerStatus maps all internal node statuses to the 7 canonical labels", () => {
1356
+ const canonical = new Set(["ok", "online", "configured", "pending", "degraded", "error", "unknown"]);
1357
+ for (const value of ["active", "healthy", "online", "configured", "pending", "draining", "degraded", "busy_capacity", "offline", "unhealthy", "error", "auth_unknown", "unknown", undefined, "weird-state"]) {
1358
+ expect(canonical.has(formatSellerStatus(value))).toBe(true);
1359
+ }
1360
+ expect(formatSellerStatus("active")).toBe("ok");
1361
+ expect(formatSellerStatus("draining")).toBe("degraded");
1362
+ expect(formatSellerStatus("busy_capacity")).toBe("degraded");
1363
+ expect(formatSellerStatus("offline")).toBe("error");
1364
+ expect(formatSellerStatus("auth_unknown")).toBe("unknown");
1365
+ });
1366
+
1367
+ test("statusTone / sellerStatusTone keep green/amber/red/blue/gray buckets stable", () => {
1368
+ expect(statusTone("ok")).toBe("green");
1369
+ expect(statusTone("online")).toBe("green");
1370
+ expect(statusTone("configured")).toBe("green");
1371
+ expect(statusTone("pending")).toBe("amber");
1372
+ expect(statusTone("degraded")).toBe("amber");
1373
+ expect(statusTone("error")).toBe("red");
1374
+ expect(statusTone("offline")).toBe("red");
1375
+ expect(statusTone("unknown")).toBe("gray");
1376
+ expect(statusTone("running")).toBe("blue");
1377
+ expect(sellerStatusTone("active")).toBe("green");
1378
+ expect(sellerStatusTone("draining")).toBe("amber");
1379
+ expect(sellerStatusTone("busy_capacity")).toBe("amber");
1380
+ expect(sellerStatusTone("offline")).toBe("red");
1381
+ expect(sellerStatusTone("auth_unknown")).toBe("gray");
1382
+ });
1383
+ });
1384
+
895
1385
  async function startFixtureAdminServer(): Promise<{ baseUrl: string; close: () => Promise<void> }> {
896
1386
  const server = http.createServer((req, res) => {
897
1387
  const pathName = new URL(req.url || "/", "http://127.0.0.1").pathname;
@@ -990,7 +1480,27 @@ function sendJson(res: http.ServerResponse, body: unknown): void {
990
1480
  res.end(JSON.stringify(body));
991
1481
  }
992
1482
 
993
- async function createSellerFetchJson(url: string): Promise<unknown> {
1483
+ function createSellerRequestFixture(overrides: Partial<CreateSellerRequest> = {}): CreateSellerRequest {
1484
+ return {
1485
+ sellerName: "tbs-nrt-07",
1486
+ app: "tbs-nrt-07",
1487
+ region: "nrt",
1488
+ image: "registry.fly.io/tb-seller:latest",
1489
+ upstreamWebsite: "https://openrouter.ai",
1490
+ upstreamUrl: "https://openrouter.ai/api/v1",
1491
+ upstreamApiKey: "fixture-upstream-key",
1492
+ upstreamBalanceProbeTemplate: "none",
1493
+ maxConnections: 8,
1494
+ maxQueueDepth: 4,
1495
+ markupRatio: 1.2,
1496
+ discountRatio: 1,
1497
+ paymentMethods: ["mock"],
1498
+ flyConfig: "deploy/fly.io/fly.tb-seller.toml",
1499
+ ...overrides
1500
+ };
1501
+ }
1502
+
1503
+ async function createSellerFetchJson(url: string, mode: "default" | "wrapped" | "wrappedArray" | "manifest" = "default"): Promise<unknown> {
994
1504
  const pathName = new URL(url).pathname;
995
1505
  if (pathName === "/registry/sellers") {
996
1506
  return {
@@ -999,6 +1509,38 @@ async function createSellerFetchJson(url: string): Promise<unknown> {
999
1509
  };
1000
1510
  }
1001
1511
  if (pathName === "/operator/admin/upstreams") {
1512
+ if (mode === "wrapped") {
1513
+ return {
1514
+ upstreams: {
1515
+ upstreamUrl: "https://openrouter.ai/api",
1516
+ models: [
1517
+ { id: "openai/gpt-5.4" },
1518
+ { id: "openai/gpt-5.4-mini" }
1519
+ ],
1520
+ supportedProtocols: ["chat_completions"]
1521
+ }
1522
+ };
1523
+ }
1524
+ if (mode === "wrappedArray") {
1525
+ return {
1526
+ upstreams: [{
1527
+ upstreamUrl: "https://openrouter.ai/api",
1528
+ models: [
1529
+ { id: "openai/gpt-5.4" },
1530
+ { id: "openai/gpt-5.4-mini" }
1531
+ ],
1532
+ supportedProtocols: ["chat_completions"]
1533
+ }]
1534
+ };
1535
+ }
1536
+ if (mode === "manifest") {
1537
+ return {
1538
+ upstreams: {
1539
+ upstreamUrl: "https://openrouter.ai/api",
1540
+ modelsCount: 1
1541
+ }
1542
+ };
1543
+ }
1002
1544
  return {
1003
1545
  upstreamUrl: "https://openrouter.ai/api",
1004
1546
  models: [
@@ -1015,12 +1557,19 @@ async function createSellerFetchJson(url: string): Promise<unknown> {
1015
1557
  supportedProtocols: ["chat_completions"]
1016
1558
  };
1017
1559
  }
1560
+ if (pathName === "/manifest" && mode === "manifest") {
1561
+ return {
1562
+ sellerId: "tbs-nrt-07",
1563
+ models: ["anthropic/claude-opus-4.7"],
1564
+ supportedProtocols: ["chat_completions"]
1565
+ };
1566
+ }
1018
1567
  throw new Error(`unexpected fetch url ${url}`);
1019
1568
  }
1020
1569
 
1021
- function isBootstrapSellersValidate(args: string[]): boolean {
1022
- const validateIndex = args.indexOf("validate");
1023
- return validateIndex >= 2 && args[validateIndex - 2] === "bootstrap" && args[validateIndex - 1] === "sellers";
1570
+ function isBootstrapSellersAdd(args: string[]): boolean {
1571
+ const addIndex = args.indexOf("add");
1572
+ return addIndex >= 2 && args[addIndex - 2] === "bootstrap" && args[addIndex - 1] === "sellers";
1024
1573
  }
1025
1574
 
1026
1575
  function jsonResponse(body: unknown, status = 200): Response {