Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions docs/src/content/docs/integrations/openai-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,28 @@ Key benefits

```

{/* <!-- embedme typescript/examples/serve/openai_responses.ts --> */}
```ts TypeScript [expandable]
// coming soon
import "dotenv/config.js";

import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo";
import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat";
import { ToolCallingAgent } from "beeai-framework/agents/toolCalling/agent";
import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory";
import { OpenAIServer } from "beeai-framework/adapters/openai/serve/server";

// ensure the model is pulled before running
const llm = new OllamaChatModel("granite4:micro");

const agent = new ToolCallingAgent({
llm,
memory: new UnconstrainedMemory(),
tools: [
new OpenMeteoTool(), // weather tool
],
});

await new OpenAIServer({ api: "responses", port: 9999 }).register(agent).serve();
```

</CodeGroup>
Expand All @@ -62,7 +82,7 @@ You can easily call the exposed entities via cURL.
<CodeGroup>

```sh Responses API
curl --location 'http://0.0.0.0:9998/responses' \
curl --location 'http://127.0.0.1:9998/responses' \
--header 'Content-Type: application/json' \
--data '{
"model": "agent",
Expand All @@ -73,7 +93,7 @@ You can easily call the exposed entities via cURL.
```

```sh Chat Completion API
curl --location 'http://0.0.0.0:9998/chat/completions' \
curl --location 'http://127.0.0.1:9998/chat/completions' \
--header 'Content-Type: application/json' \
--data '{
"model": "agent",
Expand Down
20 changes: 20 additions & 0 deletions typescript/examples/serve/openai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import "dotenv/config.js";

import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo";
import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat";
import { ToolCallingAgent } from "beeai-framework/agents/toolCalling/agent";
import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory";
import { OpenAIServer } from "beeai-framework/adapters/openai/serve/server";

// ensure the model is pulled before running
const llm = new OllamaChatModel("granite4:micro");

const agent = new ToolCallingAgent({
llm,
memory: new UnconstrainedMemory(),
tools: [
new OpenMeteoTool(), // weather tool
],
});

await new OpenAIServer().register(agent).serve();
20 changes: 20 additions & 0 deletions typescript/examples/serve/openai_responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import "dotenv/config.js";

import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo";
import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat";
import { ToolCallingAgent } from "beeai-framework/agents/toolCalling/agent";
import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory";
import { OpenAIServer } from "beeai-framework/adapters/openai/serve/server";

// ensure the model is pulled before running
const llm = new OllamaChatModel("granite4:micro");

const agent = new ToolCallingAgent({
llm,
memory: new UnconstrainedMemory(),
tools: [
new OpenMeteoTool(), // weather tool
],
});

await new OpenAIServer({ api: "responses", port: 9999 }).register(agent).serve();
179 changes: 179 additions & 0 deletions typescript/src/adapters/openai/serve/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
* SPDX-License-Identifier: Apache-2.0
*/

import express, { Request, Response, Router } from "express";
import { v4 as uuidv4 } from "uuid";
import { AnyAgent } from "@/agents/types.js";
import { transformRequestMessages } from "./utils.js";
import { ChatCompletionRequestBody, ChatCompletionResponse } from "./types.js";
import { Logger } from "@/logger/logger.js";

const logger = Logger.root.child({
name: "OpenAI API",
});

export class ChatCompletionAPI {
public readonly router: Router;

constructor(
private readonly modelFactory: (modelId: string) => Promise<AnyAgent>,
private readonly apiKey?: string,
) {
this.router = express.Router();
this.router.use(express.json());

this.router.post("/chat/completions", this.handler.bind(this));
}

private async handler(req: Request, res: Response) {
const requestBody = req.body as ChatCompletionRequestBody;
logger.debug(`Received request: ${JSON.stringify(requestBody)}`);

if (this.apiKey) {
const authHeader = req.headers.authorization;
const token = Array.isArray(authHeader) ? authHeader[0] : authHeader;
if (!token || token.replace(/^Bearer\s+/i, "") !== this.apiKey) {
res.status(401).json({ detail: "Missing or invalid API key" });
return;
}
}

try {
const messages = transformRequestMessages(requestBody.messages || []);
const agent = await this.modelFactory(requestBody.model);

// We clone the agent to avoid mutating the registered instance
const clonedAgent = await agent.clone();
clonedAgent.memory = await clonedAgent.memory.clone();
clonedAgent.memory.reset();
await clonedAgent.memory.addMany(messages);

if (requestBody.stream) {
const id = `chatcmpl-${uuidv4()}`;
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");

// Abort the agent run if the client disconnects early
const controller = new AbortController();
req.on("close", () => {
controller.abort();
});

try {
await clonedAgent
.run({ prompt: null }, { signal: controller.signal })
.observe((emitter) => {
// Match all events — required because the emitter is typed as
// Emitter<unknown> at the AnyAgent abstraction. We emit text deltas
// from "update" (ReActAgent incremental tokens) and from "success"
// (ToolCallingAgent emits only start/success, with the final answer
// in state.result). This keeps streaming working for both agents.
emitter.match("*.*", async (eventData: any, event) => {
let delta: string | undefined;
if (event.name === "update") {
delta = eventData?.update?.value;
} else if (event.name === "success") {
delta = eventData?.state?.result?.text;
}
if (!delta || res.writableEnded || res.destroyed) {
return;
}
const data = {
id,
object: "chat.completion.chunk",
model: requestBody.model,
created: Math.floor(Date.now() / 1000),
choices: [
{
index: 0,
delta: {
role: "assistant",
content: delta,
},
finish_reason: null,
},
],
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
});

if (!res.writableEnded && !res.destroyed) {
const finalData = {
id,
object: "chat.completion.chunk",
model: requestBody.model,
created: Math.floor(Date.now() / 1000),
choices: [
{
index: 0,
delta: {},
finish_reason: "stop",
},
],
};
res.write(`data: ${JSON.stringify(finalData)}\n\n`);
res.write(`data: [DONE]\n\n`);
}
} catch (error) {
logger.error(error, "Error during streaming agent run");
if (!res.writableEnded && !res.destroyed) {
// Send an error chunk so the client can detect failure
const errData = {
id,
object: "chat.completion.chunk",
model: requestBody.model,
created: Math.floor(Date.now() / 1000),
choices: [{ index: 0, delta: {}, finish_reason: "error" }],
error: { message: String(error) },
};
try {
res.write(`data: ${JSON.stringify(errData)}\n\n`);
res.write(`data: [DONE]\n\n`);
} catch (writeError) {
logger.error(writeError, "Failed to write error response chunk");
}
}
} finally {
if (!res.writableEnded && !res.destroyed) {
res.end();
}
}
} else {
const result = await clonedAgent.run({ prompt: null });

const response: ChatCompletionResponse = {
id: `chatcmpl-${uuidv4()}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: requestBody.model,
choices: [
{
index: 0,
message: {
role: "assistant",
content: result.result?.text ?? "",
},
finish_reason: "stop",
},
],
usage: {
prompt_tokens: 0, // Mock usage since agents might not bubble it up uniformly yet
completion_tokens: 0,
total_tokens: 0,
},
};

res.json(response);
}
} catch (error) {
logger.error(error, "Error handling /chat/completions request");
if (!res.headersSent) {
res.status(500).json({ error: String(error) });
}
}
}
}
8 changes: 8 additions & 0 deletions typescript/src/adapters/openai/serve/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
* SPDX-License-Identifier: Apache-2.0
*/

export * from "./server.js";
export * from "./types.js";
export * from "./responses_types.js";
Loading
Loading