@jsonstudio/rcc 0.89.1086 → 0.89.1136

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 (101) hide show
  1. package/dist/build-info.js +2 -2
  2. package/dist/cli.js +39 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/gemini/gemini-protocol-client.js +5 -0
  5. package/dist/client/gemini/gemini-protocol-client.js.map +1 -1
  6. package/dist/commands/provider-update.js +355 -5
  7. package/dist/commands/provider-update.js.map +1 -1
  8. package/dist/docs/daemon-admin-ui.html +604 -91
  9. package/dist/index.js +33 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/manager/modules/quota/index.d.ts +37 -1
  12. package/dist/manager/modules/quota/index.js +378 -18
  13. package/dist/manager/modules/quota/index.js.map +1 -1
  14. package/dist/manager/quota/provider-quota-center.d.ts +3 -0
  15. package/dist/manager/quota/provider-quota-center.js +88 -24
  16. package/dist/manager/quota/provider-quota-center.js.map +1 -1
  17. package/dist/manager/quota/provider-quota-store.js +5 -2
  18. package/dist/manager/quota/provider-quota-store.js.map +1 -1
  19. package/dist/manager/types.d.ts +5 -0
  20. package/dist/providers/core/config/service-profiles.js +1 -1
  21. package/dist/providers/core/config/service-profiles.js.map +1 -1
  22. package/dist/providers/core/runtime/gemini-cli-http-provider.js +5 -0
  23. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  24. package/dist/providers/core/runtime/http-transport-provider.js +26 -38
  25. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  26. package/dist/providers/core/utils/http-client.js +10 -1
  27. package/dist/providers/core/utils/http-client.js.map +1 -1
  28. package/dist/server/handlers/handler-utils.d.ts +1 -1
  29. package/dist/server/handlers/handler-utils.js +82 -4
  30. package/dist/server/handlers/handler-utils.js.map +1 -1
  31. package/dist/server/handlers/responses-handler.js +26 -3
  32. package/dist/server/handlers/responses-handler.js.map +1 -1
  33. package/dist/server/handlers/sse-dispatcher.js +1 -4
  34. package/dist/server/handlers/sse-dispatcher.js.map +1 -1
  35. package/dist/server/runtime/http-server/colored-logger.d.ts +1 -1
  36. package/dist/server/runtime/http-server/colored-logger.js +22 -10
  37. package/dist/server/runtime/http-server/colored-logger.js.map +1 -1
  38. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.d.ts +1 -1
  39. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +10 -14
  40. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  41. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +108 -115
  42. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  43. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +132 -7
  44. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
  45. package/dist/server/runtime/http-server/daemon-admin/restart-handler.d.ts +3 -0
  46. package/dist/server/runtime/http-server/daemon-admin/restart-handler.js +22 -0
  47. package/dist/server/runtime/http-server/daemon-admin/restart-handler.js.map +1 -0
  48. package/dist/server/runtime/http-server/daemon-admin/stats-handler.d.ts +3 -0
  49. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +56 -0
  50. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -0
  51. package/dist/server/runtime/http-server/daemon-admin/status-handler.js +100 -4
  52. package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
  53. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +24 -0
  54. package/dist/server/runtime/http-server/daemon-admin-routes.js +25 -0
  55. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  56. package/dist/server/runtime/http-server/executor-provider.js +74 -0
  57. package/dist/server/runtime/http-server/executor-provider.js.map +1 -1
  58. package/dist/server/runtime/http-server/index.d.ts +7 -1
  59. package/dist/server/runtime/http-server/index.js +171 -14
  60. package/dist/server/runtime/http-server/index.js.map +1 -1
  61. package/dist/server/runtime/http-server/middleware.d.ts +2 -1
  62. package/dist/server/runtime/http-server/middleware.js +7 -6
  63. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  64. package/dist/server/runtime/http-server/provider-utils.d.ts +1 -1
  65. package/dist/server/runtime/http-server/provider-utils.js +19 -2
  66. package/dist/server/runtime/http-server/provider-utils.js.map +1 -1
  67. package/dist/server/runtime/http-server/request-executor.js +9 -10
  68. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  69. package/dist/server/runtime/http-server/routes.d.ts +10 -0
  70. package/dist/server/runtime/http-server/routes.js +14 -1
  71. package/dist/server/runtime/http-server/routes.js.map +1 -1
  72. package/dist/server/runtime/http-server/stats-manager.d.ts +7 -0
  73. package/dist/server/runtime/http-server/stats-manager.js +22 -3
  74. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  75. package/dist/server/runtime/http-server/types.d.ts +10 -0
  76. package/dist/server/utils/http-error-mapper.js +85 -7
  77. package/dist/server/utils/http-error-mapper.js.map +1 -1
  78. package/dist/server/utils/request-id-manager.js +9 -5
  79. package/dist/server/utils/request-id-manager.js.map +1 -1
  80. package/dist/server/utils/sse-request-parser.js +2 -1
  81. package/dist/server/utils/sse-request-parser.js.map +1 -1
  82. package/dist/server/utils/utf8-chunk-buffer.d.ts +15 -30
  83. package/dist/server/utils/utf8-chunk-buffer.js +78 -88
  84. package/dist/server/utils/utf8-chunk-buffer.js.map +1 -1
  85. package/dist/server/utils/warmup-storm-tracker.js +1 -1
  86. package/dist/server/utils/warmup-storm-tracker.js.map +1 -1
  87. package/dist/token-daemon/token-daemon.js +7 -1
  88. package/dist/token-daemon/token-daemon.js.map +1 -1
  89. package/dist/tools/provider-update/fetch-models.js +8 -5
  90. package/dist/tools/provider-update/fetch-models.js.map +1 -1
  91. package/dist/tools/provider-update/probe-context.d.ts +24 -0
  92. package/dist/tools/provider-update/probe-context.js +199 -0
  93. package/dist/tools/provider-update/probe-context.js.map +1 -0
  94. package/dist/tools/provider-update/types.d.ts +1 -0
  95. package/package.json +6 -4
  96. package/scripts/scan-apply-patch-samples.mjs +362 -0
  97. package/scripts/scan-exec-command-samples.mjs +269 -0
  98. package/scripts/scan-tool-shape-samples.mjs +291 -0
  99. package/scripts/tools/sync-apply-patch-regressions.mjs +86 -0
  100. package/scripts/verify-apply-patch-regressions.mjs +119 -0
  101. package/scripts/verify-tool-arguments.mjs +1 -2
