Team Ai
Apppublic

ayoub5550/ClaudeCode

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
proxy.py432 linesDownload Raw Back to root
1#!/usr/bin/env python32"""3Anthropic → OpenAI API Translator Proxy4Accepts Anthropic /v1/messages format, translates to OpenAI /v1/chat/completions,5routes through Cloudflare proxy to NVIDIA API, translates response back.6 7Handles: text, tool_use, tool_result, streaming SSE8"""9 10import os, json, logging, uuid, time, traceback11from aiohttp import web, ClientSession, ClientTimeout12 13log = logging.getLogger("anthropic-proxy")14logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")15 16NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY", "")17MODEL = os.environ.get("MODEL", "z-ai/glm-5.1")18PROXY_URL = os.environ.get("PROXY_URL", "")19PROXY_SECRET = os.environ.get("PROXY_SECRET", "")20TARGET_URL = f"{PROXY_URL}/v1/chat/completions" if PROXY_URL else "https://integrate.api.nvidia.com/v1/chat/completions"21 22REQUEST_COUNT = 023 24 25def anthropic_tools_to_openai(tools: list) -> list:26    """Convert Anthropic tool definitions to OpenAI function tools."""27    result = []28    for t in tools:29        result.append({30            "type": "function",31            "function": {32                "name": t.get("name", ""),33                "description": t.get("description", ""),34                "parameters": t.get("input_schema", t.get("parameters", {}))35            }36        })37    return result38 39 40def anthropic_messages_to_openai(messages: list) -> list:41    """Convert Anthropic message array to OpenAI message array."""42    result = []43    for msg in messages:44        role = msg.get("role", "user")45        content = msg.get("content", "")46 47        if isinstance(content, str):48            result.append({"role": role, "content": content})49            continue50 51        if not isinstance(content, list):52            result.append({"role": role, "content": str(content)})53            continue54 55        # Content is a list of blocks56        text_parts = []57        tool_calls = []58        tool_results = []59 60        for block in content:61            btype = block.get("type", "")62 63            if btype == "text":64                text_parts.append(block.get("text", ""))65 66            elif btype == "tool_use":67                tool_calls.append({68                    "id": block.get("id", f"call_{uuid.uuid4().hex[:8]}"),69                    "type": "function",70                    "function": {71                        "name": block.get("name", ""),72                        "arguments": json.dumps(block.get("input", {}), ensure_ascii=False)73                    }74                })75 76            elif btype == "tool_result":77                # Flatten tool result content78                tc_content = block.get("content", "")79                if isinstance(tc_content, list):80                    tc_text = "\n".join(81                        b.get("text", "") for b in tc_content if b.get("type") == "text"82                    ) or json.dumps(tc_content, ensure_ascii=False)83                else:84                    tc_text = str(tc_content)85 86                tool_results.append({87                    "role": "tool",88                    "tool_call_id": block.get("tool_use_id", ""),89                    "content": tc_text90                })91 92        # Build messages from blocks93        if role == "assistant":94            m = {"role": "assistant", "content": "\n".join(text_parts) if text_parts else ""}95            if tool_calls:96                m["tool_calls"] = tool_calls97                if not m["content"]:98                    m["content"] = None99            result.append(m)100        elif tool_results:101            # Tool results become separate tool messages102            if text_parts:103                result.append({"role": "user", "content": "\n".join(text_parts)})104            result.extend(tool_results)105        else:106            result.append({"role": role, "content": "\n".join(text_parts) or ""})107 108    return result109 110 111def anthropic_to_openai_request(body: dict) -> dict:112    """Convert full Anthropic /v1/messages request to OpenAI /v1/chat/completions."""113    messages = []114 115    # System message116    system = body.get("system", "")117    if isinstance(system, str) and system:118        messages.append({"role": "system", "content": system})119    elif isinstance(system, list):120        text = "\n".join(b.get("text", "") for b in system if b.get("type") == "text")121        if text:122            messages.append({"role": "system", "content": text})123 124    # Convert messages125    messages.extend(anthropic_messages_to_openai(body.get("messages", [])))126 127    oai = {128        "model": MODEL,129        "messages": messages,130        "max_tokens": body.get("max_tokens", 4096),131        "stream": body.get("stream", False),132    }133 134    if body.get("temperature") is not None:135        oai["temperature"] = body["temperature"]136    if body.get("top_p") is not None:137        oai["top_p"] = body["top_p"]138 139    # Tools140    tools = body.get("tools", [])141    if tools:142        oai["tools"] = anthropic_tools_to_openai(tools)143        oai["tool_choice"] = "auto"144 145    return oai146 147 148def openai_to_anthropic_response(oai: dict, model_name: str) -> dict:149    """Convert OpenAI chat completion to Anthropic messages response."""150    choices = oai.get("choices", [])151    if not choices:152        return {153            "id": f"msg_{uuid.uuid4().hex[:24]}",154            "type": "message", "role": "assistant",155            "content": [{"type": "text", "text": ""}],156            "model": model_name,157            "stop_reason": "end_turn",158            "stop_sequence": None,159            "usage": {"input_tokens": 0, "output_tokens": 0}160        }161 162    message = choices[0].get("message", {})163    content = []164 165    text = message.get("content", "")166    if text:167        content.append({"type": "text", "text": text})168 169    for tc in message.get("tool_calls", []):170        fn = tc.get("function", {})171        try:172            inp = json.loads(fn.get("arguments", "{}"))173        except json.JSONDecodeError:174            inp = {"raw": fn.get("arguments", "")}175        content.append({176            "type": "tool_use",177            "id": tc.get("id", f"toolu_{uuid.uuid4().hex[:12]}"),178            "name": fn.get("name", ""),179            "input": inp180        })181 182    if not content:183        content.append({"type": "text", "text": ""})184 185    finish = choices[0].get("finish_reason", "stop")186    stop_map = {"stop": "end_turn", "tool_calls": "tool_use", "length": "max_tokens"}187    usage = oai.get("usage", {})188 189    return {190        "id": f"msg_{uuid.uuid4().hex[:24]}",191        "type": "message",192        "role": "assistant",193        "content": content,194        "model": model_name,195        "stop_reason": stop_map.get(finish, "end_turn"),196        "stop_sequence": None,197        "usage": {198            "input_tokens": usage.get("prompt_tokens", 0),199            "output_tokens": usage.get("completion_tokens", 0),200        }201    }202 203 204async def sse_write(resp: web.StreamResponse, event: str, data: dict):205    """Write one SSE event."""206    payload = f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"207    await resp.write(payload.encode("utf-8"))208 209 210async def handle_messages(request: web.Request) -> web.StreamResponse:211    """Handle POST /v1/messages — Anthropic API format."""212    global REQUEST_COUNT213    REQUEST_COUNT += 1214    req_id = REQUEST_COUNT215 216    try:217        body = await request.json()218    except Exception as e:219        log.error(f"[req#{req_id}] Invalid JSON: {e}")220        return web.json_response({"type": "error", "error": {"type": "invalid_request_error", "message": str(e)}}, status=400)221 222    model_name = body.get("model", "claude-sonnet-4-20250514")223    is_stream = body.get("stream", False)224    n_tools = len(body.get("tools", []))225    n_msgs = len(body.get("messages", []))226 227    log.info(f"[req#{req_id}] {model_name} → {MODEL} | msgs={n_msgs} tools={n_tools} stream={is_stream}")228 229    # Translate230    try:231        oai_body = anthropic_to_openai_request(body)232    except Exception as e:233        log.error(f"[req#{req_id}] Translation error: {e}\n{traceback.format_exc()}")234        return web.json_response(235            {"type": "error", "error": {"type": "api_error", "message": f"Translation error: {e}"}},236            status=500237        )238 239    log.info(f"[req#{req_id}] OpenAI request: {len(oai_body['messages'])} messages, {len(oai_body.get('tools', []))} tools")240 241    headers = {242        "Authorization": f"Bearer {NVIDIA_API_KEY}",243        "Content-Type": "application/json",244    }245    if PROXY_URL and PROXY_SECRET:246        headers["x-target-host"] = "integrate.api.nvidia.com"247        headers["x-proxy-key"] = PROXY_SECRET248 249    timeout = ClientTimeout(total=300, connect=30)250 251    async with ClientSession(timeout=timeout) as session:252        try:253            async with session.post(TARGET_URL, json=oai_body, headers=headers) as resp:254                if resp.status != 200:255                    error_text = await resp.text()256                    log.error(f"[req#{req_id}] NVIDIA API {resp.status}: {error_text[:500]}")257                    return web.json_response(258                        {"type": "error", "error": {"type": "api_error", "message": f"Backend error {resp.status}: {error_text[:300]}"}},259                        status=resp.status260                    )261 262                # ─── Non-streaming ───263                if not is_stream:264                    oai_resp = await resp.json()265                    anthropic_resp = openai_to_anthropic_response(oai_resp, model_name)266                    log.info(f"[req#{req_id}] Response: {len(anthropic_resp['content'])} blocks, stop={anthropic_resp['stop_reason']}")267                    return web.json_response(anthropic_resp)268 269                # ─── Streaming ───270                response = web.StreamResponse(271                    status=200, reason="OK",272                    headers={"Content-Type": "text/event-stream", "Cache-Control": "no-cache"}273                )274                await response.prepare(request)275 276                msg_id = f"msg_{uuid.uuid4().hex[:24]}"277 278                # message_start279                await sse_write(response, "message_start", {280                    "type": "message_start",281                    "message": {282                        "id": msg_id, "type": "message", "role": "assistant",283                        "content": [], "model": model_name,284                        "usage": {"input_tokens": 0, "output_tokens": 0}285                    }286                })287 288                block_open = False289                block_index = 0290                full_text = ""291                tool_buffers = {}  # tc_id -> {name, args}292                current_tool_id = None293 294                async for raw_line in resp.content:295                    line = raw_line.decode("utf-8", errors="replace").strip()296                    if not line.startswith("data: "):297                        continue298                    data_str = line[6:].strip()299                    if data_str == "[DONE]":300                        break301 302                    try:303                        chunk = json.loads(data_str)304                    except json.JSONDecodeError:305                        continue306 307                    choices = chunk.get("choices", [])308                    if not choices:309                        continue310 311                    delta = choices[0].get("delta", {})312                    finish = choices[0].get("finish_reason")313 314                    # ── Text delta ──315                    text = delta.get("content")316                    if text:317                        if not block_open or current_tool_id is not None:318                            # Close any tool block first319                            if block_open:320                                await sse_write(response, "content_block_stop",321                                    {"type": "content_block_stop", "index": block_index})322                                block_index += 1323                            # Open text block324                            await sse_write(response, "content_block_start", {325                                "type": "content_block_start", "index": block_index,326                                "content_block": {"type": "text", "text": ""}327                            })328                            block_open = True329                            current_tool_id = None330 331                        await sse_write(response, "content_block_delta", {332                            "type": "content_block_delta", "index": block_index,333                            "delta": {"type": "text_delta", "text": text}334                        })335                        full_text += text336 337                    # ── Tool call delta ──338                    tool_calls = delta.get("tool_calls", [])339                    for tc in tool_calls:340                        tc_id = tc.get("id")341                        fn = tc.get("function", {})342                        fn_name = fn.get("name")343                        fn_args = fn.get("arguments", "")344 345                        if tc_id and tc_id not in tool_buffers:346                            # New tool call — close previous block347                            if block_open:348                                await sse_write(response, "content_block_stop",349                                    {"type": "content_block_stop", "index": block_index})350                                block_index += 1351 352                            tool_buffers[tc_id] = {"name": fn_name or "", "args": fn_args}353                            current_tool_id = tc_id354 355                            await sse_write(response, "content_block_start", {356                                "type": "content_block_start", "index": block_index,357                                "content_block": {358                                    "type": "tool_use",359                                    "id": tc_id,360                                    "name": fn_name or ""361                                }362                            })363                            block_open = True364 365                            if fn_args:366                                await sse_write(response, "content_block_delta", {367                                    "type": "content_block_delta", "index": block_index,368                                    "delta": {"type": "input_json_delta", "partial_json": fn_args}369                                })370                        elif fn_args:371                            # Continuing an existing tool call372                            if current_tool_id and current_tool_id in tool_buffers:373                                tool_buffers[current_tool_id]["args"] += fn_args374                            await sse_write(response, "content_block_delta", {375                                "type": "content_block_delta", "index": block_index,376                                "delta": {"type": "input_json_delta", "partial_json": fn_args}377                            })378 379                    if finish:380                        break381 382                # Close last block383                if block_open:384                    await sse_write(response, "content_block_stop",385                        {"type": "content_block_stop", "index": block_index})386 387                # Determine stop reason388                stop_reason = "tool_use" if tool_buffers else "end_turn"389 390                await sse_write(response, "message_delta", {391                    "type": "message_delta",392                    "delta": {"stop_reason": stop_reason, "stop_sequence": None},393                    "usage": {"output_tokens": max(1, len(full_text.split()))}394                })395                await sse_write(response, "message_stop", {"type": "message_stop"})396 397                log.info(f"[req#{req_id}] Stream done: {len(full_text)} chars text, {len(tool_buffers)} tool calls, stop={stop_reason}")398                return response399 400        except Exception as e:401            log.error(f"[req#{req_id}] Error: {e}\n{traceback.format_exc()}")402            return web.json_response(403                {"type": "error", "error": {"type": "api_error", "message": str(e)}},404                status=500405            )406 407 408async def handle_health(request: web.Request):409    return web.json_response({410        "status": "ok",411        "proxy": "anthropic-to-openai",412        "target_model": MODEL,413        "target_url": TARGET_URL,414        "requests_served": REQUEST_COUNT,415    })416 417 418def create_app():419    app = web.Application()420    app.router.add_post("/v1/messages", handle_messages)421    app.router.add_post("/messages", handle_messages)422    app.router.add_get("/v1/messages", handle_health)423    app.router.add_get("/health", handle_health)424    app.router.add_get("/", handle_health)425    return app426 427 428if __name__ == "__main__":429    app = create_app()430    log.info(f"Anthropic→OpenAI proxy starting on :4000 → {MODEL} via {TARGET_URL}")431    web.run_app(app, host="0.0.0.0", port=4000)432