free-claude-code/api/dependencies.py
Alishahryar1 b926f60f64 feat: Anthropic web server tools, provider metadata, messaging hardening
- Add local web_search/web_fetch SSE handling and optional tool schemas
- Extend HeuristicToolParser for JSON-style WebFetch/WebSearch text
- Consolidate provider defaults, ids, and exception typing; stream contracts
- Messaging: typed options, voice config injection, platform contract cleanup
- Tests for web server tools, converters, parsers, contracts; ignore debug-*.log
2026-04-24 23:01:14 -07:00

131 lines
4.7 KiB
Python

"""Dependency injection for FastAPI."""
from fastapi import Depends, HTTPException, Request
from loguru import logger
from starlette.applications import Starlette
from config.settings import Settings
from config.settings import get_settings as _get_settings
from core.anthropic import get_user_facing_error_message
from providers.base import BaseProvider
from providers.exceptions import AuthenticationError, UnknownProviderTypeError
from providers.registry import PROVIDER_DESCRIPTORS, ProviderRegistry
# Process-level cache: only for :func:`get_provider_for_type` / :func:`get_provider`
# when there is no ``Request``/``app`` (unit tests, scripts). HTTP handlers must pass
# ``app`` to :func:`resolve_provider` so the app-scoped registry is used.
_providers: dict[str, BaseProvider] = {}
def get_settings() -> Settings:
"""Get application settings via dependency injection."""
return _get_settings()
def resolve_provider(
provider_type: str,
*,
app: Starlette | None,
settings: Settings,
) -> BaseProvider:
"""Resolve a provider using the app-scoped registry when ``app`` is set.
When ``app`` is not ``None``, the app-owned :attr:`app.state.provider_registry`
is always used. If the registry is missing (e.g. a test app without
:class:`~api.runtime.AppRuntime` startup), a new :class:`ProviderRegistry`
is installed on ``app.state`` so the process cache is never mixed with
per-request app identity.
When ``app`` is ``None`` (no HTTP context), uses the process-level
:data:`_providers` cache only.
"""
if app is not None:
reg = getattr(app.state, "provider_registry", None)
if reg is None:
reg = ProviderRegistry()
app.state.provider_registry = reg
return _resolve_with_registry(reg, provider_type, settings)
return _resolve_with_registry(ProviderRegistry(_providers), provider_type, settings)
def _resolve_with_registry(
registry: ProviderRegistry, provider_type: str, settings: Settings
) -> BaseProvider:
should_log_init = not registry.is_cached(provider_type)
try:
provider = registry.get(provider_type, settings)
except AuthenticationError as e:
raise HTTPException(
status_code=503, detail=get_user_facing_error_message(e)
) from e
except UnknownProviderTypeError:
logger.error(
"Unknown provider_type: '{}'. Supported: {}",
provider_type,
", ".join(f"'{key}'" for key in PROVIDER_DESCRIPTORS),
)
raise
if should_log_init:
logger.info("Provider initialized: {}", provider_type)
return provider
def get_provider_for_type(provider_type: str) -> BaseProvider:
"""Get or create a provider in the process-level cache (no ``app``/Request).
For server requests, use :func:`resolve_provider` with the active
:attr:`request.app` so the app-scoped provider registry is used.
"""
return resolve_provider(provider_type, app=None, settings=get_settings())
def require_api_key(
request: Request, settings: Settings = Depends(get_settings)
) -> None:
"""Require a server API key (Anthropic-style).
Checks `x-api-key` header or `Authorization: Bearer ...` against
`Settings.anthropic_auth_token`. If `ANTHROPIC_AUTH_TOKEN` is empty, this is a no-op.
"""
anthropic_auth_token = settings.anthropic_auth_token
if not anthropic_auth_token:
# No API key configured -> allow
return
header = (
request.headers.get("x-api-key")
or request.headers.get("authorization")
or request.headers.get("anthropic-auth-token")
)
if not header:
raise HTTPException(status_code=401, detail="Missing API key")
# Support both raw key in X-API-Key and Bearer token in Authorization
token = header
if header.lower().startswith("bearer "):
token = header.split(" ", 1)[1]
# Strip anything after the first colon to handle tokens with appended model names
if token and ":" in token:
token = token.split(":", 1)[0]
if token != anthropic_auth_token:
raise HTTPException(status_code=401, detail="Invalid API key")
def get_provider() -> BaseProvider:
"""Get or create the default provider (``MODEL`` / ``provider_type``).
Process-cache helper for scripts, unit tests, and non-FastAPI callers. HTTP
handlers must use :func:`resolve_provider` with :attr:`request.app` so the
app-scoped :class:`~providers.registry.ProviderRegistry` is used.
"""
return get_provider_for_type(get_settings().provider_type)
async def cleanup_provider():
"""Cleanup all provider resources."""
global _providers
await ProviderRegistry(_providers).cleanup()
_providers = {}
logger.debug("Provider cleanup completed")