Files

12 KiB
Raw Permalink 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.

Обзор альтернатив

Проект Язык Проблема 1 (orphaned) Проблема 2 (вывод CLI) Проблема 3 (caller-dispatched tools)
openclaw-claude-proxy (порт 3456) TypeScript — pool без SIGTERM cleanup — stderr не форвардится — stream-json сломан с CLI ≥ 2.1.141
claude-code-openai-wrapper (RichardAtCT, порт 8090) Python — uvicorn без tini — SDK pipe без логирования stop_after_first_assistant + disallowed_tools
claude-code-openai-wrapper (clebermasters) Rust — tokio без reaper — pipe — CLI сам исполняет инструменты
claude-max-api-proxy (mnemon-dev) TypeScript — родительский проект openclaw
Hermes provider: claude-code встроен N/A N/A — но упирается в rate limit OAuth API

Ни один существующий проект не решает все три проблемы одновременно.

openclaw-claude-proxy — форки и состояние

Репозиторий mehdic/openclaw-claude-proxy:

  • v1.0.8 (May 2026) — последний релиз
  • 2 форка (habibtalik, ppcvote), 0 звёзд
  • Нет открытых issues
  • subprocess/manager.ts (311 строк): spawn() + EventEmitter, есть disallowedTools в опциях, но они работают только в --print режиме. В stream-json режиме multi-turn сломан протоколом CLI ≥ 2.1.141.
  • kill() отправляет SIGTERM на один процесс, но глобального cleanup при SIGTERM/SIGKILL родителя нет.

clebermasters/claude-code-openai-wrapper (Rust)

Альтернатива, переписанная на Rust:

  • Один статический бинарник 4.7 MB, без зависимостей времени выполнения
  • Использует tokio::process::Command + --print --output-format stream-json
  • Проблемы те же — запускает claude как subprocess, CLI сам исполняет инструменты
  • Проект новый — 0 issues, 0 форков

claude-code-openai-wrapper (RichardAtCT, Python)

Единственный проект, решающий проблему 3 (caller-dispatched tools):

  • Логика: stop_after_first_assistant останавливает итерацию после первого assistant-сообщения, disallowed_tools=['Bash','Read',...] не даёт CLI самому исполнять инструменты
  • Возвращает tool_use блоки как OpenAI tool_calls для исполнения Hermes'ом
  • Проблема 1 (orphaned): если запущен через uvicorn без --init, при SIGKILL дочерний claude процесс остаётся orphan. SDK atexit не срабатывает при SIGKILL.
  • Проблема 2 (вывод): SDK читает stdout claude через pipe → JSON → memory channel. Человеческий вывод не транслируется. stderr CLI читается тихо в _handle_stderr и никуда не выводится.

Предложенные подходы

Против orphaned процессов (все проекты)

  1. Docker --init флаг: запускать контейнер с init: true в docker-compose (или docker run --init). Использует tini как PID 1 — корректно форвардит SIGTERM и перезахоранивает orphan'ов.
  2. SIGTERM handler: в Node.js — process.on('SIGTERM', pool.killAll()), в Python — uvicorn lifespan shutdown c force kill.
  3. Health check + restart policy: unless-stopped с --time=30 grace period.

Против отсутствия вывода CLI

  1. Python SDK: добавить stderr callback в ClaudeAgentOptions, который пишет в stderr uvicorn → видно в docker logs.
  2. Node.js proxy: форвардить stderr claude в stderr родителя.
  3. Логирование JSON: логировать каждую N-ную строку из stdout claude для отладки.

Против изоляции инструментов (caller-dispatched tools)

Единственное работающее решение — Python wrapper (claude-code-openai-wrapper RichardAtCT) с:

  • stop_after_first_assistant = True
  • disallowed_tools = ['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep']
  • permission_mode = 'bypassPermissions'

Для полного решения всех трёх проблем нужно взять Python wrapper, запустить в Docker с init: true и добавить проброс stderr из SDK.