This commit is contained in:
ahmedvirani010-art 2026-06-25 11:03:23 +05:00 committed by GitHub
commit 814e60c569
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 156 additions and 0 deletions

View File

@ -0,0 +1,156 @@
"""
Claude Code -> OpenAI-compatible adapter.
Exposes a minimal OpenAI Chat Completions API (`POST /v1/chat/completions`,
`GET /v1/models`) that fulfils each request by shelling out to the local
`claude` CLI in headless print mode (`claude -p ... --output-format json`).
This lets any OpenAI-SDK client (MiroFish, OASIS, etc.) "use Claude Code as the
LLM" without an external API key — it reuses the CLI's own credentials.
Usage:
python claude_openai_adapter.py --port 8787 --model claude-opus-4-8
Then point the client at:
LLM_BASE_URL=http://127.0.0.1:8787/v1
LLM_API_KEY=sk-anything # ignored, just must be non-empty
LLM_MODEL_NAME=claude-opus-4-8
Notes:
* temperature / max_tokens are accepted but not forwarded (the headless CLI
does not expose them); they are ignored.
* response_format={"type":"json_object"} is honoured by instructing the model
to emit raw JSON only.
* Stdlib only no extra dependencies.
"""
import argparse
import json
import subprocess
import time
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
DEFAULT_MODEL = "claude-opus-4-8"
CLAUDE_TIMEOUT_S = 600
def _flatten_messages(messages, force_json):
"""Turn OpenAI chat messages into (system_prompt, user_prompt)."""
system_parts, convo_parts = [], []
for m in messages:
role = m.get("role", "user")
content = m.get("content", "")
if isinstance(content, list): # OpenAI content-parts form
content = "".join(
p.get("text", "") for p in content if isinstance(p, dict)
)
if role == "system":
system_parts.append(content)
elif role == "assistant":
convo_parts.append(f"[assistant]\n{content}")
else:
convo_parts.append(content)
system_prompt = "\n\n".join(p for p in system_parts if p).strip()
user_prompt = "\n\n".join(p for p in convo_parts if p).strip()
if force_json:
user_prompt += (
"\n\nIMPORTANT: Respond with ONLY a single valid JSON object. "
"No markdown, no code fences, no commentary."
)
return system_prompt, user_prompt
def _call_claude(system_prompt, user_prompt, model):
cmd = ["claude", "-p", "--output-format", "json", "--model", model]
if system_prompt:
# Replace the default agent system prompt with the caller's role prompt.
cmd += ["--system-prompt", system_prompt]
proc = subprocess.run(
cmd,
input=user_prompt,
capture_output=True,
text=True,
timeout=CLAUDE_TIMEOUT_S,
)
if proc.returncode != 0:
raise RuntimeError(f"claude CLI failed ({proc.returncode}): {proc.stderr[:2000]}")
payload = json.loads(proc.stdout)
if payload.get("is_error"):
raise RuntimeError(f"claude error: {payload.get('result')}")
return payload.get("result", ""), payload.get("usage", {})
class Handler(BaseHTTPRequestHandler):
server_version = "ClaudeOpenAIAdapter/1.0"
default_model = DEFAULT_MODEL
def _send(self, code, obj):
body = json.dumps(obj).encode("utf-8")
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args): # quieter logging
print("[adapter] " + (fmt % args))
def do_GET(self):
if self.path.rstrip("/").endswith("/models"):
self._send(200, {
"object": "list",
"data": [{"id": self.default_model, "object": "model", "owned_by": "claude-code"}],
})
else:
self._send(404, {"error": {"message": "not found"}})
def do_POST(self):
if not self.path.rstrip("/").endswith("/chat/completions"):
self._send(404, {"error": {"message": "not found"}})
return
try:
length = int(self.headers.get("Content-Length", 0))
req = json.loads(self.rfile.read(length) or b"{}")
messages = req.get("messages", [])
model = req.get("model") or self.default_model
force_json = (req.get("response_format") or {}).get("type") == "json_object"
system_prompt, user_prompt = _flatten_messages(messages, force_json)
text, usage = _call_claude(system_prompt, user_prompt, model)
except Exception as e: # noqa: BLE001 - surface as OpenAI-style error
self._send(500, {"error": {"message": str(e), "type": "adapter_error"}})
return
self._send(200, {
"id": "chatcmpl-" + uuid.uuid4().hex[:24],
"object": "chat.completion",
"created": int(time.time()),
"model": model,
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": text},
"finish_reason": "stop",
}],
"usage": {
"prompt_tokens": usage.get("input_tokens", 0),
"completion_tokens": usage.get("output_tokens", 0),
"total_tokens": usage.get("input_tokens", 0) + usage.get("output_tokens", 0),
},
})
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=8787)
ap.add_argument("--model", default=DEFAULT_MODEL)
args = ap.parse_args()
Handler.default_model = args.model
srv = ThreadingHTTPServer((args.host, args.port), Handler)
print(f"[adapter] Claude->OpenAI adapter on http://{args.host}:{args.port}/v1 (model={args.model})")
srv.serve_forever()
if __name__ == "__main__":
main()