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 put
  • GET /v1/models — dostupni modeli
  • POST /v1/embeddings — numerički zapis teksta
  • messagessystem, user, assistant, tool
  • .choices[0].message.content — put do teksta odgovora
  • finish_reasonstop, length, tool_calls
  • usage — pouzdan broj tokena
  • temperature: 0 + seed — ponovljiv rezultat
  • max_tokens — postavi ga uvek
  • stream: true + curl -N — postepen ispis
  • response_format: {"type":"json_object"} — JSON umesto proze
  • tools — model predlaže poziv, tvoj kod izvršava
  • Authorization: Bearer — ključ, koji Ollama ignoriše
  • Server ne pamti ništa — istorija ide u svakom zahtevu

Vežba

  1. Pošalji zahtev kroz curl i izvuci samo tekst odgovora pomoću jq.
  2. Namerno postavi max_tokens na 10 i potvrdi vrednost finish_reason.
  3. Pošalji isti zahtev dvaput sa temperaturom 0 i fiksiranim seed, pa uporedi odgovore znak po znak.
  4. Napiši zahtev koji vraća JSON sa tri polja i proveri ga kroz jq -e.
  5. Sastavi razgovor od četiri poruke i uporedi prompt_tokens sa vrednošću za jednu poruku.
  6. 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:

Comments

Popular posts from this blog

Početak u Linuxu — šta je i zašto se uči

Konverzija tipova podataka u Pythonu

groupadd