@robhowley/pi-openrouter 0.9.0 → 0.9.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/README.md +39 -4
- package/extensions/openrouter/__tests__/cache.test.ts +769 -0
- package/extensions/openrouter/__tests__/client.test.ts +333 -15
- package/extensions/openrouter/__tests__/commands.test.ts +816 -0
- package/extensions/openrouter/__tests__/fixtures.ts +140 -1
- package/extensions/openrouter/__tests__/format.test.ts +19 -0
- package/extensions/openrouter/__tests__/hooks.test.ts +276 -0
- package/extensions/openrouter/__tests__/index.test.ts +112 -363
- package/extensions/openrouter/__tests__/local-usage.test.ts +777 -0
- package/extensions/openrouter/__tests__/normalizers.test.ts +288 -0
- package/extensions/openrouter/__tests__/overlay.test.ts +225 -0
- package/extensions/openrouter/__tests__/session-state.test.ts +233 -0
- package/extensions/openrouter/__tests__/session.test.ts +44 -43
- package/extensions/openrouter/account-client.ts +11 -61
- package/extensions/openrouter/cache.ts +203 -91
- package/extensions/openrouter/client.ts +49 -3
- package/extensions/openrouter/commands.ts +555 -0
- package/extensions/openrouter/format.ts +7 -4
- package/extensions/openrouter/hooks.ts +229 -0
- package/extensions/openrouter/index.ts +13 -990
- package/extensions/openrouter/local-usage.ts +145 -22
- package/extensions/openrouter/models/__tests__/cache.test.ts +63 -2
- package/extensions/openrouter/models/__tests__/mapper.test.ts +29 -0
- package/extensions/openrouter/models/__tests__/override-commands.test.ts +668 -0
- package/extensions/openrouter/models/__tests__/sync.test.ts +156 -4
- package/extensions/openrouter/models/cache.ts +27 -2
- package/extensions/openrouter/models/mapper.ts +35 -69
- package/extensions/openrouter/models/override-commands.ts +434 -0
- package/extensions/openrouter/models/skip-hints.ts +19 -0
- package/extensions/openrouter/models/sync.ts +22 -10
- package/extensions/openrouter/models/types.ts +2 -1
- package/extensions/openrouter/normalizers.ts +128 -0
- package/extensions/openrouter/overlay.ts +19 -8
- package/extensions/openrouter/session-state.ts +110 -0
- package/extensions/openrouter/session.ts +16 -0
- package/extensions/openrouter/types.ts +28 -9
- package/package.json +1 -1
|
@@ -134,11 +134,13 @@ export class UsageOverlayComponent {
|
|
|
134
134
|
const th = this.theme;
|
|
135
135
|
const lines: string[] = [];
|
|
136
136
|
|
|
137
|
-
if (error) {
|
|
137
|
+
if (error && !summary) {
|
|
138
138
|
lines.push(boxTop(this.width));
|
|
139
139
|
lines.push(this.getUsageHeaderRow());
|
|
140
140
|
lines.push(emptyRow(this.width));
|
|
141
|
-
|
|
141
|
+
for (const errorLine of error.split('\n')) {
|
|
142
|
+
lines.push(row(th.fg('error', errorLine), this.width));
|
|
143
|
+
}
|
|
142
144
|
if (cachedMinutesAgo !== null) {
|
|
143
145
|
lines.push(
|
|
144
146
|
row(th.fg('dim', `(last successful fetch: ${cachedMinutesAgo}m ago)`), this.width),
|
|
@@ -164,6 +166,19 @@ export class UsageOverlayComponent {
|
|
|
164
166
|
lines.push(this.getUsageHeaderRow());
|
|
165
167
|
lines.push(emptyRow(this.width));
|
|
166
168
|
|
|
169
|
+
if (error) {
|
|
170
|
+
lines.push(row(th.fg('warning', ' Stale usage data - refresh failed'), this.width));
|
|
171
|
+
for (const errorLine of error.split('\n')) {
|
|
172
|
+
lines.push(row(th.fg('dim', ` ${errorLine}`), this.width));
|
|
173
|
+
}
|
|
174
|
+
if (cachedMinutesAgo !== null) {
|
|
175
|
+
lines.push(
|
|
176
|
+
row(th.fg('dim', ` Last successful fetch: ${cachedMinutesAgo}m ago`), this.width),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
lines.push(emptyRow(this.width));
|
|
180
|
+
}
|
|
181
|
+
|
|
167
182
|
// Month row: amount stays with label, cap percentage right-aligned
|
|
168
183
|
const monthLeftBase = ` Month $${fmt(summary.month)} / $${fmt(summary.cap)}`;
|
|
169
184
|
const monthPercent = summary.cap > 0 ? Math.round((summary.month / summary.cap) * 100) : 0;
|
|
@@ -190,12 +205,8 @@ export class UsageOverlayComponent {
|
|
|
190
205
|
}
|
|
191
206
|
lines.push(rowRightAligned(weekLeftBase, weekRight + ' ', this.width));
|
|
192
207
|
|
|
193
|
-
// Today row on its own line - shows tilde since
|
|
194
|
-
|
|
195
|
-
summary.local.requests > 0 ? ` · ${fmtCount(summary.local.requests)} reqs` : '';
|
|
196
|
-
lines.push(
|
|
197
|
-
rowRightAligned(` Today ~$${fmt(summary.local.cost)}${todayReqStr}`, ' ', this.width),
|
|
198
|
-
);
|
|
208
|
+
// Today row on its own line - shows tilde since it may include local tracked usage
|
|
209
|
+
lines.push(rowRightAligned(` Today ~$${fmt(summary.today)}`, ' ', this.width));
|
|
199
210
|
lines.push(emptyRow(this.width));
|
|
200
211
|
|
|
201
212
|
// Top models (7d table)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { formatSessionId } from './session.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session state manager for OpenRouter session IDs.
|
|
6
|
+
* Ensures stable session IDs within a Pi session and proper reset across sessions.
|
|
7
|
+
*/
|
|
8
|
+
export interface SessionState {
|
|
9
|
+
/**
|
|
10
|
+
* Get the current session ID, creating one if needed.
|
|
11
|
+
* The ID is cached and stable for the lifetime of this SessionState instance.
|
|
12
|
+
*/
|
|
13
|
+
getCurrentSessionId(ctx: { sessionManager: { getSessionId(): string } }): string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Handle session_start lifecycle event.
|
|
17
|
+
* Checks if the raw session ID has changed and updates state accordingly.
|
|
18
|
+
* - If raw ID differs from cached: reset and format new ID
|
|
19
|
+
* - If raw ID is same: preserve cached formatted ID
|
|
20
|
+
* - If raw ID is empty/throws: clear state for fresh fallback
|
|
21
|
+
*/
|
|
22
|
+
startSession(ctx: { sessionManager: { getSessionId(): string } }): void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reset the cached session ID. Call this on session_shutdown
|
|
26
|
+
* to ensure fresh IDs for new sessions.
|
|
27
|
+
*/
|
|
28
|
+
reset(): void;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Peek at the current cached session ID without initializing one.
|
|
32
|
+
* Returns null if no session ID has been cached yet.
|
|
33
|
+
*/
|
|
34
|
+
peek(): string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class SessionStateImpl implements SessionState {
|
|
38
|
+
private cachedSessionId: string | null = null;
|
|
39
|
+
private cachedRawSessionId: string | null = null;
|
|
40
|
+
|
|
41
|
+
getCurrentSessionId(ctx: { sessionManager: { getSessionId(): string } }): string {
|
|
42
|
+
if (this.cachedSessionId) {
|
|
43
|
+
return this.cachedSessionId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
48
|
+
let formattedSessionId: string;
|
|
49
|
+
|
|
50
|
+
if (sessionId && sessionId !== '') {
|
|
51
|
+
formattedSessionId = formatSessionId(sessionId);
|
|
52
|
+
this.cachedRawSessionId = sessionId;
|
|
53
|
+
} else {
|
|
54
|
+
// Empty session ID: generate fallback UUID
|
|
55
|
+
formattedSessionId = formatSessionId(crypto.randomUUID());
|
|
56
|
+
this.cachedRawSessionId = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.cachedSessionId = formattedSessionId;
|
|
60
|
+
return formattedSessionId;
|
|
61
|
+
} catch {
|
|
62
|
+
// Session manager threw error: generate fallback UUID
|
|
63
|
+
const fallbackId = formatSessionId(crypto.randomUUID());
|
|
64
|
+
this.cachedSessionId = fallbackId;
|
|
65
|
+
this.cachedRawSessionId = null;
|
|
66
|
+
return fallbackId;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
startSession(ctx: { sessionManager: { getSessionId(): string } }): void {
|
|
71
|
+
try {
|
|
72
|
+
const rawSessionId = ctx.sessionManager.getSessionId();
|
|
73
|
+
|
|
74
|
+
if (rawSessionId && rawSessionId !== '') {
|
|
75
|
+
// Non-empty session ID from manager
|
|
76
|
+
if (this.cachedRawSessionId !== rawSessionId) {
|
|
77
|
+
// Raw session ID changed - reset and format new ID
|
|
78
|
+
this.cachedRawSessionId = rawSessionId;
|
|
79
|
+
this.cachedSessionId = formatSessionId(rawSessionId);
|
|
80
|
+
}
|
|
81
|
+
// else: same raw ID, preserve cached formatted ID
|
|
82
|
+
} else {
|
|
83
|
+
// Empty session ID - clear state for fresh fallback
|
|
84
|
+
this.cachedSessionId = null;
|
|
85
|
+
this.cachedRawSessionId = null;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Session manager threw - clear state for fresh fallback
|
|
89
|
+
this.cachedSessionId = null;
|
|
90
|
+
this.cachedRawSessionId = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
reset(): void {
|
|
95
|
+
this.cachedSessionId = null;
|
|
96
|
+
this.cachedRawSessionId = null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
peek(): string | null {
|
|
100
|
+
return this.cachedSessionId;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create a new SessionState instance.
|
|
106
|
+
* Each Pi session should get its own instance.
|
|
107
|
+
*/
|
|
108
|
+
export function createSessionState(): SessionState {
|
|
109
|
+
return new SessionStateImpl();
|
|
110
|
+
}
|
|
@@ -23,6 +23,22 @@ export function formatSessionId(sessionId: string): string {
|
|
|
23
23
|
// Detection Logic
|
|
24
24
|
// =============================================================================
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Detect whether a provider request should be treated as an OpenRouter request.
|
|
28
|
+
*
|
|
29
|
+
* We intentionally use several overlapping signals because Pi events do not always
|
|
30
|
+
* expose provider metadata in the same place or at the same lifecycle stage.
|
|
31
|
+
* Different integrations can identify OpenRouter by:
|
|
32
|
+
* - provider name (`event.provider` or `event.payload.provider`) when Pi resolves it directly
|
|
33
|
+
* - model prefix (`openrouter/...`) when the request payload keeps the routed model id
|
|
34
|
+
* - `context.model.baseUrl` when the active model config points at OpenRouter
|
|
35
|
+
* - `provider.zdr === true` for Shopify's ZDR path that still routes through OpenRouter
|
|
36
|
+
* - request URL / endpoint as a last fallback for events that only expose transport details
|
|
37
|
+
*
|
|
38
|
+
* The checks stay intentionally redundant so session tagging and usage tracking keep
|
|
39
|
+
* working across request-time and `turn_end`-style event shapes without depending on
|
|
40
|
+
* a single field being present.
|
|
41
|
+
*/
|
|
26
42
|
export function isOpenRouterRequest(event: BeforeProviderRequestEvent, _ctx: unknown): boolean {
|
|
27
43
|
const ev = event as unknown as Record<string, unknown>;
|
|
28
44
|
const payload = ev['payload'] as Record<string, unknown> | undefined;
|
|
@@ -89,15 +89,34 @@ export interface UsageAggregate {
|
|
|
89
89
|
cost: number;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
export function createZeroAggregate(): UsageAggregate {
|
|
93
|
+
return {
|
|
94
|
+
requests: 0,
|
|
95
|
+
promptTokens: 0,
|
|
96
|
+
completionTokens: 0,
|
|
97
|
+
reasoningTokens: 0,
|
|
98
|
+
cacheReadTokens: 0,
|
|
99
|
+
cacheWriteTokens: 0,
|
|
100
|
+
cost: 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function combineUsageAggregates(
|
|
105
|
+
official: UsageAggregate,
|
|
106
|
+
local: UsageAggregate,
|
|
107
|
+
): UsageAggregate {
|
|
108
|
+
return {
|
|
109
|
+
requests: official.requests + local.requests,
|
|
110
|
+
promptTokens: official.promptTokens + local.promptTokens,
|
|
111
|
+
completionTokens: official.completionTokens + local.completionTokens,
|
|
112
|
+
reasoningTokens: official.reasoningTokens + local.reasoningTokens,
|
|
113
|
+
cacheReadTokens: official.cacheReadTokens + local.cacheReadTokens,
|
|
114
|
+
cacheWriteTokens: official.cacheWriteTokens + local.cacheWriteTokens,
|
|
115
|
+
cost: official.cost + local.cost,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const ZERO_AGGREGATE: Readonly<UsageAggregate> = Object.freeze(createZeroAggregate());
|
|
101
120
|
|
|
102
121
|
export interface CacheEntry<T> {
|
|
103
122
|
data: T;
|
package/package.json
CHANGED