feat(gateway): add config toggle for chat completions endpoint
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user