@robhowley/pi-openrouter 0.8.1 → 0.8.3

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.
@@ -132,3 +132,30 @@ export function createPiModelConfig(overrides?: Partial<PiModelConfig>): PiModel
132
132
  ...overrides,
133
133
  };
134
134
  }
135
+
136
+ // =============================================================================
137
+ // LocalUsageEvent Fixtures
138
+ // =============================================================================
139
+
140
+ import type { LocalUsageEvent } from '../types.js';
141
+
142
+ /**
143
+ * Creates LocalUsageEvent objects for testing local usage aggregation.
144
+ * Use daysAgo=0 for today, daysAgo=3 for within 7d window, etc.
145
+ */
146
+ export function createLocalEvents(
147
+ events: Array<{ daysAgo: number; cost: number; model?: string; provider?: string }>,
148
+ ): LocalUsageEvent[] {
149
+ return events.map((e, i) => ({
150
+ id: `local-${i}`,
151
+ sessionId: 'test-session',
152
+ generationId: `gen-${i}`,
153
+ completedAt: `${createTestDate(e.daysAgo)}T12:00:00.000Z`,
154
+ requests: 1,
155
+ model: e.model ?? 'gpt-4',
156
+ provider: e.provider ?? 'openai',
157
+ promptTokens: 100,
158
+ completionTokens: 50,
159
+ cost: e.cost,
160
+ }));
161
+ }
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { aggregateUsage } from '../format.js';
3
3
  import { renderSpendSparkline } from '../chart.js';
4
- import { createTestDate, createActivityItem } from './fixtures.js';
4
+ import { createTestDate, createActivityItem, createLocalEvents } from './fixtures.js';
5
5
  import type { ActivityItem } from '@openrouter/sdk/models/index.js';
6
6
 
7
7
  describe('aggregateUsage', () => {
@@ -224,6 +224,54 @@ describe('aggregateUsage', () => {
224
224
  expect(result.byProvider[0]?.tokens.output).toBe(80); // 50 + 30
225
225
  expect(result.byProvider[0]?.requests).toBe(8); // 5 + 3
226
226
  });
227
+
228
+ it.each([
229
+ {
230
+ name: 'local today included in 7d model stats',
231
+ api: { daysAgo: 3, cost: 5, model: 'gpt-4' },
232
+ local: { daysAgo: 0, cost: 2.5, model: 'gpt-4' },
233
+ expectSpend7d: 7.5,
234
+ },
235
+ {
236
+ name: 'local outside 7d window excluded from 7d stats',
237
+ api: { daysAgo: 3, cost: 5, model: 'gpt-4' },
238
+ local: { daysAgo: 10, cost: 100, model: 'gpt-4' },
239
+ expectSpend7d: 5,
240
+ },
241
+ {
242
+ name: 'different models tracked separately',
243
+ api: { daysAgo: 1, cost: 5, model: 'gpt-4' },
244
+ local: { daysAgo: 0, cost: 3, model: 'claude-3' },
245
+ expectSpend7d: null,
246
+ expectModels: [
247
+ { name: 'gpt-4', spend7d: 5 },
248
+ { name: 'claude-3', spend7d: 3 },
249
+ ],
250
+ },
251
+ ])('$name', ({ api, local, expectSpend7d, expectModels }) => {
252
+ const credits = { totalUsage: 1000, totalCredits: 2000 };
253
+
254
+ const result = aggregateUsage(
255
+ credits,
256
+ [
257
+ createActivityItem({
258
+ date: createTestDate(api.daysAgo),
259
+ usage: api.cost,
260
+ model: api.model,
261
+ }),
262
+ ],
263
+ Date.now(),
264
+ createLocalEvents([local]),
265
+ );
266
+
267
+ if (expectModels) {
268
+ for (const { name, spend7d } of expectModels) {
269
+ expect(result.topModels.find((m) => m.name === name)?.spend7d).toBe(spend7d);
270
+ }
271
+ } else {
272
+ expect(result.topModels[0]?.spend7d).toBe(expectSpend7d);
273
+ }
274
+ });
227
275
  });