@@ -32,7 +32,7 @@
32
32
  }
33
33
 
34
34
  .container {
35
- max-width: 1120px;
35
+ max-width: 1680px;
36
36
  margin: 22px auto 40px;
37
37
  padding: 0 16px;
38
38
  }
@@ -50,6 +50,7 @@
50
50
  align-items: center;
51
51
  justify-content: space-between;
52
52
  gap: 14px;
53
+ flex-wrap: wrap;
53
54
  margin-bottom: 14px;
54
55
  }
55
56
 
@@ -156,6 +157,7 @@
156
157
  display: flex;
157
158
  gap: 6px;
158
159
  margin: 14px 0 10px;
160
+ flex-wrap: wrap;
159
161
  }
160
162
 
161
163
  .tab {
@@ -178,6 +180,15 @@
178
180
  gap: 12px;
179
181
  }
180
182
 
183
+ .grid.grid-wide-left {
184
+ grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
185
+ }
186
+
187
+ /* Ensure grid children can shrink without overflowing into the next column */
188
+ .grid > .card {
189
+ min-width: 0;
190
+ }
191
+
181
192
  @media (max-width: 980px) {
182
193
  .grid {
183
194
  grid-template-columns: minmax(0, 1fr);
@@ -199,9 +210,7 @@
199
210
  .table {
200
211
  width: 100%;
201
212
  border-collapse: collapse;
202
- border-radius: 12px;
203
- overflow: hidden;
204
- border: 1px solid var(--border);
213
+ table-layout: fixed;
205
214
  }
206
215
 
207
216
  .table th,
@@ -210,6 +219,8 @@
210
219
  font-size: 12px;
211
220
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
212
221
  vertical-align: top;
222
+ overflow-wrap: anywhere;
223
+ word-break: break-word;
213
224
  }
214
225
 
215
226
  .table th {
@@ -219,10 +230,52 @@
219
230
  background: rgba(255, 255, 255, 0.03);
220
231
  }
221
232
 
233
+ .table tr.group-row td {
234
+ background: rgba(255, 255, 255, 0.02);
235
+ color: rgba(255, 255, 255, 0.86);
236
+ font-weight: 650;
237
+ }
238
+
239
+ .indent {
240
+ padding-left: 22px !important;
241
+ }
242
+
243
+ .table-wrap {
244
+ width: 100%;
245
+ max-width: 100%;
246
+ overflow: auto;
247
+ border-radius: 12px;
248
+ border: 1px solid var(--border);
249
+ }
250
+
251
+ .table-wrap .table {
252
+ border: 0;
253
+ min-width: 860px;
254
+ }
255
+
256
+ .table td.actions-cell {
257
+ width: 220px;
258
+ overflow: visible;
259
+ }
260
+
261
+ .actions {
262
+ display: flex;
263
+ gap: 8px;
264
+ flex-wrap: wrap;
265
+ justify-content: flex-end;
266
+ }
267
+
268
+ .truncate {
269
+ white-space: nowrap;
270
+ overflow: hidden;
271
+ text-overflow: ellipsis;
272
+ }
273
+
222
274
  .mono {
223
275
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
224
276
  "Courier New", monospace;
225
277
  color: var(--muted);
278
+ word-break: break-all;
226
279
  }
227
280
 
228
281
  .notice {
@@ -246,6 +299,25 @@
246
299
  border-radius: 12px;
247
300
  padding: 10px 12px;
248
301
  }
302
+
303
+ @media (max-width: 640px) {
304
+ header {
305
+ flex-direction: column;
306
+ align-items: flex-start;
307
+ }
308
+ .statusline {
309
+ width: 100%;
310
+ justify-content: flex-start;
311
+ }
312
+ .row {
313
+ align-items: stretch;
314
+ }
315
+ .row > input,
316
+ .row > select,
317
+ .row > button {
318
+ max-width: 100%;
319
+ }
320
+ }
249
321
  </style>
250
322
  </head>
251
323
  <body>
@@ -255,13 +327,15 @@
255
327
  <div class="title">
256
328
  <h1>RouteCodex Daemon Admin</h1>
257
329
  <p>
258
- Local-only UI. Writes to <span class="mono">~/.routecodex/config.json</span>; restart
259
- required to apply routing/provider changes.
330
+ Writes to <span class="mono">~/.routecodex/config.json</span>. If
331
+ <span class="mono">httpserver.apikey</span> is configured, remote access is allowed
332
+ with the key; otherwise this page is localhost-only.
260
333
  </p>
261
334
  </div>
262
335
  <div class="statusline">
263
336
  <div class="pill"><span id="statusDot" class="dot"></span><span id="statusText">connecting…</span></div>
264
337
  <div class="pill"><span class="mono" id="serverId">serverId: —</span></div>
338
+ <button id="restartRuntimeBtn" class="primary">Restart runtime</button>
265
339
  </div>
266
340
  </header>
267
341
 
@@ -280,12 +354,14 @@
280
354
 
281
355
  <div class="tabs">
282
356
  <button class="tab active" data-tab="providers">Provider Pool</button>
357
+ <button class="tab" data-tab="tokens">Token Stats</button>
283
358
  <button class="tab" data-tab="credentials">Auth Provider Pool</button>
359
+ <button class="tab" data-tab="quota">Quota Pool</button>
284
360
  <button class="tab" data-tab="routing">Runtime Routing Pool</button>
285
361
  </div>
286
362
 
287
363
  <section id="panelProviders" data-panel="providers">
288
- <div class="grid">
364
+ <div class="grid grid-wide-left">
289
365
  <div class="card" style="box-shadow: none;">
290
366
  <p class="section-title">Providers in <span class="mono">virtualrouter.providers</span></p>
291
367
  <p class="section-sub">
@@ -295,20 +371,23 @@
295
371
  <button id="refreshProvidersBtn" class="primary">Refresh</button>
296
372
  <button id="newProviderBtn">New provider…</button>
297
373
  </div>
298
- <table class="table">
299
- <thead>
300
- <tr>
301
- <th>id</th>
302
- <th>type</th>
303
- <th>enabled</th>
304
- <th>baseURL</th>
305
- <th>models</th>
306
- <th>auth</th>
307
- <th></th>
308
- </tr>
309
- </thead>
310
- <tbody id="providersTbody"></tbody>
311
- </table>
374
+ <div class="table-wrap">
375
+ <table class="table">
376
+ <thead>
377
+ <tr>
378
+ <th>id</th>
379
+ <th>type</th>
380
+ <th>enabled</th>
381
+ <th>baseURL</th>
382
+ <th>models</th>
383
+ <th>compat</th>
384
+ <th>auth</th>
385
+ <th></th>
386
+ </tr>
387
+ </thead>
388
+ <tbody id="providersTbody"></tbody>
389
+ </table>
390
+ </div>
312
391
  </div>
313
392
 
314
393
  <div class="card" style="box-shadow: none;">
@@ -318,7 +397,7 @@
318
397
  </p>
319
398
 
320
399
  <div class="notice" style="margin-bottom: 10px;">
321
- If you use <span class="mono">httpserver.apikey</span>, set it above so API calls don’t return 401. The HTML page is always local-only.
400
+ If you use <span class="mono">httpserver.apikey</span>, set it above so API calls don’t return 401. Without apikey configured, this page is localhost-only.
322
401
  </div>
323
402
 
324
403
  <div class="row" style="margin-bottom: 10px;">
@@ -412,6 +491,37 @@
412
491
  </div>
413
492
  </section>
414
493
 
494
+ <section id="panelTokens" data-panel="tokens" style="display:none;">
495
+ <div class="grid">
496
+ <div class="card" style="box-shadow:none;">
497
+ <p class="section-title">Token usage (session + historical)</p>
498
+ <p class="section-sub">
499
+ Session stats reset on server restart. Historical totals are aggregated from
500
+ <span class="mono">~/.routecodex/logs/provider-stats.jsonl</span> (best-effort).
501
+ </p>
502
+ <div class="row" style="margin-bottom: 10px;">
503
+ <button id="refreshTokensBtn" class="primary">Refresh</button>
504
+ </div>
505
+ <div class="notice mono" id="tokenTotalsBox" style="white-space: pre-wrap;"></div>
506
+ <div class="table-wrap" style="margin-top: 10px;">
507
+ <table class="table">
508
+ <thead>
509
+ <tr>
510
+ <th>providerKey</th>
511
+ <th>model</th>
512
+ <th>session req/err</th>
513
+ <th>session tokens in/out/total</th>
514
+ <th>historical req/err</th>
515
+ <th>historical tokens in/out/total</th>
516
+ </tr>
517
+ </thead>
518
+ <tbody id="tokensTbody"></tbody>
519
+ </table>
520
+ </div>
521
+ </div>
522
+ </div>
523
+ </section>
524
+
415
525
  <section id="panelCredentials" data-panel="credentials" style="display:none;">
416
526
  <div class="grid">
417
527
  <div class="card" style="box-shadow:none;">
@@ -422,19 +532,21 @@
422
532
  <div class="row" style="margin-bottom: 10px;">
423
533
  <button id="refreshCredentialsBtn" class="primary">Refresh</button>
424
534
  </div>
425
- <table class="table">
426
- <thead>
427
- <tr>
428
- <th>kind</th>
429
- <th>provider</th>
430
- <th>alias</th>
431
- <th>status</th>
432
- <th>expires</th>
433
- <th>secretRef</th>
434
- </tr>
435
- </thead>
436
- <tbody id="credentialsTbody"></tbody>
437
- </table>
535
+ <div class="table-wrap">
536
+ <table class="table">
537
+ <thead>
538
+ <tr>
539
+ <th>kind</th>
540
+ <th>provider</th>
541
+ <th>alias</th>
542
+ <th>status</th>
543
+ <th>expires</th>
544
+ <th>secretRef</th>
545
+ </tr>
546
+ </thead>
547
+ <tbody id="credentialsTbody"></tbody>
548
+ </table>
549
+ </div>
438
550
  </div>
439
551
 
440
552
  <div class="card" style="box-shadow:none;">
@@ -474,6 +586,52 @@
474
586
  </div>
475
587
  </section>
476
588
 
589
+ <section id="panelQuota" data-panel="quota" style="display:none;">
590
+ <div class="grid">
591
+ <div class="card" style="box-shadow:none;">
592
+ <p class="section-title">Quota (daemon)</p>
593
+ <p class="section-sub">
594
+ VirtualRouter consumes this via <span class="mono">quotaView</span>. When
595
+ <span class="mono">inPool=false</span>, the provider is treated as removed from the route pool.
596
+ </p>
597
+ <div class="row" style="margin-bottom: 10px;">
598
+ <button id="refreshQuotaBtn" class="primary">Refresh</button>
599
+ <button id="resetQuotaBtn" class="danger">Reset quota module</button>
600
+ </div>
601
+ <div class="table-wrap">
602
+ <table class="table">
603
+ <thead>
604
+ <tr>
605
+ <th>providerKey</th>
606
+ <th>auth</th>
607
+ <th>inPool</th>
608
+ <th>reason</th>
609
+ <th>cooldownUntil</th>
610
+ <th>blacklistUntil</th>
611
+ <th>errCount</th>
612
+ <th></th>
613
+ </tr>
614
+ </thead>
615
+ <tbody id="quotaTbody"></tbody>
616
+ </table>
617
+ </div>
618
+ <div id="quotaOpLog" class="log" style="margin-top: 10px; display:none;"></div>
619
+ </div>
620
+
621
+ <div class="card" style="box-shadow:none;">
622
+ <p class="section-title">Notes</p>
623
+ <div class="notice">
624
+ <div style="margin-bottom: 6px;">
625
+ Use this view to confirm 429/backoff/blacklist decisions and whether a provider is currently eligible.
626
+ </div>
627
+ <div>
628
+ If a provider looks stuck, try <span class="mono">Reset quota module</span>, then <span class="mono">Restart runtime</span>.
629
+ </div>
630
+ </div>
631
+ </div>
632
+ </div>
633
+ </section>
634
+
477
635
  <section id="panelRouting" data-panel="routing" style="display:none;">
478
636
  <div class="grid">
479
637
  <div class="card" style="box-shadow:none;">
@@ -495,18 +653,20 @@
495
653
  <div class="row" style="margin-bottom: 10px;">
496
654
  <button id="refreshRuntimesBtn" class="primary">Refresh</button>
497
655
  </div>
498
- <table class="table">
499
- <thead>
500
- <tr>
501
- <th>providerKey</th>
502
- <th>runtimeKey</th>
503
- <th>family</th>
504
- <th>protocol</th>
505
- <th>series</th>
506
- </tr>
507
- </thead>
508
- <tbody id="runtimesTbody"></tbody>
509
- </table>
656
+ <div class="table-wrap">
657
+ <table class="table">
658
+ <thead>
659
+ <tr>
660
+ <th>providerKey</th>
661
+ <th>runtimeKey</th>
662
+ <th>family</th>
663
+ <th>protocol</th>
664
+ <th>series</th>
665
+ </tr>
666
+ </thead>
667
+ <tbody id="runtimesTbody"></tbody>
668
+ </table>
669
+ </div>
510
670
  </div>
511
671
  </div>
512
672
  </section>
@@ -520,7 +680,10 @@
520
680
  const el = $(id);
521
681
  if (!el) return;
522
682
  el.style.display = value ? "block" : "none";
523
- el.textContent = value || "";
683
+ const raw = value || "";
684
+ const max = 12000;
685
+ const out = raw.length > max ? raw.slice(0, max) + "\n…(truncated)" : raw;
686
+ el.textContent = out;
524
687
  }
