@planetarium/oai2a2a-server
v0.8.1
Published
Framework-agnostic Web-Fetch handlers (Request → Response) for OpenAI-compatible /v1/chat/completions and /v1/completions routes backed by A2A agents
Keywords
Readme
@planetarium/oai2a2a-server
Framework-agnostic Web Fetch handlers for exposing OpenAI-compatible endpoints backed by A2A agents.
createOpenAIRoutes serves:
GET /v1/modelsPOST /v1/chat/completionsPOST /v1/completionsPOST /v1/responses
Install
npm install @planetarium/oai2a2a-server @planetarium/oai2a2a-codec @a2x/sdk x402@planetarium/oai2a2a-codec and @a2x/sdk are peer dependencies. The host application owns
model-to-agent resolution, A2XClient construction, authentication, SSRF
protection, and any billing or usage logging.
x402 is required by @a2x/sdk/client in bundled Next.js route handlers even
when the app does not use paid agents directly.
Next.js App Router
import { createOpenAIRoutes } from "@planetarium/oai2a2a-server";
import { hooks } from "@/lib/openai-hooks";
export const runtime = "nodejs";
export const { GET, POST } = createOpenAIRoutes(hooks);For authenticated hosts, pass a context resolver:
export const { GET, POST } = createOpenAIRoutes(hooks, {
getContext: (request) => authenticate(request),
});Declare hooks as ChatCompletionsHandlerHooks<MyContext> so
resolveAgent, pollUntilTerminal, and onFinalCompletion receive the
resolved context.
Hooks
import {
pollUntilTerminal,
type ChatCompletionsHandlerHooks,
} from "@planetarium/oai2a2a-server";
export const hooks: ChatCompletionsHandlerHooks = {
async resolveAgent(req) {
return { client, agentCard };
},
pollUntilTerminal,
onFinalCompletion(req, completion) {
// Optional: usage logging for finalized non-streaming responses and
// single-chunk streaming fallbacks.
},
};onFinalCompletion does not fire for the successful streaming path because no
single completion object is built. Streaming clients should read usage from the
terminal SSE usage chunk when stream_options.include_usage is enabled.
Failure observation and failover
onFinalError observes the final OpenAI-compatible error before it is surfaced
to the caller as either a JSON error response or an SSE error chunk. The hook
receives the original request, the selected agent when one was resolved, the
mapped OpenAI error, and the terminal task when the default poller produced the
failure. It is observational: hook errors are logged and do not replace the
original response.
export const hooks: ChatCompletionsHandlerHooks = {
async resolveAgent(req) {
return { client, agentCard };
},
pollUntilTerminal,
async onFinalError(failure) {
await markUnavailable({
model: failure.request.model,
code: failure.mappedError.code,
status: failure.mappedError.status,
taskId: failure.task?.id,
});
},
};For non-streaming pool routers, resolveNextAgent can retry another candidate
after a send/poll failure. Return the next { client, agentCard } to retry, or
return undefined / null to make the failure final. This seam is not used for
live message/stream responses after streaming starts.
export const hooks: ChatCompletionsHandlerHooks = {
async resolveAgent(req, ctx) {
return ctx.pool.next(req.model);
},
pollUntilTerminal,
async resolveNextAgent(failure, ctx) {
await ctx.pool.markFailed(failure.resolvedAgent, failure.mappedError.code);
if (failure.mappedError.status < 500 && failure.mappedError.status !== 429) {
return undefined;
}
return ctx.pool.next(failure.request.model);
},
};Per-request model resolution
resolveAgent may return a modelOverride?: string alongside the client and
agent card. Routers that map a caller-supplied alias or pool slug to a specific
underlying model id (e.g. "fast-default" → "gpt-5-mini") set this field. The
codec writes the resolved value into the canonical
chat_completions_request.model slot of the openai-compat/v1 envelope the agent
reads, while the inbound req is left unmutated — so onFinalCompletion and
usage hooks still observe the caller-supplied req.model for audit and billing.
The agent never sees both values; the gateway is the boundary that records the
caller-supplied → resolved mapping.
async resolveAgent(req) {
const { agentCard, model } = await pickAgentFor(req.model);
return {
client,
agentCard,
modelOverride: model, // e.g. "gpt-5-mini"; envelope carries this, req.model stays as the caller alias
};
}Responses API shim
createResponsesHandler and createOpenAIRoutes expose an MVP
POST /v1/responses shim. It converts supported Responses inputs into the same
Chat Completions request shape used by the rest of the package, then converts
the result back into a Responses-style object or SSE stream.
Supported request fields:
inputstring or message item arraysinstructions- function
tools tool_choicetext.formatmax_output_tokensstream
Streaming text is emitted as response.output_text.delta events. When the
underlying A2A stream has first been converted into OpenAI Chat Completions SSE
chunks, any choices[].delta.tool_calls chunks are emitted as function_call
output items with response.function_call_arguments.delta and
response.function_call_arguments.done events.
The shim intentionally does not implement persisted response state. It rejects
previous_response_id, file_id references, and input_file.file_url until a
host supplies state/file resolution semantics.
