@aeye/openrouter 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +152 -65
  2. package/aeye-openrouter.md +175 -161
  3. package/dist/__integration__/openrouter.integration.test.js +122 -6
  4. package/dist/__integration__/openrouter.integration.test.js.map +1 -1
  5. package/dist/__tests__/batch.test.d.ts +5 -0
  6. package/dist/__tests__/batch.test.d.ts.map +1 -0
  7. package/dist/__tests__/batch.test.js +41 -0
  8. package/dist/__tests__/batch.test.js.map +1 -0
  9. package/dist/__tests__/features.test.d.ts +5 -0
  10. package/dist/__tests__/features.test.d.ts.map +1 -0
  11. package/dist/__tests__/features.test.js +206 -0
  12. package/dist/__tests__/features.test.js.map +1 -0
  13. package/dist/__tests__/openrouter.test.js +185 -0
  14. package/dist/__tests__/openrouter.test.js.map +1 -1
  15. package/dist/__tests__/rerank-decide.test.d.ts +5 -0
  16. package/dist/__tests__/rerank-decide.test.d.ts.map +1 -0
  17. package/dist/__tests__/rerank-decide.test.js +75 -0
  18. package/dist/__tests__/rerank-decide.test.js.map +1 -0
  19. package/dist/__tests__/schema-rejection.test.d.ts +6 -0
  20. package/dist/__tests__/schema-rejection.test.d.ts.map +1 -0
  21. package/dist/__tests__/schema-rejection.test.js +39 -0
  22. package/dist/__tests__/schema-rejection.test.js.map +1 -0
  23. package/dist/openrouter.d.ts +118 -41
  24. package/dist/openrouter.d.ts.map +1 -1
  25. package/dist/openrouter.js +407 -210
  26. package/dist/openrouter.js.map +1 -1
  27. package/dist/source.d.ts +9 -4
  28. package/dist/source.d.ts.map +1 -1
  29. package/dist/source.js +45 -28
  30. package/dist/source.js.map +1 -1
  31. package/dist/types.d.ts +244 -81
  32. package/dist/types.d.ts.map +1 -1
  33. package/package.json +71 -71
package/README.md CHANGED
@@ -4,13 +4,17 @@ OpenRouter provider for the @aeye (AI TypeScript) framework. OpenRouter provides
4
4
 
5
5
  ## Features
6
6
 
7
- - **Multi-Provider Access**: Access models from OpenAI, Anthropic, Google, Meta, and more through one API
7
+ - **Multi-Provider Access**: Access models from OpenAI, Anthropic, Google, xAI, Meta, and more through one API
8
8
  - **Automatic Fallbacks**: Seamless failover to alternative providers if primary fails
9
9
  - **Intelligent Routing**: Automatic selection of best provider for each request
10
10
  - **Cost Tracking**: Built-in cost information in responses
11
11
  - **ZDR Support**: Zero Data Retention mode for privacy-sensitive applications
12
12
  - **Provider Preferences**: Control which providers to use, prefer, or avoid
13
- - **Reasoning Models**: Support for advanced reasoning with o1, Claude, and others
13
+ - **Reasoning Models**: Effort or token-budget reasoning for GPT, Claude, Gemini, Grok, and others
14
+ - **Server Tools**: Web search, web fetch, and image generation run by OpenRouter
15
+ - **Media**: Image generation/editing, speech, transcription, and embeddings
16
+ - **Reranking, Decisions, Batches**: `/rerank` (Cohere, Voyage, Qwen, Nemotron), the alpha decisions endpoint (System One models such as Jev and Kev), and inline batches
17
+ - **Plugins & Fallback Models**: PDF parsing, response healing, context compression, and model fallbacks
14
18
  - **Streaming**: Full streaming support across compatible models
15
19
  - **Extends OpenAI**: Built on the OpenAI provider for compatibility
16
20
 
@@ -20,6 +24,8 @@ OpenRouter provider for the @aeye (AI TypeScript) framework. OpenRouter provides
20
24
  npm install @aeye/openrouter @aeye/openai @aeye/ai @aeye/core openai zod
