[2026-06-03] taiga-vault: family/how-to/claude-python-cli-proxy.md family/how-to/hermes-eagle-mac.md personal/projects/personal-os/hermes-deployment-patterns.md

This commit is contained in:
Taiga
2026-06-03 10:30:58 +00:00
parent 8a62f0ee19
commit be63dd9436
3 changed files with 235 additions and 8 deletions
+219
View File
@@ -0,0 +1,219 @@
---
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