Spaces:
Sleeping
Sleeping
| 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)) | |
| async def root(): | |
| return HTMLResponse(get_dashboard_html()) | |
| 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", "")), | |
| } | |
| 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 | |
| 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"} | |
| 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)} | |
| 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"""<!DOCTYPE html> | |
| <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Ciel Proxy Router</title> | |
| <style> | |
| *{{margin:0;padding:0;box-sizing:border-box}} | |
| body{{background:#0d1117;color:#c9d1d9;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;min-height:100vh;padding:20px}} | |
| .container{{max-width:700px;margin:0 auto}} | |
| h1{{color:#58a6ff;font-size:28px;margin-bottom:4px}} | |
| .sub{{color:#8b949e;font-size:14px;margin-bottom:30px}} | |
| .status-bar{{display:flex;align-items:center;gap:10px;background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px 18px;margin-bottom:24px}} | |
| .dot{{width:12px;height:12px;border-radius:50%;background:#3fb950;animation:pulse 2s infinite;flex-shrink:0}} | |
| @keyframes pulse{{0%,100%{{opacity:1}}50%{{opacity:.5}}}} | |
| .status-text{{font-size:15px;font-weight:500}} | |
| .card{{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:28px;margin-bottom:20px}} | |
| .card h2{{color:#f0f6fc;font-size:18px;margin-bottom:20px;display:flex;align-items:center;gap:8px}} | |
| .field{{margin-bottom:20px}} | |
| .field label{{display:block;color:#8b949e;font-size:13px;margin-bottom:6px;font-weight:500}} | |
| .field input{{width:100%;background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:12px 14px;color:#c9d1d9;font-size:14px;font-family:monospace;transition:border-color .2s}} | |
| .field input:focus{{outline:none;border-color:#58a6ff;box-shadow:0 0 0 3px rgba(88,166,255,.15)}} | |
| .field input::placeholder{{color:#484f58}} | |
| .field .hint{{color:#484f58;font-size:11px;margin-top:4px}} | |
| .field .current{{color:#79c0ff;font-size:12px;margin-top:4px;font-family:monospace}} | |
| .btn-row{{display:flex;gap:12px;margin-top:24px}} | |
| .btn{{padding:12px 24px;border-radius:8px;border:none;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s}} | |
| .btn-primary{{background:#238636;color:#fff}} | |
| .btn-primary:hover{{background:#2ea043}} | |
| .btn-test{{background:#1f6feb;color:#fff}} | |
| .btn-test:hover{{background:#388bfd}} | |
| .btn-save{{background:#30363d;color:#c9d1d9;border:1px solid #484f58}} | |
| .btn-save:hover{{background:#3d444d}} | |
| .toast{{position:fixed;top:20px;right:20px;padding:14px 20px;border-radius:8px;font-size:14px;font-weight:500;opacity:0;transform:translateY(-10px);transition:all .3s;z-index:999}} | |
| .toast.show{{opacity:1;transform:translateY(0)}} | |
| .toast.success{{background:#238636;color:#fff}} | |
| .toast.error{{background:#da3633;color:#fff}} | |
| .usage{{background:#0d1117;border:1px solid #30363d;border-radius:8px;padding:16px;margin-top:16px}} | |
| .usage p{{color:#8b949e;font-size:12px;margin-bottom:8px}} | |
| .usage code{{display:block;color:#79c0ff;background:#1f2937;padding:8px 12px;border-radius:6px;font-size:12px;margin:4px 0;word-break:break-all}} | |
| .footer{{color:#484f58;font-size:11px;margin-top:30px;text-align:center}} | |
| </style></head> | |
| <body> | |
| <div class="container"> | |
| <h1>⚡ Ciel Proxy Router</h1> | |
| <p class="sub">Transparent proxy for Claude Code CLI</p> | |
| <div class="status-bar"> | |
| <span class="dot"></span> | |
| <span class="status-text">Online — Proxy Active</span> | |
| </div> | |
| <div class="card"> | |
| <h2>🔧 Configuration</h2> | |
| <div class="field"> | |
| <label>Upstream URL</label> | |
| <input type="text" id="upstream" value="{upstream_display}" placeholder="https://agentrouter.org"> | |
| <div class="hint">Backend API server URL (default: agentrouter.org)</div> | |
| </div> | |
| <div class="field"> | |
| <label>API Key</label> | |
| <input type="password" id="apikey" value="" placeholder="sk-... (paste your key)"> | |
| <div class="current">Current: {api_key_display or 'Not set'}</div> | |
| </div> | |
| <div class="field"> | |
| <label>Proxy URL (Residential Proxy)</label> | |
| <input type="text" id="proxy" value="{proxy_display if proxy_display != 'Not set' else ''}" placeholder="http://user:pass@ip:port"> | |
| <div class="hint">Format: http://username:password@ip:port</div> | |
| <div class="current">Current: {proxy_display}</div> | |
| </div> | |
| <div class="btn-row"> | |
| <button class="btn btn-primary" onclick="saveConfig()">💾 Save Configuration</button> | |
| <button class="btn btn-test" onclick="testConnection()">🔌 Test Connection</button> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>📋 Usage in Claude Code</h2> | |
| <div class="usage"> | |
| <p>Set these environment variables before running Claude Code:</p> | |
| <code>ANTHROPIC_BASE_URL=https://samiran757-ciel-proxy-router.hf.space</code> | |
| <code>ANTHROPIC_API_KEY=your-agentrouter-key</code> | |
| </div> | |
| </div> | |
| <p class="footer">Built by Ciel • Naruto859</p> | |
| </div> | |
| <div class="toast" id="toast"></div> | |
| <script> | |
| function showToast(msg, type) {{ | |
| const t = document.getElementById('toast'); | |
| t.textContent = msg; | |
| t.className = 'toast show ' + type; | |
| setTimeout(() => t.className = 'toast', 3000); | |
| }} | |
| async function saveConfig() {{ | |
| const data = {{}}; | |
| const upstream = document.getElementById('upstream').value.trim(); | |
| const apikey = document.getElementById('apikey').value.trim(); | |
| const proxy = document.getElementById('proxy').value.trim(); | |
| if (upstream) data.upstream_url = upstream; | |
| if (apikey) data.api_key = apikey; | |
| if (proxy) data.proxy_url = proxy; | |
| try {{ | |
| const resp = await fetch('/api/config', {{ | |
| method: 'POST', | |
| headers: {{'Content-Type': 'application/json'}}, | |
| body: JSON.stringify(data) | |
| }}); | |
| const result = await resp.json(); | |
| if (result.status === 'saved') {{ | |
| showToast('✅ Configuration saved!', 'success'); | |
| document.getElementById('apikey').value = ''; | |
| }} else {{ | |
| showToast('❌ ' + result.message, 'error'); | |
| }} | |
| }} catch(e) {{ | |
| showToast('❌ Save failed: ' + e.message, 'error'); | |
| }} | |
| }} | |
| async function testConnection() {{ | |
| showToast('🔄 Testing connection...', 'success'); | |
| try {{ | |
| const resp = await fetch('/api/test', {{method: 'POST'}}); | |
| const result = await resp.json(); | |
| if (result.status === 'success') {{ | |
| showToast('✅ ' + result.message + ' (HTTP ' + result.status_code + ')', 'success'); | |
| }} else {{ | |
| showToast('⚠️ ' + result.message, 'error'); | |
| }} | |
| }} catch(e) {{ | |
| showToast('❌ Test failed: ' + e.message, 'error'); | |
| }} | |
| }} | |
| </script> | |
| </body></html>""" | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |