@robhowley/pi-openrouter 0.8.2 → 0.8.3

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.
@@ -132,3 +132,30 @@ export function createPiModelConfig(overrides?: Partial<PiModelConfig>): PiModel
132
132
  ...overrides,
133
133
  };
134
134
  }
135
+
136
+ // =============================================================================
137
+ // LocalUsageEvent Fixtures
138
+ // =============================================================================
139
+
140
+ import type { LocalUsageEvent } from '../types.js';
141
+
142
+ /**
143
+ * Creates LocalUsageEvent objects for testing local usage aggregation.
144
+ * Use daysAgo=0 for today, daysAgo=3 for within 7d window, etc.
145
+ */
146
+ export function createLocalEvents(
147
+ events: Array<{ daysAgo: number; cost: number; model?: string; provider?: string }>,
148
+ ): LocalUsageEvent[] {
149
+ return events.map((e, i) => ({
150
+ id: `local-${i}`,
151
+ sessionId: 'test-session',
152
+ generationId: `gen-${i}`,
153
+ completedAt: `${createTestDate(e.daysAgo)}T12:00:00.000Z`,
154
+ requests: 1,
155
+ model: e.model ?? 'gpt-4',
156
+ provider: e.provider ?? 'openai',
157
+ promptTokens: 100,
158
+ completionTokens: 50,
159
+ cost: e.cost,
160
+ }));
161
+ }
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { aggregateUsage } from '../format.js';
3
3
  import { renderSpendSparkline } from '../chart.js';
4
- import { createTestDate, createActivityItem } from './fixtures.js';
4
+ import { createTestDate, createActivityItem, createLocalEvents } from './fixtures.js';
5
5
  import type { ActivityItem } from '@openrouter/sdk/models/index.js';
6
6
 
7
7
  describe('aggregateUsage', () => {
@@ -224,6 +224,54 @@ describe('aggregateUsage', () => {
224
224
  expect(result.byProvider[0]?.tokens.output).toBe(80); // 50 + 30
225
225
  expect(result.byProvider[0]?.requests).toBe(8); // 5 + 3
226
226
  });
227
+
228
+ it.each([
229
+ {
230
+ name: 'local today included in 7d model stats',
231
+ api: { daysAgo: 3, cost: 5, model: 'gpt-4' },
232
+ local: { daysAgo: 0, cost: 2.5, model: 'gpt-4' },
233
+ expectSpend7d: 7.5,
234
+ },
235
+ {
236
+ name: 'local outside 7d window excluded from 7d stats',
237
+ api: { daysAgo: 3, cost: 5, model: 'gpt-4' },
238
+ local: { daysAgo: 10, cost: 100, model: 'gpt-4' },
239
+ expectSpend7d: 5,
240
+ },
241
+ {
242
+ name: 'different models tracked separately',
243
+ api: { daysAgo: 1, cost: 5, model: 'gpt-4' },
244
+ local: { daysAgo: 0, cost: 3, model: 'claude-3' },
245
+ expectSpend7d: null,
246
+ expectModels: [
247
+ { name: 'gpt-4', spend7d: 5 },
248
+ { name: 'claude-3', spend7d: 3 },
249
+ ],
250
+ },
251
+ ])('$name', ({ api, local, expectSpend7d, expectModels }) => {
252
+ const credits = { totalUsage: 1000, totalCredits: 2000 };
253
+
254
+ const result = aggregateUsage(
255
+ credits,
256
+ [
257
+ createActivityItem({
258
+ date: createTestDate(api.daysAgo),
259
+ usage: api.cost,
260
+ model: api.model,
261
+ }),
262
+ ],
263
+ Date.now(),
264
+ createLocalEvents([local]),
265
+ );
266
+
267
+ if (expectModels) {
268
+ for (const { name, spend7d } of expectModels) {
269
+ expect(result.topModels.find((m) => m.name === name)?.spend7d).toBe(spend7d);
270
+ }
271
+ } else {
272
+ expect(result.topModels[0]?.spend7d).toBe(expectSpend7d);
273
+ }
274
+ });
227
275
  });
228
276
 
229
277
  describe('renderSpendSparkline', () => {
@@ -25,13 +25,10 @@ export function aggregateUsage(
25
25
  return d.date >= utcISODate(startOfWeek);
26
26
  });
27
27
 
28
- const todayData = analytics.filter((d) => {
29
- // API dates are YYYY-MM-DD in UTC; compare by UTC date boundary
30
- return d.date >= utcISODate(startOfDay);
31
- });
32
-
33
28
  const weekFromAnalytics = sumSpend(weekData);
34
- const todayFromAnalytics = sumSpend(todayData);
29
+ const todayFromAnalytics = analytics
30
+ .filter((d) => d.date >= utcISODate(startOfDay))
31
+ .reduce((sum, d) => sum + d.usage, 0);
35
32
  const month = credits.totalUsage;
36
33
 
37
34
  // Add local events to compute combined totals
@@ -65,8 +62,10 @@ export function aggregateUsage(
65
62
  const allData = [...analytics, ...localItems];
66
63
 
67
64
  // Build model stats for both 7d and 30d windows
68
- // Use allData (combined API + local) for 30d, weekData (combined) for 7d
69
- const modelStatsMap = buildModelStats(weekData, allData);
65
+ // Combine weekData with local events in the 7d window
66
+ const weekLocalItems = localItems.filter((e) => e.date >= utcISODate(startOfWeek));
67
+ const weekAllData = [...weekData, ...weekLocalItems];
68
+ const modelStatsMap = buildModelStats(weekAllData, allData);
70
69
  const topModels = Array.from(modelStatsMap.values())
71
70
  .sort((a, b) => b.spend30d - a.spend30d)
72
71
  .slice(0, 10);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robhowley/pi-openrouter",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "type": "module",
5
5
  "description": "Live OpenRouter spend/account TUI overlays, user-scoped model sync, and session tagging for Pi.",
6
6
  "license": "MIT",