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

122 lines
6.7 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 spawn'ит свой bash без контекста Hermes
**Механизм:** proxy запускает `claude` через `child_process.spawn('claude', args, {env: process.env})`. Ниже по цепочке:
```
launchd → claude-proxy-start.sh → claude-proxy (node) → claude CLI (subprocess)
└── bash -c "..."
```
Claude Code CLI для выполнения инструментов (Bash, Write, Edit) внутри себя спавнит `bash -c "..."`. Этот bash:
- наследует env от `claude` процесса
- НЕ имеет `HERMES_HOME`, кастомного `PATH`, `SHELL=/bin/zsh` с профилем Hermes
- использует системный `/bin/bash` или `/bin/sh`
В контейнере (alpine) bash вообще может отсутствовать — claude CLI использует `/bin/sh`.
## Предложенные подходы
### Против 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 для отладки.
### Против изоляции shell
1. **В Docker:** установить bash в контейнер (`apk add bash`), установить `SHELL=/bin/bash` в env.
2. **Передача `HERMES_HOME`:** смонтировать `~/.hermes` в контейнер и передать `HERMES_HOME` в env claude CLI.
3. **wrapper-скрипт для claude:** заменить бинарник `claude` в контейнере на shell wrapper, который ставит профиль Hermes перед вызовом реального `claude`.