FastAPI LLM Ollama vLLM Python API Docker

FastAPI ile LLM API Geliştirme: Ollama ve vLLM (2026)

İleri
person Yapay Zeka Uzmanı
list_altİçindekilerexpand_more
  1. 01FastAPI ve LLM Servis Mimarisi
  2. 02FastAPI neden bu iş için doğru seçim?
  3. 03Ollama mu, vLLM mi?
  4. 04Mimari akış
  5. 05Ortam Kurulumu
  6. 06Gereksinimler
  7. 07Kurulum adımları
  8. 08Ollama ile Basit LLM Endpoint
  9. 09Streaming Response Desteği
  10. 10vLLM Entegrasyonu (GPU Sunucu Senaryosu)
  11. 11JWT Auth Middleware
  12. 12Docker ile Production Deployment
  13. 13Dockerfile
  14. 14docker-compose.yml
  15. 15Performans ve İzleme
  16. 16/health endpoint
  17. 17Yanıt süreleri ölçümü
  18. 18Sonraki Adımlar

Bir uygulamada OpenAI yerine yerel LLM kullanmak istiyorsun ama Ollama’nın socket’ına doğrudan bağlanmak hem kırılgan hem de bakımı zor bir yapı. Asıl çözüm şu: kendi servis katmanını yaz. FastAPI bu iş için tam oturur; async desteği, tip doğrulaması ve otomatik OpenAPI dökümanıyla dakikalar içinde üretim kalitesinde bir LLM API elde edersin. Bu rehberde Ollama ile geliştirme ortamı kuruyoruz, ardından GPU sunucusu için vLLM’e geçişi adım adım gösteriyoruz.

FastAPI ve LLM Servis Mimarisi

FastAPI neden bu iş için doğru seçim?

Çoğu LLM uygulaması zamanla benzer bir hal alır: birkaç endpoint, token stream’i iletmek, basit bir auth katmanı. Flask ile başlamak cazip görünse de token başına birden fazla müşteriye aynı anda yanıt verirken async olmayan bir framework tıkanır. FastAPI’nin async def tabanlı mimarisi, tek bir thread’de onlarca bağlantıyı aynı anda tutabilir; bu da streaming LLM yanıtları için kritik bir avantaj.

Bunun ötesinde Pydantic ile gelen otomatik istek doğrulaması, /docs üzerinden hazır bir Swagger UI ve type hint’lerden türetilen schema, tek başına bir sprint tasarrufu demek.

Ollama mu, vLLM mi?

İkisi aynı problemi farklı profiller için çözüyor:

KriterOllamavLLM
Hedef ortamGeliştirme, CPU/tüketici GPUÜretim, data-center GPU
KurulumTek komutpip install vllm, CUDA gerekli
ThroughputOrtaÇok yüksek (PagedAttention)
OpenAI uyumlu endpointEvet (/api/chat)Evet (/v1/chat/completions)
Model formatGGUF, Llama.cppHugging Face ağırlıkları
Bellek yönetimiOtomatik, GGUF quantizePagedAttention ile KV-cache

Kaynak: vLLM GitHub deposu, Ollama docs

Geliştirme makinende Ollama; AWS p3/p4 veya bare-metal GPU sunucusunda vLLM. İki senaryoyu da aşağıda ele alıyoruz.

Mimari akış

İstemci


FastAPI (uvicorn, 0.0.0.0:8000)

  ├── POST /chat     ──► Ollama (localhost:11434)
  │                         veya
  │                      vLLM  (localhost:8080)

  └── GET  /health

FastAPI bir proxy değil; istek doğrulaması, auth ve hata yönetimini üstleniyor. LLM backend’i çıkarılabilir; konfigürasyon değişkeni olarak tutuyoruz.

Ortam Kurulumu

Gereksinimler

  • Python 3.11+
  • Ollama (geliştirme için) veya CUDA 12.x + vLLM (üretim için)
  • Docker ve Docker Compose (deployment için)

Kurulum adımları

# Sanal ortam
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate

# Bağımlılıklar
pip install fastapi uvicorn[standard] httpx pydantic python-jose[cryptography]

# Ollama kurulumu (macOS/Linux)
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull llama3.2          # ~2GB, 8B model

Model indikten sonra Ollama otomatik olarak http://localhost:11434 adresinde dinlemeye başlar. Aşağıdaki komutla çalıştığını doğrula:

curl http://localhost:11434/api/generate \
  -d '{"model":"llama3.2","prompt":"Merhaba","stream":false}'

Ollama ile Basit LLM Endpoint

Proje yapısı:

