Public API
The external contract — the three V1 endpoints, how models are named, what passes through, and where OpenAI compatibility ends.
The Salyro Public API is OpenAI-compatible. It exposes three endpoints under one base URL, and a request to any of them is passed through to the provider that owns the model you named, using the credentials on the gateway your key belongs to.
Compatibility here means the wire format, not a copy of another company's product. Salyro defines what it accepts, states which capability each model supports, and refuses anything it cannot honour rather than quietly dropping it. The rest of this page is that contract.
Base URL
https://api.salyro.com/v1
Every endpoint below is written as its full path — /v1/chat/completions — so
an SDK configured with the base URL above and a client building URLs by hand
arrive at the same address.
/v1 is the version boundary. New fields and new capabilities are added inside
it; anything that would break an existing caller goes to a future /v2 instead.
Authentication
Every request carries a Salyro API key as a bearer token:
Authorization: Bearer sk-sly-...
The key identifies the gateway, so the gateway never appears in the URL, in a query parameter or in a second header. Authentication covers where the key comes from, how to store it, and what a rejected request looks like.
Model identifiers
A canonical model id is the provider and the provider's own name for the model, joined by a slash:
<provider>/<native-model-id>
The provider half is one of four fixed ids, and they are the same ids the dashboard stores against a credential:
| Provider id | Provider |
|---|---|
openai | OpenAI |
anthropic | Anthropic |
google | Google Gemini |
xai | Grok (xAI) |
The second half is untouched — whatever the provider calls the model is what you
write. So openai/gpt-4o-mini, anthropic/claude-sonnet-4-5-20250929,
google/gemini-2.5-flash, xai/grok-4.3.
The bare OpenAI alias
A model id with no provider prefix — gpt-4o-mini rather than
openai/gpt-4o-mini — resolves to openai/. It exists for one reason: an
existing OpenAI integration should be able to change its base URL and its key
and keep working, without editing every model string in the codebase.
It is an OpenAI alias and nothing more. There is no default provider, and a bare name is never guessed at against Anthropic, Gemini or Grok. Once you are past the migration, prefer the canonical form — it says which provider account the request will spend against, which the bare form does not.
The three endpoints
| Endpoint | What it is |
|---|---|
POST /v1/chat/completions | The compatibility surface. The broadest client support. |
POST /v1/responses | The modern surface, as a stateless subset in V1. |
GET /v1/models | What this gateway can serve right now. |
Chat Completions and Responses are two separate wire contracts, not two names for one thing. A Chat Completions stream carries Chat Completions chunks and a Responses stream carries Responses events; neither is ever translated into the other's shape on the way out.
GET /v1/models
Lists the models available to the gateway the key belongs to — the ones that are active and whose provider has an active credential connected. A provider you have not connected contributes nothing to this list.
curl https://api.salyro.com/v1/models \
-H "Authorization: Bearer $SALYRO_API_KEY"
The response is the OpenAI list shape:
{
"object": "list",
"data": [
{ "id": "openai/gpt-4o-mini", "object": "model", "owned_by": "openai" },
{ "id": "anthropic/claude-sonnet-4-5-20250929", "object": "model", "owned_by": "anthropic" }
]
}
This is the endpoint to call first, and the one to call after connecting a credential. It is also the honest answer to "which models can I use" — see Switch providers without changing your code.
POST /v1/chat/completions
The OpenAI Chat Completions contract. If your code already speaks to an OpenAI-compatible client, this is the endpoint it is already calling.
Request
| Field | Required | What it does |
|---|---|---|
model | Yes | A canonical model id, or a bare OpenAI id. |
messages | Yes | The conversation so far, oldest first. |
stream | No | true to receive the answer as server-sent events. |
tools | No | Function definitions the model may call. You execute them. |
tool_choice | No | Whether and which tool the model must use. |
response_format | No | Constrains the output to JSON, optionally to a schema. |
Other Chat Completions parameters are accepted where the selected model supports them, and are passed to the provider unchanged.
Sending a request
curl https://api.salyro.com/v1/chat/completions \
-H "Authorization: Bearer $SALYRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-4o-mini",
"messages": [
{ "role": "user", "content": "In one sentence, what is an AI gateway?" }
]
}'
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.SALYRO_API_KEY,
baseURL: 'https://api.salyro.com/v1',
});
const completion = await client.chat.completions.create({
model: 'openai/gpt-4o-mini',
messages: [{ role: 'user', content: 'In one sentence, what is an AI gateway?' }],
});
console.log(completion.choices[0].message.content);
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["SALYRO_API_KEY"],
base_url="https://api.salyro.com/v1",
)
completion = client.chat.completions.create(
model="openai/gpt-4o-mini",
messages=[{"role": "user", "content": "In one sentence, what is an AI gateway?"}],
)
print(completion.choices[0].message.content)
Response
{
"id": "chatcmpl-...",
"object": "chat.completion",
"created": 1787600000,
"model": "openai/gpt-4o-mini",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "An AI gateway is a single API in front of several model providers."
},
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 18, "completion_tokens": 16, "total_tokens": 34 }
}
model echoes the canonical id the request resolved to, which is how a bare
OpenAI alias tells you which provider actually served it. The usage counts are
the same ones the gateway prices — see Usage & costs.
Streaming
Set stream: true and the answer arrives as server-sent events: a sequence of
chat.completion.chunk objects, each carrying a delta, terminated by a
literal [DONE].
data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"An"}}]}
data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":" AI"}}]}
data: [DONE]
Stream responses covers consuming the stream, finishing cleanly, and what happens when the client disconnects halfway.
Tool calling
Tool calling is pass-through. You send tool definitions, the model may answer
with tool_calls instead of prose, and you run the tool and send the result back
as another message.
{
"model": "openai/gpt-4o-mini",
"messages": [{ "role": "user", "content": "What is the weather in Tel Aviv?" }],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city.",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}
]
}
The model answers with the call it wants made:
{
"role": "assistant",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Tel Aviv\"}" }
}
]
}
You execute get_weather yourself, append a message with "role": "tool", the
matching tool_call_id and the result, and send the conversation again.
Structured output
response_format constrains the answer to JSON, optionally against a schema you
supply:
{
"model": "openai/gpt-4o-mini",
"messages": [{ "role": "user", "content": "Extract the invoice total." }],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "invoice",
"strict": true,
"schema": {
"type": "object",
"properties": { "total": { "type": "number" }, "currency": { "type": "string" } },
"required": ["total", "currency"],
"additionalProperties": false
}
}
}
}
It passes through faithfully to models that support it. On a model that does not, the request fails with an explicit error rather than returning prose that happens not to parse — see Get structured JSON output.
Vision input
An image is a content part inside a user message, in the OpenAI shape:
{
"model": "openai/gpt-4o-mini",
"messages": [
{
"role": "user",
"content": [
{ "type": "text", "text": "What does this label say?" },
{ "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,..." } }
]
}
]
}
A public URL works in the same field. Image input is supported; image generation is not. Send images with vision covers what happens to the image and how it affects token counts.
POST /v1/responses
The Responses contract, which models a turn as an input and an output rather
than as a list of chat messages.
curl https://api.salyro.com/v1/responses \
-H "Authorization: Bearer $SALYRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-4o-mini",
"input": "In one sentence, what is an AI gateway?"
}'
{
"id": "resp_...",
"object": "response",
"status": "completed",
"model": "openai/gpt-4o-mini",
"output": [
{
"type": "message",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "An AI gateway is a single API in front of several model providers."
}
]
}
],
"usage": { "input_tokens": 18, "output_tokens": 16, "total_tokens": 34 }
}
Responses is stateless in V1
This is the part that differs most from what you may have read elsewhere, and it is a deliberate boundary rather than a gap that will be patched quietly.
Salyro keeps no server-side conversation state. Every request carries its whole input, and the response it produces is returned once and not retained for you to fetch later. Concretely, none of the following exist in V1:
previous_response_idand conversation objects — continuing a conversation means sending the earlier turns again- retrieving or deleting a stored response by its id
- background mode
- hosted tools of any kind, and the Files API they depend on
store: false is accepted, because it describes what already happens. A request
that asks for state or for a hosted capability fails explicitly; it is never
reinterpreted as something Salyro can do instead.
Streaming on Responses
stream: true works here too, and produces the Responses event stream — typed
events describing the response being built, rather than Chat Completions chunks.
The two formats are not interchangeable. A parser written for
chat.completion.chunk will not read a Responses stream, and Salyro will not
bridge one into the other's shape to make it fit. Use your SDK's Responses
streaming helper, or handle the events directly.
Headers
| Header | Direction | What it is |
|---|---|---|
Authorization | Request | Bearer <SALYRO_API_KEY>. Required. |
Content-Type | Request | application/json on both POST endpoints. |
X-Client-Request-Id | Request | Optional. Your own id, for correlating with your logs. |
x-request-id | Response | Salyro's id for this request. On every response. |
x-request-id is worth capturing on every call, successful or not. It is the
value that ties an error your code received to the record of the request that
produced it — see Debug a failed request.
Errors
A failure is returned with a meaningful HTTP status and an OpenAI-shaped body:
{
"error": {
"message": "Incorrect API key provided.",
"type": "authentication_error",
"code": "invalid_api_key",
"param": null
}
}
typeis the family the failure belongs to.codeis a stable Salyro identifier for the specific condition. Branch on this rather than onmessage, which is written for a person and may be reworded.paramnames the offending field when one failure is about one field.
The status is the first thing to read, because it decides what to do next:
| Status | Meaning | Retry? |
|---|---|---|
400 | The request is wrong as written | No — change the request |
401 | The key was missing, wrong or revoked | No — fix the key |
429 | The key's rate limit was exceeded | Yes — back off, then retry |
5xx | Salyro or the provider failed | Usually, with backoff |
A model this gateway cannot serve is rejected rather than guessed at: there is
no default provider and no substitution. GET /v1/models is what the id has to
appear in.
Errors groups these by what you should do about them, which is the more useful grouping while you are writing the code around a call.
Compatibility, and where it ends
Compatibility is a product decision with a defined scope, and this is that scope. The following are not part of the Public API in V1, and a request for one of them fails rather than being approximated:
- embeddings, image generation, audio and realtime
- the batch API, the Files API and assistants
- hosted tools, agents and workflow orchestration
- any provider-native endpoint — there is no Anthropic-shaped
/v1/messagesand no proprietary Salyro endpoint alongside the three above
Routing is pass-through only. Salyro does not retry your request against a different provider, does not balance load between providers and does not choose a model for you. Which provider serves a request is decided by the model id you send, and by nothing else.
Using an OpenAI SDK
There is no Salyro SDK, and that is the point rather than an omission. Any client that can speak to an OpenAI-compatible endpoint already speaks to Salyro; three settings change and the rest of your code does not:
const client = new OpenAI({
baseURL: 'https://api.salyro.com/v1', // was https://api.openai.com/v1
apiKey: process.env.SALYRO_API_KEY, // was OPENAI_API_KEY
});
// and the model becomes a canonical id
await client.chat.completions.create({
model: 'anthropic/claude-sonnet-4-5-20250929',
messages: [{ role: 'user', content: 'Hello' }],
});
client = OpenAI(
base_url="https://api.salyro.com/v1", # was https://api.openai.com/v1
api_key=os.environ["SALYRO_API_KEY"], # was OPENAI_API_KEY
)
# and the model becomes a canonical id
client.chat.completions.create(
model="anthropic/claude-sonnet-4-5-20250929",
messages=[{"role": "user", "content": "Hello"}],
)
Migrate an existing OpenAI integration walks through the whole change, including what you gain once the traffic runs through a gateway.
