--- 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=|" \ ~/.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 ProgramArguments /bin/zsh /Users/admin/.local/bin/claude-python-proxy-start.sh WorkingDirectory /Users/admin/openclaw/claude-proxy StandardOutPath/tmp/claude-proxy.log StandardErrorPath/tmp/claude-proxy.err ``` ## 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