pi-tokenrouter 1.0.4 → 1.0.5

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  A [pi](https://github.com/badlogic/pi-mono) provider extension for [TokenRouter](https://tokenrouter.com).
4
4
 
5
- Dynamically discovers available models from the TokenRouter API and enriches them with pricing, context window, and max output token data from [OpenRouter](https://openrouter.ai) (TokenRouter shares the same pricing).
5
+ Models are derived from TokenRouter's `/v1/models` list and enriched with metadata from [models.dev](https://models.dev) first, then [OpenRouter](https://openrouter.ai).
6
6
 
7
7
  ## Install
8
8
 
@@ -33,12 +33,9 @@ export TOKENROUTER_API_KEY=sk-...
33
33
 
34
34
  ## How it works
35
35
 
36
- 1. On startup, fetches the model list from TokenRouter's `/v1/models` endpoint.
37
- 2. In parallel, fetches pricing data from OpenRouter's public model catalog.
38
- 3. Matches models by ID and fills in cost, context window, and max output tokens.
39
- 4. Caches everything locally for 1 week (`~/.pi/agent/cache/tokenrouter-models.json`).
40
-
41
- Models that don't match an OpenRouter entry fall back to zero cost and default context limits.
36
+ 1. Registers TokenRouter as an API-key provider, so `/login tokenrouter` is handled under `Use an API key`.
37
+ 2. Uses a checked-in snapshot generated from TokenRouter's authenticated `/v1/models` response.
38
+ 3. Enriches each model with metadata from `models.dev` when available, then falls back to OpenRouter for pricing, context window, max output tokens, reasoning support, and image support.
42
39
 
43
40
  ## License
44
41
 
package/index.ts CHANGED
@@ -2,304 +2,23 @@
2
2
  * TokenRouter Provider Extension
3
3
  *
4
4
  * Registers TokenRouter (https://api.tokenrouter.com/v1) as a custom provider.
5
- * Dynamically fetches available models from the /v1/models endpoint with
6
- * file-based caching to avoid redundant API calls on startup.
7
- *
8
- * Authentication (resolved via AuthStorage with full priority chain):
9
- * 1. Runtime overrides (CLI --api-key)
10
- * 2. API key from auth.json (literal, env var name, or shell command)
11
- * 3. OAuth token from auth.json (from /login tokenrouter)
12
- * 4. TOKENROUTER_API_KEY environment variable
5
+ * Models are derived from TokenRouter's /v1/models list, enriched with
6
+ * metadata from models.dev first and OpenRouter second, so TokenRouter can be
7
+ * configured through pi's API-key login flow before any TokenRouter auth exists.
13
8
  *
14
9
  * Usage:
15
10
  * pi -e /path/to/pi-tokenrouter
16
- * /login tokenrouter # stores key in auth.json
11
+ * /login tokenrouter
17
12
  * # OR add to ~/.pi/agent/auth.json:
18
13
  * # "tokenrouter": { "type": "api_key", "key": "sk-..." }
19
14
  * # "tokenrouter": { "type": "api_key", "key": "TOKENROUTER_API_KEY" }
20
15
  * # "tokenrouter": { "type": "api_key", "key": "!op read 'op://vault/item/key'" }
21
16
  */
22
17
 
23
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
24
- import { join } from "node:path";
25
- import { homedir } from "node:os";
26
- import type { OAuthCredentials, OAuthLoginCallbacks } from "@mariozechner/pi-ai";
27
- import { AuthStorage } from "@mariozechner/pi-coding-agent";
28
18
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
29
-
30
- const BASE_URL = "https://api.tokenrouter.com/v1";
31
- const PROVIDER_NAME = "tokenrouter";
32
- const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
33
-
34
- const HOME_PI = join(homedir(), ".pi", "agent");
35
- const CACHE_DIR = join(HOME_PI, "cache");
36
- const CACHE_FILE = join(CACHE_DIR, "tokenrouter-models.json");
37
- const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 1 week
38
-
39
- const DEFAULT_CONTEXT_WINDOW = 128_000;
40
- const DEFAULT_MAX_TOKENS = 4_096;
41
-
42
- // Reasoning model name patterns
43
- const REASONING_PATTERNS = [
44
- /o1\b/i,
45
- /o3\b/i,
46
- /o4\b/i,
47
- /claude.*thinking/i,
48
- /deepseek-r/i,
49
- /deepseek-prover/i,
50
- /gemini.*thinking/i,
51
- /qwq/i,
52
- /qwen3/i,
53
- /reasoning/i,
54
- ];
55
-
56
- // Vision model name patterns
57
- const VISION_PATTERNS = [
58
- /vision/i,
59
- /claude/i,
60
- /gpt-4o/i,
61
- /gpt-4-turbo/i,
62
- /gemini/i,
63
- /llava/i,
64
- /qwen.*vl/i,
65
- /qwen.*visual/i,
66
- ];
67
-
68
- function isReasoningModel(id: string): boolean {
69
- return REASONING_PATTERNS.some((p) => p.test(id));
70
- }
71
-
72
- function supportsVision(id: string): boolean {
73
- return VISION_PATTERNS.some((p) => p.test(id));
74
- }
75
-
76
- // ---------------------------------------------------------------------------
77
- // OpenRouter pricing lookup
78
- // ---------------------------------------------------------------------------
79
-
80
- interface OpenRouterModel {
81
- id: string;
82
- context_length?: number;
83
- top_provider?: { max_completion_tokens?: number };
84
- pricing?: {
85
- prompt?: string;
86
- completion?: string;
87
- input_cache_read?: string;
88
- input_cache_write?: string;
89
- };
90
- }
91
-
92
- interface Pricing {
93
- input: number;
94
- output: number;
95
- cacheRead: number;
96
- cacheWrite: number;
97
- contextWindow: number;
98
- maxTokens: number;
99
- }
100
-
101
- function parsePerToken(price?: string): number {
102
- const n = parseFloat(price ?? "0");
103
- return Number.isFinite(n) ? n * 1_000_000 : 0;
104
- }
105
-
106
- async function fetchOpenRouterPricing(): Promise<Map<string, Pricing>> {
107
- const response = await fetch(OPENROUTER_MODELS_URL);
108
- if (!response.ok) return new Map();
109
-
110
- const payload = (await response.json()) as { data?: OpenRouterModel[] };
111
- const models = payload.data ?? [];
112
-
113
- const map = new Map<string, Pricing>();
114
- for (const m of models) {
115
- map.set(m.id, {
116
- input: parsePerToken(m.pricing?.prompt),
117
- output: parsePerToken(m.pricing?.completion),
118
- cacheRead: parsePerToken(m.pricing?.input_cache_read),
119
- cacheWrite: parsePerToken(m.pricing?.input_cache_write),
120
- contextWindow: m.context_length ?? DEFAULT_CONTEXT_WINDOW,
121
- maxTokens: m.top_provider?.max_completion_tokens ?? DEFAULT_MAX_TOKENS,
122
- });
123
- }
124
- return map;
125
- }
126
-
127
- // ---------------------------------------------------------------------------
128
- // Model cache
129
- // ---------------------------------------------------------------------------
130
-
131
- interface CachedModels {
132
- fetchedAt: number;
133
- models: RawModel[];
134
- pricing: Record<string, Pricing>;
135
- }
136
-
137
- interface RawModel {
138
- id: string;
139
- name?: string;
140
- context_window?: number;
141
- max_tokens?: number;
142
- [key: string]: unknown;
143
- }
144
-
145
- function readCache(): CachedModels | null {
146
- try {
147
- if (!existsSync(CACHE_FILE)) return null;
148
- const raw = readFileSync(CACHE_FILE, "utf-8");
149
- const data = JSON.parse(raw) as CachedModels;
150
- // Invalidate cache from before pricing was added
151
- if (!data.pricing) return null;
152
- return data;
153
- } catch {
154
- return null;
155
- }
156
- }
157
-
158
- function writeCache(models: RawModel[], pricing: Map<string, Pricing>): void {
159
- try {
160
- if (!existsSync(CACHE_DIR)) {
161
- mkdirSync(CACHE_DIR, { recursive: true });
162
- }
163
- // Serialise pricing map to plain object
164
- const pricingObj: Record<string, Pricing> = {};
165
- for (const [k, v] of pricing) pricingObj[k] = v;
166
- const payload: CachedModels = { fetchedAt: Date.now(), models, pricing: pricingObj };
167
- writeFileSync(CACHE_FILE, JSON.stringify(payload), "utf-8");
168
- } catch {
169
- // Cache write failure is non-fatal
170
- }
171
- }
172
-
173
- function isCacheFresh(cache: CachedModels): boolean {
174
- return Date.now() - cache.fetchedAt < CACHE_TTL_MS;
175
- }
176
-
177
- // ---------------------------------------------------------------------------
178
- // Model fetching
179
- // ---------------------------------------------------------------------------
180
-
181
- async function fetchModels(apiKey: string): Promise<RawModel[]> {
182
- const response = await fetch(`${BASE_URL}/models`, {
183
- headers: {
184
- Authorization: `Bearer ${apiKey}`,
185
- Accept: "application/json",
186
- },
187
- });
188
-
189
- if (!response.ok) {
190
- throw new Error(`TokenRouter /v1/models returned ${response.status}: ${await response.text()}`);
191
- }
192
-
193
- const payload = (await response.json()) as {
194
- data: Array<Record<string, unknown>>;
195
- };
196
-
197
- return payload.data.map((m) => ({
198
- id: m.id as string,
199
- name: (m.name as string | undefined) ?? (m.id as string),
200
- context_window: (m.context_window ?? m.contextWindow ?? undefined) as number | undefined,
201
- max_tokens: (m.max_tokens ?? m.maxTokens ?? undefined) as number | undefined,
202
- }));
203
- }
204
-
205
- async function getModelsAndPricing(apiKey: string): Promise<{ models: RawModel[]; pricing: Map<string, Pricing> }> {
206
- const cache = readCache();
207
- if (cache && isCacheFresh(cache)) {
208
- const pricingMap = new Map<string, Pricing>(Object.entries(cache.pricing));
209
- return { models: cache.models, pricing: pricingMap };
210
- }
211
-
212
- try {
213
- const [models, pricing] = await Promise.all([
214
- fetchModels(apiKey),
215
- fetchOpenRouterPricing(),
216
- ]);
217
- writeCache(models, pricing);
218
- return { models, pricing };
219
- } catch {
220
- if (cache) {
221
- const pricingMap = new Map<string, Pricing>(Object.entries(cache.pricing));
222
- return { models: cache.models, pricing: pricingMap };
223
- }
224
- throw new Error(
225
- "Failed to fetch models from TokenRouter and no cached data available. " +
226
- "Configure authentication via /login tokenrouter, auth.json, or TOKENROUTER_API_KEY.",
227
- );
228
- }
229
- }
230
-
231
- // ---------------------------------------------------------------------------
232
- // OAuth (API key prompt flow for /login)
233
- // ---------------------------------------------------------------------------
234
-
235
- async function loginTokenRouter(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
236
- const key = await callbacks.onPrompt({ message: "Enter your TokenRouter API key:" });
237
- if (!key || !key.trim()) {
238
- throw new Error("No API key provided");
239
- }
240
- // API keys don't expire — set far-future expiry so pi won't try to refresh
241
- const farFuture = Date.now() + 10 * 365 * 24 * 60 * 60 * 1000;
242
- return {
243
- refresh: key.trim(),
244
- access: key.trim(),
245
- expires: farFuture,
246
- };
247
- }
248
-
249
- async function refreshTokenRouter(credentials: OAuthCredentials): Promise<OAuthCredentials> {
250
- // API keys don't expire — return as-is
251
- return credentials;
252
- }
253
-
254
- // ---------------------------------------------------------------------------
255
- // Extension entry point
256
- // ---------------------------------------------------------------------------
257
-
258
- const oauth = {
259
- name: "TokenRouter",
260
- login: loginTokenRouter,
261
- refreshToken: refreshTokenRouter,
262
- getApiKey: (cred: OAuthCredentials) => cred.access,
263
- };
19
+ import { TOKENROUTER_MODELS } from "./models.generated.js";
20
+ import { createTokenRouterProviderConfig, PROVIDER_NAME } from "./provider-config.js";
264
21
 
265
22
  export default async function(pi: ExtensionAPI) {
266
- // Resolve API key through AuthStorage with full priority chain:
267
- // runtime overrides → auth.json api_key → auth.json oauth → env vars
268
- const authStorage = AuthStorage.create();
269
- const apiKey = await authStorage.getApiKey(PROVIDER_NAME);
270
-
271
- if (!apiKey) {
272
- // Register provider with OAuth only (no models) so /login works.
273
- // After /login, user needs /reload to fetch and register models.
274
- pi.registerProvider(PROVIDER_NAME, {
275
- baseUrl: BASE_URL,
276
- api: "openai-completions",
277
- authHeader: true,
278
- oauth,
279
- });
280
- return;
281
- }
282
-
283
- const { models: rawModels, pricing } = await getModelsAndPricing(apiKey);
284
-
285
- pi.registerProvider(PROVIDER_NAME, {
286
- baseUrl: BASE_URL,
287
- api: "openai-completions",
288
- authHeader: true,
289
- models: rawModels.map((m) => {
290
- const p = pricing.get(m.id);
291
- return {
292
- id: m.id,
293
- name: m.name ?? m.id,
294
- reasoning: isReasoningModel(m.id),
295
- input: supportsVision(m.id) ? (["text", "image"] as const) : (["text"] as const),
296
- cost: p
297
- ? { input: p.input, output: p.output, cacheRead: p.cacheRead, cacheWrite: p.cacheWrite }
298
- : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
299
- contextWindow: m.context_window ?? p?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
300
- maxTokens: m.max_tokens ?? p?.maxTokens ?? DEFAULT_MAX_TOKENS,
301
- };
302
- }),
303
- oauth,
304
- });
23
+ pi.registerProvider(PROVIDER_NAME, createTokenRouterProviderConfig(TOKENROUTER_MODELS));
305
24
  }
@@ -0,0 +1,1003 @@
1
+ import type { TokenRouterProviderModel } from "./provider-config.js";
2
+
3
+ // Derived from TokenRouter /v1/models and enriched with models.dev, then OpenRouter metadata.
4
+ // Generated on 2026-05-07T03:18:45.215Z.
5
+ export const TOKENROUTER_MODELS: TokenRouterProviderModel[] = [
6
+ {
7
+ "id": "claude-haiku-4-5",
8
+ "name": "Claude Haiku 4.5 (latest)",
9
+ "reasoning": true,
10
+ "input": [
11
+ "text",
12
+ "image"
13
+ ],
14
+ "cost": {
15
+ "input": 1,
16
+ "output": 5,
17
+ "cacheRead": 0.1,
18
+ "cacheWrite": 1.25
19
+ },
20
+ "contextWindow": 200000,
21
+ "maxTokens": 64000
22
+ },
23
+ {
24
+ "id": "qwen/qwen3.5-9b",
25
+ "name": "Qwen3.5 9B",
26
+ "reasoning": true,
27
+ "input": [
28
+ "text",
29
+ "image"
30
+ ],
31
+ "cost": {
32
+ "input": 0.1,
33
+ "output": 0.4,
34
+ "cacheRead": 0,
35
+ "cacheWrite": 0
36
+ },
37
+ "contextWindow": 262144,
38
+ "maxTokens": 262144
39
+ },
40
+ {
41
+ "id": "xiaomi/mimo-v2.5-pro",
42
+ "name": "MiMo V2.5 Pro",
43
+ "reasoning": true,
44
+ "input": [
45
+ "text"
46
+ ],
47
+ "cost": {
48
+ "input": 1,
49
+ "output": 3,
50
+ "cacheRead": 0.19999999999999998,
51
+ "cacheWrite": 0
52
+ },
53
+ "contextWindow": 1050000,
54
+ "maxTokens": 131000
55
+ },
56
+ {
57
+ "id": "minimax/minimax-m2.7-highspeed",
58
+ "name": "MiniMax M2.7 High Speed",
59
+ "reasoning": true,
60
+ "input": [
61
+ "text",
62
+ "image"
63
+ ],
64
+ "cost": {
65
+ "input": 0.6,
66
+ "output": 2.4,
67
+ "cacheRead": 0.06,
68
+ "cacheWrite": 0.375
69
+ },
70
+ "contextWindow": 204800,
71
+ "maxTokens": 131100
72
+ },
73
+ {
74
+ "id": "deepseek/deepseek-v4-pro",
75
+ "name": "DeepSeek V4 Pro",
76
+ "reasoning": true,
77
+ "input": [
78
+ "text"
79
+ ],
80
+ "cost": {
81
+ "input": 1.74,
82
+ "output": 3.48,
83
+ "cacheRead": 0.145,
84
+ "cacheWrite": 0
85
+ },
86
+ "contextWindow": 1000000,
87
+ "maxTokens": 384000
88
+ },
89
+ {
90
+ "id": "minimax/minimax-m2.7",
91
+ "name": "Minimax M2.7",
92
+ "reasoning": true,
93
+ "input": [
94
+ "text",
95
+ "image"
96
+ ],
97
+ "cost": {
98
+ "input": 0.3,
99
+ "output": 1.2,
100
+ "cacheRead": 0.06,
101
+ "cacheWrite": 0.375
102
+ },
103
+ "contextWindow": 204800,
104
+ "maxTokens": 131000
105
+ },
106
+ {
107
+ "id": "qwen/qwen3.5-122b-a10b",
108
+ "name": "Qwen3.5 122B A10B",
109
+ "reasoning": true,
110
+ "input": [
111
+ "text",
112
+ "image"
113
+ ],
114
+ "cost": {
115
+ "input": 0.4,
116
+ "output": 3.2,
117
+ "cacheRead": 0,
118
+ "cacheWrite": 0
119
+ },
120
+ "contextWindow": 262144,
121
+ "maxTokens": 262144
122
+ },
123
+ {
124
+ "id": "xiaomi/mimo-v2.5",
125
+ "name": "MiMo M2.5",
126
+ "reasoning": true,
127
+ "input": [
128
+ "text",
129
+ "image"
130
+ ],
131
+ "cost": {
132
+ "input": 0.39999999999999997,
133
+ "output": 2,
134
+ "cacheRead": 0.08,
135
+ "cacheWrite": 0
136
+ },
137
+ "contextWindow": 1050000,
138
+ "maxTokens": 131100
139
+ },
140
+ {
141
+ "id": "qwen/qwen3.5-plus-02-15",
142
+ "name": "Qwen: Qwen3.5 Plus 2026-02-15",
143
+ "reasoning": true,
144
+ "input": [
145
+ "text",
146
+ "image"
147
+ ],
148
+ "cost": {
149
+ "input": 0.26,
150
+ "output": 1.56,
151
+ "cacheRead": 0,
152
+ "cacheWrite": 0.325
153
+ },
154
+ "contextWindow": 1000000,
155
+ "maxTokens": 65536
156
+ },
157
+ {
158
+ "id": "x-ai/grok-4.20-beta",
159
+ "name": "Grok 4.20 Beta",
160
+ "reasoning": true,
161
+ "input": [
162
+ "text",
163
+ "image"
164
+ ],
165
+ "cost": {
166
+ "input": 2,
167
+ "output": 6,
168
+ "cacheRead": 0.2,
169
+ "cacheWrite": 0
170
+ },
171
+ "contextWindow": 2000000,
172
+ "maxTokens": 30000
173
+ },
174
+ {
175
+ "id": "deepseek/deepseek-v3.2",
176
+ "name": "DeepSeek V3.2",
177
+ "reasoning": true,
178
+ "input": [
179
+ "text"
180
+ ],
181
+ "cost": {
182
+ "input": 0.27,
183
+ "output": 0.4,
184
+ "cacheRead": 0.22,
185
+ "cacheWrite": 0
186
+ },
187
+ "contextWindow": 163842,
188
+ "maxTokens": 8000
189
+ },
190
+ {
191
+ "id": "openai/gpt-5.2",
192
+ "name": "GPT-5.2",
193
+ "reasoning": true,
194
+ "input": [
195
+ "text",
196
+ "image"
197
+ ],
198
+ "cost": {
199
+ "input": 1.75,
200
+ "output": 14,
201
+ "cacheRead": 0.18,
202
+ "cacheWrite": 0
203
+ },
204
+ "contextWindow": 400000,
205
+ "maxTokens": 128000
206
+ },
207
+ {
208
+ "id": "anthropic/claude-opus-4.7",
209
+ "name": "Claude Opus 4.7",
210
+ "reasoning": true,
211
+ "input": [
212
+ "text",
213
+ "image"
214
+ ],
215
+ "cost": {
216
+ "input": 5,
217
+ "output": 25,
218
+ "cacheRead": 0.5,
219
+ "cacheWrite": 6.25
220
+ },
221
+ "contextWindow": 1000000,
222
+ "maxTokens": 128000
223
+ },
224
+ {
225
+ "id": "qwen/qwen3.5-35b-a3b",
226
+ "name": "Qwen3.5 35B A3B",
227
+ "reasoning": true,
228
+ "input": [
229
+ "text",
230
+ "image"
231
+ ],
232
+ "cost": {
233
+ "input": 0.25,
234
+ "output": 1.3,
235
+ "cacheRead": 0.049999999999999996,
236
+ "cacheWrite": 0
237
+ },
238
+ "contextWindow": 262144,
239
+ "maxTokens": 262144
240
+ },
241
+ {
242
+ "id": "anthropic/claude-sonnet-4.5",
243
+ "name": "Claude Sonnet 4.5",
244
+ "reasoning": true,
245
+ "input": [
246
+ "text",
247
+ "image"
248
+ ],
249
+ "cost": {
250
+ "input": 3,
251
+ "output": 15,
252
+ "cacheRead": 0.3,
253
+ "cacheWrite": 3.75
254
+ },
255
+ "contextWindow": 200000,
256
+ "maxTokens": 64000
257
+ },
258
+ {
259
+ "id": "stepfun/step-3.5-flash",
260
+ "name": "StepFun: Step 3.5 Flash",
261
+ "reasoning": true,
262
+ "input": [
263
+ "text"
264
+ ],
265
+ "cost": {
266
+ "input": 0.1,
267
+ "output": 0.3,
268
+ "cacheRead": 0.02,
269
+ "cacheWrite": 0
270
+ },
271
+ "contextWindow": 256000,
272
+ "maxTokens": 256000
273
+ },
274
+ {
275
+ "id": "google/gemini-3-flash-preview",
276
+ "name": "Gemini 3 Flash",
277
+ "reasoning": true,
278
+ "input": [
279
+ "text",
280
+ "image"
281
+ ],
282
+ "cost": {
283
+ "input": 0.5,
284
+ "output": 3,
285
+ "cacheRead": 0.05,
286
+ "cacheWrite": 1
287
+ },
288
+ "contextWindow": 1048576,
289
+ "maxTokens": 65536
290
+ },
291
+ {
292
+ "id": "z-ai/glm-5-turbo",
293
+ "name": "Z.ai: GLM 5 Turbo",
294
+ "reasoning": true,
295
+ "input": [
296
+ "text"
297
+ ],
298
+ "cost": {
299
+ "input": 1.2,
300
+ "output": 4,
301
+ "cacheRead": 0.24,
302
+ "cacheWrite": 0
303
+ },
304
+ "contextWindow": 202752,
305
+ "maxTokens": 131072
306
+ },
307
+ {
308
+ "id": "openai/gpt-5.4-pro",
309
+ "name": "GPT 5.4 Pro",
310
+ "reasoning": true,
311
+ "input": [
312
+ "text",
313
+ "image"
314
+ ],
315
+ "cost": {
316
+ "input": 30,
317
+ "output": 180,
318
+ "cacheRead": 0,
319
+ "cacheWrite": 0
320
+ },
321
+ "contextWindow": 1050000,
322
+ "maxTokens": 128000
323
+ },
324
+ {
325
+ "id": "qwen/qwen3.5-397b-a17b",
326
+ "name": "Qwen3.5-397B-A17B",
327
+ "reasoning": true,
328
+ "input": [
329
+ "text",
330
+ "image"
331
+ ],
332
+ "cost": {
333
+ "input": 0,
334
+ "output": 0,
335
+ "cacheRead": 0.195,
336
+ "cacheWrite": 0
337
+ },
338
+ "contextWindow": 262144,
339
+ "maxTokens": 8192
340
+ },
341
+ {
342
+ "id": "anthropic/claude-opus-4.5",
343
+ "name": "Claude Opus 4.5",
344
+ "reasoning": true,
345
+ "input": [
346
+ "text",
347
+ "image"
348
+ ],
349
+ "cost": {
350
+ "input": 5,
351
+ "output": 25,
352
+ "cacheRead": 0.5,
353
+ "cacheWrite": 18.75
354
+ },
355
+ "contextWindow": 200000,
356
+ "maxTokens": 64000
357
+ },
358
+ {
359
+ "id": "mistralai/devstral-2512",
360
+ "name": "Mistral: Devstral 2 2512",
361
+ "reasoning": false,
362
+ "input": [
363
+ "text"
364
+ ],
365
+ "cost": {
366
+ "input": 0.4,
367
+ "output": 2,
368
+ "cacheRead": 0.025,
369
+ "cacheWrite": 0
370
+ },
371
+ "contextWindow": 262144,
372
+ "maxTokens": 65536
373
+ },
374
+ {
375
+ "id": "anthropic/claude-sonnet-4",
376
+ "name": "Claude Sonnet 4",
377
+ "reasoning": true,
378
+ "input": [
379
+ "text",
380
+ "image"
381
+ ],
382
+ "cost": {
383
+ "input": 3,
384
+ "output": 15,
385
+ "cacheRead": 0.3,
386
+ "cacheWrite": 3.75
387
+ },
388
+ "contextWindow": 200000,
389
+ "maxTokens": 64000
390
+ },
391
+ {
392
+ "id": "x-ai/grok-4.3",
393
+ "name": "xAI: Grok 4.3",
394
+ "reasoning": true,
395
+ "input": [
396
+ "text",
397
+ "image"
398
+ ],
399
+ "cost": {
400
+ "input": 1.25,
401
+ "output": 2.5,
402
+ "cacheRead": 0.2,
403
+ "cacheWrite": 0
404
+ },
405
+ "contextWindow": 1000000,
406
+ "maxTokens": 4096
407
+ },
408
+ {
409
+ "id": "deepseek-v4-flash",
410
+ "name": "DeepSeek V4 Flash",
411
+ "reasoning": true,
412
+ "input": [
413
+ "text"
414
+ ],
415
+ "cost": {
416
+ "input": 0.14,
417
+ "output": 0.28,
418
+ "cacheRead": 0.028,
419
+ "cacheWrite": 0
420
+ },
421
+ "contextWindow": 1000000,
422
+ "maxTokens": 384000
423
+ },
424
+ {
425
+ "id": "z-ai/glm-5.1",
426
+ "name": "GLM-5.1",
427
+ "reasoning": true,
428
+ "input": [
429
+ "text"
430
+ ],
431
+ "cost": {
432
+ "input": 0,
433
+ "output": 0,
434
+ "cacheRead": 0.5249999999999999,
435
+ "cacheWrite": 0
436
+ },
437
+ "contextWindow": 131072,
438
+ "maxTokens": 131072
439
+ },
440
+ {
441
+ "id": "anthropic/claude-sonnet-4.6",
442
+ "name": "Claude Sonnet 4.6",
443
+ "reasoning": true,
444
+ "input": [
445
+ "text",
446
+ "image"
447
+ ],
448
+ "cost": {
449
+ "input": 3,
450
+ "output": 15,
451
+ "cacheRead": 0.3,
452
+ "cacheWrite": 3.75
453
+ },
454
+ "contextWindow": 1000000,
455
+ "maxTokens": 128000
456
+ },
457
+ {
458
+ "id": "anthropic/claude-haiku-4.5",
459
+ "name": "Claude Haiku 4.5",
460
+ "reasoning": true,
461
+ "input": [
462
+ "text",
463
+ "image"
464
+ ],
465
+ "cost": {
466
+ "input": 1,
467
+ "output": 5,
468
+ "cacheRead": 0.1,
469
+ "cacheWrite": 1.25
470
+ },
471
+ "contextWindow": 200000,
472
+ "maxTokens": 64000
473
+ },
474
+ {
475
+ "id": "openai/gpt-5.4",
476
+ "name": "GPT 5.4",
477
+ "reasoning": true,
478
+ "input": [
479
+ "text",
480
+ "image"
481
+ ],
482
+ "cost": {
483
+ "input": 2.5,
484
+ "output": 15,
485
+ "cacheRead": 0.25,
486
+ "cacheWrite": 0
487
+ },
488
+ "contextWindow": 1050000,
489
+ "maxTokens": 128000
490
+ },
491
+ {
492
+ "id": "xiaomi/mimo-v2-pro",
493
+ "name": "MiMo V2 Pro",
494
+ "reasoning": true,
495
+ "input": [
496
+ "text"
497
+ ],
498
+ "cost": {
499
+ "input": 1,
500
+ "output": 3,
501
+ "cacheRead": 0.19999999999999998,
502
+ "cacheWrite": 0
503
+ },
504
+ "contextWindow": 1000000,
505
+ "maxTokens": 128000
506
+ },
507
+ {
508
+ "id": "z-ai/glm-4.6v",
509
+ "name": "Z.ai: GLM 4.6V",
510
+ "reasoning": true,
511
+ "input": [
512
+ "text",
513
+ "image"
514
+ ],
515
+ "cost": {
516
+ "input": 0.3,
517
+ "output": 0.9,
518
+ "cacheRead": 0.049999999999999996,
519
+ "cacheWrite": 0
520
+ },
521
+ "contextWindow": 131072,
522
+ "maxTokens": 131072
523
+ },
524
+ {
525
+ "id": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
526
+ "name": "NVIDIA: Nemotron 3 Nano Omni (free)",
527
+ "reasoning": true,
528
+ "input": [
529
+ "text",
530
+ "image"
531
+ ],
532
+ "cost": {
533
+ "input": 0,
534
+ "output": 0,
535
+ "cacheRead": 0,
536
+ "cacheWrite": 0
537
+ },
538
+ "contextWindow": 256000,
539
+ "maxTokens": 65536
540
+ },
541
+ {
542
+ "id": "deepseek/deepseek-v4-flash",
543
+ "name": "DeepSeek V4 Flash",
544
+ "reasoning": true,
545
+ "input": [
546
+ "text"
547
+ ],
548
+ "cost": {
549
+ "input": 0.14,
550
+ "output": 0.28,
551
+ "cacheRead": 0.028,
552
+ "cacheWrite": 0
553
+ },
554
+ "contextWindow": 1000000,
555
+ "maxTokens": 384000
556
+ },
557
+ {
558
+ "id": "google/gemini-3-pro-image-preview",
559
+ "name": "Google: Nano Banana Pro (Gemini 3 Pro Image Preview)",
560
+ "reasoning": true,
561
+ "input": [
562
+ "text",
563
+ "image"
564
+ ],
565
+ "cost": {
566
+ "input": 2,
567
+ "output": 12,
568
+ "cacheRead": 0.19999999999999998,
569
+ "cacheWrite": 0.375
570
+ },
571
+ "contextWindow": 65536,
572
+ "maxTokens": 32768
573
+ },
574
+ {
575
+ "id": "google/gemini-3.1-flash-image-preview",
576
+ "name": "Gemini 3.1 Flash Image Preview (Nano Banana 2)",
577
+ "reasoning": true,
578
+ "input": [
579
+ "text",
580
+ "image"
581
+ ],
582
+ "cost": {
583
+ "input": 0.5,
584
+ "output": 3,
585
+ "cacheRead": 0,
586
+ "cacheWrite": 0
587
+ },
588
+ "contextWindow": 131072,
589
+ "maxTokens": 32768
590
+ },
591
+ {
592
+ "id": "openai/gpt-4o-mini",
593
+ "name": "GPT-4o mini",
594
+ "reasoning": false,
595
+ "input": [
596
+ "text",
597
+ "image"
598
+ ],
599
+ "cost": {
600
+ "input": 0,
601
+ "output": 0,
602
+ "cacheRead": 0.075,
603
+ "cacheWrite": 0
604
+ },
605
+ "contextWindow": 128000,
606
+ "maxTokens": 16384
607
+ },
608
+ {
609
+ "id": "z-ai/glm-4.6",
610
+ "name": "Z.ai: GLM 4.6",
611
+ "reasoning": true,
612
+ "input": [
613
+ "text"
614
+ ],
615
+ "cost": {
616
+ "input": 0.39,
617
+ "output": 1.9,
618
+ "cacheRead": 0.175,
619
+ "cacheWrite": 0
620
+ },
621
+ "contextWindow": 204800,
622
+ "maxTokens": 204800
623
+ },
624
+ {
625
+ "id": "openai/gpt-5.5",
626
+ "name": "GPT 5.5",
627
+ "reasoning": true,
628
+ "input": [
629
+ "text",
630
+ "image"
631
+ ],
632
+ "cost": {
633
+ "input": 5,
634
+ "output": 30,
635
+ "cacheRead": 0.5,
636
+ "cacheWrite": 0
637
+ },
638
+ "contextWindow": 1000000,
639
+ "maxTokens": 128000
640
+ },
641
+ {
642
+ "id": "anthropic/claude-opus-4.6",
643
+ "name": "Claude Opus 4.6",
644
+ "reasoning": true,
645
+ "input": [
646
+ "text",
647
+ "image"
648
+ ],
649
+ "cost": {
650
+ "input": 5,
651
+ "output": 25,
652
+ "cacheRead": 0.5,
653
+ "cacheWrite": 6.25
654
+ },
655
+ "contextWindow": 1000000,
656
+ "maxTokens": 128000
657
+ },
658
+ {
659
+ "id": "z-ai/glm-4.7",
660
+ "name": "Z.ai: GLM 4.7",
661
+ "reasoning": true,
662
+ "input": [
663
+ "text"
664
+ ],
665
+ "cost": {
666
+ "input": 0.38,
667
+ "output": 1.98,
668
+ "cacheRead": 0.2,
669
+ "cacheWrite": 0
670
+ },
671
+ "contextWindow": 202752,
672
+ "maxTokens": 65535
673
+ },
674
+ {
675
+ "id": "x-ai/grok-4.1-fast",
676
+ "name": "xAI: Grok 4.1 Fast",
677
+ "reasoning": true,
678
+ "input": [
679
+ "text",
680
+ "image"
681
+ ],
682
+ "cost": {
683
+ "input": 0.2,
684
+ "output": 0.5,
685
+ "cacheRead": 0.05,
686
+ "cacheWrite": 0
687
+ },
688
+ "contextWindow": 2000000,
689
+ "maxTokens": 30000
690
+ },
691
+ {
692
+ "id": "qwen/qwen3-coder-next",
693
+ "name": "Qwen: Qwen3 Coder Next",
694
+ "reasoning": false,
695
+ "input": [
696
+ "text"
697
+ ],
698
+ "cost": {
699
+ "input": 0.12,
700
+ "output": 0.75,
701
+ "cacheRead": 0.035,
702
+ "cacheWrite": 0
703
+ },
704
+ "contextWindow": 262144,
705
+ "maxTokens": 65536
706
+ },
707
+ {
708
+ "id": "deepseek-v3.2",
709
+ "name": "DeepSeek-V3.2",
710
+ "reasoning": true,
711
+ "input": [
712
+ "text"
713
+ ],
714
+ "cost": {
715
+ "input": 0.58,
716
+ "output": 1.68,
717
+ "cacheRead": 0,
718
+ "cacheWrite": 0
719
+ },
720
+ "contextWindow": 128000,
721
+ "maxTokens": 128000
722
+ },
723
+ {
724
+ "id": "google/gemini-3.1-pro-preview",
725
+ "name": "Gemini 3.1 Pro Preview",
726
+ "reasoning": true,
727
+ "input": [
728
+ "text",
729
+ "image"
730
+ ],
731
+ "cost": {
732
+ "input": 2,
733
+ "output": 12,
734
+ "cacheRead": 0.2,
735
+ "cacheWrite": 0.375
736
+ },
737
+ "contextWindow": 1000000,
738
+ "maxTokens": 64000
739
+ },
740
+ {
741
+ "id": "xiaomi/mimo-v2-flash",
742
+ "name": "MiMo V2 Flash",
743
+ "reasoning": true,
744
+ "input": [
745
+ "text"
746
+ ],
747
+ "cost": {
748
+ "input": 0.1,
749
+ "output": 0.29,
750
+ "cacheRead": 0.045,
751
+ "cacheWrite": 0
752
+ },
753
+ "contextWindow": 262144,
754
+ "maxTokens": 32000
755
+ },
756
+ {
757
+ "id": "moonshotai/kimi-k2.6",
758
+ "name": "Kimi K2.6",
759
+ "reasoning": true,
760
+ "input": [
761
+ "text",
762
+ "image"
763
+ ],
764
+ "cost": {
765
+ "input": 0.95,
766
+ "output": 4,
767
+ "cacheRead": 0.16,
768
+ "cacheWrite": 0
769
+ },
770
+ "contextWindow": 262000,
771
+ "maxTokens": 262000
772
+ },
773
+ {
774
+ "id": "google/gemini-2.5-flash-image",
775
+ "name": "Nano Banana (Gemini 2.5 Flash Image)",
776
+ "reasoning": false,
777
+ "input": [
778
+ "text",
779
+ "image"
780
+ ],
781
+ "cost": {
782
+ "input": 0.3,
783
+ "output": 2.5,
784
+ "cacheRead": 0.03,
785
+ "cacheWrite": 0.08333333333333334
786
+ },
787
+ "contextWindow": 32768,
788
+ "maxTokens": 32768
789
+ },
790
+ {
791
+ "id": "deepseek-v4-pro",
792
+ "name": "DeepSeek V4 Pro",
793
+ "reasoning": true,
794
+ "input": [
795
+ "text"
796
+ ],
797
+ "cost": {
798
+ "input": 1.74,
799
+ "output": 3.48,
800
+ "cacheRead": 0.145,
801
+ "cacheWrite": 0
802
+ },
803
+ "contextWindow": 1000000,
804
+ "maxTokens": 384000
805
+ },
806
+ {
807
+ "id": "qwen/qwen3.5-flash",
808
+ "name": "Qwen3.5 Flash",
809
+ "reasoning": false,
810
+ "input": [
811
+ "text",
812
+ "image"
813
+ ],
814
+ "cost": {
815
+ "input": 0.1,
816
+ "output": 0.4,
817
+ "cacheRead": 0,
818
+ "cacheWrite": 0
819
+ },
820
+ "contextWindow": 1020000,
821
+ "maxTokens": 1020000
822
+ },
823
+ {
824
+ "id": "moonshotai/kimi-k2.5",
825
+ "name": "Kimi K2.5",
826
+ "reasoning": true,
827
+ "input": [
828
+ "text",
829
+ "image"
830
+ ],
831
+ "cost": {
832
+ "input": 0.21,
833
+ "output": 1,
834
+ "cacheRead": 0.03,
835
+ "cacheWrite": 0
836
+ },
837
+ "contextWindow": 262144,
838
+ "maxTokens": 262144
839
+ },
840
+ {
841
+ "id": "z-ai/glm-4.5-air",
842
+ "name": "Z.ai: GLM 4.5 Air",
843
+ "reasoning": true,
844
+ "input": [
845
+ "text"
846
+ ],
847
+ "cost": {
848
+ "input": 0.13,
849
+ "output": 0.85,
850
+ "cacheRead": 0.025,
851
+ "cacheWrite": 0
852
+ },
853
+ "contextWindow": 131072,
854
+ "maxTokens": 98304
855
+ },
856
+ {
857
+ "id": "minimax/minimax-m2.5",
858
+ "name": "MiniMax M2.5",
859
+ "reasoning": true,
860
+ "input": [
861
+ "text"
862
+ ],
863
+ "cost": {
864
+ "input": 0.14,
865
+ "output": 0.56,
866
+ "cacheRead": 0.014,
867
+ "cacheWrite": 0
868
+ },
869
+ "contextWindow": 1000000,
870
+ "maxTokens": 131072
871
+ },
872
+ {
873
+ "id": "qwen/qwen3.6-plus",
874
+ "name": "Qwen: Qwen3.6 Plus",
875
+ "reasoning": true,
876
+ "input": [
877
+ "text",
878
+ "image"
879
+ ],
880
+ "cost": {
881
+ "input": 0.325,
882
+ "output": 1.95,
883
+ "cacheRead": 0.0325,
884
+ "cacheWrite": 0.40625
885
+ },
886
+ "contextWindow": 1000000,
887
+ "maxTokens": 65536
888
+ },
889
+ {
890
+ "id": "xiaomi/mimo-v2-omni",
891
+ "name": "Xiaomi: MiMo-V2-Omni",
892
+ "reasoning": true,
893
+ "input": [
894
+ "text",
895
+ "image"
896
+ ],
897
+ "cost": {
898
+ "input": 0.4,
899
+ "output": 2,
900
+ "cacheRead": 0.08,
901
+ "cacheWrite": 0
902
+ },
903
+ "contextWindow": 262144,
904
+ "maxTokens": 65536
905
+ },
906
+ {
907
+ "id": "openai/gpt-5-mini",
908
+ "name": "GPT-5 Mini",
909
+ "reasoning": true,
910
+ "input": [
911
+ "text",
912
+ "image"
913
+ ],
914
+ "cost": {
915
+ "input": 0.25,
916
+ "output": 2,
917
+ "cacheRead": 0.025,
918
+ "cacheWrite": 0
919
+ },
920
+ "contextWindow": 400000,
921
+ "maxTokens": 128000
922
+ },
923
+ {
924
+ "id": "nvidia/nemotron-3-super-120b-a12b",
925
+ "name": "NVIDIA Nemotron 3 Super 120B A12B",
926
+ "reasoning": true,
927
+ "input": [
928
+ "text"
929
+ ],
930
+ "cost": {
931
+ "input": 0.15,
932
+ "output": 0.65,
933
+ "cacheRead": 0,
934
+ "cacheWrite": 0
935
+ },
936
+ "contextWindow": 256000,
937
+ "maxTokens": 32000
938
+ },
939
+ {
940
+ "id": "minimax/minimax-m2-her",
941
+ "name": "MiniMax: MiniMax M2-her",
942
+ "reasoning": false,
943
+ "input": [
944
+ "text"
945
+ ],
946
+ "cost": {
947
+ "input": 0.3,
948
+ "output": 1.2,
949
+ "cacheRead": 0.03,
950
+ "cacheWrite": 0
951
+ },
952
+ "contextWindow": 65536,
953
+ "maxTokens": 2048
954
+ },
955
+ {
956
+ "id": "minimax/minimax-m2.1",
957
+ "name": "MiniMax M2.1",
958
+ "reasoning": true,
959
+ "input": [
960
+ "text"
961
+ ],
962
+ "cost": {
963
+ "input": 0.3,
964
+ "output": 1.2,
965
+ "cacheRead": 0.03,
966
+ "cacheWrite": 0.38
967
+ },
968
+ "contextWindow": 204800,
969
+ "maxTokens": 131072
970
+ },
971
+ {
972
+ "id": "minimax/minimax-m2.1-highspeed",
973
+ "name": "minimax/minimax-m2.1-highspeed",
974
+ "reasoning": false,
975
+ "input": [
976
+ "text"
977
+ ],
978
+ "cost": {
979
+ "input": 0,
980
+ "output": 0,
981
+ "cacheRead": 0,
982
+ "cacheWrite": 0
983
+ },
984
+ "contextWindow": 4096,
985
+ "maxTokens": 4096
986
+ },
987
+ {
988
+ "id": "z-ai/glm-5",
989
+ "name": "GLM-5",
990
+ "reasoning": true,
991
+ "input": [
992
+ "text"
993
+ ],
994
+ "cost": {
995
+ "input": 0.95,
996
+ "output": 3.15,
997
+ "cacheRead": 0.12,
998
+ "cacheWrite": 0
999
+ },
1000
+ "contextWindow": 204800,
1001
+ "maxTokens": 131072
1002
+ }
1003
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tokenrouter",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "TokenRouter provider extension for pi — dynamic model discovery with OpenRouter-compatible pricing",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,9 +19,14 @@
19
19
  },
20
20
  "files": [
21
21
  "index.ts",
22
+ "models.generated.ts",
23
+ "provider-config.ts",
22
24
  "README.md",
23
25
  "LICENSE"
24
26
  ],
27
+ "scripts": {
28
+ "generate-models": "node scripts/generate-models.mjs"
29
+ },
25
30
  "pi": {
26
31
  "extensions": [
27
32
  "./index.ts"
@@ -0,0 +1,30 @@
1
+ export const BASE_URL = "https://api.tokenrouter.com/v1";
2
+ export const PROVIDER_NAME = "tokenrouter";
3
+ export const PROVIDER_DISPLAY_NAME = "TokenRouter";
4
+ export const PROVIDER_API_KEY_ENV = "TOKENROUTER_API_KEY";
5
+
6
+ export type TokenRouterProviderModel = {
7
+ id: string;
8
+ name: string;
9
+ reasoning: boolean;
10
+ input: ("text" | "image")[];
11
+ cost: {
12
+ input: number;
13
+ output: number;
14
+ cacheRead: number;
15
+ cacheWrite: number;
16
+ };
17
+ contextWindow: number;
18
+ maxTokens: number;
19
+ };
20
+
21
+ export function createTokenRouterProviderConfig(models: TokenRouterProviderModel[]) {
22
+ return {
23
+ name: PROVIDER_DISPLAY_NAME,
24
+ baseUrl: BASE_URL,
25
+ api: "openai-completions" as const,
26
+ apiKey: PROVIDER_API_KEY_ENV,
27
+ authHeader: true,
28
+ models,
29
+ };
30
+ }