From a1f5305fe4f12574cbf64070e22c5b23827c51dd Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 3 Jun 2026 16:30:41 +0600 Subject: [PATCH] [2026-06-03] eagle: family/how-to/claude-python-cli-proxy.md family/how-to/hermes-eagle-mac.md personal/projects/personal-os/hermes-deployment-patterns.md --- family/how-to/claude-python-cli-proxy.md | 219 ++++++++++++++++++ family/how-to/hermes-eagle-mac.md | 6 +- .../personal-os/hermes-deployment-patterns.md | 18 +- 3 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 family/how-to/claude-python-cli-proxy.md diff --git a/family/how-to/claude-python-cli-proxy.md b/family/how-to/claude-python-cli-proxy.md new file mode 100644 index 00000000..3a840971 --- /dev/null +++ b/family/how-to/claude-python-cli-proxy.md @@ -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=|" \ + ~/.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 diff --git a/family/how-to/hermes-eagle-mac.md b/family/how-to/hermes-eagle-mac.md index ad8f6edf..6e7da891 100644 --- a/family/how-to/hermes-eagle-mac.md +++ b/family/how-to/hermes-eagle-mac.md @@ -31,7 +31,10 @@ Hermes работает нативно (не в Docker) на Mac через `her --- -## openclaw-claude-proxy — обход rate limit Claude API +## Claude CLI прокси — обход rate limit Claude API + +> **Актуальный прокси (июнь 2026):** Python `claude-code-openai-wrapper` на +> порту 8090. Детальная документация: [[claude-python-cli-proxy]]. ### Проблема @@ -224,6 +227,7 @@ Claude падает с "Failed to spawn process". Проверить логи: ## Связанные страницы +- [[claude-python-cli-proxy]] — Python Claude CLI proxy (claude-code-openai-wrapper, порт 8090) - [[tech/hermes-docker-kraken]] — Hermes на Кракене (Docker) - [[tech/kraken-network]] — сетевая топология - [[concepts/hermes-deployment-patterns]] — сравнение трёх моделей деплоя Hermes diff --git a/personal/projects/personal-os/hermes-deployment-patterns.md b/personal/projects/personal-os/hermes-deployment-patterns.md index eebbc98d..86ee02ab 100644 --- a/personal/projects/personal-os/hermes-deployment-patterns.md +++ b/personal/projects/personal-os/hermes-deployment-patterns.md @@ -23,17 +23,21 @@ a different access model and client type. **Where:** Eagle Mac M4 Max, native process (not Docker) **Transport:** Zulip (Docker) -**Model backend:** `openclaw-claude-proxy` on port 3456 — wraps -`claude --print` as an OpenAI-compatible endpoint, preserving tool_use. -**Autostart:** launchd (`ai.claude-proxy.plist`) +**Model backend:** `claude-code-openai-wrapper` (Python) on port 8090 — uses +`claude-agent-sdk` to spawn the bundled `claude` CLI as a subprocess and +expose an OpenAI-compatible endpoint with real streaming support. +**Previous backend:** `openclaw-claude-proxy` (Node.js) on port 3456 — +still running as fallback but streaming broken since CLI ≥ 2.1.141. +**Autostart:** launchd (`com.openclaw.claude-proxy.plist`) **Why native:** Mac file system access, MCP tools (obsidian-mcp), and the cron wiki-curation job all need full host access. Docker would require volume mounts for every integration. -**Key pitfall:** `claude-proxy` wrapper script must `source ~/.hermes/.env` -explicitly — launchd does not inherit login session env. See -[[tech/hermes-eagle-mac]]. +**Key pitfall:** `claude-python-proxy-start.sh` must `source ~/.hermes/.env` +explicitly — launchd does not inherit login session env. Also `CLAUDE_CODE_OAUTH_TOKEN` +expires (access tokens are short-lived); when it does both proxies return 401. +See [[claude-python-cli-proxy]] for full details and token-refresh procedure. ## Pattern 2: Kraken Docker (RPi5) @@ -64,7 +68,7 @@ See [[tech/hermes-kraken-api]]. | Dimension | Eagle Native | Kraken Docker | Kraken API | |-----------|-------------|---------------|-----------| -| Model | claude-sonnet via proxy | Gemini/ORouter | Gemini/ORouter | +| Model | claude-sonnet via Python proxy (8090) | Gemini/ORouter | Gemini/ORouter | | Transport | Zulip | Zulip | HTTPS REST | | Clients | Cron, MCP tools | Zulip bot clients | Mobile / OpenAI SDK | | External access | No | No | Yes (CF Tunnel) |