An @intx/inference provider adapter for the OpenAI Responses API (/v1/responses): SSE and JSON responses, text, tool calls, image, PDF and text document input, and reasoning replay. An inference provider for Corbits and Interchange agents that also works in any host that runs @intx/inference.
- One adapter for every Responses backend. OpenAI, Codex, xAI and Ollama's
/v1differ in paths, headers and body fields. Those differences are a JSONquirksobject on the source, not forked code. - Reasoning survives across turns. Encrypted reasoning items are tagged with the provider that issued them. They are replayed only to the same provider and model, so multi-turn reasoning keeps its context and the backend never rejects a signature it did not issue.
- Interchange error and retry semantics. Requests run through
runInference, so rate limits, pacing headers and auth failures behave the same as for the built-in providers.
It speaks only the Responses protocol. For Chat Completions, use the built-in OpenAI adapter in @intx/inference.
bun add @corbits/openai-responses @intx/inference@^0.4.0 @intx/types@^0.4.0Runs on Bun >= 1.2 or Node >= 24.
Needs OPENAI_API_KEY set.
import { createDependencies, runInference } from "@intx/inference";
import {
createOpenAIResponsesAdapter,
OPENAI_RESPONSES_PROVIDER,
} from "@corbits/openai-responses";
const deps = createDependencies({
has: (provider) => provider === OPENAI_RESPONSES_PROVIDER,
resolve: (source, quirks) => createOpenAIResponsesAdapter(source, quirks),
});
let seq = 0;
for await (const event of runInference({
deps,
source: {
id: "openai",
provider: OPENAI_RESPONSES_PROVIDER,
baseURL: "https://api.openai.com/v1",
credentialId: "OPENAI_API_KEY",
model: "gpt-5-mini",
},
turns: [
{
role: "user",
timestamp: Date.now(),
content: [{ type: "text", text: "Say hello." }],
},
],
nextSeq: () => seq++,
readMaterial: (id) => {
const secret = process.env[id];
if (secret === undefined) throw new Error(`${id} is not set`);
return { secret };
},
})) {
if (event.type === "inference.text.delta")
process.stdout.write(event.data.token);
if (event.type === "inference.error")
throw new Error(event.data.error.message);
}
process.stdout.write("\n");Interchange runs AI agents as principals (accounts that hold their own identity, permissions and credentials). Corbits packages add what an agent product needs around it.
- Runs in: the agent sidecar (the runtime next to each agent), or any process that calls
runInference. No hub is required. - Plugs into: the
@intx/inferenceadapter registry, as the factory for theopenai-responsesprovider id. - Pairs with:
@corbits/ollama-adapterand@corbits/system-one, the other Corbits inference providers.
| Export | Description |
|---|---|
createOpenAIResponsesAdapter |
AdapterFactory. Reads quirks from the source on every resolve. |
responsesAdapterFactory(quirks, hooks?) |
Returns an AdapterFactory with fixed quirks. Any per-source quirks are ignored. |
OPENAI_RESPONSES_PROVIDER |
The "openai-responses" provider id. |
OPENAI_COMPATIBLE_RESPONSES_PROVIDER |
Deprecated "openai-compatible-responses" id from 0.1. Removed in 0.3.0. |
responsesAdapterFactories |
Record mapping both provider ids to createOpenAIResponsesAdapter. |
ResponsesQuirks |
Schema and type for the quirks object. |
ResponsesHooks |
Code hooks: wrapSystemPrompt, includeReasoningEffort. |
Every field is optional. An absent field keeps the protocol default. Unknown keys are rejected.
| Quirk | Type | Default | Effect on the request |
|---|---|---|---|
path |
string |
"/responses" |
Request path appended to the source's baseURL. |
headers.static |
Record<string, string> |
{} |
Header names are lowercased; they override the stock headers. authorization is rejected. |
headers.modelHeader |
string |
unset | Header name that carries the model id. |
headers.fromOption |
{ optionKey, header }[] |
[] |
Copies each non-empty string providerOptions[optionKey] into header. |
sessionIdOption |
string |
unset | providerOptions key whose value is sent as prompt_cache_key. |
sessionIdHeader |
string |
unset | Also sends that session id in this header. Ignored without sessionIdOption. |
systemPrompt |
{ role: "system" | "developer", shape: "string" | "parts" } |
{ role: "system", shape: "string" } |
Role and content shape of the leading system-prompt item. |
contentShape |
"typed" | "flat" |
"typed" |
flat sends text-only content as a plain string instead of typed parts. |
parallelToolCalls |
boolean |
unset | Unset omits parallel_tool_calls; a boolean is sent as given. |
maxOutputTokens |
boolean |
true |
false omits max_output_tokens even when the caller sets maxTokens. |
temperature |
boolean |
true |
false omits temperature even when the caller sets it. |
store |
boolean |
false |
Sent as store. |
stream |
boolean |
true |
Sent as stream; false also sends accept: application/json. |
reasoning.summary |
"auto" | "detailed" |
unset | Sent as reasoning.summary. |
reasoning.effortOption |
string |
unset | providerOptions key whose value is sent as reasoning.effort. |
reasoning.maxEffort |
"xhigh" | "max" |
unset | The reasoning.effort sent for the neutral max effort; unset sends high, without a warning. |
instructions |
string |
unset | Sent as instructions. |
Interchange's neutral effort (low | medium | high | max) is sent as reasoning.effort, even when the source has no reasoning quirk: low, medium and high as given, max as reasoning.maxEffort or high when that is unset. A non-empty providerOptions[reasoning.effortOption] wins over it. includeReasoningEffort filters either.
An incomplete response (response.incomplete, or a JSON body with status: "incomplete") keeps its partial output and usage. When incomplete_details.reason is content_filter, an inference.safety_rating event with blockReason: "content_filter" comes before the usage.
Interchange loads custom adapters from an operator-configured AdapterManifest. Add one entry per provider id you serve:
import { createDependencies, type AdapterManifest } from "@intx/inference";
import { loadAdapterRegistry } from "@intx/inference/providers";
const manifest: AdapterManifest = [
{
provider: "openai-responses",
specifier: "@corbits/openai-responses",
export: "createOpenAIResponsesAdapter",
},
{
provider: "openai-compatible-responses",
specifier: "@corbits/openai-responses",
export: "createOpenAIResponsesAdapter",
},
];
const deps = createDependencies(await loadAdapterRegistry(manifest));Pass deps to runInference. Sources with provider: "openai-responses" (or the deprecated "openai-compatible-responses") then resolve to this adapter, and each source's quirks configures its backend. To serve a vendor under its own id, add another entry with that provider and the same export. Manifest entries override built-in adapters with the same id, so don't reuse openai or another built-in id.
For a vendor that needs code hooks, bake its quirks into a factory in your own module and point a manifest entry at that export:
import { responsesAdapterFactory } from "@corbits/openai-responses";
export const createVendorAdapter = responsesAdapterFactory(
{ path: "/v1/responses", contentShape: "flat", temperature: false },
{ wrapSystemPrompt: (prompt) => `<system>${prompt}</system>` },
);Its manifest entry names your module and that export, for example { provider: "vendor-responses", specifier: "./vendor-adapter.js", export: "createVendorAdapter" }.
- No host change is needed.
responsesAdapterFactoriesstill mapsopenai-compatible-responsestocreateOpenAIResponsesAdapter, so sources stored under that id keep resolving and running. OPENAI_COMPATIBLE_RESPONSES_PROVIDERis deprecated and removed in 0.3.0. Move stored sources toopenai-responsesbefore then.isResponsesStreamTerminalis no longer exported. The adapter still applies it.@intx/inferenceand@intx/typespeers are now^0.4.0.- Quirks and reasoning signatures are unchanged.