Guides & Tutorials11 min read

The OpenAI Responses API on Open Models: responses.create, the Agents SDK and n8n, Measured on Qwen 3.8

Tools are moving from Chat Completions to OpenAI's Responses API, and most open-model endpoints answer it with a 404. We now serve /v1/responses on Qwen 3.8 27B and ran the official OpenAI SDK, the OpenAI Agents SDK and n8n against it unchanged: all 10 SDK checks behaved as designed, a 24-task tool-using agent correct 24 of 24 through all three paths, and no measurable latency added by the translation. What is not supported, and the one setting that cut an n8n node from 3.3 s to 1.0 s.

The Paralon capybara joining two glowing cables with differently shaped plugs through a small adapter

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 designed10 of 10 (one with a debatable model answer)
24-task support agent, hand-written Responses loop24 of 24 correct
same agent, OpenAI Agents SDK24 of 24 correct
same agent, Chat Completions loop24 of 24 correct
latency added by the Responses path, 120 paired requestsabout 2 ms, inside the noise
n8n: OpenAI node "Message a Model" and Chat Model with Responses onboth 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_id and 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_interpreter and 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)
featureworkednote
responses.create + output_textyesusage 19 in / 2 out
instructionsyes"Answer in French only" was obeyed
input_image (vision)yesa generated red square came back as "Red"
responses.parse(text_format=PydanticModel)yes, see belowoutput_parsed was a valid object
responses.stream + get_final_response()yesstreamed text equals the final output_text
function tool, then function_call_output, then the answeryesthe second turn used the tool's result
tool_choice forcing a functionyescalled even for "Tell me a joke"
max_output_tokens cut-offyesstatus: "incomplete", reason max_output_tokens
previous_response_idrefused as designedBadRequestError naming the parameter
reasoning.effortyes, with a surprisebelow

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 completionsresponses
median, requests that landed on the faster worker152 ms154 ms
median, requests that landed on the slower worker487 ms487 ms
share on the faster worker40%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 loopOpenAI Agents SDKChat Completions loop
correct24 / 2424 / 2424 / 24
model turns per task2.462.462.42
input tokens per task1,4851,6081,416
output tokens per task878791
time per task, median3.1 s3.3 s2.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 / high answer, three runs out of three.

The setting that matters is the same as in chat mode, thinking:

Chat Model, Responses on, same ticketnode timeoutput tokens billedtokens n8n displayed
default3.1 to 3.6 s35145
Extra Body {"reasoning": {"effort": "low"}}1.0 s6554

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, keep reasoning.effort at low for 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.

Keep reading

Related Articles

The Paralon capybara holding a magnifying lens up to a floating cloud of glowing connected dots
Guides & Tutorials
11 min

Semantic Search with an Open Embeddings API: 990 Tickets, 5 Languages, and Where Keyword Search Still Wins

We indexed 990 support tickets with a 384-dimension open embedding model over an OpenAI-compatible /v1/embeddings endpoint and searched them 1,290 ways, scored against known intents and a TF-IDF keyword baseline. Same-language, keyword search was better. Across languages it collapsed to chance while embeddings held 47 to 58% precision. Adding a 27B reranker over the top ten lifted every language by 17 to 37 points for $0.09 per thousand queries. The whole index cost a tenth of a cent.

embeddingssemantic searchRAG
The Paralon capybara pulling a glowing ribbon of index cards out of a server cube and stacking them into tall piles
Guides & Tutorials
11 min

Synthetic Dataset Generation with an Open LLM: 8,600 Labeled Examples an Hour on an OpenAI-Compatible API

We generated 990 labeled customer-support examples with Qwen 3.8 27B through a plain OpenAI-compatible endpoint, schema-enforced with a forced tool call. Here is the script, the throughput and latency we measured, the token cost per thousand examples at list price, what the model got right, and the two ways it quietly repeats itself.

synthetic datadataset generationfine-tuning