jorel
Version:
A unified wrapper for working with LLMs from multiple providers, including streams, images, documents & automatic tool use.
260 lines (259 loc) • 10.9 kB
JavaScript
;
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,
});
}
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");
}
throw error;
}
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) {
if (error instanceof Error && error.message.toLowerCase().includes("aborted")) {
throw new shared_1.JorElAbortError("Request was aborted");
}
throw error;
}
let inputTokens;
let outputTokens;
let reasoningTokens;
const _toolCalls = [];
let content = "";
let reasoningContent = "";
for await (const chunk of stream) {
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);
}
}
const durationMs = Date.now() - start;
const provider = this.name;
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,
};
}
else {
yield {
type: "response",
role: "assistant",
content,
reasoningContent,
meta,
};
}
}
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";