"""Anthropic Claude Provider Direct integration with Anthropic's Claude API. Supports Claude 3.5 Sonnet, Claude 3 Opus, and other models. """ import json import os # Import base classes from parent module import sys from dataclasses import dataclass import requests sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from clients.llm_client import BaseLLMProvider, LLMResponse, ToolCall class AnthropicProvider(BaseLLMProvider): """Anthropic Claude API provider. Provides direct integration with Anthropic's Claude models without going through OpenRouter. Supports: - Claude 3.5 Sonnet (claude-3-5-sonnet-20241022) - Claude 3 Opus (claude-3-opus-20240229) - Claude 3 Sonnet (claude-3-sonnet-20240229) - Claude 3 Haiku (claude-3-haiku-20240307) """ API_URL = "https://api.anthropic.com/v1/messages" API_VERSION = "2023-06-01" def __init__( self, api_key: str | None = None, model: str = "claude-3-5-sonnet-20241022", temperature: float = 0, max_tokens: int = 4096, ): """Initialize the Anthropic provider. Args: api_key: Anthropic API key. Defaults to ANTHROPIC_API_KEY env var. model: Model to use. Defaults to Claude 3.5 Sonnet. temperature: Sampling temperature (0-1). max_tokens: Maximum tokens in response. """ self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY", "") self.model = model self.temperature = temperature self.max_tokens = max_tokens def call(self, prompt: str, **kwargs) -> LLMResponse: """Make a call to the Anthropic API. Args: prompt: The prompt to send. **kwargs: Additional options (model, temperature, max_tokens). Returns: LLMResponse with the generated content. Raises: ValueError: If API key is not set. requests.HTTPError: If the API request fails. """ if not self.api_key: raise ValueError("Anthropic API key is required") response = requests.post( self.API_URL, headers={ "x-api-key": self.api_key, "anthropic-version": self.API_VERSION, "Content-Type": "application/json", }, json={ "model": kwargs.get("model", self.model), "max_tokens": kwargs.get("max_tokens", self.max_tokens), "temperature": kwargs.get("temperature", self.temperature), "messages": [{"role": "user", "content": prompt}], }, timeout=120, ) response.raise_for_status() data = response.json() # Extract content from response content = "" for block in data.get("content", []): if block.get("type") == "text": content += block.get("text", "") return LLMResponse( content=content, model=data.get("model", self.model), provider="anthropic", tokens_used=data.get("usage", {}).get("input_tokens", 0) + data.get("usage", {}).get("output_tokens", 0), finish_reason=data.get("stop_reason"), ) def call_with_tools( self, messages: list[dict], tools: list[dict] | None = None, **kwargs, ) -> LLMResponse: """Make a call to the Anthropic API with tool support. Args: messages: List of message dicts with 'role' and 'content'. tools: List of tool definitions in OpenAI format. **kwargs: Additional options. Returns: LLMResponse with content and/or tool_calls. """ if not self.api_key: raise ValueError("Anthropic API key is required") # Convert OpenAI-style messages to Anthropic format anthropic_messages = [] system_content = None for msg in messages: role = msg.get("role", "user") if role == "system": system_content = msg.get("content", "") elif role == "assistant": # Handle assistant messages with tool calls if msg.get("tool_calls"): content = [] if msg.get("content"): content.append({"type": "text", "text": msg["content"]}) for tc in msg["tool_calls"]: content.append( { "type": "tool_use", "id": tc["id"], "name": tc["function"]["name"], "input": json.loads(tc["function"]["arguments"]) if isinstance(tc["function"]["arguments"], str) else tc["function"]["arguments"], } ) anthropic_messages.append({"role": "assistant", "content": content}) else: anthropic_messages.append( { "role": "assistant", "content": msg.get("content", ""), } ) elif role == "tool": # Tool response anthropic_messages.append( { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": msg.get("tool_call_id", ""), "content": msg.get("content", ""), } ], } ) else: anthropic_messages.append( { "role": "user", "content": msg.get("content", ""), } ) # Convert OpenAI-style tools to Anthropic format anthropic_tools = None if tools: anthropic_tools = [] for tool in tools: if tool.get("type") == "function": func = tool["function"] anthropic_tools.append( { "name": func["name"], "description": func.get("description", ""), "input_schema": func.get("parameters", {}), } ) request_body = { "model": kwargs.get("model", self.model), "max_tokens": kwargs.get("max_tokens", self.max_tokens), "temperature": kwargs.get("temperature", self.temperature), "messages": anthropic_messages, } if system_content: request_body["system"] = system_content if anthropic_tools: request_body["tools"] = anthropic_tools response = requests.post( self.API_URL, headers={ "x-api-key": self.api_key, "anthropic-version": self.API_VERSION, "Content-Type": "application/json", }, json=request_body, timeout=120, ) response.raise_for_status() data = response.json() # Parse response content = "" tool_calls = None for block in data.get("content", []): if block.get("type") == "text": content += block.get("text", "") elif block.get("type") == "tool_use": if tool_calls is None: tool_calls = [] tool_calls.append( ToolCall( id=block.get("id", ""), name=block.get("name", ""), arguments=block.get("input", {}), ) ) return LLMResponse( content=content, model=data.get("model", self.model), provider="anthropic", tokens_used=data.get("usage", {}).get("input_tokens", 0) + data.get("usage", {}).get("output_tokens", 0), finish_reason=data.get("stop_reason"), tool_calls=tool_calls, )