From ed4090a91000c9540482eba05e044286f183c836 Mon Sep 17 00:00:00 2001 From: "Gan, Jimmy" Date: Mon, 2 Mar 2026 01:08:12 +0800 Subject: [PATCH] feat: add LiteLLM gateway scaffold and dashboard health probe Introduce a localhost-only LiteLLM service template and expose a protected dashboard health endpoint for operational visibility, while adding cc-connect mobile bridge templates with conservative defaults. Co-Authored-By: Claude Opus 4.6 (1M context) --- cc-connect/README.md | 30 +++++++++++++++++++++ cc-connect/config.example.toml | 27 +++++++++++++++++++ cc-connect/docker-compose.yml | 22 +++++++++++++++ dashboard/backend/config.py | 3 +++ dashboard/backend/main.py | 4 ++- dashboard/backend/routers/litellm.py | 40 ++++++++++++++++++++++++++++ litellm/.env.example | 10 +++++++ litellm/config/litellm.yaml | 30 +++++++++++++++++++++ litellm/docker-compose.yml | 28 +++++++++++++++++++ 9 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 cc-connect/README.md create mode 100644 cc-connect/config.example.toml create mode 100644 cc-connect/docker-compose.yml create mode 100644 dashboard/backend/routers/litellm.py create mode 100644 litellm/.env.example create mode 100644 litellm/config/litellm.yaml create mode 100644 litellm/docker-compose.yml diff --git a/cc-connect/README.md b/cc-connect/README.md new file mode 100644 index 0000000..a07ae75 --- /dev/null +++ b/cc-connect/README.md @@ -0,0 +1,30 @@ +# cc-connect (Phase 1) + +Mobile bridge service for interacting with coding agents through chat, with LiteLLM as model gateway. + +## Scope +- LiteLLM remains the unified cloud-model endpoint. +- cc-connect remains the chat/mobile bridge. +- Start with a single platform (Discord) for lower operational risk. + +## Files +- `config.example.toml`: non-secret template. +- `docker-compose.yml`: optional always-on service shape. + +## Setup (NAS) +1. Copy template and fill secrets locally: + - `cp cc-connect/config.example.toml cc-connect/config.toml` + - create `cc-connect/.env` with platform token(s) +2. Keep secrets local only (`.env` is gitignored). +3. Start service: + - `docker compose -f cc-connect/docker-compose.yml up -d` + +## Security defaults +- Single platform enabled. +- Confirm/permission-gated command mode. +- Keep shell/file-write capabilities disabled initially. +- Restrict users/channels with explicit allowlists. + +## Notes +- This repo only includes templates and orchestration shape. +- Exact runtime keys/fields can vary by cc-connect release; adjust `config.toml` to your installed version. diff --git a/cc-connect/config.example.toml b/cc-connect/config.example.toml new file mode 100644 index 0000000..4754212 --- /dev/null +++ b/cc-connect/config.example.toml @@ -0,0 +1,27 @@ +# cc-connect config template +# Keep one platform enabled at a time for minimal attack surface. +# This template starts with Discord only. + +[service] +name = "nas-cc-connect" +log_level = "info" + +[bridge] +provider = "litellm" +base_url = "http://host.docker.internal:4005" +model = "claude-arch" +request_timeout_sec = 30 + +[platform.discord] +enabled = true +bot_token = "" +allowed_user_ids = [] +allowed_channel_ids = [] + +[security] +# Conservative defaults for initial rollout. +permission_mode = "confirm" +allow_yolo_mode = false +allow_shell = false +allow_file_write = false +max_session_minutes = 30 diff --git a/cc-connect/docker-compose.yml b/cc-connect/docker-compose.yml new file mode 100644 index 0000000..bf79065 --- /dev/null +++ b/cc-connect/docker-compose.yml @@ -0,0 +1,22 @@ +services: + cc-connect: + image: chenhg5/cc-connect:latest + container_name: cc-connect + restart: unless-stopped + labels: + - "com.centurylinklabs.watchtower.enable=true" + env_file: + - path: .env + required: false + volumes: + - ./config.toml:/app/config.toml:ro + command: + - "--config" + - "/app/config.toml" + extra_hosts: + - "host.docker.internal:host-gateway" + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/dashboard/backend/config.py b/dashboard/backend/config.py index 80aee94..8ce7d1f 100644 --- a/dashboard/backend/config.py +++ b/dashboard/backend/config.py @@ -44,6 +44,9 @@ CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "https://nas.jimmygan.com,https:// # Chat Summary CHAT_SUMMARY_DB = os.environ.get("CHAT_SUMMARY_DB", "/app/data/chat-summarizer/chat_summary.db") +# LiteLLM +LITELLM_URL = os.environ.get("LITELLM_URL", "http://127.0.0.1:4005") + # OpenClaw OPENCLAW_GATEWAY_TOKEN = os.environ.get("OPENCLAW_GATEWAY_TOKEN", "") diff --git a/dashboard/backend/main.py b/dashboard/backend/main.py index 569f389..924a929 100644 --- a/dashboard/backend/main.py +++ b/dashboard/backend/main.py @@ -6,7 +6,7 @@ from fastapi.responses import JSONResponse from slowapi import Limiter from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded -from routers import docker_router, gitea, files, terminal, system, auth, chat_summary, security, passkey, totp +from routers import docker_router, gitea, files, terminal, system, auth, chat_summary, security, passkey, totp, litellm import auth as auth_module from rbac import require_page, require_write @@ -179,6 +179,8 @@ app.include_router(files.router, prefix="/api/files", dependencies=[Depends(_inject_user), Depends(require_page("files")), Depends(require_write())]) app.include_router(system.router, prefix="/api/system", dependencies=[Depends(_inject_user), Depends(require_page("dashboard"))]) +app.include_router(litellm.router, prefix="/api/litellm", + dependencies=[Depends(_inject_user), Depends(require_page("dashboard"))]) app.include_router(chat_summary.router, prefix="/api/chat-summary", dependencies=[Depends(_inject_user), Depends(require_page("chat-digest"))]) app.include_router(security.router, prefix="/api/security", diff --git a/dashboard/backend/routers/litellm.py b/dashboard/backend/routers/litellm.py new file mode 100644 index 0000000..8ec5e72 --- /dev/null +++ b/dashboard/backend/routers/litellm.py @@ -0,0 +1,40 @@ +import time +import httpx +from fastapi import APIRouter +from config import LITELLM_URL + +router = APIRouter() + + +@router.get("/health") +async def litellm_health(): + timeout = httpx.Timeout(2.0) + checks = ("/health", "/v1/models") + + last_error = None + async with httpx.AsyncClient(timeout=timeout) as client: + for path in checks: + url = f"{LITELLM_URL.rstrip('/')}{path}" + started = time.perf_counter() + try: + response = await client.get(url) + latency_ms = round((time.perf_counter() - started) * 1000, 1) + if response.is_success: + return { + "ok": True, + "status": "up", + "latency_ms": latency_ms, + "endpoint": path, + "http_status": response.status_code, + } + last_error = f"{path} returned {response.status_code}" + except Exception as exc: + last_error = str(exc) + + return { + "ok": False, + "status": "down", + "latency_ms": None, + "endpoint": None, + "error": last_error or "health check failed", + } diff --git a/litellm/.env.example b/litellm/.env.example new file mode 100644 index 0000000..e6b691c --- /dev/null +++ b/litellm/.env.example @@ -0,0 +1,10 @@ +# Copy to .env on NAS and fill real values. +ANTHROPIC_API_KEY= +OPENAI_API_KEY= +GEMINI_API_KEY= + +# Optional gateway auth key for internal clients. +LITELLM_MASTER_KEY= + +# Host bind port for docker-compose mapping. +LITELLM_PORT=4005 diff --git a/litellm/config/litellm.yaml b/litellm/config/litellm.yaml new file mode 100644 index 0000000..5fa95c4 --- /dev/null +++ b/litellm/config/litellm.yaml @@ -0,0 +1,30 @@ +model_list: + - model_name: claude-arch + litellm_params: + model: anthropic/claude-sonnet-4-6 + api_key: os.environ/ANTHROPIC_API_KEY + timeout: 30 + max_retries: 1 + + - model_name: codex-exec + litellm_params: + model: openai/gpt-4.1 + api_key: os.environ/OPENAI_API_KEY + timeout: 30 + max_retries: 1 + + - model_name: gemini-large + litellm_params: + model: gemini/gemini-1.5-pro + api_key: os.environ/GEMINI_API_KEY + timeout: 30 + max_retries: 1 + +general_settings: + # Optional - leave empty in .env to disable gateway auth. + master_key: os.environ/LITELLM_MASTER_KEY + +router_settings: + timeout: 30 + num_retries: 1 + retry_after: 1 diff --git a/litellm/docker-compose.yml b/litellm/docker-compose.yml new file mode 100644 index 0000000..693c366 --- /dev/null +++ b/litellm/docker-compose.yml @@ -0,0 +1,28 @@ +services: + litellm: + image: ghcr.io/berriai/litellm:main-latest + container_name: litellm + restart: unless-stopped + labels: + - "com.centurylinklabs.watchtower.enable=true" + env_file: + - path: .env + required: false + environment: + - LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY:-} + ports: + - "127.0.0.1:${LITELLM_PORT:-4005}:4005" + volumes: + - ./config:/config:ro + command: + - "--config" + - "/config/litellm.yaml" + - "--host" + - "0.0.0.0" + - "--port" + - "4005" + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3"