mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-07-03 14:05:26 +02:00
58aef0dc8a
## Problem Provider construction, model discovery, validation, and cleanup lived in one registry module. API and admin routes depended on registry-shaped app state and legacy process-level provider helpers. ## Changes | Before | After | | --- | --- | | `providers.registry` mixed provider factories, config, cache, discovery, validation, and cleanup. | `providers.runtime` splits factories, config, cache, model cache, discovery, validation, and runtime orchestration. | | API and admin routes read `app.state.provider_registry` and sometimes created registries ad hoc. | API and admin routes use app-scoped `ProviderRuntime` through `app.state.provider_runtime`. | | `api.dependencies` kept process-global provider cache helpers. | `api.dependencies` resolves providers only through the app-scoped runtime. | | Registry-shaped tests preserved old internal boundaries. | Runtime-shaped tests assert provider config, construction, cache, discovery, validation, and import boundaries. | <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> This PR moves provider lifecycle ownership from the old registry module into an app-scoped runtime package. The main changes are: - Split provider config, factory wiring, instance cache, model cache, discovery, validation, and cleanup into `providers.runtime` modules. - Updated API and admin routes to resolve providers and model metadata through `app.state.provider_runtime`. - Removed legacy process-global provider helpers and the deleted `providers.registry` module. - Updated docs, smoke metadata, import-boundary checks, and tests for the new runtime ownership model. - Bumped the package version and lockfile metadata for the production refactor. </details> <h3>Confidence Score: 5/5</h3> The provider runtime refactor appears merge-safe with no identified blocking issues. The changes consistently move provider ownership to app-scoped runtime modules and update API, admin, docs, smoke metadata, import-boundary checks, and tests around that architecture. <details><summary><h3><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="20" align="absmiddle"></a> T-Rex Logs</h3></summary> **What T-Rex did** - Ran a baseline and head comparison of provider registry and runtime states, verifying the after-state shows head state\_has\_provider\_registry=False and state\_has\_provider\_runtime=True, that GET /v1/models and admin endpoints respond with 200, and that provider\_resolver\_called via runtime, with assertions passing. - Verified that the four focused provider-runtime contract tests passed in both the before and after refactor runs, including runtime split checks, with exit code 0. - Identified environmental blockers that prevented the smoke-runtime workflow from running, including uv unavailability, missing pytest for /usr/local/bin/python, and Python 3.11 being used despite pyproject.toml requiring \>=3.14. <a href="https://app.greptile.com/trex/runs/12528505/artifacts"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=1"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=1"><img alt="View all artifacts" src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=1" height="32"></picture></a> <sub><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="14" align="absmiddle"></a> Ran code and verified through T-Rex</sub> </details> <sub>Reviews (1): Last reviewed commit: ["Refactor provider runtime ownership"](https://github.com/alishahryar1/free-claude-code/commit/01d589488185c1f85112f1a49c47f04512846161) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=40312173)</sub> <!-- /greptile_comment -->
107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
"""Dependency injection for FastAPI."""
|
|
|
|
import secrets
|
|
|
|
from fastapi import Depends, HTTPException, Request
|
|
from loguru import logger
|
|
from starlette.applications import Starlette
|
|
|
|
from config.provider_catalog import PROVIDER_CATALOG
|
|
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,
|
|
ServiceUnavailableError,
|
|
UnknownProviderTypeError,
|
|
)
|
|
from providers.runtime import ProviderRuntime
|
|
|
|
|
|
def get_settings() -> Settings:
|
|
"""Return cached :class:`~config.settings.Settings` (FastAPI-friendly alias)."""
|
|
return _get_settings()
|
|
|
|
|
|
def get_provider_runtime(app: Starlette) -> ProviderRuntime:
|
|
"""Return the app-scoped provider runtime installed by ``AppRuntime``."""
|
|
runtime = getattr(app.state, "provider_runtime", None)
|
|
if isinstance(runtime, ProviderRuntime):
|
|
return runtime
|
|
raise ServiceUnavailableError(
|
|
"Provider runtime is not configured. Ensure AppRuntime startup ran "
|
|
"or assign app.state.provider_runtime for test apps."
|
|
)
|
|
|
|
|
|
def maybe_provider_runtime(app: Starlette) -> ProviderRuntime | None:
|
|
"""Return the app-scoped provider runtime when it is installed."""
|
|
runtime = getattr(app.state, "provider_runtime", None)
|
|
return runtime if isinstance(runtime, ProviderRuntime) else None
|
|
|
|
|
|
def resolve_provider(
|
|
provider_type: str,
|
|
*,
|
|
app: Starlette,
|
|
) -> BaseProvider:
|
|
"""Resolve a provider through the app-scoped provider runtime."""
|
|
runtime = get_provider_runtime(app)
|
|
should_log_init = not runtime.is_cached(provider_type)
|
|
try:
|
|
provider = runtime.resolve_provider(provider_type)
|
|
except AuthenticationError as e:
|
|
# Provider :class:`~providers.exceptions.AuthenticationError` messages are
|
|
# curated configuration hints (env var names, docs links), not upstream noise.
|
|
detail = str(e).strip() or get_user_facing_error_message(e)
|
|
raise HTTPException(status_code=503, detail=detail) from e
|
|
except UnknownProviderTypeError:
|
|
logger.error(
|
|
"Unknown provider_type: '{}'. Supported: {}",
|
|
provider_type,
|
|
", ".join(f"'{key}'" for key in PROVIDER_CATALOG),
|
|
)
|
|
raise
|
|
if should_log_init:
|
|
logger.info("Provider initialized: {}", provider_type)
|
|
return provider
|
|
|
|
|
|
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.strip()
|
|
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.strip()
|
|
if header.lower().startswith("bearer "):
|
|
token = header.split(" ", 1)[1].strip()
|
|
|
|
# Strip anything after the first colon to handle tokens with appended model names
|
|
if token and ":" in token:
|
|
token = token.split(":", 1)[0].strip()
|
|
|
|
# Constant-time comparison to avoid leaking the configured token via
|
|
# response-time differences on a per-byte mismatch (CWE-208).
|
|
if not secrets.compare_digest(
|
|
token.encode("utf-8"), anthropic_auth_token.encode("utf-8")
|
|
):
|
|
raise HTTPException(status_code=401, detail="Invalid API key")
|