API Dependency Injection Mechanism in Free-Claude-Code: FastAPI Provider Architecture Explained

Free-Claude-Code leverages FastAPI's Depends() system to inject runtime configuration, LLM provider instances, and optional Anthropic-style API key authentication via pure functions centralized in api/dependencies.py.

This article examines how the Alishahryar1/free-claude-code repository implements a clean, testable dependency injection (DI) layer that lazily constructs service objects and caches them across requests. By routing all provider instantiation through FastAPI's native DI framework, the codebase remains environment-driven and avoids hard-coded service dependencies.

Core Dependency Functions in api/dependencies.py

The DI layer is defined in api/dependencies.py, which exposes several callable dependencies that FastAPI resolves automatically per request or caches globally.

Configuration Injection

The foundation of the system is get_settings(), a cached dependency that returns a Pydantic Settings instance. This object loads values from environment variables and .env files, making configuration available throughout the dependency chain.

def get_settings() -> Settings:
    """Get application settings via dependency injection."""
    return _get_settings()

Source: api/dependencies.py

Provider Factory and Caching

Provider instantiation follows a lazy-loading factory pattern. The private function _create_provider_for_type() (L32-L133) constructs specific BaseProvider subclasses—such as NvidiaNimProvider, OpenRouterProvider, or LMStudioProvider—based on the provider_type parameter. It validates API keys and extracts proxy URLs via the helper _get_proxy_value() (L26-L30), which falls back to empty strings for invalid values.

The public interface get_provider_for_type() (L36-L52) implements singleton caching:

def get_provider_for_type(provider_type: str) -> BaseProvider:
    if provider_type not in _providers:
        try:
            _providers[provider_type] = _create_provider_for_type(provider_type, get_settings())
        except AuthenticationError as e:
            raise HTTPException(status_code=503, detail=get_user_facing_error_message(e)) from e
    logger.info("Provider initialized: {}", provider_type)
    return _providers[provider_type]

Instances are stored in the module-level _providers dictionary, ensuring that expensive HTTP client setup occurs only once. The convenience wrapper get_provider() (L88-L94) selects the default provider based on the Settings.provider_type property derived from the MODEL environment variable.

Authentication Guard

Security is enforced through require_api_key() (L54-L86), an optional dependency that validates Anthropic-style API keys. When Settings.anthropic_auth_token is configured, this function inspects x-api-key, Authorization: Bearer, or anthropic-auth-token headers and compares them against the stored token. If validation fails, FastAPI returns a 401/403 response before the route handler executes. When the token is empty, the check becomes a no-op.

Implementing Dependencies in Route Handlers

FastAPI consumes these dependencies through the Depends() callable. Route handlers in api/app.py declare their requirements as parameters, allowing the framework to inject live objects and execute validation logic automatically.

from fastapi import APIRouter, Depends
from api.dependencies import get_provider, require_api_key

router = APIRouter()

@router.post("/v1/chat/completions")
async def chat_completion(payload: ChatRequest,
                         provider = Depends(get_provider),
                         _: None = Depends(require_api_key)
):
    return await provider.chat(payload)

Depends(get_provider) injects the cached default provider instance, while Depends(require_api_key) acts as a gatekeeper. Because _providers caches instances at the module level, concurrent requests share the same initialized provider without reinitialization overhead.

Provider Lifecycle and Cleanup

The module-level _providers dictionary maintains singleton provider instances throughout the application lifetime. When the server shuts down, cleanup_provider() (L96-L102) gracefully terminates all active connections:

async def cleanup_provider():
    global _providers
    for provider in _providers.values():
        await provider.cleanup()
    _providers = {}

This async cleanup is registered in api/app.py as a shutdown event handler, ensuring that HTTP sessions to external LLM services (NVIDIA NIM, OpenRouter, etc.) are properly closed.

Testing with Dependency Overrides

The DI architecture facilitates unit testing by allowing runtime dependency overrides. Developers can swap real providers for mocks without modifying route implementations.

from fastapi.testclient import TestClient
from api.app import app
from api.dependencies import get_provider

def fake_provider():
    class Dummy:
        async def chat(self, payload):
            return {"choices": [{"message": {"content": "mocked"}}]}
    return Dummy()

app.dependency_overrides[get_provider] = fake_provider

client = TestClient(app)
resp = client.post("/v1/chat/completions", json={"model": "any", "messages": []})
assert resp.json()["choices"][0]["message"]["content"] == "mocked"

By overriding get_provider on the FastAPI application instance, tests bypass network calls and the require_api_key validation while exercising the full route logic.

Summary

  • FastAPI Depends() provides the backbone for injecting Settings, provider instances, and authentication guards into request handlers.
  • Provider objects are lazy-loaded via _create_provider_for_type() and cached in the module-level _providers dict, minimizing HTTP client initialization overhead.
  • Optional authentication is centralized in require_api_key(), which inspects multiple header formats and fails fast before business logic executes.
  • All configuration flows through a single Pydantic BaseSettings object defined in config/settings.py, supporting .env files and environment variables.
  • The design supports complete testability through FastAPI's dependency override mechanism, allowing mock providers without code changes.

Frequently Asked Questions

How does free-claude-code cache LLM provider instances across requests?

The get_provider_for_type() function in api/dependencies.py stores constructed provider objects in a module-level _providers dictionary. When a route requests a provider, FastAPI returns the cached instance if one exists; otherwise, it triggers _create_provider_for_type() to build and store a new object. This ensures that TLS configuration, API key headers, and HTTP session setup occur only once per provider type, even under concurrent load.

Where is the API key validation logic implemented?

Authentication is handled by require_api_key() in api/dependencies.py. This dependency inspects incoming requests for x-api-key, Authorization: Bearer, or anthropic-auth-token headers and validates them against Settings.anthropic_auth_token. If the setting is configured and validation fails, FastAPI raises an HTTP exception before the route handler executes, providing centralized security without cluttering business logic.

Can specific routes use a different LLM provider than the default?

Yes. While get_provider() returns the system default based on the MODEL environment variable, you can inject a specific provider by wrapping get_provider_for_type() with a lambda: Depends(lambda: get_provider_for_type("llamacpp")). This targets the LlamaCpp provider regardless of the default configuration, useful for health checks or model-specific endpoints.

How are application settings loaded and propagated through the dependency chain?

The get_settings() function returns a Pydantic Settings instance from config/settings.py that automatically loads environment variables, .env files in the project root or ~/.config/free-claude-code/.env, and files pointed to by FCC_ENV_FILE. This singleton configuration object is passed implicitly to provider factory functions, ensuring that API keys, proxy URLs, and timeout values remain consistent across the dependency injection lifecycle.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →