@mpilot/openai
v0.1.0
Published
Raw-shape OpenAI adapter for the framework-agnostic [`@mpilot/tools`](../tools) registry. Wraps **no SDK**: `getOpenAITools` returns the Chat Completions wire-format tool array plus a `dispatch()` executor, so the same toolkit drives the `openai` client *
Readme
@mpilot/openai
Raw-shape OpenAI adapter for the framework-agnostic @mpilot/tools
registry. Wraps no SDK: getOpenAITools returns the Chat Completions
wire-format tool array plus a dispatch() executor, so the same toolkit drives
the openai client and Anthropic Messages raw tool-use — one adapter, two
runtimes.
import { getOpenAITools } from '@mpilot/openai';
import { aaveTools, dexTools } from '@mpilot/providers'; // example factories
const toolkit = getOpenAITools({ chainId: 5000 }, [aaveTools, dexTools]);Quickstart — OpenAI Chat Completions
import OpenAI from 'openai';
import { bigintSafeStringify, getOpenAITools } from '@mpilot/openai';
const client = new OpenAI();
const toolkit = getOpenAITools(agent, factories);
const messages: OpenAI.ChatCompletionMessageParam[] = [
{ role: 'user', content: 'Propose my next portfolio move.' },
];
const completion = await client.chat.completions.create({
model: 'gpt-5.2',
messages,
tools: toolkit.tools, // assignable to ChatCompletionFunctionTool[]
});
// Handle EVERY tool call — parallel tool calls are the model's default, and
// the follow-up create() 400s unless each tool_call_id gets a tool message.
const reply = completion.choices[0]?.message;
if (reply?.tool_calls?.length) {
messages.push(reply);
for (const call of reply.tool_calls) {
if (call.type !== 'function') continue;
const result = await toolkit.dispatch(call.function.name, call.function.arguments);
messages.push({
role: 'tool',
tool_call_id: call.id,
content: bigintSafeStringify(result),
});
}
}Quickstart — Anthropic Messages (raw tool-use)
The Chat Completions parameters content is Anthropic's input_schema —
only the envelope keys differ:
import Anthropic from '@anthropic-ai/sdk';
import { bigintSafeStringify, getOpenAITools } from '@mpilot/openai';
const client = new Anthropic();
const toolkit = getOpenAITools(agent, factories);
const tools: Anthropic.Messages.Tool[] = toolkit.tools.map((t) => ({
name: t.function.name,
description: t.function.description,
input_schema: t.function.parameters, // carries the literal type: 'object'
}));
const message = await client.messages.create({
model: 'claude-opus-4-7',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Propose my next portfolio move.' }],
tools,
});
// A single assistant message can carry MULTIPLE tool_use blocks — answer
// every one, or the follow-up messages.create() rejects the conversation.
for (const block of message.content) {
if (block.type !== 'tool_use') continue;
const result = await toolkit.dispatch(block.name, block.input as object);
// reply with { type: 'tool_result', tool_use_id: block.id, content: bigintSafeStringify(result) }
}dispatch semantics
argsaccepts the raw JSON string fromtool_calls[].function.argumentsor an already-parsed object from Anthropic'stool_use.input.- Input is validated with the tool's own
inputSchemabeforeinvokeruns — defaults applied, unknown keys stripped, invalid input rejects with aZodError. Malformed JSON rejects withSyntaxError; unknown tool names reject with an error listing the known tools. Nothing fails silently. dispatchreturns the rawinvoke()value. Serialize it for the tool-result message with the re-exportedbigintSafeStringify— plainJSON.stringifythrows on the wei-scale bigints Concierge tools emit.
Gotchas
- Zero tools? Omitted factories AND an unsupported
chainIdboth yieldtools: []silently — check both before debugging the model. - No abort propagation. Cancelling the model request does NOT cancel an
in-flight tool call:
ConciergeTool.invoketakes no abort signal, so a started execution (e.g. an on-chain transaction) runs to completion. - A failed
dispatchmid-loop leavesmessagesinconsistent. The assistant message is pushed before the tool replies, so if one call's dispatch rejects, the calls already answered orphan their siblings and a retriedcreate()400s on the unansweredtool_call_ids. Either reply with an error-string tool message for the failed call, or drop the assistant message before retrying. - Not strict-mode ready with optionals. OpenAI
strict: truerequiresadditionalProperties: false(emitted — fine) AND every propertyrequired—.optional()properties are omitted fromrequired, so schemas with optionals fail strict validation..default()properties are fine: they STAY inrequired(parse fills them). Leavestrictunset (the default) unless your schemas avoid.optional(). .transform()/.pipe()/z.custom()input schemas throw at construction time —.transform()andz.custom()cannot be represented in JSON Schema at all, and a plain.pipe()would silently advertise whatparse()returns instead of what it accepts. Normalize insideinvoke()instead..refine()is invisible to the model. Refinements are stripped from the emitted JSON Schema (they still validate at dispatch) — the model can't see the constraint and will violate it. Put the rule in.describe()text too.
