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

182 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.