7.9 KiB
title, type, namespace, tags, created, updated, confidence
| title | type | namespace | tags | created | updated | confidence | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| claude-code-openai-wrapper — Python Claude CLI Proxy | reference | work |
|
2026-06-03 | 2026-06-03 | 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
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
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:
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:
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-jsonflag 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:
# 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:
#!/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:
<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
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