21
25
  ```
22
26
 
27
+ Requires Node >= 22 and the `openai` SDK v7.
28
+
23
29
  ## Quick Start
24
30
 
25
31
  ```typescript
@@ -44,7 +50,7 @@ const ai = AI.with()
44
50
  const response = await ai.chat.get([
45
51
  { role: 'user', content: 'Explain TypeScript generics' }
46
52
  ], {
47
- metadata: { model: 'openai/gpt-4-turbo' }
53
+ metadata: { model: 'openai/gpt-5.4-mini' }
48
54
  });
49
55
 
50
56
  console.log(response.content);
@@ -78,30 +84,38 @@ const provider = new OpenRouterProvider({
78
84
  siteUrl: 'https://yourapp.com',
79
85
  appName: 'My AI App',
80
86
 
81
- // Automatic fallbacks
82
- allowFallbacks: true, // Enable fallback to alternative providers
83
-
84
- // Provider preferences
87
+ // Provider routing (sent as OpenRouter's `provider` object)
85
88
  providers: {
86
- prefer: ['openai', 'anthropic'], // Prefer these providers
87
- allow: ['openai', 'anthropic', 'google'], // Only use these providers
88
- deny: ['provider-x'], // Never use these providers
89
- ignore: ['low-quality-provider'], // Exclude from consideration
90
- order: ['openai', 'anthropic'], // Preferred order
91
- quantizations: ['int8', 'fp16'], // Preferred quantization levels
92
- dataCollection: 'deny', // or 'allow' - control data collection
89
+ order: ['openai', 'anthropic'], // Preferred order
90
+ allowFallbacks: true, // Fall back to other providers
91
+ only: ['openai', 'anthropic', 'google'], // Only use these providers
92
+ ignore: ['low-quality-provider'], // Never use these providers
93
+ quantizations: ['int8', 'fp16'], // Allowed quantization levels
94
+ sort: { by: 'throughput', partition: 'none' }, // or 'price' | 'latency' | 'exacto'
95
+ preferredMinThroughput: 50, // tokens/s, median
96
+ preferredMaxLatency: 2, // seconds, median
97
+ dataCollection: 'deny', // or 'allow'
98
+ zdr: true, // Zero data retention endpoints only
99
+ enforceDistillableText: false, // Only providers that allow training on outputs
100
+ requireParameters: true, // Only providers that support every parameter
101
+ maxPrice: { prompt: 10, completion: 30 },
93
102
  },
94
103
 
95
- // Privacy & security
96
- requireParameters: true, // Require all providers to support parameters
97
- dataCollection: 'deny', // Global data collection setting
104
+ // Models to try, in order, when the requested one fails
105
+ fallbackModels: ['openai/gpt-5.4-mini', 'anthropic/claude-sonnet-4.5'],
98
106
 
99
- // Model transformations
100
- transforms: ['middle-out'], // Apply transformations to requests
107
+ // Plugins
108
+ plugins: [
109
+ { id: 'file-parser', pdf: { engine: 'cloudflare-ai' } },
110
+ { id: 'response-healing' },
111
+ { id: 'context-compression', engine: 'middle-out' },
112
+ ],
101
113
  },
102
114
  });
103
115
  ```
104
116
 
117
+ `transforms` is deprecated: OpenRouter replaced transforms with plugins, and `transforms: ['middle-out']` is sent as the `context-compression` plugin. `OpenRouterConfig` has no `api` option; requests always go through Chat Completions.
118
+
105
119
  ## Usage Examples
106
120
 
107
121
  ### Basic Chat with Cost Tracking
@@ -110,7 +124,7 @@ const provider = new OpenRouterProvider({
110
124
  const response = await ai.chat.get([
111
125
  { role: 'user', content: 'What is the capital of France?' }
112
126
  ], {
113
- metadata: { model: 'openai/gpt-3.5-turbo' }
127
+ metadata: { model: 'openai/gpt-5.4-nano' }
114
128
  });
115
129
 
116
130
  console.log(response.content);
@@ -125,12 +139,12 @@ OpenRouter allows you to access models from different providers using the format
125
139
  ```typescript
