"""FastAPI application factory and configuration.""" import traceback from contextlib import asynccontextmanager from typing import Any from fastapi import FastAPI, Request from fastapi.exception_handlers import request_validation_exception_handler from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from loguru import logger from config.logging_config import configure_logging from config.settings import get_settings from providers.exceptions import ProviderError from .routes import router from .runtime import AppRuntime from .validation_log import summarize_request_validation_body @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan manager.""" runtime = AppRuntime.for_app(app, settings=get_settings()) await runtime.startup() yield await runtime.shutdown() def create_app() -> FastAPI: """Create and configure the FastAPI application.""" settings = get_settings() configure_logging( settings.log_file, verbose_third_party=settings.log_raw_api_payloads ) app = FastAPI( title="Claude Code Proxy", version="2.0.0", lifespan=lifespan, ) # Register routes app.include_router(router) # Exception handlers @app.exception_handler(RequestValidationError) async def validation_error_handler(request: Request, exc: RequestValidationError): """Log request shape for 422 debugging without content values.""" body: Any try: body = await request.json() except Exception as e: body = {"_json_error": type(e).__name__} message_summary, tool_names = summarize_request_validation_body(body) logger.debug( "Request validation failed: path={} query={} error_locs={} error_types={} message_summary={} tool_names={}", request.url.path, str(request.url.query), [list(error.get("loc", ())) for error in exc.errors()], [str(error.get("type", "")) for error in exc.errors()], message_summary, tool_names, ) return await request_validation_exception_handler(request, exc) @app.exception_handler(ProviderError) async def provider_error_handler(request: Request, exc: ProviderError): """Handle provider-specific errors and return Anthropic format.""" err_settings = get_settings() if err_settings.log_api_error_tracebacks: logger.error( "Provider Error: error_type={} status_code={} message={}", exc.error_type, exc.status_code, exc.message, ) else: logger.error( "Provider Error: error_type={} status_code={}", exc.error_type, exc.status_code, ) return JSONResponse( status_code=exc.status_code, content=exc.to_anthropic_format(), ) @app.exception_handler(Exception) async def general_error_handler(request: Request, exc: Exception): """Handle general errors and return Anthropic format.""" settings = get_settings() if settings.log_api_error_tracebacks: logger.error("General Error: {}", exc) logger.error(traceback.format_exc()) else: logger.error( "General Error: path={} method={} exc_type={}", request.url.path, request.method, type(exc).__name__, ) return JSONResponse( status_code=500, content={ "type": "error", "error": { "type": "api_error", "message": "An unexpected error occurred.", }, }, ) return app