feat(gateway): add config toggle for chat completions endpoint

This commit is contained in:
Peter Steinberger
2026-01-10 22:34:25 +01:00
parent 050c1c5391
commit 1110d96769
8 changed files with 88 additions and 3 deletions

View File

@@ -1803,6 +1803,7 @@ Related docs:
Notes:
- `clawdbot gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
- `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).
- Disable the OpenAI-compatible endpoint with `gateway.http.endpoints.chatCompletions.enabled: false`.
- Precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`.
- Non-loopback binds (`lan`/`tailnet`/`auto`) require auth. Use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
- `gateway.remote.token` is **only** for remote CLI calls; it does not enable local gateway auth. `gateway.token` is ignored.

View File

@@ -36,6 +36,22 @@ Or target a specific Clawdbot agent by header:
Advanced:
- `x-clawdbot-session-key: <sessionKey>` to fully control session routing.
## Disabling the endpoint
Set `gateway.http.endpoints.chatCompletions.enabled` to `false`:
```json5
{
gateway: {
http: {
endpoints: {
chatCompletions: { enabled: false }
}
}
}
}
```
## Session behavior
By default the endpoint is **stateless per request** (a new session key is generated each call).

View File

@@ -90,6 +90,8 @@ const FIELD_LABELS: Record<string, string> = {
"gateway.auth.token": "Gateway Token",
"gateway.auth.password": "Gateway Password",
"gateway.controlUi.basePath": "Control UI Base Path",
"gateway.http.endpoints.chatCompletions.enabled":
"OpenAI Chat Completions Endpoint",
"gateway.reload.mode": "Config Reload Mode",
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
"agents.defaults.workspace": "Workspace",
@@ -158,6 +160,8 @@ const FIELD_HELP: Record<string, string> = {
"gateway.auth.password": "Required for Tailscale funnel.",
"gateway.controlUi.basePath":
"Optional URL prefix where the Control UI is served (e.g. /clawdbot).",
"gateway.http.endpoints.chatCompletions.enabled":
"Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: true).",
"gateway.reload.mode":
'Hot reload strategy for config changes ("hybrid" recommended).',
"gateway.reload.debounceMs":

View File

@@ -1155,6 +1155,22 @@ export type GatewayReloadConfig = {
debounceMs?: number;
};
export type GatewayHttpChatCompletionsConfig = {
/**
* If false, the Gateway will not serve `POST /v1/chat/completions`.
* Default: true when absent.
*/
enabled?: boolean;
};
export type GatewayHttpEndpointsConfig = {
chatCompletions?: GatewayHttpChatCompletionsConfig;
};
export type GatewayHttpConfig = {
endpoints?: GatewayHttpEndpointsConfig;
};
export type GatewayConfig = {
/** Single multiplexed port for Gateway WS + HTTP (default: 18789). */
port?: number;
@@ -1173,6 +1189,7 @@ export type GatewayConfig = {
tailscale?: GatewayTailscaleConfig;
remote?: GatewayRemoteConfig;
reload?: GatewayReloadConfig;
http?: GatewayHttpConfig;
};
export type SkillConfig = {

View File

@@ -1477,6 +1477,19 @@ export const ClawdbotSchema = z
debounceMs: z.number().int().min(0).optional(),
})
.optional(),
http: z
.object({
endpoints: z
.object({
chatCompletions: z
.object({
enabled: z.boolean().optional(),
})
.optional(),
})
.optional(),
})
.optional(),
})
.optional(),
skills: z

View File

@@ -9,12 +9,16 @@ import {
installGatewayTestHooks();
async function startServer(port: number) {
async function startServer(
port: number,
opts?: { openAiChatCompletionsEnabled?: boolean },
) {
const { startGatewayServer } = await import("./server.js");
return await startGatewayServer(port, {
host: "127.0.0.1",
auth: { mode: "token", token: "secret" },
controlUiEnabled: false,
openAiChatCompletionsEnabled: opts?.openAiChatCompletionsEnabled,
});
}
@@ -44,6 +48,22 @@ function parseSseDataLines(text: string): string[] {
}
describe("OpenAI-compatible HTTP API (e2e)", () => {
it("can be disabled via config (404)", async () => {
const port = await getFreePort();
const server = await startServer(port, {
openAiChatCompletionsEnabled: false,
});
try {
const res = await postChatCompletions(port, {
model: "clawdbot",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(404);
} finally {
await server.close({ reason: "test done" });
}
});
it("rejects non-POST", async () => {
const port = await getFreePort();
const server = await startServer(port);

View File

@@ -206,6 +206,7 @@ export function createGatewayHttpServer(opts: {
canvasHost: CanvasHostHandler | null;
controlUiEnabled: boolean;
controlUiBasePath: string;
openAiChatCompletionsEnabled: boolean;
handleHooksRequest: HooksRequestHandler;
resolvedAuth: import("./auth.js").ResolvedGatewayAuth;
}): HttpServer {
@@ -213,6 +214,7 @@ export function createGatewayHttpServer(opts: {
canvasHost,
controlUiEnabled,
controlUiBasePath,
openAiChatCompletionsEnabled,
handleHooksRequest,
resolvedAuth,
} = opts;
@@ -222,8 +224,10 @@ export function createGatewayHttpServer(opts: {
void (async () => {
if (await handleHooksRequest(req, res)) return;
if (await handleOpenAiHttpRequest(req, res, { auth: resolvedAuth }))
return;
if (openAiChatCompletionsEnabled) {
if (await handleOpenAiHttpRequest(req, res, { auth: resolvedAuth }))
return;
}
if (canvasHost) {
if (await handleA2uiHttpRequest(req, res)) return;
if (await canvasHost.handleHttpRequest(req, res)) return;

View File

@@ -328,6 +328,11 @@ export type GatewayServerOptions = {
* Default: config `gateway.controlUi.enabled` (or true when absent).
*/
controlUiEnabled?: boolean;
/**
* If false, do not serve `POST /v1/chat/completions`.
* Default: config `gateway.http.endpoints.chatCompletions.enabled` (or true when absent).
*/
openAiChatCompletionsEnabled?: boolean;
/**
* Override gateway auth configuration (merges with config).
*/
@@ -432,6 +437,10 @@ export async function startGatewayServer(
}
const controlUiEnabled =
opts.controlUiEnabled ?? cfgAtStart.gateway?.controlUi?.enabled ?? true;
const openAiChatCompletionsEnabled =
opts.openAiChatCompletionsEnabled ??
cfgAtStart.gateway?.http?.endpoints?.chatCompletions?.enabled ??
true;
const controlUiBasePath = normalizeControlUiBasePath(
cfgAtStart.gateway?.controlUi?.basePath,
);
@@ -615,6 +624,7 @@ export async function startGatewayServer(
canvasHost,
controlUiEnabled,
controlUiBasePath,
openAiChatCompletionsEnabled,
handleHooksRequest,
resolvedAuth,
});