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`
|
- `POST /v1/chat/completions`
|
||||||
- Same port as the Gateway (WS + HTTP multiplex): `http://<gateway-host>:<port>/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
|
## Authentication
|
||||||
|
|
||||||
Uses the Gateway auth configuration. Send a bearer token:
|
Uses the Gateway auth configuration. Send a bearer token:
|
||||||
@@ -22,7 +24,12 @@ Notes:
|
|||||||
|
|
||||||
## Choosing an agent
|
## 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`)
|
- `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"}]
|
"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 () => {
|
it("honors x-clawdbot-session-key override", async () => {
|
||||||
agentCommand.mockResolvedValueOnce({
|
agentCommand.mockResolvedValueOnce({
|
||||||
payloads: [{ text: "hello" }],
|
payloads: [{ text: "hello" }],
|
||||||
|
|||||||
@@ -111,14 +111,40 @@ function buildAgentPrompt(messagesUnknown: unknown): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAgentId(req: IncomingMessage): string {
|
function resolveAgentIdFromHeader(
|
||||||
|
req: IncomingMessage,
|
||||||
|
): string | undefined {
|
||||||
const raw =
|
const raw =
|
||||||
getHeader(req, "x-clawdbot-agent-id")?.trim() ||
|
getHeader(req, "x-clawdbot-agent-id")?.trim() ||
|
||||||
getHeader(req, "x-clawdbot-agent")?.trim() ||
|
getHeader(req, "x-clawdbot-agent")?.trim() ||
|
||||||
"main";
|
"";
|
||||||
|
if (!raw) return undefined;
|
||||||
return normalizeAgentId(raw);
|
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: {
|
function resolveSessionKey(params: {
|
||||||
req: IncomingMessage;
|
req: IncomingMessage;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
@@ -183,7 +209,7 @@ export async function handleOpenAiHttpRequest(
|
|||||||
const model = typeof payload.model === "string" ? payload.model : "clawdbot";
|
const model = typeof payload.model === "string" ? payload.model : "clawdbot";
|
||||||
const user = typeof payload.user === "string" ? payload.user : undefined;
|
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 sessionKey = resolveSessionKey({ req, agentId, user });
|
||||||
const prompt = buildAgentPrompt(payload.messages);
|
const prompt = buildAgentPrompt(payload.messages);
|
||||||
if (!prompt.message) {
|
if (!prompt.message) {
|
||||||
|
|||||||
Reference in New Issue
Block a user