Responses API
POST /v1/responses is served next to Chat Completions, with the same keys, model ids, aliases and limits. Use it when a client only speaks Responses: the Vercel AI SDK's openai(id), @langchain/openai for ids such as gpt-5.6*, the n8n OpenAI Chat Model node with "Use Responses API" on, the openai SDK's client.responses.create.
A request can carry the whole conversation in input, as Chat Completions does with messages, or continue an earlier answer with previous_response_id. Plugboard keeps each answer made with store on, which is the default, so GET /v1/responses/{id}, DELETE /v1/responses/{id} and GET /v1/responses/{id}/input_items work too. See Stored responses.
import OpenAI from "openai"
const client = new OpenAI({ baseURL: "http://localhost:4010/v1", apiKey: process.env.PLUGBOARD_KEY })
const r = await client.responses.create({ model: "fast", instructions: "Be terse.", input: "Reply with exactly: pong" })
console.log(r.output_text)The same with curl and jq. PLUGBOARD_BASE_URL is http://127.0.0.1:4010/v1 and PLUGBOARD_KEY is your key:
post() { # $1 = JSON body
curl -sS -f "$PLUGBOARD_BASE_URL/responses" -H "Authorization: Bearer $PLUGBOARD_KEY" -H "Content-Type: application/json" -d "$1"
}out=$(post "$(jq -cn --arg m "$1" '{model: $m, input: "Reply with exactly: pong"}')")What is supported
| Request field | How it is handled |
|---|---|
model | Same as Chat Completions: an alias or provider/model[#variant]. |
input | A string, or a list of items: messages (user, assistant, system, developer) with input_text, output_text and input_image content; input_file with file_data, file_url or a file_id (see Files API); input_image with a file_id; function_call and function_call_output for tool round trips, custom_tool_call and custom_tool_call_output for freeform tools; additional_tools items, which declare tools (see Tools). reasoning input items are accepted and dropped. |
instructions | The system prompt, ahead of any system or developer items. |
tools | function, namespace and custom tools, and the hosted tools plugboard leaves out. See Tools. parameters: null means no arguments. strict is accepted and ignored. |
tool_choice | auto, none, required, {"type":"function","name":"..."} (with namespace for a tool of a namespace) or {"type":"custom","name":"..."}. |
text.format | json_schema and json_object, mapped as in Chat Completions (see architecture). text is the default. |
max_output_tokens, temperature, top_p | Passed to the model. |
reasoning.effort | Picks the model variant of that name when the model has one and the id carries none (openai/gpt-5.6-luna with low becomes openai/gpt-5.6-luna#low). Otherwise ignored. reasoning.summary is ignored. |
stream | The standard event sequence, below. |
parallel_tool_calls: false | Keeps only the first tool call of the answer, and the response carries x-plugboard-parallel-tools: limited. The response echoes the value it received (true unless the request sent false). |
include, metadata, user, service_tier, prompt_cache_key, safety_identifier, max_tool_calls, stream_options | Accepted and ignored. |
store | On by default, as on OpenAI: the answer is kept for previous_response_id and the stored-response routes. store: false keeps nothing: the answer is not retrievable, cannot be chained from, and a later item_reference to its items is a 400 item_not_found. The response echoes the value you sent (true when you leave it out). |
previous_response_id | The id of a stored response of the same app. Its input and output come first, then your input. instructions do not carry over. An unknown id is a 400 previous_response_not_found. See Stored responses. |
What is refused
A 400 with code: "unsupported_parameter" and param naming the field:
| Request | Why |
|---|---|
conversation | Use previous_response_id, or send the full input each time. |
item_reference input items that name no output item plugboard holds | See Multi-turn with the Vercel AI SDK. A reference to an output item of a recent answer works. |
background: true, prompt (stored prompts), top_logprobs, truncation: "auto" | Not available. |
Tool types other than function, namespace, custom and the hosted types in Tools (local_shell, tool_search, computer_use_preview, ...), and anything but a function or custom tool inside a namespace | They need a client-side exchange plugboard has no shape for. |
Hosted tools (web_search, file_search, ...) when the request has no function tool to send with them, whatever tool_choice says; and any hosted tool in an app with onHostedTools: "refuse" | They run on OpenAI's servers. Next to function tools they are left out instead, see Tools. |
tool_choice objects other than a function or custom tool name (allowed_tools, hosted tool types), and a named tool together with text.format json_schema | Not available. |
Other refusals:
| Request | Answer |
|---|---|
input_image or input_file with a file_id the app does not hold | 404 invalid_request_error, No such File object: <id>: the body the Files API answers. |
A tool named respond together with text.format json_schema | 400 reserved_tool_name. |
More tools than maxTools | 400 too_many_tools. The count is of the functions the model gets: a namespace tool counts one per tool, and hosted tools count none. |
Two tools with the same name, after a namespace tool is flattened | 400 invalid_request_error, Duplicate tool name. A later additional_tools item that declares a name again replaces the earlier one instead. |
Any other invalid input, for example a function_call_output whose call_id no earlier function_call item has | 400 invalid_request_error. |
Tools
Codex CLI sends all of these, so a stock config.toml works (see Codex CLI).
| Tool | What plugboard does |
|---|---|
function | Offered to the model as is. |
namespace ({"type":"namespace","name":"mcp__weather","tools":[...]}) | Flattened into one function per tool, named <namespace>__<name> (mcp__weather__get_weather). A name over 64 characters, the longest OpenAI and Anthropic take, is cut to 55 characters and ends in _ and 8 hex characters of a hash of the namespace and the name, so it is the same on every request. The name stays bare in the functions namespace, Codex's own. A call comes back as function_call with name set to the bare tool name and namespace set to the namespace, which is the shape Codex dispatches on. A function_call with a namespace in input is read back to the flattened name. |
custom (a freeform tool, such as Codex's exec and apply_patch) | Offered as a function with one string argument input, with the tool's grammar in its description. A call comes back as a custom_tool_call item whose input is that string, and custom_tool_call_output carries the result. A call the model made without the input argument gets an empty input. |
web_search, web_search_preview, file_search, code_interpreter, image_generation, mcp | Left out when function tools are sent with them: they run on OpenAI's servers, and a client cannot know the backend has none. The response carries x-plugboard-dropped-tools with the types, such as web_search, and the echoed tools leave them out. With no function tool, the request is refused. The per-app option onHostedTools (see Per-app policy) is drop (the default) or refuse, which refuses a hosted tool in any case. |
An additional_tools input item ({"type":"additional_tools","role":"developer","tools":[...]}) is a tool list like tools, and Codex sends it instead of tools for the models it knows. All the lists are joined, and the echoed tools include them. A name declared in more than one additional_tools item (a chain made with previous_response_id sends the list of every turn) takes the newest declaration, so a description that changed between turns is not an error. A name twice in one list, or in tools and in an additional_tools item, is a duplicate.
Output
A response object with output items in order: a message item (msg_..., one output_text part) when the model wrote text, and one function_call item (fc_..., with call_id, name, arguments, and namespace for a tool of a namespace) per tool call, or a custom_tool_call item (ctc_..., with call_id, name, input) for a freeform tool. status is completed, or incomplete with incomplete_details.reason max_output_tokens or content_filter. The message item that the cut left open is incomplete too. usage has input_tokens (including cache reads), output_tokens (including reasoning), total_tokens and the *_details objects. The x-plugboard-session header works as on Chat Completions.
Reasoning text comes back only when the server sets reasoningOutput (per app or global). A reasoning item then comes before the message, with one summary_text part holding the provider's reasoning text itself, not a generated summary. The item appears whether or not the request set reasoning.summary. See Reasoning output.
Errors use the same body as Chat Completions: {"error": {"message", "type", "param", "code"}} with the same status codes. In a stream, nothing is sent until there is output, so a failure before the first token is a real HTTP error. A failure after that ends the stream with a response.failed event carrying error.code and error.message.
Streaming
stream: true sends SSE frames event: <type> then data: <json>, each event with a sequence_number, and no [DONE] (the Responses API has none):
response.created
response.in_progress
response.output_item.added (message)
response.content_part.added
response.output_text.delta (one per model text chunk)
response.output_text.done
response.content_part.done
response.output_item.done
response.output_item.added (function_call, one per call)
response.function_call_arguments.delta
response.function_call_arguments.done
response.output_item.done
response.completed (or response.incomplete / response.failed), with the full response and usageTool calls arrive whole from the model, so each call is a single function_call_arguments.delta holding the full arguments, as in Chat Completions. A freeform call has response.custom_tool_call_input.delta and response.custom_tool_call_input.done in their place, with the whole input.
A structured answer (json_schema) is one output_text.delta. Its text is held back until the turn ends, so the stream commits with the first output event (the keep-alive comments start then) and a missing answer is a response.failed event after that. Without a respond call the answer is taken from the final text when it is the JSON the schema asks for, the same small check as Chat Completions (x-plugboard-structured-fallback: text, non-streamed responses only). Any other turn without the call is a 502 structured_output_missing for every finish reason, or a response.failed once a stream has started.
Stored responses
With store on (the default), plugboard keeps each completed or incomplete answer, per app: every input item the model saw, and the response object. A failed answer is not kept.
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:4010/v1", api_key="sk-plug-...")
first = client.responses.create(model="fast", input="My name is Ada.")
second = client.responses.create(model="fast", input="What is my name?", previous_response_id=first.id)
client.responses.retrieve(second.id)
client.responses.input_items.list(second.id, order="asc")
client.responses.delete(second.id)previous_response_id. The new request's input is the earlier response's input, then its output, then yourinput. Chains of chains work: each stored response holds the whole input it ran on.instructionsare not carried over (OpenAI's rule): send them again if you want them.tools,text.formatand the other request fields are the request's own too. The response echoes the id inprevious_response_id. A function call in the earlier output can be answered with afunction_call_outputin yourinput. The model may change between the turns of a chain.GET /v1/responses/{id}. The response object as the caller got it.stream=trueis a 400unsupported_parameter: a stored response is not replayed.includeis ignored.DELETE /v1/responses/{id}.{"id", "object": "response.deleted", "deleted": true}. It also drops the response's items from theitem_referencememory. A response chained from it keeps working, because it holds its own copy of the input.GET /v1/responses/{id}/input_items. A list object{object: "list", data, first_id, last_id, has_more}of every input item the model saw, history included.limitis 1 to 100 (default 20),orderisdesc(default, newest first) orasc, andafteris an item id: the page starts after it in the listing order. Every item has anid: the one the caller sent, or a generatedmsg_,fc_orfco_one. A message is always in OpenAI's full shape, whatever shorthand the request used:type: "message", arole, andcontentas a list of parts (input_text, oroutput_textfor an assistant message). A badlimit,orderorafteris a 400 with the parameter named.- One app only. An app reads, lists, deletes and chains from its own responses. Another app's id is the same miss as an unknown, deleted or expired id, so an id tells another app nothing. Ids are
resp_plus 32 hex characters (128 bits, 122 of them random), so they cannot be guessed. Errors use OpenAI's body withinvalid_request_error.GET,DELETEandinput_itemsanswer 404:Response with id '...' not found.A POST with an unknownprevious_response_idanswers 400, as OpenAI does, withcode: "previous_response_not_found",param: "previous_response_id"and the messagePrevious response with id '...' not found. - Where it lives and for how long. In the OpenCode plugin storage, the same place usage and runtime keys use, so it survives a restart. It is kept for the app's history retention:
sessions.maxAgeDays(orapps.<name>.sessions.maxAgeDays), 30 days when unset.maxAgeDays: 0keeps sessions forever, but a stored response is a whole conversation, so0keeps these 30 days too. There is no separate option: one number says how long an app's history lives. A response past its age is a 404 at once, and a sweep from request handling deletes it. Each app keeps at most 1000 responses and 128 MiB; the oldest go first. An answer whose stored form is over 8 MiB (large images in the input, say) is answered normally but not kept, and plugboard logsresponses.store_skipped; chaining from it is a 400. If storage fails, the answer is given andresponses.store_failedis logged. - Privacy. A stored response holds the prompts, images and answers of a conversation, in the host's storage. Use
store: falsefor anything that must not be kept, andDELETEto remove one. Nothing is logged but counts and operation names: never an id or a body.
Multi-turn with the Vercel AI SDK
The SDK works with its default store: true. It replays each earlier assistant output as an item_reference, and plugboard resolves it from an in-memory store of the output items it returned: per app, the last 1000 answers, kept for one hour, in the process only. That memory is separate from stored responses, which are kept in the host storage.
A reference to an item the store does not hold is a 400 item_not_found. That happens after a plugboard restart, after the answer expired, for another app's item, and for an answer made with store: false. To avoid it, send the earlier items in full, or set providerOptions: { openai: { store: false } }, which sends them in full on every turn. Tool loops work either way, because a tool call is replayed as a full function_call item.
LangChain
@langchain/openai forces this API for ids containing gpt-5.6, gpt-5.2-pro, gpt-5.4-pro, gpt-5.5-pro and codex. LangChain's standard base64 image blocks reach plugboard as Chat-style image_url parts, so plugboard accepts that part type here too.
Sampling parameters are forwarded as sent and some providers reject them; see the client pages.