228
276
 
229
277
  describe('renderSpendSparkline', () => {
@@ -25,13 +25,10 @@ export function aggregateUsage(
25
25
  return d.date >= utcISODate(startOfWeek);
26
26
  });
27
27
 
28
- const todayData = analytics.filter((d) => {
29
- // API dates are YYYY-MM-DD in UTC; compare by UTC date boundary
30
- return d.date >= utcISODate(startOfDay);
31
- });
32
-
33
28
  const weekFromAnalytics = sumSpend(weekData);
34
- const todayFromAnalytics = sumSpend(todayData);
29
+ const todayFromAnalytics = analytics
30
+ .filter((d) => d.date >= utcISODate(startOfDay))
31
+ .reduce((sum, d) => sum + d.usage, 0);
35
32
  const month = credits.totalUsage;
36
33
 
37
34
  // Add local events to compute combined totals
@@ -65,8 +62,10 @@ export function aggregateUsage(
65
62
  const allData = [...analytics, ...localItems];
66
63
 
67
64
  // Build model stats for both 7d and 30d windows
68
- // Use allData (combined API + local) for 30d, weekData (combined) for 7d
69
- const modelStatsMap = buildModelStats(weekData, allData);
65
+ // Combine weekData with local events in the 7d window
66
+ const weekLocalItems = localItems.filter((e) => e.date >= utcISODate(startOfWeek));
67
+ const weekAllData = [...weekData, ...weekLocalItems];
68
+ const modelStatsMap = buildModelStats(weekAllData, allData);
70
69
  const topModels = Array.from(modelStatsMap.values())
71
70
  .sort((a, b) => b.spend30d - a.spend30d)
72
71
  .slice(0, 10);
@@ -100,7 +100,7 @@ export default async function (pi: ExtensionAPI) {
100
100
 
101
101
  if (cache?.models.length) {
102
102
  try {
103
- const { configs } = mapOpenRouterModels(cache.models);
103
+ const { configs } = await mapOpenRouterModels(cache.models);
104
104
 
105
105
  // Register models directly with Pi's OpenRouter provider
106
106
  pi.registerProvider('openrouter', {
@@ -4,9 +4,9 @@ import { createValidModel } from '../../__tests__/fixtures.js';
4
4
  import type { OpenRouterModel, MapResult } from '../types.js';
5
5
 
6
6
  describe('mapOpenRouterModel', () => {
7
- it('should map a valid model correctly', () => {
7
+ it('should map a valid model correctly', async () => {
8
8
  const model = createValidModel();
9
- const result = mapOpenRouterModel(model);
9
+ const result = await mapOpenRouterModel(model);
10
10
 
11
11
  expect(result).not.toBeNull();
12
12
  expect(result!.id).toBe('test/model');
@@ -17,11 +17,11 @@ describe('mapOpenRouterModel', () => {
17
17
  expect(result!.cost.output).toBe(1.5); // 0.0000015 * 1M
18
18
  });
19
19
 
20
- it('should use name fallback to id when name missing', () => {
20
+ it('should use name fallback to id when name missing', async () => {
21
21
  const model = createValidModel();
22
22
  // @ts-expect-error: intentionally setting name to undefined for test
23
23
  model.name = undefined;
24
- const result = mapOpenRouterModel(model);
24
+ const result = await mapOpenRouterModel(model);
25
25
 
26
26
  expect(result!.name).toBe('test/model');
27
27
  });
@@ -46,78 +46,78 @@ describe('mapOpenRouterModel', () => {
46
46
  ];
47
47
 
48
48
  skipCases.forEach(({ name, overrides }) => {
49
- it(`should skip model with ${name}`, () => {
49
+ it(`should skip model with ${name}`, async () => {
50
50
  const model = createValidModel();
51
51
  if (typeof overrides === 'function') {
52
52
  overrides(model);
53
53
  } else {
54
54
  Object.assign(model, overrides);
55
55
  }
56
- expect(mapOpenRouterModel(model)).toBeNull();
56
+ expect(await mapOpenRouterModel(model)).toBeNull();
57
57
  });
58
58
  });
59
59
  });
60
60
 
61
61
  describe('reasoning detection', () => {
62
- it("should detect reasoning from 'reasoning' parameter", () => {
62
+ it("should detect reasoning from 'reasoning' parameter", async () => {
63
63
  const model = createValidModel({
64
64
  supported_parameters: ['temperature', 'reasoning', 'max_tokens'],
65
65
  });
66
- expect(mapOpenRouterModel(model)!.reasoning).toBe(true);
66
+ expect((await mapOpenRouterModel(model))!.reasoning).toBe(true);
67
67
  });
68
68
 
69
- it("should detect reasoning from 'include_reasoning' parameter", () => {
69
+ it("should detect reasoning from 'include_reasoning' parameter", async () => {
70
70
  const model = createValidModel({
71
71
  supported_parameters: ['include_reasoning'],
72
72
  });
73
- expect(mapOpenRouterModel(model)!.reasoning).toBe(true);
73
+ expect((await mapOpenRouterModel(model))!.reasoning).toBe(true);
74
74
  });
75
75
 
76
- it('should not detect reasoning when neither parameter present', () => {
76
+ it('should not detect reasoning when neither parameter present', async () => {
77
77
  const model = createValidModel({
78
78
  supported_parameters: ['temperature', 'max_tokens'],
79
79
  });
80
- expect(mapOpenRouterModel(model)!.reasoning).toBe(false);
80
+ expect((await mapOpenRouterModel(model))!.reasoning).toBe(false);
81
81
  });
82
82
 
83
- it('should handle missing supported_parameters', () => {
83
+ it('should handle missing supported_parameters', async () => {
84
84
  const model = createValidModel({ supported_parameters: [] as any });
85
- expect(mapOpenRouterModel(model)!.reasoning).toBe(false);
85
+ expect((await mapOpenRouterModel(model))!.reasoning).toBe(false);
86
86
  });
87
87
  });
88
88
 
89
89
  describe('input modality detection', () => {
90
- it('should detect image support from input_modalities', () => {
90
+ it('should detect image support from input_modalities', async () => {
91
91
  const model = createValidModel({
92
92
  architecture: { input_modalities: ['text', 'image'] } as any,
93
93
  });
94
- expect(mapOpenRouterModel(model)!.input).toEqual(['text', 'image']);
94
+ expect((await mapOpenRouterModel(model))!.input).toEqual(['text', 'image']);
95
95
  });
96
96
 
97
- it('should default to text-only without image in modalities', () => {
97
+ it('should default to text-only without image in modalities', async () => {
98
98
  const model = createValidModel({
99
99
  architecture: { input_modalities: ['text'] } as any,
100
100
  });
101
- expect(mapOpenRouterModel(model)!.input).toEqual(['text']);
101
+ expect((await mapOpenRouterModel(model))!.input).toEqual(['text']);
102
102
  });
103
103
 
104
- it('should default to text-only when architecture missing', () => {
104
+ it('should default to text-only when architecture missing', async () => {
105
105
  const model = createValidModel({ architecture: null as any });
106
- expect(mapOpenRouterModel(model)!.input).toEqual(['text']);
106
+ expect((await mapOpenRouterModel(model))!.input).toEqual(['text']);
107
107
  });
108
108
  });
109
109
 
110
110
  describe('cost calculation', () => {
111
- it('should convert per-token to per-1M-tokens', () => {
111
+ it('should convert per-token to per-1M-tokens', async () => {
112
112
  const model = createValidModel({
113
113
  pricing: { prompt: '0.000002', completion: '0.000006' },
114
114
  });
115
- const result = mapOpenRouterModel(model);
115
+ const result = await mapOpenRouterModel(model);
116
116
  expect(result!.cost.input).toBe(2.0); // 0.000002 * 1,000,000
117
117
  expect(result!.cost.output).toBe(6.0); // 0.000006 * 1,000,000
118
118
  });
119
119
 
120
- it('should handle cache pricing when present', () => {
120
+ it('should handle cache pricing when present', async () => {
121
121
  const model = createValidModel({
122
122
  pricing: {
123
123
  prompt: '0.000001',
@@ -126,71 +126,90 @@ describe('mapOpenRouterModel', () => {
126
126
  input_cache_write: '0.000001',
127
127
  },
128
128
  });
129
- const result = mapOpenRouterModel(model);
129
+ const result = await mapOpenRouterModel(model);
130
130
  expect(result!.cost.cacheRead).toBe(0.5);
131
131
  expect(result!.cost.cacheWrite).toBe(1.0);
132
132
  });
133
133
 
134
- it('should default cache pricing to 0 when missing', () => {
134
+ it('should default cache pricing to 0 when missing', async () => {
135
135
  const model = createValidModel({
136
136
  pricing: { prompt: '0.000001', completion: '0.000003' },
137
137
  });
138
- const result = mapOpenRouterModel(model);
138
+ const result = await mapOpenRouterModel(model);
139
139
  expect(result!.cost.cacheRead).toBe(0);
140
140
  expect(result!.cost.cacheWrite).toBe(0);
141
141
  });
142
142
  });
143
143
 
144
144
  describe('contextWindow fallback', () => {
145
- it('should prefer top_provider.context_length', () => {
145
+ it('should prefer top_provider.context_length', async () => {
146
146
  const model = createValidModel({
147
147
  context_length: 8000,
148
148
  top_provider: { context_length: 128000 },
149
149
  });
150
- expect(mapOpenRouterModel(model)!.contextWindow).toBe(128000);
150
+ expect((await mapOpenRouterModel(model))!.contextWindow).toBe(128000);
151
151
  });
152
152
 
153
- it('should fall back to context_length', () => {
153
+ it('should fall back to context_length', async () => {
154
154
  const model = createValidModel({
155
155
  context_length: 32000,
156
156
  top_provider: null as any,
157
157
  });
158
- expect(mapOpenRouterModel(model)!.contextWindow).toBe(32000);
158
+ expect((await mapOpenRouterModel(model))!.contextWindow).toBe(32000);
159
159
  });
160
160
  });
161
161
 
162
162
  describe('maxTokens fallback', () => {
163
- it('should prefer top_provider.max_completion_tokens', () => {
163
+ it('should prefer top_provider.max_completion_tokens', async () => {
164
164
  const model = createValidModel({
165
165
  top_provider: { max_completion_tokens: 8192 },
166
166
  per_request_limits: { completion_tokens: 4096 },
167
167
  });
168
- expect(mapOpenRouterModel(model)!.maxTokens).toBe(8192);
168
+ expect((await mapOpenRouterModel(model))!.maxTokens).toBe(8192);
169
169
  });
170
170
 
171
- it('should fall back to per_request_limits.completion_tokens', () => {
171
+ it('should fall back to per_request_limits.completion_tokens', async () => {
172
172
  const model = createValidModel({
173
173
  per_request_limits: { completion_tokens: 8192 },
174
174
  });
175
- expect(mapOpenRouterModel(model)!.maxTokens).toBe(8192);
175
+ expect((await mapOpenRouterModel(model))!.maxTokens).toBe(8192);
176
176
  });
177
177
 
178
- it('should use default when neither present', () => {
178
+ it('should use default when neither present', async () => {
179
179
  const model = createValidModel();
180
- expect(mapOpenRouterModel(model)!.maxTokens).toBe(4096);
180
+ expect((await mapOpenRouterModel(model))!.maxTokens).toBe(4096);
181
+ });
182
+ });
183
+
184
+ describe('thinkingLevelMap', () => {
185
+ it('should include thinkingLevelMap when available from built-in registry', async () => {
186
+ // This test documents the expected behavior when the model exists
187
+ // in Pi's built-in registry. The actual lookup happens via dynamic import
188
+ // of @mariozechner/pi-ai or @earendil-works/pi-ai at runtime.
189
+ const model = createValidModel({
190
+ id: 'deepseek/deepseek-v4-pro',
191
+ supported_parameters: ['reasoning'],
192
+ });
193
+ const result = await mapOpenRouterModel(model);
194
+ expect(result).not.toBeNull();
195
+ // If pi-ai is available and has this model, thinkingLevelMap will be set
196
+ // Otherwise it will be undefined (which is valid)
197
+ expect(
198
+ result!.thinkingLevelMap === undefined || typeof result!.thinkingLevelMap === 'object',
199
+ ).toBe(true);
181
200
  });
182
201
  });
183
202
  });
184
203
 
185
204
  describe('mapOpenRouterModels', () => {
186
- it('should map multiple models and track skips', () => {
205
+ it('should map multiple models and track skips', async () => {
187
206
  const models: OpenRouterModel[] = [
188
207
  createValidModel({ id: 'model/valid-1', name: 'Valid 1' }),
189
208
  createValidModel({ id: '', name: 'Invalid (no id)' }), // will skip
190
209
  createValidModel({ id: 'model/valid-2', name: 'Valid 2' }),
191
210
  ];
192
211
 
193
- const result: MapResult = mapOpenRouterModels(models);
212
+ const result: MapResult = await mapOpenRouterModels(models);
194
213
 
195
214
  expect(result.configs).toHaveLength(2);
196
215
  expect(result.skipped).toBe(1);
@@ -198,13 +217,13 @@ describe('mapOpenRouterModels', () => {
198
217
  expect(result.configs[1]!.id).toBe('model/valid-2');
199
218
  });
200
219
 
201
- it('should handle empty array', () => {
202
- const result = mapOpenRouterModels([] as OpenRouterModel[]);
220
+ it('should handle empty array', async () => {
221
+ const result = await mapOpenRouterModels([] as OpenRouterModel[]);
203
222
  expect(result.configs).toHaveLength(0);
204
223
  expect(result.skipped).toBe(0);
205
224
  });
206
225
 
207
- it('should skip all invalid models', () => {
226
+ it('should skip all invalid models', async () => {
208
227
  const models: OpenRouterModel[] = [
209
228
  createValidModel({ id: '' }),
210
229
  createValidModel({ pricing: { prompt: '0.000001' } as any }),
@@ -214,7 +233,7 @@ describe('mapOpenRouterModels', () => {
214
233
  }),
215
234
  ];
216
235
 
217
- const result = mapOpenRouterModels(models);
236
+ const result = await mapOpenRouterModels(models);
218
237
  expect(result.configs).toHaveLength(0);
219
238
  expect(result.skipped).toBe(3);
220
239
  });
@@ -2,6 +2,58 @@ import type { OpenRouterModel, PiModelConfig, SkipReason, MapResult } from './ty
2
2
  import { ROUTER_ALIASES } from './types.js';
3
3
  import type { Model as SDKModel } from '@openrouter/sdk/models/index.js';
4
4
 
5
+ // Cache for built-in OpenRouter models from pi-ai
6
+ // Populated lazily on first access
7
+ let builtInOpenRouterModels: Map<string, PiModelConfig> | undefined;
8
+
9
+ /**
10
+ * Load built-in OpenRouter models from pi-ai package if available.
11
+ * This allows us to preserve thinkingLevelMap and other metadata from
12
+ * Pi's built-in registry when syncing models from OpenRouter API.
13
+ */
14
+ async function loadBuiltInOpenRouterModels(): Promise<Map<string, PiModelConfig>> {
15
+ if (builtInOpenRouterModels !== undefined) {
16
+ return builtInOpenRouterModels;
17
+ }
18
+
19
+ const models = new Map<string, PiModelConfig>();
20
+
21
+ try {
22
+ // Import from pi-ai to get built-in model registry
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ const { getModels } = (await import('@earendil-works/pi-ai')) as {
25
+ getModels: (provider: string) => unknown[];
26
+ };
27
+
28
+ const openrouterModels = getModels('openrouter');
29
+ if (Array.isArray(openrouterModels)) {
30
+ for (const model of openrouterModels) {
31
+ // Extract thinkingLevelMap from built-in model if present
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ const modelWithThinking = model as { id: string; thinkingLevelMap?: unknown };
34
+ if (modelWithThinking.id) {
35
+ models.set(modelWithThinking.id, model as PiModelConfig);
36
+ }
37
+ }
38
+ }
39
+ } catch {
40
+ // Ignore - built-in registry not available, will sync without merging
41
+ }
42
+
43
+ builtInOpenRouterModels = models;
44
+ return models;
45
+ }
46
+
47
+ /**
48
+ * Get thinkingLevelMap from built-in registry for a model, if available.
49
+ */
50
+ async function getBuiltInThinkingLevelMap(
51
+ modelId: string,
52
+ ): Promise<PiModelConfig['thinkingLevelMap'] | undefined> {
53
+ const builtIn = await loadBuiltInOpenRouterModels();
54
+ return builtIn.get(modelId)?.thinkingLevelMap;
55
+ }
56
+
5
57
  const COST_PER_MILLION = 1_000_000;
6
58
  const DEFAULT_MAX_TOKENS = 4096;
7
59
 
@@ -103,15 +155,22 @@ function validateModel(model: OpenRouterModel): ValidationResult {
103
155
 
104
156
  /**
105
157
  * Build PiModelConfig from a validated OpenRouterModel.
158
+ * Merges thinkingLevelMap from Pi's built-in registry if available.
106
159
  */
107
- function buildPiConfig(model: OpenRouterModel, contextWindow: number): PiModelConfig {
160
+ async function buildPiConfig(
161
+ model: OpenRouterModel,
162
+ contextWindow: number,
163
+ ): Promise<PiModelConfig> {
108
164
  const supportedParams = model.supported_parameters ?? [];
109
165
  const hasReasoning =
110
166
  supportedParams.includes('reasoning') || supportedParams.includes('include_reasoning');
111
167
  const inputModalities = model.architecture?.input_modalities;
112
168
  const supportsImages = inputModalities?.includes('image') ?? false;
113
169
 
114
- return {
170
+ // Fetch thinkingLevelMap from built-in registry if this is a reasoning model
171
+ const thinkingLevelMap = hasReasoning ? await getBuiltInThinkingLevelMap(model.id) : undefined;
172
+
173
+ const config: PiModelConfig = {
115
174
  id: model.id,
116
175
  name: model.name ?? model.id,
117
176
  reasoning: hasReasoning,
@@ -128,12 +187,25 @@ function buildPiConfig(model: OpenRouterModel, contextWindow: number): PiModelCo
128
187
  model.per_request_limits?.completion_tokens ??
129
188
  DEFAULT_MAX_TOKENS,
130
189
  };
190
+
191
+ // Only add thinkingLevelMap if it's defined for exactOptionalPropertyTypes compatibility
192
+ if (thinkingLevelMap !== undefined) {
193
+ config.thinkingLevelMap = thinkingLevelMap;
194
+ }
195
+
196
+ return config;
131
197
  }
132
198
 
133
199
  /**
134
200
  * Maps multiple OpenRouter models, tracking skips.
201
+ * Async to allow fetching thinkingLevelMap from built-in registry.
135
202
  */
136
- export function mapOpenRouterModels(models: OpenRouterModel[] | SDKModel[]): MapResult {
203
+ export async function mapOpenRouterModels(
204
+ models: OpenRouterModel[] | SDKModel[],
205
+ ): Promise<MapResult> {
206
+ // Pre-load built-in models for efficient lookup during mapping
207
+ await loadBuiltInOpenRouterModels();
208
+
137
209
  const configs: PiModelConfig[] = [];
138
210
  let skipped = 0;
139
211
  const skippedDetails: SkipReason[] = [];
@@ -154,7 +226,7 @@ export function mapOpenRouterModels(models: OpenRouterModel[] | SDKModel[]): Map
154
226
  continue;
155
227
  }
156
228
 
157
- configs.push(buildPiConfig(model, validation.contextWindow));
229
+ configs.push(await buildPiConfig(model, validation.contextWindow));
158
230
  }
159
231
 
160
232
  return { configs, skipped, skippedDetails };
@@ -163,8 +235,13 @@ export function mapOpenRouterModels(models: OpenRouterModel[] | SDKModel[]): Map
163
235
  /**
164
236
  * Maps a single OpenRouter model to Pi model config.
165
237
  * Returns null if the model should be skipped.
238
+ * Async to allow fetching thinkingLevelMap from built-in registry.
166
239
  */
167
- export function mapOpenRouterModel(model: OpenRouterModel | SDKModel): PiModelConfig | null {
240
+ export async function mapOpenRouterModel(
241
+ model: OpenRouterModel | SDKModel,
242
+ ): Promise<PiModelConfig | null> {
243
+ await loadBuiltInOpenRouterModels();
244
+
168
245
  const normalized = normalizeModel(model);
169
246
 
170
247
  // Router aliases are handled separately, skip them here
@@ -132,7 +132,7 @@ export async function syncModels(_ctx: ExtensionContext): Promise<SyncResult> {
132
132
  // Attempt 1: Fetch from API
133
133
  try {
134
134
  const response = await fetchUserModels();
135
- const { configs, skipped, skippedDetails } = mapOpenRouterModels(response.data);
135
+ const { configs, skipped, skippedDetails } = await mapOpenRouterModels(response.data);
136
136
 
137
137
  // Add built-in router aliases that don't appear in /models/user endpoint
138
138
  const configsWithRouters = [...configs, ...BUILTIN_ROUTER_MODELS];
@@ -172,7 +172,7 @@ export async function syncModels(_ctx: ExtensionContext): Promise<SyncResult> {
172
172
 
173
173
  if (cache) {
174
174
  // Attempt 2: Use cached models
175
- const { configs, skipped } = mapOpenRouterModels(cache.models);
175
+ const { configs, skipped } = await mapOpenRouterModels(cache.models);
176
176
 
177
177
  await registerModelsWithProvider(_ctx, configs);
178
178
 
@@ -32,6 +32,20 @@ export interface OpenRouterModelsResponse {
32
32
  data: OpenRouterModel[];
33
33
  }
34
34
 
35
+ /**
36
+ * Pi thinking level map - copied from pi-ai's Model type.
37
+ * Levels mapped to null are hidden in Pi's UI.
38
+ * Levels mapped to strings are sent to the provider API.
39
+ */
40
+ export interface ThinkingLevelMap {
41
+ off?: string | null;
42
+ minimal?: string | null;
43
+ low?: string | null;
44
+ medium?: string | null;
45
+ high?: string | null;
46
+ xhigh?: string | null;
47
+ }
48
+
35
49
  /**
36
50
  * Mapped Pi model configuration for provider registration
37
51
  */
@@ -48,6 +62,7 @@ export interface PiModelConfig {
48
62
  };
49
63
  contextWindow: number;
50
64
  maxTokens: number;
65
+ thinkingLevelMap?: ThinkingLevelMap;
51
66
  }
52
67
 
53
68
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robhowley/pi-openrouter",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "type": "module",
5
5
  "description": "Live OpenRouter spend/account TUI overlays, user-scoped model sync, and session tagging for Pi.",
6
6
  "license": "MIT",
@@ -40,10 +40,13 @@
40
40
  "@openrouter/sdk": "^0.12.21"
41
41
  },
42
42
  "peerDependencies": {
43
- "@mariozechner/pi-ai": "*",
44
- "@mariozechner/pi-coding-agent": "*"
43
+ "@earendil-works/pi-ai": "*",
44
+ "@earendil-works/pi-coding-agent": "*"
45
45
  },
46
46
  "devDependencies": {
47
+ "@earendil-works/pi-ai": "*",
48
+ "@earendil-works/pi-coding-agent": "*",
49
+ "@mariozechner/pi-coding-agent": "*",
47
50
  "@types/node": "^22.15.17"
48
51
  }
49
52
  }