Developer docs

DocsEverything an application needs to call the gateway. Admin screens are documented in the portal itself.
Authentication

Every request carries a gateway key as a bearer token. Keys start with rt-, belong to one tenant, and are created in the portal under API Keys. The gateway stores only a hash: a lost key is replaced, not recovered.

Authorization: Bearer rt-xxxxxxxx-................................

Each key has a requests-per-minute and tokens-per-minute limit. Over either, the answer is 429 with a Retry-After header.

Endpoints

Base URL: https://ai.redtone.com/v1. The surface is OpenAI's; the request and response bodies pass through unchanged apart from model, which the gateway rewrites from the alias to the backend's own model name.

POST /v1/chat/completionsChat. Streaming and non-streaming.
POST /v1/completionsLegacy text completion, when the backend supports it.
POST /v1/embeddingsEmbeddings.
GET /v1/modelsThe aliases your key may call, in OpenAI's list shape.
POST /v1/*Any other OpenAI-shaped path with a model field is forwarded to that alias's backend and logged. Metered from the usage the backend reports, or estimated when it reports none.

Every response carries X-Request-Id, X-RT-Backend and X-RT-Location, so a support question can be matched to a placement-log row.

Data classification

Every tenant has a data policy that says where its requests may go: Open Standard Confidential On-prem only. Each backend is approved by REDtone for some of these levels; a request only ever reaches a backend approved for its level, and an on-prem backend is where Confidential and On-prem only traffic goes. Nothing is sent anywhere else.

The API keeps the underlying names public, internal, confidential, restricted. The level of a request is the strictest of: the tenant's policy, the key's policy, the X-Data-Classification header, and any raise by a guardrail. It can never be lowered.

curl https://ai.redtone.com/v1/chat/completions \
  -H "Authorization: Bearer $REDTONE_API_KEY" \
  -H "X-Data-Classification: restricted" \
  -H "Content-Type: application/json" \
  -d '{"model": "redtone/chat-default", "messages": [{"role":"user","content":"..."}]}'

Fail-closed: if no backend for the alias accepts the class, the call is refused with 403 policy_denied and logged as such. Nothing is sent anywhere.

Guardrails: why a request was blocked or kept in Malaysia

Before routing, the gateway reads the text of each request: chat messages, input, prompt and instructions. Your tenant's policy (see Settings) says what happens when it finds personal data or a secret:

Record onlyNothing changes. The finding is logged by type, never by value.
MaskThe value is replaced before it leaves the gateway, e.g. [EMAIL_1], and put back in a non-streamed answer. Streamed answers keep the masked form.
Keep in MalaysiaThe request is treated as restricted and may only go to a backend in Malaysia. If the model has none, the call is refused with 403 policy_denied and the message says why.
BlockRefused with 403 guardrail_blocked. The message names what was found ("a Malaysian IC number"), never the value.

Every answer from a request where something was found carries a header saying what happened:

X-RT-Guardrail: action=raise; raised_to=restricted; entities=MYKAD:1
X-RT-Classification: restricted
X-RT-Location: internal

With mode=monitor in that header, the policy is still being trialled: the finding was recorded and the request went through unchanged. Attached files are checked too: PDF, Word, Excel, PowerPoint and text files are read, and what is in them counts like the prompt (a value inside a file cannot be masked, so a masking rule keeps that request in Malaysia instead). A file the gateway cannot read, such as an image or a password-protected document, is handled by your policy, by default by keeping the request in Malaysia. Answers that are not streamed are checked as well: a withheld value reads [EMAIL withheld], a refused answer is403 guardrail_blocked_output and its tokens are still billed. Attempts to override an assistant's instructions in what a user or a tool sends are recognised in English, Malay and Chinese.

To check text or a file without calling a model (a document before it goes into a search index, say), use the same key:

POST /v1/guardrails/check
{"input": "Customer IC 900101-14-5678", "direction": "prompt"}

→ {"action": "raise", "matches": [{"entity": "MYKAD", "start": 12, "end": 26, …}], …}

If a value is flagged by mistake (an order number that looks like an IC), ask your administrator to mark it “Not PII”; only that exact value is then let through.

Streaming

Set "stream": true. Server-sent events are relayed as they arrive. The gateway asks the backend to include a final usage chunk (stream_options.include_usage) so streamed calls are metered exactly like the rest; you will see it as the last chunk before [DONE].

Errors

OpenAI's error envelope, with a stable code. A backend's own 4xx is passed through with its status and body. What each one means for you, and what to do, is in Troubleshooting.

401missing_api_keyThe request carried no key.
401invalid_api_keyThe key is not one this gateway issued: a typo, or a provider / LiteLLM key.
401invalid_api_key(key_revoked)The key was revoked.
401invalid_api_key(key_expired)The key passed its expiry date.
403tenant_suspendedThe whole tenant is suspended.
402credit_limit_reachedThis month's spend reached REDtone's credit limit for the account.
402budget_exceededThe tenant's own monthly budget is spent and it is set to stop traffic.
429rate_limited(rpm_limited)The key sent more requests this minute than its limit.
429rate_limited(tpm_limited)The key used more tokens this minute than its limit.
415unsupported_media_typeThe body is not JSON (audio uploads are not proxied yet).
413body_too_largeThe request is over 32 MiB.
400invalid_jsonThe body is not a JSON object.
400missing_modelThe model field is missing.
403guardrail_blockedYour data-protection policy blocks something the prompt contains (for example a MyKad number).
404model_not_foundNo model with that name exists for your tenant.
402key_budget_exceededThis API key has spent its own monthly budget.
402department_budget_exceededThe department this key is labelled with has spent its monthly budget.
403model_not_allowedThe model exists, but this tenant or this API key is not permitted to call it.
403policy_deniedNo backend for this model is approved for the request's data policy (or a guardrail finding raised it).
503no_healthy_backendEvery permitted backend for this model is down right now.
502upstream_error(upstream_unavailable)No backend answered: the gateway tried up to two and both failed.
{
  "error": {
    "message": "no backend for alias redtone/chat-default accepts classification restricted",
    "type": "policy_denied",
    "code": "policy_denied"
  }
}
Migrating from LiteLLM

Two settings change. Your code does not. LiteLLM stays where it is, behind the gateway as a backend; the gateway adds the tenant, the key, the classification check and the metering in front of it.

# before — LiteLLM directly
OPENAI_BASE_URL=https://llm.redtone.com/v1
OPENAI_API_KEY=sk-...

# after — through the gateway
OPENAI_BASE_URL=https://ai.redtone.com/v1
OPENAI_API_KEY=rt-...
# model names can stay: an alias may equal the upstream name, e.g. openai/gpt-5
Connecting a backend

A backend is any server that answers the OpenAI API: a LiteLLM proxy, vLLM, Ollama, a vendor gateway. A tenant adds its own in the portal: the address must be https on a public host, and it accepts public and internal data only until a platform admin certifies it for more. On-prem and private-network backends are added by a platform admin and marked as an internal location, which is where confidential and restricted traffic goes.

Code samples
curl
curl https://ai.redtone.com/v1/chat/completions \
  -H "Authorization: Bearer $REDTONE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "redtone/chat-default",
    "messages": [{"role": "user", "content": "Summarise this in one line: ..."}]
  }'
Python · openai
from openai import OpenAI

client = OpenAI(
    base_url="https://ai.redtone.com/v1",
    api_key="rt-...",            # a gateway key, not a provider key
)

stream = client.chat.completions.create(
    model="redtone/chat-default",
    messages=[{"role": "user", "content": "Hello"}],
    stream=True,
)
for chunk in stream:
    print(chunk.choices[0].delta.content or "", end="")
Node · openai
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://ai.redtone.com/v1", apiKey: process.env.REDTONE_API_KEY });

const r = await client.chat.completions.create({
  model: "redtone/chat-default",
  messages: [{ role: "user", content: "Hello" }],
});
console.log(r.choices[0].message.content);
Embeddings
curl https://ai.redtone.com/v1/embeddings \
  -H "Authorization: Bearer $REDTONE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "redtone/embed-default", "input": "The quick brown fox"}'