@robhowley/pi-openrouter 0.14.1 → 0.14.2

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 CHANGED
@@ -200,10 +200,10 @@ Some OpenRouter models don't have complete metadata in Pi's built-in registry or
200
200
 
201
201
  ```bash
202
202
  # Override thinking levels for DeepSeek V4 Pro
203
- /openrouter model-override-set deepseek/deepseek-v4-pro thinking.high=high thinking.xhigh=max
203
+ /openrouter model-override-set deepseek/deepseek-v4-pro thinking.high=high thinking.max=max
204
204
 
205
205
  # Same thing with exact field names
206
- /openrouter model-override-set deepseek/deepseek-v4-pro thinkingLevelMap.high=high thinkingLevelMap.xhigh=max
206
+ /openrouter model-override-set deepseek/deepseek-v4-pro thinkingLevelMap.high=high thinkingLevelMap.max=max
207
207
 
208
208
  # Override context window or max tokens
209
209
  /openrouter model-override-set custom/model contextWindow=128000 maxTokens=8192
@@ -211,7 +211,8 @@ Some OpenRouter models don't have complete metadata in Pi's built-in registry or
211
211
 
212
212
  **Scoped field names:**
213
213
 
214
- - `thinking.off`, `thinking.minimal`, `thinking.low`, `thinking.medium`, `thinking.high`, `thinking.xhigh` → map to `thinkingLevelMap.*`
214
+ - `thinking.off`, `thinking.minimal`, `thinking.low`, `thinking.medium`, `thinking.high`, `thinking.xhigh`, `thinking.max` → map to `thinkingLevelMap.*`
215
+ - `thinkingLevelMap.off`, `thinkingLevelMap.minimal`, `thinkingLevelMap.low`, `thinkingLevelMap.medium`, `thinkingLevelMap.high`, `thinkingLevelMap.xhigh`, `thinkingLevelMap.max` → map to `thinkingLevelMap.*`
215
216
  - `contextWindow` → `contextWindow` (number)
216
217
  - `maxTokens` → `maxTokens` (number)
217
218
  - `reasoning` → `reasoning` (boolean)
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import type { Model as SDKModel } from '@openrouter/sdk/models/index.js';
3
+ import { modelFromJSON } from '@openrouter/sdk/models/model.js';
3
4
  import type {
4
5
  CreateKeysData,
5
6
  GetCurrentKeyData,
@@ -33,6 +34,7 @@ function createSdkModel(overrides: Partial<SDKModel> = {}): SDKModel {
33
34
  completion: '0.0000015',
34
35
  },
35
36
  supportedParameters: [],
37
+ supportedVoices: null,
36
38
  topProvider: {
37
39
  isModerated: false,
38
40
  },
@@ -146,6 +148,37 @@ function createUpdateKeysData(overrides: Partial<UpdateKeysData> = {}): UpdateKe
146
148
  };
147
149
  }
148
150
 
151
+ function createSdkModelJson(supportedEfforts?: ReadonlyArray<string | null> | null): string {
152
+ return JSON.stringify({
153
+ architecture: {
154
+ input_modalities: ['text'],
155
+ modality: 'text',
156
+ output_modalities: ['text'],
157
+ },
158
+ canonical_slug: 'test/model',
159
+ context_length: 128000,
160
+ created: 0,
161
+ default_parameters: null,
162
+ id: 'test/model',
163
+ links: { details: 'https://openrouter.ai/test/model' },
164
+ name: 'Test Model',
165
+ per_request_limits: null,
166
+ pricing: {
167
+ prompt: '0.0000005',
168
+ completion: '0.0000015',
169
+ },
170
+ reasoning: {
171
+ mandatory: false,
172
+ ...(supportedEfforts === undefined ? {} : { supported_efforts: supportedEfforts }),
173
+ },
174
+ supported_parameters: ['reasoning'],
175
+ supported_voices: null,
176
+ top_provider: {
177
+ is_moderated: false,
178
+ },
179
+ });
180
+ }
181
+
149
182
  describe('sdkModelToOpenRouterModel', () => {
150
183
  it('normalizes SDK camelCase fields into canonical snake_case model shape', () => {
151
184
  const normalized = sdkModelToOpenRouterModel(
@@ -197,6 +230,43 @@ describe('sdkModelToOpenRouterModel', () => {
197
230
  });
198
231
  });
199
232
 
233
+ it('parses snake_case reasoning metadata through the SDK before normalizing it', () => {
234
+ const parsed = modelFromJSON(createSdkModelJson(['low', null, 'future-effort']));
235
+
236
+ expect(parsed.ok).toBe(true);
237
+ if (!parsed.ok) return;
238
+
239
+ const normalized = sdkModelToOpenRouterModel(parsed.value);
240
+ expect(normalized.reasoning).toEqual({
241
+ mandatory: false,
242
+ supported_efforts: ['low', null, 'future-effort'],
243
+ });
244
+ });
245
+
246
+ it.each([
247
+ ['omitted', undefined],
248
+ ['null', null],
249
+ ['empty', []],
250
+ ['nullable element', [null]],
251
+ ['unknown string', ['future-effort']],
252
+ ['mixed values', ['low', null, 'future-effort']],
253
+ ] as const)('preserves %s supported_efforts during SDK normalization', (_name, efforts) => {
254
+ const parsed = modelFromJSON(createSdkModelJson(efforts));
255
+
256
+ expect(parsed.ok).toBe(true);
257
+ if (!parsed.ok) return;
258
+
259
+ const normalized = sdkModelToOpenRouterModel(parsed.value);
260
+ if (efforts === undefined) {
261
+ expect(normalized.reasoning).toEqual({ mandatory: false });
262
+ } else {
263
+ expect(normalized.reasoning).toEqual({
264
+ mandatory: false,
265
+ supported_efforts: efforts,
266
+ });
267
+ }
268
+ });
269
+
200
270
  it('omits optional provider/request blocks when the SDK omits them', () => {
201
271
  const normalized = sdkModelToOpenRouterModel(
202
272
  createSdkModel({
@@ -45,15 +45,26 @@ describe('loadCache', () => {
45
45
  });
46
46
 
47
47
  it('should return parsed cache when file exists and is valid', async () => {
48
- const mockCache = createMockCache({ catalogMode: 'free-only', timestamp: 1234567890 });
48
+ const mockCache = createMockCache({
49
+ catalogMode: 'free-only',
50
+ timestamp: 1234567890,
51
+ models: [
52
+ {
53
+ ...createMockCache().models[0]!,
54
+ reasoning: {
55
+ mandatory: false,
56
+ supported_efforts: ['low', null, 'future-effort'],
57
+ },
58
+ },
59
+ ],
60
+ });
49
61
  await saveCache(mockCache);
50
62
 
51
63
  const result = await loadCache();
52
64
  expect(result).not.toBeNull();
53
65
  expect(result!.catalogMode).toBe('free-only');
54
66
  expect(result!.timestamp).toBe(1234567890);
55
- expect(result!.models).toHaveLength(1);
56
- expect(result!.models[0]!.id).toBe('test/model');
67
+ expect(result!.models).toEqual(mockCache.models);
57
68
  });
58
69
 
59
70
  it('should return null when cache file contains invalid JSON', async () => {
@@ -12,14 +12,15 @@ vi.mock('../overrides.js', () => ({
12
12
  overrides.overrides[modelId],
13
13
  }));
14
14
 
15
- vi.mock('@earendil-works/pi-ai', () => ({
16
- getModels: vi.fn(() => [
15
+ vi.mock('@earendil-works/pi-ai/providers/all', () => ({
16
+ getBuiltinModels: vi.fn(() => [
17
17
  {
18
18
  id: 'test/model',
19
19
  thinkingLevelMap: {
20
20
  minimal: 'builtin-minimal',
21
21
  high: 'builtin-high',
22
22
  xhigh: 'builtin-xhigh',
23
+ max: 'builtin-max',
23
24
  },
24
25
  },
25
26
  ]),
@@ -43,6 +44,7 @@ describe('mapOpenRouterModels overrides', () => {
43
44
  thinkingLevelMap: {
44
45
  high: 'override-high',
45
46
  xhigh: null,
47
+ max: null,
46
48
  },
47
49
  },
48
50
  },
@@ -52,6 +54,10 @@ describe('mapOpenRouterModels overrides', () => {
52
54
  createValidModel({
53
55
  id: 'test/model',
54
56
  supported_parameters: ['reasoning'],
57
+ reasoning: {
58
+ mandatory: false,
59
+ supported_efforts: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'],
60
+ },
55
61
  }),
56
62
  ]);
57
63
 
@@ -61,21 +67,163 @@ describe('mapOpenRouterModels overrides', () => {
61
67
  contextWindow: 64000,
62
68
  maxTokens: 8192,
63
69
  reasoning: false,
70
+ });
71
+ expect(result.configs[0]?.thinkingLevelMap).toEqual({
72
+ minimal: 'builtin-minimal',
73
+ high: 'override-high',
74
+ xhigh: null,
75
+ max: null,
76
+ });
77
+ });
78
+
79
+ it('derives the exact GLM 5.3 effort map from API metadata', async () => {
80
+ const result = await mapOpenRouterModels([
81
+ createValidModel({
82
+ id: 'z-ai/glm-5.3',
83
+ reasoning: { mandatory: true, supported_efforts: ['low', 'high', 'max'] },
84
+ }),
85
+ ]);
86
+
87
+ expect(result.configs[0]?.thinkingLevelMap).toEqual({
88
+ off: null,
89
+ minimal: null,
90
+ low: 'low',
91
+ medium: null,
92
+ high: 'high',
93
+ xhigh: null,
94
+ max: 'max',
95
+ });
96
+ });
97
+
98
+ it('derives an API map when an unrelated user override is present', async () => {
99
+ loadModelOverrides.mockResolvedValue({
100
+ version: 1,
101
+ overrides: {
102
+ 'api/model': { contextWindow: 64000 },
103
+ },
104
+ });
105
+
106
+ const result = await mapOpenRouterModels([
107
+ createValidModel({
108
+ id: 'api/model',
109
+ reasoning: {
110
+ mandatory: false,
111
+ supported_efforts: ['low', null, 'future-effort', 'max', 'none'],
112
+ },
113
+ }),
114
+ ]);
115
+
116
+ expect(result.configs[0]).toMatchObject({
117
+ reasoning: true,
118
+ contextWindow: 64000,
64
119
  thinkingLevelMap: {
65
- minimal: 'builtin-minimal',
66
- high: 'override-high',
120
+ off: 'none',
121
+ minimal: null,
122
+ low: 'low',
123
+ medium: null,
124
+ high: null,
67
125
  xhigh: null,
126
+ max: 'max',
68
127
  },
69
128
  });
70
129
  });
71
130
 
72
- it('applies user thinkingLevelMap when the built-in registry has no map for the model', async () => {
131
+ it.each([
132
+ [true, null],
133
+ [false, 'none'],
134
+ ] as const)('maps mandatory=%s to off=%s for API maps', async (mandatory, off) => {
135
+ const result = await mapOpenRouterModels([
136
+ createValidModel({
137
+ id: `api/mandatory-${mandatory}`,
138
+ reasoning: { mandatory, supported_efforts: ['high'] },
139
+ }),
140
+ ]);
141
+
142
+ expect(result.configs[0]?.thinkingLevelMap).toEqual({
143
+ off,
144
+ minimal: null,
145
+ low: null,
146
+ medium: null,
147
+ high: 'high',
148
+ xhigh: null,
149
+ max: null,
150
+ });
151
+ });
152
+
153
+ it.each([
154
+ [true, null],
155
+ [false, 'none'],
156
+ ] as const)('maps explicit null supported_efforts with mandatory=%s', async (mandatory, off) => {
157
+ const result = await mapOpenRouterModels([
158
+ createValidModel({
159
+ id: `api/unrestricted-${mandatory}`,
160
+ reasoning: { mandatory, supported_efforts: null },
161
+ }),
162
+ ]);
163
+
164
+ expect(result.configs[0]?.thinkingLevelMap).toEqual({
165
+ off,
166
+ minimal: 'minimal',
167
+ low: 'low',
168
+ medium: 'medium',
169
+ high: 'high',
170
+ xhigh: 'xhigh',
171
+ max: 'max',
172
+ });
173
+ });
174
+
175
+ it('does not derive an API map from absent or unusable effort metadata', async () => {
176
+ const cases: Array<Array<string | null> | undefined> = [
177
+ undefined,
178
+ [],
179
+ [null],
180
+ ['none'],
181
+ ['future-effort'],
182
+ ];
183
+
184
+ for (const [index, supported_efforts] of cases.entries()) {
185
+ const reasoning =
186
+ supported_efforts === undefined
187
+ ? { mandatory: false }
188
+ : { mandatory: false, supported_efforts };
189
+ const result = await mapOpenRouterModels([
190
+ createValidModel({ id: `api/unusable-${index}`, reasoning }),
191
+ ]);
192
+
193
+ expect(result.configs[0]?.thinkingLevelMap).toBeUndefined();
194
+ }
195
+ });
196
+
197
+ it('hides off for mandatory reasoning metadata with no usable effort', async () => {
198
+ const result = await mapOpenRouterModels([
199
+ createValidModel({
200
+ id: 'api/mandatory-unknown-effort',
201
+ reasoning: { mandatory: true, supported_efforts: [null, 'none', 'future-effort'] },
202
+ }),
203
+ ]);
204
+
205
+ expect(result.configs[0]?.thinkingLevelMap).toEqual({ off: null });
206
+ });
207
+
208
+ it('does not derive an API map from supported_parameters alone', async () => {
209
+ const result = await mapOpenRouterModels([
210
+ createValidModel({
211
+ id: 'api/parameter-only',
212
+ supported_parameters: ['reasoning'],
213
+ }),
214
+ ]);
215
+
216
+ expect(result.configs[0]?.reasoning).toBe(true);
217
+ expect(result.configs[0]?.thinkingLevelMap).toBeUndefined();
218
+ });
219
+
220
+ it('merges sparse user thinkingLevelMap over the API map', async () => {
73
221
  loadModelOverrides.mockResolvedValue({
74
222
  version: 1,
75
223
  overrides: {
76
224
  'new/model': {
77
225
  thinkingLevelMap: {
78
- high: 'high',
226
+ high: 'override-high',
79
227
  xhigh: 'max',
80
228
  },
81
229
  },
@@ -86,6 +234,7 @@ describe('mapOpenRouterModels overrides', () => {
86
234
  createValidModel({
87
235
  id: 'new/model',
88
236
  supported_parameters: ['reasoning'],
237
+ reasoning: { mandatory: true, supported_efforts: ['low', 'high', 'max'] },
89
238
  }),
90
239
  ]);
91
240
 
@@ -93,10 +242,15 @@ describe('mapOpenRouterModels overrides', () => {
93
242
  expect(result.configs[0]).toMatchObject({
94
243
  id: 'new/model',
95
244
  reasoning: true,
96
- thinkingLevelMap: {
97
- high: 'high',
98
- xhigh: 'max',
99
- },
245
+ });
246
+ expect(result.configs[0]?.thinkingLevelMap).toEqual({
247
+ off: null,
248
+ minimal: null,
249
+ low: 'low',
250
+ medium: null,
251
+ high: 'override-high',
252
+ xhigh: 'max',
253
+ max: 'max',
100
254
  });
101
255
  });
102
256
  });
@@ -111,6 +111,11 @@ describe('parseScopedAssignment', () => {
111
111
  fullPath: 'thinkingLevelMap.xhigh',
112
112
  value: 'max',
113
113
  });
114
+ expect(parseScopedAssignment('thinkingLevelMap.max=max')).toEqual({
115
+ ok: true,
116
+ fullPath: 'thinkingLevelMap.max',
117
+ value: 'max',
118
+ });
114
119
  });
115
120
 
116
121
  it('parses null string values for thinking levels', () => {
@@ -247,6 +252,7 @@ describe('parseScopedAssignment', () => {
247
252
  ['thinking.high=high', 'high'],
248
253
  ['thinking.xhigh=max', 'max'],
249
254
  ['thinking.xhigh=xhigh', 'xhigh'],
255
+ ['thinking.max=max', 'max'],
250
256
  ];
251
257
 
252
258
  for (const [input, expectedValue] of validPairs) {
@@ -402,7 +408,7 @@ describe('handleModelOverrideSet', () => {
402
408
  const userOverrides = emptyOverrides();
403
409
 
404
410
  const result = await handleModelOverrideSet(
405
- 'test/model thinking.high=high thinking.xhigh=max thinking.off=null contextWindow=64000 maxTokens=8192 reasoning=true',
411
+ 'test/model thinking.high=high thinking.xhigh=max thinking.max=max thinking.off=null contextWindow=64000 maxTokens=8192 reasoning=true',
406
412
  userOverrides,
407
413
  );
408
414
 
@@ -415,6 +421,7 @@ describe('handleModelOverrideSet', () => {
415
421
  thinkingLevelMap: {
416
422
  high: 'high',
417
423
  xhigh: 'max',
424
+ max: 'max',
418
425
  off: null,
419
426
  },
420
427
  contextWindow: 64000,
@@ -553,6 +560,8 @@ describe('handleModelOverrideList', () => {
553
560
 
554
561
  expect(result).toContain('Available override fields');
555
562
  expect(result).toContain('thinking.high');
563
+ expect(result).toContain('thinking.max');
564
+ expect(result).toContain('thinkingLevelMap.max');
556
565
  expect(result).toContain('contextWindow');
557
566
  });
558
567
 
@@ -578,7 +587,7 @@ describe('handleModelOverrideList', () => {
578
587
  version: 1,
579
588
  overrides: {
580
589
  'test/model': {
581
- thinkingLevelMap: { high: 'high', xhigh: 'max', off: null },
590
+ thinkingLevelMap: { high: 'high', xhigh: 'max', max: 'max', off: null },
582
591
  contextWindow: 64000,
583
592
  },
584
593
  },
@@ -590,6 +599,7 @@ describe('handleModelOverrideList', () => {
590
599
  expect(result).toContain('thinkingLevelMap:');
591
600
  expect(result).toContain('high: high');
592
601
  expect(result).toContain('xhigh: max');
602
+ expect(result).toContain('max: max');
593
603
  expect(result).toContain('off: null');
594
604
  expect(result).toContain('contextWindow: 64000');
595
605
  });
@@ -625,6 +635,7 @@ describe('SCOPED_FIELD_MAP', () => {
625
635
  'thinking.medium',
626
636
  'thinking.high',
627
637
  'thinking.xhigh',
638
+ 'thinking.max',
628
639
  ];
629
640
 
630
641
  for (const shorthand of expectedShorthands) {
@@ -642,6 +653,7 @@ describe('SCOPED_FIELD_MAP', () => {
642
653
  'thinkingLevelMap.medium',
643
654
  'thinkingLevelMap.high',
644
655
  'thinkingLevelMap.xhigh',
656
+ 'thinkingLevelMap.max',
645
657
  ];
646
658
 
647
659
  for (const exact of expectedExact) {
@@ -23,11 +23,9 @@ async function loadBuiltInOpenRouterModels(): Promise<Map<string, PiModelConfig>
23
23
 
24
24
  try {
25
25
  // Import from pi-ai to get built-in model registry
26
- const { getModels } = (await import('@earendil-works/pi-ai')) as {
27
- getModels: (provider: string) => unknown[];
28
- };
26
+ const { getBuiltinModels } = await import('@earendil-works/pi-ai/providers/all');
29
27
 
30
- const openrouterModels = getModels('openrouter');
28
+ const openrouterModels = getBuiltinModels('openrouter');
31
29
  if (Array.isArray(openrouterModels)) {
32
30
  for (const model of openrouterModels) {
33
31
  // Extract thinkingLevelMap from built-in model if present
@@ -57,6 +55,40 @@ async function getBuiltInThinkingLevelMap(
57
55
 
58
56
  const COST_PER_MILLION = 1_000_000;
59
57
  const DEFAULT_MAX_TOKENS = 4096;
58
+ const API_THINKING_LEVELS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const;
59
+ type ApiThinkingLevel = (typeof API_THINKING_LEVELS)[number];
60
+
61
+ function buildApiThinkingLevelMap(
62
+ reasoning: OpenRouterModel['reasoning'],
63
+ ): PiModelConfig['thinkingLevelMap'] {
64
+ if (reasoning === undefined) {
65
+ return undefined;
66
+ }
67
+
68
+ const supportedEfforts = new Set(
69
+ reasoning?.supported_efforts === null
70
+ ? API_THINKING_LEVELS
71
+ : (reasoning?.supported_efforts?.filter(
72
+ (effort): effort is ApiThinkingLevel =>
73
+ typeof effort === 'string' &&
74
+ (API_THINKING_LEVELS as readonly string[]).includes(effort),
75
+ ) ?? []),
76
+ );
77
+
78
+ if (supportedEfforts.size === 0) {
79
+ return reasoning.mandatory ? { off: null } : undefined;
80
+ }
81
+
82
+ return {
83
+ off: reasoning.mandatory ? null : 'none',
84
+ minimal: supportedEfforts.has('minimal') ? 'minimal' : null,
85
+ low: supportedEfforts.has('low') ? 'low' : null,
86
+ medium: supportedEfforts.has('medium') ? 'medium' : null,
87
+ high: supportedEfforts.has('high') ? 'high' : null,
88
+ xhigh: supportedEfforts.has('xhigh') ? 'xhigh' : null,
89
+ max: supportedEfforts.has('max') ? 'max' : null,
90
+ };
91
+ }
60
92
 
61
93
  /**
62
94
  * Validation result for a model check.
@@ -123,7 +155,9 @@ async function buildPiConfig(
123
155
  ): Promise<PiModelConfig> {
124
156
  const supportedParams = model.supported_parameters ?? [];
125
157
  const hasReasoning =
126
- supportedParams.includes('reasoning') || supportedParams.includes('include_reasoning');
158
+ model.reasoning !== undefined
159
+ ? true
160
+ : supportedParams.includes('reasoning') || supportedParams.includes('include_reasoning');
127
161
  const inputModalities = model.architecture?.input_modalities;
128
162
  const supportsImages = inputModalities?.includes('image') ?? false;
129
163
 
@@ -134,11 +168,13 @@ async function buildPiConfig(
134
168
 
135
169
  // Fetch user override for this model
136
170
  const userOverride = userOverrides ? getModelOverride(userOverrides, model.id) : undefined;
171
+ const apiThinkingLevelMap = buildApiThinkingLevelMap(model.reasoning);
137
172
 
173
+ const baseThinkingLevelMap = builtInThinkingLevelMap ?? apiThinkingLevelMap;
138
174
  const thinkingLevelMap =
139
- builtInThinkingLevelMap !== undefined || userOverride?.thinkingLevelMap !== undefined
175
+ baseThinkingLevelMap !== undefined || userOverride?.thinkingLevelMap !== undefined
140
176
  ? {
141
- ...builtInThinkingLevelMap,
177
+ ...baseThinkingLevelMap,
142
178
  ...userOverride?.thinkingLevelMap,
143
179
  }
144
180
  : undefined;
@@ -50,6 +50,7 @@ export const SCOPED_FIELD_MAP: Record<string, ScopedField> = {
50
50
  'thinking.medium': { targetField: 'thinkingLevelMap.medium', targetType: 'string' },
51
51
  'thinking.high': { targetField: 'thinkingLevelMap.high', targetType: 'string' },
52
52
  'thinking.xhigh': { targetField: 'thinkingLevelMap.xhigh', targetType: 'string' },
53
+ 'thinking.max': { targetField: 'thinkingLevelMap.max', targetType: 'string' },
53
54
 
54
55
  // exact field names (passthrough)
55
56
  'thinkingLevelMap.off': { targetField: 'thinkingLevelMap.off', targetType: 'string' },
@@ -58,6 +59,7 @@ export const SCOPED_FIELD_MAP: Record<string, ScopedField> = {
58
59
  'thinkingLevelMap.medium': { targetField: 'thinkingLevelMap.medium', targetType: 'string' },
59
60
  'thinkingLevelMap.high': { targetField: 'thinkingLevelMap.high', targetType: 'string' },
60
61
  'thinkingLevelMap.xhigh': { targetField: 'thinkingLevelMap.xhigh', targetType: 'string' },
62
+ 'thinkingLevelMap.max': { targetField: 'thinkingLevelMap.max', targetType: 'string' },
61
63
 
62
64
  // top-level fields (future extensibility)
63
65
  contextWindow: { targetField: 'contextWindow', targetType: 'number' },
@@ -16,6 +16,10 @@ export interface OpenRouterModel {
16
16
  input_cache_write?: string;
17
17
  };
18
18
  supported_parameters?: string[];
19
+ reasoning?: {
20
+ mandatory: boolean;
21
+ supported_efforts?: Array<string | null> | null;
22
+ };
19
23
  top_provider?: {
20
24
  context_length?: number;
21
25
  max_completion_tokens?: number;
@@ -44,6 +48,7 @@ export interface ThinkingLevelMap {
44
48
  medium?: string | null;
45
49
  high?: string | null;
46
50
  xhigh?: string | null;
51
+ max?: string | null;
47
52
  }
48
53
 
49
54
  /**
@@ -62,6 +62,16 @@ export function sdkModelToOpenRouterModel(model: SDKModel): OpenRouterModel {
62
62
  supported_parameters: model.supportedParameters,
63
63
  };
64
64
 
65
+ if (model.reasoning !== undefined) {
66
+ const reasoning: NonNullable<OpenRouterModel['reasoning']> = {
67
+ mandatory: model.reasoning.mandatory,
68
+ };
69
+ if (model.reasoning.supportedEfforts !== undefined) {
70
+ reasoning.supported_efforts = model.reasoning.supportedEfforts;
71
+ }
72
+ result.reasoning = reasoning;
73
+ }
74
+
65
75
  if (architecture) {
66
76
  result.architecture = architecture;
67
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robhowley/pi-openrouter",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "type": "module",
5
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",
@@ -38,15 +38,15 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@mariozechner/pi-tui": "*",
41
- "@openrouter/sdk": "^0.12.21"
41
+ "@openrouter/sdk": "^0.13.13"
42
42
  },
43
43
  "peerDependencies": {
44
- "@earendil-works/pi-ai": "*",
45
- "@earendil-works/pi-coding-agent": "*"
44
+ "@earendil-works/pi-ai": ">=0.84.4",
45
+ "@earendil-works/pi-coding-agent": ">=0.84.4"
46
46
  },
47
47
  "devDependencies": {
48
- "@earendil-works/pi-ai": "*",
49
- "@earendil-works/pi-coding-agent": "*",
48
+ "@earendil-works/pi-ai": "0.84.4",
49
+ "@earendil-works/pi-coding-agent": "0.84.4",
50
50
  "@mariozechner/pi-coding-agent": "*",
51
51
  "@types/node": "^22.15.17"
52
52
  }