Files
obsidian-vault/personal/projects/personal-os/claude-python-cli-proxy.md
T

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
hermes
mac
eagle
claude-proxy
python
llm-backend
pitfalls
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-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:

# 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}'