UNPKG

jorel

Version:

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

424 lines (423 loc) 18.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AnthropicProvider = void 0; const bedrock_sdk_1 = require("@anthropic-ai/bedrock-sdk"); const sdk_1 = require("@anthropic-ai/sdk"); const providers_1 = require("../../providers"); const shared_1 = require("../../shared"); const tools_1 = require("../../tools"); const convert_llm_message_1 = require("./convert-llm-message"); /** Provides access to OpenAI and other compatible services */ class AnthropicProvider { constructor({ apiKey, bedrock, name, maxRetries, timeout } = {}) { this.name = name || AnthropicProvider.defaultName; if (bedrock) { const region = bedrock.awsRegion || process.env.AWS_REGION; const accessKeyId = bedrock.awsAccessKey || process.env.AWS_ACCESS_KEY_ID; const secretAccessKey = bedrock.awsSecretKey || process.env.AWS_SECRET_ACCESS_KEY; if (!region) throw new Error("[AnthropicProvider]: Missing AWS region. Either pass it as config.region or set the AWS_REGION environment variable"); if (!accessKeyId) throw new Error("[AnthropicProvider]: Missing AWS access key id. Either pass it as config.accessKeyId or set the AWS_ACCESS_KEY_ID environment variable"); if (!secretAccessKey) throw new Error("[AnthropicProvider]: Missing AWS secret access key. Either pass it as config.secretAccessKey or set the AWS_SECRET_ACCESS_KEY environment variable"); this.client = new bedrock_sdk_1.AnthropicBedrock({ awsRegion: region, awsAccessKey: accessKeyId, awsSecretKey: secretAccessKey, maxRetries, timeout, }); } else { const _apiKey = apiKey || process.env.ANTHROPIC_API_KEY; if (!_apiKey) throw new Error("[AnthropicProvider]: Missing API key. Either pass it as config.apiKey or set the ANTHROPIC_API_KEY environment variable"); this.client = new sdk_1.default({ apiKey: _apiKey, maxRetries, timeout, }); } } // Helper method for parsing Anthropic API errors parseAnthropicError(error) { let errorMessage; let errorType = "unknown"; // Extract error information from Anthropic SDK errors if (error && typeof error === "object") { const err = error; // Anthropic SDK errors have status property const status = err.status || (error instanceof sdk_1.APIError ? error.status : undefined); errorMessage = err.message || (error instanceof Error ? error.message : String(error)); // Parse error messages that start with HTTP status codes followed by JSON // e.g., "404 {\"type\":\"error\",\"error\":{\"type\":\"not_found_error\",\"message\":\"model: claude-haiku-4-6\"}}" const statusCodeMatch = errorMessage.match(/^(\d{3})\s+(.+)$/); if (statusCodeMatch) { const statusCode = parseInt(statusCodeMatch[1], 10); const jsonPart = statusCodeMatch[2]; // Try to parse the JSON part try { const parsedError = JSON.parse(jsonPart); if (parsedError.error?.message) { errorMessage = parsedError.error.message; } else if (parsedError.message) { errorMessage = parsedError.message; } else { errorMessage = jsonPart; } } catch { // If JSON parsing fails, use the status code to set error type and clean message errorMessage = jsonPart; } // Override status if we parsed it from the message if (!status && statusCode) { // Use the parsed status code } } // Map status codes to error types (use parsed status or SDK status) const finalStatus = status || (statusCodeMatch ? parseInt(statusCodeMatch[1], 10) : undefined); if (finalStatus === 400) { errorType = "invalid_request"; } else if (finalStatus === 401) { errorType = "authentication_error"; } else if (finalStatus === 403) { errorType = "authentication_error"; } else if (finalStatus === 404) { errorType = "invalid_request"; } else if (finalStatus === 429) { errorType = "rate_limit"; } else if (finalStatus === 500 || finalStatus === 502 || finalStatus === 503) { 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 { chatMessages, systemMessage } = await (0, convert_llm_message_1.convertLlmMessagesToAnthropicMessages)(messages); const temperature = config.temperature ?? undefined; let response; try { response = await this.client.messages.create({ model, messages: chatMessages, temperature, max_tokens: config.maxTokens || 4096, system: systemMessage, thinking: config.reasoningEffort === "minimal" ? { type: "disabled" } : undefined, tool_choice: config.toolChoice === "none" || !config.tools || !config.tools.hasTools ? undefined : config.toolChoice === "any" ? { type: "auto", disable_parallel_tool_use: config.tools?.allowParallelCalls, } : config.toolChoice === "required" ? { type: "auto", disable_parallel_tool_use: config.tools?.allowParallelCalls, } : config.toolChoice ? { type: "tool", name: config.toolChoice, disable_parallel_tool_use: config.tools?.allowParallelCalls, } : undefined, tools: config.toolChoice === "none" ? undefined : config.tools?.asLlmFunctions?.map((tool) => ({ name: tool.function.name, input_schema: { ...tool.function.parameters?.properties, type: "object", }, description: tool.function.description, })), }, { 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.parseAnthropicError(error); throw new shared_1.JorElLlmError(`[AnthropicProvider] Error generating content: ${message}`, type); } const durationMs = Date.now() - start; const inputTokens = response.usage.input_tokens; const outputTokens = response.usage.output_tokens; const reasoningTokens = undefined; const content = response.content .map((c) => (c.type === "text" ? c.text : "")) .join("") .trim(); const reasoningContent = response.content .filter((c) => c.type === "thinking" || c.type === "redacted_thinking") .map((c) => (c.type === "thinking" ? c.thinking : c.data)) .join("") .trim(); const toolCalls = response.content .filter((c) => c.type === "tool_use") .map((c) => ({ id: (0, shared_1.generateUniqueId)(), request: { id: c.id, function: { name: c.name, arguments: c.input && typeof c.input === "object" ? c.input : {}, }, }, approvalState: config.tools?.getTool(c.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, reasoningTokens, }, }; } async *generateResponseStream(model, messages, config = {}) { const start = Date.now(); const { chatMessages, systemMessage } = await (0, convert_llm_message_1.convertLlmMessagesToAnthropicMessages)(messages); const temperature = config.temperature ?? undefined; let responseStream; try { responseStream = await this.client.messages.create({ model, messages: chatMessages, temperature, max_tokens: config.maxTokens || 4096, system: systemMessage, stream: true, thinking: config.reasoningEffort === "minimal" ? { type: "disabled" } : undefined, tool_choice: config.toolChoice === "none" || !config.tools || !config.tools.hasTools ? undefined : config.toolChoice === "any" ? { type: "auto", disable_parallel_tool_use: config.tools?.allowParallelCalls, } : config.toolChoice === "required" ? { type: "auto", disable_parallel_tool_use: config.tools?.allowParallelCalls, } : config.toolChoice ? { type: "tool", name: config.toolChoice, disable_parallel_tool_use: config.tools?.allowParallelCalls, } : undefined, tools: config.toolChoice === "none" ? undefined : config.tools?.asLlmFunctions?.map((tool) => ({ name: tool.function.name, input_schema: { ...tool.function.parameters?.properties, type: "object", }, description: tool.function.description, })), }, { signal: config.abortSignal, }); } catch (error) { const isAbort = error instanceof Error && (error.message.toLowerCase().includes("aborted") || error.name === "AbortError"); const stopReason = isAbort ? "userCancelled" : "generationError"; yield { type: "response", role: "assistant", content: "", reasoningContent: null, meta: { model, provider: this.name, temperature, durationMs: 0, inputTokens: 0, outputTokens: 0, reasoningTokens: undefined, }, stopReason, error: stopReason === "generationError" ? this.parseAnthropicError(error) : undefined, }; return; } let inputTokens = undefined; let outputTokens = undefined; const reasoningTokens = undefined; let content = ""; let reasoningContent = ""; let error; const _toolCalls = {}; const provider = this.name; try { for await (const chunk of responseStream) { if (chunk.type === "content_block_delta") { if (chunk.delta.type === "text_delta") { content += chunk.delta.text; const chunkId = (0, shared_1.generateUniqueId)(); yield { type: "chunk", content: chunk.delta.text, chunkId }; } if (chunk.delta.type === "thinking_delta") { reasoningContent += chunk.delta.thinking; const chunkId = (0, shared_1.generateUniqueId)(); yield { type: "reasoningChunk", content: chunk.delta.thinking, chunkId }; } } if (chunk.type === "message_start") { inputTokens = (inputTokens || 0) + chunk.message.usage.input_tokens; outputTokens = (outputTokens || 0) + chunk.message.usage.output_tokens; } if (chunk.type === "content_block_start") { if (chunk.content_block.type === "tool_use") { _toolCalls[chunk.index] = { ...chunk.content_block, arguments: "" }; } } if (chunk.type === "content_block_delta") { if (chunk.delta.type === "input_json_delta") { const index = chunk.index; const toolCall = _toolCalls[index]; toolCall.arguments += chunk.delta.partial_json; } } if (chunk.type === "message_delta") { outputTokens = (outputTokens || 0) + chunk.usage.output_tokens; } } } catch (e) { error = this.parseAnthropicError(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("AnthropicProvider", `Stream error: ${error.message}`); } const meta = { model, provider, temperature, durationMs, inputTokens, outputTokens, reasoningTokens, }; const toolCalls = Object.values(_toolCalls).map((c) => { let parsedArgs = null; let parseError = null; try { parsedArgs = tools_1.LlmToolKit.deserialize(c.arguments); } catch (e) { parseError = e instanceof Error ? e : new Error("Unable to parse tool call arguments"); } const approvalState = config.tools?.getTool(c.name)?.requiresConfirmation ? "requiresApproval" : "noApprovalRequired"; const base = { id: (0, shared_1.generateUniqueId)(), request: { id: c.id, function: { name: c.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, }; }); if (toolCalls && 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.get("/v1/models")); return response.data.map((model) => model.id); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async createEmbedding(model, text, abortSignal) { throw new Error("Embeddings are not yet supported for Anthropic"); } } exports.AnthropicProvider = AnthropicProvider; AnthropicProvider.defaultName = "anthropic";