feat(gateway): allow agent via model
This commit is contained in:
@@ -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://<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"}]
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
@@ -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" }],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user