Files
ProxMenux/AppImage/scripts/ai_providers/anthropic_provider.py
T

105 lines
4.0 KiB
Python

"""Anthropic (Claude) provider implementation.
Anthropic's Claude models are excellent for text generation and translation.
Models are addressed by their pinned IDs (the "-latest" aliases were retired).
"""
from typing import Optional, List
from .base import AIProvider, AIProviderError
class AnthropicProvider(AIProvider):
"""Anthropic provider using their Messages API."""
NAME = "anthropic"
REQUIRES_API_KEY = True
API_URL = "https://api.anthropic.com/v1/messages"
API_VERSION = "2023-06-01"
# Current Claude API model IDs (per docs at
# platform.claude.com/docs/en/docs/about-claude/models). Kept as a
# runtime fallback for `/api/notifications/provider-models` when
# `verified_ai_models.json` has no Anthropic entry; the JSON is the
# authoritative UI-facing list. Refreshed 2026-10-26 against the live
# Models API. Ordered cheapest → most capable so the first entry is a
# safe default.
KNOWN_MODELS = [
"claude-haiku-5-5",
"claude-haiku-4-5-20251001",
"claude-sonnet-5-5",
"claude-sonnet-5",
"claude-sonnet-4-6",
"claude-opus-5-5",
"claude-opus-4-8",
"claude-opus-4-7",
"claude-opus-4-6",
"claude-opus-4-5-20251101",
"claude-fable-5",
"claude-fable-5-1",
]
# Newer models (Opus 5.x, Fable) may think before answering and the
# thinking counts against max_tokens, so a small cap can end the turn
# with no text at all. max_tokens is a ceiling, not a cost.
MIN_MAX_TOKENS = 1024
def list_models(self) -> List[str]:
"""Return known Anthropic model IDs."""
return self.KNOWN_MODELS
def generate(self, system_prompt: str, user_message: str,
max_tokens: int = 200) -> Optional[str]:
"""Generate a response using Anthropic's API.
Note: Anthropic uses a different API format than OpenAI.
The system prompt goes in a separate field, not in messages.
Args:
system_prompt: System instructions
user_message: User message to process
max_tokens: Maximum response length
Returns:
Generated text or None if failed
Raises:
AIProviderError: If API key is missing or request fails
"""
if not self.api_key:
raise AIProviderError("API key required for Anthropic")
# Anthropic uses a different format - system is a top-level field
payload = {
'model': self.model,
'system': system_prompt,
'messages': [
{'role': 'user', 'content': user_message},
],
'max_tokens': max(max_tokens, self.MIN_MAX_TOKENS),
}
headers = {
'Content-Type': 'application/json',
'x-api-key': self.api_key,
'anthropic-version': self.API_VERSION,
}
result = self._make_request(self.API_URL, payload, headers)
# A safety classifier can decline even benign notifications
# (HTTP 200, stop_reason "refusal", empty content). Raising lets the
# caller fall back to the original, untranslated notification.
if result.get('stop_reason') == 'refusal':
category = (result.get('stop_details') or {}).get('category')
raise AIProviderError(f"Request declined by Anthropic (category: {category})")
# Content is a list of blocks; thinking blocks may come before the
# text, so take the first text block rather than content[0].
content = result.get('content')
if not isinstance(content, list):
raise AIProviderError("Unexpected response format: missing content")
for block in content:
if isinstance(block, dict) and block.get('type') == 'text':
return block.get('text', '').strip()
raise AIProviderError(
f"No text in response (stop_reason: {result.get('stop_reason')})")