Files
obsidian-vault/personal/projects/personal-os/claude-proxy-node-internals.md
T

7.5 KiB
Raw Blame History

title, type, namespace, tags, created, updated, confidence
title type namespace tags created updated confidence
openclaw-claude-proxy (Node.js) — внутреннее устройство и проблемы reference personal
hermes
mac
eagle
claude-proxy
nodejs
llm-backend
pitfalls
docker
2026-06-15 2026-06-15 0.9

openclaw-claude-proxy (Node.js) — внутреннее устройство и проблемы

Обзор

npm пакет: openclaw-claude-proxy v1.0.8+ Порт: 3456 Язык: TypeScript (компилируется в JS) Репозиторий: 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 для отладки.