Permalink to Chat completionsChat completions

POST /v1/chat/completions is the most compatible surface on the API. Requests are forwarded to the upstream essentially byte for byte and the response comes back the same way, so anything an OpenAI-compatible client can send, it can send here.

bash
curl https://hypit.ai/v1/chat/completions \
  -H "Authorization: Bearer $HYPIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini-3.1-pro",
    "messages": [
      {"role": "system", "content": "Answer in one sentence."},
      {"role": "user", "content": "What is Hypit?"}
    ]
  }'

Permalink to with-the-openai-sdksWith the OpenAI SDKs

python
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["HYPIT_API_KEY"],
    base_url=os.environ.get("HYPIT_BASE_URL", "https://hypit.ai/v1"),
)

completion = client.chat.completions.create(
    model="gemini-3.1-pro",
    messages=[{"role": "user", "content": "What is Hypit?"}],
    max_tokens=200,
)
print(completion.choices[0].message.content)
print(completion.usage)
js
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.HYPIT_API_KEY,
  baseURL: process.env.HYPIT_BASE_URL ?? "https://hypit.ai/v1",
});

const completion = await client.chat.completions.create({
  model: "gemini-3.1-pro",
  messages: [{ role: "user", content: "What is Hypit?" }],
  max_tokens: 200,
});
console.log(completion.choices[0].message.content);

Permalink to what-we-read-and-what-we-forwardWhat we read, and what we forward

Only two fields are required, and only a handful are inspected at all:

FieldWhy we read it
modelrequired — chooses the route
messagesrequired, non-empty — walked to find image parts
streamselects the SSE path
stream_optionssee below
modalities["text","image"] asks image-capable chat models for pixels

Everything else — max_tokens, max_completion_tokens, temperature, top_p, stop, tools, tool_choice, response_format, reasoning_effort, and any vendor-private key we have never heard of — rides through to the upstream untouched. The body is capped at 32 MiB.

The response body is the upstream's, verbatim, with exactly two deviations:

  1. Vendor billing keys are stripped (credits, cost, cost_usd, balance, billing and their siblings, at the top level and inside usage) so that another vendor's ledger never leaks into yours. Your real cost is in credits, not in that field.
  2. If we mapped your model name onto a different upstream name, the top-level "model" string is rewritten back to the name you asked for. Where no mapping applies, the vendor's own refinement (gpt-4o-2024-08-06 and the like) is preserved.

usage is otherwise untouched — whatever the upstream reported is what you see. We normalise it internally for billing, but that never changes the bytes on the wire.

Permalink to streamingStreaming

Set "stream": true. The response is text/event-stream, chunks are the upstream's frames verbatim, and the stream ends with data: [DONE] — we add the sentinel if the upstream did not.

python
stream = client.chat.completions.create(
    model="gemini-3.1-pro",
    messages=[{"role": "user", "content": "Write a haiku about a paper boat."}],
    stream=True,
)
for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)
js
const stream = await client.chat.completions.create({
  model: "gemini-3.1-pro",
  messages: [{ role: "user", content: "Write a haiku about a paper boat." }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
bash
curl -N https://hypit.ai/v1/chat/completions \
  -H "Authorization: Bearer $HYPIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gemini-3.1-pro","stream":true,
       "messages":[{"role":"user","content":"Write a haiku about a paper boat."}]}'

Permalink to usage-on-a-streamUsage on a stream

If you do not send stream_options at all, we add {"include_usage": true} for you, so the last frame carries a usage block. If you send stream_options yourself — including {"include_usage": false} — we leave it exactly as written.

A stream never fails over, and never changes status

Once the first byte is on the wire the HTTP status is 200 for good. A failure after that arrives as one SSE frame carrying the standard error envelope, followed by data: [DONE]:

text
data: {"error":{"message":"…","type":"upstream_error","code":"stream_broken","param":null}}

data: [DONE]

Parse error on every frame, not just on the HTTP status. Note also that disconnecting mid-stream does not stop the request being metered — the tokens were generated.

Permalink to visionVision

Image parts work the way they do on OpenAI:

json
{
  "model": "gemini-3.1-pro",
  "messages": [
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "What is in this picture?" },
        { "type": "image_url", "image_url": { "url": "https://example.com/boat.png" } }
      ]
    }
  ]
}

An http(s) image URL is downloaded by the gateway and inlined as a data: URL before a provider is chosen, so a model that only accepts inline images still works. Up to four images are fetched concurrently within a 20-second budget; exceeding it is 400 image_fetch_timeout. data: URLs are passed through as-is and are always the faster path.

Only http, https and data: schemes are accepted, and the fetch is guarded against private and link-local addresses on every redirect.

Permalink to errorsErrors

StatuscodeMeaning
400empty_body / invalid_request_bodybody missing or not a JSON object
400missing_model / missing_messagesboth are required
400invalid_message_contenta content value could not be decoded
400invalid_media_url / unsupported_media_url_schemean image_url.url failed policy
400image_fetch_timeout / image_fetch_faileda referenced image could not be retrieved
413request_too_largeover 32 MiB
404model_not_found / no_capable_providerthe model is not routable for chat under your key
502empty_response / empty_streamthe upstream returned nothing
502stream_brokenthe stream ended early — delivered as an SSE frame
503no_available_providerevery candidate provider is cooling down
504request_timeout

Upstream failures are classified rather than passed through raw: upstream_error (502), upstream_rate_limited (429), upstream_quota_exhausted (402), upstream_unauthorized (401) and upstream_rejected_request (the vendor's own 4xx, message forwarded after redaction). Provider names, base URLs, upstream model names and credentials are stripped from anything we return.

See Errors and limits for the shared codes.