@jeffusion/bungee-core 6.3.0 → 6.3.1

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 (36) hide show
  1. package/dist/main.js +29 -28
  2. package/dist/observability-worker.js +14 -1
  3. package/dist/plugins/anthropic-request-sanitizer/index.js.map +1 -1
  4. package/dist/plugins/anthropic-tool-name-transformer/index.js.map +1 -1
  5. package/dist/plugins/chatgpt-oauth/control.js.map +1 -1
  6. package/dist/plugins/chatgpt-oauth/index.js.map +1 -1
  7. package/dist/plugins/chatgpt-oauth/ui/AccountsPage.svelte +1 -1
  8. package/dist/plugins/codex-router/control.js.map +1 -1
  9. package/dist/plugins/codex-router/index.js.map +1 -1
  10. package/dist/plugins/deepseek-reasoning-fix/index.js.map +1 -1
  11. package/dist/plugins/key-access/control.js.map +1 -1
  12. package/dist/plugins/key-access/index.js.map +1 -1
  13. package/dist/plugins/key-access/ingress.js.map +1 -1
  14. package/dist/plugins/key-rate-limit/index.js.map +1 -1
  15. package/dist/plugins/llm-protocol-adapter/control.js.map +1 -1
  16. package/dist/plugins/llm-protocol-adapter/index.js.map +3 -3
  17. package/dist/plugins/llm-tps/control.js +23 -10
  18. package/dist/plugins/llm-tps/control.js.map +5 -5
  19. package/dist/plugins/llm-tps/index.js +17 -1
  20. package/dist/plugins/llm-tps/index.js.map +6 -6
  21. package/dist/plugins/llm-tps/ui/TpsWidget.svelte +29 -50
  22. package/dist/plugins/llm-tps/ui/presentation.ts +1 -15
  23. package/dist/plugins/llm-tps/ui/stats-resource.ts +2 -2
  24. package/dist/plugins/model-mapping/index.js.map +1 -1
  25. package/dist/plugins/models-dev/index.js.map +1 -1
  26. package/dist/plugins/signature-repair/index.js.map +1 -1
  27. package/dist/plugins/token-budget/control.js.map +1 -1
  28. package/dist/plugins/token-budget/index.js.map +2 -2
  29. package/dist/plugins/token-metering/index.js +7 -3
  30. package/dist/plugins/token-metering/index.js.map +5 -5
  31. package/dist/plugins/token-stats/control.js.map +1 -1
  32. package/dist/plugins/token-stats/index.js.map +2 -2
  33. package/dist/plugins/token-stats/ui/TokenStatsPage.svelte +2 -2
  34. package/dist/plugins/user-auth/control.js.map +1 -1
  35. package/dist/plugins/user-auth/index.js.map +1 -1
  36. package/package.json +2 -2
@@ -1,17 +1,15 @@
1
1
  <script lang="ts">
2
2
  import { onMount, untrack } from 'svelte';
3
3
  import { isLoading } from 'svelte-i18n';
4
- import { _, type TimeRange } from '@bungee/plugin-sdk';
4
+ import { _, formatCompactNumber, type TimeRange } from '@bungee/plugin-sdk';
5
5
  import { getPluginText } from '$utils/plugin-i18n';
6
- import { configStore, routesStore } from '$stores/config';
7
- import { resolveRouteEndpoints } from '$api/routes';
8
- import { toEditorRoute, toEditorService } from '$api/config-adapters';
9
6
  import type { NativeWidgetHeaderChange } from '$components/native-widgets/widget-header';
10
7
  import type { TpsRange } from '../contract';
11
8
  import type { TpsResource, TpsState } from './stats-resource';
12
9
  import { getTpsResource } from './stats';
13
- import { endpointIdentity, endpointLabel, formatTps, rankedEndpoints, rankedModels } from './presentation';
10
+ import { formatTps, rankedEndpoints, rankedModels } from './presentation';
14
11
  import Sparkline from '$components/dashboard/Sparkline.svelte';
12
+ import { BKpiMetric } from '$components/industrial';
15
13
 
16
14
  let { pluginName, selectedRange, onHeaderChange, mode }:
17
15
  { pluginName: string; selectedRange: TimeRange; onHeaderChange?: NativeWidgetHeaderChange; mode: 'overview' | 'endpoints' | 'models' } = $props();
@@ -20,34 +18,13 @@
20
18
  const range: TpsRange = $derived(selectedRange === '1h' || selectedRange === '12h' ? selectedRange : '24h');
21
19
  const t = (key: string, values?: Record<string, string | number>) => $isLoading ? '' : getPluginText(key, pluginName,
22
20
  (id, options) => $_(id, { ...options, values }));
