@stabgan/openrouter-mcp-multimodal 1.4.0 → 1.8.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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +164 -147
  3. package/dist/__tests__/image-utils.test.d.ts +1 -0
  4. package/dist/__tests__/image-utils.test.js +75 -0
  5. package/dist/__tests__/integration.test.d.ts +1 -0
  6. package/dist/__tests__/integration.test.js +111 -0
  7. package/dist/__tests__/model-cache.test.d.ts +1 -0
  8. package/dist/__tests__/model-cache.test.js +96 -0
  9. package/dist/index.js +47 -46
  10. package/dist/model-cache.d.ts +18 -46
  11. package/dist/model-cache.js +28 -125
  12. package/dist/openrouter-api.d.ts +2 -22
  13. package/dist/openrouter-api.js +34 -111
  14. package/dist/tool-handlers/analyze-image.d.ts +2 -17
  15. package/dist/tool-handlers/analyze-image.js +18 -259
  16. package/dist/tool-handlers/chat-completion.d.ts +1 -0
  17. package/dist/tool-handlers/chat-completion.js +10 -143
  18. package/dist/tool-handlers/generate-image.d.ts +30 -0
  19. package/dist/tool-handlers/generate-image.js +82 -0
  20. package/dist/tool-handlers/get-model-info.d.ts +5 -5
  21. package/dist/tool-handlers/get-model-info.js +11 -41
  22. package/dist/tool-handlers/image-utils.d.ts +11 -0
  23. package/dist/tool-handlers/image-utils.js +253 -0
  24. package/dist/tool-handlers/search-models.d.ts +3 -10
  25. package/dist/tool-handlers/search-models.js +6 -37
  26. package/dist/tool-handlers/validate-model.d.ts +5 -5
  27. package/dist/tool-handlers/validate-model.js +13 -37
  28. package/dist/tool-handlers.d.ts +1 -2
  29. package/dist/tool-handlers.js +55 -234
  30. package/package.json +20 -21
  31. package/dist/index.js.map +0 -1
  32. package/dist/model-cache.js.map +0 -1
  33. package/dist/openrouter-api.js.map +0 -1
  34. package/dist/tool-handlers/analyze-image.js.map +0 -1
  35. package/dist/tool-handlers/chat-completion.js.map +0 -1
  36. package/dist/tool-handlers/get-model-info.js.map +0 -1
  37. package/dist/tool-handlers/multi-image-analysis.d.ts +0 -46
  38. package/dist/tool-handlers/multi-image-analysis.js +0 -405
  39. package/dist/tool-handlers/multi-image-analysis.js.map +0 -1
  40. package/dist/tool-handlers/search-models.js.map +0 -1
  41. package/dist/tool-handlers/validate-model.js.map +0 -1
  42. package/dist/tool-handlers.js.map +0 -1
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ import { ModelCache } from '../model-cache.js';
3
+ describe('ModelCache', () => {
4
+ let cache;
5
+ beforeEach(() => {
6
+ vi.unstubAllEnvs();
7
+ cache = ModelCache.getInstance();
8
+ cache.setModels([]);
9
+ });
10
+ afterEach(() => {
11
+ vi.unstubAllEnvs();
12
+ });
13
+ const sampleModels = [
14
+ {
15
+ id: 'openai/gpt-4',
16
+ name: 'OpenAI: GPT-4',
17
+ architecture: { input_modalities: ['text', 'image'] },
18
+ context_length: 128000,
19
+ },
20
+ {
21
+ id: 'anthropic/claude-3',
22
+ name: 'Anthropic: Claude 3',
23
+ architecture: { input_modalities: ['text', 'image'] },
24
+ context_length: 200000,
25
+ },
26
+ {
27
+ id: 'meta/llama-3',
28
+ name: 'Meta: Llama 3',
29
+ architecture: { input_modalities: ['text'] },
30
+ context_length: 8192,
31
+ },
32
+ {
33
+ id: 'qwen/qwen-vl:free',
34
+ name: 'Qwen: VL (free)',
35
+ architecture: { input_modalities: ['text', 'image'] },
36
+ context_length: 32000,
37
+ },
38
+ ];
39
+ it('should be a singleton', () => {
40
+ const a = ModelCache.getInstance();
41
+ const b = ModelCache.getInstance();
42
+ expect(a).toBe(b);
43
+ });
44
+ it('should store and retrieve models', () => {
45
+ cache.setModels(sampleModels);
46
+ expect(cache.getAll()).toHaveLength(4);
47
+ expect(cache.get('openai/gpt-4')).toEqual(sampleModels[0]);
48
+ expect(cache.get('nonexistent')).toBeNull();
49
+ });
50
+ it('should check model existence', () => {
51
+ cache.setModels(sampleModels);
52
+ expect(cache.has('openai/gpt-4')).toBe(true);
53
+ expect(cache.has('nonexistent')).toBe(false);
54
+ });
55
+ it('should report valid cache after setModels', () => {
56
+ expect(cache.isValid()).toBe(false);
57
+ cache.setModels(sampleModels);
58
+ expect(cache.isValid()).toBe(true);
59
+ });
60
+ it('should search by query', () => {
61
+ cache.setModels(sampleModels);
62
+ const results = cache.search({ query: 'gpt' });
63
+ expect(results).toHaveLength(1);
64
+ expect(results[0].id).toBe('openai/gpt-4');
65
+ });
66
+ it('should search by provider', () => {
67
+ cache.setModels(sampleModels);
68
+ const results = cache.search({ provider: 'anthropic' });
69
+ expect(results).toHaveLength(1);
70
+ expect(results[0].id).toBe('anthropic/claude-3');
71
+ });
72
+ it('should filter by vision capability', () => {
73
+ cache.setModels(sampleModels);
74
+ const results = cache.search({ capabilities: { vision: true } });
75
+ expect(results).toHaveLength(3);
76
+ expect(results.every((m) => m.architecture?.input_modalities?.includes('image'))).toBe(true);
77
+ });
78
+ it('should respect limit', () => {
79
+ cache.setModels(sampleModels);
80
+ const results = cache.search({ limit: 2 });
81
+ expect(results).toHaveLength(2);
82
+ });
83
+ it('should combine filters', () => {
84
+ cache.setModels(sampleModels);
85
+ const results = cache.search({ query: 'free', capabilities: { vision: true } });
86
+ expect(results).toHaveLength(1);
87
+ expect(results[0].id).toBe('qwen/qwen-vl:free');
88
+ });
89
+ it('should expire cache after TTL from env', async () => {
90
+ vi.stubEnv('OPENROUTER_MODEL_CACHE_TTL_MS', '25');
91
+ cache.setModels(sampleModels);
92
+ expect(cache.isValid()).toBe(true);
93
+ await new Promise((r) => setTimeout(r, 60));
94
+ expect(cache.isValid()).toBe(false);
95
+ });
96
+ });
package/dist/index.js CHANGED
@@ -1,51 +1,52 @@
1
1
  #!/usr/bin/env node