llm_api/
├── main.py
├── models.py
├── auth.py
└── config.py

config.py ile backend seçimini ortam değişkenine bağlıyoruz:

# config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    llm_backend: str = "ollama"          # "ollama" veya "vllm"
    ollama_url: str = "http://localhost:11434"
    vllm_url: str   = "http://localhost:8080"
    ollama_model: str = "llama3.2"
    vllm_model: str   = "meta-llama/Meta-Llama-3-8B-Instruct"
    secret_key: str   = "change-me-in-prod"
    algorithm: str    = "HS256"

settings = Settings()

İstek/yanıt şemaları:

# models.py
from pydantic import BaseModel
from typing import Optional

class ChatRequest(BaseModel):
    message: str
    system: Optional[str] = "Sen yardımcı bir asistansın."
    temperature: float = 0.7
    max_tokens: int = 512

class ChatResponse(BaseModel):
    reply: str
    model: str
    backend: str

Temel endpoint:

# main.py
from fastapi import FastAPI, HTTPException
import httpx
from models import ChatRequest, ChatResponse
from config import settings

app = FastAPI(title="LLM API", version="1.0.0")

@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
    if settings.llm_backend == "ollama":
        return await _ollama_chat(req)
    return await _vllm_chat(req)

async def _ollama_chat(req: ChatRequest) -> ChatResponse:
    payload = {
        "model": settings.ollama_model,
        "messages": [
            {"role": "system", "content": req.system},
            {"role": "user",   "content": req.message},
        ],
        "options": {"temperature": req.temperature, "num_predict": req.max_tokens},
        "stream": False,
    }
    async with httpx.AsyncClient(timeout=60) as client:
        resp = await client.post(f"{settings.ollama_url}/api/chat", json=payload)
    if resp.status_code != 200:
        raise HTTPException(status_code=502, detail="Ollama hatası")
    data = resp.json()
    return ChatResponse(
        reply=data["message"]["content"],
        model=settings.ollama_model,
        backend="ollama",
    )

uvicorn main:app --reload ile başlat; http://localhost:8000/docs üzerinden endpoint’i hemen test edebilirsin.

Streaming Response Desteği

Büyük çıktılarda kullanıcının tüm yanıtı beklemesi kötü bir deneyim. Token-by-token akış için FastAPI’nin StreamingResponse’u ve Ollama’nın stream modunu birleştiriyoruz:

from fastapi.responses import StreamingResponse
import json

@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
    async def token_generator():
        payload = {
            "model": settings.ollama_model,
            "messages": [
                {"role": "system", "content": req.system},
                {"role": "user",   "content": req.message},
            ],
            "stream": True,
        }
        async with httpx.AsyncClient(timeout=120) as client:
            async with client.stream(
                "POST",
                f"{settings.ollama_url}/api/chat",
                json=payload,
            ) as response:
                async for line in response.aiter_lines():
                    if not line:
                        continue
                    chunk = json.loads(line)
                    token = chunk.get("message", {}).get("content", "")
                    if token:
                        yield f"data: {json.dumps({'token': token})}\n\n"
                    if chunk.get("done"):
                        yield "data: [DONE]\n\n"

    return StreamingResponse(token_generator(), media_type="text/event-stream")

Frontend tarafında bu endpoint’i EventSource veya fetch + ReadableStream ile tüketebilirsin:

const response = await fetch("/chat/stream", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message: "Merhaba!" }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const text = decoder.decode(value);
  // "data: {...}\n\n" satırlarını parse et
  text.split("\n\n").forEach(line => {
    if (line.startsWith("data: ") && !line.includes("[DONE]")) {
      const chunk = JSON.parse(line.slice(6));
      process.stdout.write(chunk.token);
    }
  });
}

vLLM Entegrasyonu (GPU Sunucu Senaryosu)

vLLM, OpenAI API formatıyla uyumlu bir HTTP sunucusu açıyor; bu yüzden FastAPI tarafındaki değişiklik minimal. Sunucuyu başlatmak için:

pip install vllm
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Meta-Llama-3-8B-Instruct \
  --port 8080 \
  --tensor-parallel-size 1

FastAPI tarafında vLLM proxy fonksiyonu:

async def _vllm_chat(req: ChatRequest) -> ChatResponse:
    payload = {
        "model": settings.vllm_model,
        "messages": [
            {"role": "system", "content": req.system},
            {"role": "user",   "content": req.message},
        ],
        "temperature": req.temperature,
        "max_tokens": req.max_tokens,
    }
    async with httpx.AsyncClient(timeout=120) as client:
        resp = await client.post(
            f"{settings.vllm_url}/v1/chat/completions",
            json=payload,
        )
    if resp.status_code != 200:
        raise HTTPException(status_code=502, detail="vLLM hatası")
    data = resp.json()
    return ChatResponse(
        reply=data["choices"][0]["message"]["content"],
        model=settings.vllm_model,
        backend="vllm",
    )

