Library API
The package is both a CLI and an importable TypeScript library. Everything below is exported from the package root and fully typed.
import { loadConfig, startServer } from "@komaa/openai-msteams-bridge";Run the bridge in your own service
Section titled “Run the bridge in your own service”loadConfig() reads the same environment variables as the CLI and throws a clear error when a required variable is missing or a numeric one is not a number. startServer(cfg) returns the Node http.Server.
import { loadConfig, startServer } from "@komaa/openai-msteams-bridge";
const server = startServer(loadConfig());server.on("listening", () => console.log("bridge up"));Signal handling is opt-in
Section titled “Signal handling is opt-in”The built-in SIGTERM/SIGINT drain ends every live call gracefully (session.end + close) and then exits the process. Because a library must never exit its host, it is only wired when you ask for it (the CLI does):
startServer(loadConfig(), undefined, undefined, { handleSignals: true });Leave it off when the bridge is embedded in a larger service and wire your own shutdown instead.
Custom function tools
Section titled “Custom function tools”StartServerOptions.tools registers function tools the bridge executes. Each entry is a name, description, JSON schema, and handler; the handler’s returned string goes back to the model as the tool output, and a throw becomes an error output the model can recover from.
import { loadConfig, startServer, type CustomTool } from "@komaa/openai-msteams-bridge";
const tools: CustomTool[] = [ { name: "lookup_order", description: "Look up the status of a customer order by its order number.", parameters: { type: "object", properties: { orderNumber: { type: "string", description: "e.g. KO-1234" } }, required: ["orderNumber"], }, async handler({ orderNumber }, ctx) { // ctx: { callId, participantCount, recordingActive, log } ctx.log.info(`lookup_order ${String(orderNumber)}`); return await myBackend.orderStatus(String(orderNumber)); // the agent speaks this }, },];
startServer(loadConfig(), undefined, undefined, { tools });Names must not collide with the built-ins (end_call, express, show_image, look) - collisions and duplicates fail at startup. Keep handlers fast (the caller is waiting on the answer) and enforce your own timeout for slow backends.
Remote MCP servers
Section titled “Remote MCP servers”StartServerOptions.mcpServers (or the OPENAI_MCP_SERVERS env variable) merges remote MCP tool entries into every session; the Realtime API executes them server-side - the bridge does nothing at call time.
startServer(loadConfig(), undefined, undefined, { mcpServers: [{ server_label: "calendar", server_url: "https://mcp.example.com/sse", allowed_tools: ["list_events", "create_event"], authorization: process.env.MCP_CALENDAR_TOKEN, }],});See Extending the Agent’s Tools for the trust model and defaults.
Custom vision hook
Section titled “Custom vision hook”The third argument to startServer is a VisionDescriber - your own answer to the agent’s look tool. The raw frame never leaves your process; only the string you return is sent to the agent.
import OpenAI from "openai";import { loadConfig, startServer, type VisionDescriber } from "@komaa/openai-msteams-bridge";
const openai = new OpenAI(); // reads OPENAI_API_KEY
const describe: VisionDescriber = async (frame, question) => { // frame: { source: "camera" | "screenshare", mime, dataBase64, width, height, participantName?, ... } const who = frame.source === "screenshare" ? "the caller's shared screen" : "the caller's camera"; const res = await openai.chat.completions.create({ model: "gpt-4o-mini", // any vision-capable model max_tokens: 300, messages: [ { role: "user", content: [ { type: "text", text: `This is ${who}. ${question}` }, { type: "image_url", image_url: { url: `data:${frame.mime};base64,${frame.dataBase64}`, detail: "low" } }, ], }, ], }); return res.choices[0]?.message?.content ?? "I could not make out the image."; // becomes the `look` tool result};
startServer(loadConfig(), undefined, describe);Pass null as the third argument to disable path-2 vision entirely (the agent then falls back to the recording-gated native Realtime image attach). Omit it to use the built-in makeVisionDescriber(cfg), which is driven by VISION_API_URL.
Custom agent transport (testing)
Section titled “Custom agent transport (testing)”The second argument to startServer is an OaConnector - a factory that returns an AgentPort. The default opens a real Realtime socket; tests substitute a fake so no network or API key is needed.
import { startServer, loadConfig, type OaConnector, type AgentPort } from "@komaa/openai-msteams-bridge";
const fakeConnector: OaConnector = async (_cfg, _log, handlers) => { const port: AgentPort = { sessionId: "sess_test", isOpen: true, sendAudioChunk() {}, sendSessionUpdate() {}, sendContext() {}, sendUserMessage() {}, sendToolResult() {}, requestResponse() {}, cancelResponse() {}, async attachImage() {}, close() {}, }; // push server->bridge events at any time with handlers.onMessage(...) return port;};
startServer(loadConfig(), fakeConnector, null);The repository’s own test suite uses exactly this shape.
HMAC helpers
Section titled “HMAC helpers”Useful if you build tools that talk to the bridge, or want to test the upgrade.
import { sign, verify, isFresh, TIMESTAMP_HEADER, SIGNATURE_HEADER } from "@komaa/openai-msteams-bridge";
const ts = Date.now();const signature = sign(secret, ts, callId); // HMAC-SHA256(secret, `${ts}.${callId}`) hex// send as headers X-StandIn-Timestamp / -Signatureverify(secret, ts, callId, signature); // constant-time, false on any missing inputisFresh(ts, 60_000); // within the freshness window?Resampler helpers
Section titled “Resampler helpers”The 16k/24k conversion is exported for tooling and tests: resamplePcm16(buf, fromRate, toRate) (anti-aliasing low-pass when decimating), upsampleBase64To24k, downsampleBase64To16k, and the WIRE_RATE / OPENAI_RATE constants.
Protocol types
Section titled “Protocol types”All wire message types are exported for building or validating messages: SessionStartMessage, AudioFrameMessage, VideoFrameMessage, ParticipantsMessage, DtmfMessage, AssistantSayMessage, AssistantCancelMessage, ExpressionMessage, DisplayImageMessage, the WorkerInbound / WorkerOutbound unions, plus parseWorkerMessage() and pcm16kBytesToMs(). See the Wire Protocol for the full contract.
Also exported
Section titled “Also exported”authorizeUpgrade,callIdFromUrl,ReplayGuard- the upgrade-authorization primitives.CallSession- the per-call relay class (advanced embedding).assertPublicHttpUrl,isForbiddenIp,readBodyWithCap,fetchPublicImage- the SSRF-guard primitives.RealtimeAgentSocket,buildSessionUpdate,buildInstructions,synthesizeGoodbye,customToolSchema,mcpTool,BRIDGE_TOOLS- the OpenAI-side helpers.renderMetrics,logger- metrics text and the minimal leveled logger.