mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-04-28 11:30:03 +00:00
227 lines
7.6 KiB
Python
227 lines
7.6 KiB
Python
"""FastAPI route handlers."""
|
|
|
|
import traceback
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
|
from fastapi.responses import StreamingResponse
|
|
from loguru import logger
|
|
|
|
from config.settings import Settings
|
|
from providers.common import get_user_facing_error_message
|
|
from providers.exceptions import InvalidRequestError, ProviderError
|
|
|
|
from .dependencies import get_provider_for_type, get_settings, require_api_key
|
|
from .models.anthropic import MessagesRequest, TokenCountRequest
|
|
from .models.responses import ModelResponse, ModelsListResponse, TokenCountResponse
|
|
from .optimization_handlers import try_optimizations
|
|
from .request_utils import get_token_count
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
SUPPORTED_CLAUDE_MODELS = [
|
|
ModelResponse(
|
|
id="claude-opus-4-20250514",
|
|
display_name="Claude Opus 4",
|
|
created_at="2025-05-14T00:00:00Z",
|
|
),
|
|
ModelResponse(
|
|
id="claude-sonnet-4-20250514",
|
|
display_name="Claude Sonnet 4",
|
|
created_at="2025-05-14T00:00:00Z",
|
|
),
|
|
ModelResponse(
|
|
id="claude-haiku-4-20250514",
|
|
display_name="Claude Haiku 4",
|
|
created_at="2025-05-14T00:00:00Z",
|
|
),
|
|
ModelResponse(
|
|
id="claude-3-opus-20240229",
|
|
display_name="Claude 3 Opus",
|
|
created_at="2024-02-29T00:00:00Z",
|
|
),
|
|
ModelResponse(
|
|
id="claude-3-5-sonnet-20241022",
|
|
display_name="Claude 3.5 Sonnet",
|
|
created_at="2024-10-22T00:00:00Z",
|
|
),
|
|
ModelResponse(
|
|
id="claude-3-haiku-20240307",
|
|
display_name="Claude 3 Haiku",
|
|
created_at="2024-03-07T00:00:00Z",
|
|
),
|
|
ModelResponse(
|
|
id="claude-3-5-haiku-20241022",
|
|
display_name="Claude 3.5 Haiku",
|
|
created_at="2024-10-22T00:00:00Z",
|
|
),
|
|
]
|
|
|
|
|
|
def _probe_response(allow: str) -> Response:
|
|
"""Return an empty success response for compatibility probes."""
|
|
return Response(status_code=204, headers={"Allow": allow})
|
|
|
|
|
|
# =============================================================================
|
|
# Routes
|
|
# =============================================================================
|
|
@router.post("/v1/messages")
|
|
async def create_message(
|
|
request_data: MessagesRequest,
|
|
raw_request: Request,
|
|
settings: Settings = Depends(get_settings),
|
|
_auth=Depends(require_api_key),
|
|
):
|
|
"""Create a message (always streaming)."""
|
|
|
|
try:
|
|
if not request_data.messages:
|
|
raise InvalidRequestError("messages cannot be empty")
|
|
|
|
optimized = try_optimizations(request_data, settings)
|
|
if optimized is not None:
|
|
return optimized
|
|
logger.debug("No optimization matched, routing to provider")
|
|
|
|
# Resolve provider from the model-aware mapping
|
|
provider_type = Settings.parse_provider_type(
|
|
request_data.resolved_provider_model or settings.model
|
|
)
|
|
provider = get_provider_for_type(provider_type)
|
|
|
|
request_id = f"req_{uuid.uuid4().hex[:12]}"
|
|
logger.info(
|
|
"API_REQUEST: request_id={} model={} messages={}",
|
|
request_id,
|
|
request_data.model,
|
|
len(request_data.messages),
|
|
)
|
|
logger.debug("FULL_PAYLOAD [{}]: {}", request_id, request_data.model_dump())
|
|
|
|
input_tokens = get_token_count(
|
|
request_data.messages, request_data.system, request_data.tools
|
|
)
|
|
return StreamingResponse(
|
|
provider.stream_response(
|
|
request_data,
|
|
input_tokens=input_tokens,
|
|
request_id=request_id,
|
|
),
|
|
media_type="text/event-stream",
|
|
headers={
|
|
"X-Accel-Buffering": "no",
|
|
"Cache-Control": "no-cache",
|
|
"Connection": "keep-alive",
|
|
},
|
|
)
|
|
|
|
except ProviderError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error: {e!s}\n{traceback.format_exc()}")
|
|
raise HTTPException(
|
|
status_code=getattr(e, "status_code", 500),
|
|
detail=get_user_facing_error_message(e),
|
|
) from e
|
|
|
|
|
|
@router.api_route("/v1/messages", methods=["HEAD", "OPTIONS"])
|
|
async def probe_messages(_auth=Depends(require_api_key)):
|
|
"""Respond to Claude compatibility probes for the messages endpoint."""
|
|
return _probe_response("POST, HEAD, OPTIONS")
|
|
|
|
|
|
@router.post("/v1/messages/count_tokens")
|
|
async def count_tokens(request_data: TokenCountRequest, _auth=Depends(require_api_key)):
|
|
"""Count tokens for a request."""
|
|
request_id = f"req_{uuid.uuid4().hex[:12]}"
|
|
with logger.contextualize(request_id=request_id):
|
|
try:
|
|
tokens = get_token_count(
|
|
request_data.messages, request_data.system, request_data.tools
|
|
)
|
|
logger.info(
|
|
"COUNT_TOKENS: request_id={} model={} messages={} input_tokens={}",
|
|
request_id,
|
|
getattr(request_data, "model", "unknown"),
|
|
len(request_data.messages),
|
|
tokens,
|
|
)
|
|
return TokenCountResponse(input_tokens=tokens)
|
|
except Exception as e:
|
|
logger.error(
|
|
"COUNT_TOKENS_ERROR: request_id={} error={}\n{}",
|
|
request_id,
|
|
get_user_facing_error_message(e),
|
|
traceback.format_exc(),
|
|
)
|
|
raise HTTPException(
|
|
status_code=500, detail=get_user_facing_error_message(e)
|
|
) from e
|
|
|
|
|
|
@router.api_route("/v1/messages/count_tokens", methods=["HEAD", "OPTIONS"])
|
|
async def probe_count_tokens(_auth=Depends(require_api_key)):
|
|
"""Respond to Claude compatibility probes for the token count endpoint."""
|
|
return _probe_response("POST, HEAD, OPTIONS")
|
|
|
|
|
|
@router.get("/")
|
|
async def root(
|
|
settings: Settings = Depends(get_settings), _auth=Depends(require_api_key)
|
|
):
|
|
"""Root endpoint."""
|
|
return {
|
|
"status": "ok",
|
|
"provider": settings.provider_type,
|
|
"model": settings.model,
|
|
}
|
|
|
|
|
|
@router.api_route("/", methods=["HEAD", "OPTIONS"])
|
|
async def probe_root(_auth=Depends(require_api_key)):
|
|
"""Respond to compatibility probes for the root endpoint."""
|
|
return _probe_response("GET, HEAD, OPTIONS")
|
|
|
|
|
|
@router.get("/health")
|
|
async def health():
|
|
"""Health check endpoint."""
|
|
return {"status": "healthy"}
|
|
|
|
|
|
@router.api_route("/health", methods=["HEAD", "OPTIONS"])
|
|
async def probe_health():
|
|
"""Respond to compatibility probes for the health endpoint."""
|
|
return _probe_response("GET, HEAD, OPTIONS")
|
|
|
|
|
|
@router.get("/v1/models", response_model=ModelsListResponse)
|
|
async def list_models(_auth=Depends(require_api_key)):
|
|
"""List the Claude model ids this proxy advertises for compatibility."""
|
|
return ModelsListResponse(
|
|
data=SUPPORTED_CLAUDE_MODELS,
|
|
first_id=SUPPORTED_CLAUDE_MODELS[0].id if SUPPORTED_CLAUDE_MODELS else None,
|
|
has_more=False,
|
|
last_id=SUPPORTED_CLAUDE_MODELS[-1].id if SUPPORTED_CLAUDE_MODELS else None,
|
|
)
|
|
|
|
|
|
@router.post("/stop")
|
|
async def stop_cli(request: Request, _auth=Depends(require_api_key)):
|
|
"""Stop all CLI sessions and pending tasks."""
|
|
handler = getattr(request.app.state, "message_handler", None)
|
|
if not handler:
|
|
# Fallback if messaging not initialized
|
|
cli_manager = getattr(request.app.state, "cli_manager", None)
|
|
if cli_manager:
|
|
await cli_manager.stop_all()
|
|
logger.info("STOP_CLI: source=cli_manager cancelled_count=N/A")
|
|
return {"status": "stopped", "source": "cli_manager"}
|
|
raise HTTPException(status_code=503, detail="Messaging system not initialized")
|
|
|
|
count = await handler.stop_all_tasks()
|
|
logger.info("STOP_CLI: source=handler cancelled_count={}", count)
|
|
return {"status": "stopped", "cancelled_count": count}
|