@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.
@@ -0,0 +1,262 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { setLocalUsageDir, dedupeLocalUsageEvents } from '../local-usage.js';
6
+ import {
7
+ calculateOpenRouterStatusStats,
8
+ formatOpenRouterStatusBar,
9
+ loadOpenRouterStatusBar,
10
+ loadOpenRouterStatusStats,
11
+ type OpenRouterStatusStats,
12
+ } from '../status-bar.js';
13
+ import type { LocalUsageEvent } from '../types.js';
14
+
15
+ let testDir: string;
16
+
17
+ function createLocalUsageEvent(
18
+ id: string,
19
+ completedAt: string,
20
+ cost: number,
21
+ overrides: Partial<LocalUsageEvent> = {},
22
+ ): LocalUsageEvent {
23
+ return {
24
+ id,
25
+ generationId: `${id}-generation`,
26
+ sessionId: 'session-test',
27
+ completedAt,
28
+ requests: 1,
29
+ model: 'openrouter/anthropic/claude-sonnet-4',
30
+ provider: 'anthropic',
31
+ promptTokens: 10,
32
+ completionTokens: 5,
33
+ cost,
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ async function writeDailyFile(
39
+ dateUtc: string,
40
+ rows: Array<LocalUsageEvent | string>,
41
+ ): Promise<void> {
42
+ const content = rows
43
+ .map((row) => (typeof row === 'string' ? row : JSON.stringify(row)))
44
+ .join('\n');
45
+ await fs.writeFile(path.join(testDir, `${dateUtc}.jsonl`), `${content}\n`, 'utf8');
46
+ }
47
+
48
+ beforeEach(async () => {
49
+ testDir = path.join(
50
+ os.tmpdir(),
51
+ `pi-openrouter-status-bar-${Date.now()}-${Math.random().toString(36).slice(2)}`,
52
+ );
53
+ await fs.mkdir(testDir, { recursive: true });
54
+ setLocalUsageDir(testDir);
55
+ });
56
+
57
+ afterEach(async () => {
58
+ setLocalUsageDir(null);
59
+ vi.restoreAllMocks();
60
+ vi.doUnmock('../local-usage.js');
61
+ vi.doUnmock('../status-bar.js');
62
+
63
+ try {
64
+ await fs.rm(testDir, { recursive: true, force: true });
65
+ } catch {
66
+ // Ignore cleanup errors.
67
+ }
68
+ });
69
+
70
+ describe('calculateOpenRouterStatusStats', () => {
71
+ it('returns null for no local events', () => {
72
+ expect(calculateOpenRouterStatusStats([], '2026-05-22')).toBeNull();
73
+ });
74
+
75
+ it('returns null when the full 30-day window totals zero local spend', () => {
76
+ expect(
77
+ calculateOpenRouterStatusStats(
78
+ [
79
+ createLocalUsageEvent('today-zero', '2026-05-22T09:15:00.000Z', 0),
80
+ createLocalUsageEvent('older-zero', '2026-05-12T09:15:00.000Z', 0),
81
+ ],
82
+ '2026-05-22',
83
+ ),
84
+ ).toBeNull();
85
+ });
86
+
87
+ it('includes only UTC-today spend and divides by exactly 30 calendar days', () => {
88
+ const stats = calculateOpenRouterStatusStats(
89
+ [
90
+ createLocalUsageEvent('today', '2026-05-22T09:15:00.000Z', 3),
91
+ createLocalUsageEvent('yesterday', '2026-05-21T09:15:00.000Z', 9),
92
+ ],
93
+ '2026-05-22',
94
+ );
95
+
96
+ expect(stats).toEqual({
97
+ todayLocalSpend: 3,
98
+ averageLocalDailySpendLast30Days: 0.4,
99
+ burnRateMultiplier: 7.5,
100
+ });
101
+ });
102
+
103
+ it('uses only the today-29 through today window', () => {
104
+ const stats = calculateOpenRouterStatusStats(
105
+ [
106
+ createLocalUsageEvent('old', '2026-04-22T12:00:00.000Z', 100),
107
+ createLocalUsageEvent('boundary', '2026-04-23T12:00:00.000Z', 6),
108
+ createLocalUsageEvent('recent', '2026-05-21T12:00:00.000Z', 3),
109
+ createLocalUsageEvent('today', '2026-05-22T12:00:00.000Z', 1.5),
110
+ ],
111
+ '2026-05-22',
112
+ );
113
+
114
+ expect(stats).toEqual({
115
+ todayLocalSpend: 1.5,
116
+ averageLocalDailySpendLast30Days: 0.35,
117
+ burnRateMultiplier: 1.5 / 0.35,
118
+ });
119
+ });
120
+
121
+ it('deduplicates event ids exactly once across the full 30-day window', () => {
122
+ const events = [
123
+ createLocalUsageEvent('duplicate-id', '2026-05-20T12:00:00.000Z', 1),
124
+ createLocalUsageEvent('duplicate-id', '2026-05-22T12:00:00.000Z', 99),
125
+ createLocalUsageEvent('unique-id', '2026-05-22T13:00:00.000Z', 2),
126
+ ];
127
+
128
+ expect(dedupeLocalUsageEvents(events)).toHaveLength(2);
129
+ expect(calculateOpenRouterStatusStats(events, '2026-05-22')).toEqual({
130
+ todayLocalSpend: 2,
131
+ averageLocalDailySpendLast30Days: 0.1,
132
+ burnRateMultiplier: 20,
133
+ });
134
+ });
135
+ });
136
+
137
+ describe('formatOpenRouterStatusBar', () => {
138
+ it('formats the representative status text exactly', () => {
139
+ const stats: OpenRouterStatusStats = {
140
+ todayLocalSpend: 2.14,
141
+ averageLocalDailySpendLast30Days: 1.64,
142
+ burnRateMultiplier: 1.3,
143
+ };
144
+
145
+ expect(formatOpenRouterStatusBar(stats)).toBe('OR $2.14 today · 1.3x 30d avg');
146
+ });
147
+
148
+ it('formats $0.00 today · 0.0x 30d avg when prior 30-day spend exists but today spend is zero', () => {
149
+ const stats: OpenRouterStatusStats = {
150
+ todayLocalSpend: 0,
151
+ averageLocalDailySpendLast30Days: 0.5,
152
+ burnRateMultiplier: 0,
153
+ };
154
+
155
+ expect(formatOpenRouterStatusBar(stats)).toBe('OR $0.00 today · 0.0x 30d avg');
156
+ });
157
+
158
+ it('omits the multiplier when given a stats object with no denominator', () => {
159
+ expect(
160
+ formatOpenRouterStatusBar({
161
+ todayLocalSpend: 2.14,
162
+ averageLocalDailySpendLast30Days: 0,
163
+ burnRateMultiplier: null,
164
+ }),
165
+ ).toBe('OR $2.14 today');
166
+ });
167
+ });
168
+
169
+ describe('loadOpenRouterStatusStats and loadOpenRouterStatusBar', () => {
170
+ it('returns an empty result for empty local usage data', async () => {
171
+ const now = new Date('2026-05-22T12:00:00.000Z');
172
+
173
+ await expect(loadOpenRouterStatusStats(now)).resolves.toBeNull();
174
+ await expect(loadOpenRouterStatusBar(now)).resolves.toEqual({ kind: 'empty' });
175
+ });
176
+
177
+ it('returns an empty result when the 30-day window has only zero-cost local rows', async () => {
178
+ await writeDailyFile('2026-05-12', [
179
+ createLocalUsageEvent('older-zero', '2026-05-12T12:00:00.000Z', 0),
180
+ ]);
181
+ await writeDailyFile('2026-05-22', [
182
+ createLocalUsageEvent('today-zero', '2026-05-22T12:00:00.000Z', 0),
183
+ ]);
184
+
185
+ const now = new Date('2026-05-22T12:00:00.000Z');
186
+
187
+ await expect(loadOpenRouterStatusStats(now)).resolves.toBeNull();
188
+ await expect(loadOpenRouterStatusBar(now)).resolves.toEqual({ kind: 'empty' });
189
+ });
190
+
191
+ it('requests local usage only from today-29 through today', async () => {
192
+ vi.resetModules();
193
+ const actualLocalUsage =
194
+ await vi.importActual<typeof import('../local-usage.js')>('../local-usage.js');
195
+ const readLocalUsage = vi
196
+ .fn()
197
+ .mockResolvedValue([
198
+ createLocalUsageEvent('boundary', '2026-04-23T12:00:00.000Z', 1),
199
+ createLocalUsageEvent('today', '2026-05-22T12:00:00.000Z', 2),
200
+ ]);
201
+
202
+ vi.doMock('../local-usage.js', () => ({
203
+ ...actualLocalUsage,
204
+ readLocalUsage,
205
+ }));
206
+
207
+ const { loadOpenRouterStatusStats: loadMockedStats } = await import('../status-bar.js');
208
+
209
+ await expect(loadMockedStats(new Date('2026-05-22T12:00:00.000Z'))).resolves.toEqual({
210
+ todayLocalSpend: 2,
211
+ averageLocalDailySpendLast30Days: 0.1,
212
+ burnRateMultiplier: 20,
213
+ });
214
+ expect(readLocalUsage).toHaveBeenCalledTimes(1);
215
+ expect(readLocalUsage).toHaveBeenCalledWith({
216
+ fromDateUtc: '2026-04-23',
217
+ toDateUtc: '2026-05-22',
218
+ });
219
+ });
220
+
221
+ it('tolerates missing files and malformed rows while returning a ready status', async () => {
222
+ await writeDailyFile('2026-05-10', [
223
+ createLocalUsageEvent('older', '2026-05-10T12:00:00.000Z', 4),
224
+ '{not json}',
225
+ ]);
226
+ await writeDailyFile('2026-05-22', [
227
+ createLocalUsageEvent('today', '2026-05-22T12:00:00.000Z', 2),
228
+ '',
229
+ ]);
230
+
231
+ const now = new Date('2026-05-22T12:00:00.000Z');
232
+
233
+ await expect(loadOpenRouterStatusStats(now)).resolves.toEqual({
234
+ todayLocalSpend: 2,
235
+ averageLocalDailySpendLast30Days: 0.2,
236
+ burnRateMultiplier: 10,
237
+ });
238
+ await expect(loadOpenRouterStatusBar(now)).resolves.toEqual({
239
+ kind: 'ready',
240
+ text: 'OR $2.00 today · 10.0x 30d avg',
241
+ });
242
+ });
243
+
244
+ it('returns a failed result when the local usage read path throws unexpectedly', async () => {
245
+ vi.resetModules();
246
+
247
+ const readLocalUsage = vi.fn().mockRejectedValue(new Error('disk exploded'));
248
+ vi.doMock('../local-usage.js', async () => {
249
+ const actual = await vi.importActual<typeof import('../local-usage.js')>('../local-usage.js');
250
+ return {
251
+ ...actual,
252
+ readLocalUsage,
253
+ };
254
+ });
255
+
256
+ const { loadOpenRouterStatusBar: loadMockedBar } = await import('../status-bar.js');
257
+
258
+ await expect(loadMockedBar(new Date('2026-05-22T12:00:00.000Z'))).resolves.toEqual({
259
+ kind: 'failed',
260
+ });
261
+ });
262
+ });
@@ -1,20 +1,52 @@
1
+ import type { CreateKeysRequestBody } from '@openrouter/sdk/models/operations/index.js';
1
2
  import { OpenRouter } from '@openrouter/sdk/sdk/sdk.js';
2
- import type { KeyInfo, KeyStatus } from './account-types.js';
3
+ import type { CurrentKeyRelation, KeyInfo, KeyStatus } from './account-types.js';
3
4
 
4
5
  // Re-export error types from client.ts
5
6
  import { AuthError, ApiError } from './client.js';
6
7
  import { normalizeSdkKeyMetadata } from './normalizers.js';
7
8
 
8
9
  let client: OpenRouter | null = null;
10
+ let clientApiKey: string | null = null;
11
+
12
+ function getClientForApiKey(apiKey: string): OpenRouter {
13
+ if (client && clientApiKey === apiKey) {
14
+ return client;
15
+ }
9
16
 
10
- function getClient(): OpenRouter | null {
11
- if (client) return client;
12
- const apiKey = process.env['OPENROUTER_MANAGEMENT_KEY'] || process.env['OPENROUTER_API_KEY'];
13
- if (!apiKey) return null;
14
17
  client = new OpenRouter({ apiKey });
18
+ clientApiKey = apiKey;
15
19
  return client;
16
20
  }
17
21
 
22
+ function getUsageOrManagementApiKey(): string | undefined {
23
+ const managementKey = process.env['OPENROUTER_MANAGEMENT_KEY'];
24
+ if (managementKey && managementKey.trim() !== '') {
25
+ return managementKey;
26
+ }
27
+
28
+ const apiKey = process.env['OPENROUTER_API_KEY'];
29
+ if (apiKey && apiKey.trim() !== '') {
30
+ return apiKey;
31
+ }
32
+
33
+ return undefined;
34
+ }
35
+
36
+ function getClient(): OpenRouter | null {
37
+ const apiKey = getUsageOrManagementApiKey();
38
+ if (!apiKey) return null;
39
+ return getClientForApiKey(apiKey);
40
+ }
41
+
42
+ function getManagementClient(): OpenRouter {
43
+ const apiKey = process.env['OPENROUTER_MANAGEMENT_KEY'];
44
+ if (!apiKey || apiKey.trim() === '') {
45
+ throw new AuthError('OPENROUTER_MANAGEMENT_KEY is required for API key management.');
46
+ }
47
+ return getClientForApiKey(apiKey);
48
+ }
49
+
18
50
  // =============================================================================
19
51
  // Account Credits API
20
52
  // =============================================================================
@@ -37,9 +69,23 @@ export async function getAccountCredits(): Promise<number | null> {
37
69
  // Workspace ID for the default workspace (empty string) - used when workspaceId is not specified
38
70
  const DEFAULT_WORKSPACE_ID = '';
39
71
 
40
- export async function getAllKeys(): Promise<KeyInfo[] | null> {
72
+ export type KeyInventoryDegradedReason = 'management-unavailable' | 'missing-api-key';
73
+
74
+ export interface KeyInventoryResult {
75
+ keys: KeyInfo[];
76
+ canManageKeys: boolean;
77
+ degradedReason?: KeyInventoryDegradedReason;
78
+ }
79
+
80
+ export async function getAllKeys(): Promise<KeyInventoryResult> {
41
81
  const client = getClient();
42
- if (!client) return null;
82
+ if (!client) {
83
+ return {
84
+ keys: [],
85
+ canManageKeys: false,
86
+ degradedReason: 'missing-api-key',
87
+ };
88
+ }
43
89
 
44
90
  try {
45
91
  // First, get all workspaces
@@ -66,12 +112,19 @@ export async function getAllKeys(): Promise<KeyInfo[] | null> {
66
112
  allKeys.push(...keys);
67
113
  }
68
114
 
69
- return allKeys;
115
+ return {
116
+ keys: allKeys,
117
+ canManageKeys: true,
118
+ };
70
119
  } catch (err) {
71
- // If management key fails (403), fall back to current key only
72
- const sdkErr = err as { status?: number };
73
- if (sdkErr.status === 403) {
74
- return null;
120
+ // If management inventory is unavailable (403), fall back to current key only.
121
+ const sdkErr = err as { status?: number; statusCode?: number };
122
+ if ((sdkErr.status ?? sdkErr.statusCode) === 403) {
123
+ return {
124
+ keys: [],
125
+ canManageKeys: false,
126
+ degradedReason: 'management-unavailable',
127
+ };
75
128
  }
76
129
  throw mapSdkError(err);
77
130
  }
@@ -88,14 +141,162 @@ export async function getCurrentKey(): Promise<KeyInfo | null> {
88
141
  }
89
142
  }
90
143
 
144
+ /**
145
+ * Resolve how the current authenticated key relates to the listed inventory rows.
146
+ *
147
+ * OpenRouter current-key metadata does not reliably expose the stable inventory
148
+ * hash, so we fall back to label matching and distinguish external provisioning
149
+ * keys from genuinely unresolved identity.
150
+ */
151
+ export async function resolveCurrentKeyRelation(keys: KeyInfo[]): Promise<CurrentKeyRelation> {
152
+ if (keys.length === 0) {
153
+ return { kind: 'unresolved', reason: 'no-inventory-keys' };
154
+ }
155
+
156
+ const client = getClient();
157
+ if (!client) {
158
+ return { kind: 'unresolved', reason: 'missing-api-key' };
159
+ }
160
+
161
+ try {
162
+ const response = await client.apiKeys.getCurrentKeyMetadata();
163
+ const raw = response.data;
164
+ const currentKey = normalizeSdkKeyMetadata(raw);
165
+
166
+ if (hasTrustedHash(currentKey.hash)) {
167
+ const matchedByHash = keys.find((key) => key.hash === currentKey.hash);
168
+ if (matchedByHash) {
169
+ return {
170
+ kind: 'inventory-match',
171
+ hash: currentKey.hash,
172
+ label: matchedByHash.label,
173
+ };
174
+ }
175
+
176
+ if (raw.isProvisioningKey === true) {
177
+ return { kind: 'external-provisioning', label: currentKey.label };
178
+ }
179
+
180
+ return {
181
+ kind: 'unresolved',
182
+ reason: 'current-hash-not-in-inventory',
183
+ label: currentKey.label,
184
+ };
185
+ }
186
+
187
+ const currentLabel = currentKey.label.trim();
188
+ if (currentLabel === '') {
189
+ return { kind: 'unresolved', reason: 'missing-current-label' };
190
+ }
191
+
192
+ const matchingHashes = Array.from(
193
+ new Set(
194
+ keys
195
+ .filter((key) => key.label.trim() === currentLabel)
196
+ .map((key) => (hasTrustedHash(key.hash) ? key.hash : undefined))
197
+ .filter((hash): hash is string => hash !== undefined),
198
+ ),
199
+ );
200
+
201
+ if (matchingHashes.length === 1) {
202
+ return { kind: 'inventory-match', hash: matchingHashes[0]!, label: currentLabel };
203
+ }
204
+
205
+ if (matchingHashes.length > 1) {
206
+ return { kind: 'ambiguous-label', label: currentLabel, matchingHashes };
207
+ }
208
+
209
+ if (raw.isProvisioningKey === true) {
210
+ return { kind: 'external-provisioning', label: currentLabel };
211
+ }
212
+
213
+ return { kind: 'unresolved', reason: 'no-inventory-match', label: currentLabel };
214
+ } catch (err) {
215
+ throw mapSdkError(err);
216
+ }
217
+ }
218
+
219
+ export interface CreateApiKeyInput {
220
+ name: string;
221
+ limit?: number | null;
222
+ limitReset?: CreateKeysRequestBody['limitReset'];
223
+ includeByokInLimit?: boolean;
224
+ workspaceId?: string;
225
+ expiresAt?: Date;
226
+ }
227
+
228
+ export type ApiKeyMutationInfo = Omit<KeyInfo, 'workspaceName'>;
229
+
230
+ export interface CreatedApiKeyResult {
231
+ key: string;
232
+ keyState: ApiKeyMutationInfo;
233
+ }
234
+
235
+ export async function createApiKey(input: CreateApiKeyInput): Promise<CreatedApiKeyResult> {
236
+ const client = getManagementClient();
237
+
238
+ const requestBody: CreateKeysRequestBody = {
239
+ name: input.name,
240
+ };
241
+
242
+ if (input.limit !== undefined) {
243
+ requestBody.limit = input.limit;
244
+ }
245
+ if (input.limitReset !== undefined) {
246
+ requestBody.limitReset = input.limitReset;
247
+ }
248
+ if (input.includeByokInLimit !== undefined) {
249
+ requestBody.includeByokInLimit = input.includeByokInLimit;
250
+ }
251
+ if (input.workspaceId !== undefined) {
252
+ requestBody.workspaceId = input.workspaceId;
253
+ }
254
+ if (input.expiresAt !== undefined) {
255
+ requestBody.expiresAt = input.expiresAt;
256
+ }
257
+
258
+ try {
259
+ const response = await client.apiKeys.create({ requestBody });
260
+ return {
261
+ key: response.key,
262
+ keyState: keyMetadataToMutationInfo(normalizeSdkKeyMetadata(response.data)),
263
+ };
264
+ } catch (err) {
265
+ throw mapManagementSdkError(err, 'create API keys');
266
+ }
267
+ }
268
+
269
+ export async function setApiKeyDisabled(
270
+ hash: string,
271
+ disabled: boolean,
272
+ ): Promise<ApiKeyMutationInfo> {
273
+ const client = getManagementClient();
274
+
275
+ try {
276
+ const response = await client.apiKeys.update({
277
+ hash,
278
+ requestBody: {
279
+ disabled,
280
+ },
281
+ });
282
+
283
+ return keyMetadataToMutationInfo(normalizeSdkKeyMetadata(response.data));
284
+ } catch (err) {
285
+ throw mapManagementSdkError(err, `${disabled ? 'disable' : 'enable'} API keys`);
286
+ }
287
+ }
288
+
91
289
  // =============================================================================
92
290
  // Helper Functions
93
291
  // =============================================================================
94
292
 
95
- function keyMetadataToKeyInfo(
293
+ function hasTrustedHash(hash?: string): hash is string {
294
+ return typeof hash === 'string' && hash.trim() !== '';
295
+ }
296
+
297
+ function keyMetadataToMutationInfo(
96
298
  metadata: ReturnType<typeof normalizeSdkKeyMetadata>,
97
- workspaceName: string,
98
- ): KeyInfo {
299
+ ): ApiKeyMutationInfo {
99
300
  const { name, label, used, limit, remaining, resetCadence, byok, hash, disabled } = metadata;
100
301
 
101
302
  // Calculate status based on usage percentage
@@ -119,7 +320,7 @@ function keyMetadataToKeyInfo(
119
320
  }
120
321
  }
121
322
 
122
- const keyInfo: KeyInfo = {
323
+ const keyState: ApiKeyMutationInfo = {
123
324
  name,
124
325
  label,
125
326
  status,
@@ -127,39 +328,85 @@ function keyMetadataToKeyInfo(
127
328
  spend: used, // spend is the same as usage (in USD)
128
329
  resetCadence,
129
330
  byok,
130
- hash,
131
331
  disabled,
132
- workspaceName,
133
332
  };
134
333
 
334
+ if (hash !== undefined) {
335
+ keyState.hash = hash;
336
+ }
135
337
  if (limit !== undefined) {
136
- keyInfo.limit = limit;
338
+ keyState.limit = limit;
137
339
  }
138
340
  if (remaining !== undefined) {
139
- keyInfo.remaining = remaining;
341
+ keyState.remaining = remaining;
140
342
  }
141
343
 
142
- return keyInfo;
344
+ return keyState;
345
+ }
346
+
347
+ function keyMetadataToKeyInfo(
348
+ metadata: ReturnType<typeof normalizeSdkKeyMetadata>,
349
+ workspaceName: string,
350
+ ): KeyInfo {
351
+ return {
352
+ ...keyMetadataToMutationInfo(metadata),
353
+ workspaceName,
354
+ };
143
355
  }
144
356
 
145
357
  function mapSdkError(err: unknown): Error {
146
- const rawErr = err as { status?: number; message?: string };
147
- const status = rawErr.status;
358
+ const rawErr = err as { status?: number; statusCode?: number; message?: string };
359
+ const status = rawErr.status ?? rawErr.statusCode;
148
360
  const message = rawErr.message ?? 'Unknown error';
149
361
 
150
362
  if (status === 401) {
151
363
  return new AuthError(message);
152
364
  }
153
365
  if (status === 403) {
154
- return new ApiError(`Forbidden: ${message}`);
366
+ return new ApiError(`Forbidden: ${message}`, 403);
367
+ }
368
+ if (status !== undefined) {
369
+ return new ApiError(message, status);
155
370
  }
156
371
 
157
372
  if (err instanceof Error) return err;
158
373
  return new Error(String(err));
159
374
  }
160
375
 
161
- export function getCurrentKeyHash(): string | undefined {
162
- // For v1, we don't hash the current API key for comparison
163
- // This is a follow-up item from the planning docs
164
- return undefined;
376
+ function mapManagementSdkError(err: unknown, action: string): Error {
377
+ const rawErr = err as { status?: number; statusCode?: number; message?: string };
378
+ const status = rawErr.status ?? rawErr.statusCode;
379
+ const message = rawErr.message ?? 'Unknown error';
380
+
381
+ if (status === 401) {
382
+ return new AuthError(
383
+ `OPENROUTER_MANAGEMENT_KEY is required to ${action}. Set it to a valid management key.`,
384
+ );
385
+ }
386
+
387
+ if (status === 403) {
388
+ return new ApiError(
389
+ `OPENROUTER_MANAGEMENT_KEY does not have permission to ${action}. Set it to a valid management key.`,
390
+ 403,
391
+ );
392
+ }
393
+
394
+ if (err instanceof Error && /unauthorized/i.test(err.message)) {
395
+ return new AuthError(
396
+ `OPENROUTER_MANAGEMENT_KEY is required to ${action}. Set it to a valid management key.`,
397
+ );
398
+ }
399
+
400
+ if (err instanceof Error && /forbidden/i.test(err.message)) {
401
+ return new ApiError(
402
+ `OPENROUTER_MANAGEMENT_KEY does not have permission to ${action}. Set it to a valid management key.`,
403
+ 403,
404
+ );
405
+ }
406
+
407
+ if (status === 400) {
408
+ return new ApiError(message, 400);
409
+ }
410
+
411
+ return mapSdkError(err);
165
412
  }