UNPKG

jorel

Version:

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

394 lines (393 loc) 17 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MistralProvider = void 0; const mistralai_1 = require("@mistralai/mistralai"); 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 OpenAI and other compatible services */ class MistralProvider { constructor({ apiKey, retryConfig, timeout } = {}) { this.name = MistralProvider.defaultName; this.client = new mistralai_1.Mistral({ apiKey: apiKey ?? process.env.MISTRAL_API_KEY, retryConfig, timeoutMs: timeout, }); } // Helper method for parsing Mistral API errors parseMistralError(error) { let errorMessage; let errorType = "unknown"; // Extract error information from Mistral SDK errors if (error && typeof error === "object") { const err = error; // Mistral SDK errors have a specific format: "API error occurred: Status XXX\nBody: {...}" errorMessage = err.message || (error instanceof Error ? error.message : String(error)); // Parse Mistral-specific error format const statusMatch = errorMessage.match(/API error occurred: Status (\d+)/); const bodyMatch = errorMessage.match(/Body: (\{.*\})$/); if (statusMatch && bodyMatch) { const statusCode = parseInt(statusMatch[1], 10); const jsonBody = bodyMatch[1]; // Try to parse the JSON body try { const parsedError = JSON.parse(jsonBody); if (parsedError.message) { errorMessage = parsedError.message; } // Map status codes to error types if (statusCode === 400) { errorType = "invalid_request"; } else if (statusCode === 401) { errorType = "authentication_error"; } else if (statusCode === 403) { errorType = "authentication_error"; } else if (statusCode === 404) { errorType = "invalid_request"; } else if (statusCode === 429) { // 429 can mean either rate limit or quota exceeded const lowerMessage = errorMessage.toLowerCase(); if (lowerMessage.includes("quota") || lowerMessage.includes("capacity")) { errorType = "quota_exceeded"; } else { errorType = "rate_limit"; } } else if (statusCode >= 500) { errorType = "server_error"; } } catch { // If JSON parsing fails, use the status code to set error type if (statusCode === 400) { errorType = "invalid_request"; } else if (statusCode === 401) { errorType = "authentication_error"; } else if (statusCode === 403) { errorType = "authentication_error"; } else if (statusCode === 404) { errorType = "invalid_request"; } 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; let response; try { response = await this.client.chat.complete({ model, messages: await (0, convert_llm_message_1.convertLlmMessagesToMistralMessages)(messages), temperature, responseFormat: (0, convert_inputs_1.jsonResponseToMistral)(config.json), maxTokens: config.maxTokens, toolChoice: (0, convert_inputs_1.toolChoiceToMistral)(config.toolChoice), tools: config.tools?.asLlmFunctions?.map((f) => ({ type: "function", function: { name: f.function.name, description: f.function.description, parameters: { type: f.function.parameters?.type ?? "object", properties: f.function.parameters?.properties ?? {}, required: f.function.parameters?.required ?? [], }, }, })), }, config.abortSignal ? { fetchOptions: { signal: config.abortSignal } } : undefined); } catch (error) { if (error.name === "AbortError" || (error.message && error.message.toLowerCase().includes("aborted"))) { throw new shared_1.JorElAbortError("Request was aborted"); } const { message, type } = this.parseMistralError(error); throw new shared_1.JorElLlmError(`[MistralProvider] Error generating content: ${message}`, type); } const durationMs = Date.now() - start; const inputTokens = response.usage?.promptTokens; const outputTokens = response.usage?.completionTokens; const message = response.choices ? (0, shared_1.firstEntry)(response.choices)?.message : undefined; const content = Array.isArray(message?.content) ? message.content.map((c) => (c.type === "text" ? c.text : "")).join("") : (message?.content ?? null); const reasoningContent = Array.isArray(message?.content) ? message.content.map((c) => (c.type === "thinking" ? c.thinking : "")).join("") : null; const toolCalls = message?.toolCalls?.map((call) => { return { id: (0, shared_1.generateUniqueId)(), request: { id: call.id ?? (0, shared_1.generateUniqueId)(), function: { name: call.function.name, arguments: typeof call.function.arguments == "string" ? tools_1.LlmToolKit.deserialize(call.function.arguments) : call.function.arguments, }, }, approvalState: config.tools?.getTool(call.function.name)?.requiresConfirmation ? "requiresApproval" : "noApprovalRequired", executionState: "pending", result: null, error: null, }; }); const provider = this.name; return { ...(0, providers_1.generateAssistantMessage)(content, reasoningContent, toolCalls), meta: { model, provider, temperature, durationMs, inputTokens, outputTokens, }, }; } async *generateResponseStream(model, messages, config = {}) { const start = Date.now(); const temperature = config.temperature ?? undefined; let response; try { response = await this.client.chat.stream({ model, messages: await (0, convert_llm_message_1.convertLlmMessagesToMistralMessages)(messages), temperature, responseFormat: (0, convert_inputs_1.jsonResponseToMistral)(config.json), maxTokens: config.maxTokens, stream: true, tools: config.tools?.asLlmFunctions?.map((f) => ({ type: "function", function: { name: f.function.name, description: f.function.description, parameters: { type: f.function.parameters?.type ?? "object", properties: f.function.parameters?.properties ?? {}, required: f.function.parameters?.required ?? [], }, }, })), toolChoice: (0, convert_inputs_1.toolChoiceToMistral)(config.toolChoice), }, config.abortSignal ? { fetchOptions: { signal: config.abortSignal } } : undefined); } catch (error) { const isAbort = error?.name === "AbortError" || (error?.message && typeof error.message === "string" && error.message.toLowerCase().includes("aborted")); const stopReason = isAbort ? "userCancelled" : "generationError"; yield { type: "response", role: "assistant", content: "", reasoningContent: "", meta: { model, provider: this.name, temperature, durationMs: 0, inputTokens: undefined, outputTokens: undefined, }, stopReason, error: stopReason === "generationError" ? this.parseMistralError(error) : undefined, }; return; } let inputTokens; let outputTokens; const _toolCalls = []; let content = ""; let reasoningContent = ""; let error; const provider = this.name; try { for await (const chunk of response) { const delta = (0, shared_1.firstEntry)(chunk.data.choices)?.delta; if (delta?.content) { const contentChunk = Array.isArray(delta.content) ? delta.content.map((c) => (c.type === "text" ? c.text : "")).join("") : delta.content; const reasoningChunk = Array.isArray(delta.content) ? delta.content.map((c) => (c.type === "thinking" ? c.thinking : "")).join("") : null; if (contentChunk) { content += contentChunk; const chunkId = (0, shared_1.generateUniqueId)(); yield { type: "chunk", content: contentChunk, chunkId, }; } if (reasoningChunk) { reasoningContent += reasoningChunk; const chunkId = (0, shared_1.generateUniqueId)(); yield { type: "reasoningChunk", content: reasoningChunk, chunkId, }; } } if (delta?.toolCalls) { for (const toolCall of delta.toolCalls) { if (toolCall.index !== undefined) { 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.data.usage) { inputTokens = chunk.data.usage?.promptTokens; outputTokens = chunk.data.usage?.completionTokens; } } } catch (e) { error = this.parseMistralError(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("MistralProvider", `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, outputTokens, }; 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 models = await this.client.models.list(); return models.data?.map((model) => model.id) ?? []; } async createEmbedding(model, text, abortSignal) { const response = await this.client.embeddings.create({ model, inputs: text, }, abortSignal ? { fetchOptions: { signal: abortSignal } } : undefined); if (!response || !response.data || !response.data || response.data.length === 0 || !response.data[0].embedding) { throw new Error("Failed to create embedding"); } return response.data[0].embedding; } } exports.MistralProvider = MistralProvider; MistralProvider.defaultName = "mistral";