@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.
- package/README.md +26 -5
- package/extensions/openrouter/__tests__/account-client.test.ts +488 -0
- package/extensions/openrouter/__tests__/account-overlay.test.ts +683 -0
- package/extensions/openrouter/__tests__/api-key-commands.test.ts +231 -0
- package/extensions/openrouter/__tests__/commands.test.ts +225 -3
- package/extensions/openrouter/__tests__/hooks.test.ts +262 -9
- package/extensions/openrouter/__tests__/normalizers.test.ts +86 -3
- package/extensions/openrouter/__tests__/status-bar.test.ts +262 -0
- package/extensions/openrouter/account-client.ts +275 -28
- package/extensions/openrouter/account-overlay.ts +417 -60
- package/extensions/openrouter/account-types.ts +10 -3
- package/extensions/openrouter/api-key-commands.ts +287 -0
- package/extensions/openrouter/commands.ts +123 -11
- package/extensions/openrouter/hooks.ts +70 -11
- package/extensions/openrouter/local-usage.ts +15 -10
- package/extensions/openrouter/normalizers.ts +22 -5
- package/extensions/openrouter/status-bar.ts +101 -0
- package/package.json +2 -2
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { Model as SDKModel } from '@openrouter/sdk/models/index.js';
|
|
2
|
-
import type {
|
|
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
|
|
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(
|
|
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.
|
|
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",
|