| """Pure FastAPI application factory.""" |
|
|
| 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 free_claude_code.application.errors import ApplicationError |
| from free_claude_code.core.anthropic import anthropic_error_payload |
| from free_claude_code.core.diagnostics import ( |
| redacted_exception_traceback, |
| safe_exception_message, |
| ) |
| from free_claude_code.core.openai_responses import openai_error_payload |
| from free_claude_code.core.trace import ( |
| extract_claude_session_id_from_headers, |
| trace_event, |
| ) |
| from free_claude_code.core.version import package_version |
|
|
| from .admin_cache import AdminNoStoreMiddleware, attach_admin_no_store |
| from .admin_routes import router as admin_router |
| from .ports import ApiServices |
| from .request_errors import ordinary_application_error_response |
| from .request_ids import ( |
| RequestCorrelationMiddleware, |
| attach_request_id_headers, |
| get_request_id, |
| ) |
| from .routes import router |
| from .validation_log import summarize_request_validation_body |
|
|
|
|
| def create_app(services: ApiServices) -> FastAPI: |
| """Create the HTTP adapter around explicitly supplied runtime services.""" |
| app = FastAPI(title="Claude Code Proxy", version=package_version()) |
| app.state.services = services |
| app.add_middleware(RequestCorrelationMiddleware) |
| app.add_middleware(AdminNoStoreMiddleware) |
|
|
| app.include_router(admin_router) |
| app.include_router(router) |
|
|
| @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 error: |
| body = {"_json_error": type(error).__name__} |
|
|
| message_summary, tool_names = summarize_request_validation_body(body) |
| trace_event( |
| stage="ingress", |
| event="server.request.validation_failed", |
| source="api", |
| path=request.url.path, |
| query=dict(request.query_params), |
| error_locs=[list(error.get("loc", ())) for error in exc.errors()], |
| error_types=[str(error.get("type", "")) for error in exc.errors()], |
| message_summary=message_summary, |
| tool_names=tool_names, |
| ) |
| return await request_validation_exception_handler(request, exc) |
|
|
| @app.exception_handler(ApplicationError) |
| async def application_error_handler(request: Request, exc: ApplicationError): |
| """Serialize defensive application failures in the selected wire protocol.""" |
| return ordinary_application_error_response( |
| exc, |
| wire_api=( |
| "responses" if request.url.path == "/v1/responses" else "messages" |
| ), |
| request_id=get_request_id(request), |
| ) |
|
|
| @app.exception_handler(Exception) |
| async def general_error_handler(request: Request, exc: Exception): |
| """Handle general errors and return Anthropic format.""" |
| request_id = get_request_id(request) |
| claude_sid = extract_claude_session_id_from_headers(request.headers) |
| settings = services.requests.current_settings() |
| with logger.contextualize( |
| http_method=request.method, |
| http_path=request.url.path, |
| claude_session_id=claude_sid, |
| request_id=request_id, |
| ): |
| if settings.log_api_error_tracebacks: |
| logger.error("General Error: {}", safe_exception_message(exc)) |
| logger.error(redacted_exception_traceback(exc)) |
| else: |
| logger.error( |
| "General Error: path={} method={} exc_type={}", |
| request.url.path, |
| request.method, |
| type(exc).__name__, |
| ) |
| message = safe_exception_message(exc) |
| if request.url.path == "/v1/responses": |
| content = openai_error_payload(message=message, error_type="api_error") |
| else: |
| content = anthropic_error_payload( |
| error_type="api_error", |
| message=message, |
| request_id=request_id, |
| ) |
| response = JSONResponse(status_code=500, content=content) |
| attach_admin_no_store(response, path=request.url.path) |
| attach_request_id_headers( |
| response, |
| request_id=request_id, |
| path=request.url.path, |
| ) |
| return response |
|
|
| return app |
|
|