Add core utility files
This commit is contained in:
167
src/model-cache.ts
Normal file
167
src/model-cache.ts
Normal 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
130
src/openrouter-api.ts
Normal 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'}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user