Ortam değişkeniyle geçiş yapılır: LLM_BACKEND=vllm uvicorn main:app.

Daha ayrıntılı bir vLLM karşılaştırması için vLLM vs SGLang vs TGI: Üretim LLM Servis Karşılaştırması yazısına bakabilirsin.

JWT Auth Middleware

Herkese açık bir endpoint üretimde tutulmamalı. FastAPI’nin Depends mekanizmasıyla Bearer token doğrulaması eklemek yalnızca birkaç satır:

# auth.py
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from config import settings

bearer_scheme = HTTPBearer()

def verify_token(
    credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
) -> dict:
    token = credentials.credentials
    try:
        payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
        return payload
    except JWTError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Geçersiz veya süresi dolmuş token",
        )

Endpoint’e eklemek için:

@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest, _: dict = Depends(verify_token)):
    ...

Token üretmek için küçük bir yardımcı:

from datetime import datetime, timedelta

def create_token(subject: str, expire_minutes: int = 60) -> str:
    payload = {
        "sub": subject,
        "exp": datetime.utcnow() + timedelta(minutes=expire_minutes),
    }
    return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)

Docker ile Production Deployment

Dockerfile

FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .

ENV LLM_BACKEND=ollama
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

docker-compose.yml

services:
  llm-api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - LLM_BACKEND=ollama
      - OLLAMA_URL=http://ollama:11434
      - SECRET_KEY=${SECRET_KEY}
    depends_on:
      - ollama

  ollama:
    image: ollama/ollama:latest
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
    # GPU erişimi için:
    # deploy:
    #   resources:
    #     reservations:
    #       devices:
    #         - driver: nvidia
    #           count: 1
    #           capabilities: [gpu]

volumes:
  ollama_data:

Başlatmak için:

export SECRET_KEY=$(openssl rand -hex 32)
docker compose up -d
docker compose exec ollama ollama pull llama3.2

Performans ve İzleme

/health endpoint

@app.get("/health")
async def health():
    backend_ok = False
    check_url = (
        f"{settings.ollama_url}/api/tags"
        if settings.llm_backend == "ollama"
        else f"{settings.vllm_url}/health"
    )
    try:
        async with httpx.AsyncClient(timeout=5) as client:
            r = await client.get(check_url)
            backend_ok = r.status_code == 200
    except Exception:
        pass
    return {
        "status": "ok" if backend_ok else "degraded",
        "backend": settings.llm_backend,
        "backend_reachable": backend_ok,
    }

Yanıt süreleri ölçümü

FastAPI middleware’i ile her isteğin ne kadar sürdüğünü loglamak:

import time
from fastapi import Request

@app.middleware("http")
async def add_timing(request: Request, call_next):
    start = time.perf_counter()
    response = await call_next(request)
    duration_ms = (time.perf_counter() - start) * 1000
    response.headers["X-Response-Time-Ms"] = f"{duration_ms:.1f}"
    print(f"{request.method} {request.url.path}{duration_ms:.1f}ms")
    return response

Yük altında profil çıkarmak için locust veya k6 kullanabilirsin. Streaming endpoint’te ilk token süresi (TTFT) ve toplam yanıt süresi ayrı ayrı ölçülmeli; LLM kalitesi değerlendirmesinde TTFT genellikle daha kritik.

Sonraki Adımlar

Bu noktada çalışan bir FastAPI + Ollama servisi var; vLLM’e geçiş tek bir ortam değişkeni. Uygulamayı büyütmek istersen bakmaya değer noktalar:

  • Aynı prompt’a tekrar eden istekler için Redis ile yanıt önbellekleme; Prompt Caching nedir? yazısında kavramsal zemin var.
  • Server-Sent Events yerine çift yönlü iletişim gerekirse websockets ekle.
  • slowapi veya nginx upstream limitleri ile API kotası koy.
  • Prometheus + Grafana ile token/saniye ve TTFT dashboard’u kur.
  • Birden fazla model arasında runtime’da geçiş için konfigürasyon katmanını genişlet.
  • /v1/ prefix’i ve schema uyumu eklersen mevcut OpenAI Python istemcisi değişiklik gerektirmeden bu sunucuya bağlanır.

İlgili kaynaklar:

auto_stories İlgili Makaleler