From f8e5e947ec5270c264e9783a4dddcc44f0d87c78 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sat, 7 Mar 2026 23:07:38 +0000 Subject: [PATCH] fix: route Claude API through backend instead of nginx envsubst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Claude API proxy in mock-backend.js (reads ANTHROPIC_API_KEY from env) - Supports SSE streaming via pipe - Move ANTHROPIC_API_KEY to backend service in docker-compose.demo.yml - Remove envsubst from entrypoint (no longer needed) - nginx-demo.conf proxies /aiui/api/claude/ to backend This fixes the 401 error when Portainer doesn't pass env vars to nginx correctly — the Node.js backend reads process.env directly. Co-Authored-By: Claude Opus 4.6 --- docker-compose.demo.yml | 3 +- neode-ui/docker/docker-entrypoint.sh | 4 +-- neode-ui/docker/nginx-demo.conf | 17 +++------- neode-ui/mock-backend.js | 49 ++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 16 deletions(-) diff --git a/docker-compose.demo.yml b/docker-compose.demo.yml index 132e2d03..5aa9eb09 100644 --- a/docker-compose.demo.yml +++ b/docker-compose.demo.yml @@ -13,6 +13,7 @@ services: container_name: archy-demo-backend environment: VITE_DEV_MODE: "existing" # Skip setup/onboarding, go straight to login + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} expose: - "5959" restart: unless-stopped @@ -22,8 +23,6 @@ services: context: . dockerfile: neode-ui/Dockerfile.web container_name: archy-demo-web - environment: - ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} ports: - "4848:80" depends_on: diff --git a/neode-ui/docker/docker-entrypoint.sh b/neode-ui/docker/docker-entrypoint.sh index 2fcbc59c..3fb0ad16 100644 --- a/neode-ui/docker/docker-entrypoint.sh +++ b/neode-ui/docker/docker-entrypoint.sh @@ -1,4 +1,4 @@ #!/bin/sh -# Substitute only ANTHROPIC_API_KEY in nginx config, leave nginx variables untouched -envsubst '${ANTHROPIC_API_KEY}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf +# Copy nginx config (no envsubst needed — API key is handled by backend) +cp /etc/nginx/nginx.conf.template /etc/nginx/nginx.conf exec nginx -g 'daemon off;' diff --git a/neode-ui/docker/nginx-demo.conf b/neode-ui/docker/nginx-demo.conf index 34e46583..4381040d 100644 --- a/neode-ui/docker/nginx-demo.conf +++ b/neode-ui/docker/nginx-demo.conf @@ -78,22 +78,15 @@ http { } } - # Proxy Claude API requests from AIUI to Anthropic + # Proxy Claude API requests to backend (which handles API key + streaming) location /aiui/api/claude/ { - rewrite ^/aiui/api/claude/(.*)$ /$1 break; - - proxy_pass https://api.anthropic.com; - proxy_ssl_server_name on; - proxy_set_header Host api.anthropic.com; - proxy_set_header x-api-key "${ANTHROPIC_API_KEY}"; - proxy_set_header anthropic-version "2023-06-01"; - proxy_set_header Content-Type "application/json"; - - # SSE streaming support + proxy_pass http://neode-backend:5959; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; proxy_buffering off; proxy_cache off; proxy_read_timeout 300s; - proxy_http_version 1.1; proxy_set_header Connection ""; } diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 95f7dd47..4340e12d 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -1016,6 +1016,55 @@ app.get('/app/filebrowser/api/raw/*', (req, res) => { } }) +// ============================================================================= +// Claude API Proxy (reads ANTHROPIC_API_KEY from environment) +// ============================================================================= +import https from 'https' + +app.post('/aiui/api/claude/*', (req, res) => { + const apiKey = process.env.ANTHROPIC_API_KEY + if (!apiKey) { + return res.status(500).json({ + type: 'error', + error: { type: 'configuration_error', message: 'ANTHROPIC_API_KEY not configured on server' } + }) + } + + const apiPath = '/' + req.params[0] + const bodyStr = JSON.stringify(req.body) + + const options = { + hostname: 'api.anthropic.com', + port: 443, + path: apiPath, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + 'Content-Length': Buffer.byteLength(bodyStr), + }, + } + + const proxyReq = https.request(options, (proxyRes) => { + res.writeHead(proxyRes.statusCode, proxyRes.headers) + proxyRes.pipe(res) + }) + + proxyReq.on('error', (err) => { + console.error('[Claude Proxy] Error:', err.message) + if (!res.headersSent) { + res.status(502).json({ + type: 'error', + error: { type: 'proxy_error', message: err.message } + }) + } + }) + + proxyReq.write(bodyStr) + proxyReq.end() +}) + // Health check app.get('/health', (req, res) => { res.status(200).send('healthy')