2
- // OpenRouter Multimodal MCP Server
2
+ import { Readable } from 'node:stream';
3
3
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
5
  import { ToolHandlers } from './tool-handlers.js';
6
- // Define the default model to use when none is specified
7
- const DEFAULT_MODEL = 'qwen/qwen2.5-vl-32b-instruct:free';
8
- class OpenRouterMultimodalServer {
9
- server;
10
- toolHandlers; // Using definite assignment assertion
11
- constructor() {
12
- // Retrieve API key and default model from environment variables
13
- const apiKey = process.env.OPENROUTER_API_KEY;
14
- const defaultModel = process.env.OPENROUTER_DEFAULT_MODEL || DEFAULT_MODEL;
15
- // Check if API key is provided
16
- if (!apiKey) {
17
- throw new Error('OPENROUTER_API_KEY environment variable is required');
18
- }
19
- // Initialize the server
20
- this.server = new Server({
21
- name: 'openrouter-multimodal-server',
22
- version: '1.0.0',
23
- }, {
24
- capabilities: {
25
- tools: {},
26
- },
27
- });
28
- // Set up error handling
29
- this.server.onerror = (error) => console.error('[MCP Error]', error);
30
- // Initialize tool handlers
31
- this.toolHandlers = new ToolHandlers(this.server, apiKey, defaultModel);
32
- process.on('SIGINT', async () => {
33
- await this.server.close();
34
- process.exit(0);
35
- });
36
- }
37
- async run() {
38
- const transport = new StdioServerTransport();
39
- await this.server.connect(transport);
40
- console.error('OpenRouter Multimodal MCP server running on stdio');
41
- console.error('Using API key from environment variable');
42
- console.error('Note: To use OpenRouter Multimodal, add the API key to your environment variables:');
43
- console.error(' OPENROUTER_API_KEY=your-api-key');
44
- const modelDisplay = process.env.OPENROUTER_DEFAULT_MODEL || DEFAULT_MODEL;
45
- console.error(` Using default model: ${modelDisplay}`);
46
- console.error('Server is ready to process tool calls. Waiting for input...');
47
- }
6
+ const DEFAULT_MODEL = 'nvidia/nemotron-nano-12b-v2-vl:free';
7
+ // Exit on fatal errors to prevent silent zombie processes (issue #5)
8
+ process.on('uncaughtException', (err) => {
9
+ console.error('[Fatal]', err);
10
+ process.exit(1);
11
+ });
12
+ process.on('unhandledRejection', (err) => {
13
+ console.error('[Fatal]', err);
14
+ process.exit(1);
15
+ });
16
+ const apiKey = process.env.OPENROUTER_API_KEY;
17
+ if (!apiKey) {
18
+ console.error('OPENROUTER_API_KEY is required');
19
+ process.exit(1);
48
20
  }
49
- const server = new OpenRouterMultimodalServer();
50
- server.run().catch(console.error);
51
- //# sourceMappingURL=index.js.map
21
+ const defaultModel = process.env.OPENROUTER_DEFAULT_MODEL || process.env.DEFAULT_MODEL || DEFAULT_MODEL;
22
+ const server = new Server({ name: 'openrouter-multimodal-server', version: '1.8.0' }, { capabilities: { tools: {} } });
23
+ server.onerror = (error) => console.error('[MCP Error]', error);
24
+ new ToolHandlers(server, apiKey, defaultModel);
25
+ process.on('SIGINT', async () => {
26
+ await server.close();
27
+ process.exit(0);
28
+ });
29
+ // Ensure stdin emits raw Buffers — some hosts (e.g. Claude Desktop) may set
30
+ // encoding on the stdin pipe, which causes the MCP SDK's ReadBuffer to receive
31
+ // strings instead of Buffers. ReadBuffer.readMessage() calls subarray() which
32
+ // doesn't exist on strings, triggering an infinite error loop.
33
+ const stdinStream = process.stdin;
34
+ stdinStream.setEncoding?.(undefined);
35
+ const safeStdin = new Readable({
36
+ read() { },
37
+ });
38
+ process.stdin.on('data', (chunk) => {
39
+ safeStdin.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk);
40
+ });
41
+ process.stdin.on('end', () => safeStdin.push(null));
42
+ process.stdin.on('error', (err) => safeStdin.destroy(err));
43
+ const transport = new StdioServerTransport(safeStdin, process.stdout);
44
+ server
45
+ .connect(transport)
46
+ .then(() => {
47
+ console.error(`OpenRouter MCP server running (model: ${defaultModel})`);
48
+ })
49
+ .catch((err) => {
50
+ console.error('[Fatal] Server failed to start:', err);
51
+ process.exit(1);
52
+ });
@@ -1,56 +1,28 @@
1
- /**
2
- * ModelCache - Caches OpenRouter model data to reduce API calls
3
- */
1
+ export interface OpenRouterModelRecord {
2
+ id: string;
3
+ name?: string;
4
+ architecture?: {
5
+ input_modalities?: string[];
6
+ };
7
+ context_length?: number;
8
+ [key: string]: unknown;
9
+ }
4
10
  export declare class ModelCache {
5
11
  private static instance;
6
12
  private models;
7
- private lastFetchTime;
8
- private cacheExpiryTime;
9
- private constructor();
10
- /**
11
- * Get singleton instance
12
- */
13
+ private fetchedAt;
13
14
  static getInstance(): ModelCache;
14
- /**
15
- * Check if the cache is valid
16
- */
17
- isCacheValid(): boolean;
18
- /**
19
- * Store all models
20
- */
21
- setModels(models: any[]): void;
22
- /**
23
- * Get all cached models
24
- */
25
- getAllModels(): any[];
26
- /**
27
- * Get a specific model by ID
28
- */
29
- getModel(modelId: string): any | null;
30
- /**
31
- * Check if a model exists
32
- */
33
- hasModel(modelId: string): boolean;
34
- /**
35
- * Search models based on criteria
36
- */
37
- searchModels(params: {
15
+ isValid(): boolean;
16
+ setModels(models: OpenRouterModelRecord[]): void;
17
+ getAll(): OpenRouterModelRecord[];
18
+ get(id: string): OpenRouterModelRecord | null;
19
+ has(id: string): boolean;
20
+ search(params: {
38
21
  query?: string;
39
22
  provider?: string;
40
- minContextLength?: number | string;
41
- maxContextLength?: number | string;
42
- maxPromptPrice?: number | string;
43
- maxCompletionPrice?: number | string;
44
23
  capabilities?: {
45
- functions?: boolean;
46
- tools?: boolean;
47
24
  vision?: boolean;
48
- json_mode?: boolean;
49
25
  };
50
- limit?: number | string;
51
- }): any[];
52
- /**
53
- * Reset the cache
54
- */
55
- resetCache(): void;
26
+ limit?: number;
27
+ }): OpenRouterModelRecord[];
56
28
  }
