@planetarium/oai2a2a-server
v0.3.1
Published
Framework-agnostic Web-Fetch handlers (Request → Response) for OpenAI-compatible /v1/chat/completions and /v1/completions routes backed by A2A agents
Downloads
369
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.
Injecting downstream message metadata
resolveAgent may return a messageMetadata map alongside the client and
agent card. Each entry is merged into the outbound A2A
sendParams.message.metadata keyed by extension URI per A2A's
Message.metadata convention. When the codec and host both provide object
values for the same extension URI, the merge is one level deep and
host-supplied fields win. This lets a model-first router forward its selector
(e.g. the openai-compat model selector key) without dropping codec-injected
fields such as chat_history, system, tools, or tool_choice.
The merge does not recurse below the extension payload: { a: { x: 1 } }
overlaid with { a: { y: 2 } } becomes { a: { y: 2 } }.
async resolveAgent(req) {
const agentCard = await pickAgentFor(req.model);
return {
client,
agentCard,
messageMetadata: {
"https://github.com/planetarium/oai2a2a/extensions/openai-compat/v1": {
model: req.model,
},
},
};
}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.
