UNPKG

openrouter-client

Version:

An API wrapper for OpenRouter

230 lines (227 loc) 7.43 kB
// src/index.ts import { EventEmitter } from "events"; // src/types.ts var types_exports = {}; // src/index.ts var errorCodesAndMesssages = { 400: "Bad Request (invalid or missing params, CORS)", 401: "Invalid credentials (OAuth session expired, disabled/invalid API key)", 402: "Your account or API key has insufficient credits. Add more credits and retry the request.", 408: "Your request timed out", 429: "You are being rate limited", 502: "Your chosen model is down or we received an invalid response from it", 503: "There is no available model provider that meets your routing requirements" }; var OpenRouter = class { apiKey; globalConfig; constructor(apiKey, globalConfig) { this.apiKey = apiKey; this.globalConfig = globalConfig || {}; } async chat(messages, config, signal) { config = config || this.globalConfig; const extraHeaders = {}; if (config.httpReferer) { extraHeaders["HTTP-Referer"] = config.httpReferer; } if (config.xTitle) { extraHeaders["X-Title"] = config.xTitle; } let request; try { request = await fetch( "https://openrouter.ai/api/v1/chat/completions", { method: "POST", signal, headers: { ...extraHeaders, Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ messages, ...config }) } ); } catch (e) { if (typeof e == "object" && e !== null) { if ("name" in e) { if (e.name == "AbortError") { return { success: false, error: "AbortError" }; } } } return { success: false, error: e }; } const response = await request.json(); if ("error" in response) { return { success: false, errorCode: response.error.status, errorMessage: response.error.message, metadata: response.error.metadata }; } return { success: true, data: response }; } async getGenerationStats(id) { const request = await fetch( `https://openrouter.ai/api/v1/generation?id=${id}` ); return await request.json(); } }; var OpenRouterStream = class extends EventEmitter { apiKey; globalConfig; constructor(apiKey, globalConfig) { super(); this.apiKey = apiKey; this.globalConfig = globalConfig || {}; } /** Sends back chunks. First message sent back might be "hello" and the second chunk might be " world" */ async chatStreamChunk(messages, config) { config = config || this.globalConfig; const extraHeaders = {}; if (config.httpReferer) { extraHeaders["HTTP-Referer"] = config.httpReferer; } if (config.xTitle) { extraHeaders["X-Title"] = config.xTitle; } const request = await fetch( "https://openrouter.ai/api/v1/chat/completions", { method: "POST", headers: { ...extraHeaders, Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ messages, ...config, stream: true }) } ); if (!request.ok || request.body === null) { const errorText = await request.text(); this.emit("error", new Error(`HTTP error ${request.status}: ${errorText}`)); return; } const reader = request.body.getReader(); const decoder = new TextDecoder("utf-8"); let buffer = ""; while (true) { const { done, value } = await reader.read(); buffer += decoder.decode(value, { stream: true }); const splitData = buffer.split("\n").filter((item) => item !== ""); for (let i = 0; i < splitData.length; i++) { const data = splitData[i]; if (data === ": OPENROUTER PROCESSING") { buffer = buffer.slice(data.length + 1); continue; } if (data.includes("[DONE]")) { this.emit("end"); return; } let parsedData; try { parsedData = JSON.parse(data.split("data: ")[1]); buffer = buffer.slice(data.length + 1); } catch (e) { continue; } this.emit("data", parsedData); } } } //** This function passes back the entire object. So the first message might be { content: "hello" } and the second message might be { content: "hello world" }. This differs from `chatStreamChunk`, which only sends the new token that was generated instead of the whole object. */ async chatStreamWhole(messages, config) { config = config || this.globalConfig; const extraHeaders = {}; if (config.httpReferer) { extraHeaders["HTTP-Referer"] = config.httpReferer; } if (config.xTitle) { extraHeaders["X-Title"] = config.xTitle; } const request = await fetch( "https://openrouter.ai/api/v1/chat/completions", { method: "POST", headers: { ...extraHeaders, Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ messages, ...config, stream: true }) } ); if (!request.ok || request.body === null) { const errorText = await request.text(); this.emit("error", new Error(`HTTP error ${request.status}: ${errorText}`)); return; } const reader = request.body.getReader(); const decoder = new TextDecoder("utf-8"); let whole = { id: "", provider: "", model: "", object: "", created: 0, finish_reason: "", //@ts-ignore choices: messages }; whole.choices.push({ role: "user", content: "", reasoning: null }); let buffer = ""; while (true) { const { done, value } = await reader.read(); buffer += decoder.decode(value, { stream: true }); const splitData = buffer.split("\n").filter((item) => item !== ""); for (let i = 0; i < splitData.length; i++) { const data = splitData[i]; if (data === ": OPENROUTER PROCESSING") { buffer = buffer.slice(data.length + 1); continue; } if (data.includes("[DONE]")) { this.emit("end"); return; } let parsedData; try { parsedData = JSON.parse(data.split("data: ")[1]); buffer = buffer.slice(data.length + 1); } catch (e) { continue; } if ("usage" in parsedData) { whole.usage = parsedData.usage; } else { whole.id = parsedData.id; whole.provider = parsedData.provider; whole.model = parsedData.model; whole.object = parsedData.object; whole.created = parsedData.created; whole.finish_reason = parsedData.choices[0].finish_reason; } whole.choices.at(-1).role = parsedData.choices[0].delta.role; whole.choices.at(-1).content += parsedData.choices[0].delta.content; if (whole.choices.at(-1).reasoning == null && parsedData.choices[0].delta.reasoning) { whole.choices.at(-1).reasoning = ""; } if (parsedData.choices[0].delta.reasoning) { whole.choices.at(-1).reasoning += parsedData.choices[0].delta.reasoning; } this.emit("data", whole); } } } }; export { OpenRouter, OpenRouterStream, errorCodesAndMesssages, types_exports as types };