feat(gateway): allow agent via model

This commit is contained in:
Peter Steinberger
2026-01-10 21:44:28 +01:00
parent 72d4317d7f
commit 6546a1a23a
3 changed files with 89 additions and 5 deletions

View File

@@ -10,6 +10,8 @@ Clawdbots Gateway can serve a small OpenAI-compatible endpoint:
- `POST /v1/chat/completions`
- Same port as the Gateway (WS + HTTP multiplex): `http://<gateway-host>:<port>/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:<agentId>"` (example: `"clawdbot:main"`, `"clawdbot:beta"`)
- `model: "agent:<agentId>"` (alias)
Or target a specific Clawdbot agent by header:
- `x-clawdbot-agent-id: <agentId>` (default: `main`)
@@ -69,4 +76,3 @@ curl -N http://127.0.0.1:18789/v1/chat/completions \
"messages": [{"role":"user","content":"hi"}]
}'
```

View File

@@ -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" }],

View File

@@ -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[:/](?<agentId>[a-z0-9][a-z0-9_-]{0,63})$/i) ??
raw.match(/^agent:(?<agentId>[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) {