525
688
 
526
689
  function getApiKey() {
@@ -569,10 +732,71 @@
569
732
  });
570
733
  const panels = [
571
734
  { name: "providers", el: $("panelProviders") },
735
+ { name: "tokens", el: $("panelTokens") },
572
736
  { name: "credentials", el: $("panelCredentials") },
737
+ { name: "quota", el: $("panelQuota") },
573
738
  { name: "routing", el: $("panelRouting") }
574
739
  ];
575
740
  for (const p of panels) p.el.style.display = p.name === name ? "block" : "none";
741
+
742
+ // Light auto-refresh on tab switch to avoid showing stale "Unauthorized" after setting apikey.
743
+ void maybeRefreshTab(name);
744
+ }
745
+
746
+ function getActiveTab() {
747
+ const active = document.querySelector(".tab.active");
748
+ const name = active ? active.getAttribute("data-tab") : null;
749
+ return name || "providers";
750
+ }
751
+
752
+ const tabLastRefreshedAt = {
753
+ providers: 0,
754
+ tokens: 0,
755
+ credentials: 0,
756
+ quota: 0,
757
+ routing: 0
758
+ };
759
+
760
+ async function maybeRefreshTab(name) {
761
+ const key = name in tabLastRefreshedAt ? name : "providers";
762
+ const now = Date.now();
763
+ if (now - tabLastRefreshedAt[key] < 1500) {
764
+ return;
765
+ }
766
+ tabLastRefreshedAt[key] = now;
767
+ try {
768
+ if (key === "providers") await refreshProviders();
769
+ else if (key === "tokens") await refreshTokens();
770
+ else if (key === "credentials") await refreshCredentials();
771
+ else if (key === "quota") await refreshQuota();
772
+ else if (key === "routing") await refreshRuntimes();
773
+ } catch {
774
+ // ignore refresh failures on tab switch
775
+ }
776
+ }
777
+
778
+ function textOf(value) {
779
+ if (value === null || value === undefined) return "";
780
+ return String(value);
781
+ }
782
+
783
+ function createCell(tag, text, className, opts = {}) {
784
+ const el = document.createElement(tag);
785
+ if (className) el.className = className;
786
+ const s = textOf(text);
787
+ el.textContent = s;
788
+ if (opts.title && s) el.title = s;
789
+ return el;
790
+ }
791
+
792
+ function createErrorRow(colSpan, message) {
793
+ const tr = document.createElement("tr");
794
+ const td = document.createElement("td");
795
+ td.colSpan = colSpan;
796
+ td.className = "mono";
797
+ td.textContent = `Failed to load: ${textOf(message)}`;
798
+ tr.appendChild(td);
799
+ return tr;
576
800
  }