126
140
  // OpenAI GPT-4
127
141
  const gpt4 = await ai.chat.get(messages, {
128
- metadata: { model: 'openai/gpt-4-turbo' }
142
+ metadata: { model: 'openai/gpt-5.4-mini' }
129
143
  });
130
144
 
131
145
  // Anthropic Claude
132
146
  const claude = await ai.chat.get(messages, {
133
- metadata: { model: 'anthropic/claude-3-opus' }
147
+ metadata: { model: 'anthropic/claude-opus-4.5' }
134
148
  });
135
149
 
136
150
  // Google Gemini
@@ -184,7 +198,7 @@ const provider = new OpenRouterProvider({
184
198
 
185
199
  // If OpenAI is down, automatically tries Anthropic, then Google
186
200
  const response = await ai.chat.get(messages, {
187
- metadata: { model: 'openai/gpt-4-turbo' }
201
+ metadata: { model: 'openai/gpt-5.4-mini' }
188
202
  });
189
203
  ```
190
204
 
@@ -217,7 +231,7 @@ let totalCost = 0;
217
231
  for await (const chunk of ai.chat.stream([
218
232
  { role: 'user', content: 'Write a story about a robot' }
219
233
  ], {
220
- metadata: { model: 'anthropic/claude-3-sonnet' }
234
+ metadata: { model: 'anthropic/claude-sonnet-4.5' }
221
235
  })) {
222
236
  if (chunk.content) {
223
237
  process.stdout.write(chunk.content);
@@ -235,14 +249,15 @@ console.log(`\n\nTotal cost: $${totalCost}`);
235
249
  ### Reasoning Models
236
250
 
237
251
  ```typescript
238
- // Use OpenAI o1 through OpenRouter
239
- const response = await ai.chat.get([
240
- {
252
+ // Reason with an effort, or a token budget via reason.maxTokens (never both)
253
+ const response = await ai.chat.get({
254
+ messages: [{
241
255
  role: 'user',
242
256
  content: 'Solve: If a train leaves NYC at 3pm going 60mph, and another leaves Boston at 4pm going 80mph, when do they meet?'
243
- }
244
- ], {
245
- metadata: { model: 'openai/o1-preview' }
257
+ }],
258
+ reason: { effort: 'high', exclude: false },
259
+ }, {
260
+ metadata: { model: 'openai/gpt-5.5' }
246
261
  });
247
262
 
248
263
  console.log('Reasoning:', response.reasoning);
@@ -250,15 +265,67 @@ console.log('Answer:', response.content);
250
265
  console.log('Cost:', response.usage.cost);
251
266
  ```
252
267
 
268
+ ### Server Tools
269
+
270
+ ```typescript
271
+ const response = await ai.chat.get({
272
+ messages: [{ role: 'user', content: 'What did the TypeScript team ship this month?' }],
273
+ hostedTools: [
274
+ { type: 'web_search', maxResults: 5, allowedDomains: ['devblogs.microsoft.com'], blockedDomains: ['example.com'] },
275
+ { type: 'web_fetch' },
276
+ ],
277
+ }, {
278
+ metadata: { model: 'openai/gpt-5.4-mini' }
279
+ });
280
+
281
+ console.log(response.citations);
282
+ console.log(response.usage?.tools); // { web_search: 1 }
283
+ ```
284
+
285
+ `web_search`, `web_fetch`, and `image_generation` run as `openrouter:*` server tools; `file_search`, `code_interpreter`, and `mcp` are rejected.
286
+
287
+ ### Images, Speech, Transcription, Embeddings
288
+
289
+ ```typescript
290
+ // Images go through chat with the image modality; size becomes an aspect ratio
291
+ const image = await ai.image.generate.get({ prompt: 'A lighthouse at dusk', size: '1536x1024' });
292
+
293
+ // Embeddings, speech, and transcription use OpenRouter's OpenAI-compatible endpoints
294
+ const { embeddings } = await ai.embed.get({ texts: ['hello'] });
295
+ const transcript = await ai.transcribe.get({ audio: fs.readFileSync('note.mp3') });
296
+ ```
297
+
298
+ Defaults: `google/gemini-2.5-flash-image` for images, `openai/gpt-transcribe`, `openai/text-embedding-3-small`, and `openai/gpt-5.4-mini` for image analysis; speech has no default model. Image and transcription streaming fall back to the non-streaming methods.
299
+
300
+ ### Reranking, Decisions, Batches
301
+
302
+ ```typescript
303
+ // Order documents by relevance (default cohere/rerank-v3.5)
304
+ const { results } = await ai.rerank.get({ query: 'refund policy', documents, topN: 3 });
305
+
306
+ // Typed answers from a decision model (default typesafe/jev-1.14)
307
+ const { answers } = await ai.decide.get({
308
+ input: 'I was charged twice for my order.',
309
+ questions: {
310
+ billing: { type: 'predicate', instructions: 'Is this a billing problem?' },
311
+ team: { type: 'choice', instructions: 'Which team handles it?', choices: { payments: 'Charges and refunds', web: 'Website bugs' } },
312
+ },
313
+ });
314
+
315
+ // Chat requests as one batch, posted inline to /batches
316
+ const job = await ai.batch.submit(requests, { metadata: { model: 'openai/gpt-5.4-mini' } });
317
+ const done = await ai.batch.wait(job, {});
318
+ ```
319
+
320
+ Rerank and decision costs come back as `usage.cost`. Decisions use OpenRouter's alpha endpoint, outside the versioned API. Change the defaults with `defaultModels.rerank` and `defaultModels.decision`.
321
+
253
322
  ### Function Calling
254
323
 
255
324
  ```typescript
256
325
  import z from 'zod';
257
326
 
258
- const response = await ai.chat.get([
259
- { role: 'user', content: 'What is the weather in Paris?' }
260
- ], {
261
- metadata: { model: 'openai/gpt-4-turbo' },
327
+ const response = await ai.chat.get({
328
+ messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
262
329
  tools: [
263
330
  {
264
331
  name: 'get_weather',
@@ -270,9 +337,13 @@ const response = await ai.chat.get([
270
337
  }
271
338
  ],
272
339
  toolChoice: 'auto',
340
+ }, {
341
+ metadata: { model: 'openai/gpt-5.4-mini' },
273
342
  });
274
343
  ```
275
344
 
345
+ `toolChoice: { allowed: [...], required? }` is emulated by sending only the allowed tools, because OpenRouter ignores `allowed_tools`. Free-text tools (`format`) go as one-field functions with any grammar in the description; `defer` and `async` are not sent.
346
+
276
347
  ### Fetching Available Models
277
348
 
278
349
  OpenRouter provides a model source that can fetch all available models:
@@ -313,38 +384,43 @@ const response = await ai.chat.get(messages);
313
384
 
314
385
  ### OpenRouterConfig
315
386
 
316
- Extends `OpenAIConfig` from `@aeye/openai`.
387
+ Extends `OpenAIConfig` from `@aeye/openai`, minus its `api` option.
317
388
 
318
389
  | Property | Type | Required | Description |
319
390
  |----------|------|----------|-------------|
320
391
  | `apiKey` | `string` | Yes | OpenRouter API key |
321
392
  | `baseURL` | `string` | No | Custom base URL (defaults to https://openrouter.ai/api/v1) |
322
393
  | `defaultParams` | `object` | No | Default parameters for all requests |
394
+ | `defaultModels` | `object` | No | Per-operation defaults; built in: `cohere/rerank-v3.5` (rerank), `typesafe/jev-1.14` (decision), plus the image, transcription, and embedding defaults above |
323
395
 
324
396
  ### defaultParams
325
397
 
326
398
  | Property | Type | Description |
327
399
  |----------|------|-------------|
328
- | `siteUrl` | `string` | Your app URL (helps with rankings on OpenRouter) |
329
- | `appName` | `string` | Your app name (shown in OpenRouter dashboard) |
330
- | `allowFallbacks` | `boolean` | Enable automatic provider fallbacks |
331
- | `requireParameters` | `boolean` | Require all providers support parameters |
332
- | `dataCollection` | `'deny' \| 'allow'` | Control data collection |
333
- | `order` | `string[]` | Preferred provider order |
334
- | `providers` | `object` | Provider preferences (see below) |
335
- | `transforms` | `string[]` | Request transformations to apply |
400
+ | `siteUrl` | `string` | Your app URL, sent as `HTTP-Referer` |
401
+ | `appName` | `string` | Your app name, sent as `X-Title` |
402
+ | `providers` | `object` | Provider routing (see below) |
403
+ | `fallbackModels` | `string[]` | Models to fall back to, in order (sent as `models`) |
404
+ | `plugins` | `OpenRouterPlugin[]` | `file-parser`, `response-healing`, `context-compression` |
405
+ | `transforms` | `string[]` | Deprecated; `middle-out` is sent as the `context-compression` plugin |
336
406
 
337
407
  ### providers Options
338
408
 
339
409
  | Property | Type | Description |
340
410
  |----------|------|-------------|
341
- | `allow` | `string[]` | Whitelist of allowed providers |
342
- | `deny` | `string[]` | Blacklist of denied providers |
343
- | `prefer` | `string[]` | Preferred providers (given priority) |
344
- | `ignore` | `string[]` | Providers to ignore |
345
411
  | `order` | `string[]` | Explicit provider order |
346
- | `quantizations` | `string[]` | Preferred quantization levels |
347
- | `dataCollection` | `'deny' \| 'allow'` | Provider-level data collection |
412
+ | `allowFallbacks` | `boolean` | Fall back to other providers |
413
+ | `requireParameters` | `boolean` | Only providers that support every parameter; auto-on for strict `json_schema` when unset |
414
+ | `dataCollection` | `'deny' \| 'allow'` | Provider data collection |
415
+ | `zdr` | `boolean` | Zero data retention endpoints only |
416
+ | `enforceDistillableText` | `boolean` | Only providers that allow training on outputs |
417
+ | `only` | `string[]` | Whitelist of providers |
418
+ | `ignore` | `string[]` | Providers to skip |
419
+ | `quantizations` | `OpenRouterQuantization[]` | Allowed quantization levels |
420
+ | `sort` | `'price' \| 'throughput' \| 'latency' \| 'exacto' \| { by, partition? }` | Sort providers; `exacto` prefers the best tool-calling accuracy, `partition: 'none'` sorts across fallback models together |
421
+ | `preferredMinThroughput` | `number` | Preferred median throughput floor (tokens/s) |
422
+ | `preferredMaxLatency` | `number` | Preferred median latency ceiling (seconds) |
423
+ | `maxPrice` | `{ prompt?, completion?, image?, audio?, request? }` | Price ceilings |
348
424
 
349
425
  ## Cost Information
350
426
 
@@ -352,7 +428,7 @@ OpenRouter includes actual cost information in every response:
352
428
 
353
429
  ```typescript
354
430
  const response = await ai.chat.get(messages, {
355
- metadata: { model: 'openai/gpt-4-turbo' }
431
+ metadata: { model: 'openai/gpt-5.4-mini' }
356
432
  });
357
433
 
358
434
  // Cost is automatically included
@@ -370,9 +446,10 @@ The cost is provided by OpenRouter's API in the `total_cost` field and automatic
370
446
 
371
447
  OpenRouter uses the format `provider/model-name`:
372
448
 
373
- - `openai/gpt-4-turbo`
374
- - `anthropic/claude-3-opus`
375
- - `google/gemini-pro`
449
+ - `openai/gpt-5.4-mini`
450
+ - `anthropic/claude-opus-4.5`
451
+ - `google/gemini-2.5-flash`
452
+ - `x-ai/grok-4.3`
376
453
  - `meta-llama/llama-3-70b-instruct`
377
454
  - `mistralai/mistral-large`
378
455
  - `cohere/command-r-plus`
@@ -418,7 +495,7 @@ import { ProviderError, RateLimitError } from '@aeye/openrouter';
418
495
 
419
496
  try {
420
497
  const response = await ai.chat.get(messages, {
421
- metadata: { model: 'openai/gpt-4-turbo' }
498
+ metadata: { model: 'openai/gpt-5.4-mini' }
422
499
  });
423
500
  } catch (error) {
424
501
  if (error instanceof RateLimitError) {
@@ -448,25 +525,26 @@ try {
448
525
  3. **Enable Fallbacks**: Use automatic fallbacks for reliability
449
526
  ```typescript
450
527
  defaultParams: {
451
- allowFallbacks: true,
452
528
  providers: {
529
+ allowFallbacks: true,
453
530
  order: ['openai', 'anthropic', 'google'],
454
531
  },
532
+ fallbackModels: ['openai/gpt-5.4-mini'],
455
533
  }
456
534
  ```
457
535
 
458
536
  4. **Specify Provider Preferences**: Control which providers are used
459
537
  ```typescript
460
538
  providers: {
461
- prefer: ['openai', 'anthropic'],
462
- deny: ['low-quality-provider'],
539
+ only: ['openai', 'anthropic'],
540
+ ignore: ['low-quality-provider'],
463
541
  }
464
542
  ```
465
543
 
466
544
  5. **Use ZDR for Sensitive Data**: Enable Zero Data Retention for privacy
467
545
  ```typescript
468
546
  defaultParams: {
469
- dataCollection: 'deny',
547
+ providers: { dataCollection: 'deny', zdr: true },
470
548
  }
471
549
  ```
472
550
 
@@ -491,7 +569,13 @@ Extends `OpenAIProvider` from `@aeye/openai`.
491
569
 
492
570
  **Overridden Methods**:
493
571
  - `createClient(config)` - Creates OpenAI client with OpenRouter endpoint
494
- - `customizeChatParams(params, config, request)` - Adds OpenRouter-specific params
572
+ - `augmentChatRequest(params, request, config)` - Adds OpenRouter-specific params
573
+ - `buildChatParams(request, run, streaming)` - Sends only the allowed tools for `{ allowed }` tool choices
574
+ - `createBatch(batch, run)` / `retrieveBatch(job, run)` - Inline batches through `/batches`
575
+
576
+ **Added Methods**:
577
+ - `rerank(request, ctx, config?)` - `/rerank`
578
+ - `decide(request, ctx, config?)` - alpha decisions endpoint
495
579
 
496
580
  ### OpenRouterModelSource
497
581
 
@@ -518,12 +602,15 @@ Model source for fetching OpenRouter models.
518
602
  - ✅ Provider routing
519
603
  - ✅ ZDR mode
520
604
  - ✅ Multi-modal (for supported models)
521
- - ❌ Image generation (not supported by OpenRouter)
522
- - ❌ Speech synthesis (not supported by OpenRouter)
523
- - ❌ Transcription (not supported by OpenRouter)
524
- - ❌ Embeddings (not supported by OpenRouter)
525
-
526
- For image generation, speech, transcription, and embeddings, use provider-specific packages like `@aeye/openai`, `@aeye/anthropic`, etc.
605
+ - ✅ Server tools (web search, web fetch, image generation)
606
+ - ✅ Prompt caching (`cacheBreakpoint` → `cache_control`)
607
+ - ✅ Image generation and editing (through chat; no streaming)
608
+ - ✅ Speech synthesis
609
+ - ✅ Transcription (no streaming)
610
+ - ✅ Embeddings
611
+ - ✅ Reranking
612
+ - ✅ Decisions (alpha endpoint)
613
+ - ✅ Batches
527
614
 
528
615
  ## Related Packages
529
616