| """ |
| Dual-Protocol API Server |
| ======================== |
| Supports BOTH: |
| β’ OpenAI Python library β POST /v1/chat/completions |
| β’ Claude Code / Anthropic SDK β POST /v1/messages |
| |
| Usage as base URL |
| ----------------- |
| OpenAI Python: |
| client = OpenAI(base_url="http://localhost:7860/v1", api_key="any") |
| |
| Anthropic SDK / Claude Code: |
| export ANTHROPIC_BASE_URL=http://localhost:7860 |
| export ANTHROPIC_API_KEY=any-value |
| claude # or use the Python SDK with base_url=... |
| """ |
|
|
| from fastapi import FastAPI, Request |
| from fastapi.responses import StreamingResponse, JSONResponse |
| from fastapi.middleware.cors import CORSMiddleware |
|
|
| import requests |
| import json |
| import uvicorn |
| import uuid |
| import traceback |
| import time |
|
|
| app = FastAPI(title="Dual-Protocol LLM Proxy") |
|
|
| |
| |
| |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| |
| |
|
|
| HF_SPACE_URL = "https://akhaliq-ling-2-6-1t.hf.space/stream_chat" |
| HF_SPACE_ORIGIN = "https://akhaliq-ling-2-6-1t.hf.space" |
| MODEL_NAME = "ling-2.6-1t" |
|
|
| |
| UPSTREAM_RETRIES = 3 |
| |
| UPSTREAM_CONNECT_TIMEOUT = 30 |
| |
| UPSTREAM_READ_TIMEOUT = 300 |
|
|
| UPSTREAM_HEADERS = { |
| "accept": "*/*", |
| "content-type": "application/json", |
| "origin": HF_SPACE_ORIGIN, |
| "referer": HF_SPACE_ORIGIN + "/", |
| "user-agent": "Mozilla/5.0", |
| } |
|
|
| |
| |
| |
|
|
| def normalize_content(content) -> str: |
| """ |
| Accept any of: |
| - plain string |
| - OpenAI/Anthropic multimodal list [{"type": "text", "text": "..."}, ...] |
| - None |
| Returns a plain string. |
| """ |
| if isinstance(content, list): |
| parts = [] |
| for item in content: |
| if isinstance(item, dict): |
| t = item.get("type", "") |
| if t in ("text", "input_text"): |
| parts.append(item.get("text", "")) |
| |
| elif isinstance(item, str): |
| parts.append(item) |
| return "\n".join(parts) |
| if isinstance(content, str): |
| return content |
| if content is None: |
| return "" |
| return str(content) |
|
|
|
|
| |
| |
| |
|
|
| def warmup_upstream() -> dict: |
| """ |
| Send a tiny probe request to wake the HF Space from sleep. |
| Returns {"ok": True, "elapsed": N} or {"ok": False, "error": "..."}. |
| """ |
| probe = { |
| "messages": [{"role": "user", "content": "hi"}], |
| "system_prompt": "", |
| } |
| t0 = time.time() |
| try: |
| r = requests.post( |
| HF_SPACE_URL, |
| headers=UPSTREAM_HEADERS, |
| json=probe, |
| stream=True, |
| timeout=(UPSTREAM_CONNECT_TIMEOUT, UPSTREAM_READ_TIMEOUT), |
| ) |
| |
| for _ in r.iter_lines(): |
| break |
| return {"ok": True, "elapsed": round(time.time() - t0, 2)} |
| except Exception as e: |
| return {"ok": False, "error": str(e), "elapsed": round(time.time() - t0, 2)} |
|
|
|
|
| def call_upstream(system_prompt: str, messages: list, stream: bool): |
| """ |
| Forward to the HF-space backend with automatic retry on timeout / 5xx. |
| Returns a requests.Response object (always opened in stream mode). |
| Raises RuntimeError if all retries are exhausted. |
| """ |
| payload = { |
| "messages": messages, |
| "system_prompt": system_prompt, |
| } |
| last_exc = None |
| for attempt in range(1, UPSTREAM_RETRIES + 1): |
| try: |
| resp = requests.post( |
| HF_SPACE_URL, |
| headers=UPSTREAM_HEADERS, |
| json=payload, |
| stream=True, |
| timeout=(UPSTREAM_CONNECT_TIMEOUT, UPSTREAM_READ_TIMEOUT), |
| ) |
| if resp.status_code >= 500: |
| raise RuntimeError(f"upstream returned {resp.status_code}") |
| return resp |
| except (requests.exceptions.Timeout, |
| requests.exceptions.ConnectionError, |
| RuntimeError) as e: |
| last_exc = e |
| wait = 2 ** (attempt - 1) |
| print(f"[upstream] attempt {attempt}/{UPSTREAM_RETRIES} failed: {e} β retry in {wait}s") |
| time.sleep(wait) |
|
|
| raise RuntimeError(f"upstream unreachable after {UPSTREAM_RETRIES} attempts: {last_exc}") |
|
|
|
|
| def collect_full_text(response) -> str: |
| """Drain an upstream streaming response and return the concatenated text.""" |
| full = "" |
| for line in response.iter_lines(): |
| if not line: |
| continue |
| try: |
| decoded = line.decode("utf-8") |
| if decoded.startswith("data:"): |
| data_str = decoded[len("data:"):].strip() |
| if data_str == "[DONE]": |
| break |
| parsed = json.loads(data_str) |
| full += parsed.get("token", "") |
| except Exception: |
| continue |
| return full |
|
|
|
|
| |
| |
| |
|
|
| def convert_openai_messages(messages: list): |
| """ |
| Split OpenAI-style messages (which may include role='system' anywhere) |
| into (system_prompt_str, user_assistant_messages). |
| """ |
| system_parts = [] |
| converted = [] |
| for msg in messages: |
| role = msg.get("role", "user") |
| content = normalize_content(msg.get("content", "")) |
| if role in ("system", "developer"): |
| system_parts.append(content) |
| else: |
| converted.append({"role": role, "content": content}) |
| return "\n".join(system_parts), converted |
|
|
|
|
| |
| |
| |
|
|
| @app.get("/") |
| async def root(): |
| return { |
| "status": "ok", |
| "message": "Dual-Protocol API Server (OpenAI + Anthropic/Claude-Code)", |
| "model": MODEL_NAME, |
| "endpoints": [ |
| "POST /v1/chat/completions β OpenAI Python library", |
| "POST /v1/messages β Anthropic SDK / Claude Code", |
| ], |
| } |
|
|
|
|
| @app.get("/health") |
| async def health(): |
| return {"status": "healthy"} |
|
|
|
|
| @app.get("/warmup") |
| @app.post("/warmup") |
| async def warmup(): |
| """ |
| Wake the upstream HF Space from sleep. |
| Call this once before running tests or the first real request. |
| Returns when the upstream has responded to a probe message. |
| """ |
| result = warmup_upstream() |
| status = 200 if result["ok"] else 503 |
| return JSONResponse(content={ |
| "upstream": HF_SPACE_URL, |
| **result, |
| }, status_code=status) |
|
|
|
|
| @app.get("/upstream-status") |
| async def upstream_status(): |
| """Quick reachability check for the upstream HF Space (no LLM call).""" |
| try: |
| r = requests.get(HF_SPACE_ORIGIN, timeout=(10, 10), allow_redirects=True) |
| return {"reachable": True, "http_status": r.status_code, "url": HF_SPACE_ORIGIN} |
| except Exception as e: |
| return JSONResponse( |
| content={"reachable": False, "error": str(e), "url": HF_SPACE_ORIGIN}, |
| status_code=503, |
| ) |
|
|
|
|
| |
| @app.get("/v1/models") |
| async def openai_models(): |
| return { |
| "object": "list", |
| "data": [{ |
| "id": MODEL_NAME, |
| "object": "model", |
| "created": int(time.time()), |
| "owned_by": "custom", |
| }], |
| } |
|
|
|
|
| |
| @app.get("/v1/models", include_in_schema=False) |
| async def _noop(): pass |
|
|
|
|
| |
| |
| |
|
|
| @app.post("/v1/chat/completions") |
| async def chat_completions(request: Request): |
| try: |
| body = await request.json() |
| messages = body.get("messages", []) |
| stream = body.get("stream", False) |
|
|
| system_prompt, converted_messages = convert_openai_messages(messages) |
|
|
| upstream = call_upstream(system_prompt, converted_messages, stream) |
|
|
| |
| if stream: |
| async def openai_stream(): |
| cid = f"chatcmpl-{uuid.uuid4().hex}" |
| for line in upstream.iter_lines(): |
| if not line: |
| continue |
| try: |
| decoded = line.decode("utf-8") |
| if not decoded.startswith("data:"): |
| continue |
| data_str = decoded[len("data:"):].strip() |
| if data_str == "[DONE]": |
| yield "data: [DONE]\n\n" |
| break |
| parsed = json.loads(data_str) |
| token = parsed.get("token", "") |
| chunk = { |
| "id": cid, |
| "object": "chat.completion.chunk", |
| "created": int(time.time()), |
| "model": MODEL_NAME, |
| "choices": [{ |
| "index": 0, |
| "delta": {"role": "assistant", "content": token}, |
| "finish_reason": None, |
| }], |
| } |
| yield f"data: {json.dumps(chunk)}\n\n" |
| except Exception: |
| continue |
|
|
| |
| cid = f"chatcmpl-{uuid.uuid4().hex}" |
| final_chunk = { |
| "id": cid, |
| "object": "chat.completion.chunk", |
| "created": int(time.time()), |
| "model": MODEL_NAME, |
| "choices": [{ |
| "index": 0, |
| "delta": {}, |
| "finish_reason": "stop", |
| }], |
| } |
| yield f"data: {json.dumps(final_chunk)}\n\n" |
|
|
| return StreamingResponse(openai_stream(), media_type="text/event-stream") |
|
|
| |
| full_text = collect_full_text(upstream) |
| return JSONResponse({ |
| "id": f"chatcmpl-{uuid.uuid4().hex}", |
| "object": "chat.completion", |
| "created": int(time.time()), |
| "model": MODEL_NAME, |
| "choices": [{ |
| "index": 0, |
| "message": {"role": "assistant", "content": full_text}, |
| "finish_reason": "stop", |
| }], |
| "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}, |
| }) |
|
|
| except Exception as e: |
| traceback.print_exc() |
| return JSONResponse( |
| status_code=500, |
| content={"error": {"message": str(e), "type": "server_error"}}, |
| ) |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| def _sse_event(event_name: str, data: dict) -> str: |
| """Format a single SSE event in Anthropic's named-event style.""" |
| return f"event: {event_name}\ndata: {json.dumps(data)}\n\n" |
|
|
|
|
| @app.post("/v1/messages") |
| async def anthropic_messages(request: Request): |
| try: |
| body = await request.json() |
| stream = body.get("stream", False) |
| |
| system_raw = body.get("system", "") |
| system_prompt = normalize_content(system_raw) |
| messages = body.get("messages", []) |
|
|
| |
| converted_messages = [ |
| {"role": m.get("role", "user"), "content": normalize_content(m.get("content", ""))} |
| for m in messages |
| if m.get("role") not in ("system",) |
| ] |
|
|
| msg_id = f"msg_{uuid.uuid4().hex[:24]}" |
| upstream = call_upstream(system_prompt, converted_messages, stream) |
|
|
| |
| if stream: |
| async def anthropic_stream(): |
| |
| yield _sse_event("message_start", { |
| "type": "message_start", |
| "message": { |
| "id": msg_id, |
| "type": "message", |
| "role": "assistant", |
| "content": [], |
| "model": MODEL_NAME, |
| "stop_reason": None, |
| "stop_sequence": None, |
| "usage": {"input_tokens": 0, "output_tokens": 1}, |
| }, |
| }) |
|
|
| |
| yield _sse_event("content_block_start", { |
| "type": "content_block_start", |
| "index": 0, |
| "content_block": {"type": "text", "text": ""}, |
| }) |
|
|
| |
| yield _sse_event("ping", {"type": "ping"}) |
|
|
| |
| output_tokens = 0 |
| for line in upstream.iter_lines(): |
| if not line: |
| continue |
| try: |
| decoded = line.decode("utf-8") |
| if not decoded.startswith("data:"): |
| continue |
| data_str = decoded[len("data:"):].strip() |
| if data_str == "[DONE]": |
| break |
| parsed = json.loads(data_str) |
| token = parsed.get("token", "") |
| if not token: |
| continue |
| output_tokens += len(token.split()) |
| yield _sse_event("content_block_delta", { |
| "type": "content_block_delta", |
| "index": 0, |
| "delta": {"type": "text_delta", "text": token}, |
| }) |
| except Exception: |
| continue |
|
|
| |
| yield _sse_event("content_block_stop", { |
| "type": "content_block_stop", |
| "index": 0, |
| }) |
|
|
| |
| yield _sse_event("message_delta", { |
| "type": "message_delta", |
| "delta": {"stop_reason": "end_turn", "stop_sequence": None}, |
| "usage": {"output_tokens": output_tokens}, |
| }) |
|
|
| |
| yield _sse_event("message_stop", {"type": "message_stop"}) |
|
|
| return StreamingResponse( |
| anthropic_stream(), |
| media_type="text/event-stream", |
| headers={ |
| |
| "anthropic-version": "2023-06-01", |
| "x-request-id": msg_id, |
| }, |
| ) |
|
|
| |
| full_text = collect_full_text(upstream) |
| return JSONResponse( |
| content={ |
| "id": msg_id, |
| "type": "message", |
| "role": "assistant", |
| "content": [{"type": "text", "text": full_text}], |
| "model": MODEL_NAME, |
| "stop_reason": "end_turn", |
| "stop_sequence": None, |
| "usage": {"input_tokens": 0, "output_tokens": 0}, |
| }, |
| headers={ |
| "anthropic-version": "2023-06-01", |
| "x-request-id": msg_id, |
| }, |
| ) |
|
|
| except Exception as e: |
| traceback.print_exc() |
| |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "type": "error", |
| "error": {"type": "api_error", "message": str(e)}, |
| }, |
| ) |
|
|
|
|
| |
| |
| |
| |
|
|
| @app.post("/v1/responses") |
| async def responses_api(request: Request): |
| try: |
| body = await request.json() |
| stream = body.get("stream", False) |
| input_data = body.get("input", "") |
|
|
| messages = [] |
| if isinstance(input_data, list): |
| for item in input_data: |
| role = item.get("role", "user") |
| content = normalize_content(item.get("content", "")) |
| messages.append({"role": role, "content": content}) |
| else: |
| messages.append({"role": "user", "content": str(input_data)}) |
|
|
| |
| class _FakeRequest: |
| async def json(self): |
| return {"messages": messages, "stream": stream} |
|
|
| return await chat_completions(_FakeRequest()) |
|
|
| except Exception as e: |
| traceback.print_exc() |
| return JSONResponse( |
| status_code=500, |
| content={"error": {"message": str(e), "type": "server_error"}}, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| uvicorn.run(app, host="0.0.0.0", port=7860) |