Add core utility files

This commit is contained in:
Kaustabh Ganguly
2025-03-26 22:20:25 +05:30
parent 19e9dd9a3f
commit 9ddbf4e56c
2 changed files with 297 additions and 0 deletions

167
src/model-cache.ts Normal file
View File

@@ -0,0 +1,167 @@
/**
* ModelCache - Caches OpenRouter model data to reduce API calls
*/
export class ModelCache {
private static instance: ModelCache;
private models: Record<string, any>;
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;
}
}

130
src/openrouter-api.ts Normal file
View File

@@ -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<any[]> {
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<any> {
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'}`);
}
}