feat: Anthropic SDK backend — API key alternative to Claude CLI OAuth
Adds `anthropic_api` model type so users can authenticate with a direct
Anthropic API key instead of (or alongside) the CLI OAuth session.
- model_registry.py: `anthropic_api` type; `save/get/remove_anthropic_api_key()`
mirroring the Google account pattern; `save_cloud_model()` now picks type
based on credential type (cli → claude_cli, api_key → anthropic_api);
`_resolve_model()` merges api_key from the credential entry
- llm_client.py: `_anthropic_api()` backend (AsyncAnthropic SDK); dispatch
and fallback wiring; usage tracking
- routers/local_llm.py: Anthropic API key management routes
(POST /settings/local/anthropic-key, /anthropic-key/{id}/remove);
`anthropic_api` badge and edit-form credential selector
- static/local_llm.html: Anthropic Cloud Provider block now shows API key
management (add/remove); Add Model → Anthropic tab has credential selector
(CLI vs API key)
- requirements.txt: enable anthropic>=0.40.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,15 +33,16 @@ async def cleanup() -> None:
|
||||
|
||||
# Map from registry model type → dispatch function key
|
||||
_TYPE_TO_BACKEND = {
|
||||
"claude_cli": "claude",
|
||||
"gemini_cli": "gemini",
|
||||
"gemini_api": "gemini", # gemini_api falls back to CLI in this context
|
||||
"local_openai": "local",
|
||||
"claude_cli": "claude",
|
||||
"gemini_cli": "gemini",
|
||||
"gemini_api": "gemini", # gemini_api falls back to CLI in this context
|
||||
"local_openai": "local",
|
||||
"anthropic_api": "anthropic_api",
|
||||
}
|
||||
|
||||
# Explicit UI toggle values (kept for backward compat)
|
||||
_EXPLICIT_BACKENDS = ("claude", "gemini", "local")
|
||||
_FALLBACK = {"claude": "gemini", "gemini": "claude", "local": "claude"}
|
||||
_FALLBACK = {"claude": "gemini", "gemini": "claude", "local": "claude", "anthropic_api": "claude"}
|
||||
|
||||
|
||||
async def complete(
|
||||
@@ -123,6 +124,8 @@ async def _dispatch(
|
||||
return await _gemini(system_prompt, messages)
|
||||
if backend == "local":
|
||||
return await _local(system_prompt, messages, model_cfg, attachment=attachment)
|
||||
if backend == "anthropic_api":
|
||||
return await _anthropic_api(system_prompt, messages, model_cfg)
|
||||
return await _claude(system_prompt, messages, model_cfg)
|
||||
|
||||
|
||||
@@ -254,6 +257,51 @@ async def _local(
|
||||
return text.strip()
|
||||
|
||||
|
||||
async def _anthropic_api(system_prompt: str, messages: list[dict], model_cfg: dict | None) -> str:
|
||||
"""Direct Anthropic API backend using the anthropic SDK."""
|
||||
try:
|
||||
import anthropic
|
||||
except ImportError:
|
||||
raise RuntimeError("anthropic SDK not installed — run: pip install 'anthropic>=0.40.0'")
|
||||
|
||||
cfg = model_cfg or {}
|
||||
api_key = cfg.get("api_key", "")
|
||||
model_name = cfg.get("model_name") or settings.default_model
|
||||
|
||||
if not api_key:
|
||||
raise RuntimeError("No Anthropic API key — add one at /settings/models")
|
||||
|
||||
client = anthropic.AsyncAnthropic(api_key=api_key)
|
||||
|
||||
msgs = [{"role": m["role"], "content": m["content"]} for m in messages]
|
||||
kwargs: dict = {
|
||||
"model": model_name,
|
||||
"max_tokens": 4096,
|
||||
"messages": msgs,
|
||||
}
|
||||
if system_prompt:
|
||||
kwargs["system"] = system_prompt
|
||||
|
||||
resp = await client.messages.create(**kwargs)
|
||||
|
||||
text = resp.content[0].text if resp.content else ""
|
||||
if not text.strip():
|
||||
raise RuntimeError("Anthropic API returned an empty response")
|
||||
|
||||
if resp.usage:
|
||||
import usage_tracker
|
||||
from persona import _user
|
||||
asyncio.create_task(usage_tracker.record(
|
||||
username=_user.get(),
|
||||
backend="anthropic_api",
|
||||
model_name=model_name,
|
||||
prompt_tokens=resp.usage.input_tokens,
|
||||
completion_tokens=resp.usage.output_tokens,
|
||||
))
|
||||
|
||||
return text.strip()
|
||||
|
||||
|
||||
async def _gemini(system_prompt: str, messages: list[dict]) -> str:
|
||||
# Gemini CLI spawns MCP child processes that keep stdout pipes open after responding.
|
||||
# start_new_session=True puts the whole tree in its own process group so
|
||||
|
||||
Reference in New Issue
Block a user