feat(gateway): add config toggle for chat completions endpoint
This commit is contained in:
@@ -1803,6 +1803,7 @@ Related docs:
|
|||||||
Notes:
|
Notes:
|
||||||
- `clawdbot gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
|
- `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).
|
- `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`.
|
- 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`).
|
- 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.
|
- `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:
|
Advanced:
|
||||||
- `x-clawdbot-session-key: <sessionKey>` to fully control session routing.
|
- `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
|
## Session behavior
|
||||||
|
|
||||||
By default the endpoint is **stateless per request** (a new session key is generated each call).
|
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.token": "Gateway Token",
|
||||||
"gateway.auth.password": "Gateway Password",
|
"gateway.auth.password": "Gateway Password",
|
||||||
"gateway.controlUi.basePath": "Control UI Base Path",
|
"gateway.controlUi.basePath": "Control UI Base Path",
|
||||||
|
"gateway.http.endpoints.chatCompletions.enabled":
|
||||||
|
"OpenAI Chat Completions Endpoint",
|
||||||
"gateway.reload.mode": "Config Reload Mode",
|
"gateway.reload.mode": "Config Reload Mode",
|
||||||
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
|
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
|
||||||
"agents.defaults.workspace": "Workspace",
|
"agents.defaults.workspace": "Workspace",
|
||||||
@@ -158,6 +160,8 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
"gateway.auth.password": "Required for Tailscale funnel.",
|
"gateway.auth.password": "Required for Tailscale funnel.",
|
||||||
"gateway.controlUi.basePath":
|
"gateway.controlUi.basePath":
|
||||||
"Optional URL prefix where the Control UI is served (e.g. /clawdbot).",
|
"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":
|
"gateway.reload.mode":
|
||||||
'Hot reload strategy for config changes ("hybrid" recommended).',
|
'Hot reload strategy for config changes ("hybrid" recommended).',
|
||||||
"gateway.reload.debounceMs":
|
"gateway.reload.debounceMs":
|
||||||
|
|||||||
@@ -1155,6 +1155,22 @@ export type GatewayReloadConfig = {
|
|||||||
debounceMs?: number;
|
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 = {
|
export type GatewayConfig = {
|
||||||
/** Single multiplexed port for Gateway WS + HTTP (default: 18789). */
|
/** Single multiplexed port for Gateway WS + HTTP (default: 18789). */
|
||||||
port?: number;
|
port?: number;
|
||||||
@@ -1173,6 +1189,7 @@ export type GatewayConfig = {
|
|||||||
tailscale?: GatewayTailscaleConfig;
|
tailscale?: GatewayTailscaleConfig;
|
||||||
remote?: GatewayRemoteConfig;
|
remote?: GatewayRemoteConfig;
|
||||||
reload?: GatewayReloadConfig;
|
reload?: GatewayReloadConfig;
|
||||||
|
http?: GatewayHttpConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SkillConfig = {
|
export type SkillConfig = {
|
||||||
|
|||||||
@@ -1477,6 +1477,19 @@ export const ClawdbotSchema = z
|
|||||||
debounceMs: z.number().int().min(0).optional(),
|
debounceMs: z.number().int().min(0).optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
http: z
|
||||||
|
.object({
|
||||||
|
endpoints: z
|
||||||
|
.object({
|
||||||
|
chatCompletions: z
|
||||||
|
.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
skills: z
|
skills: z
|
||||||
|
|||||||
@@ -9,12 +9,16 @@ import {
|
|||||||
|
|
||||||
installGatewayTestHooks();
|
installGatewayTestHooks();
|
||||||
|
|
||||||
async function startServer(port: number) {
|
async function startServer(
|
||||||
|
port: number,
|
||||||
|
opts?: { openAiChatCompletionsEnabled?: boolean },
|
||||||
|
) {
|
||||||
const { startGatewayServer } = await import("./server.js");
|
const { startGatewayServer } = await import("./server.js");
|
||||||
return await startGatewayServer(port, {
|
return await startGatewayServer(port, {
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
auth: { mode: "token", token: "secret" },
|
auth: { mode: "token", token: "secret" },
|
||||||
controlUiEnabled: false,
|
controlUiEnabled: false,
|
||||||
|
openAiChatCompletionsEnabled: opts?.openAiChatCompletionsEnabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +48,22 @@ function parseSseDataLines(text: string): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("OpenAI-compatible HTTP API (e2e)", () => {
|
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 () => {
|
it("rejects non-POST", async () => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
const server = await startServer(port);
|
const server = await startServer(port);
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ export function createGatewayHttpServer(opts: {
|
|||||||
canvasHost: CanvasHostHandler | null;
|
canvasHost: CanvasHostHandler | null;
|
||||||
controlUiEnabled: boolean;
|
controlUiEnabled: boolean;
|
||||||
controlUiBasePath: string;
|
controlUiBasePath: string;
|
||||||
|
openAiChatCompletionsEnabled: boolean;
|
||||||
handleHooksRequest: HooksRequestHandler;
|
handleHooksRequest: HooksRequestHandler;
|
||||||
resolvedAuth: import("./auth.js").ResolvedGatewayAuth;
|
resolvedAuth: import("./auth.js").ResolvedGatewayAuth;
|
||||||
}): HttpServer {
|
}): HttpServer {
|
||||||
@@ -213,6 +214,7 @@ export function createGatewayHttpServer(opts: {
|
|||||||
canvasHost,
|
canvasHost,
|
||||||
controlUiEnabled,
|
controlUiEnabled,
|
||||||
controlUiBasePath,
|
controlUiBasePath,
|
||||||
|
openAiChatCompletionsEnabled,
|
||||||
handleHooksRequest,
|
handleHooksRequest,
|
||||||
resolvedAuth,
|
resolvedAuth,
|
||||||
} = opts;
|
} = opts;
|
||||||
@@ -222,8 +224,10 @@ export function createGatewayHttpServer(opts: {
|
|||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (await handleHooksRequest(req, res)) return;
|
if (await handleHooksRequest(req, res)) return;
|
||||||
if (await handleOpenAiHttpRequest(req, res, { auth: resolvedAuth }))
|
if (openAiChatCompletionsEnabled) {
|
||||||
return;
|
if (await handleOpenAiHttpRequest(req, res, { auth: resolvedAuth }))
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (canvasHost) {
|
if (canvasHost) {
|
||||||
if (await handleA2uiHttpRequest(req, res)) return;
|
if (await handleA2uiHttpRequest(req, res)) return;
|
||||||
if (await canvasHost.handleHttpRequest(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).
|
* Default: config `gateway.controlUi.enabled` (or true when absent).
|
||||||
*/
|
*/
|
||||||
controlUiEnabled?: boolean;
|
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).
|
* Override gateway auth configuration (merges with config).
|
||||||
*/
|
*/
|
||||||
@@ -432,6 +437,10 @@ export async function startGatewayServer(
|
|||||||
}
|
}
|
||||||
const controlUiEnabled =
|
const controlUiEnabled =
|
||||||
opts.controlUiEnabled ?? cfgAtStart.gateway?.controlUi?.enabled ?? true;
|
opts.controlUiEnabled ?? cfgAtStart.gateway?.controlUi?.enabled ?? true;
|
||||||
|
const openAiChatCompletionsEnabled =
|
||||||
|
opts.openAiChatCompletionsEnabled ??
|
||||||
|
cfgAtStart.gateway?.http?.endpoints?.chatCompletions?.enabled ??
|
||||||
|
true;
|
||||||
const controlUiBasePath = normalizeControlUiBasePath(
|
const controlUiBasePath = normalizeControlUiBasePath(
|
||||||
cfgAtStart.gateway?.controlUi?.basePath,
|
cfgAtStart.gateway?.controlUi?.basePath,
|
||||||
);
|
);
|
||||||
@@ -615,6 +624,7 @@ export async function startGatewayServer(
|
|||||||
canvasHost,
|
canvasHost,
|
||||||
controlUiEnabled,
|
controlUiEnabled,
|
||||||
controlUiBasePath,
|
controlUiBasePath,
|
||||||
|
openAiChatCompletionsEnabled,
|
||||||
handleHooksRequest,
|
handleHooksRequest,
|
||||||
resolvedAuth,
|
resolvedAuth,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user