@robhowley/pi-openrouter 0.1.0 → 0.3.0
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/extensions/openrouter/__tests__/format.test.ts +161 -53
- package/extensions/openrouter/cache.ts +21 -10
- package/extensions/openrouter/chart.ts +158 -0
- package/extensions/openrouter/client.ts +10 -6
- package/extensions/openrouter/format.ts +77 -33
- package/extensions/openrouter/index.ts +14 -5
- package/extensions/openrouter/overlay.ts +284 -157
- package/extensions/openrouter/types.ts +28 -5
- package/package.json +3 -2
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { ActivityItem } from '@openrouter/sdk/models/index.js';
|
|
2
|
-
import type { UsageSummary } from './types.js';
|
|
2
|
+
import type { ModelStats, ProviderStats, TokenStats, UsageSummary } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** Convert a Date to YYYY-MM-DD string in local timezone (matching API format) */
|
|
5
|
+
function localISODate(date: Date): string {
|
|
6
|
+
const year = date.getFullYear();
|
|
7
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
8
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
9
|
+
return `${year}-${month}-${day}`;
|
|
10
|
+
}
|
|
3
11
|
|
|
4
12
|
export function aggregateUsage(
|
|
5
13
|
credits: { totalUsage: number; totalCredits?: number },
|
|
@@ -12,32 +20,24 @@ export function aggregateUsage(
|
|
|
12
20
|
startOfWeek.setDate(startOfWeek.getDate() - 7);
|
|
13
21
|
|
|
14
22
|
const weekData = analytics.filter((d) => {
|
|
15
|
-
|
|
16
|
-
return
|
|
23
|
+
// API dates are YYYY-MM-DD; compare by local date boundary
|
|
24
|
+
return d.date >= localISODate(startOfWeek);
|
|
17
25
|
});
|
|
18
26
|
|
|
19
27
|
const todayData = analytics.filter((d) => {
|
|
20
|
-
|
|
21
|
-
return
|
|
28
|
+
// API dates are YYYY-MM-DD; compare by local date boundary
|
|
29
|
+
return d.date >= localISODate(startOfDay);
|
|
22
30
|
});
|
|
23
31
|
|
|
24
32
|
const week = sumSpend(weekData);
|
|
25
33
|
const today = sumSpend(todayData);
|
|
26
34
|
const month = credits.totalUsage;
|
|
27
35
|
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
.
|
|
32
|
-
.
|
|
33
|
-
.slice(0, 3);
|
|
34
|
-
|
|
35
|
-
// Top models by 30d spend
|
|
36
|
-
const modelSpend30d = aggregateByModel(analytics);
|
|
37
|
-
const topModels30d = Object.entries(modelSpend30d)
|
|
38
|
-
.map(([name, spend]) => ({ name, spend }))
|
|
39
|
-
.sort((a, b) => b.spend - a.spend)
|
|
40
|
-
.slice(0, 3);
|
|
36
|
+
// Build model stats for both 7d and 30d windows
|
|
37
|
+
const modelStatsMap = buildModelStats(weekData, analytics);
|
|
38
|
+
const topModels = Array.from(modelStatsMap.values())
|
|
39
|
+
.sort((a, b) => b.spend30d - a.spend30d)
|
|
40
|
+
.slice(0, 10);
|
|
41
41
|
|
|
42
42
|
return {
|
|
43
43
|
today,
|
|
@@ -45,12 +45,11 @@ export function aggregateUsage(
|
|
|
45
45
|
month,
|
|
46
46
|
cap: credits.totalCredits ?? 0,
|
|
47
47
|
burnRate: (week / 7) * 30,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
byModel: aggregateByModel(analytics),
|
|
51
|
-
byKey: aggregateByProvider(analytics),
|
|
48
|
+
topModels,
|
|
49
|
+
byProvider: buildProviderStats(analytics),
|
|
52
50
|
byDay: aggregateByDay(analytics),
|
|
53
51
|
timestamp,
|
|
52
|
+
hasActivityData: true, // aggregateUsage is only called when analytics data is available
|
|
54
53
|
};
|
|
55
54
|
}
|
|
56
55
|
|
|
@@ -58,24 +57,69 @@ function sumSpend(data: ActivityItem[]): number {
|
|
|
58
57
|
return data.reduce((sum, d) => sum + d.usage, 0);
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
function
|
|
60
|
+
function aggregateTokens(data: ActivityItem[]): TokenStats {
|
|
62
61
|
return data.reduce(
|
|
63
62
|
(acc, d) => {
|
|
64
|
-
acc
|
|
63
|
+
acc.input += d.promptTokens || 0;
|
|
64
|
+
acc.output += d.completionTokens || 0;
|
|
65
|
+
acc.reasoning += d.reasoningTokens || 0;
|
|
66
|
+
acc.total += (d.promptTokens || 0) + (d.completionTokens || 0) + (d.reasoningTokens || 0);
|
|
65
67
|
return acc;
|
|
66
68
|
},
|
|
67
|
-
{} as
|
|
69
|
+
{ input: 0, output: 0, reasoning: 0, total: 0 } as TokenStats,
|
|
68
70
|
);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
function
|
|
72
|
-
return data.reduce(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
function aggregateRequests(data: ActivityItem[]): number {
|
|
74
|
+
return data.reduce((sum, d) => sum + (d.requests || 0), 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildModelStats(data7d: ActivityItem[], data30d: ActivityItem[]): Map<string, ModelStats> {
|
|
78
|
+
const all = new Map<string, ModelStats>();
|
|
79
|
+
const modelNames = new Set<string>();
|
|
80
|
+
|
|
81
|
+
// Collect all unique model names from both time windows
|
|
82
|
+
for (const d of data30d) modelNames.add(d.model);
|
|
83
|
+
for (const d of data7d) modelNames.add(d.model);
|
|
84
|
+
|
|
85
|
+
for (const name of modelNames) {
|
|
86
|
+
const data7dForModel = data7d.filter((d) => d.model === name);
|
|
87
|
+
const data30dForModel = data30d.filter((d) => d.model === name);
|
|
88
|
+
|
|
89
|
+
all.set(name, {
|
|
90
|
+
name,
|
|
91
|
+
spend7d: data7dForModel.reduce((s, d) => s + d.usage, 0),
|
|
92
|
+
spend30d: data30dForModel.reduce((s, d) => s + d.usage, 0),
|
|
93
|
+
tokens7d: aggregateTokens(data7dForModel),
|
|
94
|
+
tokens30d: aggregateTokens(data30dForModel),
|
|
95
|
+
requests7d: aggregateRequests(data7dForModel),
|
|
96
|
+
requests30d: aggregateRequests(data30dForModel),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return all;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildProviderStats(data: ActivityItem[]): ProviderStats[] {
|
|
104
|
+
const byName = new Map<string, ActivityItem[]>();
|
|
105
|
+
|
|
106
|
+
for (const d of data) {
|
|
107
|
+
const existing = byName.get(d.providerName) || [];
|
|
108
|
+
existing.push(d);
|
|
109
|
+
byName.set(d.providerName, existing);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const stats: ProviderStats[] = [];
|
|
113
|
+
for (const [name, items] of byName) {
|
|
114
|
+
stats.push({
|
|
115
|
+
name,
|
|
116
|
+
spend: items.reduce((s, d) => s + d.usage, 0),
|
|
117
|
+
tokens: aggregateTokens(items),
|
|
118
|
+
requests: aggregateRequests(items),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return stats.sort((a, b) => b.spend - a.spend);
|
|
79
123
|
}
|
|
80
124
|
|
|
81
125
|
function aggregateByDay(data: ActivityItem[]): Record<string, number> {
|
|
@@ -10,8 +10,6 @@ import { AuthError } from './client.js';
|
|
|
10
10
|
import { UsageOverlayComponent } from './overlay.js';
|
|
11
11
|
|
|
12
12
|
export default function (pi: ExtensionAPI) {
|
|
13
|
-
startBackgroundRefresh();
|
|
14
|
-
|
|
15
13
|
pi.on('session_start', async (_event, ctx) => {
|
|
16
14
|
ctx.ui.notify('OpenRouter extension loaded', 'info');
|
|
17
15
|
});
|
|
@@ -24,6 +22,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
24
22
|
description: 'Show OpenRouter usage: caps, spend, burn rate, and model breakdowns',
|
|
25
23
|
getArgumentCompletions: () => null,
|
|
26
24
|
handler: async (args, ctx) => {
|
|
25
|
+
startBackgroundRefresh(); // Start cache refresh on first use
|
|
27
26
|
const subcommand = args.trim() || undefined;
|
|
28
27
|
await showUsageOverlay(ctx, subcommand);
|
|
29
28
|
},
|
|
@@ -47,9 +46,14 @@ async function showUsageOverlay(ctx: ExtensionContext, _subcommand?: string) {
|
|
|
47
46
|
|
|
48
47
|
try {
|
|
49
48
|
summary = await fetchAndAggregate();
|
|
50
|
-
|
|
49
|
+
if (!summary) {
|
|
50
|
+
error =
|
|
51
|
+
'OpenRouter API key not found. Set OPENROUTER_MANAGEMENT_KEY (preferred) or OPENROUTER_API_KEY to use /usage.';
|
|
52
|
+
} else {
|
|
53
|
+
usageCache.set('usage', summary);
|
|
54
|
+
}
|
|
51
55
|
|
|
52
|
-
await showOverlay(ctx, summary,
|
|
56
|
+
await showOverlay(ctx, summary, error, 0);
|
|
53
57
|
} catch (error_) {
|
|
54
58
|
const err = error_ as Error;
|
|
55
59
|
error =
|
|
@@ -89,6 +93,11 @@ async function showOverlay(
|
|
|
89
93
|
},
|
|
90
94
|
};
|
|
91
95
|
},
|
|
92
|
-
{
|
|
96
|
+
{
|
|
97
|
+
overlay: true,
|
|
98
|
+
overlayOptions: {
|
|
99
|
+
width: 100,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
93
102
|
);
|
|
94
103
|
}
|