""" Anthropic Claude provider using httpx """ import httpx import json import logging from typing import Dict, List, Any, Optional from .base import BaseProvider logger = logging.getLogger("picogent.anthropic") # Claude Code tool name mapping (lowercase -> CC canonical) CLAUDE_CODE_TOOLS = { "read": "Read", "write": "Write", "edit": "Edit", "bash": "Bash", "grep": "Grep", "glob": "Glob", } CC_TOOL_REVERSE = {v.lower(): k for k, v in CLAUDE_CODE_TOOLS.items()} class AnthropicProvider(BaseProvider): """Anthropic Claude provider using direct API calls""" def __init__(self, api_key: str, model: str = "claude-sonnet-4-20250514"): super().__init__(api_key, model) self.base_url = "https://api.anthropic.com/v1" self.anthropic_version = "2023-06-01" self.is_oauth = api_key.startswith("sk-ant-oat") async def generate_response( self, messages: List[Dict[str, Any]], system_prompt: str, tools: Optional[List[Dict[str, Any]]] = None, max_tokens: int = 8192 ) -> Dict[str, Any]: """Generate response using Anthropic Messages API""" is_oauth = self.api_key.startswith("sk-ant-oat") if is_oauth: # OAuth tokens: Bearer auth + Claude Code stealth headers headers = { "Content-Type": "application/json", "Accept": "application/json", "Authorization": f"Bearer {self.api_key}", "anthropic-version": self.anthropic_version, "anthropic-beta": "claude-code-20250219,oauth-2025-04-20", "anthropic-dangerous-direct-browser-access": "true", "User-Agent": "claude-cli/2.1.2 (external, cli)", "x-app": "cli", } else: headers = { "Content-Type": "application/json", "x-api-key": self.api_key, "anthropic-version": self.anthropic_version } payload = { "model": self.model, "max_tokens": max_tokens, "system": system_prompt, "messages": messages } # Add tools if provided (rename for OAuth/Claude Code stealth) if tools: if is_oauth: payload["tools"] = [ {**t, "name": CLAUDE_CODE_TOOLS.get(t["name"], t["name"])} for t in tools ] else: payload["tools"] = tools logger.info(f"POST {self.base_url}/messages | model={self.model} | key={self.api_key[:8]}...{self.api_key[-4:]} (len={len(self.api_key)})") logger.debug(f"Headers: x-api-key={self.api_key[:8]}..., anthropic-version={self.anthropic_version}") logger.debug(f"Messages count: {len(messages)}, tools: {len(tools) if tools else 0}") async with httpx.AsyncClient() as client: response = await client.post( f"{self.base_url}/messages", headers=headers, json=payload, timeout=120.0 ) logger.info(f"Response status: {response.status_code}") if response.status_code != 200: error_text = response.text logger.error(f"API error: {error_text}") raise Exception(f"Anthropic API error {response.status_code}: {error_text}") result = response.json() # Parse the response content = result.get("content", []) # Extract text content and tool calls text_parts = [] tool_calls = [] for block in content: if block.get("type") == "text": text_parts.append(block.get("text", "")) elif block.get("type") == "tool_use": name = block.get("name") # Map Claude Code names back to original if self.is_oauth: name = CC_TOOL_REVERSE.get(name.lower(), name) tool_calls.append({ "id": block.get("id"), "name": name, "input": block.get("input", {}) }) response_data = { "content": "\n".join(text_parts) if text_parts else "", "usage": result.get("usage", {}), "model": result.get("model", self.model) } if tool_calls: response_data["tool_calls"] = tool_calls # Also include raw content for session storage response_data["raw_content"] = content return response_data