go2llm API documentation

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 whose endpoints in GET /v1/models lists only messages is not reachable through /v1/responses and returns 400 model_protocol_mismatch.
  • responses does not appear in the catalog's endpoints field. endpoints lists what is sold on a model, not which dialects you can speak to us in. Don't look for responses there — go by chat.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

FieldBehaviour
input as a stringbecomes a single user message
input as an arraymessage items with input_text, input_image, output_text
instructionssystem instruction, placed before input
tools, tool_choicethe flat Responses shape: {"type":"function","name",...}
function_call / function_call_outputthe tool-call loop inside input
max_output_tokenscaps the answer length
streamSSE with named events
parallel_tool_callspassed through as is
response_format, text.formatstructured 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:

FieldResponse
previous_response_id400 stateful_responses_unsupported
store: true400 stateful_responses_unsupported
background: true400 — 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.