The platform serves more than chat models. Some models turn text into vectors, some
score documents against a question, some answer typed questions with probabilities, and
some read text out of images or audio. This article explains the kinds, which apps show
which, and how to call each kind from your own code. It uses the Evren models (EV/…)
throughout, because they cover every kind. Every sample was run against a live
installation on 10 October 2026, and the answers quoted are the ones it gave.
The model kinds
Every entry in GET /v1/models has a type. The type tells you which endpoint answers
the model. A model works only on its own endpoint: a rerank model sent to the chat
endpoint gets a 400 that names the right one.
| Type | What it does | Typical use | Endpoint | Evren models |
|---|---|---|---|---|
llm |
Writes text, follows instructions, calls tools | Assistants, agents, summaries, code | POST /v1/chat/completions |
EV/deepseek-v4-flash, EV/glm-5.3, EV/gemma-4-31b, EV/qwen3-vl-30b (reads images), EV/auto and others |
guard |
Classifies a message as safe or unsafe, with categories | Moderation before or after an LLM call | POST /v1/chat/completions |
EV/qwen3-guard-4b |
embedding |
Turns text into a vector of 4096 numbers | Semantic search, retrieval, clustering, duplicates | POST /v1/embeddings |
EV/qwen3-embedding-8b |
rerank |
Scores how relevant each document is to a query | Ordering search hits before they reach an LLM | POST /v1/rerank |
EV/qwen3-reranker-8b |
decision |
Answers typed questions about a text with probabilities, no prose | Ticket routing, triage, yes/no checks | POST /api/alpha/decisions |
EV/clef (text and images), EV/laya |
ocr |
Returns the text in an image, verbatim | Receipts, scanned forms, screenshots | POST /v1/ocr |
EV/dots-ocr, EV/deepseek-ocr-2 |
transcription |
Turns speech into text | Meeting notes, voice input | POST /v1/audio/transcriptions |
EV/qwen3-asr-1.7b |
Other providers use the same types; a vlm is a chat model on the on-premises pool that
also reads images. Chat models also carry capabilities when the provider reports them:
vision, tools and reasoning. The other kinds carry none.
How people choose a model
| Where | What it offers |
|---|---|
| aimo, the chat window | Chat models only. The other kinds are hidden there, because a chat with them fails. |
| aimo for VS Code 1.1.4 and later | Chat models only, in Select Model and in the default pick. |
| The agent workspace | Chat models only in the agent editor. Its model router can use decision models to pick a model per request. |
| Your own code | Read GET /v1/models with your key and choose by type. Your model profile may narrow the list; you only see what you may call. |
Model ids carry a provider prefix. EV/ means Evren: hosted in Türkiye, run on your own
Evren key, and paid from your own Evren credits. The id without the prefix also works.
curl -s $GW/v1/models -H "Authorization: Bearer $BOSPHORUS_KEY" \
| jq -r '.data[] | select(.id | startswith("EV/")) | "\(.type)\t\(.id)"' | sort
On the day this was written it listed 16 Evren models: two decision, one embedding,
one guard, eight llm, two ocr, one rerank and one transcription.
Part A: your keys, once
Evren models run on each person’s own Evren key. You never send that key in a request:
you keep it on My variables, and the gateway reads it for the person the request
identifies. So your code needs a personal platform key, gpk_…, that identifies you.
The samples call it $BOSPHORUS_KEY. A team or service key names no person, and Evren
models refuse it with a 503.
A1. Save your Evren key
- Create an Evren LLM key in the Evren portal. It starts with
evren_llm_. A key that starts withevren_mdl_is the wrong kind and is refused. - Open My variables in the console, find Evren keys, paste the LLM key and press
Save. Saving accepts Evren’s terms of use on your behalf. The Platform key
(
evren_plt_…) is optional and only shows your credit balance.
A2. Get your personal API key
On the same My variables page, press Create my API key on the My API key card
at the top. The key appears with a Copy button; load it into your shell as
BOSPHORUS_KEY. A copy is kept in your own vault, so Show key gives you the same key
again later, and Replace issues a new one when you need it.
From a terminal instead
If your account cannot open the console, mint the key from a sign-in with your company
account. You need curl and jq; on Windows use WSL or Git Bash. On an installation that
signs in with Microsoft Entra ID:
GW=https://ai.corp.example
CFG=$(curl -s $GW/api/identity/config)
TENANT=$(echo "$CFG" | jq -r .entra.tenantId)
CLIENT=$(echo "$CFG" | jq -r .entra.clientId)
SCOPE=$(echo "$CFG" | jq -r .entra.scope)
DA=https://login.microsoftonline.com/$TENANT/oauth2/v2.0/devicecode
TE=https://login.microsoftonline.com/$TENANT/oauth2/v2.0/token
DC=$(curl -s $DA -d client_id=$CLIENT --data-urlencode "scope=$SCOPE")
echo "$DC" | jq -r .message
Open the address it prints, enter the code and sign in. Then, in the same terminal:
TOKEN=$(curl -s $TE -d grant_type=urn:ietf:params:oauth:grant-type:device_code \
-d client_id=$CLIENT -d device_code=$(echo "$DC" | jq -r .device_code) | jq -r .access_token)
curl -s -X POST "$GW/api/identity/token?client=api" -H "Authorization: Bearer $TOKEN" | jq -r .key.key
The last command prints your key once and keeps no copy. Keep it in a password manager
and load it into your shell as BOSPHORUS_KEY. An installation that signs in with its own OpenID Connect
provider uses the same steps with different addresses; the
API with your own keys article shows both.
Whichever way you got it, check that the key is you and that your Evren key is saved:
export GW=https://ai.corp.example
export BOSPHORUS_KEY=gpk_...
curl -s $GW/api/identity/me -H "Authorization: Bearer $BOSPHORUS_KEY" | jq -r .user.email
curl -s $GW/api/identity/evren-key -H "Authorization: Bearer $BOSPHORUS_KEY" | jq -r .llm.status
The first line prints your address and the second prints active. A null on the first
line means you are using a team key.
A3. Lost the key, or need help?
| Your situation | What to do |
|---|---|
| You have never had one | Press Create my API key on My variables. It takes a few seconds. |
| You had one and lost it | If you created it on My variables, press Show key there. A key from the terminal is shown only when minted: run the terminal steps again and add &rotate=true to the last command, and the old key stops working while a new one is printed. Your other keys, such as the one aimo for VS Code holds, are not touched. |
| It may have leaked | Replace it now: Replace on My variables, or &rotate=true for a terminal key. Anyone holding it can call models as you, on your Evren credits. |
| You only want to try one call | The sign-in token, $TOKEN, works as the Bearer for about an hour, once you have minted a personal key at least once. |
Sign-in is refused, or the check prints null |
Ask your platform administrator. They can check that your account may sign in and that you are not using a team key. They cannot create a personal key for you: it is minted only from your own sign-in, which is what makes it identify you. |
| You need a key for a shared service | Ask the administrator for a team or service key on Teams & Keys. Such a key cannot use EV/ models, because no person stands behind it whose Evren key could be read. |
Part B: the samples
The Python samples use the OpenAI SDK where it has a matching call, and requests for the
three endpoints it does not know: rerank, decision and OCR. Set GW and BOSPHORUS_KEY
first, as in A2.
B1. Chat
Any OpenAI SDK works with only the address and the key changed. Several Evren chat models
reason before they answer and share max_tokens between reasoning and the answer, so a
small max_tokens can return an empty answer. Leave it out or keep it generous.
curl -s $GW/v1/chat/completions -H "Authorization: Bearer $BOSPHORUS_KEY" \
-H 'Content-Type: application/json' \
-d '{"model":"EV/deepseek-v4-flash","messages":[{"role":"user","content":"Merhaba"}]}' \
| jq -r '.choices[0].message.content'
On one run it answered Merhaba! Size nasıl yardımcı olabilirim? The same call from Python, followed by a streamed one:
import os
from openai import OpenAI
client = OpenAI(base_url=os.environ["GW"] + "/v1", api_key=os.environ["BOSPHORUS_KEY"])
reply = client.chat.completions.create(
model="EV/deepseek-v4-flash",
messages=[{"role": "user", "content": "Merhaba"}],
)
print(reply.choices[0].message.content)
for chunk in client.chat.completions.create(
model="EV/glm-5.3", stream=True,
messages=[{"role": "user", "content": "Count to five."}],
):
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
B2. Guard
EV/qwen3-guard-4b does not hold a conversation. Send it the message to check and it
answers with a verdict and categories. Use it on user input before an agent acts on it, or
on an answer before you show it.
curl -s $GW/v1/chat/completions -H "Authorization: Bearer $BOSPHORUS_KEY" \
-H 'Content-Type: application/json' \
-d '{"model":"EV/qwen3-guard-4b","messages":[{"role":"user","content":"What is the weather in Izmir?"}]}' \
| jq -r '.choices[0].message.content'
It answered Safety: Safe and Categories: None on two lines. A question about making a
weapon answered Safety: Unsafe and Categories: Violent. A small parser:
def guard(client, text):
out = client.chat.completions.create(
model="EV/qwen3-guard-4b",
messages=[{"role": "user", "content": text}],
).choices[0].message.content
fields = dict(line.split(": ", 1) for line in out.splitlines() if ": " in line)
safe = fields.get("Safety") == "Safe"
cats = [] if fields.get("Categories") in (None, "None") else fields["Categories"].split(", ")
return safe, cats
B3. Embeddings
An embedding places a text by meaning: texts about the same thing land close together.
Embed your documents once, store the vectors, then embed each query and compare. Pass a
list as input to embed several texts in one call.
curl -s $GW/v1/embeddings -H "Authorization: Bearer $BOSPHORUS_KEY" \
-H 'Content-Type: application/json' \
-d '{"model":"EV/qwen3-embedding-8b","input":"Merhaba dünya"}' \
| jq '.data[0].embedding | length'
It printed 4096. Comparing three texts:
import math
def embed(client, texts):
r = client.embeddings.create(model="EV/qwen3-embedding-8b", input=texts)
return [d.embedding for d in r.data]
def cosine(a, b):
dot = sum(x * y for x, y in zip(a, b))
return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))
q, related, unrelated = embed(client, [
"How do I reset my VPN password?",
"Steps to change the password of your VPN account",
"The canteen serves lentil soup on Fridays",
])
print(round(cosine(q, related), 3), round(cosine(q, unrelated), 3))
It printed 0.856 0.262: the related pair scores far higher. Keep one embedding model per
index, because vectors from different models cannot be compared.
B4. Rerank
A reranker reads the query and each document together and scores relevance. It is more
accurate than comparing embeddings, and slower, so use it on a short list: fetch 20 to 50
candidates with embeddings, rerank them, and give the best few to the LLM. Results come
back sorted by score, and index points into the list you sent.
curl -s $GW/v1/rerank -H "Authorization: Bearer $BOSPHORUS_KEY" \
-H 'Content-Type: application/json' -d '{
"model": "EV/qwen3-reranker-8b",
"query": "What is the capital of France?",
"documents": ["Bananas are yellow.", "Paris is the capital of France.", "Berlin is in Germany."]
}' | jq -c '.results[] | {index, relevance_score}'
It ranked index 1 first at 0.551, then index 2 at 0.002 and index 0 at 0.0004. The answer
also echoes each document and reports usage.prompt_tokens. The model reads up to 4K
tokens per query and document, so split long documents into passages. From Python:
import os, requests
GW, KEY = os.environ["GW"], os.environ["BOSPHORUS_KEY"]
def rerank(query, documents, top=3):
r = requests.post(f"{GW}/v1/rerank", timeout=60,
headers={"Authorization": f"Bearer {KEY}"},
json={"model": "EV/qwen3-reranker-8b", "query": query, "documents": documents})
r.raise_for_status()
return [(documents[x["index"]], x["relevance_score"]) for x in r.json()["results"][:top]]
B5. Decision
A decision model reads a state, such as an e-mail, a ticket or a JSON record, and answers
named, typed questions with probabilities instead of prose. The answer is fast and has a
fixed shape, which suits routing and triage. A choice question takes criteria: a list
of labels, or an object of label and description. The other types are score and noul
(a yes/no probability); this article has tested choice. EV/clef also accepts up to four
images as data URLs in an images field.
curl -s $GW/api/alpha/decisions -H "Authorization: Bearer $BOSPHORUS_KEY" \
-H 'Content-Type: application/json' -d '{
"model": "EV/clef",
"state": "Hi, I want my money back for the broken kettle.",
"questions": {
"intent": {
"type": "choice",
"instructions": "What does the customer want?",
"criteria": {"refund": "money back", "shipping": "where is my package", "other": "anything else"}
}
}
}' | jq .answers
It answered "choice": "refund" with confidence 0.99, and a probability for each label.
Sending options instead of criteria is refused with a choice question takes
‘criteria’ as a dict of label -> description, or a list of labels. From Python:
import os, requests
GW, KEY = os.environ["GW"], os.environ["BOSPHORUS_KEY"]
def route_ticket(text):
r = requests.post(f"{GW}/api/alpha/decisions", timeout=60,
headers={"Authorization": f"Bearer {KEY}"},
json={
"model": "EV/clef",
"state": text,
"questions": {"intent": {
"type": "choice",
"instructions": "What does the customer want?",
"criteria": {"refund": "money back",
"shipping": "where is my package",
"other": "anything else"},
}},
})
r.raise_for_status()
a = r.json()["answers"]["intent"]
return a["choice"], a["confidence"]
B6. OCR
Send the image as a data URL in image. The answer is the text in it, verbatim, in
text. The gateway takes JSON only, so encode the file yourself. The audit trail keeps
the image’s type and size, never the image.
IMG=$(base64 < invoice.png | tr -d '\n')
jq -n --arg img "data:image/png;base64,$IMG" '{model: "EV/dots-ocr", image: $img}' \
| curl -s $GW/v1/ocr -H "Authorization: Bearer $BOSPHORUS_KEY" \
-H 'Content-Type: application/json' -d @- | jq -r .text
For a 640 by 160 pixel image of an invoice line it returned Invoice 4711 and
Total: 128.50 EUR. From Python:
import base64, mimetypes, os, requests
GW, KEY = os.environ["GW"], os.environ["BOSPHORUS_KEY"]
def ocr(path, model="EV/dots-ocr"):
mime = mimetypes.guess_type(path)[0] or "image/png"
data = base64.b64encode(open(path, "rb").read()).decode()
r = requests.post(f"{GW}/v1/ocr", timeout=120,
headers={"Authorization": f"Bearer {KEY}"},
json={"model": model, "image": f"data:{mime};base64,{data}"})
r.raise_for_status()
return r.json()["text"]
Check the output on small images. The same text shrunk to 420 by 90 pixels in two colours
came back as unrelated text, so send images at their original resolution. In our test
EV/dots-ocr read the sample and EV/deepseek-ocr-2 returned an empty string for it, so
start with EV/dots-ocr.
B7. Speech to text
This is the OpenAI audio API: a multipart upload with file and model. The answer is
JSON by default; response_format text, srt and vtt also work. Evren returns no
timestamps, so srt and vtt carry the whole clip as one cue, and verbose_json is
refused.
curl -s $GW/v1/audio/transcriptions -H "Authorization: Bearer $BOSPHORUS_KEY" \
-F model=EV/qwen3-asr-1.7b -F file=@note.wav | jq -r .text
A spoken test sentence came back as The meeting starts at 9:30 tomorrow morning. On a
Mac, say -o note.aiff "…" followed by afconvert -f WAVE -d LEI16@16000 -c 1 note.aiff note.wav
makes such a clip.
Part C: test every kind in one run
This script checks your setup and calls each kind once. It prints one line per check and
exits with a non-zero code if anything fails, so it also works as a smoke test in a
pipeline. Set OCR_IMAGE to an image with some text in it to include OCR, and
AUDIO_FILE to a WAV file to include speech to text.
#!/usr/bin/env python3
import math, os, sys, base64, mimetypes, requests
GW = os.environ.get("GW", "https://ai.corp.example").rstrip("/")
KEY = os.environ.get("BOSPHORUS_KEY") or sys.exit("set BOSPHORUS_KEY to your personal key")
H = {"Authorization": f"Bearer {KEY}"}
failed = 0
def post(path, body):
r = requests.post(GW + path, headers=H, json=body, timeout=120)
if r.status_code != 200:
raise RuntimeError(f"HTTP {r.status_code}: {r.text[:300]}")
return r.json()
def check(name, fn):
global failed
try:
print(f" ok {name}: {fn()}")
except Exception as e:
failed += 1
print(f" FAIL {name}: {e}")
def whoami():
me = requests.get(GW + "/api/identity/me", headers=H, timeout=30).json()
email = (me.get("user") or {}).get("email")
assert email, "this key names no person: mint a personal key with ?client=api"
return email
def evren_key():
st = requests.get(GW + "/api/identity/evren-key", headers=H, timeout=30).json()
status = (st.get("llm") or {}).get("status")
assert status == "active", f"Evren LLM key status is {status!r}: save it on My variables"
return status
def listing():
data = requests.get(GW + "/v1/models", headers=H, timeout=30).json()["data"]
ev = {m["id"]: m.get("type") for m in data if m["id"].startswith("EV/")}
want = {"EV/qwen3-embedding-8b": "embedding", "EV/qwen3-reranker-8b": "rerank",
"EV/clef": "decision", "EV/dots-ocr": "ocr", "EV/qwen3-guard-4b": "guard"}
for mid, t in want.items():
assert ev.get(mid) == t, f"{mid} is listed as {ev.get(mid)!r}, expected {t!r}"
return f"{len(ev)} EV/ models, types as expected"
def chat():
j = post("/v1/chat/completions", {"model": "EV/deepseek-v4-flash",
"messages": [{"role": "user", "content": "Reply with the single word: pong"}]})
text = j["choices"][0]["message"]["content"] or ""
assert "pong" in text.lower(), repr(text)
return text.strip()
def guard():
j = post("/v1/chat/completions", {"model": "EV/qwen3-guard-4b",
"messages": [{"role": "user", "content": "What is the weather in Izmir?"}]})
text = j["choices"][0]["message"]["content"]
assert text.startswith("Safety: Safe"), repr(text)
return text.replace("\n", " / ")
def embeddings():
j = post("/v1/embeddings", {"model": "EV/qwen3-embedding-8b", "input": [
"How do I reset my VPN password?",
"Steps to change the password of your VPN account",
"The canteen serves lentil soup on Fridays"]})
v = [d["embedding"] for d in j["data"]]
cos = lambda a, b: sum(x * y for x, y in zip(a, b)) / (
math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))
related, unrelated = cos(v[0], v[1]), cos(v[0], v[2])
assert len(v[0]) == 4096 and related > unrelated, (len(v[0]), related, unrelated)
return f"{len(v[0])} dims, related {related:.3f} vs unrelated {unrelated:.3f}"
def rerank():
docs = ["Bananas are yellow.", "Paris is the capital of France.", "Berlin is in Germany."]
j = post("/v1/rerank", {"model": "EV/qwen3-reranker-8b",
"query": "What is the capital of France?", "documents": docs})
top = j["results"][0]
assert top["index"] == 1, j["results"]
return f"top: {docs[top['index']]!r} ({top['relevance_score']:.3f})"
def decision():
j = post("/api/alpha/decisions", {"model": "EV/clef",
"state": "Hi, I want my money back for the broken kettle.",
"questions": {"intent": {"type": "choice", "instructions": "What does the customer want?",
"criteria": {"refund": "money back",
"shipping": "where is my package",
"other": "anything else"}}}})
a = j["answers"]["intent"]
assert a["choice"] == "refund", a
return f"choice {a['choice']} ({a['confidence']:.2f})"
def ocr():
path = os.environ["OCR_IMAGE"]
mime = mimetypes.guess_type(path)[0] or "image/png"
data = base64.b64encode(open(path, "rb").read()).decode()
text = post("/v1/ocr", {"model": "EV/dots-ocr", "image": f"data:{mime};base64,{data}"})["text"]
assert text.strip(), "no text came back"
return text.replace("\n", " / ")[:120]
def speech():
with open(os.environ["AUDIO_FILE"], "rb") as f:
r = requests.post(GW + "/v1/audio/transcriptions", headers=H, timeout=120,
data={"model": "EV/qwen3-asr-1.7b"}, files={"file": f})
assert r.status_code == 200, f"HTTP {r.status_code}: {r.text[:300]}"
return r.json()["text"]
def wrong_endpoint():
r = requests.post(GW + "/v1/chat/completions", headers=H, timeout=60,
json={"model": "EV/qwen3-reranker-8b",
"messages": [{"role": "user", "content": "hi"}]})
assert r.status_code == 400 and "/v1/rerank" in r.text, f"HTTP {r.status_code}: {r.text[:200]}"
return "400, names /v1/rerank"
print(f"gateway {GW}")
for name, fn in [("personal key", whoami), ("Evren key", evren_key), ("model listing", listing),
("chat", chat), ("guard", guard), ("embeddings", embeddings),
("rerank", rerank), ("decision", decision),
("wrong endpoint is refused", wrong_endpoint)]:
check(name, fn)
if os.environ.get("OCR_IMAGE"):
check("ocr", ocr)
if os.environ.get("AUDIO_FILE"):
check("speech to text", speech)
print("all passed" if not failed else f"{failed} failed")
sys.exit(1 if failed else 0)
Save it as evren_smoke.py, install requests, and run python3 evren_smoke.py. A
passing run ends with all passed.
When something is not right
| What you see | What to do |
|---|---|
| 503 “EV/…” is served with YOUR OWN Evren key and you have not registered one | Save your Evren LLM key on My variables (A1). |
| 503 … this request carries no user identity to look one up for | You sent a team or service key. Use your personal key (A2). |
| 400 “qwen3-reranker-8b” is a rerank model — use POST /v1/rerank, not /v1/chat/completions | Call the endpoint the message names. |
| 404 … does not serve /v1/rerank — it is served by Evren rerank models only | Use an EV/ model of that kind. |
| 400 field “image” is required … | OCR needs "image": "data:image/png;base64,…". |
| 400 a choice question takes ‘criteria’ … | Use criteria, as a list or an object. |
| 400 Currently do not support verbose_json … | Use json, text, srt or vtt for speech to text. |
An empty chat answer with finish_reason: "length" |
A reasoning model spent max_tokens on thinking. Raise it or leave it out. |
| 401 invalid api key | The key was revoked or mistyped, or you sent your Evren key itself. Send your gpk_… key, or mint a new one with &rotate=true (A3). |
| 429 | A rate limit. Wait for the time in the retry-after header. |
Every call appears under your name on the activity page, with its endpoint.