@robhowley/pi-openrouter 0.9.1 → 0.11.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.
@@ -1,5 +1,11 @@
1
1
  import type { Model as SDKModel } from '@openrouter/sdk/models/index.js';
2
- import type { GetCurrentKeyData, ListData } from '@openrouter/sdk/models/operations/index.js';
2
+ import type {
3
+ CreateKeysData,
4
+ GetCurrentKeyData,
5
+ GetKeyData,
6
+ ListData,
7
+ UpdateKeysData,
8
+ } from '@openrouter/sdk/models/operations/index.js';
3
9
  import type { BYOKStatus, ResetCadence } from './account-types.js';
4
10
  import type { OpenRouterModel } from './models/types.js';
5
11
 
@@ -9,7 +15,7 @@ export interface NormalizedKeyMetadata {
9
15
  used: number;
10
16
  resetCadence: ResetCadence;
11
17
  byok: BYOKStatus;
12
- hash: string;
18
+ hash?: string;
13
19
  disabled: boolean;
14
20
  limit?: number;
15
21
  remaining?: number;
@@ -83,7 +89,9 @@ export function normalizeOpenRouterModel(model: OpenRouterModel | SDKModel): Ope
83
89
  * Normalize SDK key metadata into the package's canonical internal shape.
84
90
  * Converts SDK null/variant fields once so account code can stay domain-focused.
85
91
  */
86
- export function normalizeSdkKeyMetadata(raw: GetCurrentKeyData | ListData): NormalizedKeyMetadata {
92
+ export function normalizeSdkKeyMetadata(
93
+ raw: GetCurrentKeyData | ListData | GetKeyData | CreateKeysData | UpdateKeysData,
94
+ ): NormalizedKeyMetadata {
87
95
  const used = raw.usage ?? raw.usageMonthly ?? 0;
88
96
  const limit = raw.limit ?? undefined;
89
97
  const remaining = raw.limitRemaining ?? undefined;
@@ -96,10 +104,14 @@ export function normalizeSdkKeyMetadata(raw: GetCurrentKeyData | ListData): Norm
96
104
  }
97
105
 
98
106
  let resetCadence: ResetCadence = 'partial';
99
- if (raw.limitReset) {
107
+ if (raw.limitReset === null) {
108
+ resetCadence = 'never';
109
+ } else if (raw.limitReset) {
100
110
  const reset = raw.limitReset.toLowerCase();
101
111
  if (reset === 'monthly') {
102
112
  resetCadence = 'monthly';
113
+ } else if (reset === 'weekly') {
114
+ resetCadence = 'weekly';
103
115
  } else if (reset === 'daily') {
104
116
  resetCadence = 'daily';
105
117
  } else if (reset === 'never') {
@@ -107,16 +119,21 @@ export function normalizeSdkKeyMetadata(raw: GetCurrentKeyData | ListData): Norm
107
119
  }
108
120
  }
109
121
 
122
+ const hash =
123
+ 'hash' in raw && typeof raw.hash === 'string' && raw.hash.trim() !== '' ? raw.hash : undefined;
124
+
110
125
  const normalized: NormalizedKeyMetadata = {
111
126
  name: 'name' in raw ? (raw as ListData).name : raw.label,
112
127
  label: raw.label,
113
128
  used,
114
129
  resetCadence,
115
130
  byok,
116
- hash: 'hash' in raw ? (raw as ListData).hash : 'unknown',
117
131
  disabled: 'disabled' in raw ? (raw as ListData).disabled : false,
118
132
  };
119
133
 
134
+ if (hash !== undefined) {
135
+ normalized.hash = hash;
136
+ }
120
137
  if (limit !== undefined) {
121
138
  normalized.limit = limit;
122
139
  }
@@ -0,0 +1,101 @@
1
+ import {
2
+ addUtcDays,
3
+ dedupeLocalUsageEvents,
4
+ getCurrentUtcDate,
5
+ getUtcDateFromTimestamp,
6
+ readLocalUsage,
7
+ } from './local-usage.js';
8
+ import type { LocalUsageEvent } from './types.js';
9
+
10
+ const STATUS_WINDOW_DAYS = 30;
11
+ const STATUS_WINDOW_LABEL = '30d avg';
12
+ const STATUS_PREFIX = 'OR';
13
+ const STATUS_SEPARATOR = ' · ';
14
+
15
+ export interface OpenRouterStatusStats {
16
+ todayLocalSpend: number;
17
+ averageLocalDailySpendLast30Days: number;
18
+ burnRateMultiplier: number | null;
19
+ }
20
+
21
+ export type OpenRouterStatusBarLoadResult =
22
+ | { kind: 'ready'; text: string }
23
+ | { kind: 'empty' }
24
+ | { kind: 'failed' };
25
+
26
+ function getUtcDateForNow(now?: Date): string {
27
+ return now ? now.toISOString().slice(0, 10) : getCurrentUtcDate();
28
+ }
29
+
30
+ export function calculateOpenRouterStatusStats(
31
+ events: LocalUsageEvent[],
32
+ nowUtcDate: string = getCurrentUtcDate(),
33
+ ): OpenRouterStatusStats | null {
34
+ const windowStartUtc = addUtcDays(nowUtcDate, -(STATUS_WINDOW_DAYS - 1));
35
+ const uniqueEvents = dedupeLocalUsageEvents(events);
36
+
37
+ let todayLocalSpend = 0;
38
+ let totalLocalSpendInWindow = 0;
39
+
40
+ for (const event of uniqueEvents) {
41
+ const completedDateUtc = getUtcDateFromTimestamp(event.completedAt);
42
+ if (completedDateUtc < windowStartUtc || completedDateUtc > nowUtcDate) {
43
+ continue;
44
+ }
45
+
46
+ const cost = event.cost ?? 0;
47
+ totalLocalSpendInWindow += cost;
48
+
49
+ if (completedDateUtc === nowUtcDate) {
50
+ todayLocalSpend += cost;
51
+ }
52
+ }
53
+
54
+ if (totalLocalSpendInWindow <= 0) {
55
+ return null;
56
+ }
57
+
58
+ const averageLocalDailySpendLast30Days = totalLocalSpendInWindow / STATUS_WINDOW_DAYS;
59
+
60
+ return {
61
+ todayLocalSpend,
62
+ averageLocalDailySpendLast30Days,
63
+ burnRateMultiplier:
64
+ averageLocalDailySpendLast30Days === 0
65
+ ? null
66
+ : todayLocalSpend / averageLocalDailySpendLast30Days,
67
+ };
68
+ }
69
+
70
+ export function formatOpenRouterStatusBar(stats: OpenRouterStatusStats): string {
71
+ const today = `${STATUS_PREFIX} $${stats.todayLocalSpend.toFixed(2)} today`;
72
+ if (stats.burnRateMultiplier === null) {
73
+ return today;
74
+ }
75
+
76
+ return `${today}${STATUS_SEPARATOR}${stats.burnRateMultiplier.toFixed(1)}x ${STATUS_WINDOW_LABEL}`;
77
+ }
78
+
79
+ export async function loadOpenRouterStatusStats(now?: Date): Promise<OpenRouterStatusStats | null> {
80
+ const todayUtc = getUtcDateForNow(now);
81
+ const fromDateUtc = addUtcDays(todayUtc, -(STATUS_WINDOW_DAYS - 1));
82
+ const events = await readLocalUsage({ fromDateUtc, toDateUtc: todayUtc });
83
+
84
+ return calculateOpenRouterStatusStats(events, todayUtc);
85
+ }
86
+
87
+ export async function loadOpenRouterStatusBar(now?: Date): Promise<OpenRouterStatusBarLoadResult> {
88
+ try {
89
+ const stats = await loadOpenRouterStatusStats(now);
90
+ if (!stats) {
91
+ return { kind: 'empty' };
92
+ }
93
+
94
+ return {
95
+ kind: 'ready',
96
+ text: formatOpenRouterStatusBar(stats),
97
+ };
98
+ } catch {
99
+ return { kind: 'failed' };
100
+ }
101
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@robhowley/pi-openrouter",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
- "description": "Live OpenRouter spend/account TUI overlays, user-scoped model sync, and session tagging for Pi.",
5
+ "description": "Live OpenRouter spend/account TUI overlays, user-scoped model sync, api key management, and session tagging for Pi.",
6
6
  "license": "MIT",
7
7
  "files": [
8
8
  "extensions",