23
- const maximum = $derived(Math.max(100,
24
- (state.data?.endpoints ?? []).reduce((max, row) => Math.max(max, row.tps ?? 0), 0),
25
- (state.data?.models ?? []).reduce((max, row) => Math.max(max, row.tps ?? 0), 0)));
26
21
  const sampleCount = $derived(state.data?.total.sampleCount ?? 0);
27
22
  const estimatedCount = $derived(state.data?.total.estimatedSampleCount ?? 0);
28
- const trendValues = $derived((state.data?.history ?? []).map(point => point.tps ?? NaN));
29
- const services = $derived($configStore?.services ?? []);
30
- const routeLabels = $derived(new Map($routesStore.map(route => [route.id,
31
- 'name' in route && typeof route.name === 'string' && route.name.trim() ? route.name.trim() : route.path || '—'])));
32
- const labels = $derived.by(() => {
33
- const result = new Map<string, { route: string; endpoint: string; description: string }>();
34
- const editorServices = services.map(toEditorService);
35
- for (const route of $routesStore) {
36
- const endpoints = resolveRouteEndpoints(toEditorRoute(route, services), editorServices);
37
- for (const endpoint of endpoints) if (endpoint._uid) result.set(JSON.stringify([route.id, endpoint._uid]), {
38
- route: routeLabels.get(route.id) ?? '—',
39
- endpoint: endpointLabel(endpoint.target, endpoint._uid),
40
- description: endpoint.description?.trim() ?? '',
41
- });
42
- }
43
- return result;
44
- });
23
+ const trendValues = $derived((state.data?.history ?? []).map(point => point.tps ?? 0));
45
24
  const rows = $derived(mode === 'models'
46
- ? rankedModels(state.data?.models ?? []).map(row => ({ ...row, key: row.model, label: row.model, description: '' }))
47
- : rankedEndpoints(state.data?.endpoints ?? []).map(row => {
48
- const label = labels.get(JSON.stringify([row.routeId, row.endpointId]));
49
- return { ...row, key: row.endpointId, label: label?.endpoint ?? endpointLabel(undefined, row.endpointId), description: label?.description ?? '' };
50
- }));
25
+ ? rankedModels(state.data?.models ?? []).map(row => ({ ...row, key: row.model, label: row.model }))
26
+ : rankedEndpoints(state.data?.endpoints ?? []).map(row => ({ ...row, key: row.upstreamHost, label: row.upstreamHost })));
27
+ const maximum = $derived(rows.reduce((max, row) => Math.max(max, row.tps ?? 0), 0));
51
28
  $effect(() => {
52
29
  const next = getTpsResource(pluginName, range);
53
30
  resource = next;
@@ -57,6 +34,7 @@
57
34
  const report = onHeaderChange;
58
35
  const header = { summary: range, refresh: { label: t('ui.refresh'), busy: state.busy,
59
36
  disabled: !resource, run: () => { void resource?.refresh(); } },
37
+ ...(mode !== 'overview' ? { actions: rankingUnit } : {}),
60
38
  ...(mode === 'overview' && state.data?.total.sampleCount ? { footer: sampleFooter } : {}) };
61
39
  untrack(() => report?.(header));
62
40
  });
@@ -66,6 +44,10 @@
66
44
  });
67
45
  </script>
68
46
 
