Skip to main content
Glama
IBM

IBM i MCP Server

Official
by IBM
openRouterProvider.ts11 kB
/** * @fileoverview Provides a service class (`OpenRouterProvider`) for interacting with the * OpenRouter API. This file implements the "handler" pattern internally, where the * OpenRouterProvider class manages state and error handling, while private logic functions * execute the core API interactions and throw structured errors. * @module src/services/llm-providers/openRouterProvider */ import OpenAI from "openai"; import { ChatCompletion, ChatCompletionChunk, ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming, } from "openai/resources/chat/completions"; import { Stream } from "openai/streaming"; import { config } from "@/config/index.js"; import { JsonRpcErrorCode, McpError } from "@/types-global/errors.js"; import { ErrorHandler } from "@/utils/internal/errorHandler.js"; import { logger } from "@/utils/internal/logger.js"; import { RequestContext, requestContextService, } from "@/utils/internal/requestContext.js"; import { rateLimiter } from "@/utils/security/rateLimiter.js"; import { sanitization } from "@/utils/security/sanitization.js"; // Note: OpenRouter recommends setting HTTP-Referer (e.g., config.openrouterAppUrl) // and X-Title (e.g., config.openrouterAppName) headers. /** * Options for configuring the OpenRouter client. */ export interface OpenRouterClientOptions { apiKey?: string; baseURL?: string; siteUrl?: string; siteName?: string; } /** * Defines the parameters for an OpenRouter chat completion request. */ export type OpenRouterChatParams = ( | ChatCompletionCreateParamsNonStreaming | ChatCompletionCreateParamsStreaming ) & { top_k?: number; min_p?: number; transforms?: string[]; models?: string[]; route?: "fallback"; provider?: Record<string, unknown>; }; // #region Internal Logic Functions (Throwing Errors) /** * Prepares parameters for the OpenRouter API call, separating standard * and extra parameters and applying defaults. * @internal */ function _prepareApiParameters(params: OpenRouterChatParams) { const effectiveModelId = params.model || config.llmDefaultModel; const standardParams: Partial< ChatCompletionCreateParamsStreaming | ChatCompletionCreateParamsNonStreaming > = { model: effectiveModelId, messages: params.messages, ...(params.temperature !== undefined || config.llmDefaultTemperature !== undefined ? { temperature: params.temperature ?? config.llmDefaultTemperature } : {}), ...(params.top_p !== undefined || config.llmDefaultTopP !== undefined ? { top_p: params.top_p ?? config.llmDefaultTopP } : {}), ...(params.presence_penalty !== undefined ? { presence_penalty: params.presence_penalty } : {}), ...(params.stream !== undefined && { stream: params.stream }), ...(params.tools !== undefined && { tools: params.tools }), ...(params.tool_choice !== undefined && { tool_choice: params.tool_choice, }), ...(params.response_format !== undefined && { response_format: params.response_format, }), ...(params.stop !== undefined && { stop: params.stop }), ...(params.seed !== undefined && { seed: params.seed }), ...(params.frequency_penalty !== undefined ? { frequency_penalty: params.frequency_penalty } : {}), ...(params.logit_bias !== undefined && { logit_bias: params.logit_bias }), }; const extraBody: Record<string, unknown> = {}; const standardKeys = new Set(Object.keys(standardParams)); standardKeys.add("messages"); for (const key in params) { if ( Object.prototype.hasOwnProperty.call(params, key) && !standardKeys.has(key) && key !== "max_tokens" ) { extraBody[key] = (params as unknown as Record<string, unknown>)[key]; } } if (extraBody.top_k === undefined && config.llmDefaultTopK !== undefined) { extraBody.top_k = config.llmDefaultTopK; } if (extraBody.min_p === undefined && config.llmDefaultMinP !== undefined) { extraBody.min_p = config.llmDefaultMinP; } if ( extraBody.provider && typeof extraBody.provider === "object" && extraBody.provider !== null ) { const provider = extraBody.provider as Record<string, unknown>; if (!provider.sort) { provider.sort = "throughput"; } } else if (extraBody.provider === undefined) { extraBody.provider = { sort: "throughput" }; } const modelsRequiringMaxCompletionTokens = ["openai/o1", "openai/gpt-4.1"]; const needsMaxCompletionTokens = modelsRequiringMaxCompletionTokens.some( (modelPrefix) => effectiveModelId.startsWith(modelPrefix), ); const effectiveMaxTokensValue = params.max_tokens ?? config.llmDefaultMaxTokens; if (effectiveMaxTokensValue !== undefined) { if (needsMaxCompletionTokens) { extraBody.max_completion_tokens = effectiveMaxTokensValue; } else { standardParams.max_tokens = effectiveMaxTokensValue; } } return { standardParams, extraBody }; } async function _openRouterChatCompletionLogic( client: OpenAI, params: OpenRouterChatParams, context: RequestContext, ): Promise<ChatCompletion | Stream<ChatCompletionChunk>> { const isStreaming = params.stream === true; const { standardParams, extraBody } = _prepareApiParameters(params); const apiParams = { ...standardParams }; if (Object.keys(extraBody).length > 0) { (apiParams as Record<string, unknown>).extra_body = extraBody; } logger.logInteraction("OpenRouterRequest", { context, request: apiParams, }); try { if (isStreaming) { return await client.chat.completions.create( apiParams as unknown as ChatCompletionCreateParamsStreaming, ); } const response = await client.chat.completions.create( apiParams as unknown as ChatCompletionCreateParamsNonStreaming, ); logger.logInteraction("OpenRouterResponse", { context, response, streaming: false, }); return response; } catch (e: unknown) { const error = e as Error & { status?: number; cause?: unknown }; logger.logInteraction("OpenRouterError", { context, error: { message: error.message, stack: error.stack, status: error.status, cause: error.cause, }, }); const errorDetails = { providerStatus: error.status, providerMessage: error.message, cause: error?.cause, }; if (error.status === 401) { throw new McpError( JsonRpcErrorCode.Unauthorized, `OpenRouter authentication failed: ${error.message}`, errorDetails, ); } else if (error.status === 429) { throw new McpError( JsonRpcErrorCode.RateLimited, `OpenRouter rate limit exceeded: ${error.message}`, errorDetails, ); } else if (error.status === 402) { throw new McpError( JsonRpcErrorCode.Forbidden, `OpenRouter insufficient credits or payment required: ${error.message}`, errorDetails, ); } throw new McpError( JsonRpcErrorCode.InternalError, `OpenRouter API error (${error.status || "unknown status"}): ${ error.message }`, errorDetails, ); } } class OpenRouterProvider { private client?: OpenAI; public status: "unconfigured" | "initializing" | "ready" | "error"; private initializationError: Error | null = null; constructor() { this.status = "unconfigured"; } public initialize(options?: OpenRouterClientOptions): void { const opContext = requestContextService.createRequestContext({ operation: "OpenRouterProvider.initialize", }); this.status = "initializing"; const apiKey = options?.apiKey !== undefined ? options.apiKey : config.openrouterApiKey; if (!apiKey) { this.status = "unconfigured"; this.initializationError = new McpError( JsonRpcErrorCode.ConfigurationError, "OpenRouter API key is not configured.", ); logger.error( { ...opContext, error: this.initializationError, }, this.initializationError.message, ); return; } try { this.client = new OpenAI({ baseURL: options?.baseURL || "https://openrouter.ai/api/v1", apiKey, defaultHeaders: { "HTTP-Referer": options?.siteUrl || config.openrouterAppUrl, "X-Title": options?.siteName || config.openrouterAppName, }, maxRetries: 0, }); this.status = "ready"; logger.info(opContext, "OpenRouter Service Initialized and Ready"); } catch (e: unknown) { const error = e as Error; this.status = "error"; this.initializationError = error; logger.error( { ...opContext, error, }, "Failed to initialize OpenRouter client", ); } } private checkReady(operation: string, context: RequestContext): void { if (this.status !== "ready" || !this.client) { const message = `OpenRouter service is not available (status: ${this.status}).`; logger.error( { ...context, status: this.status, }, `[${operation}] ${message}`, ); throw new McpError(JsonRpcErrorCode.ServiceUnavailable, message, { cause: this.initializationError, }); } } public async chatCompletion( params: OpenRouterChatParams, context: RequestContext, ): Promise<ChatCompletion | Stream<ChatCompletionChunk>> { const operation = "OpenRouterProvider.chatCompletion"; const sanitizedParams = sanitization.sanitizeForLogging(params); return await ErrorHandler.tryCatch( async () => { this.checkReady(operation, context); const rateLimitKey = context.requestId || "openrouter_default_key"; rateLimiter.check(rateLimitKey, context); return await _openRouterChatCompletionLogic( this.client!, params, context, ); }, { operation, context, input: sanitizedParams }, ); } public async chatCompletionStream( params: OpenRouterChatParams, context: RequestContext, ): Promise<AsyncIterable<ChatCompletionChunk>> { const streamParams = { ...params, stream: true }; const response = await this.chatCompletion(streamParams, context); const responseStream = response as Stream<ChatCompletionChunk>; async function* loggingStream(): AsyncGenerator<ChatCompletionChunk> { const chunks: ChatCompletionChunk[] = []; try { for await (const chunk of responseStream) { chunks.push(chunk); yield chunk; } } finally { logger.logInteraction("OpenRouterResponse", { context, response: chunks, streaming: true, }); } } return loggingStream(); } } const openRouterProviderInstance = new OpenRouterProvider(); export { openRouterProviderInstance as openRouterProvider, OpenRouterProvider };

Latest Blog Posts

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/IBM/ibmi-mcp-server'

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