OpenAI's current SDKs lead with client.responses.create(), the Agents SDK
is built on it, and n8n's OpenAI nodes now call it by default. The request
goes to /v1/responses, not /v1/chat/completions, and point any of
those tools at an open-model endpoint and the usual answer is:
404 Cannot POST /v1/responses
We now serve the Responses API on the same open models as chat
completions. This guide is what happened when we pointed three real
clients at it without changing a line of their code: the official
openai Python SDK, the OpenAI Agents SDK, and n8n 2.38. Every number
below comes from a script published at the end.
| result | |
|---|---|
| openai-python Responses checks behaving as designed | 10 of 10 (one with a debatable model answer) |
| 24-task support agent, hand-written Responses loop | 24 of 24 correct |
| same agent, OpenAI Agents SDK | 24 of 24 correct |
| same agent, Chat Completions loop | 24 of 24 correct |
| latency added by the Responses path, 120 paired requests | about 2 ms, inside the noise |
| n8n: OpenAI node "Message a Model" and Chat Model with Responses on | both work |
What the endpoint is
POST https://paraloncloud.com/v1/responses accepts the Responses request
and translates it into a chat completion on the way in: instructions
become the system message, input items become messages, function tools
become chat tools, text.format becomes response_format,
max_output_tokens becomes max_tokens. The answer is translated back:
an output array of message and function_call items, usage with
input_tokens and output_tokens, and when stream is on, the typed
event stream (response.created through response.completed, with
sequence_number on every event).
The model, the worker, the rate limits and the billing are those of chat completions. What that buys is compatibility; what it costs is three features a translation cannot fake:
- No stored state.
previous_response_idand conversations answer 400. Send the whole input each time, earlier items included, which is what the Agents SDK does anyway. - No built-in tools.
web_search_preview,file_search,code_interpreterand computer use answer 400 with the tool named. Function tools only. - No background mode.
Every refusal is a named 400 invalid_request_error, never a silently
dropped field. The reference is in the
Responses API docs.
Test 1: the OpenAI SDK, feature by feature
The client is the stock openai package, version 3.13, with two
arguments changed:
from openai import OpenAI
client = OpenAI(base_url="https://paraloncloud.com/v1", api_key="prlc_...")
r = client.responses.create(model="qwen3.8-27b", input="Reply with the single word OK.",
reasoning={"effort": "low"})
print(r.output_text)
| feature | worked | note |
|---|---|---|
responses.create + output_text | yes | usage 19 in / 2 out |
instructions | yes | "Answer in French only" was obeyed |
input_image (vision) | yes | a generated red square came back as "Red" |
responses.parse(text_format=PydanticModel) | yes, see below | output_parsed was a valid object |
responses.stream + get_final_response() | yes | streamed text equals the final output_text |
function tool, then function_call_output, then the answer | yes | the second turn used the tool's result |
tool_choice forcing a function | yes | called even for "Tell me a joke" |
max_output_tokens cut-off | yes | status: "incomplete", reason max_output_tokens |
previous_response_id | refused as designed | BadRequestError naming the parameter |
reasoning.effort | yes, with a surprise | below |
The one check our script flagged was responses.parse: the Pydantic
object came back valid, schema enforced, but for "Cancel order A1002
right now, I ordered the wrong size" the model chose the intent
order_status over cancel. That is the model's judgement on a single
ticket, not the API; the classification
guide measures how often
that judgement is right over thousands of documents.
The reasoning surprise. reasoning.effort set to low, minimal
or none switches Qwen 3's thinking pass off; anything else leaves it
on. On the bat-and-ball riddle, thinking on produced 108 output
tokens and the one-line answer "$0.05"; thinking off produced 274,
because the model then shows its working in the answer instead of in a
hidden pass. Thinking tokens are billed as output tokens and never
appear in the text. So "low" is not automatically cheaper: on short
extraction and classification tasks it is much shorter, as the n8n
numbers below show; on a question that needs working, it can be longer.
Test 2: does the translation add latency?
120 identical short requests, alternating between /v1/chat/completions
and /v1/responses on one keep-alive connection, same model and
settings:
| chat completions | responses | |
|---|---|---|
| median, requests that landed on the faster worker | 152 ms | 154 ms |
| median, requests that landed on the slower worker | 487 ms | 487 ms |
| share on the faster worker | 40% | 45% |
The two workers serving the model at the time differed by 330 ms; the two endpoints by about 2 ms. Our first attempt at this measurement, 20 pairs through the SDK, showed Responses 170 ms slower, and it was entirely which worker each request happened to land on. If you benchmark an API with more than one worker behind it, pair the requests and split by worker, or you will measure the router.
Test 3: an agent, three ways
The realistic workload for the Responses API is an agent: a model that
calls tools, reads the results, and decides what to do next. We wrote a
support agent for an outdoor shop with three tools, get_order,
cancel_order and refund_order, a policy in the instructions (cancel
only while processing, refund only within 30 days of delivery, look the
order up first, ask for the order number if it is missing), and eight
orders in a fake backend. Then 24 customer messages, each with a known
right outcome: "Please cancel order A1002" should cancel it; "Refund
A1003, the tent leaks" should not, because it was delivered 41 days ago;
"Refund A1004 and also cancel A1005" should do both; "Hi, I need a
refund" should call nothing. Four of the 24 are in Romanian, German,
Spanish and Chinese.
A task counts as correct only if the set of cancel and refund calls the agent made is exactly the expected set: no missing action, no action the policy forbids.
| Responses loop | OpenAI Agents SDK | Chat Completions loop | |
|---|---|---|---|
| correct | 24 / 24 | 24 / 24 | 24 / 24 |
| model turns per task | 2.46 | 2.46 | 2.42 |
| input tokens per task | 1,485 | 1,608 | 1,416 |
| output tokens per task | 87 | 87 | 91 |
| time per task, median | 3.1 s | 3.3 s | 2.9 s |
| cost per 1,000 tasks | $0.33 | $0.34 | $0.32 |
All three got every task right, including the policy refusals and the multilingual ones. The Agents SDK's requests carry about 8% more input tokens per task, from what the SDK adds to each request; we did not dissect which fields. Everything else is the same agent. The choice between the three is about code, not quality.
The Agents SDK, pointed at an open model
from openai import AsyncOpenAI
from openai.types.shared import Reasoning
from agents import Agent, Runner, function_tool, OpenAIResponsesModel, ModelSettings, set_tracing_disabled
set_tracing_disabled(True) # traces would otherwise be sent to OpenAI with this key
client = AsyncOpenAI(base_url="https://paraloncloud.com/v1", api_key="prlc_...")
@function_tool
def get_order(order_id: str) -> str:
"""Look up an order by id"""
return lookup(order_id)
@function_tool
def cancel_order(order_id: str) -> str:
"""Cancel an order"""
return cancel(order_id)
agent = Agent(
name="support",
instructions=POLICY,
tools=[get_order, cancel_order],
model=OpenAIResponsesModel(model="qwen3.8-27b", openai_client=client),
model_settings=ModelSettings(temperature=0, reasoning=Reasoning(effort="low")),
)
result = await Runner.run(agent, "Please cancel order A1002, I changed my mind.")
print(result.final_output)
Two lines matter. OpenAIResponsesModel with your own client sends the
SDK's requests to /v1/responses on our endpoint. set_tracing_disabled
stops the SDK from uploading traces to OpenAI's tracing service, which
would fail with a non-OpenAI key; if you want traces, add your own
processor.
The loop without an SDK
items = [{"role": "user", "content": message}]
while True:
r = client.responses.create(model="qwen3.8-27b", instructions=POLICY, input=items,
tools=TOOLS, reasoning={"effort": "low"}, temperature=0)
calls = [o for o in r.output if o.type == "function_call"]
if not calls:
break
items += [o.model_dump(exclude_none=True) for o in r.output]
for c in calls:
items.append({"type": "function_call_output", "call_id": c.call_id,
"output": run_tool(c.name, json.loads(c.arguments))})
print(r.output_text)
Because nothing is stored server-side, the growing items list is the
conversation: the model's own function_call items go back in, followed
by your function_call_output for each.
Test 4: n8n
The n8n guide in this series was written
around two traps: the OpenAI Chat Model node's Use Responses API
switch, on by default, and the OpenAI node's Message a Model
operation, which only speaks Responses. Against an endpoint without
/v1/responses, both fail. We reran both on n8n 2.38.7 against ours.
- OpenAI node → Message a Model (node version 2.3): works. 950 ms
for a one-word answer. The text came back as
"\n\nYellow": with thinking on, the template leaves two newlines where the hidden reasoning was; trim it in the next node. - OpenAI Chat Model with Use Responses API on, in the ticket-triage
workflow from the n8n guide: works, same
damaged_item/highanswer, three runs out of three.
The setting that matters is the same as in chat mode, thinking:
| Chat Model, Responses on, same ticket | node time | output tokens billed | tokens n8n displayed |
|---|---|---|---|
| default | 3.1 to 3.6 s | 351 | 45 |
Extra Body {"reasoning": {"effort": "low"}} | 1.0 s | 65 | 54 |
Two findings in that table. First, one Extra Body line takes the node from three and a half seconds to one. Second, n8n's token counter in Responses mode is an estimate from the visible text: it showed 45 output tokens for a call that generated, and billed, 351. If you track cost from n8n's execution data, you undercount thinking models by up to eight times; use your provider's usage figures instead.
What to use, and when
- Your code or framework already speaks Responses: point it at
https://paraloncloud.com/v1, keepreasoning.effortatlowfor tool calling and extraction, and send the full input each turn. - You are starting fresh with no framework: Chat Completions is the wider standard across providers and cost the same here; the Responses shape is nicer for tool loops because the model's calls are items you append, not a message you rebuild.
- You need server-side memory, web search or file search from the API: not on this endpoint. Keep the history in your application and run retrieval yourself; the embeddings endpoint and the semantic search guide cover that part.
The script
responses_bench.py: the SDK feature matrix, the paired latency test, and the 24-task agent through all three paths, with the tasks, the fake order backend and the policy included. About 350 requests and a few cents per full run.
pip install openai openai-agents pydantic
PRL_KEY=prlc_... python3 responses_bench.py
A free key from the Console carries a 250,000-token trial, enough for several full runs; after that the Responses API is billed exactly like chat completions, per input and output token (billing).
Frequently asked questions
Is this the same as OpenAI's Responses API? Same request and
response shape, same streaming events, on open models. It is stateless:
no previous_response_id, no stored responses, no built-in tools.
Does store: true do anything? It is accepted and echoed, and
nothing is stored. There is nothing to retrieve with GET /v1/responses/{id}.
Which models does it work with? Every chat model in GET /v1/models;
qwen3.8-27b accepts images through input_image as well.
Do reasoning tokens show up? They are counted in output_tokens and
billed; the reasoning text itself is not returned, and
output_tokens_details.reasoning_tokens reads 0 because the model does
not report the split.
Why did my other provider return 404 for the same code? Most
OpenAI-compatible servers implement only chat completions. The fix on
their side is a switch back to chat completions in your client (n8n's
Use Responses API, OpenAIChatCompletionsModel in the Agents SDK);
on ours it is not needed.



