UNPKG

jorel

Version:

A unified wrapper for working with LLMs from multiple providers, including streams, images, documents & automatic tool use.

393 lines (392 loc) 16.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OpenRouterProviderNative = void 0; const sdk_1 = require("@openrouter/sdk"); const providers_1 = require("../../providers"); const shared_1 = require("../../shared"); const tools_1 = require("../../tools"); const convert_inputs_1 = require("./convert-inputs"); const convert_llm_message_1 = require("./convert-llm-message"); /** * Provides access to OpenRouter using their native SDK */ class OpenRouterProviderNative { constructor(config) { this.name = config?.name || OpenRouterProviderNative.defaultName; this.client = new sdk_1.OpenRouter({ apiKey: config?.apiKey || process.env.OPEN_ROUTER_API_KEY, }); } // Helper method for parsing OpenRouter API errors parseOpenRouterError(error) { let errorMessage; let errorType = "unknown"; // Extract error information from OpenRouter SDK errors if (error && typeof error === "object") { const err = error; // OpenRouter SDK errors have statusCode and error properties const statusCode = err.statusCode || err.error?.code; errorMessage = err.error?.message || err.message || String(error); // Map status codes to error types if (statusCode === 400) { errorType = "invalid_request"; } else if (statusCode === 401) { errorType = "authentication_error"; } else if (statusCode === 402) { errorType = "quota_exceeded"; } else if (statusCode === 403) { errorType = "moderation_error"; } else if (statusCode === 404) { errorType = "invalid_request"; } else if (statusCode === 408) { errorType = "timeout"; } else if (statusCode === 429) { errorType = "rate_limit"; } else if (statusCode >= 500) { errorType = "server_error"; } // Handle network-related errors if (err.message) { const lowerMessage = err.message.toLowerCase(); if (lowerMessage.includes("network") || lowerMessage.includes("fetch failed") || lowerMessage.includes("econnrefused")) { errorType = "network_error"; } else if (lowerMessage.includes("timeout")) { errorType = "timeout"; } } } else { errorMessage = error instanceof Error ? error.message : String(error); } return { message: errorMessage, type: errorType }; } async generateResponse(model, messages, config = {}) { const start = Date.now(); const temperature = config.temperature ?? undefined; // Convert LlmMessage[] to OpenRouter format const openRouterMessages = await (0, convert_llm_message_1.convertLlmMessagesToOpenRouterMessages)(messages); let response; try { response = await this.client.chat.send({ model, messages: openRouterMessages, temperature, maxTokens: config.maxTokens, responseFormat: (0, convert_inputs_1.jsonResponseToOpenRouter)(config.json, config.jsonDescription), tools: config.tools?.asLlmFunctions, toolChoice: (0, convert_inputs_1.toolChoiceToOpenRouter)(config.toolChoice), reasoning: (0, convert_inputs_1.reasoningToOpenRouter)(config.reasoningEffort ?? undefined, config.reasoningSummaryVerbosity ?? undefined), stream: false, maxCompletionTokens: config.maxCompletionTokens, }, { signal: config.abortSignal, }); } catch (error) { if (error instanceof Error && error.message.toLowerCase().includes("aborted")) { throw new shared_1.JorElAbortError("Request was aborted"); } const { message, type } = this.parseOpenRouterError(error); throw new shared_1.JorElLlmError(`[OpenRouterProvider] Error generating content: ${message}`, type); } const durationMs = Date.now() - start; const inputTokens = response.usage?.promptTokens; const outputTokens = response.usage?.completionTokens; const reasoningTokens = response.usage?.completionTokensDetails?.reasoningTokens ?? undefined; const message = response.choices[0].message; const toolCalls = message.toolCalls?.map((call) => { return { id: (0, shared_1.generateUniqueId)(), request: { id: call.id, function: { name: call.function.name, arguments: tools_1.LlmToolKit.deserialize(call.function.arguments), }, }, approvalState: config.tools?.getTool(call.function.name)?.requiresConfirmation ? "requiresApproval" : "noApprovalRequired", executionState: "pending", result: null, error: null, }; }); const provider = this.name; // Extract text content (handles both string and array formats) const textContent = (0, convert_llm_message_1.extractTextContent)(message.content); const reasoningContent = message.reasoning ?? null; return { ...(0, providers_1.generateAssistantMessage)(textContent, reasoningContent, toolCalls), meta: { model, provider, temperature, durationMs, inputTokens: inputTokens ?? undefined, outputTokens: outputTokens ?? undefined, reasoningTokens: reasoningTokens ?? undefined, }, }; } async *generateResponseStream(model, messages, config = {}) { const start = Date.now(); const temperature = config.temperature ?? undefined; // Convert LlmMessage[] to OpenRouter format const openRouterMessages = await (0, convert_llm_message_1.convertLlmMessagesToOpenRouterMessages)(messages); let stream; try { stream = await this.client.chat.send({ model, messages: openRouterMessages, temperature, maxTokens: config.maxTokens, responseFormat: (0, convert_inputs_1.jsonResponseToOpenRouter)(config.json, config.jsonDescription), tools: config.tools?.asLlmFunctions, toolChoice: (0, convert_inputs_1.toolChoiceToOpenRouter)(config.toolChoice), reasoning: (0, convert_inputs_1.reasoningToOpenRouter)(config.reasoningEffort ?? undefined, config.reasoningSummaryVerbosity ?? undefined), stream: true, maxCompletionTokens: config.maxCompletionTokens, streamOptions: { includeUsage: true, }, }, { signal: config.abortSignal, }); } catch (error) { const isAbort = error instanceof Error && (error.message.toLowerCase().includes("aborted") || error.name === "AbortError"); const stopReason = isAbort ? "userCancelled" : "generationError"; const { message: errorMessage, type: errorType } = stopReason === "generationError" ? this.parseOpenRouterError(error) : { message: "", type: "unknown" }; yield { type: "response", role: "assistant", content: "", reasoningContent: "", meta: { model, provider: this.name, temperature, durationMs: 0, inputTokens: 0, outputTokens: 0, reasoningTokens: 0, }, stopReason, error: stopReason === "generationError" ? { message: errorMessage, type: errorType, } : undefined, }; return; } let inputTokens; let outputTokens; let reasoningTokens; const _toolCalls = []; let content = ""; let reasoningContent = ""; let error; const provider = this.name; try { for await (const chunk of stream) { // Handle mid-stream errors from OpenRouter // https://openrouter.ai/docs/api/reference/errors-and-debugging.md if (chunk.error) { const errorCode = typeof chunk.error.code === "number" ? chunk.error.code : parseInt(String(chunk.error.code), 10); let type = "unknown"; // Map HTTP status codes to error types if (errorCode === 400) { type = "invalid_request"; } else if (errorCode === 401) { type = "authentication_error"; } else if (errorCode === 402) { type = "quota_exceeded"; } else if (errorCode === 403) { type = "moderation_error"; } else if (errorCode === 408) { type = "timeout"; } else if (errorCode === 429) { type = "rate_limit"; } else if (errorCode === 500 || errorCode === 502) { type = "server_error"; } else if (errorCode === 503) { type = "no_available_model"; } error = { message: chunk.error.message || "Unknown error from OpenRouter", type, }; } const delta = (0, shared_1.firstEntry)(chunk.choices)?.delta; if (delta?.content) { content += delta.content; const chunkId = (0, shared_1.generateUniqueId)(); yield { type: "chunk", content: delta.content, chunkId }; } if (delta?.reasoning) { reasoningContent += delta.reasoning; const chunkId = (0, shared_1.generateUniqueId)(); yield { type: "reasoningChunk", content: delta.reasoning, chunkId }; } if (delta?.toolCalls) { for (const toolCall of delta.toolCalls) { const _toolCall = _toolCalls[toolCall.index] || { id: "", function: { name: "", arguments: "" } }; if (toolCall.id) _toolCall.id += toolCall.id; if (toolCall.function) { if (toolCall.function.name) _toolCall.function.name += toolCall.function.name; if (toolCall.function.arguments) _toolCall.function.arguments += toolCall.function.arguments; } _toolCalls[toolCall.index] = _toolCall; } } if (chunk.usage) { inputTokens = (inputTokens || 0) + (chunk.usage?.promptTokens ?? 0); outputTokens = (outputTokens || 0) + (chunk.usage?.completionTokens ?? 0); reasoningTokens = (reasoningTokens || 0) + (chunk.usage?.completionTokensDetails?.reasoningTokens ?? 0); } } } catch (e) { // Only set error if one wasn't already set by chunk.error // This handles unexpected errors during streaming (e.g., network issues, parsing errors) if (!error) { error = this.parseOpenRouterError(e); } } const durationMs = Date.now() - start; // Determine stop reason and error message const stopReason = config.abortSignal?.aborted ? "userCancelled" : error ? "generationError" : "completed"; // Log non-abort errors if (error && stopReason === "generationError") { config.logger?.error("OpenRouterProviderNative", `Stream error: ${error.message}`); } const toolCalls = _toolCalls.map((call) => { let parsedArgs = null; let parseError = null; try { parsedArgs = tools_1.LlmToolKit.deserialize(call.function.arguments); } catch (e) { parseError = e instanceof Error ? e : new Error("Unable to parse tool call arguments"); } const approvalState = config.tools?.getTool(call.function.name) ?.requiresConfirmation ? "requiresApproval" : "noApprovalRequired"; const base = { id: (0, shared_1.generateUniqueId)(), request: { id: call.id, function: { name: call.function.name, arguments: parsedArgs ?? {}, }, }, approvalState, }; if (parseError) { return { ...base, executionState: "error", result: null, error: { type: parseError.name || "ToolArgumentParseError", message: parseError.message || "Invalid tool call arguments", numberOfAttempts: 1, lastAttempt: new Date(), }, }; } return { ...base, executionState: "pending", result: null, error: null, }; }); const meta = { model, provider, temperature, durationMs, inputTokens: inputTokens ?? undefined, outputTokens: outputTokens ?? undefined, reasoningTokens: reasoningTokens ?? undefined, }; if (_toolCalls.length > 0) { yield { type: "response", role: "assistant_with_tools", content, reasoningContent, toolCalls, meta, stopReason, error: stopReason === "generationError" ? error : undefined, }; } else { yield { type: "response", role: "assistant", content, reasoningContent, meta, stopReason, error: stopReason === "generationError" ? error : undefined, }; } } async getAvailableModels() { const response = await this.client.models.list(); return response.data.map((model) => model.id); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async createEmbedding(model, text, abortSignal) { // OpenRouter SDK doesn't currently support abort signals for embeddings const response = await this.client.embeddings.generate({ model, input: text, }); if (typeof response === "string") { throw new Error("Received unexpected string response from embeddings endpoint"); } if (!response || !response.data || response.data.length === 0) { throw new Error("Failed to create embedding"); } const embedding = response.data[0].embedding; if (typeof embedding === "string") { const buffer = Buffer.from(embedding, "base64"); const floatArray = new Float32Array(buffer.buffer); return Array.from(floatArray); } return embedding; } } exports.OpenRouterProviderNative = OpenRouterProviderNative; OpenRouterProviderNative.defaultName = "open-router";