577
801
 
578
802
  function presetFor(type) {
@@ -640,29 +864,67 @@
640
864
 
641
865
  async function refreshProviders() {
642
866
  const body = $("providersTbody");
643
- body.innerHTML = "";
867
+ body.replaceChildren();
644
868
  try {
645
869
  const data = await apiFetch("/config/providers");
646
- for (const p of data.providers || []) {
647
- const tr = document.createElement("tr");
648
- tr.innerHTML = `
649
- <td class="mono">${p.id || ""}</td>
650
- <td>${p.type || ""}</td>
651
- <td>${String(p.enabled)}</td>
652
- <td class="mono">${p.baseURL || ""}</td>
653
- <td>${p.modelCount || 0}</td>
654
- <td>${p.authType || ""}</td>
655
- <td>
656
- <button data-action="edit" data-id="${p.id}">Edit</button>
657
- <button data-action="delete" data-id="${p.id}" class="danger">Delete</button>
658
- </td>
659
- `;
660
- body.appendChild(tr);
870
+ const list = Array.isArray(data.providers) ? data.providers : [];
871
+ const grouped = new Map();
872
+ for (const p of list) {
873
+ const t = textOf(p.type || "unknown") || "unknown";
874
+ if (!grouped.has(t)) grouped.set(t, []);
875
+ grouped.get(t).push(p);
876
+ }
877
+ const types = Array.from(grouped.keys()).sort((a, b) => a.localeCompare(b));
878
+ for (const type of types) {
879
+ const groupRow = document.createElement("tr");
880
+ groupRow.className = "group-row";
881
+ const groupCell = document.createElement("td");
882
+ groupCell.colSpan = 8;
883
+ groupCell.textContent = `${type} (${grouped.get(type).length})`;
884
+ groupRow.appendChild(groupCell);
885
+ body.appendChild(groupRow);
886
+
887
+ const items = grouped.get(type);
888
+ items.sort((a, b) => textOf(a.id).localeCompare(textOf(b.id)));
889
+ for (const p of items) {
890
+ const tr = document.createElement("tr");
891
+ tr.appendChild(createCell("td", p.id || "", "mono indent"));
892
+ tr.appendChild(createCell("td", p.type || "", ""));
893
+ tr.appendChild(createCell("td", String(Boolean(p.enabled)), ""));
894
+ tr.appendChild(createCell("td", p.baseURL || "", "mono truncate", { title: true }));
895
+ const preview = Array.isArray(p.modelsPreview) ? p.modelsPreview.map((x) => textOf(x)).filter(Boolean) : [];
896
+ const modelSummary = preview.length ? `${p.modelCount || 0}: ${preview.join(", ")}${(p.modelCount || 0) > preview.length ? ", …" : ""}` : String(p.modelCount || 0);
897
+ tr.appendChild(createCell("td", modelSummary, "mono truncate", { title: true }));
898
+ tr.appendChild(createCell("td", p.compatibilityProfile || "", "mono truncate", { title: true }));
899
+ tr.appendChild(createCell("td", p.authType || "", ""));
900
+ const actionsTd = document.createElement("td");
901
+ actionsTd.className = "actions-cell";
902
+ const box = document.createElement("div");
903
+ box.className = "actions";
904
+ const edit = document.createElement("button");
905
+ edit.textContent = "Edit";
906
+ edit.setAttribute("data-action", "edit");
907
+ edit.setAttribute("data-id", textOf(p.id));
908
+ const test = document.createElement("button");
909
+ test.textContent = "Test";
910
+ test.setAttribute("data-action", "test");
911
+ test.setAttribute("data-id", textOf(p.id));
912
+ test.disabled = !(p && p.enabled !== false && Number(p.modelCount || 0) > 0);
913
+ const del = document.createElement("button");
914
+ del.textContent = "Delete";
915
+ del.className = "danger";
916
+ del.setAttribute("data-action", "delete");
917
+ del.setAttribute("data-id", textOf(p.id));
918
+ box.appendChild(edit);
919
+ box.appendChild(test);
920
+ box.appendChild(del);
921
+ actionsTd.appendChild(box);
922
+ tr.appendChild(actionsTd);
923
+ body.appendChild(tr);
924
+ }
661
925
  }
662
926
  } catch (e) {
663
- const tr = document.createElement("tr");
664
- tr.innerHTML = `<td colspan="7" class="mono">Failed to load: ${e.message}</td>`;
665
- body.appendChild(tr);
927
+ body.appendChild(createErrorRow(8, e && e.message ? e.message : e));
666
928
  }
667
929
  }
668
930
 
@@ -775,26 +1037,22 @@
775
1037
 
776
1038
  async function refreshCredentials() {
777
1039
  const body = $("credentialsTbody");
778
- body.innerHTML = "";
1040
+ body.replaceChildren();
779
1041
  try {
780
1042
  const items = await apiFetch("/daemon/credentials");
781
1043
  for (const c of items || []) {
782
1044
  const tr = document.createElement("tr");
783
1045
  const exp = c.expiresInSec == null ? "—" : `${c.expiresInSec}s`;
784
- tr.innerHTML = `
785
- <td>${c.kind}</td>
786
- <td class="mono">${c.provider}</td>
787
- <td class="mono">${c.alias}</td>
788
- <td>${c.status}</td>
789
- <td class="mono">${exp}</td>
790
- <td class="mono">${c.secretRef || "—"}</td>
791
- `;
1046
+ tr.appendChild(createCell("td", c.kind || "", ""));
1047
+ tr.appendChild(createCell("td", c.provider || "", "mono"));
1048
+ tr.appendChild(createCell("td", c.alias || "", "mono"));
1049
+ tr.appendChild(createCell("td", c.status || "", ""));
1050
+ tr.appendChild(createCell("td", exp, "mono"));
1051
+ tr.appendChild(createCell("td", c.secretRef || "—", "mono"));
792
1052
  body.appendChild(tr);
793
1053
  }
794
1054
  } catch (e) {
795
- const tr = document.createElement("tr");
796
- tr.innerHTML = `<td colspan="6" class="mono">Failed to load: ${e.message}</td>`;
797
- body.appendChild(tr);
1055
+ body.appendChild(createErrorRow(6, e && e.message ? e.message : e));
798
1056
  }
799
1057
  }
800
1058
 
@@ -865,24 +1123,236 @@
865
1123
 
866
1124
  async function refreshRuntimes() {
867
1125
  const body = $("runtimesTbody");
868
- body.innerHTML = "";
1126
+ body.replaceChildren();
869
1127
  try {
870
1128
  const items = await apiFetch("/providers/runtimes");
871
1129
  for (const r of items || []) {
872
1130
  const tr = document.createElement("tr");
873
- tr.innerHTML = `
874
- <td class="mono">${r.providerKey || ""}</td>
875
- <td class="mono">${r.runtimeKey || ""}</td>
876
- <td>${r.family || ""}</td>
877
- <td>${r.protocol || ""}</td>
878
- <td>${r.series || ""}</td>
879
- `;
1131
+ tr.appendChild(createCell("td", r.providerKey || "", "mono truncate", { title: true }));
1132
+ tr.appendChild(createCell("td", r.runtimeKey || "", "mono truncate", { title: true }));
1133
+ tr.appendChild(createCell("td", r.family || "", ""));
1134
+ tr.appendChild(createCell("td", r.protocol || "", ""));
1135
+ tr.appendChild(createCell("td", r.series || "", ""));
1136
+ body.appendChild(tr);
1137
+ }
1138
+ } catch (e) {
1139
+ body.appendChild(createErrorRow(5, e && e.message ? e.message : e));
1140
+ }
1141
+ }
1142
+
1143
+ function formatEpochMs(ms) {
1144
+ if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0) return "—";
1145
+ try {
1146
+ return new Date(ms).toLocaleString();
1147
+ } catch {
1148
+ return String(ms);
1149
+ }
1150
+ }
1151
+
1152
+ async function refreshQuota() {
1153
+ const body = $("quotaTbody");
1154
+ body.replaceChildren();
1155
+ setLog("quotaOpLog", "");
1156
+ try {
1157
+ const out = await apiFetch("/quota/providers");
1158
+ const list = Array.isArray(out.providers) ? out.providers : [];
1159
+ for (const q of list) {
1160
+ const tr = document.createElement("tr");
1161
+ tr.appendChild(createCell("td", q.providerKey || "", "mono truncate", { title: true }));
1162
+ tr.appendChild(createCell("td", q.authType || "", ""));
1163
+ tr.appendChild(createCell("td", String(Boolean(q.inPool)), ""));
1164
+ tr.appendChild(createCell("td", q.reason || "", ""));
1165
+ tr.appendChild(createCell("td", formatEpochMs(q.cooldownUntil), "mono"));
1166
+ tr.appendChild(createCell("td", formatEpochMs(q.blacklistUntil), "mono"));
1167
+ tr.appendChild(createCell("td", q.consecutiveErrorCount ?? 0, "mono"));
1168
+ const actionsTd = document.createElement("td");
1169
+ actionsTd.className = "actions-cell";
1170
+ const box = document.createElement("div");
1171
+ box.className = "actions";
1172
+ const recover = document.createElement("button");
1173
+ recover.textContent = "Recover";
1174
+ recover.setAttribute("data-action", "quota-recover");
1175
+ recover.setAttribute("data-key", textOf(q.providerKey));
1176
+ const reset = document.createElement("button");
1177
+ reset.textContent = "Reset";
1178
+ reset.setAttribute("data-action", "quota-reset");
1179
+ reset.setAttribute("data-key", textOf(q.providerKey));
1180
+ const disable = document.createElement("button");
1181
+ disable.textContent = "Disable…";
1182
+ disable.className = "danger";
1183
+ disable.setAttribute("data-action", "quota-disable");
1184
+ disable.setAttribute("data-key", textOf(q.providerKey));
1185
+ box.appendChild(recover);
1186
+ box.appendChild(reset);
1187
+ box.appendChild(disable);
1188
+ actionsTd.appendChild(box);
1189
+ tr.appendChild(actionsTd);
1190
+ body.appendChild(tr);
1191
+ }
1192
+ } catch (e) {
1193
+ body.appendChild(createErrorRow(8, e && e.message ? e.message : e));
1194
+ }
1195
+ }
1196
+
1197
+ function formatInt(n) {
1198
+ const v = typeof n === "number" && Number.isFinite(n) ? n : 0;
1199
+ try { return v.toLocaleString(); } catch { return String(v); }
1200
+ }
1201
+
1202
+ function formatTokensRow(label, totals) {
1203
+ const inTok = formatInt(totals.totalPromptTokens);
1204
+ const outTok = formatInt(totals.totalCompletionTokens);
1205
+ const totTok = formatInt(totals.totalOutputTokens);
1206
+ const req = formatInt(totals.requestCount);
1207
+ const err = formatInt(totals.errorCount);
1208
+ return `${label}: requests=${req} (err=${err}) tokens in/out/total=${inTok}/${outTok}/${totTok}`;
1209
+ }
1210
+
1211
+ async function refreshTokens() {
1212
+ const body = $("tokensTbody");
1213
+ body.replaceChildren();
1214
+ $("tokenTotalsBox").textContent = "";
1215
+ try {
1216
+ const out = await apiFetch("/daemon/stats");
1217
+ const session = out && out.session ? out.session : null;
1218
+ const historical = out && out.historical ? out.historical : null;
1219
+ const totals = out && out.totals ? out.totals : null;
1220
+
1221
+ const sessionTotals = totals && totals.session ? totals.session : { requestCount: 0, errorCount: 0, totalPromptTokens: 0, totalCompletionTokens: 0, totalOutputTokens: 0 };
1222
+ const historicalTotals = totals && totals.historical ? totals.historical : { requestCount: 0, errorCount: 0, totalPromptTokens: 0, totalCompletionTokens: 0, totalOutputTokens: 0 };
1223
+
1224
+ const lines = [];
1225
+ lines.push(formatTokensRow("ALL (session)", sessionTotals));
1226
+ lines.push(formatTokensRow("ALL (historical)", historicalTotals));
1227
+ $("tokenTotalsBox").textContent = lines.join("\n");
1228
+
1229
+ const sessionRows = session && Array.isArray(session.totals) ? session.totals : [];
1230
+ const histRows = historical && Array.isArray(historical.totals) ? historical.totals : [];
1231
+
1232
+ const byKey = new Map();
1233
+ const keyOf = (r) => `${textOf(r.providerKey)}|${textOf(r.model || "")}`;
1234
+ for (const r of sessionRows) {
1235
+ if (!r || !r.providerKey) continue;
1236
+ byKey.set(keyOf(r), { providerKey: textOf(r.providerKey), model: textOf(r.model || ""), session: r, historical: null });
1237
+ }
1238
+ for (const r of histRows) {
1239
+ if (!r || !r.providerKey) continue;
1240
+ const k = keyOf(r);
1241
+ const existing = byKey.get(k) || { providerKey: textOf(r.providerKey), model: textOf(r.model || ""), session: null, historical: null };
1242
+ existing.historical = r;
1243
+ byKey.set(k, existing);
1244
+ }
1245
+
1246
+ const rows = Array.from(byKey.values()).sort((a, b) => {
1247
+ const ak = `${a.providerKey}.${a.model}`;
1248
+ const bk = `${b.providerKey}.${b.model}`;
1249
+ return ak.localeCompare(bk);
1250
+ });
1251
+
1252
+ for (const row of rows) {
1253
+ const tr = document.createElement("tr");
1254
+ tr.appendChild(createCell("td", row.providerKey, "mono truncate", { title: true }));
1255
+ tr.appendChild(createCell("td", row.model || "—", "mono truncate", { title: true }));
1256
+
1257
+ const s = row.session;
1258
+ const sReqErr = s ? `${formatInt(s.requestCount)} / ${formatInt(s.errorCount)}` : "—";
1259
+ const sTok = s ? `${formatInt(s.totalPromptTokens)}/${formatInt(s.totalCompletionTokens)}/${formatInt(s.totalOutputTokens)}` : "—";
1260
+ tr.appendChild(createCell("td", sReqErr, "mono"));
1261
+ tr.appendChild(createCell("td", sTok, "mono"));
1262
+
1263
+ const h = row.historical;
1264
+ const hReqErr = h ? `${formatInt(h.requestCount)} / ${formatInt(h.errorCount)}` : "—";
1265
+ const hTok = h ? `${formatInt(h.totalPromptTokens)}/${formatInt(h.totalCompletionTokens)}/${formatInt(h.totalOutputTokens)}` : "—";
1266
+ tr.appendChild(createCell("td", hReqErr, "mono"));
1267
+ tr.appendChild(createCell("td", hTok, "mono"));
1268
+
880
1269
  body.appendChild(tr);
881
1270
  }
882
1271
  } catch (e) {
883
- const tr = document.createElement("tr");
884
- tr.innerHTML = `<td colspan="5" class="mono">Failed to load: ${e.message}</td>`;
885
- body.appendChild(tr);
1272
+ body.appendChild(createErrorRow(6, e && e.message ? e.message : e));
1273
+ }
1274
+ }
1275
+
1276
+ async function testProviderFromPool(providerId) {
1277
+ setLog("providerOpLog", "");
1278
+ try {
1279
+ const detail = await apiFetch(`/config/providers/${encodeURIComponent(providerId)}`);
1280
+ const provider = detail && detail.provider ? detail.provider : null;
1281
+ const models = provider && provider.models && typeof provider.models === "object" ? Object.keys(provider.models) : [];
1282
+ if (!models.length) {
1283
+ throw new Error("No models configured for this provider");
1284
+ }
1285
+ const modelId = models[0];
1286
+ const directModel = `${providerId}.${modelId}`;
1287
+ const payload = { model: directModel, input: [{ role: "user", content: "ping" }], stream: false };
1288
+ const headers = new Headers({ "content-type": "application/json" });
1289
+ const apiKey = getApiKey();
1290
+ if (apiKey) headers.set("x-api-key", apiKey);
1291
+ const started = Date.now();
1292
+ const resp = await fetch("/v1/responses", { method: "POST", headers, body: JSON.stringify(payload) });
1293
+ const text = await resp.text();
1294
+ const ms = Date.now() - started;
1295
+ if (!resp.ok) {
1296
+ throw new Error(`HTTP ${resp.status} (${ms}ms): ${text}`);
1297
+ }
1298
+ let json = null;
1299
+ try { json = text ? JSON.parse(text) : null; } catch { json = null; }
1300
+ const summary =
1301
+ json && typeof json.output_text === "string"
1302
+ ? json.output_text.slice(0, 200)
1303
+ : json && Array.isArray(json.output)
1304
+ ? "(output items=" + json.output.length + ")"
1305
+ : "(ok)";
1306
+ setLog("providerOpLog", `Test OK (${ms}ms) model=${directModel}\n${summary}`);
1307
+ } catch (e) {
1308
+ setLog("providerOpLog", `Test failed: ${e && e.message ? e.message : e}`);
1309
+ }
1310
+ }
1311
+
1312
+ async function quotaAction(kind, providerKey) {
1313
+ setLog("quotaOpLog", "");
1314
+ if (!providerKey) return;
1315
+ try {
1316
+ if (kind === "recover") {
1317
+ await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/recover`, { method: "POST" });
1318
+ await refreshQuota();
1319
+ return;
1320
+ }
1321
+ if (kind === "reset") {
1322
+ await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/reset`, { method: "POST" });
1323
+ await refreshQuota();
1324
+ return;
1325
+ }
1326
+ if (kind === "disable") {
1327
+ const minutesRaw = prompt("Disable duration (minutes)", "60");
1328
+ if (!minutesRaw) return;
1329
+ const minutes = Number.parseFloat(minutesRaw);
1330
+ if (!Number.isFinite(minutes) || minutes <= 0) {
1331
+ throw new Error("Invalid minutes");
1332
+ }
1333
+ const modeRaw = prompt("Mode: cooldown or blacklist", "cooldown");
1334
+ const mode = (modeRaw || "cooldown").trim().toLowerCase() === "blacklist" ? "blacklist" : "cooldown";
1335
+ await apiFetch(`/quota/providers/${encodeURIComponent(providerKey)}/disable`, {
1336
+ method: "POST",
1337
+ body: JSON.stringify({ mode, durationMinutes: minutes })
1338
+ });
1339
+ await refreshQuota();
1340
+ return;
1341
+ }
1342
+ } catch (e) {
1343
+ setLog("quotaOpLog", `Action failed: ${e && e.message ? e.message : e}`);
1344
+ }
1345
+ }
1346
+
1347
+ async function resetQuotaModule() {
1348
+ setLog("quotaOpLog", "");
1349
+ if (!confirm("Reset provider-quota module now? This clears cooldown/blacklist state.")) return;
1350
+ try {
1351
+ const out = await apiFetch("/daemon/modules/provider-quota/reset", { method: "POST" });
1352
+ setLog("quotaOpLog", `OK. resetAt=${out.resetAt || "—"}`);
1353
+ await refreshQuota();
1354
+ } catch (e) {
1355
+ setLog("quotaOpLog", `Reset failed: ${e.message}`);
886
1356
  }
887
1357
  }
