Chat Completions
POST /v1/chat/completions is the OpenAI Chat Completions API, and the one most apps speak. Point any OpenAI SDK at http://127.0.0.1:4010/v1 with your plugboard key.
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:4010/v1", api_key=PLUGBOARD_KEY)
reply = client.chat.completions.create(model="fast", messages=[{"role": "user", "content": "reply with exactly: pong"}])The same with curl and jq. PLUGBOARD_BASE_URL is http://127.0.0.1:4010/v1 and PLUGBOARD_KEY is your key. post sends a JSON body:
post() { # $1 = JSON body
curl -sS -f "$PLUGBOARD_BASE_URL/chat/completions" \
-H "Authorization: Bearer $PLUGBOARD_KEY" -H "Content-Type: application/json" -d "$1"
}out=$(post "$(jq -cn --arg m "$1" '{model: $m, messages: [{role: "user", content: "Reply with exactly: pong"}]}')")Plugboard is stateless here: every request carries the whole conversation in messages.
Request fields
| Field | What plugboard does |
|---|---|
model | An alias, or provider/model[#variant] that models allows. |
messages | system, developer, user, assistant and tool messages. User content can hold text and images. A file part takes file_data (a PDF data URL) or a file_id from the Files API. An unknown file_id is a 404. |
stream, stream_options.include_usage | Server-sent events. With include_usage, the last chunk carries the usage. |
tools, tool_choice, parallel_tool_calls | Tool calls come back to you; plugboard never runs them. parallel_tool_calls: false keeps the first call and sets x-plugboard-parallel-tools: limited. |
response_format | json_schema or json_object. See structured output. |
n | Several answers, one full model call each, up to maxChoices. See completions. |
temperature, top_p, presence_penalty, frequency_penalty | Passed to the model. Values a model is known to reject are dropped and listed in x-plugboard-dropped-params. |
seed | An integer, passed to the model. A model whose protocol sends no seed (Claude, GPT-5 and 6) drops it and lists seed in x-plugboard-dropped-params. There is no top-k field here. |
max_completion_tokens, max_tokens | The output cap. max_completion_tokens wins when both are sent. |
stop | Up to 4 sequences. The answer ends before the first match. |
reasoning_effort | Picks the model's variant of that name, else the nearest one above it, else its highest. A model with no variants answers without thinking and sets x-plugboard-thinking: unavailable. |
user | The end user, for usage per user and the history title. |
Refused
Errors are 400s with the code below and param naming the field.
| Request | Code |
|---|---|
logprobs: true, or any top_logprobs | unsupported_parameter |
n above maxChoices | too_many_choices |
A tool_choice that names a tool, with response_format json_schema | unsupported_parameter |
A tool named respond, with response_format json_schema | reserved_tool_name |
Streaming
out=$(curl -sS -N "$PLUGBOARD_BASE_URL/chat/completions" -H "Authorization: Bearer $PLUGBOARD_KEY" -H "Content-Type: application/json" \
-d "$(jq -cn --arg m "$1" '{model: $m, stream: true, stream_options: {include_usage: true}, messages: [{role: "user", content: "Reply with exactly: pong"}]}')")The status and headers go out with the first model event. A slow model gets a keep-alive comment every 15 seconds. If the run fails after the stream has started, the client gets an error event and then data: [DONE].
Tools
The model's tool calls come back with finish_reason: "tool_calls". Run the tool yourself and send the result back as a tool message with the same call id. The whole round trip with curl:
body=$(jq -cn --arg m "$model" --argjson tool "$tool" \
'{model: $m, messages: [{role: "user", content: "What is the weather in Paris? Use the tool."}], tools: [$tool]}')
out1=$(post "$body")
call_id=$(jq -r '.choices[0].message.tool_calls[0].id // ""' <<<"$out1")
args=$(jq -r '.choices[0].message.tool_calls[0].function.arguments // "{}"' <<<"$out1")
# The assistant message goes back as the model sent it, then the tool result with the same call id.
body=$(jq -cn --arg m "$model" --argjson tool "$tool" --argjson turn1 "$(jq -c '.choices[0].message' <<<"$out1")" \
--arg id "$call_id" --rawfile result "$SHARED/get_weather.result.json" \
'{model: $m, tools: [$tool], messages: [{role: "user", content: "What is the weather in Paris? Use the tool."}, $turn1, {role: "tool", tool_call_id: $id, content: $result}]}')
out2=$(post "$body")Tool call ids differ by model, so do not parse them. maxTools (default 64) caps the tools one request may carry. An app with opencodeTools can also let OpenCode run some of its own tools; see Tools, structured output and images.
Structured output
response_format: {"type": "json_schema", ...} returns the JSON as the message content. The model answers through a respond tool, and the provider does not enforce the schema, so validate the result. json_object carries no schema: name the keys in the prompt.
With caller tools as well, the model may either call one of your tools or give the schema answer.
Errors
Errors use OpenAI's shape, {"error": {"message", "type", "code", "param"}}. See Limits and errors for the codes and Response headers for the x-plugboard-* headers.