Skip to main content
Glama

Nexus MCP Server

openrouter.test.ts19.9 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { OpenRouterClient, OpenRouterApiError, AuthenticationError, RateLimitError, ServerError, ClientError, } from '../../../src/clients/openrouter'; import type { ChatCompletionRequest, ChatCompletionResponse, } from '../../../src/types/openrouter'; // Mock fetch globally const mockFetch = vi.fn(); global.fetch = mockFetch; describe('OpenRouterClient', () => { beforeEach(() => { vi.clearAllMocks(); // Temporarily disable fake timers to avoid deadlocks // vi.useFakeTimers(); }); afterEach(() => { // vi.useRealTimers(); // vi.clearAllTimers(); }); describe('constructor', () => { it('should create client with valid API key', () => { const client = new OpenRouterClient({ apiKey: 'sk-or-test-key-123' }); expect(client).toBeInstanceOf(OpenRouterClient); }); it('should throw error with missing API key', () => { expect(() => new OpenRouterClient({ apiKey: '' })).toThrow( 'OpenRouter API key is required' ); }); it('should use default base URL when not provided', () => { const client = new OpenRouterClient({ apiKey: 'sk-or-test-key-123' }); const headers = client.getHeaders(); expect(headers['Authorization']).toBe('Bearer sk-or-test-key-123'); }); it('should use custom base URL when provided', () => { const client = new OpenRouterClient({ apiKey: 'sk-or-test-key-123', baseUrl: 'https://custom.api.com', }); expect(client).toBeInstanceOf(OpenRouterClient); }); it('should set default headers correctly', () => { const client = new OpenRouterClient({ apiKey: 'sk-or-test-key-123' }); const headers = client.getHeaders(); expect(headers['Authorization']).toBe('Bearer sk-or-test-key-123'); expect(headers['Content-Type']).toBe('application/json'); expect(headers['User-Agent']).toBe('nexus-mcp/1.0.0'); expect(headers['HTTP-Referer']).toBe('https://github.com/adawalli/nexus'); expect(headers['X-Title']).toBe('Nexus Search MCP'); }); it('should use custom user agent when provided', () => { const client = new OpenRouterClient({ apiKey: 'sk-or-test-key-123', userAgent: 'custom-agent/1.0', }); const headers = client.getHeaders(); expect(headers['User-Agent']).toBe('custom-agent/1.0'); }); }); describe('API key validation', () => { it('should accept valid API key format', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: [] }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', }); const result = await client.testConnection(); expect(result).toBe(true); }); it('should reject invalid API key format', async () => { const client = new OpenRouterClient({ apiKey: 'invalid-key' }); await expect(client.makeRequest('/models')).rejects.toThrow( 'Invalid OpenRouter API key format' ); }); }); describe('setHeaders', () => { it('should allow setting custom headers', () => { const client = new OpenRouterClient({ apiKey: 'sk-or-test-key-123' }); client.setHeaders({ 'X-Custom': 'test-value' }); const headers = client.getHeaders(); expect(headers['X-Custom']).toBe('test-value'); }); it('should merge with existing headers', () => { const client = new OpenRouterClient({ apiKey: 'sk-or-test-key-123' }); client.setHeaders({ 'X-Custom': 'test-value' }); const headers = client.getHeaders(); expect(headers['Authorization']).toBe('Bearer sk-or-test-key-123'); expect(headers['X-Custom']).toBe('test-value'); }); }); describe('makeRequest error handling', () => { it('should handle HTTP error responses', async () => { mockFetch.mockResolvedValue({ ok: false, status: 429, statusText: 'Too Many Requests', headers: { get: () => null }, json: () => Promise.resolve({ error: { code: 429, message: 'Rate limit exceeded', type: 'rate_limit_error', }, }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', maxRetries: 0, // Don't retry }); await expect(client.makeRequest('/models')).rejects.toThrow( OpenRouterApiError ); }, 10000); it('should handle network errors', async () => { mockFetch.mockRejectedValue(new Error('Network error')); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', maxRetries: 0, }); await expect(client.makeRequest('/models')).rejects.toThrow( 'Network error' ); }, 10000); it.skip('should handle timeout', async () => { // Skip timeout test for now - complex to test with mocked timers // The timeout logic is implemented in the client }); }); describe('testConnection', () => { it('should return true for successful connection', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: [] }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', }); const result = await client.testConnection(); expect(result).toBe(true); expect(mockFetch).toHaveBeenCalledWith( 'https://openrouter.ai/api/v1/models', expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer sk-or-valid-key-123456789', }), }) ); }); it('should return false for failed connection', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 401, json: () => Promise.resolve({ error: { code: 401, message: 'Unauthorized', type: 'auth_error' }, }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', }); const result = await client.testConnection(); expect(result).toBe(false); }); }); describe('chatCompletions', () => { it('should make successful chat completion request', async () => { const mockResponse: ChatCompletionResponse = { id: 'chatcmpl-123', object: 'chat.completion', created: Date.now(), model: 'perplexity/sonar', choices: [ { index: 0, message: { role: 'assistant', content: 'Hello! How can I help you?', }, finish_reason: 'stop', }, ], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30, }, }; mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockResponse), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', }); const request: ChatCompletionRequest = { model: 'perplexity/sonar', messages: [{ role: 'user', content: 'Hello' }], }; const response = await client.chatCompletions(request); expect(response).toEqual(mockResponse); expect(mockFetch).toHaveBeenCalledWith( 'https://openrouter.ai/api/v1/chat/completions', expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ Authorization: 'Bearer sk-or-valid-key-123456789', }), body: JSON.stringify({ model: 'perplexity/sonar', messages: [{ role: 'user', content: 'Hello' }], stream: false, }), }) ); }); it('should use default Perplexity model when model not specified', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ id: 'test', object: 'chat.completion', created: Date.now(), model: 'perplexity/sonar', choices: [], usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }, }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', }); const request: ChatCompletionRequest = { model: '', // Will be overridden by default messages: [{ role: 'user', content: 'Hello' }], }; await client.chatCompletions(request); expect(mockFetch).toHaveBeenCalledWith( 'https://openrouter.ai/api/v1/chat/completions', expect.objectContaining({ body: expect.stringContaining('perplexity/sonar'), }) ); }); it('should throw error for empty messages array', async () => { const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', }); const request: ChatCompletionRequest = { model: 'test-model', messages: [], }; await expect(client.chatCompletions(request)).rejects.toThrow( 'Messages array is required and cannot be empty' ); }); it('should handle API error responses', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 400, json: () => Promise.resolve({ error: { code: 400, message: 'Invalid request', type: 'invalid_request_error', }, }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', }); const request: ChatCompletionRequest = { model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], }; await expect(client.chatCompletions(request)).rejects.toThrow( OpenRouterApiError ); }); }); describe('chatCompletionsStream', () => { it('should handle streaming responses', async () => { const mockReadableStream = { getReader: () => ({ read: vi .fn() .mockResolvedValueOnce({ done: false, value: new TextEncoder().encode( 'data: {"id":"test","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hello"}}]}\n\n' ), }) .mockResolvedValueOnce({ done: false, value: new TextEncoder().encode('data: [DONE]\n\n'), }) .mockResolvedValueOnce({ done: true, }), releaseLock: vi.fn(), }), }; mockFetch.mockResolvedValueOnce({ ok: true, body: mockReadableStream, }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', }); const request: ChatCompletionRequest = { model: 'perplexity/sonar', messages: [{ role: 'user', content: 'Hello' }], }; const chunks = []; for await (const chunk of client.chatCompletionsStream(request)) { chunks.push(chunk); } expect(chunks).toHaveLength(1); expect(chunks[0].choices[0].delta.content).toBe('Hello'); }); it('should handle streaming errors', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 400, headers: { get: () => null }, json: () => Promise.resolve({ error: { code: 400, message: 'Bad Request', type: 'invalid_request_error', }, }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', }); const request: ChatCompletionRequest = { model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], }; try { const stream = client.chatCompletionsStream(request); await stream.next(); } catch (error) { expect(error).toBeInstanceOf(ClientError); } }); it('should validate messages for streaming', async () => { const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', }); const request: ChatCompletionRequest = { model: 'test-model', messages: [], }; try { const stream = client.chatCompletionsStream(request); await stream.next(); } catch (error) { expect((error as Error).message).toBe( 'Messages array is required and cannot be empty' ); } }); }); describe('retry logic', () => { it('should retry on 429 rate limit errors', async () => { mockFetch .mockResolvedValueOnce({ ok: false, status: 429, headers: { get: () => '5' }, json: () => Promise.resolve({ error: { code: 429, message: 'Rate limit exceeded', type: 'rate_limit_error', }, }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: [] }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', maxRetries: 1, retryDelay: 1, }); const result = await client.makeRequest('/test'); expect(result).toEqual({ data: [] }); expect(mockFetch).toHaveBeenCalledTimes(2); }, 10000); it('should not retry on 401 authentication errors', async () => { mockFetch.mockResolvedValue({ ok: false, status: 401, headers: { get: () => null }, json: () => Promise.resolve({ error: { code: 401, message: 'Unauthorized', type: 'authentication_error', }, }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', maxRetries: 2, }); await expect(client.makeRequest('/test')).rejects.toThrow( AuthenticationError ); expect(mockFetch).toHaveBeenCalledTimes(1); }, 10000); it('should retry on 500 server errors', async () => { mockFetch .mockResolvedValueOnce({ ok: false, status: 500, headers: { get: () => null }, json: () => Promise.resolve({ error: { code: 500, message: 'Internal Server Error', type: 'server_error', }, }), }) .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ data: [] }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', maxRetries: 1, retryDelay: 1, }); const result = await client.makeRequest('/test'); expect(result).toEqual({ data: [] }); expect(mockFetch).toHaveBeenCalledTimes(2); }, 10000); it('should throw error after max retries exceeded', async () => { mockFetch.mockResolvedValue({ ok: false, status: 503, headers: { get: () => null }, json: () => Promise.resolve({ error: { code: 503, message: 'Service Unavailable', type: 'server_error', }, }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', maxRetries: 2, retryDelay: 1, }); await expect(client.makeRequest('/test')).rejects.toThrow(ServerError); expect(mockFetch).toHaveBeenCalledTimes(3); // Initial + 2 retries }, 10000); }); describe('error classes', () => { it('should throw AuthenticationError for 401 status', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 401, headers: { get: () => null }, json: () => Promise.resolve({ error: { code: 401, message: 'Invalid API key', type: 'authentication_error', }, }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', }); try { await client.makeRequest('/test'); } catch (error) { expect(error).toBeInstanceOf(AuthenticationError); expect((error as AuthenticationError).statusCode).toBe(401); expect((error as AuthenticationError).message).toBe('Invalid API key'); } }); it('should throw RateLimitError for 429 status with retry-after', async () => { mockFetch.mockResolvedValue({ ok: false, status: 429, headers: { get: (header: string) => (header === 'retry-after' ? '60' : null), }, json: () => Promise.resolve({ error: { code: 429, message: 'Rate limit exceeded', type: 'rate_limit_error', }, }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', maxRetries: 0, }); await expect(client.makeRequest('/test')).rejects.toThrow(RateLimitError); }, 10000); it('should throw ServerError for 5xx status codes', async () => { mockFetch.mockResolvedValue({ ok: false, status: 502, headers: { get: () => null }, json: () => Promise.resolve({ error: { code: 502, message: 'Bad Gateway', type: 'server_error' }, }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', maxRetries: 0, }); await expect(client.makeRequest('/test')).rejects.toThrow(ServerError); }, 10000); it('should throw ClientError for 4xx status codes', async () => { mockFetch.mockResolvedValue({ ok: false, status: 400, headers: { get: () => null }, json: () => Promise.resolve({ error: { code: 400, message: 'Bad Request', type: 'invalid_request_error', }, }), }); const client = new OpenRouterClient({ apiKey: 'sk-or-valid-key-123456789', maxRetries: 0, }); try { await client.makeRequest('/test'); } catch (error) { expect(error).toBeInstanceOf(ClientError); expect((error as ClientError).statusCode).toBe(400); } }, 10000); }); }); describe('OpenRouterApiError', () => { it('should create error with correct properties', () => { const error = new OpenRouterApiError( 'Test error', 429, 'rate_limit_error', 429 ); expect(error.message).toBe('Test error'); expect(error.statusCode).toBe(429); expect(error.type).toBe('rate_limit_error'); expect(error.code).toBe(429); expect(error.name).toBe('OpenRouterApiError'); }); it('should identify rate limit errors', () => { const error = new OpenRouterApiError( 'Rate limit', 429, 'rate_limit_error', 429 ); expect(error.isRateLimitError()).toBe(true); }); it('should identify authentication errors', () => { const error = new OpenRouterApiError( 'Unauthorized', 401, 'authentication_error', 401 ); expect(error.isAuthenticationError()).toBe(true); }); it('should identify server errors', () => { const error = new OpenRouterApiError( 'Server error', 500, 'server_error', 500 ); expect(error.isServerError()).toBe(true); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/adawalli/nexus'

If you have feedback or need assistance with the MCP directory API, please join our Discord server