Skip to main content
Glama

MCP Perplexity Pro

perplexity-api.tsโ€ข17.9 kB
import fetch from 'node-fetch'; import { Readable } from 'stream'; import type { PerplexityRequest, PerplexityResponse, PerplexityStreamChunk, StreamingCallbacks, AsyncJob, ErrorResponse, Config, } from './types.js'; import { suggestFallbackModel } from './models.js'; // Perplexity API endpoints const PERPLEXITY_BASE_URL = 'https://api.perplexity.ai'; const CHAT_COMPLETIONS_ENDPOINT = '/chat/completions'; const ASYNC_CHAT_COMPLETIONS_ENDPOINT = '/async/chat/completions'; export class PerplexityApiError extends Error { constructor( message: string, public status?: number, public response?: any ) { super(message); this.name = 'PerplexityApiError'; } } export class PerplexityApiClient { private apiKey: string; private baseUrl: string; constructor(config: Config) { this.apiKey = config.api_key; this.baseUrl = PERPLEXITY_BASE_URL; } /** * Makes a request to the Perplexity API with proper error handling */ private async makeRequest<T>(endpoint: string, body: any, method = 'POST'): Promise<T> { const url = `${this.baseUrl}${endpoint}`; console.error(`Making request to: ${url}`); console.error(`API key: ${this.apiKey ? this.apiKey.substring(0, 10) + '...' : 'MISSING'}`); try { const response = await fetch(url, { method, headers: { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: method === 'POST' ? JSON.stringify(body) : null, }); // Check if response is JSON before parsing const contentType = response.headers.get('content-type'); let data: any; if (contentType?.includes('application/json')) { data = await response.json(); } else { // If not JSON, get the text for better error messages const text = await response.text(); data = { error: { message: `Non-JSON response: ${text.substring(0, 200)}...` } }; } if (!response.ok) { throw new PerplexityApiError( data.error?.message || `HTTP ${response.status}: ${response.statusText}`, response.status, data ); } return data as T; } catch (error) { if (error instanceof PerplexityApiError) { throw error; } if (error instanceof Error) { throw new PerplexityApiError(`Network error: ${error.message}`); } throw new PerplexityApiError('Unknown error occurred'); } } /** * Sends a chat completion request to Perplexity */ async chatCompletion(request: PerplexityRequest): Promise<PerplexityResponse> { // Prepare the request body according to Perplexity API format const requestBody = { model: request.model, messages: request.messages, ...(request.max_tokens && { max_tokens: request.max_tokens }), ...(request.temperature !== undefined && { temperature: request.temperature }), ...(request.top_p !== undefined && { top_p: request.top_p }), ...(request.search_domain_filter && { search_domain_filter: request.search_domain_filter }), ...(request.return_images !== undefined && { return_images: request.return_images }), ...(request.return_related_questions !== undefined && { return_related_questions: request.return_related_questions, }), ...(request.search_recency_filter && { search_recency_filter: request.search_recency_filter, }), ...(request.search_after_date_filter && { search_after_date_filter: request.search_after_date_filter, }), ...(request.search_before_date_filter && { search_before_date_filter: request.search_before_date_filter, }), ...(request.last_updated_after_filter && { last_updated_after_filter: request.last_updated_after_filter, }), ...(request.last_updated_before_filter && { last_updated_before_filter: request.last_updated_before_filter, }), ...(request.top_k !== undefined && { top_k: request.top_k }), ...(request.stream !== undefined && { stream: request.stream }), ...(request.presence_penalty !== undefined && { presence_penalty: request.presence_penalty }), ...(request.frequency_penalty !== undefined && { frequency_penalty: request.frequency_penalty, }), ...(request.response_format && { response_format: request.response_format }), ...(request.disable_search !== undefined && { disable_search: request.disable_search }), ...(request.enable_search_classifier !== undefined && { enable_search_classifier: request.enable_search_classifier, }), ...(request.web_search_options && { web_search_options: request.web_search_options }), }; return this.makeRequest<PerplexityResponse>(CHAT_COMPLETIONS_ENDPOINT, requestBody); } /** * Sends a streaming chat completion request to Perplexity */ async chatCompletionStream( request: PerplexityRequest, callbacks: StreamingCallbacks ): Promise<PerplexityResponse> { const url = `${this.baseUrl}${CHAT_COMPLETIONS_ENDPOINT}`; // Force streaming mode const requestBody = { model: request.model, messages: request.messages, stream: true, // Force streaming ...(request.max_tokens && { max_tokens: request.max_tokens }), ...(request.temperature !== undefined && { temperature: request.temperature }), ...(request.top_p !== undefined && { top_p: request.top_p }), ...(request.search_domain_filter && { search_domain_filter: request.search_domain_filter }), ...(request.return_images !== undefined && { return_images: request.return_images }), ...(request.return_related_questions !== undefined && { return_related_questions: request.return_related_questions, }), ...(request.search_recency_filter && { search_recency_filter: request.search_recency_filter, }), ...(request.search_after_date_filter && { search_after_date_filter: request.search_after_date_filter, }), ...(request.search_before_date_filter && { search_before_date_filter: request.search_before_date_filter, }), ...(request.last_updated_after_filter && { last_updated_after_filter: request.last_updated_after_filter, }), ...(request.last_updated_before_filter && { last_updated_before_filter: request.last_updated_before_filter, }), ...(request.top_k !== undefined && { top_k: request.top_k }), ...(request.presence_penalty !== undefined && { presence_penalty: request.presence_penalty }), ...(request.frequency_penalty !== undefined && { frequency_penalty: request.frequency_penalty, }), ...(request.response_format && { response_format: request.response_format }), ...(request.disable_search !== undefined && { disable_search: request.disable_search }), ...(request.enable_search_classifier !== undefined && { enable_search_classifier: request.enable_search_classifier, }), ...(request.web_search_options && { web_search_options: request.web_search_options }), }; try { const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', Accept: 'text/event-stream', }, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response.text(); throw new PerplexityApiError( `HTTP ${response.status}: ${response.statusText}`, response.status, { error: { message: errorText } } ); } if (!response.body) { throw new PerplexityApiError('No response body received'); } // In node-fetch v3, response.body is already a Node.js Readable stream // Cast it to the correct type for TypeScript const bodyStream = response.body as unknown as Readable; return this.processStreamingResponse(bodyStream, callbacks); } catch (error) { if (callbacks.onError) { callbacks.onError(error instanceof Error ? error : new Error('Unknown streaming error')); } throw error; } } /** * Processes Server-Sent Events from Perplexity's streaming API */ private async processStreamingResponse( body: Readable, callbacks: StreamingCallbacks ): Promise<PerplexityResponse> { return new Promise((resolve, reject) => { let buffer = ''; let finalResponse: PerplexityResponse | null = null; let aggregatedContent = ''; let responseMetadata: any = {}; body.on('data', chunk => { buffer += chunk.toString(); // Process complete lines const lines = buffer.split('\n'); buffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { this.processSSELine(line, { onChunk: chunk => { // Aggregate content for final response if (chunk.choices?.[0]?.delta?.content) { aggregatedContent += chunk.choices[0].delta.content; } // Store metadata from first chunk if (!responseMetadata.id) { responseMetadata = { id: chunk.id, model: chunk.model, created: chunk.created, object: 'chat.completion', }; } // Forward chunk to callback if (callbacks.onChunk) { callbacks.onChunk(chunk); } }, onComplete: response => { finalResponse = response; }, onError: callbacks.onError || (() => {}), }); } }); body.on('end', () => { // If we have a final response from [DONE], use it if (finalResponse) { if (callbacks.onComplete) { callbacks.onComplete(finalResponse); } resolve(finalResponse); } else { // Otherwise, construct final response from aggregated data const constructedResponse: PerplexityResponse = { ...responseMetadata, choices: [ { index: 0, finish_reason: 'stop', message: { role: 'assistant', content: aggregatedContent, }, }, ], usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, }, }; if (callbacks.onComplete) { callbacks.onComplete(constructedResponse); } resolve(constructedResponse); } }); body.on('error', error => { if (callbacks.onError) { callbacks.onError(error); } reject(error); }); }); } /** * Processes a single Server-Sent Event line */ private processSSELine(line: string, callbacks: StreamingCallbacks): void { if (!line.trim()) return; // Handle SSE format: "data: {json}" if (line.startsWith('data: ')) { const data = line.slice(6).trim(); // Check for completion signal if (data === '[DONE]') { return; } try { const chunk: PerplexityStreamChunk = JSON.parse(data); if (callbacks.onChunk) { callbacks.onChunk(chunk); } } catch (error) { console.warn('Failed to parse SSE chunk:', data, error); } } } /** * Creates an async chat completion job */ async createAsyncChatCompletion(request: PerplexityRequest): Promise<AsyncJob> { const requestBody = { request: { model: request.model, messages: request.messages, ...(request.max_tokens && { max_tokens: request.max_tokens }), ...(request.temperature !== undefined && { temperature: request.temperature }), ...(request.top_p !== undefined && { top_p: request.top_p }), ...(request.search_domain_filter && { search_domain_filter: request.search_domain_filter }), ...(request.return_images !== undefined && { return_images: request.return_images }), ...(request.return_related_questions !== undefined && { return_related_questions: request.return_related_questions, }), ...(request.search_recency_filter && { search_recency_filter: request.search_recency_filter, }), ...(request.search_after_date_filter && { search_after_date_filter: request.search_after_date_filter, }), ...(request.search_before_date_filter && { search_before_date_filter: request.search_before_date_filter, }), ...(request.last_updated_after_filter && { last_updated_after_filter: request.last_updated_after_filter, }), ...(request.last_updated_before_filter && { last_updated_before_filter: request.last_updated_before_filter, }), ...(request.top_k !== undefined && { top_k: request.top_k }), ...(request.stream !== undefined && { stream: request.stream }), ...(request.presence_penalty !== undefined && { presence_penalty: request.presence_penalty, }), ...(request.frequency_penalty !== undefined && { frequency_penalty: request.frequency_penalty, }), ...(request.response_format && { response_format: request.response_format }), ...(request.disable_search !== undefined && { disable_search: request.disable_search }), ...(request.enable_search_classifier !== undefined && { enable_search_classifier: request.enable_search_classifier, }), ...(request.web_search_options && { web_search_options: request.web_search_options }), }, }; return this.makeRequest<AsyncJob>(ASYNC_CHAT_COMPLETIONS_ENDPOINT, requestBody); } /** * Alias for backward compatibility */ async createAsyncChat(request: PerplexityRequest): Promise<AsyncJob> { return this.createAsyncChatCompletion(request); } /** * Lists async chat completion jobs */ async listAsyncJobs( limit = 20, nextToken?: string ): Promise<{ jobs: AsyncJob[]; next_token?: string }> { const params = new URLSearchParams(); params.append('limit', limit.toString()); if (nextToken) { params.append('next_token', encodeURIComponent(nextToken)); } const endpoint = `${ASYNC_CHAT_COMPLETIONS_ENDPOINT}?${params.toString()}`; return this.makeRequest(endpoint, null, 'GET'); } /** * Gets the status and result of an async job */ async getAsyncJob(jobId: string): Promise<AsyncJob> { const endpoint = `${ASYNC_CHAT_COMPLETIONS_ENDPOINT}/${jobId}`; return this.makeRequest<AsyncJob>(endpoint, null, 'GET'); } /** * Handles API errors and creates structured error responses */ static handleError(error: unknown, context?: { model?: string; query?: string }): ErrorResponse { if (error instanceof PerplexityApiError) { // Rate limiting if (error.status === 429) { return { error: { type: 'rate_limit', message: 'Rate limit exceeded', details: { suggestion: 'Wait before retrying or use async_perplexity for queuing', retry_after: error.response?.headers?.['retry-after'] || 60, ...(context?.model && { fallback_model: suggestFallbackModel(context.model as any, 'rate_limit'), }), }, }, }; } // Invalid model if (error.status === 400 && error.message.toLowerCase().includes('model')) { return { error: { type: 'invalid_model', message: `Invalid model: ${context?.model || 'unknown'}`, details: { suggestion: 'Use a supported model: sonar, sonar-pro, sonar-reasoning, sonar-reasoning-pro, sonar-deep-research', fallback_model: 'sonar-reasoning-pro', }, }, }; } // Authentication error if (error.status === 401) { return { error: { type: 'api_error', message: 'Authentication failed - check your API key', details: { suggestion: 'Verify your Perplexity API key is correct and has sufficient credits', }, }, }; } // Server error if (error.status && error.status >= 500) { return { error: { type: 'api_error', message: 'Perplexity API server error', details: { suggestion: 'Try again later or use a different model', fallback_model: context?.model ? suggestFallbackModel(context.model as any) : 'sonar', }, }, }; } // General API error return { error: { type: 'api_error', message: error.message || 'Unknown API error', details: { suggestion: 'Check your request parameters and try again', fallback_model: context?.model ? suggestFallbackModel(context.model as any) : 'sonar-reasoning-pro', }, }, }; } // Network or unknown error return { error: { type: 'api_error', message: error instanceof Error ? error.message : 'Unknown error occurred', details: { suggestion: 'Check your internet connection and try again', }, }, }; } } // Export alias for backward compatibility export { PerplexityApiClient as PerplexityAPI };

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/cfdude/mcp-perplexity-pro'

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