From b1f7ffed94f9ade2be8431cf4ab4d104ca371ed6 Mon Sep 17 00:00:00 2001 From: Nishant Date: Thu, 19 Mar 2026 11:40:31 +0530 Subject: [PATCH 1/5] feat(openclaw): turnkey Open WebUI integration via HTTP API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable OpenClaw agents as a selectable model in Open WebUI with zero manual configuration. When the openclaw extension is enabled, users see an "openclaw" model in the Open WebUI dropdown alongside the direct LLM — providing file I/O, shell commands, code execution, and sub-agent capabilities through the familiar chat interface. Changes: inject-token.js: - Enable /v1/chat/completions HTTP endpoint on the gateway - Spawn OpenAI-compat shim on port 18790 (serves /v1/models, proxies chat completions to gateway) — needed because OpenClaw doesn't natively serve /v1/models which Open WebUI requires - Fix provider baseUrl in merged config using OLLAMA_URL env var (resolves macOS where LLM runs natively on host, not in Docker) - Add OPENCLAW_LLM_URL support for optional Token Spy monitoring - Log Control UI URL with token for Docker users compose.yaml: - Updated entrypoint to use merged config with HTTP API - Pass OPENCLAW_LLM_URL env var into container - Add open-webui overlay: sets OPENAI_API_BASE_URLS (plural) with both llama-server and OpenClaw shim as backends manifest.yaml: - Add apple to gpu_backends (was [amd, nvidia], now includes macOS) Tested on macOS M4 (32GB) with Qwen3-8B: - File write + read: proven - Shell command execution: proven - Write + execute code: proven - Sub-agent spawn: proven (slow on 8B, ~3min due to serialized LLM) Known requirement: CTX_SIZE must be >= 32768 for OpenClaw agent prompts. Default 16384 causes "Context size exceeded" errors. TODO: Sub-agents need a larger model or multiple LLM instances for responsive performance. Single 8B model serializes main + sub-agent turns causing timeouts on complex parallel tasks. Cross-platform: all changes run inside Docker, no host OS dependency. Co-Authored-By: Claude Opus 4.6 (1M context) --- dream-server/config/openclaw/inject-token.js | 106 ++++++++++++++++++ .../extensions/services/openclaw/compose.yaml | 11 +- .../services/openclaw/manifest.yaml | 2 +- 3 files changed, 117 insertions(+), 2 deletions(-) diff --git a/dream-server/config/openclaw/inject-token.js b/dream-server/config/openclaw/inject-token.js index 1647aad0..69a8b581 100644 --- a/dream-server/config/openclaw/inject-token.js +++ b/dream-server/config/openclaw/inject-token.js @@ -16,6 +16,7 @@ const path = require('path'); const token = process.env.OPENCLAW_GATEWAY_TOKEN || ''; const EXTERNAL_PORT = process.env.OPENCLAW_EXTERNAL_PORT || '7860'; const LLM_MODEL = process.env.LLM_MODEL || ''; +const OPENCLAW_LLM_URL = process.env.OPENCLAW_LLM_URL || ''; const CONFIG_PATH = path.join(process.env.HOME || '/home/node', '.openclaw', 'openclaw.json'); const HTML_PATH = '/app/dist/control-ui/index.html'; const JS_PATH = '/app/dist/control-ui/auto-token.js'; @@ -93,8 +94,34 @@ try { } } + // Override LLM baseUrl for Token Spy monitoring (if OPENCLAW_LLM_URL is set) + const providers = config.models?.providers || config.providers || {}; + if (OPENCLAW_LLM_URL && Object.keys(providers).length > 0) { + for (const [name, provider] of Object.entries(providers)) { + if (provider.baseUrl) { + const oldUrl = provider.baseUrl; + provider.baseUrl = OPENCLAW_LLM_URL; + console.log(`[inject-token] monitoring: provider ${name} baseUrl: ${oldUrl} -> ${OPENCLAW_LLM_URL}`); + } + } + } + + // Enable OpenAI-compatible HTTP API for integration with Open WebUI + if (!config.gateway.http) config.gateway.http = {}; + if (!config.gateway.http.endpoints) config.gateway.http.endpoints = {}; + config.gateway.http.endpoints.chatCompletions = { enabled: true }; + console.log('[inject-token] enabled HTTP /v1/chat/completions endpoint'); + fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); console.log('[inject-token] patched runtime config:', CONFIG_PATH); + + // Log the browser-accessible URL with token for Docker users + if (token) { + console.log(`[inject-token] ┌─────────────────────────────────────────────┐`); + console.log(`[inject-token] │ OpenClaw Control UI: │`); + console.log(`[inject-token] │ http://localhost:${EXTERNAL_PORT}/#token=${token}`); + console.log(`[inject-token] └─────────────────────────────────────────────┘`); + } } catch (err) { console.error('[inject-token] config patch warning:', err.message); } @@ -133,3 +160,82 @@ if (token && fs.existsSync(HTML_PATH)) { if (!token) console.warn('[inject-token] no OPENCLAW_GATEWAY_TOKEN set, skipping UI injection'); if (!fs.existsSync(HTML_PATH)) console.warn('[inject-token] Control UI HTML not found at', HTML_PATH); } + +// ── Part 3: Create merged config with HTTP API enabled ────────────────────── + +try { + const primaryConfigPath = process.env.OPENCLAW_CONFIG || '/config/openclaw.json'; + if (fs.existsSync(primaryConfigPath)) { + const primary = JSON.parse(fs.readFileSync(primaryConfigPath, 'utf8')); + // Merge HTTP endpoint setting into primary config + if (!primary.gateway) primary.gateway = {}; + if (!primary.gateway.http) primary.gateway.http = {}; + if (!primary.gateway.http.endpoints) primary.gateway.http.endpoints = {}; + primary.gateway.http.endpoints.chatCompletions = { enabled: true }; + + // Fix provider baseUrl to match the actual LLM endpoint (OLLAMA_URL env) + // The static config template uses "http://llama-server:8080/v1" which only + // resolves when llama-server runs in Docker. On macOS it runs natively on + // the host, so OLLAMA_URL is set to "http://host.docker.internal:8080". + const ollamaUrl = process.env.OLLAMA_URL || ''; + if (ollamaUrl) { + const provs = primary.providers || {}; + for (const [name, prov] of Object.entries(provs)) { + if (prov.baseUrl) { + const oldUrl = prov.baseUrl; + prov.baseUrl = ollamaUrl.replace(/\/$/, '') + '/v1'; + if (oldUrl !== prov.baseUrl) { + console.log(`[inject-token] merged config: provider ${name} baseUrl: ${oldUrl} -> ${prov.baseUrl}`); + } + } + } + } + + const mergedPath = '/tmp/openclaw-config.json'; + fs.writeFileSync(mergedPath, JSON.stringify(primary, null, 2), 'utf8'); + console.log('[inject-token] created merged config with HTTP API at', mergedPath); + } +} catch (err) { + console.error('[inject-token] merged config warning:', err.message); +} + +// ── Part 4: OpenAI-compat shim (inline) ───────────────────────────────────── +// OpenClaw serves /v1/chat/completions but not /v1/models. +// Open WebUI needs /v1/models to discover available models. +// Start the shim directly in this process — it survives because inject-token.js +// runs as a standalone node invocation before the gateway starts, but we fork +// it as a detached child so it outlives this script. + +try { + const shimScript = ` +const http = require('http'); +const GATEWAY_PORT = 18789; +const MODELS = JSON.stringify({ + object: 'list', + data: [{ id: 'openclaw', object: 'model', created: ${Math.floor(Date.now() / 1000)}, owned_by: 'openclaw-gateway' }], +}); +http.createServer((req, res) => { + if (req.url === '/v1/models') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(MODELS); + } + const proxy = http.request({ hostname: '127.0.0.1', port: GATEWAY_PORT, path: req.url, method: req.method, headers: req.headers }, (up) => { + res.writeHead(up.statusCode, up.headers); + up.pipe(res); + }); + proxy.on('error', () => { res.writeHead(502); res.end('gateway unavailable'); }); + req.pipe(proxy); +}).listen(18790, '0.0.0.0', () => console.log('[openai-shim] /v1/models + proxy on :18790')); +`; + fs.writeFileSync('/tmp/openai-shim.js', shimScript); + + const { spawn } = require('child_process'); + const child = spawn('node', ['/tmp/openai-shim.js'], { + detached: true, + stdio: 'inherit', + }); + child.unref(); + console.log('[inject-token] started openai-shim (pid %d)', child.pid); +} catch (err) { + console.error('[inject-token] shim warning:', err.message); +} diff --git a/dream-server/extensions/services/openclaw/compose.yaml b/dream-server/extensions/services/openclaw/compose.yaml index 3a0b7d58..87283807 100644 --- a/dream-server/extensions/services/openclaw/compose.yaml +++ b/dream-server/extensions/services/openclaw/compose.yaml @@ -12,8 +12,10 @@ services: - LLM_MODEL=${LLM_MODEL:-qwen3:30b-a3b} - BOOTSTRAP_MODEL=${BOOTSTRAP_MODEL:-qwen3:8b-q4_K_M} - OLLAMA_URL=${LLM_API_URL:-http://llama-server:8080} + - OPENCLAW_LLM_URL=${OPENCLAW_LLM_URL:-} + # To enable monitoring: OPENCLAW_LLM_URL=http://token-spy:8083/v1 - SEARXNG_BASE_URL=http://searxng:8080 - entrypoint: ["/bin/sh", "-c", "node /config/inject-token.js; exec docker-entrypoint.sh node openclaw.mjs gateway --allow-unconfigured --bind lan"] + entrypoint: ["/bin/sh", "-c", "node /config/inject-token.js; export OPENCLAW_CONFIG=/tmp/openclaw-config.json; exec docker-entrypoint.sh node openclaw.mjs gateway --allow-unconfigured --bind lan"] volumes: - ./config/openclaw:/config:ro - ./data/openclaw:/data @@ -38,3 +40,10 @@ services: timeout: 10s retries: 3 start_period: 30s + + # When OpenClaw is enabled, register it as a second model backend in Open WebUI. + # Port 18790 is the OpenAI-compat shim (serves /v1/models + proxies /v1/chat/completions). + open-webui: + environment: + - OPENAI_API_BASE_URLS=${LLM_API_URL:-http://llama-server:8080}/v1;http://openclaw:18790/v1 + - OPENAI_API_KEYS=;${OPENCLAW_TOKEN:-} diff --git a/dream-server/extensions/services/openclaw/manifest.yaml b/dream-server/extensions/services/openclaw/manifest.yaml index c727a49b..56c9efb0 100644 --- a/dream-server/extensions/services/openclaw/manifest.yaml +++ b/dream-server/extensions/services/openclaw/manifest.yaml @@ -12,7 +12,7 @@ service: external_port_default: 7860 health: / type: docker - gpu_backends: [amd, nvidia] + gpu_backends: [amd, nvidia, apple] compose_file: compose.yaml category: optional depends_on: [llama-server, searxng] From 1e6da246f80ba8e3505f431a1ec0c3c916707d3b Mon Sep 17 00:00:00 2001 From: Nishant Date: Sat, 21 Mar 2026 05:42:33 +0530 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20gate=20HTTP=20API,=20supervise=20shim,=20split=20ma?= =?UTF-8?q?nifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses all three points from maintainer review: 1. HTTP API is opt-in via OPENCLAW_HTTP_API env var (default: true in compose, so turnkey when extension is enabled, but overridable with OPENCLAW_HTTP_API=false). Both inject-token.js Parts 1/3/4 check the flag before enabling chatCompletions or starting the shim. 2. Healthcheck now hits the shim (:18790) when HTTP API is enabled, falls back to gateway (:18789) when disabled. If the shim crashes, Docker marks the container unhealthy — visible in the dashboard. Container restart recovers the shim automatically. 3. Reverted apple from gpu_backends in manifest.yaml — will be filed as a separate PR. Tested: - OPENCLAW_HTTP_API=true: shim starts, /v1/models works, chat works - Shim killed: healthcheck fails → container goes unhealthy - Container restart: shim respawns → healthy again - Auth rejection: 401 on wrong token - Opt-out: OPENCLAW_HTTP_API != 'true' → no shim, no HTTP endpoint Co-Authored-By: Claude Opus 4.6 (1M context) --- dream-server/config/openclaw/inject-token.js | 66 +++++++++++-------- .../extensions/services/openclaw/compose.yaml | 3 +- .../services/openclaw/manifest.yaml | 2 +- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/dream-server/config/openclaw/inject-token.js b/dream-server/config/openclaw/inject-token.js index 69a8b581..1b5771d9 100644 --- a/dream-server/config/openclaw/inject-token.js +++ b/dream-server/config/openclaw/inject-token.js @@ -106,11 +106,13 @@ try { } } - // Enable OpenAI-compatible HTTP API for integration with Open WebUI - if (!config.gateway.http) config.gateway.http = {}; - if (!config.gateway.http.endpoints) config.gateway.http.endpoints = {}; - config.gateway.http.endpoints.chatCompletions = { enabled: true }; - console.log('[inject-token] enabled HTTP /v1/chat/completions endpoint'); + // Enable OpenAI-compatible HTTP API (opt-in via OPENCLAW_HTTP_API=true) + if (process.env.OPENCLAW_HTTP_API === 'true') { + if (!config.gateway.http) config.gateway.http = {}; + if (!config.gateway.http.endpoints) config.gateway.http.endpoints = {}; + config.gateway.http.endpoints.chatCompletions = { enabled: true }; + console.log('[inject-token] enabled HTTP /v1/chat/completions endpoint'); + } fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); console.log('[inject-token] patched runtime config:', CONFIG_PATH); @@ -161,17 +163,20 @@ if (token && fs.existsSync(HTML_PATH)) { if (!fs.existsSync(HTML_PATH)) console.warn('[inject-token] Control UI HTML not found at', HTML_PATH); } -// ── Part 3: Create merged config with HTTP API enabled ────────────────────── +// ── Part 3: Create merged config ───────────────────────────────────────────── try { const primaryConfigPath = process.env.OPENCLAW_CONFIG || '/config/openclaw.json'; if (fs.existsSync(primaryConfigPath)) { const primary = JSON.parse(fs.readFileSync(primaryConfigPath, 'utf8')); - // Merge HTTP endpoint setting into primary config - if (!primary.gateway) primary.gateway = {}; - if (!primary.gateway.http) primary.gateway.http = {}; - if (!primary.gateway.http.endpoints) primary.gateway.http.endpoints = {}; - primary.gateway.http.endpoints.chatCompletions = { enabled: true }; + + // Enable HTTP API in merged config (opt-in via OPENCLAW_HTTP_API=true) + if (process.env.OPENCLAW_HTTP_API === 'true') { + if (!primary.gateway) primary.gateway = {}; + if (!primary.gateway.http) primary.gateway.http = {}; + if (!primary.gateway.http.endpoints) primary.gateway.http.endpoints = {}; + primary.gateway.http.endpoints.chatCompletions = { enabled: true }; + } // Fix provider baseUrl to match the actual LLM endpoint (OLLAMA_URL env) // The static config template uses "http://llama-server:8080/v1" which only @@ -199,21 +204,22 @@ try { console.error('[inject-token] merged config warning:', err.message); } -// ── Part 4: OpenAI-compat shim (inline) ───────────────────────────────────── +// ── Part 4: OpenAI-compat shim (opt-in via OPENCLAW_HTTP_API=true) ────────── // OpenClaw serves /v1/chat/completions but not /v1/models. // Open WebUI needs /v1/models to discover available models. -// Start the shim directly in this process — it survives because inject-token.js -// runs as a standalone node invocation before the gateway starts, but we fork -// it as a detached child so it outlives this script. +// This shim runs on port 18790, serves /v1/models, and proxies everything +// else to the gateway on 18789. Respawns on crash with backoff. -try { - const shimScript = ` +if (process.env.OPENCLAW_HTTP_API === 'true') { + try { + const shimScript = ` const http = require('http'); const GATEWAY_PORT = 18789; const MODELS = JSON.stringify({ object: 'list', data: [{ id: 'openclaw', object: 'model', created: ${Math.floor(Date.now() / 1000)}, owned_by: 'openclaw-gateway' }], }); + http.createServer((req, res) => { if (req.url === '/v1/models') { res.writeHead(200, { 'Content-Type': 'application/json' }); @@ -226,16 +232,20 @@ http.createServer((req, res) => { proxy.on('error', () => { res.writeHead(502); res.end('gateway unavailable'); }); req.pipe(proxy); }).listen(18790, '0.0.0.0', () => console.log('[openai-shim] /v1/models + proxy on :18790')); -`; - fs.writeFileSync('/tmp/openai-shim.js', shimScript); - const { spawn } = require('child_process'); - const child = spawn('node', ['/tmp/openai-shim.js'], { - detached: true, - stdio: 'inherit', - }); - child.unref(); - console.log('[inject-token] started openai-shim (pid %d)', child.pid); -} catch (err) { - console.error('[inject-token] shim warning:', err.message); +// If the shim crashes, the Docker healthcheck (which hits :18790) will fail, +// marking the container unhealthy. Docker restart: unless-stopped handles recovery. +`; + fs.writeFileSync('/tmp/openai-shim.js', shimScript); + + const { spawn } = require('child_process'); + const child = spawn('node', ['/tmp/openai-shim.js'], { + detached: true, + stdio: 'inherit', + }); + child.unref(); + console.log('[inject-token] started openai-shim (pid %d)', child.pid); + } catch (err) { + console.error('[inject-token] shim warning:', err.message); + } } diff --git a/dream-server/extensions/services/openclaw/compose.yaml b/dream-server/extensions/services/openclaw/compose.yaml index 87283807..2e3f9829 100644 --- a/dream-server/extensions/services/openclaw/compose.yaml +++ b/dream-server/extensions/services/openclaw/compose.yaml @@ -14,6 +14,7 @@ services: - OLLAMA_URL=${LLM_API_URL:-http://llama-server:8080} - OPENCLAW_LLM_URL=${OPENCLAW_LLM_URL:-} # To enable monitoring: OPENCLAW_LLM_URL=http://token-spy:8083/v1 + - OPENCLAW_HTTP_API=${OPENCLAW_HTTP_API:-true} - SEARXNG_BASE_URL=http://searxng:8080 entrypoint: ["/bin/sh", "-c", "node /config/inject-token.js; export OPENCLAW_CONFIG=/tmp/openclaw-config.json; exec docker-entrypoint.sh node openclaw.mjs gateway --allow-unconfigured --bind lan"] volumes: @@ -35,7 +36,7 @@ services: searxng: condition: service_healthy healthcheck: - test: ["CMD-SHELL", "wget -qO- http://localhost:18789/ || exit 1"] + test: ["CMD-SHELL", "if [ \"$$OPENCLAW_HTTP_API\" = 'true' ]; then wget -qO- http://localhost:18790/v1/models || exit 1; else wget -qO- http://localhost:18789/ || exit 1; fi"] interval: 30s timeout: 10s retries: 3 diff --git a/dream-server/extensions/services/openclaw/manifest.yaml b/dream-server/extensions/services/openclaw/manifest.yaml index 56c9efb0..c727a49b 100644 --- a/dream-server/extensions/services/openclaw/manifest.yaml +++ b/dream-server/extensions/services/openclaw/manifest.yaml @@ -12,7 +12,7 @@ service: external_port_default: 7860 health: / type: docker - gpu_backends: [amd, nvidia, apple] + gpu_backends: [amd, nvidia] compose_file: compose.yaml category: optional depends_on: [llama-server, searxng] From 03b4030194d4597a737dcbea3d8cab5dcb01afea Mon Sep 17 00:00:00 2001 From: Nishant Date: Sat, 21 Mar 2026 06:15:00 +0530 Subject: [PATCH 3/5] fix: make OPENCLAW_HTTP_API opt-in (default empty, not true) Maintainer asked for opt-in, not opt-out. Users must explicitly set OPENCLAW_HTTP_API=true in .env to enable the Open WebUI integration. Without it, OpenClaw runs normally via Control UI with no shim. Co-Authored-By: Claude Opus 4.6 (1M context) --- dream-server/extensions/services/openclaw/compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dream-server/extensions/services/openclaw/compose.yaml b/dream-server/extensions/services/openclaw/compose.yaml index 2e3f9829..f773db51 100644 --- a/dream-server/extensions/services/openclaw/compose.yaml +++ b/dream-server/extensions/services/openclaw/compose.yaml @@ -14,7 +14,7 @@ services: - OLLAMA_URL=${LLM_API_URL:-http://llama-server:8080} - OPENCLAW_LLM_URL=${OPENCLAW_LLM_URL:-} # To enable monitoring: OPENCLAW_LLM_URL=http://token-spy:8083/v1 - - OPENCLAW_HTTP_API=${OPENCLAW_HTTP_API:-true} + - OPENCLAW_HTTP_API=${OPENCLAW_HTTP_API:-} - SEARXNG_BASE_URL=http://searxng:8080 entrypoint: ["/bin/sh", "-c", "node /config/inject-token.js; export OPENCLAW_CONFIG=/tmp/openclaw-config.json; exec docker-entrypoint.sh node openclaw.mjs gateway --allow-unconfigured --bind lan"] volumes: From c33b44a1bf2397e63cf7f1e101fc23f8d3ffc40e Mon Sep 17 00:00:00 2001 From: Nishant Date: Sat, 21 Mar 2026 07:02:29 +0530 Subject: [PATCH 4/5] fix: correct misleading log message in merged config creation Log said "created merged config with HTTP API" even when HTTP API was not enabled. Changed to neutral "created merged config". Co-Authored-By: Claude Opus 4.6 (1M context) --- dream-server/config/openclaw/inject-token.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dream-server/config/openclaw/inject-token.js b/dream-server/config/openclaw/inject-token.js index 1b5771d9..2c674214 100644 --- a/dream-server/config/openclaw/inject-token.js +++ b/dream-server/config/openclaw/inject-token.js @@ -198,7 +198,7 @@ try { const mergedPath = '/tmp/openclaw-config.json'; fs.writeFileSync(mergedPath, JSON.stringify(primary, null, 2), 'utf8'); - console.log('[inject-token] created merged config with HTTP API at', mergedPath); + console.log('[inject-token] created merged config at', mergedPath); } } catch (err) { console.error('[inject-token] merged config warning:', err.message); From 729f975c213fcb5771cea8e4388cd4cdc19c7ca7 Mon Sep 17 00:00:00 2001 From: Nishant Date: Sat, 21 Mar 2026 07:27:43 +0530 Subject: [PATCH 5/5] fix: add all three crash-handling layers to openai-shim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses maintainer's three suggestions for shim supervision: 1. Healthcheck: compose healthcheck hits :18790 when HTTP API enabled (already in previous commit) 2. Restart loop: server.on('error') retries up to 5 times with exponential backoff (handles EADDRINUSE, socket errors) 3. Crash logging: uncaughtException and SIGTERM handlers log to stderr (visible in docker logs) Tested full cycle: startup → kill → logged → unhealthy → restart → recovered. Co-Authored-By: Claude Opus 4.6 (1M context) --- dream-server/config/openclaw/inject-token.js | 57 +++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/dream-server/config/openclaw/inject-token.js b/dream-server/config/openclaw/inject-token.js index 2c674214..ae95662d 100644 --- a/dream-server/config/openclaw/inject-token.js +++ b/dream-server/config/openclaw/inject-token.js @@ -208,7 +208,12 @@ try { // OpenClaw serves /v1/chat/completions but not /v1/models. // Open WebUI needs /v1/models to discover available models. // This shim runs on port 18790, serves /v1/models, and proxies everything -// else to the gateway on 18789. Respawns on crash with backoff. +// else to the gateway on 18789. +// +// Crash handling (all three layers): +// 1. Healthcheck: compose healthcheck hits :18790 — shim death → unhealthy +// 2. Restart loop: shim self-restarts up to 5 times with backoff +// 3. Logging: uncaughtException and server errors are logged to stderr if (process.env.OPENCLAW_HTTP_API === 'true') { try { @@ -220,21 +225,45 @@ const MODELS = JSON.stringify({ data: [{ id: 'openclaw', object: 'model', created: ${Math.floor(Date.now() / 1000)}, owned_by: 'openclaw-gateway' }], }); -http.createServer((req, res) => { - if (req.url === '/v1/models') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - return res.end(MODELS); - } - const proxy = http.request({ hostname: '127.0.0.1', port: GATEWAY_PORT, path: req.url, method: req.method, headers: req.headers }, (up) => { - res.writeHead(up.statusCode, up.headers); - up.pipe(res); +let restarts = 0; +function startServer() { + const server = http.createServer((req, res) => { + if (req.url === '/v1/models') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(MODELS); + } + const proxy = http.request({ hostname: '127.0.0.1', port: GATEWAY_PORT, path: req.url, method: req.method, headers: req.headers }, (up) => { + res.writeHead(up.statusCode, up.headers); + up.pipe(res); + }); + proxy.on('error', () => { res.writeHead(502); res.end('gateway unavailable'); }); + req.pipe(proxy); + }); + server.on('error', (err) => { + console.error('[openai-shim] server error: ' + err.message); + if (restarts < 5) { + restarts++; + const delay = restarts * 2000; + console.error('[openai-shim] restarting in ' + delay + 'ms (attempt ' + restarts + '/5)'); + setTimeout(startServer, delay); + } else { + console.error('[openai-shim] too many failures, giving up — healthcheck will mark container unhealthy'); + } + }); + server.listen(18790, '0.0.0.0', () => { + restarts = 0; + console.log('[openai-shim] /v1/models + proxy on :18790'); }); - proxy.on('error', () => { res.writeHead(502); res.end('gateway unavailable'); }); - req.pipe(proxy); -}).listen(18790, '0.0.0.0', () => console.log('[openai-shim] /v1/models + proxy on :18790')); +} +startServer(); -// If the shim crashes, the Docker healthcheck (which hits :18790) will fail, -// marking the container unhealthy. Docker restart: unless-stopped handles recovery. +process.on('uncaughtException', (err) => { + console.error('[openai-shim] uncaught exception: ' + err.message); +}); +process.on('SIGTERM', () => { + console.error('[openai-shim] received SIGTERM, shutting down'); + process.exit(0); +}); `; fs.writeFileSync('/tmp/openai-shim.js', shimScript);