Team Ai
Apppublic

Jack1808/Claude_Code

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
routes.py346 linesDownload Raw Back to api
1"""FastAPI route handlers."""2 3import os4import shutil5import tempfile6import time7import traceback8import uuid9from pathlib import Path10 11from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request12from fastapi.responses import HTMLResponse, StreamingResponse13from loguru import logger14 15from config.settings import Settings16from providers.common import get_user_facing_error_message17from providers.exceptions import InvalidRequestError, ProviderError18 19from .dependencies import get_provider_for_type, get_settings, require_api_key20from .models.anthropic import MessagesRequest, TokenCountRequest21from .models.responses import TokenCountResponse22from .optimization_handlers import try_optimizations23from .request_utils import get_token_count24 25router = APIRouter()26 27 28def _home_page_html(status_payload: dict[str, str]) -> str:29        """Render the home page HTML with the factory reset button."""30        return f"""31<!doctype html>32<html lang=\"en\">33<head>34    <meta charset=\"utf-8\" />35    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />36    <title>Claude Code Proxy</title>37    <style>38        :root {{ color-scheme: dark; }}39        body {{ margin: 0; min-height: 100vh; display: grid; place-items: center; font-family: Inter, Segoe UI, Arial, sans-serif; background: radial-gradient(1200px 600px at 20% 10%, #2e1b4a, #12131f 45%, #0b0c12); color: #e8ecff; }}40        .card {{ width: min(92vw, 560px); background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.14); border-radius: 20px; padding: 28px; box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35); }}41        h1 {{ margin: 0 0 10px; font-size: 1.35rem; }}42        p {{ margin: 0 0 8px; color: #cfd7ff; line-height: 1.45; }}43        .meta {{ margin: 14px 0 18px; font-size: 0.95rem; color: #dfe6ff; }}44        .meta span {{ display: inline-block; margin-right: 12px; opacity: 0.95; }}45        button {{ border: none; border-radius: 14px; padding: 12px 18px; font-size: 1rem; font-weight: 700; color: white; cursor: pointer; background: linear-gradient(135deg, #ff507a, #7f5bff); box-shadow: 0 10px 20px rgba(127, 91, 255, 0.35); }}46        button:disabled {{ opacity: 0.65; cursor: wait; }}47        .status {{ margin-top: 14px; min-height: 24px; font-size: 0.95rem; color: #b8ffd8; }}48    </style>49</head>50<body>51    <div class=\"card\">52        <h1>Claude Code Proxy</h1>53        <p>Server is running.</p>54        <div class=\"meta\">55            <span><strong>Status:</strong> {status_payload['status']}</span>56            <span><strong>Provider:</strong> {status_payload['provider']}</span>57            <span><strong>Model:</strong> {status_payload['model']}</span>58        </div>59        <button id=\"resetBtn\">Factory Restart</button>60        <div class=\"status\" id=\"status\"></div>61    </div>62    <script>63        const btn = document.getElementById('resetBtn');64        const status = document.getElementById('status');65        btn.addEventListener('click', async () => {{66            btn.disabled = true;67            status.textContent = 'Resetting cache/workspace and restarting...';68            try {{69                const response = await fetch('/admin/factory-reset' + window.location.search, {{ method: 'POST' }});70                const data = await response.json();71                if (!response.ok) {{72                    throw new Error(data.detail || 'Request failed');73                }}74                status.textContent = 'Restart initiated. This page will disconnect briefly.';75            }} catch (err) {{76                status.textContent = 'Failed: ' + (err.message || String(err));77                btn.disabled = false;78            }}79        }});80    </script>81</body>82</html>83"""84 85 86def _clear_path(path: Path) -> int:87    """Best-effort removal of a file/directory path. Returns removed item count."""88    if not path.exists():89        return 090    try:91        if path.is_dir():92            shutil.rmtree(path)93        else:94            path.unlink()95        return 196    except Exception as e:97        logger.warning("Failed to remove path {}: {}", path, e)98        return 099 100 101def _clear_workspace_contents(workspace: Path) -> int:102    """Best-effort clear of workspace contents while preserving root directory."""103    if not workspace.exists() or not workspace.is_dir():104        return 0105    removed = 0106    for child in workspace.iterdir():107        removed += _clear_path(child)108    return removed109 110 111def _clear_runtime_state(settings: Settings) -> dict[str, int]:112    """Clear runtime caches/workspace data for a lightweight factory reset."""113    removed = {114        "workspace_items": 0,115        "cache_dirs": 0,116        "pycache_dirs": 0,117    }118 119    workspace = Path(settings.claude_workspace).expanduser().resolve()120    removed["workspace_items"] = _clear_workspace_contents(workspace)121 122    cache_dirs = [123        Path.home() / ".cache" / "huggingface",124        Path.home() / ".cache" / "uv",125        Path.home() / ".cache" / "pip",126        Path(tempfile.gettempdir()) / "huggingface",127    ]128    for cache_dir in cache_dirs:129        removed["cache_dirs"] += _clear_path(cache_dir)130 131    project_root = Path.cwd()132    for pycache_dir in project_root.rglob("__pycache__"):133        if ".venv" in pycache_dir.parts:134            continue135        removed["pycache_dirs"] += _clear_path(pycache_dir)136 137    return removed138 139 140def _restart_process() -> None:141    """Terminate process so container orchestrator restarts the app."""142    logger.warning("Factory reset requested: restarting process")143    time.sleep(1.0)144    os._exit(0)145 146 147# =============================================================================148# Routes149# =============================================================================150@router.post("/v1/messages")151async def create_message(152    request_data: MessagesRequest,153    raw_request: Request,154    settings: Settings = Depends(get_settings),155    _auth=Depends(require_api_key),156):157    """Create a message (always streaming)."""158 159    try:160        if not request_data.messages:161            raise InvalidRequestError("messages cannot be empty")162 163        optimized = try_optimizations(request_data, settings)164        if optimized is not None:165            return optimized166        logger.debug("No optimization matched, routing to provider")167 168        # Resolve provider from the model-aware mapping169        provider_type = Settings.parse_provider_type(170            request_data.resolved_provider_model or settings.model171        )172        provider = get_provider_for_type(provider_type)173 174        request_id = f"req_{uuid.uuid4().hex[:12]}"175        logger.info(176            "API_REQUEST: request_id={} model={} messages={}",177            request_id,178            request_data.model,179            len(request_data.messages),180        )181        logger.debug("FULL_PAYLOAD [{}]: {}", request_id, request_data.model_dump())182 183        input_tokens = get_token_count(184            request_data.messages, request_data.system, request_data.tools185        )186        return StreamingResponse(187            provider.stream_response(188                request_data,189                input_tokens=input_tokens,190                request_id=request_id,191            ),192            media_type="text/event-stream",193            headers={194                "X-Accel-Buffering": "no",195                "Cache-Control": "no-cache",196                "Connection": "keep-alive",197            },198        )199 200    except ProviderError:201        raise202    except Exception as e:203        logger.error(f"Error: {e!s}\n{traceback.format_exc()}")204        raise HTTPException(205            status_code=getattr(e, "status_code", 500),206            detail=get_user_facing_error_message(e),207        ) from e208 209 210@router.post("/v1/messages/count_tokens")211async def count_tokens(request_data: TokenCountRequest, _auth=Depends(require_api_key)):212    """Count tokens for a request."""213    request_id = f"req_{uuid.uuid4().hex[:12]}"214    with logger.contextualize(request_id=request_id):215        try:216            tokens = get_token_count(217                request_data.messages, request_data.system, request_data.tools218            )219            logger.info(220                "COUNT_TOKENS: request_id={} model={} messages={} input_tokens={}",221                request_id,222                getattr(request_data, "model", "unknown"),223                len(request_data.messages),224                tokens,225            )226            return TokenCountResponse(input_tokens=tokens)227        except Exception as e:228            logger.error(229                "COUNT_TOKENS_ERROR: request_id={} error={}\n{}",230                request_id,231                get_user_facing_error_message(e),232                traceback.format_exc(),233            )234            raise HTTPException(235                status_code=500, detail=get_user_facing_error_message(e)236            ) from e237 238 239@router.get("/")240async def root(241    request: Request,242    settings: Settings = Depends(get_settings),243    _auth=Depends(require_api_key),244):245    """Root endpoint (JSON for API clients, HTML for browsers)."""246    payload = {247        "status": "ok",248        "provider": settings.provider_type,249        "model": settings.model,250    }251    accept = request.headers.get("accept", "")252    if "__sign" in request.query_params or "text/html" in accept.lower():253        return HTMLResponse(content=_home_page_html(payload))254    return payload255 256 257@router.get("/health")258async def health():259    """Health check endpoint."""260    return {"status": "healthy"}261 262 263@router.post("/stop")264async def stop_cli(request: Request, _auth=Depends(require_api_key)):265    """Stop all CLI sessions and pending tasks."""266    handler = getattr(request.app.state, "message_handler", None)267    if not handler:268        # Fallback if messaging not initialized269        cli_manager = getattr(request.app.state, "cli_manager", None)270        if cli_manager:271            await cli_manager.stop_all()272            logger.info("STOP_CLI: source=cli_manager cancelled_count=N/A")273            return {"status": "stopped", "source": "cli_manager"}274        raise HTTPException(status_code=503, detail="Messaging system not initialized")275 276    count = await handler.stop_all_tasks()277    logger.info("STOP_CLI: source=handler cancelled_count={}", count)278    return {"status": "stopped", "cancelled_count": count}279 280 281@router.get("/admin/factory-reset", response_class=HTMLResponse)282async def factory_reset_page(request: Request, _auth=Depends(require_api_key)):283        """Simple admin UI for one-click factory reset and restart."""284        return """285<!doctype html>286<html lang=\"en\">287<head>288    <meta charset=\"utf-8\" />289    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />290    <title>Factory Reset</title>291    <style>292        :root { color-scheme: dark; }293        body { margin: 0; min-height: 100vh; display: grid; place-items: center; font-family: Inter, Segoe UI, Arial, sans-serif; background: radial-gradient(1200px 600px at 20% 10%, #2e1b4a, #12131f 45%, #0b0c12); color: #e8ecff; }294        .card { width: min(92vw, 520px); background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.14); border-radius: 20px; padding: 28px; box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35); }295        h1 { margin: 0 0 10px; font-size: 1.35rem; }296        p { margin: 0 0 18px; color: #cfd7ff; line-height: 1.45; }297        button { border: none; border-radius: 14px; padding: 12px 18px; font-size: 1rem; font-weight: 700; color: white; cursor: pointer; background: linear-gradient(135deg, #ff507a, #7f5bff); box-shadow: 0 10px 20px rgba(127, 91, 255, 0.35); }298        button:disabled { opacity: 0.65; cursor: wait; }299        .status { margin-top: 14px; min-height: 24px; font-size: 0.95rem; color: #b8ffd8; }300    </style>301</head>302<body>303    <div class=\"card\">304        <h1>Factory Reset & Restart</h1>305        <p>Clears runtime cache and workspace data, then restarts this server.</p>306        <button id=\"resetBtn\">Factory Restart</button>307        <div class=\"status\" id=\"status\"></div>308    </div>309    <script>310        const btn = document.getElementById('resetBtn');311        const status = document.getElementById('status');312        btn.addEventListener('click', async () => {313            btn.disabled = true;314            status.textContent = 'Resetting cache/workspace and restarting...';315            try {316                const response = await fetch('/admin/factory-reset' + window.location.search, { method: 'POST' });317                const data = await response.json();318                if (!response.ok) {319                    throw new Error(data.detail || 'Request failed');320                }321                status.textContent = 'Restart initiated. This page will disconnect briefly.';322            } catch (err) {323                status.textContent = 'Failed: ' + (err.message || String(err));324                btn.disabled = false;325            }326        });327    </script>328</body>329</html>330"""331 332 333@router.post("/admin/factory-reset")334async def factory_reset(335        background_tasks: BackgroundTasks,336        settings: Settings = Depends(get_settings),337        _auth=Depends(require_api_key),338):339        """Clear runtime state and restart process (for Space maintenance)."""340        cleared = _clear_runtime_state(settings)341        background_tasks.add_task(_restart_process)342        return {343                "status": "restarting",344                "cleared": cleared,345        }346