@@ -1,143 +1,46 @@
1
- /**
2
- * ModelCache - Caches OpenRouter model data to reduce API calls
3
- */
1
+ function getCacheTtlMs() {
2
+ const raw = process.env.OPENROUTER_MODEL_CACHE_TTL_MS;
3
+ if (raw === undefined || raw === '')
4
+ return 3600000;
5
+ const n = parseInt(raw, 10);
6
+ return Number.isFinite(n) && n > 0 ? n : 3600000;
7
+ }
4
8
  export class ModelCache {
5
9
  static instance;
6
- models;
7
- lastFetchTime;
8
- cacheExpiryTime; // in milliseconds (1 hour = 3600000)
9
- constructor() {
10
- this.models = {};
11
- this.lastFetchTime = 0;
12
- this.cacheExpiryTime = 3600000; // 1 hour
13
- }
14
- /**
15
- * Get singleton instance
16
- */
10
+ models = {};
11
+ fetchedAt = 0;
17
12
  static getInstance() {
18
- if (!ModelCache.instance) {
19
- ModelCache.instance = new ModelCache();
20
- }
21
- return ModelCache.instance;
13
+ return (ModelCache.instance ??= new ModelCache());
22
14
  }
23
- /**
24
- * Check if the cache is valid
25
- */
26
- isCacheValid() {
27
- return (Object.keys(this.models).length > 0 &&
28
- Date.now() - this.lastFetchTime < this.cacheExpiryTime);
15
+ isValid() {
16
+ return Object.keys(this.models).length > 0 && Date.now() - this.fetchedAt < getCacheTtlMs();
29
17
  }
30
- /**
31
- * Store all models
32
- */
33
18
  setModels(models) {
34
- this.models = {};
35
- for (const model of models) {
36
- this.models[model.id] = model;
37
- }
38
- this.lastFetchTime = Date.now();
19
+ this.models = Object.fromEntries(models.map((m) => [m.id, m]));
20
+ this.fetchedAt = Date.now();
39
21
  }
40
- /**
41
- * Get all cached models
42
- */
43
- getAllModels() {
22
+ getAll() {
44
23
  return Object.values(this.models);
45
24
  }
46
- /**
47
- * Get a specific model by ID
48
- */
49
- getModel(modelId) {
50
- return this.models[modelId] || null;
25
+ get(id) {
26
+ return this.models[id] ?? null;
51
27
  }
52
- /**
53
- * Check if a model exists
54
- */
55
- hasModel(modelId) {
56
- return !!this.models[modelId];
28
+ has(id) {
29
+ return id in this.models;
57
30
  }
58
- /**
59
- * Search models based on criteria
60
- */
61
- searchModels(params) {
62
- let results = this.getAllModels();
63
- // Apply text search
31
+ search(params) {
32
+ let results = this.getAll();
64
33
  if (params.query) {
65
- const query = params.query.toLowerCase();
66
- results = results.filter((model) => model.id.toLowerCase().includes(query) ||
67
- (model.description && model.description.toLowerCase().includes(query)) ||
68
- (model.provider && model.provider.toLowerCase().includes(query)));
34
+ const q = params.query.toLowerCase();
35
+ results = results.filter((m) => m.id.toLowerCase().includes(q) || m.name?.toLowerCase().includes(q));
69
36
  }
70
- // Filter by provider
71
37
  if (params.provider) {
72
- results = results.filter((model) => model.provider && model.provider.toLowerCase() === params.provider.toLowerCase());
73
- }
74
- // Filter by context length
75
- if (params.minContextLength !== undefined) {
76
- const minContextLength = typeof params.minContextLength === 'string'
77
- ? parseInt(params.minContextLength, 10)
78
- : params.minContextLength;
79
- if (!isNaN(minContextLength)) {
80
- results = results.filter((model) => model.context_length >= minContextLength);
81
- }
38
+ const p = params.provider.toLowerCase();
39
+ results = results.filter((m) => m.id.toLowerCase().startsWith(p + '/'));
82
40
  }
83
- if (params.maxContextLength !== undefined) {
84
- const maxContextLength = typeof params.maxContextLength === 'string'
85
- ? parseInt(params.maxContextLength, 10)
86
- : params.maxContextLength;
87
- if (!isNaN(maxContextLength)) {
88
- results = results.filter((model) => model.context_length <= maxContextLength);
89
- }
41
+ if (params.capabilities?.vision) {
42
+ results = results.filter((m) => m.architecture?.input_modalities?.includes('image'));
90
43
  }
91
- // Filter by price
92
- if (params.maxPromptPrice !== undefined) {
93
- const maxPromptPrice = typeof params.maxPromptPrice === 'string'
94
- ? parseFloat(params.maxPromptPrice)
95
- : params.maxPromptPrice;
96
- if (!isNaN(maxPromptPrice)) {
97
- results = results.filter((model) => !model.pricing?.prompt || model.pricing.prompt <= maxPromptPrice);
98
- }
99
- }
100
- if (params.maxCompletionPrice !== undefined) {
101
- const maxCompletionPrice = typeof params.maxCompletionPrice === 'string'
102
- ? parseFloat(params.maxCompletionPrice)
103
- : params.maxCompletionPrice;
104
- if (!isNaN(maxCompletionPrice)) {
105
- results = results.filter((model) => !model.pricing?.completion ||
106
- model.pricing.completion <= maxCompletionPrice);
107
- }
108
- }
109
- // Filter by capabilities
110
- if (params.capabilities) {
111
- if (params.capabilities.functions) {
112
- results = results.filter((model) => model.capabilities?.function_calling);
113
- }
114
- if (params.capabilities.tools) {
115
- results = results.filter((model) => model.capabilities?.tools);
116
- }
117
- if (params.capabilities.vision) {
118
- results = results.filter((model) => model.capabilities?.vision);
119
- }
120
- if (params.capabilities.json_mode) {
121
- results = results.filter((model) => model.capabilities?.json_mode);
122
- }
123
- }
124
- // Apply limit
125
- if (params.limit !== undefined) {
126
- const limit = typeof params.limit === 'string'
127
- ? parseInt(params.limit, 10)
128
- : params.limit;
129
- if (!isNaN(limit) && limit > 0) {
130
- results = results.slice(0, limit);
131
- }
132
- }
133
- return results;
134
- }
135
- /**
136
- * Reset the cache
137
- */
138
- resetCache() {
139
- this.models = {};
140
- this.lastFetchTime = 0;
44
+ return results.slice(0, params.limit ?? 10);
141
45
  }
142
46
  }
143
- //# sourceMappingURL=model-cache.js.map
@@ -1,26 +1,6 @@
1
- /**
2
- * Client for interacting with the OpenRouter API
3
- */
1
+ import type { OpenRouterModelRecord } from './model-cache.js';
4
2
  export declare class OpenRouterAPIClient {
5
3
  private apiKey;
6
- private axiosInstance;
7
- private retryCount;
8
- private retryDelay;
9
4
  constructor(apiKey: string);
10
- /**
11
- * Get all available models from OpenRouter
12
- */
13
- getModels(): Promise<any[]>;
14
- /**
15
- * Send a request to the OpenRouter API with retry functionality
16
- */
17
- request(endpoint: string, method: string, data?: any): Promise<any>;
18
- /**
19
- * Handle retryable errors
20
- */
21
- private handleRetryableError;
22
- /**
23
- * Handle request errors
24
- */
25
- private handleRequestError;
5
+ getModels(): Promise<OpenRouterModelRecord[]>;
26
6
  }
@@ -1,119 +1,42 @@
1
- import axios from 'axios';
2
- import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
3
- /**
4
- * Client for interacting with the OpenRouter API
5
- */
1
+ const BASE_URL = 'https://openrouter.ai/api/v1';
2
+ async function sleep(ms) {
3
+ return new Promise((r) => setTimeout(r, ms));
4
+ }
5
+ async function fetchWithRetry(url, init, retries = 2) {
6
+ let lastErr;
7
+ for (let attempt = 0; attempt <= retries; attempt++) {
8
+ try {
9
+ const res = await fetch(url, init);
10
+ if (res.status === 429 || res.status >= 500) {
11
+ if (attempt < retries)
12
+ await sleep(400 * (attempt + 1));
13
+ else
14
+ return res;
15
+ continue;
16
+ }
17
+ return res;
18
+ }
19
+ catch (e) {
20
+ lastErr = e;
21
+ if (attempt < retries)
22
+ await sleep(400 * (attempt + 1));
23
+ }
24
+ }
25
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
26
+ }
6
27
  export class OpenRouterAPIClient {
7
28
  apiKey;
8
- axiosInstance;
9
- retryCount = 3;
10
- retryDelay = 1000; // Initial delay in ms
11
29
  constructor(apiKey) {
12
30
  this.apiKey = apiKey;
13
- this.axiosInstance = axios.create({
14
- baseURL: 'https://openrouter.ai/api/v1',
15
- headers: {
16
- 'Authorization': `Bearer ${this.apiKey}`,
17
- 'Content-Type': 'application/json',
18
- 'HTTP-Referer': 'https://github.com/stabgan/openrouter-mcp-multimodal',
19
- 'X-Title': 'OpenRouter MCP Multimodal Server'
20
- },
21
- timeout: 60000 // 60 seconds timeout
22
- });
23
31
  }
24
- /**
25
- * Get all available models from OpenRouter
26
- */
27
32
  async getModels() {
28
- try {
29
- const response = await this.axiosInstance.get('/models');
30
- return response.data.data;
31
- }
32
- catch (error) {
33
- this.handleRequestError(error);
34
- return [];
35
- }
36
- }
37
- /**
38
- * Send a request to the OpenRouter API with retry functionality
39
- */
40
- async request(endpoint, method, data) {
41
- let lastError = null;
42
- let retries = 0;
43
- while (retries <= this.retryCount) {
44
- try {
45
- const response = await this.axiosInstance.request({
46
- url: endpoint,
47
- method,
48
- data
49
- });
50
- return response.data;
51
- }
52
- catch (error) {
53
- lastError = this.handleRetryableError(error, retries);
54
- retries++;
55
- if (retries <= this.retryCount) {
56
- // Exponential backoff with jitter
57
- const delay = this.retryDelay * Math.pow(2, retries - 1) * (0.5 + Math.random() * 0.5);
58
- console.error(`Retrying in ${Math.round(delay)}ms (${retries}/${this.retryCount})`);
59
- await new Promise(resolve => setTimeout(resolve, delay));
60
- }
61
- }
62
- }
63
- // If we get here, all retries failed
64
- throw lastError || new Error('Request failed after multiple retries');
65
- }
66
- /**
67
- * Handle retryable errors
68
- */
69
- handleRetryableError(error, retryCount) {
70
- if (axios.isAxiosError(error)) {
71
- const axiosError = error;
72
- // Rate limiting (429) or server errors (5xx)
73
- if (axiosError.response?.status === 429 || (axiosError.response?.status && axiosError.response.status >= 500)) {
74
- console.error(`Request error (retry ${retryCount}): ${axiosError.message}`);
75
- if (axiosError.response?.status === 429) {
76
- console.error('Rate limit exceeded. Retrying with backoff...');
77
- }
78
- return new Error(`OpenRouter API error: ${axiosError.response?.status} ${axiosError.message}`);
79
- }
80
- // For other status codes, don't retry
81
- if (axiosError.response) {
82
- const responseData = axiosError.response.data;
83
- const message = responseData?.error?.message || axiosError.message;
84
- throw new McpError(ErrorCode.InternalError, `OpenRouter API error: ${message}`);
85
- }
86
- }
87
- // Network errors should be retried
88
- console.error(`Network error (retry ${retryCount}): ${error.message}`);
89
- return new Error(`Network error: ${error.message}`);
90
- }
91
- /**
92
- * Handle request errors
93
- */
94
- handleRequestError(error) {
95
- console.error('Error in OpenRouter API request:', error);
96
- if (axios.isAxiosError(error)) {
97
- const axiosError = error;
98
- if (axiosError.response) {
99
- const status = axiosError.response.status;
100
- const responseData = axiosError.response.data;
101
- const message = responseData?.error?.message || axiosError.message;
102
- if (status === 401 || status === 403) {
103
- throw new McpError(ErrorCode.InvalidRequest, `Authentication error: ${message}`);
104
- }
105
- else if (status === 429) {
106
- throw new McpError(ErrorCode.InternalError, `Rate limit exceeded: ${message}`);
107
- }
108
- else {
109
- throw new McpError(ErrorCode.InternalError, `OpenRouter API error (${status}): ${message}`);
110
- }
111
- }
112
- else if (axiosError.request) {
113
- throw new McpError(ErrorCode.ConnectionClosed, `Network error: ${axiosError.message}`);
114
- }
115
- }
116
- throw new McpError(ErrorCode.InternalError, `Unknown error: ${error.message || 'No error message'}`);
33
+ const res = await fetchWithRetry(`${BASE_URL}/models`, {
34
+ headers: { Authorization: `Bearer ${this.apiKey}` },
35
+ signal: AbortSignal.timeout(30000),
36
+ }, 2);
37
+ if (!res.ok)
38
+ throw new Error(`Failed to fetch models: HTTP ${res.status}`);
39
+ const data = (await res.json());
40
+ return data.data ?? [];
117
41
  }
118
42
  }
119
- //# sourceMappingURL=openrouter-api.js.map
@@ -4,9 +4,6 @@ export interface AnalyzeImageToolRequest {
4
4
  question?: string;
5
5
  model?: string;
6
6
  }
7
- /**
8
- * Handler for analyzing a single image
9
- */
10
7
  export declare function handleAnalyzeImage(request: {
11
8
  params: {
12
9
  arguments: AnalyzeImageToolRequest;
@@ -16,23 +13,11 @@ export declare function handleAnalyzeImage(request: {
16
13
  type: string;
17
14
  text: string;
18
15
  }[];
19
- metadata: {
20
- model: string;
21
- usage: OpenAI.Completions.CompletionUsage | undefined;
22
- error_type?: undefined;
23
- error_message?: undefined;
24
- };
25
- isError?: undefined;
16
+ isError: boolean;
26
17
  } | {
27
18
  content: {
28
19
  type: string;
29
20
  text: string;
30
21
  }[];
31
- isError: boolean;
32
- metadata: {
33
- error_type: string;
34
- error_message: string;
35
- model?: undefined;
36
- usage?: undefined;
37
- };
22
+ isError?: undefined;
38
23
  }>;