@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
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
6
|
import type { ExtensionContext, ModelRegistry } from '@mariozechner/pi-coding-agent';
|
|
7
|
-
import type { SyncResult } from '../types.js';
|
|
7
|
+
import type { PiModelConfig, SyncResult } from '../types.js';
|
|
8
|
+
import { ROUTER_ALIASES } from '../types.js';
|
|
9
|
+
import { createPiModelConfig, createValidModel } from '../../__tests__/fixtures.js';
|
|
8
10
|
|
|
9
11
|
// Import modules
|
|
10
12
|
import {
|
|
@@ -13,9 +15,10 @@ import {
|
|
|
13
15
|
getSyncState,
|
|
14
16
|
getStatusText,
|
|
15
17
|
areModelsAvailable,
|
|
18
|
+
includeBuiltinRouterModels,
|
|
16
19
|
} from '../sync.js';
|
|
17
20
|
import { fetchUserModels, AuthError } from '../../client.js';
|
|
18
|
-
import { loadCache } from '../cache.js';
|
|
21
|
+
import { loadCache, saveCache } from '../cache.js';
|
|
19
22
|
|
|
20
23
|
// Mock the client module to control API behavior
|
|
21
24
|
vi.mock('../../client.js', () => ({
|
|
@@ -133,8 +136,13 @@ describe('syncModels', () => {
|
|
|
133
136
|
it('should return failure when API key is missing and no cache', async () => {
|
|
134
137
|
// Ensure API key is not set
|
|
135
138
|
delete process.env['OPENROUTER_API_KEY'];
|
|
139
|
+
delete process.env['OPENROUTER_MANAGEMENT_KEY'];
|
|
136
140
|
// Mock fetchUserModels to throw AuthError
|
|
137
|
-
vi.mocked(fetchUserModels).mockRejectedValueOnce(
|
|
141
|
+
vi.mocked(fetchUserModels).mockRejectedValueOnce(
|
|
142
|
+
new AuthError(
|
|
143
|
+
'OpenRouter API key not configured. Set OPENROUTER_API_KEY or OPENROUTER_MANAGEMENT_KEY.',
|
|
144
|
+
),
|
|
145
|
+
);
|
|
138
146
|
// Mock loadCache to return null (no cache available)
|
|
139
147
|
vi.mocked(loadCache).mockResolvedValueOnce(null);
|
|
140
148
|
|
|
@@ -143,7 +151,7 @@ describe('syncModels', () => {
|
|
|
143
151
|
expect(result.success).toBe(false);
|
|
144
152
|
expect(result.registeredCount).toBe(0);
|
|
145
153
|
expect(result.source).toBe('none');
|
|
146
|
-
expect(result.error).toContain('
|
|
154
|
+
expect(result.error).toContain('OpenRouter API key not configured');
|
|
147
155
|
});
|
|
148
156
|
|
|
149
157
|
it('should sync models from API and register with provider', async () => {
|
|
@@ -182,6 +190,150 @@ describe('syncModels', () => {
|
|
|
182
190
|
expect(result.registeredCount).toBeGreaterThan(0);
|
|
183
191
|
expect(mockRegisterProvider).toHaveBeenCalled();
|
|
184
192
|
});
|
|
193
|
+
|
|
194
|
+
it('should register the API user catalog plus router aliases exactly once', async () => {
|
|
195
|
+
const mockModel = {
|
|
196
|
+
id: 'user/model-a',
|
|
197
|
+
name: 'User Model A',
|
|
198
|
+
architecture: {
|
|
199
|
+
inputModalities: ['text'],
|
|
200
|
+
outputModalities: ['text'],
|
|
201
|
+
},
|
|
202
|
+
contextLength: 128000,
|
|
203
|
+
pricing: {
|
|
204
|
+
prompt: 0.000001,
|
|
205
|
+
completion: 0.000002,
|
|
206
|
+
},
|
|
207
|
+
supportedParameters: [],
|
|
208
|
+
topProvider: {
|
|
209
|
+
contextLength: 128000,
|
|
210
|
+
maxCompletionTokens: 4096,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
vi.mocked(fetchUserModels).mockResolvedValueOnce({
|
|
215
|
+
data: [mockModel],
|
|
216
|
+
} as any);
|
|
217
|
+
|
|
218
|
+
const result = await syncModels(mockCtx);
|
|
219
|
+
const providerConfig = mockRegisterProvider.mock.calls[0]![1] as { models: PiModelConfig[] };
|
|
220
|
+
const registeredIds = providerConfig.models.map((model) => model.id);
|
|
221
|
+
|
|
222
|
+
expect(result.registeredCount).toBe(1 + ROUTER_ALIASES.length);
|
|
223
|
+
expect(registeredIds).toEqual(['user/model-a', ...ROUTER_ALIASES]);
|
|
224
|
+
expect(registeredIds).not.toContain('anthropic/claude-3-opus');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should register cached user models plus router aliases on API failure', async () => {
|
|
228
|
+
vi.mocked(fetchUserModels).mockRejectedValueOnce(new Error('api down'));
|
|
229
|
+
vi.mocked(loadCache).mockResolvedValueOnce({
|
|
230
|
+
models: [createValidModel({ id: 'cached/model-a', name: 'Cached Model A' })],
|
|
231
|
+
skippedDetails: [
|
|
232
|
+
{
|
|
233
|
+
id: 'bad/model',
|
|
234
|
+
reason: 'missing context window',
|
|
235
|
+
hint: "Add a local contextWindow override with '/openrouter model-override-set <model-id> contextWindow=<tokens>' if the model's limit is known.",
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
timestamp: Date.now() - 60000,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const result = await syncModels(mockCtx);
|
|
242
|
+
const providerConfig = mockRegisterProvider.mock.calls[0]![1] as { models: PiModelConfig[] };
|
|
243
|
+
const registeredIds = providerConfig.models.map((model) => model.id);
|
|
244
|
+
|
|
245
|
+
expect(result.source).toBe('cache');
|
|
246
|
+
expect(result.registeredCount).toBe(1 + ROUTER_ALIASES.length);
|
|
247
|
+
expect(registeredIds).toEqual(['cached/model-a', ...ROUTER_ALIASES]);
|
|
248
|
+
expect(result.skippedDetails).toEqual([
|
|
249
|
+
{
|
|
250
|
+
id: 'bad/model',
|
|
251
|
+
reason: 'missing context window',
|
|
252
|
+
hint: "Add a local contextWindow override with '/openrouter model-override-set <model-id> contextWindow=<tokens>' if the model's limit is known.",
|
|
253
|
+
},
|
|
254
|
+
]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should persist skipped reason hints in the saved cache', async () => {
|
|
258
|
+
vi.mocked(fetchUserModels).mockResolvedValueOnce({
|
|
259
|
+
data: [
|
|
260
|
+
{
|
|
261
|
+
id: 'user/model-a',
|
|
262
|
+
name: 'User Model A',
|
|
263
|
+
architecture: {
|
|
264
|
+
inputModalities: ['text'],
|
|
265
|
+
outputModalities: ['text'],
|
|
266
|
+
},
|
|
267
|
+
contextLength: 128000,
|
|
268
|
+
pricing: {
|
|
269
|
+
prompt: 0.000001,
|
|
270
|
+
completion: 0.000002,
|
|
271
|
+
},
|
|
272
|
+
supportedParameters: [],
|
|
273
|
+
topProvider: {
|
|
274
|
+
contextLength: 128000,
|
|
275
|
+
maxCompletionTokens: 4096,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: 'bad/model',
|
|
280
|
+
name: 'Bad Model',
|
|
281
|
+
architecture: {
|
|
282
|
+
inputModalities: ['text'],
|
|
283
|
+
outputModalities: ['text'],
|
|
284
|
+
},
|
|
285
|
+
contextLength: 0,
|
|
286
|
+
pricing: {
|
|
287
|
+
prompt: 0.000001,
|
|
288
|
+
completion: 0.000002,
|
|
289
|
+
},
|
|
290
|
+
supportedParameters: [],
|
|
291
|
+
topProvider: {
|
|
292
|
+
contextLength: 0,
|
|
293
|
+
maxCompletionTokens: 4096,
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
} as any);
|
|
298
|
+
|
|
299
|
+
const result = await syncModels(mockCtx);
|
|
300
|
+
|
|
301
|
+
expect(result.skippedDetails).toEqual([
|
|
302
|
+
{
|
|
303
|
+
id: 'bad/model',
|
|
304
|
+
reason: 'missing context window',
|
|
305
|
+
hint: expect.stringContaining(
|
|
306
|
+
'/openrouter model-override-set <model-id> contextWindow=<tokens>',
|
|
307
|
+
),
|
|
308
|
+
},
|
|
309
|
+
]);
|
|
310
|
+
expect(vi.mocked(saveCache)).toHaveBeenCalledWith(
|
|
311
|
+
expect.objectContaining({
|
|
312
|
+
skippedDetails: [
|
|
313
|
+
{
|
|
314
|
+
id: 'bad/model',
|
|
315
|
+
reason: 'missing context window',
|
|
316
|
+
hint: expect.stringContaining(
|
|
317
|
+
'/openrouter model-override-set <model-id> contextWindow=<tokens>',
|
|
318
|
+
),
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should include router aliases at most once', () => {
|
|
326
|
+
const configs = includeBuiltinRouterModels([
|
|
327
|
+
createPiModelConfig({ id: 'user/model-a' }),
|
|
328
|
+
createPiModelConfig({ id: ROUTER_ALIASES[0]! }),
|
|
329
|
+
]);
|
|
330
|
+
const registeredIds = configs.map((model) => model.id);
|
|
331
|
+
|
|
332
|
+
expect(registeredIds).toEqual(['user/model-a', ...ROUTER_ALIASES]);
|
|
333
|
+
for (const routerId of ROUTER_ALIASES) {
|
|
334
|
+
expect(registeredIds.filter((id) => id === routerId)).toHaveLength(1);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
185
337
|
});
|
|
186
338
|
|
|
187
339
|
describe('syncState management', () => {
|
|
@@ -6,6 +6,7 @@ import { MS_PER_MINUTE } from './types.js';
|
|
|
6
6
|
|
|
7
7
|
const CACHE_FILENAME = 'models-cache.json';
|
|
8
8
|
const DEFAULT_CACHE_DIR = join(homedir(), '.pi', 'openrouter');
|
|
9
|
+
const MAX_FUTURE_SKEW_MS = 5 * MS_PER_MINUTE;
|
|
9
10
|
|
|
10
11
|
// Allow overriding cache directory for testing
|
|
11
12
|
let cacheDirOverride: string | null = null;
|
|
@@ -40,6 +41,21 @@ async function ensureCacheDir(): Promise<void> {
|
|
|
40
41
|
await mkdir(getCacheDir(), { recursive: true });
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Returns true when a cache timestamp is structurally valid and not too far in the future.
|
|
46
|
+
*/
|
|
47
|
+
function isValidCacheTimestamp(timestamp: number, now = Date.now()): boolean {
|
|
48
|
+
return Number.isFinite(timestamp) && timestamp <= now + MAX_FUTURE_SKEW_MS;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Clamp future timestamps when saving so new cache writes never persist negative ages.
|
|
53
|
+
*/
|
|
54
|
+
function normalizeTimestampForSave(timestamp: number, now = Date.now()): number {
|
|
55
|
+
if (!isValidCacheTimestamp(timestamp, now)) return now;
|
|
56
|
+
return Math.min(timestamp, now);
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
/**
|
|
44
60
|
* Load cached models from disk.
|
|
45
61
|
* Returns null if cache doesn't exist or is corrupted.
|
|
@@ -55,6 +71,10 @@ export async function loadCache(): Promise<ModelsCache | null> {
|
|
|
55
71
|
return null;
|
|
56
72
|
}
|
|
57
73
|
|
|
74
|
+
if (!isValidCacheTimestamp(parsed.timestamp)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
return parsed;
|
|
59
79
|
} catch {
|
|
60
80
|
// File doesn't exist, permission error, or invalid JSON
|
|
@@ -68,14 +88,18 @@ export async function loadCache(): Promise<ModelsCache | null> {
|
|
|
68
88
|
export async function saveCache(cache: ModelsCache): Promise<void> {
|
|
69
89
|
await ensureCacheDir();
|
|
70
90
|
const cachePath = getCachePath();
|
|
71
|
-
|
|
91
|
+
const normalizedCache: ModelsCache = {
|
|
92
|
+
...cache,
|
|
93
|
+
timestamp: normalizeTimestampForSave(cache.timestamp),
|
|
94
|
+
};
|
|
95
|
+
await writeFile(cachePath, JSON.stringify(normalizedCache, null, 2));
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
/**
|
|
75
99
|
* Get the age of the cache in milliseconds.
|
|
76
100
|
*/
|
|
77
101
|
export function getCacheAgeMs(cache: ModelsCache): number {
|
|
78
|
-
return Date.now() - cache.timestamp;
|
|
102
|
+
return Math.max(0, Date.now() - cache.timestamp);
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
/**
|
|
@@ -84,6 +108,7 @@ export function getCacheAgeMs(cache: ModelsCache): number {
|
|
|
84
108
|
*/
|
|
85
109
|
export function formatDuration(ms: number | null): string {
|
|
86
110
|
if (ms === null) return 'unknown';
|
|
111
|
+
if (ms <= 0) return '<1m';
|
|
87
112
|
|
|
88
113
|
const minutes = Math.floor(ms / MS_PER_MINUTE);
|
|
89
114
|
if (minutes < 1) return '<1m';
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { OpenRouterModel, PiModelConfig, SkipReason, MapResult } from './types.js';
|
|
2
2
|
import { ROUTER_ALIASES } from './types.js';
|
|
3
|
+
import { getSkipReasonHint } from './skip-hints.js';
|
|
3
4
|
import type { Model as SDKModel } from '@openrouter/sdk/models/index.js';
|
|
4
5
|
import { loadModelOverrides, getModelOverride } from './overrides.js';
|
|
6
|
+
import { normalizeOpenRouterModel } from '../normalizers.js';
|
|
5
7
|
|
|
6
8
|
// Cache for built-in OpenRouter models from pi-ai
|
|
7
9
|
// Populated lazily on first access
|
|
@@ -57,68 +59,24 @@ const COST_PER_MILLION = 1_000_000;
|
|
|
57
59
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
|
-
*
|
|
61
|
-
* Handles SDK's camelCase naming convention.
|
|
62
|
+
* Validation result for a model check.
|
|
62
63
|
*/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
context_length: model.topProvider.contextLength ?? 0,
|
|
67
|
-
max_completion_tokens: model.topProvider.maxCompletionTokens ?? 0,
|
|
68
|
-
}
|
|
69
|
-
: undefined;
|
|
70
|
-
|
|
71
|
-
const perRequestLimits = model.perRequestLimits
|
|
72
|
-
? {
|
|
73
|
-
completion_tokens: model.perRequestLimits.completionTokens ?? 0,
|
|
74
|
-
}
|
|
75
|
-
: undefined;
|
|
76
|
-
|
|
77
|
-
// Build the object conditionally to avoid undefined property issues
|
|
78
|
-
const result: OpenRouterModel = {
|
|
79
|
-
id: model.id,
|
|
80
|
-
name: model.name,
|
|
81
|
-
architecture: {
|
|
82
|
-
input_modalities: model.architecture.inputModalities ?? [],
|
|
83
|
-
output_modalities: model.architecture.outputModalities ?? [],
|
|
84
|
-
},
|
|
85
|
-
context_length: model.contextLength ?? 0,
|
|
86
|
-
pricing: {
|
|
87
|
-
prompt: String(model.pricing.prompt ?? 0),
|
|
88
|
-
completion: String(model.pricing.completion ?? 0),
|
|
89
|
-
input_cache_read: String(model.pricing.inputCacheRead ?? 0),
|
|
90
|
-
input_cache_write: String(model.pricing.inputCacheWrite ?? 0),
|
|
91
|
-
},
|
|
92
|
-
supported_parameters: model.supportedParameters,
|
|
93
|
-
};
|
|
64
|
+
type PricedOpenRouterModel = OpenRouterModel & {
|
|
65
|
+
pricing: NonNullable<OpenRouterModel['pricing']>;
|
|
66
|
+
};
|
|
94
67
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
if (perRequestLimits) {
|
|
100
|
-
result.per_request_limits = perRequestLimits;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
68
|
+
type ValidationResult =
|
|
69
|
+
| { valid: true; model: PricedOpenRouterModel; contextWindow: number }
|
|
70
|
+
| { valid: false; reason: string; modelId: string; hint?: string };
|
|
105
71
|
|
|
106
72
|
/**
|
|
107
|
-
*
|
|
73
|
+
* Build a failed validation result with a stable machine reason and optional hint.
|
|
108
74
|
*/
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
: (model as OpenRouterModel);
|
|
75
|
+
function invalidModel(reason: string, modelId: string): ValidationResult {
|
|
76
|
+
const hint = getSkipReasonHint(reason);
|
|
77
|
+
return hint ? { valid: false, reason, modelId, hint } : { valid: false, reason, modelId };
|
|
113
78
|
}
|
|
114
79
|
|
|
115
|
-
/**
|
|
116
|
-
* Validation result for a model check.
|
|
117
|
-
*/
|
|
118
|
-
type ValidationResult =
|
|
119
|
-
| { valid: true; model: OpenRouterModel; contextWindow: number }
|
|
120
|
-
| { valid: false; reason: string; modelId: string };
|
|
121
|
-
|
|
122
80
|
/**
|
|
123
81
|
* Validate a model and return either a valid result with extracted context window
|
|
124
82
|
* or a failure reason.
|
|
@@ -126,30 +84,31 @@ type ValidationResult =
|
|
|
126
84
|
function validateModel(model: OpenRouterModel): ValidationResult {
|
|
127
85
|
// Check: missing required id
|
|
128
86
|
if (!model.id) {
|
|
129
|
-
return
|
|
87
|
+
return invalidModel('missing id', 'unknown');
|
|
130
88
|
}
|
|
131
89
|
|
|
132
90
|
// Check: missing required pricing fields
|
|
133
|
-
|
|
134
|
-
|
|
91
|
+
const pricing = model.pricing;
|
|
92
|
+
if (!pricing?.prompt) {
|
|
93
|
+
return invalidModel('missing prompt pricing', model.id);
|
|
135
94
|
}
|
|
136
|
-
if (!
|
|
137
|
-
return
|
|
95
|
+
if (!pricing.completion) {
|
|
96
|
+
return invalidModel('missing completion pricing', model.id);
|
|
138
97
|
}
|
|
139
98
|
|
|
140
99
|
// Check: missing context window (both primary and fallback)
|
|
141
100
|
const contextWindow = model.top_provider?.context_length ?? model.context_length;
|
|
142
101
|
if (!contextWindow) {
|
|
143
|
-
return
|
|
102
|
+
return invalidModel('missing context window', model.id);
|
|
144
103
|
}
|
|
145
104
|
|
|
146
105
|
// Check: explicitly non-text output (if specified)
|
|
147
106
|
const outputModalities = model.architecture?.output_modalities;
|
|
148
107
|
if (outputModalities && !outputModalities.includes('text')) {
|
|
149
|
-
return
|
|
108
|
+
return invalidModel('non-text output modalities', model.id);
|
|
150
109
|
}
|
|
151
110
|
|
|
152
|
-
return { valid: true, model, contextWindow };
|
|
111
|
+
return { valid: true, model: { ...model, pricing }, contextWindow };
|
|
153
112
|
}
|
|
154
113
|
|
|
155
114
|
/**
|
|
@@ -158,7 +117,7 @@ function validateModel(model: OpenRouterModel): ValidationResult {
|
|
|
158
117
|
* Priority: user overrides > built-in registry > API data
|
|
159
118
|
*/
|
|
160
119
|
async function buildPiConfig(
|
|
161
|
-
model:
|
|
120
|
+
model: PricedOpenRouterModel,
|
|
162
121
|
contextWindow: number,
|
|
163
122
|
userOverrides?: Awaited<ReturnType<typeof loadModelOverrides>>,
|
|
164
123
|
): Promise<PiModelConfig> {
|
|
@@ -227,7 +186,7 @@ export async function mapOpenRouterModels(
|
|
|
227
186
|
const skippedDetails: SkipReason[] = [];
|
|
228
187
|
|
|
229
188
|
for (const rawModel of models) {
|
|
230
|
-
const model =
|
|
189
|
+
const model = normalizeOpenRouterModel(rawModel);
|
|
231
190
|
|
|
232
191
|
// Skip router aliases - they're added manually after mapping
|
|
233
192
|
if (ROUTER_ALIASES.includes(model.id)) {
|
|
@@ -238,11 +197,18 @@ export async function mapOpenRouterModels(
|
|
|
238
197
|
|
|
239
198
|
if (!validation.valid) {
|
|
240
199
|
skipped++;
|
|
241
|
-
|
|
200
|
+
const skippedDetail: SkipReason = {
|
|
201
|
+
id: validation.modelId,
|
|
202
|
+
reason: validation.reason,
|
|
203
|
+
};
|
|
204
|
+
if (validation.hint) {
|
|
205
|
+
skippedDetail.hint = validation.hint;
|
|
206
|
+
}
|
|
207
|
+
skippedDetails.push(skippedDetail);
|
|
242
208
|
continue;
|
|
243
209
|
}
|
|
244
210
|
|
|
245
|
-
configs.push(await buildPiConfig(model, validation.contextWindow, userOverrides));
|
|
211
|
+
configs.push(await buildPiConfig(validation.model, validation.contextWindow, userOverrides));
|
|
246
212
|
}
|
|
247
213
|
|
|
248
214
|
return { configs, skipped, skippedDetails };
|
|
@@ -259,7 +225,7 @@ export async function mapOpenRouterModel(
|
|
|
259
225
|
await loadBuiltInOpenRouterModels();
|
|
260
226
|
const userOverrides = await loadModelOverrides();
|
|
261
227
|
|
|
262
|
-
const normalized =
|
|
228
|
+
const normalized = normalizeOpenRouterModel(model);
|
|
263
229
|
|
|
264
230
|
// Router aliases are handled separately, skip them here
|
|
265
231
|
if (ROUTER_ALIASES.includes(normalized.id)) {
|
|
@@ -272,5 +238,5 @@ export async function mapOpenRouterModel(
|
|
|
272
238
|
return null;
|
|
273
239
|
}
|
|
274
240
|
|
|
275
|
-
return buildPiConfig(
|
|
241
|
+
return buildPiConfig(validation.model, validation.contextWindow, userOverrides);
|
|
276
242
|
}
|