diff --git a/docs/gateway/openai-http-api.md b/docs/gateway/openai-http-api.md index 519158d11..13831d336 100644 --- a/docs/gateway/openai-http-api.md +++ b/docs/gateway/openai-http-api.md @@ -10,6 +10,8 @@ Clawdbot’s Gateway can serve a small OpenAI-compatible endpoint: - `POST /v1/chat/completions` - Same port as the Gateway (WS + HTTP multiplex): `http://:/v1/chat/completions` +Under the hood, requests are executed as a normal Gateway agent run (same codepath as `clawdbot agent`), so routing/permissions/config match your Gateway. + ## Authentication Uses the Gateway auth configuration. Send a bearer token: @@ -22,7 +24,12 @@ Notes: ## Choosing an agent -Target a specific Clawdbot agent by id: +No custom headers required: encode the agent id in the OpenAI `model` field: + +- `model: "clawdbot:"` (example: `"clawdbot:main"`, `"clawdbot:beta"`) +- `model: "agent:"` (alias) + +Or target a specific Clawdbot agent by header: - `x-clawdbot-agent-id: ` (default: `main`) @@ -69,4 +76,3 @@ curl -N http://127.0.0.1:18789/v1/chat/completions \ "messages": [{"role":"user","content":"hi"}] }' ``` - diff --git a/src/gateway/openai-http.e2e.test.ts b/src/gateway/openai-http.e2e.test.ts index ec85285d5..30ef2e92d 100644 --- a/src/gateway/openai-http.e2e.test.ts +++ b/src/gateway/openai-http.e2e.test.ts @@ -98,6 +98,58 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { } }); + it("routes to a specific agent via model (no custom headers)", async () => { + agentCommand.mockResolvedValueOnce({ + payloads: [{ text: "hello" }], + } as never); + + const port = await getFreePort(); + const server = await startServer(port); + try { + const res = await postChatCompletions(port, { + model: "clawdbot:beta", + messages: [{ role: "user", content: "hi" }], + }); + expect(res.status).toBe(200); + + expect(agentCommand).toHaveBeenCalledTimes(1); + const [opts] = agentCommand.mock.calls[0] ?? []; + expect( + (opts as { sessionKey?: string } | undefined)?.sessionKey ?? "", + ).toMatch(/^agent:beta:/); + } finally { + await server.close({ reason: "test done" }); + } + }); + + it("prefers explicit header agent over model agent", async () => { + agentCommand.mockResolvedValueOnce({ + payloads: [{ text: "hello" }], + } as never); + + const port = await getFreePort(); + const server = await startServer(port); + try { + const res = await postChatCompletions( + port, + { + model: "clawdbot:beta", + messages: [{ role: "user", content: "hi" }], + }, + { "x-clawdbot-agent-id": "alpha" }, + ); + expect(res.status).toBe(200); + + expect(agentCommand).toHaveBeenCalledTimes(1); + const [opts] = agentCommand.mock.calls[0] ?? []; + expect( + (opts as { sessionKey?: string } | undefined)?.sessionKey ?? "", + ).toMatch(/^agent:alpha:/); + } finally { + await server.close({ reason: "test done" }); + } + }); + it("honors x-clawdbot-session-key override", async () => { agentCommand.mockResolvedValueOnce({ payloads: [{ text: "hello" }], diff --git a/src/gateway/openai-http.ts b/src/gateway/openai-http.ts index 4194cb33a..49db909ad 100644 --- a/src/gateway/openai-http.ts +++ b/src/gateway/openai-http.ts @@ -111,14 +111,40 @@ function buildAgentPrompt(messagesUnknown: unknown): { }; } -function resolveAgentId(req: IncomingMessage): string { +function resolveAgentIdFromHeader( + req: IncomingMessage, +): string | undefined { const raw = getHeader(req, "x-clawdbot-agent-id")?.trim() || getHeader(req, "x-clawdbot-agent")?.trim() || - "main"; + ""; + if (!raw) return undefined; return normalizeAgentId(raw); } +function resolveAgentIdFromModel(model: string | undefined): string | undefined { + const raw = model?.trim(); + if (!raw) return undefined; + + const m = + raw.match(/^clawdbot[:/](?[a-z0-9][a-z0-9_-]{0,63})$/i) ?? + raw.match(/^agent:(?[a-z0-9][a-z0-9_-]{0,63})$/i); + const agentId = m?.groups?.agentId; + if (!agentId) return undefined; + return normalizeAgentId(agentId); +} + +function resolveAgentIdForRequest(params: { + req: IncomingMessage; + model: string | undefined; +}): string { + const fromHeader = resolveAgentIdFromHeader(params.req); + if (fromHeader) return fromHeader; + + const fromModel = resolveAgentIdFromModel(params.model); + return fromModel ?? "main"; +} + function resolveSessionKey(params: { req: IncomingMessage; agentId: string; @@ -183,7 +209,7 @@ export async function handleOpenAiHttpRequest( const model = typeof payload.model === "string" ? payload.model : "clawdbot"; const user = typeof payload.user === "string" ? payload.user : undefined; - const agentId = resolveAgentId(req); + const agentId = resolveAgentIdForRequest({ req, model }); const sessionKey = resolveSessionKey({ req, agentId, user }); const prompt = buildAgentPrompt(payload.messages); if (!prompt.message) {