Skip to content
Salyro
Reference

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

Text
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:

HTTP
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:

Text
<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 idProvider
openaiOpenAI
anthropicAnthropic
googleGoogle Gemini
xaiGrok (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

EndpointWhat it is
POST /v1/chat/completionsThe compatibility surface. The broadest client support.
POST /v1/responsesThe modern surface, as a stateless subset in V1.
GET /v1/modelsWhat 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.

Shell
curl https://api.salyro.com/v1/models \
  -H "Authorization: Bearer $SALYRO_API_KEY"

The response is the OpenAI list shape:

JSON
{
  "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

FieldRequiredWhat it does
modelYesA canonical model id, or a bare OpenAI id.
messagesYesThe conversation so far, oldest first.
streamNotrue to receive the answer as server-sent events.
toolsNoFunction definitions the model may call. You execute them.
tool_choiceNoWhether and which tool the model must use.
response_formatNoConstrains 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

Shell
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?" }
    ]
  }'

Response

JSON
{
  "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].

Text
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.

JSON
{
  "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:

JSON
{
  "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:

JSON
{
  "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:

JSON
{
  "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.

Shell
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?"
  }'
JSON
{
  "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_id and 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

HeaderDirectionWhat it is
AuthorizationRequestBearer <SALYRO_API_KEY>. Required.
Content-TypeRequestapplication/json on both POST endpoints.
X-Client-Request-IdRequestOptional. Your own id, for correlating with your logs.
x-request-idResponseSalyro'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:

JSON
{
  "error": {
    "message": "Incorrect API key provided.",
    "type": "authentication_error",
    "code": "invalid_api_key",
    "param": null
  }
}
  • type is the family the failure belongs to.
  • code is a stable Salyro identifier for the specific condition. Branch on this rather than on message, which is written for a person and may be reworded.
  • param names 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:

StatusMeaningRetry?
400The request is wrong as writtenNo — change the request
401The key was missing, wrong or revokedNo — fix the key
429The key's rate limit was exceededYes — back off, then retry
5xxSalyro or the provider failedUsually, 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/messages and 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:

TypeScript
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' }],
});

Migrate an existing OpenAI integration walks through the whole change, including what you gain once the traffic runs through a gateway.