Files
wehub-resource-sync 5296d0e97c
CI / Ban suppressions and legacy annotations (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / ruff-check (push) Has been cancelled
CI / ruff-format (push) Has been cancelled
CI / ty (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:44 +08:00

222 lines
6.9 KiB
Python

"""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}