47
+ {#snippet rankingUnit()}
48
+ <span class="ranking-unit">TOKENS/S</span>
49
+ {/snippet}
50
+
69
51
  {#snippet sampleFooter()}
70
52
  {#if sampleCount}
71
53
  <div class="tps-samples" data-testid="tps-total-samples">
@@ -80,13 +62,10 @@
80
62
  <span class="nx-metric text-zinc-50">—</span>
81
63
  {:else}
82
64
  {#if mode === 'overview'}
83
- <div class="kpi-metric-row flex w-full min-w-0 items-center justify-between gap-3">
84
- <div class="kpi-metric-group flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-1.5">
85
- <span class="nx-metric tps-value text-zinc-50" data-testid="tps-total-value">{formatTps(state.data?.total.sampleCount ? state.data.total.tps : null)}</span>
86
- <span class="nx-label">TOKENS/S</span>
87
- </div>
88
- <div class="kpi-icon flex shrink-0 items-center"><Sparkline values={trendValues} tone="orange" /></div>
89
- </div>
65
+ <BKpiMetric value={formatCompactNumber(state.data?.total.sampleCount ? state.data.total.tps : null)}
66
+ valueTitle={`${formatTps(state.data?.total.sampleCount ? state.data.total.tps : null)} TOKENS/S`} unit="TOKENS/S" valueTestId="tps-total-value">
67
+ {#snippet icon()}<Sparkline values={trendValues} tone="orange" wide />{/snippet}
68
+ </BKpiMetric>
90
69
  {/if}
91
70
  {#if state.error}
92
71
  <p role="alert" class="text-sm text-red-300" data-testid="tps-error">{t('ui.loadFailed')}</p>
@@ -96,20 +75,19 @@
96
75
  {#if mode === 'overview' ? !state.data.total.sampleCount : !rows.length}
97
76
  <p class="text-sm text-zinc-400" data-testid="tps-empty">{t('ui.noData')}</p>
98
77
  {:else if mode !== 'overview'}
99
- <p class="ranking-unit">TOKENS/S</p>
100
78
  <!-- svelte-ignore a11y_no_noninteractive_tabindex (A scrolling region needs keyboard focus so every endpoint remains reachable.) -->
101
79
  <div class="ranking-list" role="region" aria-label={t(mode === 'models' ? 'ui.modelRanking' : 'ui.ranking')} data-testid={mode === 'models' ? 'tps-model-list' : 'tps-endpoint-list'} tabindex="0">
102
80
  <ol class="ranking-items">
103
81
  {#each rows as row, index (JSON.stringify([row.routeId, row.key]))}
104
82
  <li class="ranking-row" data-testid={mode === 'models' ? 'tps-model-row' : 'tps-endpoint-row'} data-route-id={row.routeId}
105
- data-endpoint-id={mode === 'endpoints' ? endpointIdentity(row.key) : undefined} data-model={mode === 'models' ? row.key : undefined}>
83
+ data-upstream-host={mode === 'endpoints' ? row.key : undefined} data-model={mode === 'models' ? row.key : undefined}>
106
84
  <span class="ranking-position nx-display" aria-hidden="true">{index + 1}</span>
107
85
  <div class="ranking-identity">
108
- <p class="ranking-name" title={row.description || undefined}>{row.label}</p>
109
- <p class="ranking-route">{routeLabels.get(row.routeId) ?? '—'}</p>
86
+ <p class="ranking-name">{row.label}</p>
87
+ <p class="ranking-route">{row.routeId}</p>
110
88
  </div>
111
- <span class="ranking-value nx-display" title={t('ui.samples', { count: row.sampleCount, estimated: row.estimatedSampleCount })}>{formatTps(row.sampleCount ? row.tps : null)}</span>
112
- <div class="ranking-track" aria-hidden="true"><span class="ranking-bar" style:width={`${(row.tps ?? 0) / maximum * 100}%`}></span></div>
89
+ <span class="ranking-value nx-display" title={`${formatTps(row.sampleCount ? row.tps : null)} TOKENS/S · ${t('ui.samples', { count: row.sampleCount, estimated: row.estimatedSampleCount })}`}>{formatCompactNumber(row.sampleCount ? row.tps : null)}</span>
90
+ <div class="ranking-track" aria-hidden="true"><span class="ranking-bar" style:width={`${maximum > 0 ? (row.tps ?? 0) / maximum * 100 : 0}%`}></span></div>
113
91
  </li>
114
92
  {/each}
115
93
  </ol>
@@ -122,20 +100,21 @@
122
100
  <style>
123
101
  .tps-content { display: flex; flex-direction: column; gap: 6px; }
124
102
  .tps-content:not(.tps-overview) { height: 100%; min-height: 0; }
125
- .tps-overview { justify-content: center; min-height: 64px; }
126
- .tps-value { font-size: clamp(28px, 4cqw, 42px); overflow-wrap: anywhere; }
103
+ .tps-overview { justify-content: center; }
127
104
  .tps-samples { display: flex; flex-wrap: wrap; gap: 6px 18px; font-family: var(--font-mono, 'DM Mono', monospace); font-size: 10px; letter-spacing: .08em; color: var(--nx-text-mute); }
128
105
  .tps-samples b { margin-left: 4px; font-weight: 400; font-variant-numeric: tabular-nums; color: var(--nx-text); }
129
- .ranking-unit { margin: 0; text-align: right; font: 10px var(--font-mono, 'DM Mono', monospace); letter-spacing: .08em; color: var(--nx-text-dim); }
130
- .ranking-list { flex: 1; min-height: 0; max-height: 480px; overflow-y: auto; overscroll-behavior: contain; }
131
- .ranking-items { list-style: none; margin: 0; padding: 0; }
132
- .ranking-row { display: grid; grid-template-columns: 20px minmax(0, 1fr) max-content; align-items: center; gap: 6px 12px; padding: 12px 0; border-bottom: 1px solid var(--nx-edge); }
106
+ .ranking-unit { white-space: nowrap; font: 10px var(--font-mono, 'DM Mono', monospace); letter-spacing: .08em; color: var(--nx-text-dim); }
107
+ .ranking-list { flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: none; }
108
+ .ranking-list::-webkit-scrollbar { display: none; }
109
+ .ranking-items { display: grid; grid-template-columns: 20px minmax(0, 1fr) max-content; column-gap: 8px; list-style: none; margin: 0; padding: 0; }
110
+ .ranking-row { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; align-items: center; gap: 4px 8px; padding: 6px 0; border-bottom: 1px solid var(--nx-edge); }
111
+ .ranking-row:first-child { padding-top: 0; }
133
112
  .ranking-row:last-child { border-bottom: 0; }
134
113
  .ranking-position { grid-column: 1; grid-row: 1 / 3; color: var(--nx-text-dim); font-size: 12px; font-variant-numeric: tabular-nums; }
135
114
  .ranking-identity { min-width: 0; }
136
115
  .ranking-name { margin: 0; font-size: 14px; font-weight: 500; line-height: 1.4; color: var(--nx-text); overflow-wrap: anywhere; }
137
116
  .ranking-route { margin: 3px 0 0; font: 12px/1.4 var(--font-mono, 'DM Mono', monospace); color: var(--nx-text-dim); overflow-wrap: anywhere; }
138
- .ranking-value { grid-column: 3; grid-row: 1 / 3; font-size: 20px; font-variant-numeric: tabular-nums; color: var(--nx-text); }
117
+ .ranking-value { grid-column: 3; grid-row: 1 / 3; text-align: right; font-size: 14px; font-variant-numeric: tabular-nums; color: var(--nx-text); }
139
118
  .ranking-track { grid-column: 2; height: 6px; }
140
119
  .ranking-bar { display: block; height: 100%; background: var(--nx-accent); }
141
120
  </style>
@@ -5,23 +5,9 @@ export function formatTps(value: number | null | undefined): string {
5
5
  ? new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value) : '—';
6
6
  }
7
7
 
8
- /** Host-issued opaque identities are safe fallbacks; URL-shaped values are never labels. */
9
- export function endpointIdentity(id: string): string {
10
- return /^[a-zA-Z0-9_-]+$/.test(id) ? id : 'endpoint';
11
- }
12
-
13
- /** Match the dashboard's host labels without exposing URL credentials, paths or queries. */
14
- export function endpointLabel(target: string | undefined, id: string): string {
15
- try {
16
- const url = new URL(target ?? '');
17
- if (url.protocol === 'http:' || url.protocol === 'https:') return url.host;
18
- } catch { /* Deleted or managed endpoints may have no URL. */ }
19
- return `endpoint · ${endpointIdentity(id).slice(0, 8)}`;
20
- }
21
-
22
8
  export function rankedEndpoints(rows: readonly TpsEndpointStats[]): TpsEndpointStats[] {
23
9
  return [...rows].sort((a, b) => (b.tps ?? -1) - (a.tps ?? -1)
24
- || a.routeId.localeCompare(b.routeId) || a.endpointId.localeCompare(b.endpointId));
10
+ || a.routeId.localeCompare(b.routeId) || a.upstreamHost.localeCompare(b.upstreamHost));
25
11
  }
26
12
 
27
13
  export function rankedModels(rows: readonly TpsModelStats[]): TpsModelStats[] {
@@ -1,5 +1,5 @@
1
1
  import { writable } from 'svelte/store';
2
- import type { TpsAggregate, TpsRange, TpsStatsResponse } from '../contract';
2
+ import { isUpstreamHost, type TpsAggregate, type TpsRange, type TpsStatsResponse } from '../contract';
3
3
 
4
4
  export type TpsState = { data: TpsStatsResponse | null; busy: boolean; error: boolean };
5
5
  type Load = (plugin: string, range: TpsRange, signal: AbortSignal) => Promise<TpsStatsResponse>;
@@ -17,7 +17,7 @@ export function validateStats(data: TpsStatsResponse, range: TpsRange): TpsStats
17
17
  || typeof data.reportingIncomplete !== 'boolean' || !Array.isArray(data.endpoints) || !Array.isArray(data.models) || !Array.isArray(data.history)
18
18
  || !data.history.every((row, index) => validAggregate(row) && Number.isSafeInteger(row.bucketStartMs) && row.bucketStartMs >= 0
19
19
  && (index === 0 || row.bucketStartMs > data.history[index - 1]!.bucketStartMs))
20
- || !data.endpoints.every(row => validAggregate(row) && typeof row.routeId === 'string' && typeof row.endpointId === 'string')
20
+ || !data.endpoints.every(row => validAggregate(row) && typeof row.routeId === 'string' && !!row.routeId.trim() && isUpstreamHost(row.upstreamHost))
21
21
  || !data.models.every(row => validAggregate(row) && typeof row.routeId === 'string' && !!row.routeId.trim()
22
22
  && typeof row.model === 'string' && !!row.model.trim())) {
23
23
  throw new Error('invalid_tps_stats');