import os import httpx import asyncio import json import time from pathlib import Path from fastapi import FastAPI, Request, HTTPException from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse app = FastAPI(title="Ciel Proxy Router") CONFIG_FILE = Path("/tmp/proxy_config.json") def load_config(): if CONFIG_FILE.exists(): with open(CONFIG_FILE) as f: return json.load(f) return { "upstream_url": os.getenv("UPSTREAM_URL", "https://agentrouter.org"), "api_key": os.getenv("API_KEY", ""), "proxy_url": os.getenv("PROXY_URL", ""), } def save_config(cfg): with open(CONFIG_FILE, "w") as f: json.dump(cfg, f, indent=2) config = load_config() async def get_proxy_client(): proxy = config.get("proxy_url", "") if proxy: return httpx.AsyncClient(proxy=proxy, timeout=httpx.Timeout(300.0, connect=30.0)) return httpx.AsyncClient(timeout=httpx.Timeout(300.0, connect=30.0)) @app.get("/") async def root(): return HTMLResponse(get_dashboard_html()) @app.get("/health") async def health(): return { "status": "ok", "upstream": config.get("upstream_url", ""), "proxy": bool(config.get("proxy_url", "")), "api_key_set": bool(config.get("api_key", "")), } @app.get("/api/config") async def get_config(): cfg = config.copy() if cfg["api_key"]: cfg["api_key_masked"] = cfg["api_key"][:8] + "..." + cfg["api_key"][-4:] else: cfg["api_key_masked"] = "" return cfg @app.post("/api/config") async def update_config(request: Request): global config body = await request.json() if "upstream_url" in body: config["upstream_url"] = body["upstream_url"].strip() if "api_key" in body: config["api_key"] = body["api_key"].strip() if "proxy_url" in body: config["proxy_url"] = body["proxy_url"].strip() save_config(config) return {"status": "saved", "message": "Configuration updated successfully"} @app.post("/api/test") async def test_connection(): proxy = config.get("proxy_url", "") upstream = config.get("upstream_url", "https://agentrouter.org") api_key = config.get("api_key", "") try: client = await get_proxy_client() headers = {} if api_key: headers["authorization"] = f"Bearer {api_key}" headers["content-type"] = "application/json" resp = await client.post( f"{upstream}/v1/messages", headers=headers, json={ "model": "claude-sonnet-4-20250514", "max_tokens": 10, "messages": [{"role": "user", "content": "hi"}] }, timeout=30.0 ) await client.aclose() return { "status": "success" if resp.status_code < 500 else "error", "status_code": resp.status_code, "message": "Connection working!" if resp.status_code in [200, 401, 403] else f"Error: {resp.status_code}" } except Exception as e: return {"status": "error", "message": str(e)} @app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) async def proxy_request(path: str, request: Request): body = await request.body() headers = dict(request.headers) headers.pop("host", None) headers.pop("content-length", None) api_key = config.get("api_key", "") if api_key: headers["authorization"] = f"Bearer {api_key}" upstream = config.get("upstream_url", "https://agentrouter.org") target_url = f"{upstream}/v1/{path}" client = await get_proxy_client() try: req = client.build_request( method=request.method, url=target_url, headers=headers, content=body, ) resp = await client.send(req, stream=True) resp_headers = dict(resp.headers) resp_headers.pop("content-encoding", None) resp_headers.pop("transfer-encoding", None) resp_headers.pop("content-length", None) async def stream(): async for chunk in resp.aiter_bytes(): yield chunk await resp.aclose() await client.aclose() return StreamingResponse(stream(), status_code=resp.status_code, headers=resp_headers) except Exception as e: await client.aclose() return JSONResponse({"error": str(e)}, status_code=502) def get_dashboard_html(): cfg = config api_key_display = "" if cfg.get("api_key"): k = cfg["api_key"] api_key_display = k[:8] + "..." + k[-4:] if len(k) > 12 else "***" proxy_display = cfg.get("proxy_url", "") or "Not set" upstream_display = cfg.get("upstream_url", "") or "https://agentrouter.org" return f"""
Transparent proxy for Claude Code CLI
Set these environment variables before running Claude Code:
ANTHROPIC_BASE_URL=https://samiran757-ciel-proxy-router.hf.space
ANTHROPIC_API_KEY=your-agentrouter-key