888
1358
 
@@ -895,14 +1365,40 @@
895
1365
  const value = ($("apiKeyInput").value || "").trim();
896
1366
  setApiKey(value);
897
1367
  $("apiKeyHint").textContent = value ? "saved (session only)" : "";
1368
+ Promise.resolve()
1369
+ .then(refreshStatus)
1370
+ .then(() => selectTab(getActiveTab()))
1371
+ .catch(() => {});
898
1372
  });
899
1373
  $("clearApiKeyBtn").addEventListener("click", () => {
900
1374
  setApiKey("");
901
1375
  $("apiKeyInput").value = "";
902
1376
  $("apiKeyHint").textContent = "";
1377
+ Promise.resolve()
1378
+ .then(refreshStatus)
1379
+ .then(() => selectTab(getActiveTab()))
1380
+ .catch(() => {});
1381
+ });
1382
+
1383
+ $("restartRuntimeBtn").addEventListener("click", async () => {
1384
+ setLog("providerOpLog", "");
1385
+ if (!confirm("Reload config from disk and rebuild runtime now?")) return;
1386
+ try {
1387
+ const out = await apiFetch("/daemon/restart", { method: "POST" });
1388
+ const warnings = Array.isArray(out.warnings) && out.warnings.length ? `\nWarnings:\n- ${out.warnings.join("\n- ")}` : "";
1389
+ setLog("providerOpLog", `Restarted.\nconfigPath: ${out.configPath || "—"}\nreloadedAt: ${out.reloadedAt || "—"}${warnings}`);
1390
+ await refreshStatus();
1391
+ await refreshProviders();
1392
+ await refreshCredentials();
1393
+ await refreshQuota();
1394
+ await refreshRuntimes();
1395
+ } catch (e) {
1396
+ setLog("providerOpLog", `Restart failed: ${e.message}`);
1397
+ }
903
1398
  });
