feat: add Claude backend with automatic Ollama fallback (Claudification Phase 1)
Build and Push / release (release) Failing after 6s
Build and Push / build (release) Successful in 3m5s

All agents now prefer Claude API when ANTHROPIC_API_KEY is configured,
with automatic fallback to Ollama when offline or unconfigured. New
src/anthropic/ module provides model selection via get_model() factory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 07:19:29 +01:00
co-authored by Claude Opus 4.5
parent 5d23bcae79
commit 496f37a538
16 changed files with 790 additions and 486 deletions
+151
View File
@@ -0,0 +1,151 @@
"""
Model selector for Claude/Ollama backend switching.
Provides automatic model selection with Claude as preferred backend
and Ollama as offline fallback.
"""
from typing import Union
from pydantic_ai.models.anthropic import AnthropicModel
from pydantic_ai.models.openai import OpenAIChatModel
from src.core.config import config
from src.core.logging_config import get_logger
logger = get_logger(__name__)
# Cached health check result (set once at startup)
_claude_available: bool | None = None
async def check_claude_health() -> bool:
"""
Check if Claude API is reachable and working.
This should be called once at application startup.
The result is cached in `_claude_available`.
Returns:
True if Claude API is accessible, False otherwise.
"""
global _claude_available
# No API key configured - Claude not available
if not config.ANTHROPIC_API_KEY:
logger.info(
"claude_health_check_skipped",
reason="no_api_key",
)
_claude_available = False
return False
try:
from anthropic import AsyncAnthropic
client = AsyncAnthropic(api_key=config.ANTHROPIC_API_KEY)
# Minimal API call to verify connectivity
# Using a tiny max_tokens to minimize cost
await client.messages.create(
model=config.ANTHROPIC_MODEL,
max_tokens=1,
messages=[{"role": "user", "content": "hi"}],
)
_claude_available = True
logger.info(
"claude_health_check_passed",
model=config.ANTHROPIC_MODEL,
)
return True
except Exception as e:
_claude_available = False
logger.warning(
"claude_health_check_failed",
error=str(e),
model=config.ANTHROPIC_MODEL,
)
return False
def is_claude_available() -> bool:
"""
Check if Claude is available (from cached health check result).
Returns:
True if Claude API was reachable at startup, False otherwise.
Note:
Returns False if health check hasn't been run yet.
Call `check_claude_health()` at startup first.
"""
return _claude_available is True
def get_model(prefer_cloud: bool | None = None) -> Union[AnthropicModel, OpenAIChatModel]:
"""
Get the best available model.
Returns Claude if available and preferred, otherwise Ollama.
Args:
prefer_cloud: Override config.PREFER_CLOUD_BACKEND for this call.
If None, uses the config value.
Returns:
PydanticAI model instance (AnthropicModel or OpenAIChatModel).
Example:
>>> model = get_model()
>>> agent = Agent(model, system_prompt="...")
"""
# Determine preference
use_cloud = prefer_cloud if prefer_cloud is not None else config.PREFER_CLOUD_BACKEND
# Use Claude if available and preferred
if use_cloud and is_claude_available():
logger.debug(
"model_selected",
backend="claude",
model=config.ANTHROPIC_MODEL,
)
return AnthropicModel(
model_name=config.ANTHROPIC_MODEL,
api_key=config.ANTHROPIC_API_KEY,
)
# Fall back to Ollama
from src.ollama.provider import get_ollama_provider
logger.debug(
"model_selected",
backend="ollama",
model=config.OLLAMA_DEFAULT_MODEL,
reason="fallback" if use_cloud else "preferred_local",
)
return OpenAIChatModel(
model_name=config.OLLAMA_DEFAULT_MODEL,
provider=get_ollama_provider(),
)
def get_model_info() -> dict:
"""
Get information about the current model configuration.
Useful for health checks and debugging.
Returns:
Dict with backend, model name, and availability info.
"""
use_cloud = config.PREFER_CLOUD_BACKEND and is_claude_available()
return {
"backend": "claude" if use_cloud else "ollama",
"model": config.ANTHROPIC_MODEL if use_cloud else config.OLLAMA_DEFAULT_MODEL,
"claude_available": is_claude_available(),
"claude_configured": bool(config.ANTHROPIC_API_KEY),
"prefer_cloud": config.PREFER_CLOUD_BACKEND,
}