@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
|
@@ -1,36 +1,38 @@
|
|
|
1
|
-
import type { CacheEntry, UsageSummary, LocalUsageEvent
|
|
1
|
+
import type { CacheEntry, UsageSummary, LocalUsageEvent } from './types.js';
|
|
2
2
|
import type { ActivityItem } from '@openrouter/sdk/models/index.js';
|
|
3
3
|
import { aggregateUsage } from './format.js';
|
|
4
4
|
import { getCredits, getActivity } from './client.js';
|
|
5
5
|
import { readLocalUsage, aggregateLocal, getCurrentUtcDate, addUtcDays } from './local-usage.js';
|
|
6
|
-
import {
|
|
6
|
+
import { combineUsageAggregates, createZeroAggregate } from './types.js';
|
|
7
7
|
|
|
8
8
|
export const CACHE_TTL_MS = 45000;
|
|
9
9
|
export const BACKGROUND_REFRESH_INTERVAL_MS = 30000;
|
|
10
10
|
|
|
11
|
+
interface CacheGetOptions {
|
|
12
|
+
allowStale?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
export class TTLCache<T> {
|
|
12
16
|
private cache = new Map<string, CacheEntry<T>>();
|
|
13
17
|
|
|
14
18
|
constructor(private ttlMs: number = CACHE_TTL_MS) {}
|
|
15
19
|
|
|
16
|
-
get(key: string): T | undefined {
|
|
20
|
+
get(key: string, options: CacheGetOptions = {}): T | undefined {
|
|
17
21
|
const entry = this.cache.get(key);
|
|
18
22
|
if (!entry) return undefined;
|
|
19
23
|
|
|
20
|
-
if (Date.now() - entry.timestamp > this.ttlMs) {
|
|
21
|
-
this.cache.delete(key);
|
|
24
|
+
if (!options.allowStale && Date.now() - entry.timestamp > this.ttlMs) {
|
|
22
25
|
return undefined;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
return entry.data;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
getTimestamp(key: string): number | undefined {
|
|
31
|
+
getTimestamp(key: string, options: CacheGetOptions = {}): number | undefined {
|
|
29
32
|
const entry = this.cache.get(key);
|
|
30
33
|
if (!entry) return undefined;
|
|
31
34
|
|
|
32
|
-
if (Date.now() - entry.timestamp > this.ttlMs) {
|
|
33
|
-
this.cache.delete(key);
|
|
35
|
+
if (!options.allowStale && Date.now() - entry.timestamp > this.ttlMs) {
|
|
34
36
|
return undefined;
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -40,18 +42,175 @@ export class TTLCache<T> {
|
|
|
40
42
|
set(key: string, data: T): void {
|
|
41
43
|
this.cache.set(key, { data, timestamp: Date.now() });
|
|
42
44
|
}
|
|
45
|
+
|
|
46
|
+
clear(key?: string): void {
|
|
47
|
+
if (key) {
|
|
48
|
+
this.cache.delete(key);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.cache.clear();
|
|
52
|
+
}
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
export const usageCache = new TTLCache<UsageSummary>(CACHE_TTL_MS);
|
|
46
56
|
|
|
47
|
-
|
|
57
|
+
export type RefreshStatus = 'idle' | 'healthy' | 'refreshing' | 'stale' | 'failed';
|
|
58
|
+
|
|
59
|
+
export interface RefreshState {
|
|
60
|
+
status: RefreshStatus;
|
|
61
|
+
consecutiveFailures: number;
|
|
62
|
+
lastError: string | null;
|
|
63
|
+
lastSuccessAt: number | null;
|
|
64
|
+
nextDelayMs: number | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface StartBackgroundRefreshOptions {
|
|
68
|
+
onFailure?: (state: RefreshState) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let refreshTimer: NodeJS.Timeout | null = null;
|
|
72
|
+
let refreshActive = false;
|
|
48
73
|
let consecutiveFailures = 0;
|
|
74
|
+
let refreshFailureCallback: ((state: RefreshState) => void) | undefined;
|
|
75
|
+
let refreshState: RefreshState = {
|
|
76
|
+
status: 'idle',
|
|
77
|
+
consecutiveFailures: 0,
|
|
78
|
+
lastError: null,
|
|
79
|
+
lastSuccessAt: null,
|
|
80
|
+
nextDelayMs: null,
|
|
81
|
+
};
|
|
49
82
|
const MAX_RETRY_BACKOFF = 5; // Max 2^5 = 32x base interval (16 min)
|
|
50
|
-
const
|
|
83
|
+
const RATE_LIMIT_BACKOFF_MULTIPLIER = 8; // 4 minutes with the default 30s interval
|
|
84
|
+
|
|
85
|
+
function getErrorMessage(error: unknown): string {
|
|
86
|
+
return error instanceof Error ? error.message : String(error);
|
|
87
|
+
}
|
|
51
88
|
|
|
52
|
-
function
|
|
89
|
+
export function isRateLimitError(error: unknown): boolean {
|
|
90
|
+
const message = getErrorMessage(error).toLowerCase();
|
|
91
|
+
return (
|
|
92
|
+
message.includes('429') || message.includes('rate limit') || message.includes('rate-limit')
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getBackoffInterval(error?: unknown): number {
|
|
53
97
|
const backoffMultiplier = Math.min(consecutiveFailures, MAX_RETRY_BACKOFF);
|
|
54
|
-
|
|
98
|
+
const exponentialDelay = BACKGROUND_REFRESH_INTERVAL_MS * Math.pow(2, backoffMultiplier);
|
|
99
|
+
const cappedDelay = Math.min(
|
|
100
|
+
exponentialDelay,
|
|
101
|
+
BACKGROUND_REFRESH_INTERVAL_MS * Math.pow(2, MAX_RETRY_BACKOFF),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!isRateLimitError(error)) {
|
|
105
|
+
return cappedDelay;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const rateLimitDelay = BACKGROUND_REFRESH_INTERVAL_MS * RATE_LIMIT_BACKOFF_MULTIPLIER;
|
|
109
|
+
return Math.min(
|
|
110
|
+
Math.max(cappedDelay, rateLimitDelay),
|
|
111
|
+
BACKGROUND_REFRESH_INTERVAL_MS * Math.pow(2, MAX_RETRY_BACKOFF),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getRefreshState(): RefreshState {
|
|
116
|
+
return { ...refreshState };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function scheduleRefresh(delay: number): void {
|
|
120
|
+
if (!refreshActive) return;
|
|
121
|
+
refreshState = { ...refreshState, nextDelayMs: delay };
|
|
122
|
+
|
|
123
|
+
refreshTimer = setTimeout(async () => {
|
|
124
|
+
refreshTimer = null;
|
|
125
|
+
await runBackgroundRefreshOnce();
|
|
126
|
+
if (refreshActive) {
|
|
127
|
+
scheduleRefresh(refreshState.nextDelayMs ?? BACKGROUND_REFRESH_INTERVAL_MS);
|
|
128
|
+
}
|
|
129
|
+
}, delay);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function runBackgroundRefreshOnce(): Promise<void> {
|
|
133
|
+
refreshState = { ...refreshState, status: 'refreshing' };
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const summary = await fetchAndAggregate();
|
|
137
|
+
if (!summary) {
|
|
138
|
+
throw new Error('OpenRouter usage unavailable: no configured API key or credits response.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
usageCache.set('usage', summary);
|
|
142
|
+
consecutiveFailures = 0;
|
|
143
|
+
refreshState = {
|
|
144
|
+
status: 'healthy',
|
|
145
|
+
consecutiveFailures,
|
|
146
|
+
lastError: null,
|
|
147
|
+
lastSuccessAt: Date.now(),
|
|
148
|
+
nextDelayMs: BACKGROUND_REFRESH_INTERVAL_MS,
|
|
149
|
+
};
|
|
150
|
+
} catch (error) {
|
|
151
|
+
consecutiveFailures++;
|
|
152
|
+
const hasStaleData = usageCache.get('usage', { allowStale: true }) !== undefined;
|
|
153
|
+
refreshState = {
|
|
154
|
+
status: hasStaleData ? 'stale' : 'failed',
|
|
155
|
+
consecutiveFailures,
|
|
156
|
+
lastError: getErrorMessage(error),
|
|
157
|
+
lastSuccessAt: refreshState.lastSuccessAt,
|
|
158
|
+
nextDelayMs: getBackoffInterval(error),
|
|
159
|
+
};
|
|
160
|
+
refreshFailureCallback?.(getRefreshState());
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Extract the latest date from Activity API data.
|
|
166
|
+
* Returns undefined if analytics is null, empty, or has no valid dates.
|
|
167
|
+
*/
|
|
168
|
+
function getOfficialThroughDate(analytics: ActivityItem[] | null): string | undefined {
|
|
169
|
+
if (!analytics || analytics.length === 0) return undefined;
|
|
170
|
+
let maxDate = '';
|
|
171
|
+
// Match YYYY-MM-DD or YYYY-MM-DD HH:MM:SS
|
|
172
|
+
const dateRE = /^\d{4}-\d{2}-\d{2}/;
|
|
173
|
+
for (let i = 0; i < analytics.length; i++) {
|
|
174
|
+
const d = analytics[i];
|
|
175
|
+
if (d && d.date && dateRE.test(d.date)) {
|
|
176
|
+
const datePart = d.date.slice(0, 10); // Extract YYYY-MM-DD
|
|
177
|
+
if (datePart > maxDate) {
|
|
178
|
+
maxDate = datePart;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return maxDate || undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Compute the date range for reading local JSONL usage.
|
|
187
|
+
* Returns { fromDateUtc, toDateUtc } for the bounded window.
|
|
188
|
+
*
|
|
189
|
+
* Product decision:
|
|
190
|
+
* - If officialThroughDate exists: read from day after through today,
|
|
191
|
+
* but cap to 30-day window (never read more than today - 29 through today)
|
|
192
|
+
* - If no official data: read from today - 29 days through today (30 days total)
|
|
193
|
+
*/
|
|
194
|
+
function getLocalUsageReadRange(
|
|
195
|
+
officialThroughDate: string | undefined,
|
|
196
|
+
now: string,
|
|
197
|
+
): { fromDateUtc: string; toDateUtc: string } {
|
|
198
|
+
const fallbackStart = addUtcDays(now, -29); // 30-day window start
|
|
199
|
+
|
|
200
|
+
if (officialThroughDate) {
|
|
201
|
+
const dayAfterOfficial = addUtcDays(officialThroughDate, 1);
|
|
202
|
+
// Cap to 30-day window: use the later of dayAfterOfficial or fallbackStart
|
|
203
|
+
return {
|
|
204
|
+
fromDateUtc: dayAfterOfficial > fallbackStart ? dayAfterOfficial : fallbackStart,
|
|
205
|
+
toDateUtc: now,
|
|
206
|
+
};
|
|
207
|
+
} else {
|
|
208
|
+
// No official data: read bounded 30-day window (today - 29 through today)
|
|
209
|
+
return {
|
|
210
|
+
fromDateUtc: fallbackStart,
|
|
211
|
+
toDateUtc: now,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
55
214
|
}
|
|
56
215
|
|
|
57
216
|
export async function fetchAndAggregate(): Promise<UsageSummary | null> {
|
|
@@ -69,22 +228,7 @@ export async function fetchAndAggregate(): Promise<UsageSummary | null> {
|
|
|
69
228
|
const timestamp = Date.now();
|
|
70
229
|
|
|
71
230
|
// Get official aggregate from Activity API data
|
|
72
|
-
const officialThroughDate = (
|
|
73
|
-
if (!analytics || analytics.length === 0) return undefined;
|
|
74
|
-
let maxDate = '';
|
|
75
|
-
// Match YYYY-MM-DD or YYYY-MM-DD HH:MM:SS
|
|
76
|
-
const dateRE = /^\d{4}-\d{2}-\d{2}/;
|
|
77
|
-
for (let i = 0; i < analytics.length; i++) {
|
|
78
|
-
const d = analytics[i];
|
|
79
|
-
if (d && d.date && dateRE.test(d.date)) {
|
|
80
|
-
const datePart = d.date.slice(0, 10); // Extract YYYY-MM-DD
|
|
81
|
-
if (datePart > maxDate) {
|
|
82
|
-
maxDate = datePart;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return maxDate || undefined;
|
|
87
|
-
})();
|
|
231
|
+
const officialThroughDate = getOfficialThroughDate(analytics);
|
|
88
232
|
|
|
89
233
|
// Compute official aggregate (only from Activity API data up to officialThroughDate)
|
|
90
234
|
const officialAggregate =
|
|
@@ -106,38 +250,25 @@ export async function fetchAndAggregate(): Promise<UsageSummary | null> {
|
|
|
106
250
|
}) as LocalUsageEvent,
|
|
107
251
|
),
|
|
108
252
|
)
|
|
109
|
-
:
|
|
253
|
+
: createZeroAggregate();
|
|
110
254
|
|
|
111
|
-
// Read local JSONL
|
|
255
|
+
// Read local JSONL for bounded recent window
|
|
256
|
+
// Always compute local read range when credits exist (Activity API may be absent/empty)
|
|
257
|
+
const now = getCurrentUtcDate();
|
|
258
|
+
const localReadRange = getLocalUsageReadRange(officialThroughDate, now);
|
|
112
259
|
const localEvents: LocalUsageEvent[] = [];
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const localEventsList = await readLocalUsage({
|
|
119
|
-
fromDateUtc: localFrom,
|
|
120
|
-
toDateUtc: localTo,
|
|
121
|
-
});
|
|
122
|
-
localEvents.push(...localEventsList);
|
|
123
|
-
} catch {
|
|
124
|
-
// Fail open - if local read fails, continue with empty local
|
|
125
|
-
}
|
|
260
|
+
try {
|
|
261
|
+
const localEventsList = await readLocalUsage(localReadRange);
|
|
262
|
+
localEvents.push(...localEventsList);
|
|
263
|
+
} catch {
|
|
264
|
+
// Fail open - if local read fails, continue with empty local
|
|
126
265
|
}
|
|
127
266
|
|
|
128
267
|
// Aggregate local events
|
|
129
268
|
const localAggregate = aggregateLocal(localEvents);
|
|
130
269
|
|
|
131
270
|
// Combine official + local
|
|
132
|
-
const combinedAggregate
|
|
133
|
-
requests: officialAggregate.requests + localAggregate.requests,
|
|
134
|
-
promptTokens: officialAggregate.promptTokens + localAggregate.promptTokens,
|
|
135
|
-
completionTokens: officialAggregate.completionTokens + localAggregate.completionTokens,
|
|
136
|
-
reasoningTokens: officialAggregate.reasoningTokens + localAggregate.reasoningTokens,
|
|
137
|
-
cacheReadTokens: officialAggregate.cacheReadTokens + localAggregate.cacheReadTokens,
|
|
138
|
-
cacheWriteTokens: officialAggregate.cacheWriteTokens + localAggregate.cacheWriteTokens,
|
|
139
|
-
cost: officialAggregate.cost + localAggregate.cost,
|
|
140
|
-
};
|
|
271
|
+
const combinedAggregate = combineUsageAggregates(officialAggregate, localAggregate);
|
|
141
272
|
|
|
142
273
|
// Build full summary with local events included for 7d/30d totals
|
|
143
274
|
const summary = aggregateUsage(credits, analytics ?? [], timestamp, localEvents);
|
|
@@ -150,47 +281,28 @@ export async function fetchAndAggregate(): Promise<UsageSummary | null> {
|
|
|
150
281
|
return summary;
|
|
151
282
|
}
|
|
152
283
|
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
usageCache.set('usage', summary);
|
|
161
|
-
|
|
162
|
-
// Reset failure count on success and restart with normal interval
|
|
163
|
-
if (consecutiveFailures > 0) {
|
|
164
|
-
consecutiveFailures = 0;
|
|
165
|
-
stopBackgroundRefresh();
|
|
166
|
-
scheduleRefresh();
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
} catch {
|
|
170
|
-
consecutiveFailures++;
|
|
171
|
-
|
|
172
|
-
// Stop after max retries reached
|
|
173
|
-
if (consecutiveFailures >= MAX_RETRY_COUNT) {
|
|
174
|
-
stopBackgroundRefresh();
|
|
175
|
-
// TODO: Fire UI notification for persistent failure
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Restart with backoff interval
|
|
180
|
-
stopBackgroundRefresh();
|
|
181
|
-
scheduleRefresh();
|
|
182
|
-
}
|
|
183
|
-
}, delay);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function startBackgroundRefresh(): void {
|
|
187
|
-
if (refreshInterval) return;
|
|
188
|
-
scheduleRefresh();
|
|
284
|
+
export function startBackgroundRefresh(options: StartBackgroundRefreshOptions = {}): void {
|
|
285
|
+
if (options.onFailure) {
|
|
286
|
+
refreshFailureCallback = options.onFailure;
|
|
287
|
+
}
|
|
288
|
+
if (refreshActive || refreshTimer) return;
|
|
289
|
+
refreshActive = true;
|
|
290
|
+
scheduleRefresh(BACKGROUND_REFRESH_INTERVAL_MS);
|
|
189
291
|
}
|
|
190
292
|
|
|
191
293
|
export function stopBackgroundRefresh(): void {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
294
|
+
refreshActive = false;
|
|
295
|
+
refreshFailureCallback = undefined;
|
|
296
|
+
if (refreshTimer) {
|
|
297
|
+
clearTimeout(refreshTimer);
|
|
298
|
+
refreshTimer = null;
|
|
195
299
|
}
|
|
300
|
+
consecutiveFailures = 0;
|
|
301
|
+
refreshState = {
|
|
302
|
+
status: 'idle',
|
|
303
|
+
consecutiveFailures,
|
|
304
|
+
lastError: null,
|
|
305
|
+
lastSuccessAt: null,
|
|
306
|
+
nextDelayMs: null,
|
|
307
|
+
};
|
|
196
308
|
}
|
|
@@ -8,7 +8,7 @@ let client: OpenRouter | null = null;
|
|
|
8
8
|
|
|
9
9
|
function getClient(): OpenRouter | null {
|
|
10
10
|
if (client) return client;
|
|
11
|
-
const apiKey =
|
|
11
|
+
const apiKey = getUsageApiKey();
|
|
12
12
|
if (!apiKey) return null;
|
|
13
13
|
client = new OpenRouter({ apiKey });
|
|
14
14
|
return client;
|
|
@@ -41,9 +41,11 @@ export async function getActivity(): Promise<ActivityResponse['data'] | null> {
|
|
|
41
41
|
* Uses the SDK for consistent error handling and retry behavior.
|
|
42
42
|
*/
|
|
43
43
|
export async function fetchUserModels(): Promise<ModelsListResponse> {
|
|
44
|
-
const key =
|
|
44
|
+
const key = getModelSyncApiKey();
|
|
45
45
|
if (!key) {
|
|
46
|
-
throw new AuthError(
|
|
46
|
+
throw new AuthError(
|
|
47
|
+
'OpenRouter API key not configured. Set OPENROUTER_API_KEY or OPENROUTER_MANAGEMENT_KEY.',
|
|
48
|
+
);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
try {
|
|
@@ -69,6 +71,50 @@ export function getApiKey(): string | undefined {
|
|
|
69
71
|
return process.env['OPENROUTER_API_KEY'];
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Get the API key for usage/account endpoints.
|
|
76
|
+
* Prefers OPENROUTER_MANAGEMENT_KEY for full analytics access.
|
|
77
|
+
*/
|
|
78
|
+
export function getUsageApiKey(): string | undefined {
|
|
79
|
+
const mgmtKey = process.env['OPENROUTER_MANAGEMENT_KEY'];
|
|
80
|
+
const apiKey = process.env['OPENROUTER_API_KEY'];
|
|
81
|
+
// Treat empty strings as absent
|
|
82
|
+
return mgmtKey && mgmtKey.trim() !== ''
|
|
83
|
+
? mgmtKey
|
|
84
|
+
: apiKey && apiKey.trim() !== ''
|
|
85
|
+
? apiKey
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get the API key for model sync endpoint.
|
|
91
|
+
* Prefers OPENROUTER_API_KEY but falls back to OPENROUTER_MANAGEMENT_KEY.
|
|
92
|
+
*/
|
|
93
|
+
export function getModelSyncApiKey(): string | undefined {
|
|
94
|
+
const apiKey = process.env['OPENROUTER_API_KEY'];
|
|
95
|
+
const mgmtKey = process.env['OPENROUTER_MANAGEMENT_KEY'];
|
|
96
|
+
// Treat empty strings as absent
|
|
97
|
+
return apiKey && apiKey.trim() !== ''
|
|
98
|
+
? apiKey
|
|
99
|
+
: mgmtKey && mgmtKey.trim() !== ''
|
|
100
|
+
? mgmtKey
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if model sync is configured.
|
|
106
|
+
*/
|
|
107
|
+
export function isConfiguredForModelSync(): boolean {
|
|
108
|
+
return !!getModelSyncApiKey();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if usage/account endpoints are configured.
|
|
113
|
+
*/
|
|
114
|
+
export function isConfiguredForUsage(): boolean {
|
|
115
|
+
return !!getUsageApiKey();
|
|
116
|
+
}
|
|
117
|
+
|
|
72
118
|
/**
|
|
73
119
|
* Map SDK errors to our error types with proper status codes.
|
|
74
120
|
*/
|