904
1399
 
905
1400
  $("refreshProvidersBtn").addEventListener("click", refreshProviders);
1401
+ $("refreshTokensBtn").addEventListener("click", refreshTokens);
906
1402
  $("newProviderBtn").addEventListener("click", () => {
907
1403
  $("providerEditorTitle").textContent = "Provider editor (new)";
908
1404
  $("providerIdInput").value = "";
@@ -914,6 +1410,10 @@
914
1410
  if (!btn) return;
915
1411
  const id = btn.getAttribute("data-id");
916
1412
  const action = btn.getAttribute("data-action");
1413
+ if (action === "test" && id) {
1414
+ await testProviderFromPool(id);
1415
+ return;
1416
+ }
917
1417
  if (action === "edit") await loadProvider(id);
918
1418
  if (action === "delete") await deleteProvider(id);
919
1419
  });
@@ -936,6 +1436,18 @@
936
1436
  $("saveOauthBrowserBtn").addEventListener("click", saveSettings);
937
1437
  $("oauthAuthorizeBtn").addEventListener("click", authorizeOauth);
938
1438
 
1439
+ $("refreshQuotaBtn").addEventListener("click", refreshQuota);
1440
+ $("quotaTbody").addEventListener("click", (ev) => {
1441
+ const el = ev.target;
1442
+ if (!el || el.tagName !== "BUTTON") return;
1443
+ const action = el.getAttribute("data-action");
1444
+ const key = el.getAttribute("data-key");
1445
+ if (action === "quota-recover") void quotaAction("recover", key);
1446
+ else if (action === "quota-reset") void quotaAction("reset", key);
1447
+ else if (action === "quota-disable") void quotaAction("disable", key);
1448
+ });
1449
+ $("resetQuotaBtn").addEventListener("click", resetQuotaModule);
1450
+
939
1451
  $("loadRoutingBtn").addEventListener("click", loadRouting);
940
1452
  $("saveRoutingBtn").addEventListener("click", saveRouting);
941
1453
  $("refreshRuntimesBtn").addEventListener("click", refreshRuntimes);
@@ -950,9 +1462,10 @@
950
1462
  await refreshStatus();
951
1463
  await refreshProviders();
952
1464
  await refreshCredentials();
1465
+ await refreshQuota();
1466
+ await refreshRuntimes();
953
1467
  await loadSettings();
954
1468
  })();
955
1469
  </script>
956
1470
  </body>
957
1471
  </html>
958
-