diff --git a/docs/reference/openapi.yaml b/docs/reference/openapi.yaml index 0fd408fa1e..7dbf3da566 100644 --- a/docs/reference/openapi.yaml +++ b/docs/reference/openapi.yaml @@ -1104,6 +1104,42 @@ paths: "502": description: All upstream providers failed + /api/v1/ws: + get: + tags: [Chat] + summary: Chat completion over WebSocket (handshake + upgrade) + description: >- + OpenAI-compatible chat over a WebSocket connection. `GET` with + `?handshake=1` returns the connection descriptor (auth path, message + protocol and live-event channels) as JSON; a plain `GET` without an + Upgrade returns `426 Upgrade Required`. After upgrading, the client + exchanges JSON frames — `{type:"request", id, payload:{model, messages}}` + to start a completion and `{type:"cancel", id}` to abort it. A separate + live channel (default port `LIVE_WS_PORT=20129`, path `/live`) streams + dashboard events on the `requests`, `combo` and `credentials` topics with + a 15s heartbeat. Requires an API key. + security: + - BearerAuth: [] + parameters: + - name: handshake + in: query + description: Set to `1` to receive the JSON connection descriptor instead of upgrading. + required: false + schema: + type: string + enum: ["1"] + responses: + "101": + description: WebSocket upgrade successful + "200": + description: Handshake descriptor (auth path, message protocol, live channels) + "401": + description: WebSocket auth required (no credential supplied) + "403": + description: Invalid WebSocket credential + "426": + description: Upgrade Required — connect via WebSocket or use `?handshake=1` + /api/v1/providers/{provider}/chat/completions: post: tags: [Chat] diff --git a/tests/unit/openapi-ws-endpoint.test.ts b/tests/unit/openapi-ws-endpoint.test.ts new file mode 100644 index 0000000000..a6d4efc055 --- /dev/null +++ b/tests/unit/openapi-ws-endpoint.test.ts @@ -0,0 +1,32 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import yaml from "js-yaml"; + +// Regression guard: the OpenAI-compatible chat WebSocket endpoint (/api/v1/ws) +// must be documented in openapi.yaml so it shows up on the dashboard's +// "API Endpoints" page (which renders /api/openapi/spec, parsed from this file). +// The route (src/app/api/v1/ws/route.ts) shipped in v3.6.6 but was never listed +// in the spec, so it was invisible in the endpoints reference. + +const ROOT = fileURLToPath(new URL("../../", import.meta.url)); +const spec = yaml.load(readFileSync(ROOT + "docs/reference/openapi.yaml", "utf8")) as { + paths: Record }>>; +}; + +test("openapi.yaml documents the /api/v1/ws chat WebSocket endpoint", () => { + const entry = spec.paths["/api/v1/ws"]; + assert.ok(entry, "/api/v1/ws must be present in openapi.yaml paths"); + assert.ok(entry.get, "/api/v1/ws must document the GET (handshake/upgrade) operation"); +}); + +test("/api/v1/ws is tagged, authenticated and documents the WS upgrade responses", () => { + const op = spec.paths["/api/v1/ws"].get; + assert.ok((op.tags ?? []).length > 0, "should be tagged so it groups on the endpoints page"); + assert.ok(Array.isArray(op.security) && op.security.length > 0, "should require auth (BearerAuth)"); + const responses = op.responses ?? {}; + for (const code of ["101", "426"]) { + assert.ok(code in responses, `should document the ${code} WebSocket response`); + } +});