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
JavaScript
;
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";