"""FastAPI route handlers.""" from fastapi import APIRouter, Depends, HTTPException, Request, Response from loguru import logger from free_claude_code.application.errors import ApplicationError from free_claude_code.application.ports import ProviderResolver, RequestRuntimeLease from free_claude_code.config.model_refs import parse_provider_type from free_claude_code.config.settings import Settings from free_claude_code.core.anthropic import ( MessagesRequest, TokenCountRequest, get_token_count, ) from free_claude_code.core.openai_responses import OpenAIResponsesRequest from free_claude_code.core.trace import trace_event from .dependencies import ( get_services, get_settings, require_api_key, resolve_provider, ) from .handlers import MessagesHandler, ResponsesHandler, TokenCountHandler from .model_catalog import ModelsListResponse, build_models_list_response from .ports import ApiServices from .request_errors import ordinary_application_error_response from .request_ids import get_request_id from .response_streams import bind_response_lifetime router = APIRouter() def _provider_resolver(lease: RequestRuntimeLease) -> ProviderResolver: return lambda provider_type: resolve_provider(provider_type, lease=lease) async def _create_messages_response( services: ApiServices, request_data: MessagesRequest, *, request_id: str, ) -> object: lease: RequestRuntimeLease | None = None try: lease = await services.requests.acquire() handler = MessagesHandler( lease.settings, provider_resolver=_provider_resolver(lease), token_counter=get_token_count, generation_id=lease.generation_id, ) response = await handler.create(request_data, request_id=request_id) except ApplicationError as exc: if lease is not None: await lease.release() return ordinary_application_error_response( exc, wire_api="messages", request_id=request_id, ) except BaseException: if lease is not None: await lease.release() raise assert lease is not None return await bind_response_lifetime(response, lease.release) async def _create_responses_response( services: ApiServices, request_data: OpenAIResponsesRequest, *, request_id: str, ) -> object: lease: RequestRuntimeLease | None = None try: lease = await services.requests.acquire() handler = ResponsesHandler( lease.settings, provider_resolver=_provider_resolver(lease), generation_id=lease.generation_id, ) response = await handler.create(request_data, request_id=request_id) except ApplicationError as exc: if lease is not None: await lease.release() return ordinary_application_error_response( exc, wire_api="responses", request_id=request_id, ) except BaseException: if lease is not None: await lease.release() raise assert lease is not None return await bind_response_lifetime(response, lease.release) def _probe_response(allow: str) -> Response: return Response(status_code=204, headers={"Allow": allow}) @router.post("/v1/messages") async def create_message( request: Request, request_data: MessagesRequest, services: ApiServices = Depends(get_services), _auth=Depends(require_api_key), ): """Create a message (streaming by default; stream=false gets aggregated JSON).""" return await _create_messages_response( services, request_data, request_id=get_request_id(request), ) @router.api_route("/v1/messages", methods=["HEAD", "OPTIONS"]) async def probe_messages(_auth=Depends(require_api_key)): return _probe_response("POST, HEAD, OPTIONS") @router.post("/v1/responses") async def create_response( request: Request, request_data: OpenAIResponsesRequest, services: ApiServices = Depends(get_services), _auth=Depends(require_api_key), ): """Create an OpenAI Responses-compatible response through this proxy.""" return await _create_responses_response( services, request_data, request_id=get_request_id(request), ) @router.api_route("/v1/responses", methods=["HEAD", "OPTIONS"]) async def probe_responses(_auth=Depends(require_api_key)): return _probe_response("POST, HEAD, OPTIONS") @router.post("/v1/messages/count_tokens") async def count_tokens( request: Request, request_data: TokenCountRequest, settings: Settings = Depends(get_settings), _auth=Depends(require_api_key), ): """Count tokens for a request.""" handler = TokenCountHandler(settings, token_counter=get_token_count) return handler.count(request_data, request_id=get_request_id(request)) @router.api_route("/v1/messages/count_tokens", methods=["HEAD", "OPTIONS"]) async def probe_count_tokens(_auth=Depends(require_api_key)): return _probe_response("POST, HEAD, OPTIONS") @router.get("/") async def root( settings: Settings = Depends(get_settings), _auth=Depends(require_api_key), ): return { "status": "ok", "provider": parse_provider_type(settings.model), "model": settings.model, } @router.api_route("/", methods=["HEAD", "OPTIONS"]) async def probe_root(): return _probe_response("GET, HEAD, OPTIONS") @router.get("/health") async def health(): return {"status": "healthy"} @router.api_route("/health", methods=["HEAD", "OPTIONS"]) async def probe_health(): return _probe_response("GET, HEAD, OPTIONS") @router.get("/v1/models", response_model=ModelsListResponse) async def list_models( services: ApiServices = Depends(get_services), settings: Settings = Depends(get_settings), _auth=Depends(require_api_key), ): """List the model ids this proxy advertises to compatible clients.""" trace_event(stage="ingress", event="free_claude_code.api.models.list", source="api") return build_models_list_response(settings, services.requests) @router.post("/stop") async def stop_cli( services: ApiServices = Depends(get_services), _auth=Depends(require_api_key), ): """Stop all CLI sessions and pending tasks.""" result = await services.tasks.stop_all() if result is None: raise HTTPException(status_code=503, detail="Messaging system not initialized") if result.source is not None: logger.info("STOP_CLI: source={} cancelled_count=N/A", result.source) return {"status": "stopped", "source": result.source} count = result.cancelled_count or 0 trace_event( stage="ingress", event="free_claude_code.api.cli.stop_via_messaging_workflow", source="api", cancelled_nodes=count, ) logger.info("STOP_CLI: source=messaging_workflow cancelled_count={}", count) return {"status": "stopped", "cancelled_count": count}