AI Services Gateway API
A secure gateway in front of our hosted language models. It speaks the OpenAI and Ollama protocols with the same credentials and routing, so existing SDKs, tools and apps work as they are: change the base URL and the API key, nothing else.
https://api.adelk.sa/llm/v1
OpenAI Compatible
Drop-in for the OpenAI SDKs and any tool with an "OpenAI-compatible" provider.
Ollama Native
The native Ollama API for Ollama clients, Open WebUI and code editors.
Quick start#
curl https://api.adelk.sa/llm/v1/models -H "Authorization: Bearer YOUR_API_KEY"model:
curl https://api.adelk.sa/llm/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "team-chat", "messages": [{"role": "user", "content": "Hello!"}]}'model, or with a name the
gateway does not know (e.g. gpt-4o-mini), is answered by your key's
default model, and the model field of every reply names the model
that answered.
Authentication#
Every request carries your key as a Bearer token:
Authorization: Bearer YOUR_API_KEY- A key is granted one or more clusters; their models are the models you can use. Nothing else is listed or reachable.
- Ollama clients take the key in their connection settings (as a Bearer token / API key).
- The
x-api-keyandapi-keyheaders are not accepted — useAuthorization. - Repeated invalid keys from one address are answered with
429 too_many_auth_failuresfor the rest of the minute. A valid key is never blocked.
Models#
curl https://api.adelk.sa/llm/v1/models -H "Authorization: Bearer YOUR_API_KEY"{
"object": "list",
"data": [
{ "id": "team-chat", "object": "model", "created": 1791309574, "owned_by": "gateway" },
{ "id": "team-embed", "object": "model", "created": 1791309574, "owned_by": "gateway" }
]
}Model names
- Models are published under public names chosen by the administrator. Send
the name exactly as listed; letter case does not matter and
:latestis optional. - Only public names are accepted. Any other name is treated as unknown: it goes to the default
model, or
404 model_not_foundif the administrator disabled that fallback./models/{id}and/api/showalways answer 404 for unknown names. - Chat models work with chat / responses / completions / generate; embedding models with the embeddings endpoints.
What the gateway tells you about a model
- Capabilities (
completion,tools,vision,thinking,insert,embedding) and the context length — fromPOST /api/show. - The model's underlying identity (family, size, quantization, license, weights digest) is not disclosed; those fields are empty. Error messages from the model also use the public name.
Context window and output limit
- Each model runs with a fixed context window set by the administrator, so it
stays loaded and answers fast.
num_ctxfrom a client is ignored. - A prompt that does not fit is rejected with
400 context_length_exceeded— never silently truncated. Coding tools use this code to compact their context. max_tokensis capped by the model's output limit, which is also the default when you omit it.
All endpoints#
All paths are relative to the base URL https://api.adelk.sa/llm/v1. For the OpenAI SDKs set it as
base_url; for Ollama clients set it as the Ollama host.
| Method | Path | Protocol | Purpose |
|---|---|---|---|
| POST | /chat/completions |
OpenAI | Chat — streaming, tools, vision, JSON / json_schema, reasoning |
| POST | /responses |
OpenAI | Responses API (stateless — send the full conversation). Codex CLI, agent frameworks |
| POST | /completions |
OpenAI | Legacy text completions, fill-in-the-middle (suffix), echo |
| POST | /embeddings |
OpenAI | Embeddings (float or base64) |
| GET | /models |
OpenAI | The models your key can use |
| GET | /models/{id} |
OpenAI | One of your models (anything else → 404) |
| POST | /api/chat |
Ollama | Chat — streams by default |
| POST | /api/generate |
Ollama | Generate, FIM (suffix), raw; an empty prompt is a load request |
| POST | /api/embed |
Ollama | Embeddings (current API) |
| POST | /api/embeddings |
Ollama | Embeddings (legacy API, one prompt) |
| GET | /api/tags |
Ollama | The models your key can use |
| POST | /api/show |
Ollama | Capabilities and context length of one model |
| GET | /api/ps |
Ollama | Which of your models are loaded now |
| GET | /api/version |
Ollama | Connection check |
| GET | / |
Ollama | Connection check — Adelk AI Gateway is running |
Every endpoint answers CORS preflight requests, so browser apps
can call it directly. The model listings and details (/models,
/api/tags, /api/show, /api/ps) do not count against your
rate limit or quota.
Chat#
The same conversation body works on both. OpenAI returns a
chat.completion (SSE when streaming); Ollama returns its native object (NDJSON
when streaming, which is its default).
{
"model": "team-chat",
"messages": [
{ "role": "system", "content": "You are concise." },
{ "role": "user", "content": "Capital of Saudi Arabia?" }
],
"temperature": 0.7,
"max_tokens": 1024
}{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1791309574,
"model": "team-chat",
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "Riyadh." },
"finish_reason": "stop"
}],
"usage": { "prompt_tokens": 20, "completion_tokens": 3, "total_tokens": 23 }
}{
"model": "team-chat",
"created_at": "2026-10-07T10:00:00Z",
"message": { "role": "assistant", "content": "Riyadh." },
"done": true,
"done_reason": "stop",
"prompt_eval_count": 20,
"eval_count": 3,
"total_duration": 123456789
}finish_reasonislengthwhenmax_tokenswas reached andtool_callswhen the model called a tool.- Reasoning models return their thinking in
message.reasoning_content(OpenAI) ormessage.thinking(Ollama). /chat/completionswith an emptymessageslist is a400. On/api/chatit is a load request — see Ollama native.
Responses#
The OpenAI Responses API, used by Codex CLI and agent frameworks. The gateway is
stateless: send the whole conversation in input on every turn.
{
"model": "team-chat",
"instructions": "You are a helpful assistant.",
"input": [
{ "role": "user", "content": "What is 2 + 2?" }
],
"max_output_tokens": 512
}{
"id": "resp_abc",
"object": "response",
"status": "completed",
"model": "team-chat",
"output": [{
"type": "message", "role": "assistant",
"content": [{ "type": "output_text", "text": "4" }]
}],
"usage": { "input_tokens": 18, "output_tokens": 1, "total_tokens": 19 },
"store": false
}| Field | Behaviour |
|---|---|
instructions | The first system message. |
input | A string, or items: messages (input_text,
input_image as a data: URI), function_call,
function_call_output. |
tools, tool_choice | function tools
are supported. Built-in tools (web_search, file_search, computer_use, mcp…) are
dropped — the gateway does not run them. |
text.format | json_object or
json_schema (structured outputs). |
reasoning.effort | Reasoning level; none turns it off.
Thinking is returned as a reasoning output item. |
max_output_tokens | When reached: status: "incomplete",
incomplete_details.reason: "max_output_tokens". |
store | Accepted; always returned as false. |
previous_response_id, conversation, background |
400 unsupported_parameter — send the full conversation instead. |
With "stream": true the reply is named SSE events
(response.created → response.output_text.delta… →
response.completed), without [DONE]. See Streaming.
Completions & fill-in-the-middle#
Raw text completion without a chat template, and FIM (code
completion in editors) when a suffix is sent. FIM needs a model with the
insert capability; others answer 400 "does not support insert".
{
"model": "team-coder",
"prompt": "def add(a, b):\n ",
"suffix": "\n\nprint(add(1, 2))",
"max_tokens": 64
}{
"id": "cmpl-abc",
"object": "text_completion",
"model": "team-coder",
"choices": [{
"text": "return a + b",
"index": 0, "logprobs": null, "finish_reason": "stop"
}],
"usage": { "prompt_tokens": 15, "completion_tokens": 5, "total_tokens": 20 }
}/completions:echo: trueprepends the prompt. Withoutmax_tokensthe model's output limit applies (not 16). Several prompts, token ids,n/best_of> 1,logprobsandlogit_biasare rejected with400./api/generate:prompt+ optionalsystem/imagesworks as a one-turn chat;raw: truesends the text verbatim;formatrequests JSON.template,contextandkeep_aliveare ignored.
Embeddings#
| POST /embeddings | POST /api/embed | POST /api/embeddings | |
|---|---|---|---|
| Input | input: string or array (≤ 2048) | input: string or array | prompt: one string |
| Output | {data:[{embedding,index}], usage} | {embeddings:[[…]]} | {embedding:[…]} |
| Options | encoding_format (float | base64), dimensions | truncate, dimensions | — |
| Too long | 400 context_length_exceeded | Truncated, unless truncate: false | Truncated |
{
"model": "team-embed",
"input": ["first text", "second text"]
}{
"object": "list",
"data": [
{ "object": "embedding", "index": 0, "embedding": [0.012, -0.034, …] },
{ "object": "embedding", "index": 1, "embedding": [0.021, 0.008, …] }
],
"model": "team-embed",
"usage": { "prompt_tokens": 6, "total_tokens": 6 }
}Only input tokens are metered. Token-id arrays and empty strings
are rejected. Use an embedding model from your list; with none, the reply is
403 model_not_available.
Ollama native#
Besides chat, generate and embeddings above, Ollama clients use these endpoints to discover models and check the connection.
GET /api/tags
{
"models": [{
"name": "team-chat",
"model": "team-chat",
"modified_at": "2026-10-01T09:00:00+03:00",
"size": 0,
"digest": "5b1e…",
"details": { "parent_model": "", "format": "", "family": "", "families": null, "parameter_size": "", "quantization_level": "" }
}]
}POST /api/show
{ "model": "team-chat" }{
"license": "",
"modelfile": "",
"parameters": "num_ctx 32768",
"template": "",
"details": { "family": "", "parameter_size": "", … },
"model_info": {
"general.architecture": "gateway",
"gateway.context_length": 32768
},
"capabilities": ["completion", "tools", "vision"],
"modified_at": "2026-10-01T09:00:00+03:00"
}gateway.context_length is the context window the
model really runs with. A missing model shows the default chat model.
GET /api/ps · GET /api/version · GET /
/api/ps lists which of your models are loaded now, with expires_at.
/api/version and / ("Adelk AI Gateway is running") are connection checks.
Load requests
Ollama clients "warm up" a model with an empty /api/generate prompt or an empty
/api/chat messages list. The gateway answers these itself, after checking
the key and the model — nothing is sent to the servers, and nothing is metered. Models are kept
loaded by the administrator; a cold model loads with your first real request.
{ "model": "team-chat", "prompt": "" }{ "model": "team-chat", "created_at": "2026-10-07T10:00:00Z", "response": "", "done": true, "done_reason": "load" }Parameters#
| Parameter | Type | Description |
|---|---|---|
model | string | A public name from /models.
Missing or unknown → your key's default model. |
messages required | array | Roles system / developer / user /
assistant / tool. Content is a string or an array of
text / image_url parts. |
stream | boolean | Default false on OpenAI
endpoints and true on Ollama endpoints, as on the originals. |
stream_options | object | OpenAI only:
{"include_usage": true} adds a final usage chunk. |
max_tokens / max_completion_tokens | number | Output tokens (Ollama: options.num_predict). Capped by the model's
output limit, which is also the default. Reasoning models spend part of it on
thinking — leave room. |
temperature, top_p, top_k, min_p,
presence_penalty, frequency_penalty,
repeat_penalty, seed, stop | number / string[] | Sampling controls. Ollama clients may also send them, and tfs_z,
typical_p, mirostat*, num_keep,
repeat_last_n, inside options. Values are type-checked
(400 invalid_type). |
reasoning_effort / think | string / bool | Reasoning level (minimal/none turn it off). See
capabilities. |
response_format / format | object | JSON mode or structured output. |
tools, tool_choice | array / string | Function calling (OpenAI tool schema). |
num_ctx (context window) and keep_alive (how long a model stays
loaded). user and parallel_tool_calls are ignored too.
Tools, vision, JSON, reasoning#
Each feature uses the protocol's own parameters. The model's capabilities decide:
if the model does not support a feature, its error comes back as 400
upstream_invalid_request instead of being silently ignored. Check
capabilities in /api/show.
| Feature | OpenAI | Ollama |
|---|---|---|
| Tools | tools, tool_choice ("none" sends no tools); replies carry tool_calls | tools |
| Vision | image_url with a data:image/…;base64,… URI | images: ["<base64>"] |
| JSON | response_format: {"type":"json_object"} or {"type":"json_schema","json_schema":{"name":…,"schema":{…}}} | format: "json" or a JSON Schema |
| Reasoning | reasoning_effort → message.reasoning_content | think: true | false | "high" → message.thinking |
{
"model": "team-chat",
"messages": [{ "role": "user", "content": "What is the weather in Riyadh?" }],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}],
"tool_choice": "auto"
}{
"model": "team-chat",
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "What is in this image?" },
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KGgo..." } }
]
}]
}{
"model": "team-chat",
"messages": [
{ "role": "user", "content": "What is in this image?", "images": ["iVBORw0KGgo..."] }
],
"stream": false
}{
"model": "team-chat",
"messages": [{ "role": "user", "content": "Capital of Saudi Arabia?" }],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "answer",
"schema": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] }
}
}
}Images must be inline base64. Remote image URLs are not
fetched and are rejected with 400 invalid_image_url.
Streaming#
Set "stream": true (Ollama endpoints stream by default). Tokens are delivered as
they are generated, in each protocol's own format.
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":9,"completion_tokens":3,"total_tokens":12}}
data: [DONE]The usage chunk is sent when
stream_options.include_usage is true.
{"model":"team-chat","message":{"role":"assistant","content":"Hello"},"done":false}
{"model":"team-chat","message":{"role":"assistant","content":" there"},"done":false}
{"model":"team-chat","message":{"role":"assistant","content":""},"done":true,"done_reason":"stop","prompt_eval_count":9,"eval_count":3}event: response.created
data: {"type":"response.created","sequence_number":0,"response":{…}}
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence_number":4,"delta":"Hello"}
event: response.completed
data: {"type":"response.completed","sequence_number":9,"response":{…}}{"error":…} line (Ollama), a data: {"error":{…}} event (OpenAI) or
response.failed (Responses).
If you put the gateway behind your own proxy, turn off response buffering and compression for these paths, or tokens arrive in bursts.
Integrations#
/v1: clients append /models,
/chat/completions or /api/… themselves. A full endpoint path as the
base URL gives 404 (often shown as "Network Problem").
# OpenAI protocol
curl https://api.adelk.sa/llm/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "team-chat", "messages": [{"role": "user", "content": "Explain DNS in one sentence."}]}'
# Ollama protocol
curl https://api.adelk.sa/llm/v1/api/chat \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"model": "team-chat", "messages": [{"role": "user", "content": "Why is the sky blue?"}], "stream": false}'from openai import OpenAI
client = OpenAI(base_url="https://api.adelk.sa/llm/v1", api_key="YOUR_API_KEY")
print([m.id for m in client.models.list()]) # the models your key can use
resp = client.chat.completions.create(
model="team-chat",
messages=[{"role": "user", "content": "Write a haiku about code."}],
)
print(resp.choices[0].message.content)
# Streaming
for chunk in client.chat.completions.create(
model="team-chat",
messages=[{"role": "user", "content": "Count to five."}],
stream=True,
):
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="", flush=True)
# Embeddings
vec = client.embeddings.create(model="team-embed", input="hello").data[0].embeddingimport OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.adelk.sa/llm/v1", apiKey: process.env.GATEWAY_API_KEY });
const resp = await client.chat.completions.create({
model: "team-chat",
messages: [{ role: "user", content: "Tell me a joke." }],
});
console.log(resp.choices[0].message.content);
// Responses API
const r = await client.responses.create({ model: "team-chat", input: "Say hi" });
console.log(r.output_text);from ollama import Client
client = Client(host="https://api.adelk.sa/llm/v1", headers={"Authorization": "Bearer YOUR_API_KEY"})
print([m.model for m in client.list().models])
resp = client.chat(model="team-chat", messages=[{"role": "user", "content": "Hello!"}])
print(resp.message.content)const res = await fetch("https://api.adelk.sa/llm/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_KEY",
},
body: JSON.stringify({
model: "team-chat",
messages: [{ role: "user", content: "Tell me a joke." }],
}),
});
const data = await res.json();
console.log(data.choices[0].message.content);import requests
resp = requests.post(
"https://api.adelk.sa/llm/v1/chat/completions",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"model": "team-chat", "messages": [{"role": "user", "content": "Hello!"}]},
timeout=300,
)
resp.raise_for_status()
print(resp.json()["choices"][0]["message"]["content"])Tools and apps
Open WebUI
Recommended: Settings → Connections → OpenAI API, URL
https://api.adelk.sa/llm/v1, your key. Or the Ollama connection with the same URL
and key.
Editors (Continue and similar)
Provider ollama, apiBase =
https://api.adelk.sa/llm/v1, the key as apiKey. For autocomplete use a
model with the insert capability (FIM).
# ~/.codex/config.toml — Codex CLI over the Responses API
model = "team-coder" # a name from GET /v1/models, strong at tool calling
model_provider = "gateway"
[model_providers.gateway]
name = "AI Gateway"
base_url = "https://api.adelk.sa/llm/v1"
env_key = "GATEWAY_API_KEY" # environment variable holding your key
wire_api = "responses"Codex sends a large context: use a model with a large context window, and ask your administrator to turn off prompt logging for coding keys.
Policies & limits#
Set by the administrator per key and enforced on every request. They are not part of the request body, but they shape the replies you get.
| Policy | Effect |
|---|---|
| Clusters | The key can use the models of its clusters only. |
| Model settings | Fixed context window and output limit per model. |
| Rate limit | Requests per minute. Exceeding it → 429. |
| Token quotas | Daily / weekly / monthly token caps, per key or per owning
user. Exceeding them → 429 Token quota exceeded (…). There is no
endpoint to query the remaining quota yet; ask your administrator. |
| IP & domain allow-lists | Other addresses or referrer domains →
403. |
| Expiry / owner | An expired key, or one whose owner account is inactive →
403. |
| System prompt | The administrator may set a system prompt on the servers
of a model: either placed before yours (both reach the model as one system
prompt), or used instead of yours (your system / developer
messages, instructions and /api/generate
system are then ignored). It counts against the context window. Raw and
fill-in-the-middle completions (/v1/completions, raw: true,
suffix) cannot carry it: depending on the administrator's choice they
are served without it, or refused with 400 raw_not_allowed. |
| Prompt logging | Prompt text may be stored for audit; the administrator can turn this off per key (e.g. for source code). Usage is always metered. |
Long requests are bounded by a gateway-wide time limit (including retries on other servers); set your client timeout generously (minutes, not seconds) for long generations.
Errors#
Errors are JSON in each protocol's own shape:
{"error": {"message": "…", "type": "invalid_request_error", "param": "model", "code": "model_not_found"}}{"error": "model 'x' not found"}| Status | Code | Meaning |
|---|---|---|
| 400 | context_length_exceeded | The prompt is larger than the model's context window. |
| 400 | upstream_invalid_request | The model rejected the request (e.g. no tools, images or suffix support). The message names the model by its public name. |
| 400 | unsupported_by_model | The servers of this model do not support the operation at all (e.g. raw completion or FIM on some cloud providers). |
| 400 | invalid_type, invalid_value, invalid_image_url, unsupported_parameter, unsupported_value | A malformed or unsupported parameter; param names it. |
| 400 | raw_not_allowed | A raw (raw: true, /v1/completions) or fill-in-the-middle (suffix) completion on a model whose servers enforce a system prompt; param is raw or suffix. Use a chat endpoint, or a key whose cluster allows raw / fill-in-the-middle completions. |
| 400 | model_required | No model sent, and the key has several models but no default. |
| 400 | — / invalid_request_error | Invalid JSON or malformed messages; an empty messages list on /chat/completions. |
| 401 | — | Missing or invalid API key. |
| 403 | model_not_available, key_owner_inactive | No model of this kind for the key; key expired; IP or domain not allowed; owner account inactive. |
| 404 | model_not_found | Unknown model (when the default fallback is off), or /models/{id} / /api/show for a name the key cannot use. |
| 405 | — | Wrong HTTP method. |
| 413 | request_too_large | The request body is too large. |
| 429 | — | Your rate limit or token quota is exceeded. |
| 429 | upstream_rate_limited | Every server of the model is rate-limited by its provider. Retry after the Retry-After header. |
| 429 | too_many_auth_failures | Too many invalid keys from your address; retry after Retry-After. |
| 502 | — | A server returned an invalid response. |
| 503 | — | No server can serve the model right now. Retry later. |
Not available#
| Feature | Why / alternative |
|---|---|
/api/pull, /api/push, /api/create, /api/copy, /api/delete |
Clients cannot change what is installed or loaded on the servers. Models are managed by the administrator. |
Loading / unloading via keep_alive | Ignored; load requests are answered without touching the servers. |
Anthropic /v1/messages | Not offered to clients; use the OpenAI or Ollama protocol. |
Stored responses (previous_response_id, GET /responses/{id}) | The gateway is stateless; send the full conversation. |
n > 1, logprobs, logit_bias | 400. |
| Remote image URLs | Send images inline as base64. |
x-api-key / api-key headers | Use Authorization: Bearer. |
Postman#
A ready collection covers every endpoint: chat (plain, streaming, vision, tools, JSON, reasoning), Responses, completions and FIM, embeddings, the Ollama native endpoints and load requests, plus examples of common errors.
- Import both files into Postman.
- Select the AI Gateway environment (top right).
- Set
base_url(e.g.https://api.adelk.sa/llm, without/v1) andapi_key. - Run List models: when
modelis empty it is set to the first listed model (make sure it is a chat model). Setembed_modelto an embedding model for the embeddings folder.