@robhowley/pi-openrouter 0.11.1 → 0.12.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.
@@ -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
- if (!isValidCacheTimestamp(parsed.timestamp)) {
84
+ const catalogMode = normalizeCatalogMode(parsed.catalogMode);
85
+ if (!catalogMode || !isValidCacheTimestamp(parsed.timestamp)) {
75
86
  return null;
76
87
  }
77
88
 
78
- return parsed;
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 { loadCache, saveCache } from './cache.js';
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
- SyncResult,
13
- PiModelConfig,
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
- const BUILTIN_ROUTER_MODELS: PiModelConfig[] = ROUTER_DEFINITIONS.map((r) => ({
89
- id: r.id,
90
- name: r.name,
91
- reasoning: r.reasoning,
92
- input: [...r.input],
93
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
94
- contextWindow: r.contextLength,
95
- maxTokens: r.maxTokens,
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(configs: PiModelConfig[]): PiModelConfig[] {
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 = BUILTIN_ROUTER_MODELS.filter((router) => !seen.has(router.id));
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 ROUTER_DEFINITIONS.map((r) => ({
115
- id: r.id,
116
- name: r.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: [...r.input],
119
- output_modalities: [...r.output],
165
+ input_modalities: [...router.input],
166
+ output_modalities: [...router.output],
120
167
  },
121
- context_length: r.contextLength,
168
+ context_length: router.contextLength,
122
169
  pricing: { prompt: '0', completion: '0' },
123
- supported_parameters: r.reasoning ? ['reasoning'] : [],
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(_ctx: ExtensionContext): Promise<SyncResult> {
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 { configs, skipped, skippedDetails } = await mapOpenRouterModels(response.data);
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(_ctx, configsWithRouters);
256
+ await registerModelsWithProvider(ctx, configsWithRouters);
153
257
 
154
- // Convert SDK Model[] to OpenRouterModel[] for cache storage
155
- const cacheModels: OpenRouterModel[] = response.data.map(sdkModelToOpenRouterModel);
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
- models: [...cacheModels, ...getRouterCacheModels()],
160
- skippedDetails: skippedDetails,
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: skipped,
169
- skippedDetails: skippedDetails,
291
+ skippedCount,
292
+ skippedDetails,
170
293
  source: 'api',
171
294
  cacheUpdated: true,
172
- cacheAgeMs: 0, // Cache was just updated
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 { configs, skipped } = await mapOpenRouterModels(cache.models);
187
- const configsWithRouters = includeBuiltinRouterModels(configs);
188
-
189
- await registerModelsWithProvider(_ctx, configsWithRouters);
190
-
191
- // Use cached skip details if available
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: skipped,
334
+ skippedCount,
198
335
  source: 'cache',
199
336
  cacheUpdated: false,
200
- cacheAgeMs: Date.now() - cache.timestamp,
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 state = getSyncState();
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 state = getSyncState();
254
- if (state) {
255
- return state.registeredCount > 0 || state.source === 'cache';
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 current sync state or cache.
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: number = 10): SkipReason[] {
269
- const state = getSyncState();
270
- if (!state) return [];
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
- // Prefer in-memory state
273
- if (state.skippedDetails && state.skippedDetails.length > 0) {
274
- return state.skippedDetails.slice(0, maxResults);
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: number = 10): Promise<SkipReason[]> {
284
- const state = getSyncState();
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
- // First check in-memory state
287
- if (state?.skippedDetails && state.skippedDetails.length > 0) {
288
- return state.skippedDetails.slice(0, maxResults);
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 && cache.skippedDetails.length > 0) {
294
- return cache.skippedDetails.slice(0, maxResults);
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
- * Result of a sync operation
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: 'api' | 'cache' | 'none';
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
- * Cache file structure - using our OpenRouterModel type for consistency
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.11.1",
3
+ "version": "0.12.1",
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",