OpenAI + Ollama · one key · one base URL

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.

Base URL
https://api.adelk.sa/llm/v1
OpenAI Compatible

Drop-in for the OpenAI SDKs and any tool with an "OpenAI-compatible" provider.

/chat/completions · /responses · /completions · /embeddings · /models
Ollama Native

The native Ollama API for Ollama clients, Open WebUI and code editors.

/api/chat · /api/generate · /api/embed · /api/tags · /api/show · /api/ps

Quick start#

1
Get an API key from your administrator. It decides which models you can use and your limits.
2
List your models and pick one:
curl https://api.adelk.sa/llm/v1/models -H "Authorization: Bearer YOUR_API_KEY"
3
Send a request with that name as 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!"}]}'
Migrating from OpenAI or Ollama?
Change only the base URL and the key. A request without 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-key and api-key headers are not accepted — use Authorization.
  • Repeated invalid keys from one address are answered with 429 too_many_auth_failures for the rest of the minute. A valid key is never blocked.

Models#

GET /models
curl https://api.adelk.sa/llm/v1/models -H "Authorization: Bearer YOUR_API_KEY"
Response
{
  "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 :latest is optional.
  • Only public names are accepted. Any other name is treated as unknown: it goes to the default model, or 404 model_not_found if the administrator disabled that fallback. /models/{id} and /api/show always 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 — from POST /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_ctx from 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_tokens is 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.

MethodPathProtocolPurpose
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#

POST /chat/completions OpenAI
POST /api/chat Ollama

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

Request
{
  "model": "team-chat",
  "messages": [
    { "role": "system", "content": "You are concise." },
    { "role": "user", "content": "Capital of Saudi Arabia?" }
  ],
  "temperature": 0.7,
  "max_tokens": 1024
}
OpenAI response
{
  "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 }
}
Ollama response (/api/chat, "stream": false)
{
  "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_reason is length when max_tokens was reached and tool_calls when the model called a tool.
  • Reasoning models return their thinking in message.reasoning_content (OpenAI) or message.thinking (Ollama).
  • /chat/completions with an empty messages list is a 400. On /api/chat it is a load request — see Ollama native.

Responses#

POST /responses OpenAI

The OpenAI Responses API, used by Codex CLI and agent frameworks. The gateway is stateless: send the whole conversation in input on every turn.

Request
{
  "model": "team-chat",
  "instructions": "You are a helpful assistant.",
  "input": [
    { "role": "user", "content": "What is 2 + 2?" }
  ],
  "max_output_tokens": 512
}
Response
{
  "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
}
FieldBehaviour
instructionsThe first system message.
inputA string, or items: messages (input_text, input_image as a data: URI), function_call, function_call_output.
tools, tool_choicefunction tools are supported. Built-in tools (web_search, file_search, computer_use, mcp…) are dropped — the gateway does not run them.
text.formatjson_object or json_schema (structured outputs).
reasoning.effortReasoning level; none turns it off. Thinking is returned as a reasoning output item.
max_output_tokensWhen reached: status: "incomplete", incomplete_details.reason: "max_output_tokens".
storeAccepted; 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#

POST /completions OpenAI
POST /api/generate Ollama

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

POST /completions (FIM)
{
  "model": "team-coder",
  "prompt": "def add(a, b):\n    ",
  "suffix": "\n\nprint(add(1, 2))",
  "max_tokens": 64
}
Response
{
  "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: true prepends the prompt. Without max_tokens the model's output limit applies (not 16). Several prompts, token ids, n/best_of > 1, logprobs and logit_bias are rejected with 400.
  • /api/generate: prompt + optional system / images works as a one-turn chat; raw: true sends the text verbatim; format requests JSON. template, context and keep_alive are ignored.

Embeddings#

POST /embeddingsPOST /api/embedPOST /api/embeddings
Inputinput: string or array (≤ 2048)input: string or arrayprompt: one string
Output{data:[{embedding,index}], usage}{embeddings:[[…]]}{embedding:[…]}
Optionsencoding_format (float | base64), dimensionstruncate, dimensions—
Too long400 context_length_exceededTruncated, unless truncate: falseTruncated
POST /embeddings
{
  "model": "team-embed",
  "input": ["first text", "second text"]
}
Response
{
  "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

Request
{ "model": "team-chat" }
Response
{
  "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.

POST /api/generate
{ "model": "team-chat", "prompt": "" }
Response
{ "model": "team-chat", "created_at": "2026-10-07T10:00:00Z", "response": "", "done": true, "done_reason": "load" }

Parameters#

ParameterTypeDescription
modelstringA public name from /models. Missing or unknown → your key's default model.
messages requiredarray Roles system / developer / user / assistant / tool. Content is a string or an array of text / image_url parts.
streambooleanDefault false on OpenAI endpoints and true on Ollama endpoints, as on the originals.
stream_optionsobjectOpenAI only: {"include_usage": true} adds a final usage chunk.
max_tokens / max_completion_tokensnumber 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, stopnumber / 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 / thinkstring / bool Reasoning level (minimal/none turn it off). See capabilities.
response_format / formatobject JSON mode or structured output.
tools, tool_choicearray / string Function calling (OpenAI tool schema).
Fixed by the administrator — ignored when sent
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.

FeatureOpenAIOllama
Toolstools, tool_choice ("none" sends no tools); replies carry tool_callstools
Visionimage_url with a data:image/…;base64,… URIimages: ["<base64>"]
JSONresponse_format: {"type":"json_object"} or {"type":"json_schema","json_schema":{"name":…,"schema":{…}}}format: "json" or a JSON Schema
Reasoningreasoning_effort → message.reasoning_contentthink: 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.

Content-Type: text/event-stream
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.

Content-Type: application/x-ndjson
{"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}
Content-Type: text/event-stream
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":{…}}
Failures during a stream
Before the first token the gateway can still move your request to another server. Once tokens have been sent it cannot, and ends the stream with the protocol's error form: an {"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#

The base URL must end at /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].embedding
import 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 CLI
# ~/.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.

PolicyEffect
ClustersThe key can use the models of its clusters only.
Model settingsFixed context window and output limit per model.
Rate limitRequests per minute. Exceeding it → 429.
Token quotasDaily / 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-listsOther addresses or referrer domains → 403.
Expiry / ownerAn expired key, or one whose owner account is inactive → 403.
System promptThe 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 loggingPrompt text may be stored for audit; the administrator can turn this off per key (e.g. for source code). Usage is always metered.
Metering
Tokens are counted from the model's own usage figures. If you cancel a request or disconnect mid-answer, the tokens the model already produced are still counted. Model listings and details, and load requests, are free.

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:

OpenAI
{"error": {"message": "…", "type": "invalid_request_error", "param": "model", "code": "model_not_found"}}
Ollama
{"error": "model 'x' not found"}
StatusCodeMeaning
400context_length_exceededThe prompt is larger than the model's context window.
400upstream_invalid_requestThe model rejected the request (e.g. no tools, images or suffix support). The message names the model by its public name.
400unsupported_by_modelThe servers of this model do not support the operation at all (e.g. raw completion or FIM on some cloud providers).
400invalid_type, invalid_value, invalid_image_url, unsupported_parameter, unsupported_valueA malformed or unsupported parameter; param names it.
400raw_not_allowedA 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.
400model_requiredNo model sent, and the key has several models but no default.
400— / invalid_request_errorInvalid JSON or malformed messages; an empty messages list on /chat/completions.
401—Missing or invalid API key.
403model_not_available, key_owner_inactiveNo model of this kind for the key; key expired; IP or domain not allowed; owner account inactive.
404model_not_foundUnknown model (when the default fallback is off), or /models/{id} / /api/show for a name the key cannot use.
405—Wrong HTTP method.
413request_too_largeThe request body is too large.
429—Your rate limit or token quota is exceeded.
429upstream_rate_limitedEvery server of the model is rate-limited by its provider. Retry after the Retry-After header.
429too_many_auth_failuresToo 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#

FeatureWhy / 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_aliveIgnored; load requests are answered without touching the servers.
Anthropic /v1/messagesNot 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_bias400.
Remote image URLsSend images inline as base64.
x-api-key / api-key headersUse 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.

  1. Import both files into Postman.
  2. Select the AI Gateway environment (top right).
  3. Set base_url (e.g. https://api.adelk.sa/llm, without /v1) and api_key.
  4. Run List models: when model is empty it is set to the first listed model (make sure it is a chat model). Set embed_model to an embedding model for the embeddings folder.