--- title: openclaw-claude-proxy (Node.js) — внутреннее устройство и проблемы type: reference namespace: personal tags: - hermes - mac - eagle - claude-proxy - nodejs - llm-backend - pitfalls - docker created: '2026-06-15' updated: '2026-06-15' confidence: 0.9 --- # openclaw-claude-proxy (Node.js) — внутреннее устройство и проблемы ## Обзор **npm пакет:** `openclaw-claude-proxy` v1.0.8+ **Порт:** 3456 **Язык:** TypeScript (компилируется в JS) **Репозиторий:** [mehdic/openclaw-claude-proxy](https://github.com/mehdic/openclaw-claude-proxy) **Форк:** `mnemon-dev/claude-max-api-proxy` + OpenClaw compatibility PR **Текущий статус:** fallback (активен Python wrapper на порту 8090) ## Архитектура ``` Hermes → POST /v1/chat/completions (порт 3456, Node.js Express) └── proxy преобразует OpenAI-формат → stream-json протокол └── спавнит claude CLI как child process └── claude --output-format stream-json --verbose └── читает/пишет JSON через stdin/stdout pipe ``` ### Runtime-модели (CLAUDE_PROXY_RUNTIME) **stream-json** (default): - init pool: несколько долгоживущих `claude` subprocess'ов - session pool: переиспользование сессий между запросами - промпт-кэш между вызовами сохраняется - использует `--output-format stream-json --input-format stream-json` **print** (fallback): - на каждый запрос spawn'ит свежий `claude --print` - медленнее, но изолированно - не требует совместимости stream-json протокола ## Контейнерный сетап `~/Docker/claude-proxy-node/Dockerfile` — образ на node:20-alpine: ``` FROM node:20-alpine RUN npm install -g openclaw-claude-proxy @anthropic-ai/claude-code EXPOSE 3456 CMD ["claude-proxy", "3456"] ``` Образ не собран — в работе. ## Проблемы ### 1. Orphaned claude subprocess'ы **Механизм:** `stream-json` runtime держит пул долгоживущих `claude` child process'ов через `child_process.spawn()`. Proxy управляет их жизненным циклом: при нормальном shutdown (SIGTERM) должен завершить pool. Но: - **SIGKILL** родительского proxy (kill -9, docker stop --time=0, launchd force quit) — Node.js не успевает выполнить cleanup - **SIGTERM с таймаутом** — если graceful shutdown превышает лимит времени, Node.js процесс убивается до завершения pool - **launchd KeepAlive** — при падении proxy по любой причине launchd убивает и перезапускает, но старые `claude` процессы с PPID 1 остаются в системе - **docker restart** — если контейнер перезапускается без `--time` (grace period), старые процессы переживают контейнер На macOS orphan'ы выглядят как `claude` с PPID=1. Их количество растёт со временем. ### 2. Нет живого вывода claude CLI **Причина:** proxy читает stdout `claude` исключительно как JSON pipe. Человеческий вывод markdown/thinking/прогресса `claude` пишет в **stderr**, но proxy его не форвардит. По умолчанию stderr `claude` либо: - не пипруется вообще (pipe не открывается) — stderr уходит в /dev/null контейнера - пипруется и выбрасывается В режиме `print` проблема та же — proxy ждёт завершения `claude --print` и возвращает результат целиком, не транслируя промежуточный вывод. ### 3. Claude CLI сам исполняет инструменты вместо Hermes **Проблема:** Claude Code CLI **сам выполняет Bash/Read/Write/Edit инструменты внутри своей сессии**, а не возвращает tool_use блоки Hermes'у для исполнения. Текущий flow: ``` Hermes → claude-proxy (Node.js) → claude CLI └── сам делает Bash, Read, Write └── bash -c "..." ``` Hermes видит только финальный текст ответа. Он теряет контроль над: - какие файлы читаются - какие команды выполняются - кто их логирует и аудитит - allower/denier политики Hermes игнорируются Правильный flow (caller-dispatched tools): ``` Hermes → claude-proxy (Node.js) → claude CLI (только генерация) ↓ текст ответа с tool_use блоками Hermes ── парсит tool_use ── выполняет через свои инструменты ── результат → обратно в claude CLI ``` Для этого proxy должен: 1. Выключить инструменты в `claude` CLI — передать `disallowed_tools = ["Bash", "Read", "Write", "Edit", "Glob", "Grep"]` 2. Остановиться после первого assistant-сообщения 3. Отдать tool_use блоки Hermes'у как OpenAI `tool_calls` 4. Принять результат выполнения от Hermes и передать обратно в `claude` для продолжения (multi-turn) **Текущий `openclaw-claude-proxy` не поддерживает этот flow корректно:** stream-json протокол для multi-turn interaction сломан начиная с claude CLI 2.1.141. В режиме `print` multi-turn вообще невозможен. **Python wrapper (`claude-code-openai-wrapper`) этот flow поддерживает** — у него есть `stop_after_first_assistant` и `disallowed_tools`. Именно поэтому он активен, а Node.js — fallback. ## Предложенные подходы ### Против orphaned процессов 1. **Docker `--init` флаг:** запускать контейнер с `init: true` в docker-compose (или `docker run --init`). Использует `tini` как PID 1 — корректно форвардит SIGTERM и перезахоранивает orphan'ов. 2. **SIGTERM handler в Node.js:** добавить `process.on('SIGTERM', ...)` который форсированно убивает весь child process pool перед exit. 3. **Health check + restart policy:** не `always`, а `unless-stopped` с `--time=30` grace period. ### Против отсутствия вывода CLI 1. **Проброс stderr:** pipe'ить stderr `claude` в stderr proxy — видно в `docker logs` или launchd логах. 2. **Режим `print`:** переключить `CLAUDE_PROXY_RUNTIME=print` — теряется производительность stream-json, но логов больше. 3. **Node.js `stream-json` мониторинг:** логировать каждую N-ную JSON строку из stdout claude для отладки.