182 lines
12 KiB
Markdown
182 lines
12 KiB
Markdown
---
|
||
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.
|
||
|
||
## Обзор альтернатив
|
||
|
||
| Проект | Язык | Проблема 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](https://github.com/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.
|