OpenAI Responses API on your server
POST /v1/responses speaks the OpenAI Responses API: typed input and output items, function calls as
items and typed stream events. Code written for the Responses API works with ZenithAI after two changes:
the base URL and the model name. Your prompts and answers stay on your own server.
Send a request
Section titled “Send a request”from openai import OpenAI
client = OpenAI(base_url="https://dummydomain/v1", api_key="YOUR_API_KEY")
response = client.responses.create( model="Fast", instructions="Answer in two sentences.", input="Explain why leaves look green.",)print(response.output_text)import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://dummydomain/v1", apiKey: process.env.API_KEY });
const response = await client.responses.create({ model: "Fast", instructions: "Answer in two sentences.", input: "Explain why leaves look green.",});console.log(response.output_text);curl --fail-with-body "https://dummydomain/v1/responses" \ -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \ -d '{"model":"Fast","instructions":"Answer in two sentences.", "input":"Explain why leaves look green."}'Use Instant, Fast, Smart or Genius (when your server has it) as the model. See
Choose a tier. instructions becomes the system prompt.
The answer is a response object:
| Field | What it holds |
|---|---|
id |
A resp_... id |
status |
completed, incomplete (the answer reached max_output_tokens) or failed |
output |
A message item with output_text content, and function_call items when the model calls your functions |
output_text |
In the OpenAI libraries, all the text of output joined together |
usage |
input_tokens, output_tokens and total_tokens |
Follow-up questions
Section titled “Follow-up questions”The endpoint is stateless: nothing is stored on the server. For a follow-up, send the conversation
again in input. Every output item can be sent back exactly as you got it.
history = [{"role": "user", "content": "Suggest one name for a tea shop in Pune. Reply with the name only."}]first = client.responses.create(model="Fast", input=history)
history += first.output # the answer's items, sent back unchangedhistory.append({"role": "user", "content": "Now give it a short Marathi tagline, with its meaning in English."})second = client.responses.create(model="Fast", input=history)print(second.output_text)store: true, previous_response_id and conversation are refused with HTTP 400 and the code
unsupported_parameter. Leave store out, or send false.
Call your own functions
Section titled “Call your own functions”On tiers whose capabilities.tools is true in /v1/models, the model can ask your code to run a
function. A tier without it answers HTTP 400 unsupported_tools. Run it, add the result as a function_call_output item with the same
call_id, and send the request again.
import json
tools = [{ "type": "function", "name": "get_order_status", "description": "Look up an order by its number", "parameters": { "type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"], "additionalProperties": False, }, "strict": True,}]
def get_order_status(order_id): return {"order_id": order_id, "status": "shipped", "expected": "14-10-2026"}
items = [{"role": "user", "content": "Where is my order SO-4471?"}]response = client.responses.create(model="Smart", input=items, tools=tools)
for _ in range(5): # cap the rounds calls = [item for item in response.output if item.type == "function_call"] if not calls: break items += response.output for call in calls: result = get_order_status(**json.loads(call.arguments)) items.append({"type": "function_call_output", "call_id": call.call_id, "output": json.dumps(result)}) response = client.responses.create(model="Smart", input=items, tools=tools)
print(response.output_text)Only function tools are accepted. tool_choice takes auto, none, required or
{"type": "function", "name": "..."}. The arguments are checked against your schema before you get them,
even when strict is left out. Still check values in your code before you act on them.
Return images from your functions
Section titled “Return images from your functions”A function can hand a picture back to the model. Put input_text and input_image parts in the
output of a function_call_output, in the order you want them read. The model looks at the image and
answers from it. This is how an agent fetches a scanned bill from your document store, a photo from a site
camera or a chart from your reporting tool, and then reasons about what it sees.
import base64, jsonfrom pathlib import Path
tools = [{ "type": "function", "name": "get_bill_scan", "description": "Fetch the scanned image of a bill by its id", "parameters": { "type": "object", "properties": {"bill_id": {"type": "string"}}, "required": ["bill_id"], "additionalProperties": False, }, "strict": True,}]
def get_bill_scan(bill_id): # Your document store goes here. This example reads a file from disk. data = base64.b64encode(Path("inbox/bill-001.png").read_bytes()).decode() return [ {"type": "input_text", "text": f"Scan of bill {bill_id}:"}, {"type": "input_image", "image_url": f"data:image/png;base64,{data}"}, ]
items = [{"role": "user", "content": "Who is the vendor on bill B-17, and what is the total payable?"}]response = client.responses.create(model="Fast", input=items, tools=tools)
for _ in range(5): # cap the rounds calls = [item for item in response.output if item.type == "function_call"] if not calls: break items += response.output for call in calls: parts = get_bill_scan(**json.loads(call.arguments)) items.append({"type": "function_call_output", "call_id": call.call_id, "output": parts}) response = client.responses.create(model="Fast", input=items, tools=tools)
print(response.output_text)outputis either a string, as before, or a list ofinput_textandinput_imageparts. Text and images can be mixed, and their order is kept.- Send each image output right after the
function_callit answers, in the run of outputs that follows that call, as the loop above does. Otherwise the request is refused with HTTP 400unpaired_tool_output_image. - Images from your functions and images in user messages share one budget: 8 images and 20 MiB per
request, 10 MiB per image, inside the tier’s context window. Use a tier whose
capabilities.toolsandcapabilities.visionare both true in/v1/models. - It works with streaming, every
tool_choicemode andtext.format, so the final answer can be JSON that matches your schema. - In a request that carries images, the text
[img]and[img-is reserved. In ordinary text it is sent with a space added ([ img]). In tool names, descriptions, schemas and call arguments it is refused with HTTP 400reserved_image_marker.
JSON that matches your schema
Section titled “JSON that matches your schema”Set text.format to a JSON schema. The answer in output_text is JSON that matches it. The checks are
the same as for response_format; see Structured JSON output.
import json
schema = { "type": "object", "properties": { "invoice_number": {"type": "string"}, "date": {"type": "string"}, "total": {"type": "number"}, }, "required": ["invoice_number", "date", "total"], "additionalProperties": False,}
text = "Tax invoice INV-2026-0412 from Sharma Traders dated 03-10-2026. Total payable Rs 1,18,000."response = client.responses.create( model="Fast", instructions="Extract the invoice fields. Keep dates as DD-MM-YYYY.", input=text, text={"format": {"type": "json_schema", "name": "invoice", "schema": schema, "strict": True}},)print(json.loads(response.output_text)){"type": "json_object"} asks for any JSON object, and {"type": "text"} is the default.
Streaming
Section titled “Streaming”With stream: true the answer arrives as typed events, numbered by sequence_number. Show the text from
response.output_text.delta events. The last event is response.completed (or response.incomplete),
which carries the whole response. There is no [DONE] line.
stream = client.responses.create(model="Fast", input="Write a two-line welcome note.", stream=True)for event in stream: if event.type == "response.output_text.delta": print(event.delta, end="", flush=True) elif event.type == "response.failed": raise RuntimeError(event.response.error.message)print()import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://dummydomain/v1", apiKey: process.env.API_KEY });
const stream = await client.responses.create({ model: "Fast", input: "Write a two-line welcome note.", stream: true });for await (const event of stream) { if (event.type === "response.output_text.delta") process.stdout.write(event.delta); else if (event.type === "response.failed") throw new Error(event.response.error.message);}console.log();curl --no-buffer "https://dummydomain/v1/responses" \ -H "Authorization: Bearer $API_KEY" -H 'Content-Type: application/json' \ -d '{"model":"Fast","stream":true,"input":"Write a two-line welcome note."}'The events come in this order: response.created, response.in_progress, then for each output item
response.output_item.added, response.content_part.added, response.output_text.delta (many),
response.output_text.done, response.content_part.done and response.output_item.done. A function
call sends response.function_call_arguments.delta and .done instead of the text events.
Answers with tools or a text.format are checked in full before anything is sent, so they arrive in one
piece after the check.
If a stream fails after text has started, the open message is closed cleanly first: you get
response.output_text.done, response.content_part.done and response.output_item.done with the item’s
status set to incomplete, and then response.failed. A stream that failed before any text has an
empty output.
OpenAI Agents SDK
Section titled “OpenAI Agents SDK”The OpenAI Agents SDK works with ZenithAI through
OpenAIResponsesModel. Turn tracing off: by default the SDK uploads traces of every run to OpenAI.
from agents import Agent, OpenAIResponsesModel, Runner, function_tool, set_tracing_disabledfrom openai import AsyncOpenAI
set_tracing_disabled(True) # keep every prompt and answer on your own server
client = AsyncOpenAI(base_url="https://dummydomain/v1", api_key="YOUR_API_KEY")
@function_tooldef get_order_status(order_id: str) -> str: """Look up an order by its number.""" return f"Order {order_id} was shipped and is expected on 14-10-2026."
agent = Agent( name="Support desk", instructions="Answer customers briefly and politely.", model=OpenAIResponsesModel(model="Smart", openai_client=client), tools=[get_order_status],)print(Runner.run_sync(agent, "Where is my order SO-4471?").final_output)LangChain works the same way with ChatOpenAI(base_url=..., api_key=..., model="Smart", use_responses_api=True). Coding agents that need hosted tools or stored responses, such as Codex CLI,
are not supported.
Request fields
Section titled “Request fields”| Field | Notes |
|---|---|
model |
Required. A tier name from /v1/models |
input |
A string, or a list of items: messages (roles system, developer, user, assistant), function_call and function_call_output items. Message parts: input_text, input_image. A function_call_output holds a string or a list of input_text and input_image parts |
instructions |
The system prompt |
tools, tool_choice, parallel_tool_calls |
Function tools only, on tiers with capabilities.tools in /v1/models |
text.format |
text, json_object or json_schema |
max_output_tokens |
The longest answer you want |
temperature, top_p |
As on /v1/chat/completions |
stream |
true for typed events |
metadata |
Up to 16 string values, sent back in the response and not stored |
Images go in input_image parts as data: URLs, with the same rules as on
/v1/chat/completions. An https image address works only if your administrator has allowed its host; this is off by default, and the server then fetches the image itself, with no redirects.
These are accepted and ignored, so client defaults keep working: store (false or null), user,
prompt_cache_key, safety_identifier, max_tool_calls, client_metadata, service_tier (auto or
default), truncation: "disabled", an empty include, reasoning (null or {}), background: false,
top_logprobs: 0 and text.verbosity: "medium". A reasoning item in input is accepted and dropped.
These are refused with HTTP 400, and param names the field:
| Refused | Code |
|---|---|
store: true, previous_response_id, conversation, background: true, prompt, truncation: "auto", a non-empty include, reasoning options, other service_tier values, context_management |
unsupported_parameter |
Hosted tools such as web_search or file_search |
unsupported_tool |
input_file, input_audio and file_id parts |
unsupported_content_part |
| Other item types | unsupported_input_item |
Any other field is refused as unknown. For web search use
POST /websearch, and for PDFs use POST /pdf.
Limits and errors
Section titled “Limits and errors”/v1/responses and /v1/chat/completions share the same API keys, tiers, request limits and
usage counting. The default limits are:
| Limit | Default |
|---|---|
| Requests per API key | 60 a minute, across both endpoints |
| Open streams per API key | 4 at a time |
| Requests from one network address | 120 a minute, 40 in any 10 seconds, 16 at the same time |
Your administrator can change these numbers. Read each tier’s limits, such as its context window, from
/v1/models, as described in Choose a tier.
Errors before the answer starts use the same error object and status codes as
OpenAI-compatible chat: message, type, code, param and
request_id. A request over a limit gets HTTP 429 with the code rate_limit_exceeded and a Retry-After
header in seconds: wait that long and send it again. The OpenAI libraries retry for you. For every field
and error code, see the Responses API reference.
Common questions
Does ZenithAI support the OpenAI Responses API?
Yes. POST /v1/responses works with the official OpenAI libraries, the OpenAI Agents SDK and LangChain. Point the client at your server followed by /v1 and use a tier name such as Fast or Smart as the model.
Can I use previous_response_id or store true with ZenithAI?
No. The endpoint is stateless and keeps nothing, so store true and previous_response_id are refused with HTTP 400. Send the whole conversation in input each turn. Every output item can be sent back unchanged.
Should I use /v1/responses or /v1/chat/completions?
Both run on the same engine with the same keys, tiers and limits. Use /v1/responses for new code or for tools built on it, such as the Agents SDK. Keep /v1/chat/completions for code that already uses it.
Does the OpenAI Agents SDK send data to OpenAI when used with ZenithAI?
The SDK uploads traces to OpenAI by default. Call set_tracing_disabled(True) before you run an agent, as the example on this page does, so prompts and answers stay on your own server.
Can my function return an image to the model?
Yes. A function_call_output can hold input_text and input_image parts, so your tool can hand back a scan, a camera photo or a chart and the model reads it before it answers. Use a tier whose capabilities.vision is true.
Why does my server answer 404 for /v1/responses?
Either your server runs an installer older than 4.6.0, which does not have the endpoint, or your administrator turned it off, which answers api_disabled. Update the server with installer 4.6.0, or ask your administrator to turn it on.
