@jeffusion/bungee-core 5.3.2 → 5.4.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.
- package/dist/main.js +86 -22
- package/dist/plugins/ai-transformer/index.js.map +2 -2
- package/dist/plugins/anthropic-request-sanitizer/index.js.map +2 -2
- package/dist/plugins/anthropic-tool-name-transformer/index.js.map +2 -2
- package/dist/plugins/chatgpt-oauth/index.js.map +2 -2
- package/dist/plugins/deepseek-reasoning-fix/index.js.map +2 -2
- package/dist/plugins/model-mapping/index.js.map +2 -2
- package/dist/plugins/openai-messages-to-chat/index.js.map +2 -2
- package/dist/plugins/signature-repair/index.js.map +2 -2
- package/dist/plugins/token-stats/control.js +29 -10
- package/dist/plugins/token-stats/control.js.map +6 -5
- package/dist/plugins/token-stats/index.js +12 -4
- package/dist/plugins/token-stats/index.js.map +6 -5
- package/dist/plugins/token-stats/manifest.json +24 -4
- package/dist/plugins/token-stats/ui/TokenStatsMetric.svelte +31 -9
- package/dist/plugins/token-stats/ui/TokenStatsPage.svelte +29 -41
- package/dist/plugins/token-stats/ui/UsageTimeChart.svelte +132 -70
- package/dist/plugins/token-stats/ui/labels.ts +22 -4
- package/dist/plugins/token-stats/ui/overview-trends.ts +33 -0
- package/dist/plugins/token-stats/ui/stats-resource.ts +5 -2
- package/dist/plugins/token-stats/ui/stats.ts +3 -2
- package/package.json +2 -2
|
@@ -84,6 +84,8 @@ export type RankedModel = {
|
|
|
84
84
|
export type TimeBucket = { startMs: number; total: number; parts: { id: string; tokens: number }[]; details: { id: string; tokens: number }[] };
|
|
85
85
|
export type TimeSeries = { buckets: TimeBucket[]; models: string[]; maxTokens: number };
|
|
86
86
|
export type TokenRange = '1h' | '12h' | '24h';
|
|
87
|
+
export type TokenPageRange = 'day' | 'week' | 'month' | '1d' | '7d' | '30d';
|
|
88
|
+
export type TokenStatsRange = TokenRange | TokenPageRange;
|
|
87
89
|
export const OTHER_MODEL = '\0other';
|
|
88
90
|
const RANGE_MS: Record<TokenRange, number> = { '1h': 3_600_000, '12h': 43_200_000, '24h': 86_400_000 };
|
|
89
91
|
|
|
@@ -98,6 +100,16 @@ export function cacheDetail(value: unknown): number | undefined {
|
|
|
98
100
|
return amount !== undefined && amount > 0 ? amount : undefined;
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
/** Both cache read and cache write are subsets of input tokens. */
|
|
104
|
+
export function formatCacheInputPercentage(value: unknown, row: UsageSnapshot): string {
|
|
105
|
+
const amount = tokenAmount(value);
|
|
106
|
+
const input = usagePresentation(row).input;
|
|
107
|
+
if (amount === undefined || input === undefined || input <= 0) return '—';
|
|
108
|
+
const percentage = amount / input * 100;
|
|
109
|
+
if (percentage > 0 && percentage < 0.01) return '<0.01%';
|
|
110
|
+
return `${new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(percentage)}%`;
|
|
111
|
+
}
|
|
112
|
+
|
|
101
113
|
export function rankedModels(rows: readonly ModelUsageRow[]): RankedModel[] {
|
|
102
114
|
return rows.map(row => {
|
|
103
115
|
const usage = usagePresentation(row);
|
|
@@ -117,10 +129,16 @@ export function modelColorIndex(id: string): number {
|
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
/** Only counted tokens make a bar; empty buckets have no visible segments. */
|
|
120
|
-
export function buildTimeSeries(rows: readonly ModelUsageRow[], range:
|
|
132
|
+
export function buildTimeSeries(rows: readonly ModelUsageRow[], range: TokenStatsRange, bucketMs: number, nowMs: number, bucketStarts?: readonly number[]): TimeSeries {
|
|
121
133
|
if (!Number.isSafeInteger(bucketMs) || bucketMs <= 0) return { buckets: [], models: [], maxTokens: 0 };
|
|
122
|
-
const
|
|
123
|
-
|
|
134
|
+
const starts = bucketStarts ? [...bucketStarts] : [];
|
|
135
|
+
if (!bucketStarts && range in RANGE_MS) {
|
|
136
|
+
const first = Math.floor((nowMs - RANGE_MS[range as TokenRange]) / bucketMs) * bucketMs;
|
|
137
|
+
const last = Math.floor(nowMs / bucketMs) * bucketMs;
|
|
138
|
+
for (let start = first; start <= last; start += bucketMs) starts.push(start);
|
|
139
|
+
}
|
|
140
|
+
if (!starts.length) return { buckets: [], models: [], maxTokens: 0 };
|
|
141
|
+
const first = starts[0]!, last = starts[starts.length - 1]!;
|
|
124
142
|
const byBucket = new Map<number, Map<string, number>>();
|
|
125
143
|
const totals = new Map<string, number>();
|
|
126
144
|
for (const row of rows) {
|
|
@@ -137,7 +155,7 @@ export function buildTimeSeries(rows: readonly ModelUsageRow[], range: TokenRang
|
|
|
137
155
|
const leading = named.slice(0, 4).map(([id]) => id);
|
|
138
156
|
const models = [...leading, ...(totals.has('unknown') ? ['unknown'] : []), ...(named.length > 4 ? [OTHER_MODEL] : [])];
|
|
139
157
|
const buckets: TimeBucket[] = [];
|
|
140
|
-
for (
|
|
158
|
+
for (const startMs of starts) {
|
|
141
159
|
const amounts = byBucket.get(startMs) ?? new Map<string, number>();
|
|
142
160
|
const details = [...amounts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
143
161
|
.map(([id, tokens]) => ({ id, tokens }));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { bucketTrend } from '../../../packages/ui/src/components/dashboard/trends';
|
|
2
|
+
import { usagePresentation } from './labels';
|
|
3
|
+
import type { StatsResponse } from './stats-resource';
|
|
4
|
+
|
|
5
|
+
export const OVERVIEW_METRICS = ['input', 'output', 'cost'] as const;
|
|
6
|
+
export type OverviewMetric = typeof OVERVIEW_METRICS[number];
|
|
7
|
+
export type OverviewTrend = { metric: OverviewMetric; percent: number | null; change: 'up' | 'down' | 'flat' | null; isNew: boolean };
|
|
8
|
+
|
|
9
|
+
/** Compare adjacent completed server buckets. Missing traffic is zero; missing metering stays unknown. */
|
|
10
|
+
export function overviewTrends(snapshot: StatsResponse | null): OverviewTrend[] {
|
|
11
|
+
const { asOfMs, bucketMs } = snapshot ?? {};
|
|
12
|
+
const valid = snapshot?.groupBy === 'time' && Number.isSafeInteger(asOfMs) && Number.isSafeInteger(bucketMs) && bucketMs! > 0;
|
|
13
|
+
const currentStart = valid ? Math.floor(asOfMs! / bucketMs!) * bucketMs! - bucketMs! : NaN;
|
|
14
|
+
return OVERVIEW_METRICS.map(metric => {
|
|
15
|
+
const amount = (start: number): number | null => {
|
|
16
|
+
if (!valid) return null;
|
|
17
|
+
const rows = snapshot!.data.filter(row => row.bucketStartMs === start);
|
|
18
|
+
let total = 0;
|
|
19
|
+
for (const row of rows) {
|
|
20
|
+
const value = metric === 'cost' ? row.estimatedCostUsd : usagePresentation(row)[metric];
|
|
21
|
+
if (value == null || !Number.isFinite(value) || value < 0) return null;
|
|
22
|
+
total += value;
|
|
23
|
+
}
|
|
24
|
+
return total;
|
|
25
|
+
};
|
|
26
|
+
const previous = amount(currentStart - bucketMs!), current = amount(currentStart);
|
|
27
|
+
const known = previous !== null && current !== null;
|
|
28
|
+
const percent = known ? bucketTrend([previous, current]) : null;
|
|
29
|
+
return { metric, percent: Number.isFinite(percent) ? percent : null,
|
|
30
|
+
change: !known ? null : current > previous ? 'up' : current < previous ? 'down' : 'flat',
|
|
31
|
+
isNew: known && previous === 0 && current > 0 };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { writable } from 'svelte/store';
|
|
2
|
-
import type { ModelUsageRow,
|
|
2
|
+
import type { ModelUsageRow, TokenStatsRange, UsageSnapshot } from './labels';
|
|
3
3
|
|
|
4
4
|
export type StatsResponse = UsageSnapshot & {
|
|
5
5
|
groupBy: 'model' | 'time';
|
|
6
|
+
asOfMs?: number;
|
|
6
7
|
estimatedCostUsd: number | null;
|
|
7
8
|
bucketMs?: number;
|
|
9
|
+
bucketStarts?: number[];
|
|
10
|
+
bucketEndMs?: number;
|
|
8
11
|
data: ModelUsageRow[];
|
|
9
12
|
};
|
|
10
13
|
export type StatsState = { data: StatsResponse | null; busy: boolean; error: string; refreshedAt: number };
|
|
@@ -49,4 +52,4 @@ export function createStatsResource(
|
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
export type StatsResource = ReturnType<typeof createStatsResource>;
|
|
52
|
-
export type StatsRange =
|
|
55
|
+
export type StatsRange = TokenStatsRange;
|
|
@@ -3,11 +3,12 @@ import { createStatsResource, type StatsRange, type StatsResource, type StatsRes
|
|
|
3
3
|
|
|
4
4
|
const resources = new Map<string, StatsResource>();
|
|
5
5
|
export function getStatsResource(pluginName: string, range: StatsRange, groupBy: 'model' | 'time'): StatsResource {
|
|
6
|
-
const
|
|
6
|
+
const timeZone = ['day', 'week', 'month'].includes(range) ? Intl.DateTimeFormat().resolvedOptions().timeZone : undefined;
|
|
7
|
+
const key = `${pluginName}:${range}:${groupBy}:${timeZone ?? ''}`;
|
|
7
8
|
let resource = resources.get(key);
|
|
8
9
|
if (!resource) {
|
|
9
10
|
resource = createStatsResource(signal => requestPluginControl<StatsResponse>(pluginName,
|
|
10
|
-
`/stats?range=${encodeURIComponent(range)}&groupBy=${groupBy}`, 'GET', undefined, signal),
|
|
11
|
+
`/stats?range=${encodeURIComponent(range)}&groupBy=${groupBy}${timeZone ? `&timeZone=${encodeURIComponent(timeZone)}` : ''}`, 'GET', undefined, signal),
|
|
11
12
|
() => { if (resources.get(key) === resource) resources.delete(key); });
|
|
12
13
|
resources.set(key, resource);
|
|
13
14
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffusion/bungee-core",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.1",
|
|
4
4
|
"description": "Bungee reverse proxy core engine",
|
|
5
5
|
"main": "./dist/main.js",
|
|
6
6
|
"types": "./dist/main.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"test": "bun test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@jeffusion/bungee-types": "^5.
|
|
15
|
+
"@jeffusion/bungee-types": "^5.4.1",
|
|
16
16
|
"@types/lodash-es": "^4.17.12",
|
|
17
17
|
"acorn": "^8.15.0",
|
|
18
18
|
"canonicalize": "3.0.0",
|