openvibers/openvibe.ai Commits

AI: the public vhost proxies /v1/chat/completions (POST, unbuffered) and /v1/models (GET) (#71)

OP OpenVibers committed 29 days ago · 55784d52e1 · 2 files, +33 −1

**ai.openvibe.services lets OpenAI-compatible requests through: `POST /v1/chat/completions` and `GET /v1/models` (follow-up to #70).**

**Why:** the production proof of #70 had nginx answer `403` to `POST /v1/chat/completions`. The vhost proxies only the developer-app routes it names, and the catch-all `location /` allows only GET and HEAD. `GET /v1/models` worked only because that catch-all let it through.

**The vhost (`deploy/nginx/ai.openvibe.services.conf`):**
- `location = /v1/chat/completions` takes POST only. It uses the same `ovai_apps` rate limit, `$remote_addr`-only client headers and proxying as the six operations.
- That location also sets `proxy_buffering off`, so `stream: true` events arrive as they are sent, and `proxy_read_timeout 120s`.
- `location = /v1/models` takes GET only, with the same limits.

**Test:** `home.test.js` now checks both locations: their verbs, the rate limit, the client headers, and that streamed answers are not buffered.

Changes

ddiff --git a/deploy/nginx/ai.openvibe.services.conf b/deploy/nginx/ai.openvibe.services.conf
iindex abed85d..53a8c1c 100644
---- a/deploy/nginx/ai.openvibe.services.conf
++++ b/deploy/nginx/ai.openvibe.services.conf
@@@ -88,6 +88,34 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
}
++ # ── OpenAI-compatible (the same app tokens): Chat Completions and the model list ──
++ location = /v1/chat/completions {
++ limit_req zone=ovai_apps burst=20 nodelay;
++ limit_req_status 429;
++ limit_except POST { deny all; }
++ proxy_pass http://127.0.0.1:4700;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $remote_addr;
++ proxy_set_header CF-Connecting-IP $remote_addr;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ proxy_buffering off; # stream: true answers server-sent events
++ proxy_read_timeout 120s;
++ }
++ location = /v1/models {
++ limit_req zone=ovai_apps burst=20 nodelay;
++ limit_req_status 429;
++ limit_except GET { deny all; }
++ proxy_pass http://127.0.0.1:4700;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $remote_addr;
++ proxy_set_header CF-Connecting-IP $remote_addr;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
++
# ── Service API: host-local only (see header) ───────────
location /api/ {
return 403;
ddiff --git a/test/home.test.js b/test/home.test.js
iindex d9e339c..3486c12 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -87,8 +87,12 @@ t.test('the public vhost proxies the home, its files and exactly the developer-a
for (const loc of ['location = / {', 'location = /stats', 'location = /sitemap.xml', 'location = /llms.txt', 'location /shared/']) assert.ok(conf.includes(loc), loc);
const ops = conf.indexOf('location ~ ^/api/v1/(chat|generate|summarize|classify|extract|embed)$ {');
const run = conf.indexOf('location ~ ^/api/v1/runs/[A-Za-z0-9_-]+$ {');
++ const completions = conf.indexOf('location = /v1/chat/completions {');
++ const models = conf.indexOf('location = /v1/models {');
assert.ok(ops >= 0 && run >= 0, 'the six operations and the run read');
-- for (const [i, verb] of [[ops, 'POST'], [run, 'GET']]) {
++ assert.ok(completions >= 0 && models >= 0, 'the OpenAI-compatible endpoint and its model list');
++ assert.ok(conf.slice(completions, conf.indexOf('\n }', completions)).includes('proxy_buffering off;'), 'streamed answers are not buffered');
++ for (const [i, verb] of [[ops, 'POST'], [run, 'GET'], [completions, 'POST'], [models, 'GET']]) {
const block = conf.slice(i, conf.indexOf('\n }', i));
assert.ok(block.includes(`limit_except ${verb} { deny all; }`), verb);
assert.ok(block.includes('limit_req zone=ovai_apps'), 'rate-limited per address');