@aeye/openrouter 0.4.1 → 0.7.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 +652 -556
  2. package/aeye-openrouter.md +39 -21
  3. package/dist/__integration__/openrouter.integration.test.js +6 -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 +38 -1
  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 +100 -41
  24. package/dist/openrouter.d.ts.map +1 -1
  25. package/dist/openrouter.js +390 -211
  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 +6 -6
package/README.md CHANGED
@@ -1,556 +1,652 @@
1
- # @aeye/openrouter
2
-
3
- OpenRouter provider for the @aeye (AI TypeScript) framework. OpenRouter provides unified access to multiple AI providers through a single API, with automatic fallbacks, routing optimization, and competitive pricing.
4
-
5
- ## Features
6
-
7
- - **Multi-Provider Access**: Access models from OpenAI, Anthropic, Google, Meta, and more through one API
8
- - **Automatic Fallbacks**: Seamless failover to alternative providers if primary fails
9
- - **Intelligent Routing**: Automatic selection of best provider for each request
10
- - **Cost Tracking**: Built-in cost information in responses
11
- - **ZDR Support**: Zero Data Retention mode for privacy-sensitive applications
12
- - **Provider Preferences**: Control which providers to use, prefer, or avoid
13
- - **Reasoning Models**: Support for advanced reasoning with o1, Claude, and others
14
- - **Streaming**: Full streaming support across compatible models
15
- - **Extends OpenAI**: Built on the OpenAI provider for compatibility
16
-
17
- ## Installation
18
-
19
- ```bash
20
- npm install @aeye/openrouter @aeye/openai @aeye/ai @aeye/core openai zod
21
- ```
22
-
23
- ## Quick Start
24
-
25
- ```typescript
26
- import { OpenRouterProvider } from '@aeye/openrouter';
27
- import { AI } from '@aeye/ai';
28
-
29
- // Create provider instance
30
- const openrouter = new OpenRouterProvider({
31
- apiKey: process.env.OPENROUTER_API_KEY!,
32
- defaultParams: {
33
- siteUrl: 'https://yourapp.com',
34
- appName: 'Your App Name',
35
- },
36
- });
37
-
38
- // Use with AI instance
39
- const ai = AI.with()
40
- .providers({ openrouter })
41
- .create();
42
-
43
- // Make a request (OpenRouter picks the best provider)
44
- const response = await ai.chat.get([
45
- { role: 'user', content: 'Explain TypeScript generics' }
46
- ], {
47
- metadata: { model: 'openai/gpt-4-turbo' }
48
- });
49
-
50
- console.log(response.content);
51
- console.log('Cost:', response.usage.cost); // Actual cost from OpenRouter
52
- ```
53
-
54
- ## Configuration
55
-
56
- ### Basic Configuration
57
-
58
- ```typescript
59
- import { OpenRouterProvider, OpenRouterConfig } from '@aeye/openrouter';
60
-
61
- const config: OpenRouterConfig = {
62
- apiKey: process.env.OPENROUTER_API_KEY!,
63
- defaultParams: {
64
- siteUrl: 'https://yourapp.com', // Your app URL (helps with rankings)
65
- appName: 'My AI App', // Your app name (shown in OpenRouter dashboard)
66
- },
67
- };
68
-
69
- const provider = new OpenRouterProvider(config);
70
- ```
71
-
72
- ### Advanced Configuration
73
-
74
- ```typescript
75
- const provider = new OpenRouterProvider({
76
- apiKey: process.env.OPENROUTER_API_KEY!,
77
- defaultParams: {
78
- siteUrl: 'https://yourapp.com',
79
- appName: 'My AI App',
80
-
81
- // Automatic fallbacks
82
- allowFallbacks: true, // Enable fallback to alternative providers
83
-
84
- // Provider preferences
85
- 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
93
- },
94
-
95
- // Privacy & security
96
- requireParameters: true, // Require all providers to support parameters
97
- dataCollection: 'deny', // Global data collection setting
98
-
99
- // Model transformations
100
- transforms: ['middle-out'], // Apply transformations to requests
101
- },
102
- });
103
- ```
104
-
105
- ## Usage Examples
106
-
107
- ### Basic Chat with Cost Tracking
108
-
109
- ```typescript
110
- const response = await ai.chat.get([
111
- { role: 'user', content: 'What is the capital of France?' }
112
- ], {
113
- metadata: { model: 'openai/gpt-3.5-turbo' }
114
- });
115
-
116
- console.log(response.content);
117
- console.log('Tokens:', response.usage.totalTokens);
118
- console.log('Cost: $', response.usage.cost); // Actual cost from OpenRouter
119
- ```
120
-
121
- ### Using Multiple Models
122
-
123
- OpenRouter allows you to access models from different providers using the format `provider/model`:
124
-
125
- ```typescript
126
- // OpenAI GPT-4
127
- const gpt4 = await ai.chat.get(messages, {
128
- metadata: { model: 'openai/gpt-4-turbo' }
129
- });
130
-
131
- // Anthropic Claude
132
- const claude = await ai.chat.get(messages, {
133
- metadata: { model: 'anthropic/claude-3-opus' }
134
- });
135
-
136
- // Google Gemini
137
- const gemini = await ai.chat.get(messages, {
138
- metadata: { model: 'google/gemini-pro' }
139
- });
140
-
141
- // Meta Llama
142
- const llama = await ai.chat.get(messages, {
143
- metadata: { model: 'meta-llama/llama-3-70b' }
144
- });
145
- ```
146
-
147
- ### Automatic Model Selection
148
-
149
- Let OpenRouter choose the best model based on your criteria:
150
-
151
- ```typescript
152
- const ai = AI.with()
153
- .providers({ openrouter })
154
- .create({
155
- defaultMetadata: {
156
- required: ['chat', 'streaming'],
157
- weights: {
158
- speed: 0.3,
159
- cost: 0.4,
160
- quality: 0.3,
161
- },
162
- },
163
- });
164
-
165
- // OpenRouter will select the best model matching criteria
166
- const response = await ai.chat.get([
167
- { role: 'user', content: 'Explain quantum computing' }
168
- ]);
169
- ```
170
-
171
- ### Provider Fallbacks
172
-
173
- ```typescript
174
- // Primary provider fails? OpenRouter automatically falls back
175
- const provider = new OpenRouterProvider({
176
- apiKey: process.env.OPENROUTER_API_KEY!,
177
- defaultParams: {
178
- allowFallbacks: true,
179
- providers: {
180
- order: ['openai', 'anthropic', 'google'], // Try in this order
181
- },
182
- },
183
- });
184
-
185
- // If OpenAI is down, automatically tries Anthropic, then Google
186
- const response = await ai.chat.get(messages, {
187
- metadata: { model: 'openai/gpt-4-turbo' }
188
- });
189
- ```
190
-
191
- ### Zero Data Retention (ZDR)
192
-
193
- ```typescript
194
- // Only use ZDR-compliant models
195
- const ai = AI.with()
196
- .providers({ openrouter })
197
- .create({
198
- defaultMetadata: {
199
- required: ['chat', 'zdr'], // Require ZDR support
200
- },
201
- });
202
-
203
- // Also configure at provider level
204
- const provider = new OpenRouterProvider({
205
- apiKey: process.env.OPENROUTER_API_KEY!,
206
- defaultParams: {
207
- dataCollection: 'deny', // Ensure no data collection
208
- },
209
- });
210
- ```
211
-
212
- ### Streaming with Cost Tracking
213
-
214
- ```typescript
215
- let totalCost = 0;
216
-
217
- for await (const chunk of ai.chat.stream([
218
- { role: 'user', content: 'Write a story about a robot' }
219
- ], {
220
- metadata: { model: 'anthropic/claude-3-sonnet' }
221
- })) {
222
- if (chunk.content) {
223
- process.stdout.write(chunk.content);
224
- }
225
-
226
- // Cost is sent in final chunk
227
- if (chunk.usage?.cost) {
228
- totalCost = chunk.usage.cost;
229
- }
230
- }
231
-
232
- console.log(`\n\nTotal cost: $${totalCost}`);
233
- ```
234
-
235
- ### Reasoning Models
236
-
237
- ```typescript
238
- // Use OpenAI o1 through OpenRouter
239
- const response = await ai.chat.get([
240
- {
241
- role: 'user',
242
- 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' }
246
- });
247
-
248
- console.log('Reasoning:', response.reasoning);
249
- console.log('Answer:', response.content);
250
- console.log('Cost:', response.usage.cost);
251
- ```
252
-
253
- ### Function Calling
254
-
255
- ```typescript
256
- import z from 'zod';
257
-
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' },
262
- tools: [
263
- {
264
- name: 'get_weather',
265
- description: 'Get current weather for a location',
266
- parameters: z.object({
267
- location: z.string(),
268
- unit: z.enum(['celsius', 'fahrenheit']).optional(),
269
- }),
270
- }
271
- ],
272
- toolChoice: 'auto',
273
- });
274
- ```
275
-
276
- ### Fetching Available Models
277
-
278
- OpenRouter provides a model source that can fetch all available models:
279
-
280
- ```typescript
281
- import { OpenRouterModelSource } from '@aeye/openrouter';
282
-
283
- const source = new OpenRouterModelSource({
284
- apiKey: process.env.OPENROUTER_API_KEY,
285
- includeZDR: true, // Include ZDR information
286
- });
287
-
288
- const models = await source.fetchModels();
289
-
290
- models.forEach(model => {
291
- console.log(`${model.id}`);
292
- console.log(` Pricing: $${model.pricing.text.input}/1M input tokens`);
293
- console.log(` Context: ${model.contextWindow} tokens`);
294
- console.log(` ZDR: ${model.capabilities.has('zdr')}`);
295
- });
296
- ```
297
-
298
- ### Automatic Model Registration
299
-
300
- ```typescript
301
- // Automatically fetch and register OpenRouter models
302
- const ai = AI.with()
303
- .providers({ openrouter })
304
- .create({
305
- fetchOpenRouterModels: true, // Auto-fetch all available models
306
- });
307
-
308
- // Now all OpenRouter models are available for automatic selection
309
- const response = await ai.chat.get(messages);
310
- ```
311
-
312
- ## Configuration Options
313
-
314
- ### OpenRouterConfig
315
-
316
- Extends `OpenAIConfig` from `@aeye/openai`.
317
-
318
- | Property | Type | Required | Description |
319
- |----------|------|----------|-------------|
320
- | `apiKey` | `string` | Yes | OpenRouter API key |
321
- | `baseURL` | `string` | No | Custom base URL (defaults to https://openrouter.ai/api/v1) |
322
- | `defaultParams` | `object` | No | Default parameters for all requests |
323
-
324
- ### defaultParams
325
-
326
- | Property | Type | Description |
327
- |----------|------|-------------|
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 |
336
-
337
- ### providers Options
338
-
339
- | Property | Type | Description |
340
- |----------|------|-------------|
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
- | `order` | `string[]` | Explicit provider order |
346
- | `quantizations` | `string[]` | Preferred quantization levels |
347
- | `dataCollection` | `'deny' \| 'allow'` | Provider-level data collection |
348
-
349
- ## Cost Information
350
-
351
- OpenRouter includes actual cost information in every response:
352
-
353
- ```typescript
354
- const response = await ai.chat.get(messages, {
355
- metadata: { model: 'openai/gpt-4-turbo' }
356
- });
357
-
358
- // Cost is automatically included
359
- console.log('Input tokens:', response.usage.text.input);
360
- console.log('Output tokens:', response.usage.text.output);
361
- console.log('Cost: $', response.usage.cost); // Actual USD cost
362
-
363
- // Cost calculation is skipped when provider returns cost
364
- // No need to manually calculate costs!
365
- ```
366
-
367
- The cost is provided by OpenRouter's API in the `total_cost` field and automatically extracted into the `usage.cost` field.
368
-
369
- ## Model Format
370
-
371
- OpenRouter uses the format `provider/model-name`:
372
-
373
- - `openai/gpt-4-turbo`
374
- - `anthropic/claude-3-opus`
375
- - `google/gemini-pro`
376
- - `meta-llama/llama-3-70b-instruct`
377
- - `mistralai/mistral-large`
378
- - `cohere/command-r-plus`
379
-
380
- See the [OpenRouter Models](https://openrouter.ai/models) page for the full list.
381
-
382
- ## Privacy & Data Retention
383
-
384
- ### Zero Data Retention (ZDR)
385
-
386
- OpenRouter supports ZDR mode where your data is not stored:
387
-
388
- ```typescript
389
- // Require ZDR-compliant models
390
- const ai = AI.with()
391
- .providers({ openrouter })
392
- .create({
393
- defaultMetadata: {
394
- required: ['chat', 'zdr'],
395
- },
396
- });
397
-
398
- // Or configure at provider level
399
- const provider = new OpenRouterProvider({
400
- apiKey: process.env.OPENROUTER_API_KEY!,
401
- defaultParams: {
402
- dataCollection: 'deny',
403
- providers: {
404
- dataCollection: 'deny',
405
- },
406
- },
407
- });
408
- ```
409
-
410
- Models with ZDR support have the `zdr` capability tag.
411
-
412
- ## Error Handling
413
-
414
- OpenRouter uses the same error types as the OpenAI provider:
415
-
416
- ```typescript
417
- import { ProviderError, RateLimitError } from '@aeye/openrouter';
418
-
419
- try {
420
- const response = await ai.chat.get(messages, {
421
- metadata: { model: 'openai/gpt-4-turbo' }
422
- });
423
- } catch (error) {
424
- if (error instanceof RateLimitError) {
425
- console.error('Rate limit exceeded');
426
- console.log(`Retry after ${error.retryAfter} seconds`);
427
- } else if (error instanceof ProviderError) {
428
- console.error(`Provider error: ${error.message}`);
429
- }
430
- }
431
- ```
432
-
433
- ## Best Practices
434
-
435
- 1. **Set Site URL and App Name**: This helps with OpenRouter rankings and analytics
436
- ```typescript
437
- defaultParams: {
438
- siteUrl: 'https://yourapp.com',
439
- appName: 'Your App',
440
- }
441
- ```
442
-
443
- 2. **Use Cost Tracking**: Monitor costs with the built-in `usage.cost` field
444
- ```typescript
445
- console.log('Cost: $', response.usage.cost);
446
- ```
447
-
448
- 3. **Enable Fallbacks**: Use automatic fallbacks for reliability
449
- ```typescript
450
- defaultParams: {
451
- allowFallbacks: true,
452
- providers: {
453
- order: ['openai', 'anthropic', 'google'],
454
- },
455
- }
456
- ```
457
-
458
- 4. **Specify Provider Preferences**: Control which providers are used
459
- ```typescript
460
- providers: {
461
- prefer: ['openai', 'anthropic'],
462
- deny: ['low-quality-provider'],
463
- }
464
- ```
465
-
466
- 5. **Use ZDR for Sensitive Data**: Enable Zero Data Retention for privacy
467
- ```typescript
468
- defaultParams: {
469
- dataCollection: 'deny',
470
- }
471
- ```
472
-
473
- 6. **Monitor Model Availability**: OpenRouter's model list changes frequently
474
- ```typescript
475
- fetchOpenRouterModels: true // Auto-fetch latest models
476
- ```
477
-
478
- ## API Reference
479
-
480
- ### OpenRouterProvider
481
-
482
- Extends `OpenAIProvider` from `@aeye/openai`.
483
-
484
- **Constructor**: `new OpenRouterProvider(config: OpenRouterConfig)`
485
-
486
- **Methods**:
487
- - Inherits all methods from `OpenAIProvider`
488
- - `createExecutor<TContext, TMetadata>(config?)` - Returns executor with cost tracking
489
- - `createStreamer<TContext, TMetadata>(config?)` - Returns streamer with cost tracking
490
- - `listModels(config?)` - Fetches models from OpenRouter API with ZDR info
491
-
492
- **Overridden Methods**:
493
- - `createClient(config)` - Creates OpenAI client with OpenRouter endpoint
494
- - `customizeChatParams(params, config, request)` - Adds OpenRouter-specific params
495
-
496
- ### OpenRouterModelSource
497
-
498
- Model source for fetching OpenRouter models.
499
-
500
- **Constructor**: `new OpenRouterModelSource(config?: OpenRouterSourceConfig)`
501
-
502
- **Methods**:
503
- - `fetchModels(config?): Promise<ModelInfo[]>` - Fetch all available models
504
-
505
- **Config Options**:
506
- - `apiKey?: string` - OpenRouter API key (optional for public models)
507
- - `includeZDR?: boolean` - Include ZDR information (default: true)
508
-
509
- ## Supported Features
510
-
511
- - ✅ Chat completions
512
- - ✅ Streaming
513
- - ✅ Function calling
514
- - ✅ Vision (for supported models)
515
- - ✅ Reasoning models
516
- - ✅ Cost tracking
517
- - ✅ Automatic fallbacks
518
- - ✅ Provider routing
519
- - ✅ ZDR mode
520
- - ✅ 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.
527
-
528
- ## Related Packages
529
-
530
- - **[@aeye/core](../core)**: Core @aeye framework types and interfaces
531
- - **[@aeye/ai](../ai)**: AI abstractions and utilities
532
- - **[@aeye/openai](../openai)**: OpenAI provider (base class)
533
- - **[@aeye/anthropic](../anthropic)**: Anthropic Claude provider
534
- - **[@aeye/google](../google)**: Google AI provider
535
-
536
- ## Links
537
-
538
- - [OpenRouter Website](https://openrouter.ai)
539
- - [OpenRouter Models](https://openrouter.ai/models)
540
- - [OpenRouter Documentation](https://openrouter.ai/docs)
541
- - [OpenRouter API Keys](https://openrouter.ai/keys)
542
-
543
- ## License
544
-
545
- GPL-3.0
546
-
547
- ## Contributing
548
-
549
- Contributions are welcome! Please see the main [@aeye repository](https://github.com/ClickerMonkey/aeye) for contribution guidelines.
550
-
551
- ## Support
552
-
553
- For issues and questions:
554
- - GitHub Issues: https://github.com/ClickerMonkey/aeye/issues
555
- - Documentation: https://github.com/ClickerMonkey/aeye
556
- - OpenRouter Discord: https://discord.gg/openrouter
1
+ # @aeye/openrouter
2
+
3
+ OpenRouter provider for the @aeye (AI TypeScript) framework. OpenRouter provides unified access to multiple AI providers through a single API, with automatic fallbacks, routing optimization, and competitive pricing.
4
+
5
+ ## Features
6
+
7
+ - **Multi-Provider Access**: Access models from OpenAI, Anthropic, Google, xAI, Meta, and more through one API
8
+ - **Automatic Fallbacks**: Seamless failover to alternative providers if primary fails
9
+ - **Intelligent Routing**: Automatic selection of best provider for each request
10
+ - **Cost Tracking**: Built-in cost information in responses
11
+ - **ZDR Support**: Zero Data Retention mode for privacy-sensitive applications
12
+ - **Provider Preferences**: Control which providers to use, prefer, or avoid
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
18
+ - **Streaming**: Full streaming support across compatible models
19
+ - **Extends OpenAI**: Built on the OpenAI provider for compatibility
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @aeye/openrouter @aeye/openai @aeye/ai @aeye/core openai zod
25
+ ```
26
+
27
+ Requires Node >= 22 and the `openai` SDK v7.
28
+
29
+ ## Quick Start
30
+
31
+ ```typescript
32
+ import { OpenRouterProvider } from '@aeye/openrouter';
33
+ import { AI } from '@aeye/ai';
34
+
35
+ // Create provider instance
36
+ const openrouter = new OpenRouterProvider({
37
+ apiKey: process.env.OPENROUTER_API_KEY!,
38
+ defaultParams: {
39
+ siteUrl: 'https://yourapp.com',
40
+ appName: 'Your App Name',
41
+ },
42
+ });
43
+
44
+ // Use with AI instance
45
+ const ai = AI.with()
46
+ .providers({ openrouter })
47
+ .create();
48
+
49
+ // Make a request (OpenRouter picks the best provider)
50
+ const response = await ai.chat.get([
51
+ { role: 'user', content: 'Explain TypeScript generics' }
52
+ ], {
53
+ metadata: { model: 'openai/gpt-5.4-mini' }
54
+ });
55
+
56
+ console.log(response.content);
57
+ console.log('Cost:', response.usage.cost); // Actual cost from OpenRouter
58
+ ```
59
+
60
+ ## Configuration
61
+
62
+ ### Basic Configuration
63
+
64
+ ```typescript
65
+ import { OpenRouterProvider, OpenRouterConfig } from '@aeye/openrouter';
66
+
67
+ const config: OpenRouterConfig = {
68
+ apiKey: process.env.OPENROUTER_API_KEY!,
69
+ defaultParams: {
70
+ siteUrl: 'https://yourapp.com', // Your app URL (helps with rankings)
71
+ appName: 'My AI App', // Your app name (shown in OpenRouter dashboard)
72
+ },
73
+ };
74
+
75
+ const provider = new OpenRouterProvider(config);
76
+ ```
77
+
78
+ ### Advanced Configuration
79
+
80
+ ```typescript
81
+ const provider = new OpenRouterProvider({
82
+ apiKey: process.env.OPENROUTER_API_KEY!,
83
+ defaultParams: {
84
+ siteUrl: 'https://yourapp.com',
85
+ appName: 'My AI App',
86
+
87
+ // Provider routing (sent as OpenRouter's `provider` object)
88
+ providers: {
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 },
102
+ },
103
+
104
+ // Models to try, in order, when the requested one fails
105
+ fallbackModels: ['openai/gpt-5.4-mini', 'anthropic/claude-sonnet-4.5'],
106
+
107
+ // Plugins
108
+ plugins: [
109
+ { id: 'file-parser', pdf: { engine: 'cloudflare-ai' } },
110
+ { id: 'response-healing' },
111
+ { id: 'context-compression', engine: 'middle-out' },
112
+ ],
113
+ },
114
+ });
115
+ ```
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
+
119
+ ## Usage Examples
120
+
121
+ ### Basic Chat with Cost Tracking
122
+
123
+ ```typescript
124
+ const response = await ai.chat.get([
125
+ { role: 'user', content: 'What is the capital of France?' }
126
+ ], {
127
+ metadata: { model: 'openai/gpt-5.4-nano' }
128
+ });
129
+
130
+ console.log(response.content);
131
+ console.log('Tokens:', response.usage.totalTokens);
132
+ console.log('Cost: $', response.usage.cost); // Actual cost from OpenRouter
133
+ ```
134
+
135
+ ### Using Multiple Models
136
+
137
+ OpenRouter allows you to access models from different providers using the format `provider/model`:
138
+
139
+ ```typescript
140
+ // OpenAI GPT-4
141
+ const gpt4 = await ai.chat.get(messages, {
142
+ metadata: { model: 'openai/gpt-5.4-mini' }
143
+ });
144
+
145
+ // Anthropic Claude
146
+ const claude = await ai.chat.get(messages, {
147
+ metadata: { model: 'anthropic/claude-opus-4.5' }
148
+ });
149
+
150
+ // Google Gemini
151
+ const gemini = await ai.chat.get(messages, {
152
+ metadata: { model: 'google/gemini-pro' }
153
+ });
154
+
155
+ // Meta Llama
156
+ const llama = await ai.chat.get(messages, {
157
+ metadata: { model: 'meta-llama/llama-3-70b' }
158
+ });
159
+ ```
160
+
161
+ ### Automatic Model Selection
162
+
163
+ Let OpenRouter choose the best model based on your criteria:
164
+
165
+ ```typescript
166
+ const ai = AI.with()
167
+ .providers({ openrouter })
168
+ .create({
169
+ defaultMetadata: {
170
+ required: ['chat', 'streaming'],
171
+ weights: {
172
+ speed: 0.3,
173
+ cost: 0.4,
174
+ quality: 0.3,
175
+ },
176
+ },
177
+ });
178
+
179
+ // OpenRouter will select the best model matching criteria
180
+ const response = await ai.chat.get([
181
+ { role: 'user', content: 'Explain quantum computing' }
182
+ ]);
183
+ ```
184
+
185
+ ### Provider Fallbacks
186
+
187
+ ```typescript
188
+ // Primary provider fails? OpenRouter automatically falls back
189
+ const provider = new OpenRouterProvider({
190
+ apiKey: process.env.OPENROUTER_API_KEY!,
191
+ defaultParams: {
192
+ allowFallbacks: true,
193
+ providers: {
194
+ order: ['openai', 'anthropic', 'google'], // Try in this order
195
+ },
196
+ },
197
+ });
198
+
199
+ // If OpenAI is down, automatically tries Anthropic, then Google
200
+ const response = await ai.chat.get(messages, {
201
+ metadata: { model: 'openai/gpt-5.4-mini' }
202
+ });
203
+ ```
204
+
205
+ ### Zero Data Retention (ZDR)
206
+
207
+ ```typescript
208
+ // Only use ZDR-compliant models
209
+ const ai = AI.with()
210
+ .providers({ openrouter })
211
+ .create({
212
+ defaultMetadata: {
213
+ required: ['chat', 'zdr'], // Require ZDR support
214
+ },
215
+ });
216
+
217
+ // Also configure at provider level
218
+ const provider = new OpenRouterProvider({
219
+ apiKey: process.env.OPENROUTER_API_KEY!,
220
+ defaultParams: {
221
+ dataCollection: 'deny', // Ensure no data collection
222
+ },
223
+ });
224
+ ```
225
+
226
+ ### Streaming with Cost Tracking
227
+
228
+ ```typescript
229
+ let totalCost = 0;
230
+
231
+ for await (const chunk of ai.chat.stream([
232
+ { role: 'user', content: 'Write a story about a robot' }
233
+ ], {
234
+ metadata: { model: 'anthropic/claude-sonnet-4.5' }
235
+ })) {
236
+ if (chunk.content) {
237
+ process.stdout.write(chunk.content);
238
+ }
239
+
240
+ // Cost is sent in final chunk
241
+ if (chunk.usage?.cost) {
242
+ totalCost = chunk.usage.cost;
243
+ }
244
+ }
245
+
246
+ console.log(`\n\nTotal cost: $${totalCost}`);
247
+ ```
248
+
249
+ ### Reasoning Models
250
+
251
+ ```typescript
252
+ // Reason with an effort, or a token budget via reason.maxTokens (never both)
253
+ const response = await ai.chat.get({
254
+ messages: [{
255
+ role: 'user',
256
+ content: 'Solve: If a train leaves NYC at 3pm going 60mph, and another leaves Boston at 4pm going 80mph, when do they meet?'
257
+ }],
258
+ reason: { effort: 'high', exclude: false },
259
+ }, {
260
+ metadata: { model: 'openai/gpt-5.5' }
261
+ });
262
+
263
+ console.log('Reasoning:', response.reasoning);
264
+ console.log('Answer:', response.content);
265
+ console.log('Cost:', response.usage.cost);
266
+ ```
267
+
268
+ Reasoning comes back with `reasoning.provider` set to `'openrouter'`. Keep it on the assistant message and it is replayed on that same message (with its text and tool calls) on the next turn; reasoning made by another provider, such as OpenAI directly or Bedrock, isn't replayed.
269
+
270
+ ### Server Tools
271
+
272
+ ```typescript
273
+ const response = await ai.chat.get({
274
+ messages: [{ role: 'user', content: 'What did the TypeScript team ship this month?' }],
275
+ hostedTools: [
276
+ { type: 'web_search', maxResults: 5, allowedDomains: ['devblogs.microsoft.com'], blockedDomains: ['example.com'] },
277
+ { type: 'web_fetch' },
278
+ ],
279
+ }, {
280
+ metadata: { model: 'openai/gpt-5.4-mini' }
281
+ });
282
+
283
+ console.log(response.citations);
284
+ console.log(response.usage?.tools); // { web_search: 1 }
285
+ ```
286
+
287
+ `web_search`, `web_fetch`, and `image_generation` run as `openrouter:*` server tools; `file_search`, `code_interpreter`, and `mcp` are rejected.
288
+
289
+ ### Images, Speech, Transcription, Embeddings
290
+
291
+ ```typescript
292
+ // Images go through chat with the image modality; size becomes an aspect ratio
293
+ const image = await ai.image.generate.get({ prompt: 'A lighthouse at dusk', size: '1536x1024' });
294
+
295
+ // Embeddings, speech, and transcription use OpenRouter's OpenAI-compatible endpoints
296
+ const { embeddings } = await ai.embed.get({ texts: ['hello'] });
297
+ const transcript = await ai.transcribe.get({ audio: fs.readFileSync('note.mp3') });
298
+ ```
299
+
300
+ 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.
301
+
302
+ ### Reranking, Decisions, Batches
303
+
304
+ ```typescript
305
+ // Order documents by relevance (default cohere/rerank-v3.5)
306
+ const { results } = await ai.rerank.get({ query: 'refund policy', documents, topN: 3 });
307
+
308
+ // Typed answers from a decision model (default typesafe/jev-1.14)
309
+ const { answers } = await ai.decide.get({
310
+ input: 'I was charged twice for my order.',
311
+ questions: {
312
+ billing: { type: 'predicate', instructions: 'Is this a billing problem?' },
313
+ team: { type: 'choice', instructions: 'Which team handles it?', choices: { payments: 'Charges and refunds', web: 'Website bugs' } },
314
+ },
315
+ });
316
+
317
+ // Chat requests as one batch, posted inline to /batches
318
+ const job = await ai.batch.submit(requests, { metadata: { model: 'openai/gpt-5.4-mini' } });
319
+ const done = await ai.batch.wait(job, {});
320
+ ```
321
+
322
+ Rerank and decision costs come back as `usage.cost`, and both run the provider's `hooks.rerank` / `hooks.decide`. Decisions use OpenRouter's alpha endpoint, outside the versioned API. Change the defaults with `defaultModels.rerank` and `defaultModels.decision`.
323
+
324
+ OpenRouter reports a batch's cost only for the whole batch: `job.cost` (with BYOK, its fee plus the provider's bill). Batch results carry token counts but no cost.
325
+
326
+ ### Function Calling
327
+
328
+ ```typescript
329
+ import z from 'zod';
330
+
331
+ const response = await ai.chat.get({
332
+ messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
333
+ tools: [
334
+ {
335
+ name: 'get_weather',
336
+ description: 'Get current weather for a location',
337
+ parameters: z.object({
338
+ location: z.string(),
339
+ unit: z.enum(['celsius', 'fahrenheit']).optional(),
340
+ }),
341
+ }
342
+ ],
343
+ toolChoice: 'auto',
344
+ }, {
345
+ metadata: { model: 'openai/gpt-5.4-mini' },
346
+ });
347
+ ```
348
+
349
+ `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.
350
+
351
+ When any tool is strict, the request carries `x-anthropic-beta: structured-outputs-2025-11-13`: without it OpenRouter strips `strict` from tools bound for Anthropic. Other upstreams ignore the header.
352
+
353
+ When a request sets a JSON or schema `responseFormat` and no message mentions JSON, a `Respond in JSON.` system message is put first, because some upstreams serve every response format as JSON mode and reject a conversation that never says "json" (Alibaba for Qwen).
354
+
355
+ ### Fetching Available Models
356
+
357
+ OpenRouter provides a model source that can fetch all available models:
358
+
359
+ ```typescript
360
+ import { OpenRouterModelSource } from '@aeye/openrouter';
361
+
362
+ const source = new OpenRouterModelSource({
363
+ apiKey: process.env.OPENROUTER_API_KEY,
364
+ includeZDR: true, // Include ZDR information
365
+ });
366
+
367
+ const models = await source.fetchModels();
368
+
369
+ models.forEach(model => {
370
+ console.log(`${model.id}`);
371
+ console.log(` Pricing: $${model.pricing.text.input}/1M input tokens`);
372
+ console.log(` Context: ${model.contextWindow} tokens`);
373
+ console.log(` ZDR: ${model.capabilities.has('zdr')}`);
374
+ });
375
+ ```
376
+
377
+ ### Automatic Model Registration
378
+
379
+ ```typescript
380
+ // Automatically fetch and register OpenRouter models
381
+ const ai = AI.with()
382
+ .providers({ openrouter })
383
+ .create({
384
+ fetchOpenRouterModels: true, // Auto-fetch all available models
385
+ });
386
+
387
+ // Now all OpenRouter models are available for automatic selection
388
+ const response = await ai.chat.get(messages);
389
+ ```
390
+
391
+ ## Configuration Options
392
+
393
+ ### OpenRouterConfig
394
+
395
+ Extends `OpenAIConfig` from `@aeye/openai`, minus its `api` option.
396
+
397
+ | Property | Type | Required | Description |
398
+ |----------|------|----------|-------------|
399
+ | `apiKey` | `string` | Yes | OpenRouter API key |
400
+ | `baseURL` | `string` | No | Custom base URL (defaults to https://openrouter.ai/api/v1) |
401
+ | `defaultParams` | `object` | No | Default parameters for all requests |
402
+ | `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 |
403
+
404
+ ### defaultParams
405
+
406
+ | Property | Type | Description |
407
+ |----------|------|-------------|
408
+ | `siteUrl` | `string` | Your app URL, sent as `HTTP-Referer` |
409
+ | `appName` | `string` | Your app name, sent as `X-Title` |
410
+ | `providers` | `object` | Provider routing (see below) |
411
+ | `fallbackModels` | `string[]` | Models to fall back to, in order (sent as `models`) |
412
+ | `plugins` | `OpenRouterPlugin[]` | `file-parser`, `response-healing`, `context-compression` |
413
+ | `transforms` | `string[]` | Deprecated; `middle-out` is sent as the `context-compression` plugin |
414
+
415
+ ### providers Options
416
+
417
+ | Property | Type | Description |
418
+ |----------|------|-------------|
419
+ | `order` | `string[]` | Explicit provider order |
420
+ | `allowFallbacks` | `boolean` | Fall back to other providers |
421
+ | `requireParameters` | `boolean` | Only providers that support every parameter; auto-on for strict `json_schema` when unset |
422
+ | `dataCollection` | `'deny' \| 'allow'` | Provider data collection |
423
+ | `zdr` | `boolean` | Zero data retention endpoints only |
424
+ | `enforceDistillableText` | `boolean` | Only providers that allow training on outputs |
425
+ | `only` | `string[]` | Whitelist of providers |
426
+ | `ignore` | `string[]` | Providers to skip |
427
+ | `quantizations` | `OpenRouterQuantization[]` | Allowed quantization levels |
428
+ | `sort` | `'price' \| 'throughput' \| 'latency' \| 'exacto' \| { by, partition? }` | Sort providers; `exacto` prefers the best tool-calling accuracy, `partition: 'none'` sorts across fallback models together |
429
+ | `preferredMinThroughput` | `number` | Preferred median throughput floor (tokens/s) |
430
+ | `preferredMaxLatency` | `number` | Preferred median latency ceiling (seconds) |
431
+ | `maxPrice` | `{ prompt?, completion?, image?, audio?, request? }` | Price ceilings |
432
+
433
+ ## Cost Information
434
+
435
+ OpenRouter includes actual cost information in every response:
436
+
437
+ ```typescript
438
+ const response = await ai.chat.get(messages, {
439
+ metadata: { model: 'openai/gpt-5.4-mini' }
440
+ });
441
+
442
+ // Cost is automatically included
443
+ console.log('Input tokens:', response.usage.text.input);
444
+ console.log('Output tokens:', response.usage.text.output);
445
+ console.log('Cost: $', response.usage.cost); // Actual USD cost
446
+
447
+ // Cost calculation is skipped when provider returns cost
448
+ // No need to manually calculate costs!
449
+ ```
450
+
451
+ The cost is provided by OpenRouter's API in the `total_cost` field and automatically extracted into the `usage.cost` field.
452
+
453
+ ## Model Format
454
+
455
+ OpenRouter uses the format `provider/model-name`:
456
+
457
+ - `openai/gpt-5.4-mini`
458
+ - `anthropic/claude-opus-4.5`
459
+ - `google/gemini-2.5-flash`
460
+ - `x-ai/grok-4.3`
461
+ - `meta-llama/llama-3-70b-instruct`
462
+ - `mistralai/mistral-large`
463
+ - `cohere/command-r-plus`
464
+
465
+ See the [OpenRouter Models](https://openrouter.ai/models) page for the full list.
466
+
467
+ ## Privacy & Data Retention
468
+
469
+ ### Zero Data Retention (ZDR)
470
+
471
+ OpenRouter supports ZDR mode where your data is not stored:
472
+
473
+ ```typescript
474
+ // Require ZDR-compliant models
475
+ const ai = AI.with()
476
+ .providers({ openrouter })
477
+ .create({
478
+ defaultMetadata: {
479
+ required: ['chat', 'zdr'],
480
+ },
481
+ });
482
+
483
+ // Or configure at provider level
484
+ const provider = new OpenRouterProvider({
485
+ apiKey: process.env.OPENROUTER_API_KEY!,
486
+ defaultParams: {
487
+ dataCollection: 'deny',
488
+ providers: {
489
+ dataCollection: 'deny',
490
+ },
491
+ },
492
+ });
493
+ ```
494
+
495
+ Models with ZDR support have the `zdr` capability tag.
496
+
497
+ ## Error Handling
498
+
499
+ OpenRouter uses the same error types as the OpenAI provider:
500
+
501
+ ```typescript
502
+ import { ProviderError, RateLimitError } from '@aeye/openrouter';
503
+
504
+ try {
505
+ const response = await ai.chat.get(messages, {
506
+ metadata: { model: 'openai/gpt-5.4-mini' }
507
+ });
508
+ } catch (error) {
509
+ if (error instanceof RateLimitError) {
510
+ console.error('Rate limit exceeded');
511
+ console.log(`Retry after ${error.retryAfter} seconds`);
512
+ } else if (error instanceof ProviderError) {
513
+ console.error(`Provider error: ${error.message}`);
514
+ }
515
+ }
516
+ ```
517
+
518
+ ## Best Practices
519
+
520
+ 1. **Set Site URL and App Name**: This helps with OpenRouter rankings and analytics
521
+ ```typescript
522
+ defaultParams: {
523
+ siteUrl: 'https://yourapp.com',
524
+ appName: 'Your App',
525
+ }
526
+ ```
527
+
528
+ 2. **Use Cost Tracking**: Monitor costs with the built-in `usage.cost` field
529
+ ```typescript
530
+ console.log('Cost: $', response.usage.cost);
531
+ ```
532
+
533
+ 3. **Enable Fallbacks**: Use automatic fallbacks for reliability
534
+ ```typescript
535
+ defaultParams: {
536
+ providers: {
537
+ allowFallbacks: true,
538
+ order: ['openai', 'anthropic', 'google'],
539
+ },
540
+ fallbackModels: ['openai/gpt-5.4-mini'],
541
+ }
542
+ ```
543
+
544
+ 4. **Specify Provider Preferences**: Control which providers are used
545
+ ```typescript
546
+ providers: {
547
+ only: ['openai', 'anthropic'],
548
+ ignore: ['low-quality-provider'],
549
+ }
550
+ ```
551
+
552
+ 5. **Use ZDR for Sensitive Data**: Enable Zero Data Retention for privacy
553
+ ```typescript
554
+ defaultParams: {
555
+ providers: { dataCollection: 'deny', zdr: true },
556
+ }
557
+ ```
558
+
559
+ 6. **Monitor Model Availability**: OpenRouter's model list changes frequently
560
+ ```typescript
561
+ fetchOpenRouterModels: true // Auto-fetch latest models
562
+ ```
563
+
564
+ ## API Reference
565
+
566
+ ### OpenRouterProvider
567
+
568
+ Extends `OpenAIProvider` from `@aeye/openai`.
569
+
570
+ **Constructor**: `new OpenRouterProvider(config: OpenRouterConfig)`
571
+
572
+ **Methods**:
573
+ - Inherits all methods from `OpenAIProvider`
574
+ - `createExecutor<TContext, TMetadata>(config?)` - Returns executor with cost tracking
575
+ - `createStreamer<TContext, TMetadata>(config?)` - Returns streamer with cost tracking
576
+ - `listModels(config?)` - Fetches models from OpenRouter API with ZDR info
577
+
578
+ **Overridden Methods**:
579
+ - `createClient(config)` - Creates OpenAI client with OpenRouter endpoint
580
+ - `augmentChatRequest(params, request, config)` - Adds OpenRouter-specific params
581
+ - `buildChatParams(request, run, streaming)` - Sends only the allowed tools for `{ allowed }` tool choices
582
+ - `chatHeaders(params)` - Adds `x-anthropic-beta: structured-outputs-2025-11-13` when a tool is strict
583
+ - `createBatch(batch, run)` / `retrieveBatch(job, run)` - Inline batches through `/batches`
584
+
585
+ **Added Methods**:
586
+ - `rerank(request, ctx, config?)` - `/rerank`
587
+ - `decide(request, ctx, config?)` - alpha decisions endpoint
588
+
589
+ ### OpenRouterModelSource
590
+
591
+ Model source for fetching OpenRouter models.
592
+
593
+ **Constructor**: `new OpenRouterModelSource(config?: OpenRouterSourceConfig)`
594
+
595
+ **Methods**:
596
+ - `fetchModels(config?): Promise<ModelInfo[]>` - Fetch all available models
597
+
598
+ **Config Options**:
599
+ - `apiKey?: string` - OpenRouter API key (optional for public models)
600
+ - `includeZDR?: boolean` - Include ZDR information (default: true)
601
+
602
+ ## Supported Features
603
+
604
+ - ✅ Chat completions
605
+ - ✅ Streaming
606
+ - ✅ Function calling
607
+ - ✅ Vision (for supported models)
608
+ - ✅ Reasoning models
609
+ - ✅ Cost tracking
610
+ - ✅ Automatic fallbacks
611
+ - ✅ Provider routing
612
+ - ✅ ZDR mode
613
+ - ✅ Multi-modal (for supported models)
614
+ - ✅ Server tools (web search, web fetch, image generation)
615
+ - ✅ Prompt caching (`cacheBreakpoint` → `cache_control`)
616
+ - ✅ Image generation and editing (through chat; no streaming)
617
+ - ✅ Speech synthesis
618
+ - ✅ Transcription (no streaming)
619
+ - ✅ Embeddings
620
+ - ✅ Reranking
621
+ - ✅ Decisions (alpha endpoint)
622
+ - ✅ Batches
623
+
624
+ ## Related Packages
625
+
626
+ - **[@aeye/core](../core)**: Core @aeye framework types and interfaces
627
+ - **[@aeye/ai](../ai)**: AI abstractions and utilities
628
+ - **[@aeye/openai](../openai)**: OpenAI provider (base class)
629
+ - **[@aeye/anthropic](../anthropic)**: Anthropic Claude provider
630
+ - **[@aeye/google](../google)**: Google AI provider
631
+
632
+ ## Links
633
+
634
+ - [OpenRouter Website](https://openrouter.ai)
635
+ - [OpenRouter Models](https://openrouter.ai/models)
636
+ - [OpenRouter Documentation](https://openrouter.ai/docs)
637
+ - [OpenRouter API Keys](https://openrouter.ai/keys)
638
+
639
+ ## License
640
+
641
+ GPL-3.0
642
+
643
+ ## Contributing
644
+
645
+ Contributions are welcome! Please see the main [@aeye repository](https://github.com/ClickerMonkey/aeye) for contribution guidelines.
646
+
647
+ ## Support
648
+
649
+ For issues and questions:
650
+ - GitHub Issues: https://github.com/ClickerMonkey/aeye/issues
651
+ - Documentation: https://github.com/ClickerMonkey/aeye
652
+ - OpenRouter Discord: https://discord.gg/openrouter