@robhowley/pi-openrouter 0.11.1 → 0.12.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 +27 -1
- package/extensions/openrouter/__tests__/commands.test.ts +142 -44
- package/extensions/openrouter/__tests__/fixtures.ts +1 -0
- package/extensions/openrouter/__tests__/hooks.test.ts +82 -3
- package/extensions/openrouter/commands.ts +95 -17
- package/extensions/openrouter/hooks.ts +24 -4
- package/extensions/openrouter/models/__tests__/cache.test.ts +20 -1
- package/extensions/openrouter/models/__tests__/sync.test.ts +398 -222
- package/extensions/openrouter/models/cache.ts +27 -5
- package/extensions/openrouter/models/sync.ts +230 -77
- package/extensions/openrouter/models/types.ts +36 -3
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import type { ModelsCache } from './types.js';
|
|
4
|
+
import type { CatalogMode, ModelsCache } from './types.js';
|
|
5
5
|
import { MS_PER_MINUTE } from './types.js';
|
|
6
6
|
|
|
7
7
|
const CACHE_FILENAME = 'models-cache.json';
|
|
@@ -48,6 +48,16 @@ function isValidCacheTimestamp(timestamp: number, now = Date.now()): boolean {
|
|
|
48
48
|
return Number.isFinite(timestamp) && timestamp <= now + MAX_FUTURE_SKEW_MS;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Normalize persisted catalog mode.
|
|
53
|
+
* Missing mode means an older cache file and defaults to full catalog mode.
|
|
54
|
+
*/
|
|
55
|
+
function normalizeCatalogMode(mode: unknown): CatalogMode | null {
|
|
56
|
+
if (mode === undefined) return 'full';
|
|
57
|
+
if (mode === 'full' || mode === 'free-only') return mode;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
/**
|
|
52
62
|
* Clamp future timestamps when saving so new cache writes never persist negative ages.
|
|
53
63
|
*/
|
|
@@ -64,18 +74,29 @@ export async function loadCache(): Promise<ModelsCache | null> {
|
|
|
64
74
|
try {
|
|
65
75
|
const cachePath = getCachePath();
|
|
66
76
|
const data = await readFile(cachePath, 'utf-8');
|
|
67
|
-
const parsed = JSON.parse(data) as ModelsCache;
|
|
77
|
+
const parsed = JSON.parse(data) as Partial<ModelsCache> & { catalogMode?: unknown };
|
|
68
78
|
|
|
69
|
-
// Validate structure
|
|
79
|
+
// Validate required structure
|
|
70
80
|
if (!parsed.models || !Array.isArray(parsed.models) || typeof parsed.timestamp !== 'number') {
|
|
71
81
|
return null;
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
const catalogMode = normalizeCatalogMode(parsed.catalogMode);
|
|
85
|
+
if (!catalogMode || !isValidCacheTimestamp(parsed.timestamp)) {
|
|
75
86
|
return null;
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
|
|
89
|
+
const normalizedCache: ModelsCache = {
|
|
90
|
+
catalogMode,
|
|
91
|
+
models: parsed.models,
|
|
92
|
+
timestamp: parsed.timestamp,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (Array.isArray(parsed.skippedDetails)) {
|
|
96
|
+
normalizedCache.skippedDetails = parsed.skippedDetails;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return normalizedCache;
|
|
79
100
|
} catch {
|
|
80
101
|
// File doesn't exist, permission error, or invalid JSON
|
|
81
102
|
return null;
|
|
@@ -90,6 +111,7 @@ export async function saveCache(cache: ModelsCache): Promise<void> {
|
|
|
90
111
|
const cachePath = getCachePath();
|
|
91
112
|
const normalizedCache: ModelsCache = {
|
|
92
113
|
...cache,
|
|
114
|
+
catalogMode: normalizeCatalogMode(cache.catalogMode) ?? 'full',
|
|
93
115
|
timestamp: normalizeTimestampForSave(cache.timestamp),
|
|
94
116
|
};
|
|
95
117
|
await writeFile(cachePath, JSON.stringify(normalizedCache, null, 2));
|
|
@@ -4,25 +4,30 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { fetchUserModels } from '../client.js';
|
|
7
|
-
import { mapOpenRouterModels } from './mapper.js';
|
|
8
7
|
import { sdkModelToOpenRouterModel } from '../normalizers.js';
|
|
9
|
-
import {
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
10
11
|
import type { ExtensionContext } from '@mariozechner/pi-coding-agent';
|
|
12
|
+
import { loadCache, saveCache } from './cache.js';
|
|
13
|
+
import { mapOpenRouterModels } from './mapper.js';
|
|
14
|
+
import { ROUTER_DEFINITIONS } from './types.js';
|
|
11
15
|
import type {
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
ActiveCatalogState,
|
|
17
|
+
CatalogMode,
|
|
14
18
|
ModelsCache,
|
|
15
19
|
OpenRouterModel,
|
|
20
|
+
PiModelConfig,
|
|
16
21
|
SkipReason,
|
|
22
|
+
SyncResult,
|
|
17
23
|
} from './types.js';
|
|
18
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
19
|
-
import { ROUTER_DEFINITIONS } from './types.js';
|
|
20
|
-
import { join } from 'node:path';
|
|
21
|
-
import { homedir } from 'node:os';
|
|
22
24
|
|
|
23
25
|
// Store the current sync state for status display.
|
|
24
26
|
let currentSyncState: SyncResult | null = null;
|
|
25
27
|
|
|
28
|
+
// Store the catalog currently registered with Pi.
|
|
29
|
+
let currentActiveCatalogState: ActiveCatalogState | null = null;
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
32
|
* Check if model sync is enabled via user config.
|
|
28
33
|
* Default is true (sync enabled) if config is not set.
|
|
@@ -50,7 +55,7 @@ export function isSyncEnabled(): boolean {
|
|
|
50
55
|
* Set the current sync state.
|
|
51
56
|
* Called after each sync operation.
|
|
52
57
|
*/
|
|
53
|
-
export function setSyncState(result: SyncResult): void {
|
|
58
|
+
export function setSyncState(result: SyncResult | null): void {
|
|
54
59
|
currentSyncState = result;
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -61,6 +66,20 @@ export function getSyncState(): SyncResult | null {
|
|
|
61
66
|
return currentSyncState;
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Set the currently active catalog state.
|
|
71
|
+
*/
|
|
72
|
+
export function setActiveCatalogState(state: ActiveCatalogState | null): void {
|
|
73
|
+
currentActiveCatalogState = state;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the currently active catalog state.
|
|
78
|
+
*/
|
|
79
|
+
export function getActiveCatalogState(): ActiveCatalogState | null {
|
|
80
|
+
return currentActiveCatalogState;
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
/**
|
|
65
84
|
* Register mapped models with Pi's OpenRouter provider.
|
|
66
85
|
*
|
|
@@ -80,50 +99,109 @@ export async function registerModelsWithProvider(
|
|
|
80
99
|
});
|
|
81
100
|
}
|
|
82
101
|
|
|
102
|
+
function getBuiltinRouterDefinitionsForCatalogMode(mode: CatalogMode) {
|
|
103
|
+
return ROUTER_DEFINITIONS.filter((router) => mode === 'full' || router.id === 'openrouter/free');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns true when an OpenRouter model ID is an explicit free variant.
|
|
108
|
+
*/
|
|
109
|
+
export function isExplicitFreeModelId(id: string): boolean {
|
|
110
|
+
return id.endsWith(':free');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Filter raw models for the requested catalog mode before mapping/validation.
|
|
115
|
+
*/
|
|
116
|
+
export function filterModelsForCatalogMode<T extends { id?: string }>(
|
|
117
|
+
models: T[],
|
|
118
|
+
mode: CatalogMode,
|
|
119
|
+
): T[] {
|
|
120
|
+
if (mode === 'full') {
|
|
121
|
+
return [...models];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return models.filter((model) => typeof model.id === 'string' && isExplicitFreeModelId(model.id));
|
|
125
|
+
}
|
|
126
|
+
|
|
83
127
|
/**
|
|
84
128
|
* Built-in router models derived from ROUTER_DEFINITIONS in types.ts.
|
|
85
129
|
* This ensures sync with mapper.ts skip logic.
|
|
86
130
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}));
|
|
131
|
+
export function getBuiltinRoutersForCatalogMode(mode: CatalogMode): PiModelConfig[] {
|
|
132
|
+
return getBuiltinRouterDefinitionsForCatalogMode(mode).map((router) => ({
|
|
133
|
+
id: router.id,
|
|
134
|
+
name: router.name,
|
|
135
|
+
reasoning: router.reasoning,
|
|
136
|
+
input: [...router.input],
|
|
137
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
138
|
+
contextWindow: router.contextLength,
|
|
139
|
+
maxTokens: router.maxTokens,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
97
142
|
|
|
98
143
|
/**
|
|
99
144
|
* Add built-in router aliases exactly once to a synced user catalog.
|
|
100
|
-
*
|
|
101
|
-
* The OpenRouter provider registration replaces the built-in list, so router aliases are the
|
|
102
|
-
* only built-ins we intentionally preserve in this cleanup pass.
|
|
103
145
|
*/
|
|
104
|
-
export function includeBuiltinRouterModels(
|
|
146
|
+
export function includeBuiltinRouterModels(
|
|
147
|
+
configs: PiModelConfig[],
|
|
148
|
+
mode: CatalogMode = 'full',
|
|
149
|
+
): PiModelConfig[] {
|
|
105
150
|
const seen = new Set(configs.map((config) => config.id));
|
|
106
|
-
const routersToAdd =
|
|
151
|
+
const routersToAdd = getBuiltinRoutersForCatalogMode(mode).filter(
|
|
152
|
+
(router) => !seen.has(router.id),
|
|
153
|
+
);
|
|
107
154
|
return [...configs, ...routersToAdd];
|
|
108
155
|
}
|
|
109
156
|
|
|
110
157
|
/**
|
|
111
158
|
* Convert router definitions to OpenRouterModel format for cache storage.
|
|
112
159
|
*/
|
|
113
|
-
function getRouterCacheModels(): OpenRouterModel[] {
|
|
114
|
-
return
|
|
115
|
-
id:
|
|
116
|
-
name:
|
|
160
|
+
function getRouterCacheModels(mode: CatalogMode): OpenRouterModel[] {
|
|
161
|
+
return getBuiltinRouterDefinitionsForCatalogMode(mode).map((router) => ({
|
|
162
|
+
id: router.id,
|
|
163
|
+
name: router.name,
|
|
117
164
|
architecture: {
|
|
118
|
-
input_modalities: [...
|
|
119
|
-
output_modalities: [...
|
|
165
|
+
input_modalities: [...router.input],
|
|
166
|
+
output_modalities: [...router.output],
|
|
120
167
|
},
|
|
121
|
-
context_length:
|
|
168
|
+
context_length: router.contextLength,
|
|
122
169
|
pricing: { prompt: '0', completion: '0' },
|
|
123
|
-
supported_parameters:
|
|
170
|
+
supported_parameters: router.reasoning ? ['reasoning'] : [],
|
|
124
171
|
}));
|
|
125
172
|
}
|
|
126
173
|
|
|
174
|
+
function getEffectiveSkippedCount(skipped: number, skippedDetails: SkipReason[]): number {
|
|
175
|
+
return skippedDetails.length > 0 ? skippedDetails.length : skipped;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildActiveCatalogState(args: {
|
|
179
|
+
mode: CatalogMode;
|
|
180
|
+
registeredModelIds: string[];
|
|
181
|
+
registeredCount: number;
|
|
182
|
+
skippedCount: number;
|
|
183
|
+
skippedDetails: SkipReason[];
|
|
184
|
+
source: ActiveCatalogState['source'];
|
|
185
|
+
cacheAgeMs: number;
|
|
186
|
+
}): ActiveCatalogState {
|
|
187
|
+
return {
|
|
188
|
+
mode: args.mode,
|
|
189
|
+
registeredModelIds: args.registeredModelIds,
|
|
190
|
+
registeredCount: args.registeredCount,
|
|
191
|
+
skippedCount: args.skippedCount,
|
|
192
|
+
skippedDetails: args.skippedDetails,
|
|
193
|
+
source: args.source,
|
|
194
|
+
cacheAgeMs: args.cacheAgeMs,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sliceSkipReasons(reasons: SkipReason[], maxResults?: number): SkipReason[] {
|
|
199
|
+
if (maxResults === undefined) {
|
|
200
|
+
return reasons;
|
|
201
|
+
}
|
|
202
|
+
return reasons.slice(0, maxResults);
|
|
203
|
+
}
|
|
204
|
+
|
|
127
205
|
/**
|
|
128
206
|
* Execute a full sync operation:
|
|
129
207
|
* 1. Fetch models from OpenRouter API
|
|
@@ -134,42 +212,87 @@ function getRouterCacheModels(): OpenRouterModel[] {
|
|
|
134
212
|
* On API failure, falls back to cached models if available.
|
|
135
213
|
*
|
|
136
214
|
* @param ctx - Extension context for provider registration
|
|
215
|
+
* @param requestedMode - full catalog or free-only catalog filter
|
|
137
216
|
* @returns SyncResult with details of the operation
|
|
138
217
|
*/
|
|
139
|
-
export async function syncModels(
|
|
218
|
+
export async function syncModels(
|
|
219
|
+
ctx: ExtensionContext,
|
|
220
|
+
requestedMode: CatalogMode = 'full',
|
|
221
|
+
): Promise<SyncResult> {
|
|
140
222
|
// Note: Config check (isSyncEnabled) is now handled at the command level
|
|
141
223
|
// in index.ts. This allows tests to run without file system dependencies.
|
|
142
224
|
|
|
143
225
|
// Attempt 1: Fetch from API
|
|
144
226
|
try {
|
|
145
227
|
const response = await fetchUserModels();
|
|
146
|
-
const
|
|
228
|
+
const filteredApiModels = filterModelsForCatalogMode(response.data, requestedMode);
|
|
229
|
+
|
|
230
|
+
if (requestedMode === 'free-only' && filteredApiModels.length === 0) {
|
|
231
|
+
const activeState = getActiveCatalogState();
|
|
232
|
+
const result: SyncResult = {
|
|
233
|
+
success: false,
|
|
234
|
+
outcome: 'no-change',
|
|
235
|
+
requestedMode,
|
|
236
|
+
catalogMode: activeState?.mode ?? null,
|
|
237
|
+
registeredCount: 0,
|
|
238
|
+
skippedCount: 0,
|
|
239
|
+
skippedDetails: [],
|
|
240
|
+
source: 'api',
|
|
241
|
+
cacheUpdated: false,
|
|
242
|
+
cacheAgeMs: activeState?.cacheAgeMs ?? null,
|
|
243
|
+
error: null,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
setSyncState(result);
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const { configs, skipped, skippedDetails } = await mapOpenRouterModels(filteredApiModels);
|
|
147
251
|
|
|
148
252
|
// Add built-in router aliases that don't appear in /models/user endpoint.
|
|
149
|
-
const configsWithRouters = includeBuiltinRouterModels(configs);
|
|
253
|
+
const configsWithRouters = includeBuiltinRouterModels(configs, requestedMode);
|
|
150
254
|
|
|
151
255
|
// Register with Pi's OpenRouter provider
|
|
152
|
-
await registerModelsWithProvider(
|
|
256
|
+
await registerModelsWithProvider(ctx, configsWithRouters);
|
|
153
257
|
|
|
154
|
-
// Convert SDK Model[] to OpenRouterModel[] for cache storage
|
|
155
|
-
|
|
258
|
+
// Convert SDK Model[] to OpenRouterModel[] for cache storage.
|
|
259
|
+
// Store mode-shaped raw models so free-only cache cannot resurrect paid variants.
|
|
260
|
+
const cacheModels: OpenRouterModel[] = filteredApiModels.map((model) =>
|
|
261
|
+
sdkModelToOpenRouterModel(model),
|
|
262
|
+
);
|
|
156
263
|
|
|
157
|
-
// Update last-good cache (include routers and skip details)
|
|
264
|
+
// Update last-good cache (include mode-shaped routers and skip details)
|
|
158
265
|
const cache: ModelsCache = {
|
|
159
|
-
|
|
160
|
-
|
|
266
|
+
catalogMode: requestedMode,
|
|
267
|
+
models: [...cacheModels, ...getRouterCacheModels(requestedMode)],
|
|
268
|
+
skippedDetails,
|
|
161
269
|
timestamp: Date.now(),
|
|
162
270
|
};
|
|
163
271
|
await saveCache(cache);
|
|
164
272
|
|
|
273
|
+
const skippedCount = getEffectiveSkippedCount(skipped, skippedDetails);
|
|
274
|
+
const activeState = buildActiveCatalogState({
|
|
275
|
+
mode: requestedMode,
|
|
276
|
+
registeredModelIds: configsWithRouters.map((config) => config.id),
|
|
277
|
+
registeredCount: configsWithRouters.length,
|
|
278
|
+
skippedCount,
|
|
279
|
+
skippedDetails,
|
|
280
|
+
source: 'api',
|
|
281
|
+
cacheAgeMs: 0,
|
|
282
|
+
});
|
|
283
|
+
setActiveCatalogState(activeState);
|
|
284
|
+
|
|
165
285
|
const result: SyncResult = {
|
|
166
286
|
success: true,
|
|
287
|
+
outcome: 'synced',
|
|
288
|
+
requestedMode,
|
|
289
|
+
catalogMode: requestedMode,
|
|
167
290
|
registeredCount: configsWithRouters.length,
|
|
168
|
-
skippedCount
|
|
169
|
-
skippedDetails
|
|
291
|
+
skippedCount,
|
|
292
|
+
skippedDetails,
|
|
170
293
|
source: 'api',
|
|
171
294
|
cacheUpdated: true,
|
|
172
|
-
cacheAgeMs: 0,
|
|
295
|
+
cacheAgeMs: 0,
|
|
173
296
|
error: null,
|
|
174
297
|
};
|
|
175
298
|
|
|
@@ -178,26 +301,40 @@ export async function syncModels(_ctx: ExtensionContext): Promise<SyncResult> {
|
|
|
178
301
|
} catch (error) {
|
|
179
302
|
// API failed - try cache fallback
|
|
180
303
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
181
|
-
|
|
182
304
|
const cache = await loadCache();
|
|
183
305
|
|
|
184
306
|
if (cache) {
|
|
185
|
-
// Attempt 2: Use cached models
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const cachedSkipDetails = cache.skippedDetails
|
|
307
|
+
// Attempt 2: Use cached models with the cache's persisted catalog mode.
|
|
308
|
+
const filteredCacheModels = filterModelsForCatalogMode(cache.models, cache.catalogMode);
|
|
309
|
+
const { configs, skipped, skippedDetails } = await mapOpenRouterModels(filteredCacheModels);
|
|
310
|
+
const configsWithRouters = includeBuiltinRouterModels(configs, cache.catalogMode);
|
|
311
|
+
|
|
312
|
+
await registerModelsWithProvider(ctx, configsWithRouters);
|
|
313
|
+
|
|
314
|
+
const cachedSkipDetails = cache.skippedDetails ?? skippedDetails;
|
|
315
|
+
const skippedCount = getEffectiveSkippedCount(skipped, cachedSkipDetails);
|
|
316
|
+
const cacheAgeMs = Math.max(0, Date.now() - cache.timestamp);
|
|
317
|
+
const activeState = buildActiveCatalogState({
|
|
318
|
+
mode: cache.catalogMode,
|
|
319
|
+
registeredModelIds: configsWithRouters.map((config) => config.id),
|
|
320
|
+
registeredCount: configsWithRouters.length,
|
|
321
|
+
skippedCount,
|
|
322
|
+
skippedDetails: cachedSkipDetails,
|
|
323
|
+
source: 'cache',
|
|
324
|
+
cacheAgeMs,
|
|
325
|
+
});
|
|
326
|
+
setActiveCatalogState(activeState);
|
|
193
327
|
|
|
194
328
|
const result: SyncResult = {
|
|
195
329
|
success: false,
|
|
330
|
+
outcome: 'cache-fallback',
|
|
331
|
+
requestedMode,
|
|
332
|
+
catalogMode: cache.catalogMode,
|
|
196
333
|
registeredCount: configsWithRouters.length,
|
|
197
|
-
skippedCount
|
|
334
|
+
skippedCount,
|
|
198
335
|
source: 'cache',
|
|
199
336
|
cacheUpdated: false,
|
|
200
|
-
cacheAgeMs
|
|
337
|
+
cacheAgeMs,
|
|
201
338
|
error: errorMsg,
|
|
202
339
|
skippedDetails: cachedSkipDetails,
|
|
203
340
|
};
|
|
@@ -206,14 +343,20 @@ export async function syncModels(_ctx: ExtensionContext): Promise<SyncResult> {
|
|
|
206
343
|
return result;
|
|
207
344
|
}
|
|
208
345
|
|
|
209
|
-
// Attempt 3: No cache available - complete failure
|
|
346
|
+
// Attempt 3: No cache available - complete failure.
|
|
347
|
+
// Preserve any previously active in-memory catalog.
|
|
348
|
+
const activeState = getActiveCatalogState();
|
|
210
349
|
const result: SyncResult = {
|
|
211
350
|
success: false,
|
|
351
|
+
outcome: 'unavailable',
|
|
352
|
+
requestedMode,
|
|
353
|
+
catalogMode: activeState?.mode ?? null,
|
|
212
354
|
registeredCount: 0,
|
|
213
355
|
skippedCount: 0,
|
|
356
|
+
skippedDetails: [],
|
|
214
357
|
source: 'none',
|
|
215
358
|
cacheUpdated: false,
|
|
216
|
-
cacheAgeMs: null,
|
|
359
|
+
cacheAgeMs: activeState?.cacheAgeMs ?? null,
|
|
217
360
|
error: errorMsg,
|
|
218
361
|
};
|
|
219
362
|
|
|
@@ -226,8 +369,14 @@ export async function syncModels(_ctx: ExtensionContext): Promise<SyncResult> {
|
|
|
226
369
|
* Get a human-readable status string for the current sync state.
|
|
227
370
|
*/
|
|
228
371
|
export function getStatusText(): string {
|
|
229
|
-
const
|
|
372
|
+
const activeState = getActiveCatalogState();
|
|
230
373
|
|
|
374
|
+
if (activeState) {
|
|
375
|
+
const status = activeState.source === 'cache' ? 'cached' : 'healthy';
|
|
376
|
+
return `OpenRouter models: ${status} (${activeState.registeredCount} registered)`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const state = getSyncState();
|
|
231
380
|
if (!state) {
|
|
232
381
|
return 'OpenRouter models: not synced';
|
|
233
382
|
}
|
|
@@ -247,12 +396,12 @@ export function getStatusText(): string {
|
|
|
247
396
|
|
|
248
397
|
/**
|
|
249
398
|
* Check if models are currently available (synced or cached).
|
|
250
|
-
* Checks in-memory state first, then falls back to cache file on disk.
|
|
399
|
+
* Checks in-memory active state first, then falls back to cache file on disk.
|
|
251
400
|
*/
|
|
252
401
|
export async function areModelsAvailable(): Promise<boolean> {
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
255
|
-
return
|
|
402
|
+
const activeState = getActiveCatalogState();
|
|
403
|
+
if (activeState) {
|
|
404
|
+
return activeState.registeredCount > 0;
|
|
256
405
|
}
|
|
257
406
|
|
|
258
407
|
// Check cache file on disk
|
|
@@ -261,17 +410,19 @@ export async function areModelsAvailable(): Promise<boolean> {
|
|
|
261
410
|
}
|
|
262
411
|
|
|
263
412
|
/**
|
|
264
|
-
* Get skip reasons from the
|
|
413
|
+
* Get skip reasons from the active catalog, last sync state, or cache.
|
|
265
414
|
* Note: For models-status (synchronous), we can't await here.
|
|
266
415
|
* For async usage, use getSkipReasonsAsync instead.
|
|
267
416
|
*/
|
|
268
|
-
export function getSkipReasons(maxResults
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
417
|
+
export function getSkipReasons(maxResults?: number): SkipReason[] {
|
|
418
|
+
const activeState = getActiveCatalogState();
|
|
419
|
+
if (activeState?.skippedDetails.length) {
|
|
420
|
+
return sliceSkipReasons(activeState.skippedDetails, maxResults);
|
|
421
|
+
}
|
|
271
422
|
|
|
272
|
-
|
|
273
|
-
if (state
|
|
274
|
-
return state.skippedDetails
|
|
423
|
+
const state = getSyncState();
|
|
424
|
+
if (state?.skippedDetails?.length) {
|
|
425
|
+
return sliceSkipReasons(state.skippedDetails, maxResults);
|
|
275
426
|
}
|
|
276
427
|
|
|
277
428
|
return [];
|
|
@@ -280,18 +431,20 @@ export function getSkipReasons(maxResults: number = 10): SkipReason[] {
|
|
|
280
431
|
/**
|
|
281
432
|
* Async version of getSkipReasons that reads from cache if needed.
|
|
282
433
|
*/
|
|
283
|
-
export async function getSkipReasonsAsync(maxResults
|
|
284
|
-
const
|
|
434
|
+
export async function getSkipReasonsAsync(maxResults?: number): Promise<SkipReason[]> {
|
|
435
|
+
const activeState = getActiveCatalogState();
|
|
436
|
+
if (activeState?.skippedDetails.length) {
|
|
437
|
+
return sliceSkipReasons(activeState.skippedDetails, maxResults);
|
|
438
|
+
}
|
|
285
439
|
|
|
286
|
-
|
|
287
|
-
if (state?.skippedDetails
|
|
288
|
-
return state.skippedDetails
|
|
440
|
+
const state = getSyncState();
|
|
441
|
+
if (state?.skippedDetails?.length) {
|
|
442
|
+
return sliceSkipReasons(state.skippedDetails, maxResults);
|
|
289
443
|
}
|
|
290
444
|
|
|
291
|
-
// Fall back to cache if not available in state
|
|
292
445
|
const cache = await loadCache();
|
|
293
|
-
if (cache?.skippedDetails
|
|
294
|
-
return cache.skippedDetails
|
|
446
|
+
if (cache?.skippedDetails?.length) {
|
|
447
|
+
return sliceSkipReasons(cache.skippedDetails, maxResults);
|
|
295
448
|
}
|
|
296
449
|
|
|
297
450
|
return [];
|
|
@@ -66,23 +66,56 @@ export interface PiModelConfig {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* Registered catalog mode.
|
|
70
|
+
*/
|
|
71
|
+
export type CatalogMode = 'full' | 'free-only';
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Source for the currently active OpenRouter catalog.
|
|
75
|
+
*/
|
|
76
|
+
export type CatalogSource = 'api' | 'cache';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Rich classification for the last sync attempt.
|
|
80
|
+
*/
|
|
81
|
+
export type SyncOutcome = 'synced' | 'cache-fallback' | 'no-change' | 'unavailable';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Result of a sync operation.
|
|
70
85
|
*/
|
|
71
86
|
export interface SyncResult {
|
|
72
87
|
success: boolean;
|
|
88
|
+
outcome: SyncOutcome;
|
|
89
|
+
requestedMode: CatalogMode;
|
|
90
|
+
catalogMode: CatalogMode | null;
|
|
73
91
|
registeredCount: number;
|
|
74
92
|
skippedCount: number;
|
|
75
93
|
skippedDetails?: SkipReason[]; // Track why models were skipped
|
|
76
|
-
source:
|
|
94
|
+
source: CatalogSource | 'none';
|
|
77
95
|
cacheUpdated: boolean;
|
|
78
96
|
cacheAgeMs: number | null;
|
|
79
97
|
error: string | null;
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
/**
|
|
83
|
-
*
|
|
101
|
+
* Snapshot of the catalog currently registered with Pi.
|
|
102
|
+
*/
|
|
103
|
+
export interface ActiveCatalogState {
|
|
104
|
+
mode: CatalogMode;
|
|
105
|
+
registeredModelIds?: string[];
|
|
106
|
+
registeredCount: number;
|
|
107
|
+
skippedCount: number;
|
|
108
|
+
skippedDetails: SkipReason[];
|
|
109
|
+
source: CatalogSource;
|
|
110
|
+
cacheAgeMs: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Cache file structure - using our OpenRouterModel type for consistency.
|
|
115
|
+
* Stores the raw models for the currently active catalog mode.
|
|
84
116
|
*/
|
|
85
117
|
export interface ModelsCache {
|
|
118
|
+
catalogMode: CatalogMode;
|
|
86
119
|
models: OpenRouterModel[];
|
|
87
120
|
skippedDetails?: SkipReason[]; // New field for tracking skip reasons
|
|
88
121
|
timestamp: number;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robhowley/pi-openrouter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Live OpenRouter spend/account TUI overlays, user-scoped model sync, api key management, and session tagging for Pi.",
|
|
5
|
+
"description": "Live OpenRouter spend/account TUI overlays, user-scoped or free-only model sync, api key management, and session tagging for Pi.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"files": [
|
|
8
8
|
"extensions",
|