220 lines
7.9 KiB
Markdown
220 lines
7.9 KiB
Markdown
---
|
|
title: claude-code-openai-wrapper — Python Claude CLI Proxy
|
|
type: reference
|
|
namespace: work
|
|
tags:
|
|
- hermes
|
|
- mac
|
|
- eagle
|
|
- claude-proxy
|
|
- python
|
|
- llm-backend
|
|
- pitfalls
|
|
created: 2026-06-03
|
|
updated: 2026-06-03
|
|
confidence: 0.95
|
|
---
|
|
|
|
# claude-code-openai-wrapper — Python Claude CLI Proxy
|
|
|
|
OpenAI-compatible FastAPI server that wraps the `claude` CLI via the official
|
|
`claude-agent-sdk`. Runs at **port 8090** on Eagle. Hermes uses it as its LLM
|
|
backend (`base_url: http://localhost:8090/v1`).
|
|
|
|
## Why this exists
|
|
|
|
Hermes needs an OpenAI-compatible endpoint that routes calls through the local
|
|
`claude` CLI session (Max/OAuth subscription, no API key). Two wrappers were
|
|
evaluated — see below. The Python one is currently active.
|
|
|
|
### Node.js wrapper (port 3456) — status: broken streaming, kept as fallback
|
|
|
|
**Package:** `openclaw-claude-proxy` v1.0.8 (npm)
|
|
**Repo:** [mehdic/openclaw-claude-proxy](https://github.com/mehdic/openclaw-claude-proxy)
|
|
**LaunchAgent:** `ai.claude-proxy.plist` → `~/.local/bin/claude-proxy-start.sh`
|
|
|
|
**Issue:** uses a reverse-engineered `stream-json` protocol to talk to `claude`
|
|
as a long-lived subprocess. This protocol broke with claude CLI ≥ 2.1.141.
|
|
Streaming requests stall after the initial role chunk — only keepalives come
|
|
through. Non-streaming (`--print` fallback path) still works.
|
|
|
|
The proxy has `CLAUDE_PROXY_RUNTIME=stream-json` (default) and
|
|
`CLAUDE_PROXY_RUNTIME=print` (fallback). Switching to `print` fixes
|
|
hangs but removes true incremental streaming.
|
|
|
|
---
|
|
|
|
### Python wrapper (port 8090) — status: **active**
|
|
|
|
**Repo:** [RichardAtCT/claude-code-openai-wrapper](https://github.com/RichardAtCT/claude-code-openai-wrapper)
|
|
**Local path:** `/Users/admin/openclaw/claude-proxy/`
|
|
**Version (pyproject.toml):** 2.2.0
|
|
**SDK:** `claude-agent-sdk` 0.2.88 (updated June 2026 from 0.1.56)
|
|
**Bundled CLI:** 2.1.161 (inside SDK venv, used instead of system CLI — see pitfalls)
|
|
**LaunchAgent:** `com.openclaw.claude-proxy.plist` → `~/.local/bin/claude-python-proxy-start.sh`
|
|
|
|
#### How it works
|
|
|
|
```
|
|
Hermes → POST /v1/chat/completions (port 8090)
|
|
└── FastAPI (uvicorn)
|
|
└── claude-agent-sdk query()
|
|
└── spawns .venv/…/_bundled/claude 2.1.161
|
|
--output-format stream-json --verbose
|
|
(reads CLAUDE_CODE_OAUTH_TOKEN from env)
|
|
→ streams AssistantMessage events → SSE chunks
|
|
```
|
|
|
|
Key: the SDK uses its own **bundled** `claude` binary (not the system one at
|
|
`/opt/homebrew/bin/claude`) because the SDK's stream-json protocol must match
|
|
the exact CLI version it was built against.
|
|
|
|
#### Hermes config
|
|
|
|
`~/.hermes/config.yaml`:
|
|
```yaml
|
|
model:
|
|
default: claude-sonnet-4-6
|
|
provider: custom
|
|
base_url: 'http://localhost:8090/v1'
|
|
```
|
|
|
|
---
|
|
|
|
## What was fixed (June 2026)
|
|
|
|
### 1. Venv Python version mismatch
|
|
|
|
Venv was created with Python 3.14.3, Homebrew had upgraded to 3.14.5.
|
|
Compiled C extensions (`.so` files) were incompatible → `pip install` crashed
|
|
with `ImportError: Symbol not found: _XML_SetAllocTrackerActivationThreshold`.
|
|
|
|
**Fix:** rebuilt venv with `uv`:
|
|
```bash
|
|
cd /Users/admin/openclaw/claude-proxy
|
|
uv venv .venv --python python3.14 --clear
|
|
uv pip install fastapi "uvicorn[standard]" pydantic python-dotenv httpx \
|
|
sse-starlette python-multipart claude-agent-sdk slowapi
|
|
```
|
|
|
|
### 2. SDK too old — bundled CLI mismatch
|
|
|
|
`claude-agent-sdk` 0.1.56 bundled `claude` 2.1.92. Between 2.1.92 and
|
|
2.1.141+, Anthropic changed how the stream-json protocol is initiated:
|
|
- Old: inferred from `--output-format stream-json`
|
|
- New: requires `--input-format stream-json` flag explicitly
|
|
|
|
Result: SDK sent `control_request` on stdin; new CLI ignored it (no output,
|
|
exited code 1).
|
|
|
|
**Fix:** `uv pip install claude-agent-sdk` → upgraded to 0.2.88, bundled CLI
|
|
2.1.161 which matches the updated SDK protocol.
|
|
|
|
### 3. `cli_path=SYSTEM_CLAUDE_PATH` override
|
|
|
|
The wrapper hard-coded `cli_path = shutil.which("claude")` (system 2.1.145)
|
|
in `ClaudeAgentOptions`. This forced the SDK to use the system CLI instead
|
|
of its own bundled binary, breaking the protocol handshake.
|
|
|
|
**Fix:** set `SYSTEM_CLAUDE_PATH = None` in `src/claude_cli.py` so the SDK
|
|
auto-discovers and uses its bundled CLI.
|
|
|
|
### 4. LaunchAgent missing auth token
|
|
|
|
The original plist ran `uvicorn` directly and only set `CLAUDE_AUTH_METHOD=cli`.
|
|
The `claude` subprocess (spawned by the SDK) inherits the process env, which
|
|
had no `CLAUDE_CODE_OAUTH_TOKEN`. CLI responded: *"Not logged in"*.
|
|
|
|
**Fix:** added wrapper script `~/.local/bin/claude-python-proxy-start.sh` that
|
|
sources `~/.hermes/.env` before starting uvicorn (same pattern as the Node.js
|
|
proxy). Updated plist `ProgramArguments` to call the wrapper instead of
|
|
uvicorn directly.
|
|
|
|
### 5. Expired OAuth token (root cause of 401s)
|
|
|
|
`CLAUDE_CODE_OAUTH_TOKEN` in `~/.hermes/.env` expires (access tokens are
|
|
short-lived). When the stored token is expired and passed to the CLI
|
|
explicitly via env, the CLI uses it as-is → 401 from Anthropic API. The
|
|
CLI does **not** auto-refresh when `CLAUDE_CODE_OAUTH_TOKEN` is set to an
|
|
expired value in the env.
|
|
|
|
**Symptom:** both proxies return `401 Invalid authentication credentials`
|
|
even though `claude` works fine in the terminal (terminal has a fresh token
|
|
from a previous session).
|
|
|
|
**Immediate fix:** copy fresh token from terminal into `~/.hermes/.env`:
|
|
```bash
|
|
# In the terminal that has a working claude session:
|
|
grep CLAUDE_CODE_OAUTH_TOKEN <(env)
|
|
# Then update:
|
|
sed -i '' "s|CLAUDE_CODE_OAUTH_TOKEN=.*|CLAUDE_CODE_OAUTH_TOKEN=<fresh_token>|" \
|
|
~/.hermes/.env
|
|
# Restart both proxies:
|
|
launchctl unload ~/Library/LaunchAgents/ai.claude-proxy.plist
|
|
launchctl load ~/Library/LaunchAgents/ai.claude-proxy.plist
|
|
launchctl unload ~/Library/LaunchAgents/com.openclaw.claude-proxy.plist
|
|
launchctl load ~/Library/LaunchAgents/com.openclaw.claude-proxy.plist
|
|
```
|
|
|
|
**Long-term:** a token-refresh cron/daemon that keeps `~/.hermes/.env` updated
|
|
is needed. Not yet implemented.
|
|
|
|
---
|
|
|
|
## Wrapper script
|
|
|
|
`~/.local/bin/claude-python-proxy-start.sh`:
|
|
```bash
|
|
#!/bin/zsh
|
|
set -a
|
|
source /Users/admin/.hermes/.env 2>/dev/null
|
|
set +a
|
|
export PATH="/Users/admin/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
|
|
cd /Users/admin/openclaw/claude-proxy
|
|
exec .venv/bin/python -m uvicorn src.main:app --host 127.0.0.1 --port 8090
|
|
```
|
|
|
|
## LaunchAgent
|
|
|
|
`~/Library/LaunchAgents/com.openclaw.claude-proxy.plist`:
|
|
```xml
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/bin/zsh</string>
|
|
<string>/Users/admin/.local/bin/claude-python-proxy-start.sh</string>
|
|
</array>
|
|
<key>WorkingDirectory</key>
|
|
<string>/Users/admin/openclaw/claude-proxy</string>
|
|
<key>StandardOutPath</key><string>/tmp/claude-proxy.log</string>
|
|
<key>StandardErrorPath</key><string>/tmp/claude-proxy.err</string>
|
|
```
|
|
|
|
## Pitfall summary
|
|
|
|
| Pitfall | Symptom | Fix |
|
|
|---------|---------|-----|
|
|
| Token expired in `.env` | 401 from both proxies, terminal claude works | Copy fresh token from terminal to `~/.hermes/.env`, restart proxies |
|
|
| Venv Python version mismatch | `pip` crashes, `ImportError` on `.so` | Rebuild with `uv venv --clear`, reinstall deps |
|
|
| `cli_path` set to system claude | SDK exits code 1, no response | Set `SYSTEM_CLAUDE_PATH = None` in `src/claude_cli.py` |
|
|
| LaunchAgent has no OAuth token | "Not logged in" | Wrapper script must `source ~/.hermes/.env` |
|
|
| SDK version too old | CLI protocol mismatch, exit code 1 | `uv pip install claude-agent-sdk` to update |
|
|
| Node.js proxy stream-json | Streaming hangs after role chunk | Use Python proxy on 8090 instead |
|
|
|
|
## Health check
|
|
|
|
```bash
|
|
curl http://localhost:8090/health
|
|
curl http://localhost:8090/v1/auth/status
|
|
# Test streaming:
|
|
curl -s -X POST http://localhost:8090/v1/chat/completions \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"model":"claude-haiku-4-5-20251001","messages":[{"role":"user","content":"hi"}],"stream":true}'
|
|
```
|
|
|
|
---
|
|
|
|
## Related
|
|
|
|
- [[hermes-eagle-mac]] — full Eagle setup including Zulip, MCP, launchd
|
|
- [[hermes-deployment-patterns]] — comparison of Eagle/Kraken deployment models
|