From 9ddbf4e56c1f83b4339955496bf804fa56d5c888 Mon Sep 17 00:00:00 2001 From: Kaustabh Ganguly Date: Wed, 26 Mar 2025 22:20:25 +0530 Subject: [PATCH] Add core utility files --- src/model-cache.ts | 167 ++++++++++++++++++++++++++++++++++++++++++ src/openrouter-api.ts | 130 ++++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 src/model-cache.ts create mode 100644 src/openrouter-api.ts diff --git a/src/model-cache.ts b/src/model-cache.ts new file mode 100644 index 0000000..72a29a7 --- /dev/null +++ b/src/model-cache.ts @@ -0,0 +1,167 @@ +/** + * ModelCache - Caches OpenRouter model data to reduce API calls + */ +export class ModelCache { + private static instance: ModelCache; + private models: Record; + private lastFetchTime: number; + private cacheExpiryTime: number; // in milliseconds (1 hour = 3600000) + + private constructor() { + this.models = {}; + this.lastFetchTime = 0; + this.cacheExpiryTime = 3600000; // 1 hour + } + + /** + * Get singleton instance + */ + public static getInstance(): ModelCache { + if (!ModelCache.instance) { + ModelCache.instance = new ModelCache(); + } + return ModelCache.instance; + } + + /** + * Check if the cache is valid + */ + public isCacheValid(): boolean { + return ( + Object.keys(this.models).length > 0 && + Date.now() - this.lastFetchTime < this.cacheExpiryTime + ); + } + + /** + * Store all models + */ + public setModels(models: any[]): void { + this.models = {}; + for (const model of models) { + this.models[model.id] = model; + } + this.lastFetchTime = Date.now(); + } + + /** + * Get all cached models + */ + public getAllModels(): any[] { + return Object.values(this.models); + } + + /** + * Get a specific model by ID + */ + public getModel(modelId: string): any | null { + return this.models[modelId] || null; + } + + /** + * Check if a model exists + */ + public hasModel(modelId: string): boolean { + return !!this.models[modelId]; + } + + /** + * Search models based on criteria + */ + public searchModels(params: { + query?: string; + provider?: string; + minContextLength?: number; + maxContextLength?: number; + maxPromptPrice?: number; + maxCompletionPrice?: number; + capabilities?: { + functions?: boolean; + tools?: boolean; + vision?: boolean; + json_mode?: boolean; + }; + limit?: number; + }): any[] { + let results = this.getAllModels(); + + // Apply text search + if (params.query) { + const query = params.query.toLowerCase(); + results = results.filter((model) => + model.id.toLowerCase().includes(query) || + (model.description && model.description.toLowerCase().includes(query)) || + (model.provider && model.provider.toLowerCase().includes(query)) + ); + } + + // Filter by provider + if (params.provider) { + results = results.filter((model) => + model.provider && model.provider.toLowerCase() === params.provider!.toLowerCase() + ); + } + + // Filter by context length + if (params.minContextLength) { + results = results.filter( + (model) => model.context_length >= params.minContextLength + ); + } + + if (params.maxContextLength) { + results = results.filter( + (model) => model.context_length <= params.maxContextLength + ); + } + + // Filter by price + if (params.maxPromptPrice) { + results = results.filter( + (model) => + !model.pricing?.prompt || model.pricing.prompt <= params.maxPromptPrice + ); + } + + if (params.maxCompletionPrice) { + results = results.filter( + (model) => + !model.pricing?.completion || + model.pricing.completion <= params.maxCompletionPrice + ); + } + + // Filter by capabilities + if (params.capabilities) { + if (params.capabilities.functions) { + results = results.filter( + (model) => model.capabilities?.function_calling + ); + } + if (params.capabilities.tools) { + results = results.filter((model) => model.capabilities?.tools); + } + if (params.capabilities.vision) { + results = results.filter((model) => model.capabilities?.vision); + } + if (params.capabilities.json_mode) { + results = results.filter((model) => model.capabilities?.json_mode); + } + } + + // Apply limit + if (params.limit && params.limit > 0) { + results = results.slice(0, params.limit); + } + + return results; + } + + /** + * Reset the cache + */ + public resetCache(): void { + this.models = {}; + this.lastFetchTime = 0; + } +} diff --git a/src/openrouter-api.ts b/src/openrouter-api.ts new file mode 100644 index 0000000..0a0803e --- /dev/null +++ b/src/openrouter-api.ts @@ -0,0 +1,130 @@ +import axios, { AxiosError, AxiosInstance } from 'axios'; +import { McpError } from '@modelcontextprotocol/sdk/types.js'; + +/** + * Client for interacting with the OpenRouter API + */ +export class OpenRouterAPIClient { + private apiKey: string; + private axiosInstance: AxiosInstance; + private retryCount: number = 3; + private retryDelay: number = 1000; // Initial delay in ms + + constructor(apiKey: string) { + this.apiKey = apiKey; + this.axiosInstance = axios.create({ + baseURL: 'https://openrouter.ai/api/v1', + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + 'HTTP-Referer': 'https://github.com/stabgan/openrouter-mcp-multimodal', + 'X-Title': 'OpenRouter MCP Multimodal Server' + }, + timeout: 60000 // 60 seconds timeout + }); + } + + /** + * Get all available models from OpenRouter + */ + public async getModels(): Promise { + try { + const response = await this.axiosInstance.get('/models'); + return response.data.data; + } catch (error) { + this.handleRequestError(error); + return []; + } + } + + /** + * Send a request to the OpenRouter API with retry functionality + */ + public async request(endpoint: string, method: string, data?: any): Promise { + let lastError: Error | null = null; + let retries = 0; + + while (retries <= this.retryCount) { + try { + const response = await this.axiosInstance.request({ + url: endpoint, + method, + data + }); + + return response.data; + } catch (error) { + lastError = this.handleRetryableError(error, retries); + retries++; + + if (retries <= this.retryCount) { + // Exponential backoff with jitter + const delay = this.retryDelay * Math.pow(2, retries - 1) * (0.5 + Math.random() * 0.5); + console.error(`Retrying in ${Math.round(delay)}ms (${retries}/${this.retryCount})`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + // If we get here, all retries failed + throw lastError || new Error('Request failed after multiple retries'); + } + + /** + * Handle retryable errors + */ + private handleRetryableError(error: any, retryCount: number): Error { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + + // Rate limiting (429) or server errors (5xx) + if (axiosError.response?.status === 429 || (axiosError.response?.status && axiosError.response.status >= 500)) { + console.error(`Request error (retry ${retryCount}): ${axiosError.message}`); + if (axiosError.response?.status === 429) { + console.error('Rate limit exceeded. Retrying with backoff...'); + } + return new Error(`OpenRouter API error: ${axiosError.response?.status} ${axiosError.message}`); + } + + // For other status codes, don't retry + if (axiosError.response) { + const responseData = axiosError.response.data as any; + const message = responseData?.error?.message || axiosError.message; + throw new McpError('RequestFailed', `OpenRouter API error: ${message}`); + } + } + + // Network errors should be retried + console.error(`Network error (retry ${retryCount}): ${error.message}`); + return new Error(`Network error: ${error.message}`); + } + + /** + * Handle request errors + */ + private handleRequestError(error: any): never { + console.error('Error in OpenRouter API request:', error); + + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + + if (axiosError.response) { + const status = axiosError.response.status; + const responseData = axiosError.response.data as any; + const message = responseData?.error?.message || axiosError.message; + + if (status === 401 || status === 403) { + throw new McpError('Unauthorized', `Authentication error: ${message}`); + } else if (status === 429) { + throw new McpError('RateLimitExceeded', `Rate limit exceeded: ${message}`); + } else { + throw new McpError('RequestFailed', `OpenRouter API error (${status}): ${message}`); + } + } else if (axiosError.request) { + throw new McpError('NetworkError', `Network error: ${axiosError.message}`); + } + } + + throw new McpError('UnknownError', `Unknown error: ${error.message || 'No error message'}`); + } +}