400 stateful_responses_unsupported: why previous_response_id and store fail on /v1/responses
This 400 has nothing to do with your key or your JSON. It fires because the request asked go2llm to remember something between calls, and go2llm doesn't. previous_response_id, store: true, and background: true are all rejected for related reasons, but under two different codes — read error.code before deciding what to change.
Written 2026-09-22.
What it looks like
Checked with a live request on 2026-09-22. A request carrying previous_response_id:
curl https://go2llm.tech/v1/responses \
-H "Authorization: Bearer $GO2LLM_API_KEY" \
-H "content-type: application/json" \
-d '{"model":"qwen3-8-flash","input":"ping","previous_response_id":"resp_test123"}'
comes back as a 400:
{
"error": {
"type": "stateful_responses_unsupported",
"code": "stateful_responses_unsupported",
"message": "previous_response_id is not supported: this gateway stores no response state; send the full history in `input`",
"request_id": "gw_33bd1b88efad4611a5b63bd67638b4a2"
}
}
store: true on its own gets the same code with a message naming that field instead:
{
"error": {
"type": "stateful_responses_unsupported",
"code": "stateful_responses_unsupported",
"message": "store: true is not supported: this gateway stores no response state; send the full history in `input`",
"request_id": "gw_bb33b20417724aec87624b2aa5f5dc77"
}
}
background: true is a separate code, not stateful_responses_unsupported — confirmed on the same live run:
{
"error": {
"type": "background_unsupported",
"code": "background_unsupported",
"message": "background: true is not supported: this gateway runs no background jobs, every call is synchronous",
"request_id": "gw_db1c486d2db3489b9840bc8c8851fda0"
}
}
The two codes point at different problems. stateful_responses_unsupported is about having no memory between requests. background_unsupported is about having no async job queue — every call runs synchronously, start to finish, inside one HTTP request. Both checks run ahead of everything else: a request carrying any of the three fields gets its matching 400 even with no key, or a bad one, before x-api-key/Authorization is ever checked.
Why these fields get rejected
A client configured for OpenAI's stateful mode. OpenAI's Responses API defaults to keeping state: store: true saves the response on OpenAI's servers, and previous_response_id continues a conversation without resending the history. A client written against the direct OpenAI API, or migrated from it without changes, carries these defaults straight into go2llm's config.
/v1/responses on go2llm is a protocol adapter, not a product with memory. It translates a request into chat.completions and back, serving exactly the models available on that route. Nothing is stored anywhere, so previous_response_id has no prior context to resolve into, and store: true has nothing to execute.
background: true expects a job to poll, and there isn't one. OpenAI's async mode hands the client a task ID and expects it to poll status separately. go2llm handles the whole call inside one HTTP connection — there's no "running in the background" state in between, so nothing to poll, and the field is rejected on the way in.
Sending history the way this gateway expects
Drop previous_response_id, store, and background from the request body entirely — not set to false or null, just left out, if your client library allows that.
Then send the whole conversation in input on every call, the same way you'd build messages for chat.completions:
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY", base_url="https://go2llm.tech/v1")
history = [
{"role": "user", "content": "What's 12 times 7?"},
{"role": "assistant", "content": "84"},
{"role": "user", "content": "And if I add 16?"},
]
response = client.responses.create(model="qwen3-8-flash", input=history)
print(response.output_text)
Your code owns the history, same as it would with chat.completions. usage.cost_usd still shows up in the response in the usual place — Responses doesn't have a separate billing path.
If you're calling this through Codex CLI, you don't need to touch any of that by hand: codex exec (verified on version 0.151.0) tracks the session's context itself and resends the accumulated input on every call, so previous_response_id never comes up in normal CLI use. More on that setup is on the coding tools docs.
Confirm the fix landed:
curl -s -o /dev/null -w "%{http_code}\n" https://go2llm.tech/v1/responses \
-H "Authorization: Bearer $GO2LLM_API_KEY" \
-H "content-type: application/json" \
-d '{"model":"qwen3-8-flash","input":"Reply with one word: working"}'
A 200 means the rejected fields were the whole problem.
What this endpoint honestly doesn't do
Exactly one mode is published: a synchronous call with no state kept. previous_response_id, store: true, and background: true don't work, and neither does polling a task with GET /v1/responses/{id} — that route doesn't exist on go2llm. The models available are the same ones served on chat.completions, no more: a model whose catalog entry lists only the messages protocol won't answer through /v1/responses.
Related errors
- 404 model_not_found — the model doesn't answer at all, rather than rejecting a parameter.
- 401 authentication_error — key checks on
/v1/responseswork the same as on any other endpoint. - The full error reference.
- Responses API docs — the endpoint's full contract.
FAQ
Can I emulate previous_response_id on my own side? Yes — keep the previous response's output yourself and add it to the next request's input, alongside the new user message, the way the example above does. That's exactly what OpenAI's stateful Responses API does for you server-side; here, the storage just moves to your client.
Why is background: true a different code from previous_response_id and store, when all three are Responses API modes? They're different mechanisms. previous_response_id and store are about keeping conversation state; background is about running the call asynchronously and polling its status. go2llm splits them into separate error codes so your code can branch on error.code instead of guessing from a shared 400.
Does a request rejected with this code cost anything? No. Checking previous_response_id, store, and background happens before cost is reserved, and before the key is even checked — the 400 for these fields comes back before go2llm tries to price the call.
Does streaming still work on /v1/responses without store or previous_response_id? Yes, the synchronous mode streams fine. The restriction only touches state and background execution, not the response protocol itself.
Get a key and make your first request in 3 minutes → go2llm.tech. Registration credits $1 to your balance. Per-model details are on the pricing page.