#!/usr/bin/env python3 import json import os import time import uuid from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.request import Request, urlopen from urllib.error import HTTPError UPSTREAM_BASE = os.environ.get("BRIDGE_UPSTREAM_BASE", "http://127.0.0.1:20128/v1") UPSTREAM_MODEL = os.environ.get("BRIDGE_UPSTREAM_MODEL", "default") UPSTREAM_KEY = os.environ.get("BRIDGE_UPSTREAM_KEY", "") def _text_from_content(content): if isinstance(content, str): return content if isinstance(content, list): parts = [] for item in content: if isinstance(item, dict): if item.get("type") == "text": parts.append(item.get("text", "")) elif "text" in item: parts.append(str(item.get("text", ""))) return "\n".join([p for p in parts if p]) return str(content or "") def anthropic_to_openai(body): messages = [] system = body.get("system") if system: messages.append({"role": "system", "content": _text_from_content(system)}) for m in body.get("messages", []): role = m.get("role", "user") if role == "assistant": out_role = "assistant" else: out_role = "user" messages.append({"role": out_role, "content": _text_from_content(m.get("content", ""))}) return { "model": UPSTREAM_MODEL, "messages": messages, "stream": False, "temperature": body.get("temperature", 0.2), "max_tokens": body.get("max_tokens", 1024), } def call_upstream(payload): url = UPSTREAM_BASE.rstrip("/") + "/chat/completions" headers = {"Content-Type": "application/json"} if UPSTREAM_KEY: headers["Authorization"] = f"Bearer {UPSTREAM_KEY}" req = Request(url, data=json.dumps(payload).encode(), headers=headers, method="POST") try: with urlopen(req, timeout=180) as r: return r.status, json.loads(r.read().decode()) except HTTPError as e: raw = e.read().decode(errors="replace") try: err_obj = json.loads(raw) except Exception: err_obj = {"raw": raw} return e.code, {"error": err_obj} def openai_to_anthropic(obj): text = "" try: text = obj["choices"][0]["message"].get("content") or "" except Exception: text = json.dumps(obj, ensure_ascii=False)[:4000] return { "id": "msg_" + uuid.uuid4().hex, "type": "message", "role": "assistant", "model": "claude-3-5-sonnet-20241022", # Fake model name back to client "content": [{"type": "text", "text": text}], "stop_reason": "end_turn", "stop_sequence": None, "usage": {"input_tokens": 100, "output_tokens": 100}, } class Handler(BaseHTTPRequestHandler): def _send(self, code, obj): data = json.dumps(obj, ensure_ascii=False).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(data))) self.end_headers() self.wfile.write(data) def do_HEAD(self): # Respond to connectivity check self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() def do_GET(self): normalized = self.path.split("?")[0].rstrip("/") if normalized in ("", "/health", "/v1/health", "/v1"): return self._send(200, {"ok": True, "upstream": UPSTREAM_BASE, "model": UPSTREAM_MODEL}) # If client lists models if normalized == "/v1/models": return self._send(200, { "data": [ {"id": "claude-3-5-sonnet-20241022", "type": "model"}, {"id": "claude-3-5-haiku-20241022", "type": "model"} ] }) return self._send(404, {"error": "not_found", "path": self.path}) def do_POST(self): normalized = self.path.split("?")[0].rstrip("/") if normalized != "/v1/messages": return self._send(404, {"error": "not_found", "path": self.path}) length = int(self.headers.get("content-length", "0")) raw = self.rfile.read(length).decode(errors="replace") try: body = json.loads(raw) except Exception as e: return self._send(400, {"error": f"bad_json: {e}"}) payload = anthropic_to_openai(body) status, upstream = call_upstream(payload) if status >= 400: return self._send(status, {"error": "upstream_failed", "detail": upstream}) return self._send(200, openai_to_anthropic(upstream)) def log_message(self, fmt, *args): print(time.strftime("%Y-%m-%d %H:%M:%S"), self.address_string(), fmt % args, flush=True) if __name__ == "__main__": host = os.environ.get("BRIDGE_HOST", "127.0.0.1") port = int(os.environ.get("BRIDGE_PORT", "20131")) print(f"claude-openai-bridge listening on {host}:{port}, upstream={UPSTREAM_BASE}, model={UPSTREAM_MODEL}", flush=True) ThreadingHTTPServer((host, port), Handler).serve_forever()