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.
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
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)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:
| Field | Why we read it |
|---|---|
model | required — chooses the route |
messages | required, non-empty — walked to find image parts |
stream | selects the SSE path |
stream_options | see 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:
- Vendor billing keys are stripped (
credits,cost,cost_usd,balance,billingand their siblings, at the top level and insideusage) so that another vendor's ledger never leaks into yours. Your real cost is in credits, not in that field. - 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-06and 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.
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)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 ?? "");
}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]:
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:
{
"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
| Status | code | Meaning |
|---|---|---|
400 | empty_body / invalid_request_body | body missing or not a JSON object |
400 | missing_model / missing_messages | both are required |
400 | invalid_message_content | a content value could not be decoded |
400 | invalid_media_url / unsupported_media_url_scheme | an image_url.url failed policy |
400 | image_fetch_timeout / image_fetch_failed | a referenced image could not be retrieved |
413 | request_too_large | over 32 MiB |
404 | model_not_found / no_capable_provider | the model is not routable for chat under your key |
502 | empty_response / empty_stream | the upstream returned nothing |
502 | stream_broken | the stream ended early — delivered as an SSE frame |
503 | no_available_provider | every candidate provider is cooling down |
504 | request_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.