Responses API (OpenAI Codex)

OpenAI-compatible Responses API for the Codex CLI and other Responses-format clients.

POST /v1/responses

Pendra implements the OpenAI Responses API at /v1/responses (also aliased at /api/v1/responses) so the OpenAI Codex CLI works without modification when pointed at Pendra. The Python and JavaScript examples use the official OpenAI SDKs.

Body application/json
model string Required
Model ID. Must be a model your workers serve — see available models. An id Pendra doesn't serve returns 404.
input string | array Required
The prompt or full conversation. This endpoint is stateless — send everything on each request. Message content can include input_text, input_image and input_file parts; an input_file carries a .txt or .csv of up to 1 MB (see Text and CSV files).
store boolean
Accepted but has no effect — nothing is persisted server-side.
reasoning object
Control thinking on reasoning-capable models. Pass {"effort":"low"|"medium"|"high"} to set reasoning depth, or {"effort":"none"} (or the synonym "minimal") to skip thinking entirely. Pendra also accepts {"max_tokens":4096} here to set a thinking budget in tokens: when it is used up the thinking ends and the model goes on to its answer, and max_output_tokens is then kept for the answer alone. Leave it out and a default budget of 8,192 tokens applies on models whose thinking can be ended early. max_tokens must be a whole number, 0 or more; anything else is rejected (422, or 400 on an encrypted request). With no max_output_tokens, the reply stops at 32,768 tokens, thinking included. See Thinking.
temperature number default: the model's recommended value, else 1.0
How random the output is, from 0 to 2. At 0 the model always picks its most likely next token, for the most focused, repeatable answers. Leave it out to use the value the model's authors recommend.
top_p number default: the model's recommended value, else 1.0
Nucleus sampling: only the most likely tokens whose probabilities add up to top_p are considered. Leave it out to use the value the model's authors recommend.

previous_response_id is not supported and returns a 400.

Response

Pendra returns the OpenAI Responses envelope (see example). output is an array of items; each message item carries an array of content blocks. status is completed on a clean finish, or incomplete when the model hits a stop condition before exhausting max_output_tokens.

When Pendra has something to tell you about a reply, such as the thinking having reached its budget, the response also carries a pendra object with a notice: a stable code and a readable message. When streaming it is on the response in the final response.completed event. OpenAI SDKs ignore the extra field. The codes are listed under Notices.

A non-streaming reply also says which of your workers ran it and how long it took: the X-Worker-Id, X-Worker-Name and Server-Timing headers, and a pendra field in the body that OpenAI clients ignore:

"pendra": {"worker": {"id": "wrk-1a2b3c4d", "name": "london-gpu-01", "version": "3.115.1"},
           "request_id": "3f1c8a2e-5b7d-4e19-9c0a-6d2f8b1e4a73",
           "timing": {"ttft_ms": 115, "tokens_per_second": 58.34, "queue_wait_ms": 16600}}

timing is the request's latency and throughput, the same figures as the Server-Timing header. Read them from the body: a reply slow enough that Pendra keeps the connection alive is sent without that header. Streamed replies don't carry the pendra field. They name the worker the request was sent to in the X-Worker-Id and X-Worker-Name headers when one is free straight away; a stream that had to wait for a busy worker starts without them. See Which worker served the request and Timing for what each field means.

Prompt caching

Pendra caches prompts automatically. When you send follow-up turns of the same conversation, it reuses the part of the prompt it already processed instead of reprocessing it from scratch, which makes the reply start sooner. You don't have to do anything to switch this on.

usage.input_tokens_details.cached_tokens tells you how many of that request's input_tokens came from the cache — the same field OpenAI returns, so a cache hit rate of cached_tokens / input_tokens is comparable. It's always present, and 0 on a first, cold request. A higher rate means a faster first token on that request.

On a streamed response, the cache count lands on response.completed; the response.created event reports 0, because nothing is known about the prompt until the model has read it.

Pin a Pendra model

Codex hard-codes OpenAI model names like gpt-5-codex and gpt-5.5. Pendra serves open models under their own ids and won't substitute one for a name it doesn't recognise, so those requests come back as a 404 telling you to pin a model. Set model in your Codex config to an id from /models — see Integrations → Codex.

Streaming

The Responses API uses its own event taxonomy (response.output_text.delta, response.completed, etc.). Pendra emits these events from streamed chat completions; the Codex CLI consumes them directly.

event: response.created
data: {"type":"response.created","response":{"id":"resp_01HZ8b","object":"response","status":"in_progress","model":"qwen3.6:27b"}}

event: response.output_text.delta
data: {"type":"response.output_text.delta","item_id":"msg_01","output_index":0,"content_index":0,"delta":"Hello"}

event: response.completed
data: {"type":"response.completed","response":{"id":"resp_01HZ8b","status":"completed","usage":{"input_tokens":8,"output_tokens":2,"total_tokens":10,"input_tokens_details":{"cached_tokens":0}}}}

While the model is still reading a long prompt, Pendra sends an SSE comment line (: keep-alive) every 15 seconds so the connection isn't dropped as idle before the first event arrives. It carries no event and no data, so a Server-Sent Events client — the Codex CLI included — discards it; only hand-rolled parsers need to skip lines starting with :.

If you pin a worker with X-Pendra-Worker-Id and it can't serve the request — it doesn't exist, doesn't have the model, or is running a version too old for this endpoint — you get a normal HTTP error response instead of a stream, so check the status code before you start reading events. The same goes, without a pin, when none of your workers is online, when none of your connected workers is new enough for this endpoint, or when an encrypted request is sealed to a key none of your workers holds any more — each a 503 with a Retry-After header, the last with X-Pendra-Error-Code: no_private_inference_worker (fetch your organisation's key again and retry). A worker that's merely busy still reports in-band as an error event once the stream has started. See Errors & rate limits.

Quick start

The full Codex setup — config file, env vars, and a model pin — lives in Integrations → Codex.