OpenAI-kompatibilan API na svom serveru
Sve što si do sada pokrenuo — Ollama, llama.cpp, vLLM — izlaže isti oblik API-ja. To nije slučajnost nego pristanak: OpenAI je definisao oblik zahteva, ostali su ga preuzeli, i zahvaljujući tome tvoj lokalni model radi sa alatima koji o njemu ništa ne znaju.
Ovaj tekst razlaže taj oblik do kraja. Sve dalje u serijalu — skripte, analiza logova, RAG, agenti — svodi se na zahteve opisane ovde.
Najmanji mogući zahtev
$ curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:8b",
"messages": [
{"role": "user", "content": "Šta radi komanda ss -tulpn?"}
]
}'
Adresa se razlikuje po alatu, ostalo je isto:
| Alat | Adresa | Polje model |
|---|---|---|
| Ollama | :11434/v1 |
Obavezno, oznaka iz ollama list |
| llama.cpp | :8080/v1 |
Zanemaruje se, drži jedan model |
| vLLM | :8000/v1 |
Obavezno, mora se poklopiti |
Šta je servis stvarno spreman da posluži:
$ curl -s http://localhost:11434/v1/models | jq -r '.data[].id' qwen3:8b linux-pomocnik:latest
Oblik odgovora
{
"id": "chatcmpl-843",
"object": "chat.completion",
"created": 1755870123,
"model": "qwen3:8b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Prikazuje otvorene portove sa procesima koji ih drže."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 24,
"completion_tokens": 18,
"total_tokens": 42
}
}
Sam tekst je duboko ukopan, pa u skriptama ide kroz jq:
$ curl -s http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen3:8b","messages":[{"role":"user","content":"Zdravo"}]}' \
| jq -r '.choices[0].message.content'
Dva polja u odgovoru vredi pratiti.
finish_reason govori zašto je generisanje stalo:
| Vrednost | Značenje |
|---|---|
stop |
Model je završio prirodno |
length |
Dostignut max_tokens, odgovor je odsečen |
tool_calls |
Model traži poziv funkcije |
usage je jedini pouzdan brojač tokena. Ako pratiš koliko ti konteksta ostaje ili poredaš postavke, tu je podatak.
Uloge poruka
{
"model": "qwen3:8b",
"messages": [
{"role": "system", "content": "Ti si pomoćnik za Linux administraciju. Odgovaraj kratko, na srpskom."},
{"role": "user", "content": "Kako da vidim zauzeće diska?"},
{"role": "assistant", "content": "Komandom df -h za particije, du -sh za direktorijume."},
{"role": "user", "content": "A po direktorijumima, sortirano?"}
]
}
| Uloga | Čemu služi |
|---|---|
system |
Uloga i pravila; ide prva, jednom |
user |
Pitanje ili zadatak |
assistant |
Raniji odgovori modela |
tool |
Rezultat izvršene funkcije |
Server ne pamti ništa između zahteva. Ceo dosadašnji razgovor šalješ ponovo u svakom pozivu — i to je razlog zašto dug razgovor postaje sporiji i skuplji, kako objašnjava tekst o tome šta je AI model. Istorija koju vidiš u Open WebUI je posao aplikacije, ne servisa.
Parametri generisanja
{
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Napiši komandu za pretragu logova"}],
"temperature": 0.1,
"max_tokens": 256,
"top_p": 0.9,
"seed": 42,
"stop": ["\n\n"]
}
| Parametar | Praktična vrednost |
|---|---|
temperature |
0 – 0.2 za komande i podatke, 0.7 – 1.0 za tekst |
max_tokens |
Postavi ga; bez njega odgovor ume da beži |
top_p |
0.9, retko se dira |
seed |
Uz temperaturu 0, za ponovljiv rezultat |
stop |
Nizovi na kojima generisanje prestaje |
stream |
Postepen ispis |
Za skripte je kombinacija temperature: 0 i fiksiran seed ono što daje ponovljivost. Bez nje ista komanda danas i sutra daje različit rezultat, što u automatizaciji nije prihvatljivo.
Streaming
$ curl -N http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Objasni šta je inode"}],
"stream": true
}'
data: {"choices":[{"delta":{"content":"Inode"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":" je"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":" struktura"},"finish_reason":null}]}
data: [DONE]
Format je Server-Sent Events. Umesto message dobijaš delta sa komadićem teksta, a niz se završava oznakom [DONE].
Sklapanje u bashu:
$ curl -sN http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen3:8b","messages":[{"role":"user","content":"Zdravo"}],"stream":true}' \
| grep '^data: ' | sed 's/^data: //' | grep -v '^\[DONE\]' \
| jq -j '.choices[0].delta.content // empty'
Oznaka -N kod curl isključuje baferovanje. Bez nje sve stiže odjednom na kraju i cela poenta otpada.
Za skripte streaming najčešće nije potreban. Ako obrađuješ ceo odgovor odjednom, izostavi ga i uzmi jednostavniji oblik. Koristan je samo tamo gde neko gleda u ekran.
Strukturiran izlaz
Ovo je najkorisnija stvar u celom tekstu za administratora. Umesto da parsiraš prozu, tražiš JSON:
$ curl -s http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3:8b",
"messages": [
{"role": "system", "content": "Odgovaraj isključivo u JSON obliku, bez ikakvog dodatnog teksta."},
{"role": "user", "content": "Klasifikuj ovu poruku iz loga: SSH login failed for root from 203.0.113.45. Vrati polja: tip, ozbiljnost (1-5), ip."}
],
"temperature": 0,
"response_format": {"type": "json_object"}
}' | jq -r '.choices[0].message.content'
{
"tip": "neuspela_prijava",
"ozbiljnost": 4,
"ip": "203.0.113.45"
}
Polje response_format tera model da vrati ispravan JSON. vLLM ide korak dalje i podržava tačnu šemu:
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "log_zapis",
"schema": {
"type": "object",
"properties": {
"tip": {"type": "string"},
"ozbiljnost": {"type": "integer", "minimum": 1, "maximum": 5},
"ip": {"type": "string"}
},
"required": ["tip", "ozbiljnost", "ip"]
}
}
}
Sa šemom je izlaz zajamčeno ispravan, jer se ograničenje primenjuje pri samom biranju tokena. Bez nje se oslanjaš na to da će model poslušati — što obično hoće, ali ne uvek.
I dalje proveravaj rezultat pre upotrebe. Ispravan JSON nije isto što i tačan JSON; model može uredno vratiti pogrešnu vrednost u ispravnom obliku.
Tool calling
Model ne izvršava ništa — vraća zahtev da nešto bude pozvano, a tvoj kod odlučuje šta dalje.
{
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "Koliko je slobodno na disku?"}],
"tools": [
{
"type": "function",
"function": {
"name": "provera_diska",
"description": "Vraća zauzeće diska na serveru",
"parameters": {
"type": "object",
"properties": {
"putanja": {"type": "string", "description": "Putanja, npr. /var"}
},
"required": ["putanja"]
}
}
}
]
}
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "provera_diska",
"arguments": "{\"putanja\": \"/\"}"
}
}
]
},
"finish_reason": "tool_calls"
Ti izvršiš komandu i rezultat vratiš kao poruku sa ulogom tool:
{"role": "tool", "tool_call_id": "call_1", "content": "/dev/sda1 234G 189G 33G 86% /"}
Model zatim odgovara na osnovu stvarnog podatka. Podrška nije univerzalna — proveri je pre nego što nešto gradiš na njoj:
$ ollama show qwen3:8b | grep -A5 Capabilities
Ovde je bezbednosna granica cele postavke. Ime funkcije i argumente predlaže model, a model može biti naveden podmetnutom instrukcijom u podacima koje čita. Nikad ne prosleđuj argumente pravo u ljusku i drži spisak dozvoljenih radnji uzak. Tema je razrađena u tekstu o bezbednosti, a standardizovan pristup u tekstu o MCP-u.
Embeddings
Zaseban put, za pretvaranje teksta u numerički zapis značenja:
$ curl -s http://localhost:11434/v1/embeddings \
-H "Content-Type: application/json" \
-d '{"model": "nomic-embed-text", "input": "restart nginx servisa"}' \
| jq '.data[0].embedding | length'
768
Model za razgovor ovo ne radi — treba poseban, namenski. Osnova je za RAG.
Autentifikacija
$ curl http://localhost:8000/v1/chat/completions \ -H "Authorization: Bearer tajni-kljuc-ovde" ...
Ollama ovo polje ignoriše i prihvata bilo šta. llama.cpp i vLLM ga proveravaju ako je servis podignut sa --api-key. Za ozbiljnu zaštitu ide proxy ispred, o čemu govore tekstovi o reverse proxy-ju i zaštiti API-ja.
Greške
$ curl -s -w '\n%{http_code}\n' http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"nepostojeci","messages":[{"role":"user","content":"test"}]}'
| Kod | Uzrok |
|---|---|
| 400 | Neispravan JSON ili predugačak kontekst |
| 401 | Pogrešan ili izostavljen ključ |
| 404 | Model nije preuzet ili naziv ne odgovara |
| 500 | Servis puca, najčešće zbog memorije |
| 503 | Model se učitava ili je red pun |
Skripte pišu tako da uvek gledaju kod odgovora:
#!/bin/bash
ODGOVOR=$(curl -s -w '\n%{http_code}' http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d "$ZAHTEV")
KOD=$(echo "$ODGOVOR" | tail -1)
TELO=$(echo "$ODGOVOR" | head -n -1)
if [ "$KOD" != "200" ]; then
echo "Greška $KOD: $(echo "$TELO" | jq -r '.error.message // .')" >&2
exit 1
fi
echo "$TELO" | jq -r '.choices[0].message.content'
Ovaj obrazac se ponavlja kroz ceo ostatak serijala. Osnove pisanja skripti pokriva serijal o bash skriptama.
Praktični scenariji
Scenario 1: odgovor se seče na pola rečenice
Pogledaj finish_reason. Vrednost length znači da je max_tokens premali.
Scenario 2: isti zahtev daje različite odgovore
Postavi temperature na 0 i fiksiraj seed. Bez oba nema ponovljivosti.
Scenario 3: JSON u odgovoru okružen objašnjenjem
Dodaj response_format, a u sistemski prompt izričito napiši da nema propratnog teksta. Ako i to ne pomogne, izvuci deo između vitičastih zagrada.
Scenario 4: greška 400 sa pomenom konteksta
Razgovor je premašio prozor. Skrati istoriju ili podigni kontekst servisa, uz proveru memorije.
Scenario 5: prvi zahtev traje dugo, ostali su brzi
Model se učitava pri prvom pozivu. Podigni OLLAMA_KEEP_ALIVE, kako je opisano u tekstu o systemd jedinici.
Scenario 6: model zanemaruje sistemski prompt
Proveri da nije base verzija. Kod nekih modela pomaže i da se pravilo ponovi na kraju korisničke poruke.
Kratka referenca
POST /v1/chat/completions— glavni putGET /v1/models— dostupni modeliPOST /v1/embeddings— numerički zapis tekstamessages—system,user,assistant,tool.choices[0].message.content— put do teksta odgovorafinish_reason—stop,length,tool_callsusage— pouzdan broj tokenatemperature: 0+seed— ponovljiv rezultatmax_tokens— postavi ga uvekstream: true+curl -N— postepen ispisresponse_format: {"type":"json_object"}— JSON umesto prozetools— model predlaže poziv, tvoj kod izvršavaAuthorization: Bearer— ključ, koji Ollama ignoriše- Server ne pamti ništa — istorija ide u svakom zahtevu
Vežba
- Pošalji zahtev kroz
curli izvuci samo tekst odgovora pomoćujq. - Namerno postavi
max_tokensna 10 i potvrdi vrednostfinish_reason. - Pošalji isti zahtev dvaput sa temperaturom 0 i fiksiranim
seed, pa uporedi odgovore znak po znak. - Napiši zahtev koji vraća JSON sa tri polja i proveri ga kroz
jq -e. - Sastavi razgovor od četiri poruke i uporedi
prompt_tokenssa vrednošću za jednu poruku. - Napiši skriptu koja proverava HTTP kod i vraća razumnu poruku pri grešci.
Sledeći tekst u serijalu: Nginx reverse proxy i HTTPS ispred lokalnog LLM-a
Povezano:
- AI na Linux serveru — pregled celog serijala
- Open WebUI — prethodni tekst
- Rečnik AI pojmova — značenje parametara generisanja
- curl — rad sa HTTP zahtevima iz terminala
- Bash skripte — osnove pisanja skripti
Comments
Post a Comment