Responses API
POST /v1/responses accepts requests in the OpenAI Responses API format. The base URL is https://go2llm.tech/v1 and auth is Authorization: Bearer YOUR_API_KEY.
Under the hood it is a protocol adapter on top of chat.completions, not a separate billing route. Two rules follow from that, and it is cheaper to learn them here than from a 400:
- Exactly the same models are served as on
POST /v1/chat/completions. A model whoseendpointsinGET /v1/modelslists onlymessagesis not reachable through/v1/responsesand returns400 model_protocol_mismatch. responsesdoes not appear in the catalog'sendpointsfield.endpointslists what is sold on a model, not which dialects you can speak to us in. Don't look forresponsesthere — go bychat.completions.
Reservation, actual charge, RPM limit and usage.cost_usd are the same as on chat.completions. Responses has no separate tariff.
Quick check
curl https://go2llm.tech/v1/responses \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"model": "gpt-5.6-luna",
"input": "Reply with one word: works",
"max_output_tokens": 32
}'
The response:
{
"id": "resp_...",
"object": "response",
"status": "completed",
"model": "gpt-5.6-luna",
"output": [
{
"type": "message",
"id": "msg_...",
"role": "assistant",
"status": "completed",
"content": [{ "type": "output_text", "text": "Works", "annotations": [] }]
}
],
"usage": {
"input_tokens": 14,
"output_tokens": 3,
"total_tokens": 17,
"input_tokens_details": { "cached_tokens": 0 },
"cost_usd": 0.0000013
}
}
usage.cost_usd is the actual USD charge — the same field, with the same meaning, as on every other route. If the answer ran into max_output_tokens, status is incomplete and the reason is in incomplete_details.reason.
What is supported
| Field | Behaviour |
|---|---|
input as a string | becomes a single user message |
input as an array | message items with input_text, input_image, output_text |
instructions | system instruction, placed before input |
tools, tool_choice | the flat Responses shape: {"type":"function","name",...} |
function_call / function_call_output | the tool-call loop inside input |
max_output_tokens | caps the answer length |
stream | SSE with named events |
parallel_tool_calls | passed through as is |
response_format, text.format | structured output |
What is rejected, and why
The shim is stateless: the gateway keeps no response history. Keeping it would mean a persistent store with its own TTL and privacy surface, for the sake of something agentic clients don't use anyway — they resend the full history every turn. So instead of breaking silently on the second turn you get a named rejection:
| Field | Response |
|---|---|
previous_response_id | 400 stateful_responses_unsupported |
store: true | 400 stateful_responses_unsupported |
background: true | 400 — there are no background jobs |
curl -i https://go2llm.tech/v1/responses \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{"model":"gpt-5.6-luna","input":"go on","previous_response_id":"resp_abc"}'
{
"error": {
"type": "invalid_request_error",
"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_..."
}
}
Match on error.code, not on the message text: the wording may change, the code will not.
The fix is one change on the client side: instead of passing previous_response_id, append the previous output items back into the input of the next request. store can simply be omitted, or sent as false.
Python — OpenAI SDK, no streaming
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["GO2LLM_API_KEY"],
base_url="https://go2llm.tech/v1",
)
response = client.responses.create(
model="gpt-5.6-luna",
instructions="Answer in one line, no preamble.",
input="Name three ways to speed up a container cold start",
max_output_tokens=300,
)
print(response.output_text)
print(response.usage) # on this gateway it also carries cost_usd
Python — streaming
A Responses stream is made of named SSE events, not anonymous data: frames. Text is assembled from response.output_text.delta, and the end is response.completed.
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["GO2LLM_API_KEY"],
base_url="https://go2llm.tech/v1",
)
stream = client.responses.create(
model="gpt-5.6-luna",
input="Write a SQL query that counts DAU over an events table",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
elif event.type == "response.completed":
print()
print(event.response.usage)
Event order for a text answer: response.created → response.in_progress → response.output_item.added → response.content_part.added → response.output_text.delta (many) → response.output_text.done → response.content_part.done → response.output_item.done → response.completed. For a tool call, response.function_call_arguments.delta and .done arrive instead of the text events.
After the first valid SSE chunk the gateway neither switches routes nor retries the request automatically — that is a general rule, not a Responses quirk. Your client shouldn't blindly retry a started stream either.
Python — a tool call
In Responses, a tool description is flat: name, description and parameters sit directly on the object, with no nested function.
import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["GO2LLM_API_KEY"],
base_url="https://go2llm.tech/v1",
)
tools = [
{
"type": "function",
"name": "get_order_status",
"description": "Returns the status of an order by its number",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
}
]
def get_order_status(order_id: str) -> dict:
# your code runs here, not the model
return {"order_id": order_id, "status": "shipped", "eta_days": 2}
conversation = [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "What's going on with order A-1043?"}],
}
]
first = client.responses.create(
model="gpt-5.6-luna",
input=conversation,
tools=tools,
)
for item in first.output:
if item.type == "function_call":
result = get_order_status(**json.loads(item.arguments))
conversation.append(
{
"type": "function_call",
"call_id": item.call_id,
"name": item.name,
"arguments": item.arguments,
}
)
conversation.append(
{
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(result, ensure_ascii=False),
}
)
second = client.responses.create(
model="gpt-5.6-luna",
input=conversation,
tools=tools,
)
print(second.output_text)
Your application executes the tool; the model only asks for it. Both requests are billed separately, each with its own usage.cost_usd.
TypeScript
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.GO2LLM_API_KEY,
baseURL: "https://go2llm.tech/v1",
});
const response = await client.responses.create({
model: "gpt-5.6-luna",
input: "Reply: OK",
max_output_tokens: 32,
});
console.log(response.output_text);
Clients
/v1/responses is published and was verified by a live Codex CLI 0.151.0 run on 2026-09-14 with the qwen3-8-flash model — the status and the configuration are described on the coding tools page. We have not tested Cursor with a live request and claim no status for it.
Route errors and the x-go2llm-request-id header are on the API errors page, the full route table is in API endpoints, and per-model prices are in the catalog and on the pricing page.