first commit
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# AnthropicHandler
|
||||
|
||||
**Özet:** `/anthropic/*` route'unu karşılar (`handlers/anthropic.go:75`). Anthropic Messages API formatını OpenAI Chat Completions formatına çevirir (Bifrost), upstream'e iletir ve yanıtı geri Anthropic formatına dönüştürür.
|
||||
|
||||
**Kütüphaneler:** Fiber v3, GORM, Go `encoding/json`
|
||||
|
||||
**Bağlantılar:** [[Main]], [[BifrostTransform]], [[ProxyEngine]], [[RequestLog]], [[Config]], [[Anthropic Flow]]
|
||||
|
||||
## Davranış
|
||||
|
||||
1. `GET /v1/models` → [[AnthropicHandler#modelsList|modelleri listele]]
|
||||
2. `HEAD` → 404 (DeepSeek uyumluluğu)
|
||||
3. Empty body → varsayılan "optoant gateway ready" yanıtı
|
||||
4. `x-api-key` → `Authorization: Bearer` dönüşümü (Claude Code uyumluluğu)
|
||||
5. Gerekirse `OPENAI_KEY`'den auto-inject
|
||||
6. [[BifrostTransform#AnthropicToBifrost|AnthropicToBifrost()]] ile dönüşüm
|
||||
7. [[ProxyEngine]] ile `OPENAI_BACKEND/v1/chat/completions`'a forward
|
||||
8. Başarılıysa (200): [[BifrostTransform#BifrostToAnthropic|BifrostToAnthropic()]] → Anthropic format
|
||||
9. Hata durumunda: raw passthrough
|
||||
10. DB log (fire-and-forget)
|
||||
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
POST /anthropic/v1/messages
|
||||
x-api-key: <api-key>
|
||||
# veya
|
||||
Authorization: Bearer <api-key>
|
||||
```
|
||||
|
||||
## Modeller (`modelsList()`)
|
||||
|
||||
| ID | Display Name |
|
||||
|---|---|
|
||||
| `deepseek-v4-flash` | DeepSeek V4 Flash |
|
||||
| `deepseek-v4-pro` | DeepSeek V4 Pro |
|
||||
|
||||
## Ek Fonksiyonlar
|
||||
|
||||
| Fonksiyon | Açıklama |
|
||||
|---|---|
|
||||
| `infoPage()` | `GET /anthropic` (boş path) için HTML bilgi sayfası |
|
||||
| `modelsList()` | `GET /anthropic/v1/models` için model listesi |
|
||||
|
||||
## Edge Case'ler
|
||||
|
||||
- Geçersiz Anthropic formatı → uyarı log'u + raw passthrough
|
||||
- Upstream hatası → `502 Bad Gateway`
|
||||
- Yanıt dönüşüm hatası → raw yanıtı forward et
|
||||
@@ -0,0 +1,46 @@
|
||||
# Anthropic Flow
|
||||
|
||||
**Özet:** Anthropic Messages API formatındaki isteklerin gateway üzerinden Bifrost dönüşümüyle OpenAI formatına çevrilip upstream'e iletilmesi ve yanıtın geri Anthropic formatına dönüştürülmesi.
|
||||
|
||||
**Kütüphaneler:** Fiber v3, Go `net/http`, `encoding/json`
|
||||
|
||||
**Bağlantılar:** [[AnthropicHandler]], [[BifrostTransform]], [[ProxyEngine]], [[RequestLog]], [[Index]]
|
||||
|
||||
## Akış Diyagramı
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Gateway
|
||||
participant Bifrost as BifrostTransform
|
||||
participant Proxy
|
||||
participant Upstream
|
||||
participant DB
|
||||
|
||||
Client->>Gateway: POST /anthropic/v1/messages
|
||||
Note over Gateway: AnthropicHandler
|
||||
Gateway->>Gateway: x-api-key → Authorization: Bearer
|
||||
Gateway->>Gateway: API key auto-inject (gerekirse)
|
||||
Gateway->>Bifrost: AnthropicToBifrost()
|
||||
Bifrost-->>Gateway: OpenAI Chat Completions format
|
||||
Gateway->>Proxy: Forward() to /v1/chat/completions
|
||||
Proxy->>Upstream: POST <BACKEND>/v1/chat/completions
|
||||
Upstream-->>Proxy: 200 OK + OpenAI format response
|
||||
Proxy-->>Gateway: ForwardResult
|
||||
Gateway->>Bifrost: BifrostToAnthropic()
|
||||
Bifrost-->>Gateway: Anthropic Messages format
|
||||
Gateway->>DB: go db.Create(logEntry)
|
||||
Gateway-->>Client: 200 OK (Anthropic format)
|
||||
```
|
||||
|
||||
## Edge Case'ler
|
||||
|
||||
| Senaryo | Davranış |
|
||||
|---|---|
|
||||
| **HEAD isteği** | 404 (DeepSeek uyumluluğu) |
|
||||
| **Empty body** | Varsayılan "optoant gateway ready" yanıtı |
|
||||
| **Geçersiz Anthropic formatı** | Raw passthrough + uyarı log'u |
|
||||
| **Yanıt dönüşüm hatası** | Raw yanıtı forward et (passthrough) |
|
||||
| **GET /v1/models** | DeepSeek model listesi (Anthropic formatında) |
|
||||
| **Boş path /anthropic** | HTML info sayfası (`infoPage()`) |
|
||||
| **Upstream hatası** | 502 Bad Gateway |
|
||||
@@ -0,0 +1,61 @@
|
||||
# BifrostTransform
|
||||
|
||||
**Özet:** Anthropic Messages API formatı ile OpenAI Chat Completions formatı arasında çift yönlü dönüşüm yapan katman (`internal/transform/anthropic_bifrost.go:74`). "Bifrost" (İskandinav mitolojisindeki köprü) adı, two-way dönüşümü simgeler.
|
||||
|
||||
**Kütüphaneler:** Go `encoding/json`, `strings`
|
||||
|
||||
**Bağlantılar:** [[AnthropicHandler]], [[Index]]
|
||||
|
||||
## Struct'lar
|
||||
|
||||
### Anthropic Format (Giriş/Çıkış)
|
||||
|
||||
| Struct | Alanlar |
|
||||
|---|---|
|
||||
| `AnthropicRequest` | `Model`, `MaxTokens`, `Messages []AnthropicMessage`, `System`, `Stream` |
|
||||
| `AnthropicMessage` | `Role`, `Content` |
|
||||
| `AnthropicResponse` | `ID`, `Type`, `Role`, `Content []AnthropicContent`, `Model`, `StopReason`, `StopSequence` |
|
||||
| `AnthropicContent` | `Type`, `Text` |
|
||||
|
||||
### OpenAI / Bifrost Format (Ara Katman)
|
||||
|
||||
| Struct | Alanlar |
|
||||
|---|---|
|
||||
| `BifrostRequest` | `Model`, `Messages []BifrostMessage`, `MaxTokens`, `Stream` |
|
||||
| `BifrostMessage` | `Role`, `Content` |
|
||||
| `BifrostResponse` | `ID`, `Object`, `Created`, `Model`, `Choices[{Message{Role,Content}, FinishReason, Index}]` |
|
||||
|
||||
## Dönüşüm Detayları
|
||||
|
||||
### Anthropic → OpenAI (`AnthropicToBifrost()`)
|
||||
|
||||
```
|
||||
Anthropic Request OpenAI Request
|
||||
───────────────── ─────────────
|
||||
model: "claude-3-5-sonnet" → model: "Anthropic/claude-3-5-sonnet"
|
||||
system: "Be helpful" → messages: [{role:"system", ...}, ...]
|
||||
messages: [{role, content}] → messages: [{role, content}]
|
||||
max_tokens: 1024 → max_tokens: 1024
|
||||
stream: true → stream: true
|
||||
```
|
||||
|
||||
### OpenAI → Anthropic (`BifrostToAnthropic()`)
|
||||
|
||||
```
|
||||
OpenAI Response Anthropic Response
|
||||
─────────────── ─────────────────
|
||||
id: "chatcmpl-xxx" → id: "chatcmpl-xxx"
|
||||
choices[].message.content → content: [{type:"text", text:"..."}]
|
||||
choices[].finish_reason → stop_reason: "stop"/"end_turn"
|
||||
model → model (same)
|
||||
```
|
||||
|
||||
## Model Prefix Tahmini (`guessProvider()`)
|
||||
|
||||
| Model içerir | Prefix | Örnek |
|
||||
|---|---|---|
|
||||
| `deepseek` | `DeepSeek/` | `deepseek-v4` → `DeepSeek/deepseek-v4` |
|
||||
| `gpt`, `openai` | `OpenAI/` | `gpt-4` → `OpenAI/gpt-4` |
|
||||
| `claude`, `anthropic` | `Anthropic/` | `claude-3` → `Anthropic/claude-3` |
|
||||
| `gemini` | `Google/` | `gemini-pro` → `Google/gemini-pro` |
|
||||
| diğer | `DeepSeek/` (varsayılan) | — |
|
||||
@@ -0,0 +1,95 @@
|
||||
# ClaudeCode_Setup
|
||||
|
||||
**Özet:** Claude Code CLI'ı bu LLM Gateway üzerinden kullanmak için gerekli yapılandırma. Claude Code, Anthropic Messages API formatında konuşur; gateway `/anthropic/*` endpoint'i ile bunu OpenAI formatına çevirip herhangi bir OpenAI-compatible backend'e (DeepSeek, vb.) iletir.
|
||||
|
||||
**Kütüphaneler:** Claude Code CLI, Go Fiber v3
|
||||
|
||||
**Bağlantılar:** [[AnthropicHandler]], [[BifrostTransform]], [[Anthropic Flow]], [[Config]], [[Index]]
|
||||
|
||||
## Nasıl Çalışır
|
||||
|
||||
```
|
||||
Claude Code → x-api-key → Gateway /anthropic/v1/messages → AnthropicToBifrost()
|
||||
↓
|
||||
OpenAI Chat Completions
|
||||
↓
|
||||
OPENAI_BACKEND/v1/chat/completions
|
||||
↓
|
||||
BifrostToAnthropic()
|
||||
↓
|
||||
Claude Code ← Anthropic Messages API ← Gateway ← ← ← ← ← ←
|
||||
```
|
||||
|
||||
## Claude Code Yapılandırması
|
||||
|
||||
Claude Code'u gateway'e yönlendirmek için iki yol var:
|
||||
|
||||
### 1. Environment Variable (Önerilen)
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_BASE_URL="http://localhost:8000/anthropic"
|
||||
export ANTHROPIC_API_KEY="<upstream-api-key>"
|
||||
claude
|
||||
```
|
||||
|
||||
### 2. Claude Code Settings JSON
|
||||
|
||||
`~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"anthropicBaseUrl": "http://localhost:8000/anthropic",
|
||||
"apiKey": "<upstream-api-key>"
|
||||
}
|
||||
```
|
||||
|
||||
## Gateway Konfigürasyonu (`.env`)
|
||||
|
||||
```env
|
||||
PORT=8000
|
||||
OPENAI_BACKEND=https://api.deepseek.com
|
||||
OPENAI_KEY=sk-your-deepseek-api-key
|
||||
REQUEST_TIMEOUT_SECONDS=30
|
||||
```
|
||||
|
||||
| Değişken | Değer | Açıklama |
|
||||
|---|---|---|
|
||||
| `OPENAI_BACKEND` | `https://api.deepseek.com` | Upstream LLM API (OpenAI-compatible) |
|
||||
| `OPENAI_KEY` | `sk-...` | Upstream API anahtarı (auto-inject) |
|
||||
|
||||
## Test
|
||||
|
||||
Gateway çalışırken Claude Code'un gönderdiği formatta test:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/anthropic/v1/messages" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: sk-test-key" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-d '{
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"max_tokens": 256,
|
||||
"messages": [{"role": "user", "content": "Merhaba, 2+2 nedir?"}]
|
||||
}'
|
||||
```
|
||||
|
||||
Başarılı yanıt formatı:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-xxx",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [{"type": "text", "text": "4"}],
|
||||
"model": "DeepSeek/deepseek-v4",
|
||||
"stop_reason": "end_turn"
|
||||
}
|
||||
```
|
||||
|
||||
## Önemli Notlar
|
||||
|
||||
- Claude Code `x-api-key` header'ı ile authentication yapar → Gateway bunu `Authorization: Bearer`'a çevirir
|
||||
- `anthropic-version` header'ı olduğu gibi upstream'e iletilir
|
||||
- Model adı otomatik prefix alır: `claude-sonnet-4-20250514` → `Anthropic/claude-sonnet-4-20250514`
|
||||
- **Streaming (`stream: true`) henüz desteklenmez** — response tamamlanana kadar bekler
|
||||
- Claude Code `--no-stream` flag'i ile streaming'i kapatarak kullanılabilir: `claude --no-stream`
|
||||
@@ -0,0 +1,30 @@
|
||||
# Config
|
||||
|
||||
**Özet:** Tüm runtime konfigürasyonu `.env` dosyasından okur (`config/config.go:9`). Hiçbir varsayılan IP veya port sabit kodlanmamıştır — tüm değerler `os.Getenv()` ile okunur.
|
||||
|
||||
**Kütüphaneler:** Go `os`, `strconv`
|
||||
|
||||
**Bağlantılar:** [[Main]], [[Index]]
|
||||
|
||||
## Parametreler
|
||||
|
||||
| Değişken | Varsayılan | Config Alanı | Açıklama |
|
||||
|---|---|---|---|
|
||||
| `PORT` | `8000` | `Port` | HTTP dinleme portu |
|
||||
| `OPENAI_BACKEND` | `https://api.deepseek.com` | `OpenAIBackend` | Upstream LLM backend base URL |
|
||||
| `OPENAI_KEY` | `""` | `OpenAIApiKey` | Opsiyonel API anahtarı (otomatik enjekte) |
|
||||
| `DB_MODE` | `pgs` | `DBMode` | Veritabanı motoru: `sqlite` veya `pgs` |
|
||||
| `DB_PATH` | `data/gateway.db` | `DBPath` | SQLite veritabanı dosya yolu (`DB_MODE=sqlite` iken) |
|
||||
| `LOG_LEVEL` | `info` | `LogLevel` | Konsol log seviyesi: `debug` (her şey), `info` (özet), `warn` (sadece hatalar) |
|
||||
| `POSTGRES_DSN` / `DATABASE_DSN` | `""` | `PostgresDSN` | PostgreSQL bağlantı DSN (`DB_MODE=pgs` iken) |
|
||||
| `REQUEST_TIMEOUT_SECONDS` | `30` | `RequestTimeoutSeconds` | Upstream istek zaman aşımı (saniye) |
|
||||
| `OPENAI_MODEL` | `""` | `OpenAIModel` | Varsayılan model adı (istekte model yoksa enjekte edilir) |
|
||||
|
||||
## Önemli Detaylar
|
||||
|
||||
- `DB_MODE=sqlite` → `DB_PATH` değeri SQLite dosya yolu olarak kullanılır (varsayılan: `data/gateway.db`)
|
||||
- `DB_MODE=pgs` veya tanımsız → PostgreSQL bağlantısı (mevcut davranış)
|
||||
- `POSTGRES_DSN` ve `DATABASE_DSN` çifti desteklenir (birincil: `POSTGRES_DSN`)
|
||||
- `OPENAI_KEY` tanımlanmışsa, client `Authorization` header'ı göndermediğinde otomatik enjekte edilir
|
||||
- Tüm değerler `os.Getenv()` ile okunur
|
||||
- Config struct'ı main.go'da bir kez yüklenir ve tüm handler'lara pointer olarak geçilir
|
||||
@@ -0,0 +1,60 @@
|
||||
# DockerSetup
|
||||
|
||||
**Özet:** Multi-stage Docker build ile optimize edilmiş imaj üretimi ve docker-compose ile PostgreSQL + Gateway orchestration'ı.
|
||||
|
||||
**Kütüphaneler:** Docker, docker-compose, Alpine Linux
|
||||
|
||||
**Bağlantılar:** [[Main]], [[Config]], [[Index]]
|
||||
|
||||
## Dockerfile (`docker/Dockerfile`)
|
||||
|
||||
**Stage 1 — Build:** `golang:1.24-alpine`
|
||||
- Dependency caching için önce `go.mod` + `go.sum` kopyalanır
|
||||
- `CGO_ENABLED=0` ile statik binary
|
||||
- `-ldflags="-w -s"` ile boyut optimizasyonu
|
||||
|
||||
**Stage 2 — Runtime:** `alpine:3.21`
|
||||
- Sadece binary + `ca-certificates` + `tzdata`
|
||||
- Non-root `gateway` kullanıcısı (güvenlik)
|
||||
- Port 8000 expose
|
||||
|
||||
## docker-compose (`docker/docker-compose.yml`)
|
||||
|
||||
| Servis | İmaj/Rol | Özellikler |
|
||||
|---|---|---|
|
||||
| `app` | LLM Gateway (build) | `.env`'den config, postgres'e bağımlı |
|
||||
| `postgres` | `postgres:16-alpine` | Healthcheck, persistent volume `pgdata` |
|
||||
|
||||
### Servis Detayları (app)
|
||||
|
||||
- Port mapping: `${PORT:-8000}:8000`
|
||||
- Environment: `PORT`, `OPENAI_BACKEND`, `DATABASE_DSN`, `REQUEST_TIMEOUT_SECONDS`
|
||||
- `depends_on` ile postgres healthcheck bekler
|
||||
- `restart: unless-stopped`
|
||||
|
||||
## Kullanım
|
||||
|
||||
```bash
|
||||
# Tüm stack'i başlat
|
||||
docker compose -f docker/docker-compose.yml up -d
|
||||
|
||||
# Sadece PostgreSQL (geliştirme)
|
||||
docker compose -f docker/docker-compose.yml up -d postgres
|
||||
|
||||
# Port override
|
||||
PORT=9000 docker compose -f docker/docker-compose.yml up -d
|
||||
|
||||
# Sadece imaj build
|
||||
docker build -t optoant-gateway:latest -f docker/Dockerfile .
|
||||
```
|
||||
|
||||
## Alternatif Derleme
|
||||
|
||||
```bash
|
||||
# build.sh ile production binary
|
||||
./build.sh
|
||||
# Çıktı: ./gateway
|
||||
|
||||
# Veya direkt Go build
|
||||
CGO_ENABLED=0 go build -ldflags="-w -s" -o gateway ./main.go
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# HealthHandler
|
||||
|
||||
**Özet:** `/health` endpoint'i ile servis durumu, veritabanı bağlantısı ve aktif konfigürasyon bilgilerini döndürür (`handlers/health.go:27`).
|
||||
|
||||
**Kütüphaneler:** Fiber v3, GORM
|
||||
|
||||
**Bağlantılar:** [[Main]], [[Config]], [[Index]]
|
||||
|
||||
## Yanıt Formatı
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"database": "ok" | "unreachable" | "disabled",
|
||||
"config": {
|
||||
"openai_backend": "https://api.deepseek.com",
|
||||
"port": "8000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Davranış
|
||||
|
||||
| Durum | `database` değeri |
|
||||
|---|---|
|
||||
| DB bağlantısı yok (DSN boş) | `"disabled"` |
|
||||
| DB var, ping başarılı | `"ok"` |
|
||||
| DB var, ping başarısız | `"unreachable"` |
|
||||
| Her durumda `status` | `"ok"` (servis ayakta) |
|
||||
@@ -0,0 +1,86 @@
|
||||
# LLM Gateway — Mimari Haritası
|
||||
|
||||
**Özet:** Bu proje, birden fazla LLM sağlayıcısını (OpenAI-compatible, Anthropic) tek bir gateway üzerinden proxy'leyen, istekleri dönüştüren ve loglayan bir Go hizmetidir. Go 1.26 + Fiber v3 + GORM + PostgreSQL / SQLite (`DB_MODE` ile seçimli) teknolojileriyle inşa edilmiştir.
|
||||
|
||||
**Kütüphaneler:** Go 1.26, Fiber v3, GORM, PostgreSQL, SQLite, godotenv, swaggo
|
||||
|
||||
**Bağlantılar:** [[Main]], [[Config]], [[HealthHandler]], [[OpenAIHandler]], [[AnthropicHandler]], [[ProxyEngine]], [[BifrostTransform]], [[RequestLog]], [[SwaggerUI]], [[Testing]], [[DockerSetup]], [[OpenAI_Flow]], [[Anthropic_Flow]], [[ClaudeCode_Setup]]
|
||||
|
||||
---
|
||||
|
||||
## Katman Mimarisi
|
||||
|
||||
| Katman | Dosya | Açıklama |
|
||||
|--------|-------|----------|
|
||||
| **Giriş** | `main.go:38` | Fiber v3 app başlatma, DB bağlantısı, route tanımları |
|
||||
| **Konfigürasyon** | `config/config.go:9` | `.env` tabanlı runtime config (port, backend, DSN) |
|
||||
| **Handler** | `handlers/*.go` | HTTP handler fonksiyonları (health, openai, anthropic) |
|
||||
| **Proxy** | `internal/proxy/proxy.go:14` | Generic HTTP forward motoru |
|
||||
| **Transform** | `internal/transform/anthropic_bifrost.go:10` | Anthropic ↔ OpenAI format dönüştürücü |
|
||||
| **Model** | `models/request_log.go:13` | GORM veritabanı modeli |
|
||||
|
||||
## API Endpoint'leri
|
||||
|
||||
| Method | Path | Handler | Açıklama |
|
||||
|--------|------|---------|----------|
|
||||
| `GET` | `/health` | [[HealthHandler]] | Servis durumu, DB sağlığı, config bilgisi |
|
||||
| `ALL` | `/v1/*` | [[OpenAIHandler]] | OpenAI-compatible direct passthrough proxy |
|
||||
| `ALL` | `/anthropic`, `/anthropic/*` | [[AnthropicHandler]] | Anthropic Messages API → OpenAI dönüşüm proxy |
|
||||
| `GET` | `/swagger`, `/swagger/*` | Inline Fiber | CDN'den yüklenen Swagger UI |
|
||||
|
||||
## Veri Akışları
|
||||
|
||||
- [[OpenAI Flow]] — `/v1/*` isteklerinin upstream'e direkt iletilmesi
|
||||
- [[Anthropic Flow]] — `/anthropic/*` isteklerinin Bifrost dönüşümüyle upstream'e iletilmesi
|
||||
|
||||
## Claude Code Entegrasyonu
|
||||
|
||||
- [[ClaudeCode_Setup]] — Claude Code CLI'ı gateway üzerinden kullanma
|
||||
|
||||
## Altyapı ve Geliştirme
|
||||
|
||||
- [[DockerSetup]] — Multi-stage Docker build + docker-compose (app + postgres)
|
||||
- [[SwaggerUI]] — `/swagger/` adresinde OpenAPI dokümantasyonu
|
||||
- [[Testing]] — `httptest.Server` ile mock upstream testleri
|
||||
- `.air.toml` — Air hot-reload (geliştirme)
|
||||
- `build.sh` — Production binary derleme scripti
|
||||
- [[RequestLog]] — GORM + PostgreSQL/SQLite istek loglama
|
||||
|
||||
## Veritabanı Şeması
|
||||
|
||||
| Tablo | Model | Amaç |
|
||||
|-------|-------|------|
|
||||
| `request_logs` | [[RequestLog]] | Tüm proxy isteklerinin log kaydı |
|
||||
|
||||
## Dönüşüm Katmanı
|
||||
|
||||
| Yön | Fonksiyon | Açıklama |
|
||||
|-----|-----------|----------|
|
||||
| Anthropic → OpenAI | `AnthropicToBifrost()` | Mesaj API → Chat Completions dönüşümü |
|
||||
| OpenAI → Anthropic | `BifrostToAnthropic()` | Chat Completions → Mesaj API dönüşümü |
|
||||
|
||||
Desteklenen model prefix'leri: `DeepSeek/`, `OpenAI/`, `Anthropic/`, `Google/`
|
||||
|
||||
## Proje Haritası
|
||||
|
||||
```
|
||||
.
|
||||
├── main.go # Giriş noktası
|
||||
├── config/config.go # Konfigürasyon
|
||||
├── models/request_log.go # Veritabanı modeli
|
||||
├── handlers/
|
||||
│ ├── health.go # Health check
|
||||
│ ├── openai.go # OpenAI proxy
|
||||
│ └── anthropic.go # Anthropic proxy
|
||||
├── internal/
|
||||
│ ├── proxy/proxy.go # Forward motoru
|
||||
│ └── transform/anthropic_bifrost.go # Format dönüştürücü
|
||||
├── tests/openai_test.go # Unit testler
|
||||
├── docker/
|
||||
│ ├── Dockerfile # Multi-stage build
|
||||
│ └── docker-compose.yml # Orchestration
|
||||
├── build.sh # Derleme scripti
|
||||
└── docs/
|
||||
├── swagger.json/yaml # Swagger spec
|
||||
└── wiki/ # Bu wiki (Obsidian Knowledge Graph)
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
# Main
|
||||
|
||||
**Özet:** Uygulamanın giriş noktası (`main.go:38`). Fiber v3 web framework'ünü başlatır, `DB_MODE`'a göre PostgreSQL veya SQLite bağlantısını kurar, AutoMigrate çalıştırır ve tüm route'ları tanımlar.
|
||||
|
||||
**Kütüphaneler:** Fiber v3, GORM, PostgreSQL, SQLite, godotenv
|
||||
|
||||
**Bağlantılar:** [[Index]], [[Config]], [[RequestLog]], [[OpenAIHandler]], [[AnthropicHandler]], [[HealthHandler]], [[SwaggerUI]], [[DockerSetup]]
|
||||
|
||||
## Başlangıç Adımları
|
||||
|
||||
1. `.env` dosyası yüklenir (`godotenv.Load()`)
|
||||
2. [[Config]] okunur (`config.Load()`)
|
||||
3. `DB_MODE` kontrol edilir — PostgreSQL DSN veya SQLite bağlantısı + AutoMigrate (`RequestLog`)
|
||||
4. Fiber v3 app oluşturulur (`fiber.Config{AppName: "LLM Gateway v1.0", ServerHeader: "optoant-gateway"}`)
|
||||
5. Route'lar tanımlanır:
|
||||
- `GET /health` → [[HealthHandler]]
|
||||
- `GET /swagger/swagger.json` → statik dosya serve
|
||||
- `GET /swagger` → `/swagger/` redirect
|
||||
- `GET /swagger/*` → Swagger UI HTML (CDN)
|
||||
- `ALL /v1/*` → [[OpenAIHandler]] (direct passthrough)
|
||||
- `ALL /anthropic`, `/anthropic/*` → [[AnthropicHandler]] (Bifrost dönüşümü)
|
||||
6. `app.Listen(addr)` ile server başlatılır
|
||||
|
||||
## Önemli Detaylar
|
||||
|
||||
- DB bağlantısı başarısız olursa uygulama **devam eder** (DB'siz çalışabilir)
|
||||
- Tüm backend istekleri `.env`'deki `OPENAI_BACKEND` adresine gider
|
||||
- OpenAI endpoint'i direkt passthrough yaparken, Anthropic endpoint'i format dönüşümü yapar
|
||||
- Swagger UI **CDN'den** yüklenir (offline çalışmaz)
|
||||
- API key auto-injection: `OPENAI_KEY` env'i tanımlıysa, client Authorization göndermezse otomatik eklenir
|
||||
|
||||
## Geliştirme
|
||||
|
||||
- **Air hot-reload:** `.air.toml` ile canlı yeniden derleme (`air`)
|
||||
- **Production build:** `build.sh` ile `CGO_ENABLED=0` optimize binary
|
||||
- **Docker:** `docker/Dockerfile` multi-stage + `docker-compose.yml`
|
||||
@@ -0,0 +1,36 @@
|
||||
# OpenAIHandler
|
||||
|
||||
**Özet:** `/v1/*` route'unu karşılar (`handlers/openai.go:29`), gelen istekleri olduğu gibi `OPENAI_BACKEND` adresine iletir. OpenAI-compatible bir proxy'dir.
|
||||
|
||||
**Kütüphaneler:** Fiber v3, GORM, Go `net/http`
|
||||
|
||||
**Bağlantılar:** [[Main]], [[ProxyEngine]], [[RequestLog]], [[Config]], [[OpenAI Flow]]
|
||||
|
||||
## Davranış
|
||||
|
||||
1. Gelen isteği alır, header'ları `net/http.Header` formatına çevirir (`fiberToHTTPHeaders()`)
|
||||
2. Eğer `OPENAI_KEY` tanımlanmışsa ve client Authorization göndermemişse, otomatik ekler
|
||||
3. [[ProxyEngine]] ile isteği `OPENAI_BACKEND + originalURL` adresine forward eder
|
||||
4. Response header'larını kopyalar (`copyResponseHeaders()` — Transfer-Encoding, Content-Length, Connection atlanır)
|
||||
5. Yanıtı olduğu gibi client'a döndürür
|
||||
6. [[RequestLog]]'a fire-and-forget goroutine ile kayıt ekler
|
||||
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
ANY /v1/chat/completions
|
||||
ANY /v1/models
|
||||
Authorization: Bearer <api-key>
|
||||
```
|
||||
|
||||
## Yardımcı Fonksiyonlar
|
||||
|
||||
| Fonksiyon | Açıklama |
|
||||
|---|---|
|
||||
| `fiberToHTTPHeaders(c fiber.Ctx) http.Header` | Fiber header'larını `net/http` formatına çevirir |
|
||||
| `copyResponseHeaders(c fiber.Ctx, headers http.Header)` | Upstream response header'larını Fiber response'a kopyalar (hop-by-hop atlanır) |
|
||||
|
||||
## Hata Durumu
|
||||
|
||||
- Upstream hatasında → `502 Bad Gateway` + hata mesajı
|
||||
- Loglama hatası → ana isteği etkilemez (fire-and-forget)
|
||||
@@ -0,0 +1,38 @@
|
||||
# OpenAI Flow
|
||||
|
||||
**Özet:** OpenAI-compatible isteklerin gateway üzerinden direkt passthrough akışı.
|
||||
|
||||
**Kütüphaneler:** Fiber v3, Go `net/http`
|
||||
|
||||
**Bağlantılar:** [[OpenAIHandler]], [[ProxyEngine]], [[RequestLog]], [[Index]]
|
||||
|
||||
## Akış Diyagramı
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Gateway
|
||||
participant Proxy
|
||||
participant Upstream
|
||||
participant DB
|
||||
|
||||
Client->>Gateway: POST /v1/chat/completions
|
||||
Note over Gateway: OpenAIHandler
|
||||
Gateway->>Gateway: fiberToHTTPHeaders()
|
||||
Gateway->>Gateway: API key auto-inject (gerekirse)
|
||||
Gateway->>Gateway: copyResponseHeaders()
|
||||
Gateway->>Proxy: Forward(method, targetURL, headers, body, timeout)
|
||||
Proxy->>Upstream: POST <OPENAI_BACKEND>/v1/chat/completions
|
||||
Upstream-->>Proxy: 200 OK + response body
|
||||
Proxy-->>Gateway: ForwardResult{StatusCode, Body, Headers}
|
||||
Gateway->>Gateway: copyResponseHeaders() (skip hop-by-hop)
|
||||
Gateway->>DB: go db.Create(logEntry) — fire-and-forget
|
||||
Gateway-->>Client: 200 OK + original response body
|
||||
```
|
||||
|
||||
## Önemli Noktalar
|
||||
|
||||
- İstek body'si **olduğu gibi** iletilir (dönüşüm yok)
|
||||
- Response header'ları kopyalanırken `Transfer-Encoding`, `Content-Length`, `Connection` atlanır
|
||||
- Upstream hatasında client `502 Bad Gateway` alır
|
||||
- DB loglama bir goroutine içinde çalışır, asla ana isteği bloke etmez
|
||||
@@ -0,0 +1,41 @@
|
||||
# ProxyEngine
|
||||
|
||||
**Özet:** HTTP isteklerini upstream backend'e ileten merkezi proxy motoru (`internal/proxy/proxy.go:24`). Tüm header'ları korur, hassas header'ları log'da maskeler, hop-by-hop header'ları otomatik atlar.
|
||||
|
||||
**Kütüphaneler:** Go `net/http`, `context`, `io`
|
||||
|
||||
**Bağlantılar:** [[OpenAIHandler]], [[AnthropicHandler]], [[Index]]
|
||||
|
||||
## Struct
|
||||
|
||||
```go
|
||||
type ForwardResult struct {
|
||||
StatusCode int
|
||||
Body []byte
|
||||
Headers http.Header
|
||||
}
|
||||
```
|
||||
|
||||
## Fonksiyonlar
|
||||
|
||||
### `Forward(ctx, method, targetURL, headers, body, timeoutSec) *ForwardResult`
|
||||
- HTTP isteğini hedef URL'ye iletir
|
||||
- Context üzerinden timeout yönetimi (`time.Duration(timeoutSec) * time.Second`)
|
||||
- Hop-by-hop header'ları atlar: `Connection`, `TE`, `Trailers`, `Transfer-Encoding`, `Upgrade`
|
||||
- Body'yi log için okur, sonra tekrar sarar
|
||||
- `ForwardResult{StatusCode, Body, Headers}` döndürür
|
||||
|
||||
### `MaskSensitiveHeaders(headers) http.Header`
|
||||
- Header'ları clone'lar
|
||||
- `Authorization` → `Bearer [REDACTED]`
|
||||
- `X-Api-Key` → `[REDACTED]`
|
||||
- Sadece loglama amaçlı kullanılır; upstream'e orijinal header gider
|
||||
|
||||
### `truncateForLog(s, maxLen) string`
|
||||
- Uzun string'leri `maxLen` (2000) karakterde kırpar, `...[TRUNCATED]` ekler
|
||||
|
||||
## Güvenlik
|
||||
|
||||
- Hassas header'lar upstream'e **olduğu gibi** iletilir ama **log'da maskelenir**
|
||||
- Hop-by-hop header'lar otomatik atlanır (proxy zincirini bozmamak için)
|
||||
- Sensitive header masking sadece log çıktısı içindir
|
||||
@@ -0,0 +1,33 @@
|
||||
# RequestLog
|
||||
|
||||
**Özet:** Her proxy'lenen isteğin kaydını tutan GORM modeli (`models/request_log.go:13`). Hassas header'lar (Authorization) asla düz metin olarak saklanmaz. Loglama fire-and-forget goroutine ile yapılır.
|
||||
|
||||
**Kütüphaneler:** GORM, PostgreSQL, Go `time`
|
||||
|
||||
**Bağlantılar:** [[Main]], [[OpenAIHandler]], [[AnthropicHandler]], [[Index]]
|
||||
|
||||
## Tablo: `request_logs`
|
||||
|
||||
| Column | GORM Tipi | JSON | Açıklama |
|
||||
|--------|-----------|------|----------|
|
||||
| `id` | `uint` (PK) | `id` | Otomatik artan |
|
||||
| `created_at` | `time.Time` | `created_at` | Oluşturulma zamanı |
|
||||
| `updated_at` | `time.Time` | `updated_at` | Güncellenme zamanı |
|
||||
| `deleted_at` | `gorm.DeletedAt` | (gizli) | Soft delete index |
|
||||
| `endpoint` | `string(512)` | `endpoint` | İstek yolu |
|
||||
| `method` | `string(16)` | `method` | HTTP metodu |
|
||||
| `client_ip` | `string(64)` | `client_ip` | İstemci IP |
|
||||
| `request_body` | `text` | `request_body` | Body (2000 char limit) |
|
||||
| `response_status` | `int` | `response_status` | HTTP yanıt kodu |
|
||||
| `latency_ms` | `int64` | `latency_ms` | İşlem süresi (ms) |
|
||||
|
||||
## Sabitler
|
||||
|
||||
- `maxBodyLength = 2000` — `TruncateBody()` ile body bu limite kırpılır
|
||||
|
||||
## Önemli Detaylar
|
||||
|
||||
- `TruncateBody()` body'yi 2000 karakterde kırpar, sonuna `…[truncated]` ekler
|
||||
- Loglama **fire-and-forget** (`go db.Create()`): loglama hatası ana isteği etkilemez
|
||||
- Hassas header'lar (Authorization, X-Api-Key) veritabanına yazılmaz
|
||||
- DB bağlantısı yoksa loglama tamamen atlanır
|
||||
@@ -0,0 +1,40 @@
|
||||
# SwaggerUI
|
||||
|
||||
**Özet:** Fiber v3 native Swagger UI entegrasyonu (`main.go:79`). OpenAPI spec `docs/swagger.json` dosyasından statik olarak servis edilir, UI CDN'den (unpkg) yüklenir.
|
||||
|
||||
**Kütüphaneler:** swaggo/swag, Swagger UI 5.x (CDN)
|
||||
|
||||
**Bağlantılar:** [[Main]], [[Index]]
|
||||
|
||||
## Route'lar
|
||||
|
||||
| Path | Açıklama |
|
||||
|---|---|
|
||||
| `GET /swagger` | `/swagger/`'a redirect |
|
||||
| `GET /swagger/` | Swagger UI HTML (CDN) |
|
||||
| `GET /swagger/swagger.json` | Statik OpenAPI spec dosyası |
|
||||
|
||||
## Kullanım
|
||||
|
||||
```bash
|
||||
# Swagger spec'i yenile
|
||||
swag init
|
||||
|
||||
# Swagger UI'a tarayıcıdan eriş
|
||||
open http://localhost:8000/swagger/
|
||||
```
|
||||
|
||||
## Swagger Annotation'ları
|
||||
|
||||
- `main.go:1-19` — Genel API bilgisi (title, version, host, security)
|
||||
- `handlers/health.go:21-27` — `/health` endpoint
|
||||
- `handlers/openai.go:20-28` — `/v1/{path}` endpoint
|
||||
- `handlers/anthropic.go:65-73` — `/anthropic/{path}` endpoint
|
||||
|
||||
## Önemli Detaylar
|
||||
|
||||
- Swagger UI **unpkg CDN'inden** yüklenir (offline çalışmaz)
|
||||
- Spec statik dosyadır: `docs/swagger.json`
|
||||
- `docs/swagger.yaml` da mevcuttur (aynı spec)
|
||||
- `swag init` her endpoint/değişiklik sonrası çalıştırılmalıdır
|
||||
- API key: `Authorization: Bearer` (Swagger UI'da "Authorize" butonu)
|
||||
@@ -0,0 +1,71 @@
|
||||
# Testing
|
||||
|
||||
**Özet:** Fiber v3 test framework'ü ile mock upstream sunucuları kullanarak proxy mantığının test edilmesi. Transform katmanı unit testleri `internal/transform`, entegrasyon testleri `tests/` paketinde.
|
||||
|
||||
**Kütüphaneler:** Go `testing`, `net/http/httptest`, Fiber v3 test (`app.Test()`), `encoding/json`
|
||||
|
||||
**Bağlantılar:** [[OpenAIHandler]], [[AnthropicHandler]], [[BifrostTransform]], [[ProxyEngine]], [[Index]]
|
||||
|
||||
## Test Stratejisi
|
||||
|
||||
- `mockUpstream(t, status, body)` — `httptest.Server` ile sabit yanıt dönen test sunucusu
|
||||
- `newTestApp(cfg)` — OpenAI handler bağlı Fiber test uygulaması (DB'siz)
|
||||
- `newAnthropicTestApp(cfg)` — Anthropic handler bağlı Fiber test uygulaması (DB'siz)
|
||||
- Gerçek upstream'e ihtiyaç yok, mock sunucu kullanılır
|
||||
- `app.Test(req, fiber.TestConfig{Timeout: -1})` ile Fiber native HTTP test
|
||||
|
||||
## Transform Unit Testleri (`internal/transform`)
|
||||
|
||||
### Anthropic → OpenAI (`AnthropicToBifrost`)
|
||||
|
||||
| Test | Açıklama |
|
||||
|---|---|
|
||||
| `TestAnthropicToBifrost_Basic` | Temel dönüşüm, model prefix + max_tokens + messages |
|
||||
| `TestAnthropicToBifrost_WithSystem` | `system` alanının `messages[0]`'a eklenmesi |
|
||||
| `TestAnthropicToBifrost_ModelPrefix` | 6 model için prefix tahmini (claude→Anthropic/, deepseek→DeepSeek/, vb.) |
|
||||
| `TestAnthropicToBifrost_StreamPassthrough` | `stream: true` korunur |
|
||||
| `TestAnthropicToBifrost_InvalidJSON` | Geçersiz JSON → hata döner |
|
||||
|
||||
### OpenAI → Anthropic (`BifrostToAnthropic`)
|
||||
|
||||
| Test | Açıklama |
|
||||
|---|---|
|
||||
| `TestBifrostToAnthropic_Basic` | Temel dönüşüm, content/text/stop_reason kontrolü |
|
||||
| `TestBifrostToAnthropic_EmptyFinishReason` | Boş `finish_reason` → `end_turn` |
|
||||
| `TestBifrostToAnthropic_MultipleChoices` | 2 choice'ın 2 content block'a dönüşümü |
|
||||
| `TestBifrostToAnthropic_InvalidJSON` | Geçersiz JSON → hata döner |
|
||||
|
||||
## Entegrasyon Testleri (`tests/anthropic_test.go`, `tests/openai_test.go`)
|
||||
|
||||
| Test | Açıklama |
|
||||
|---|---|
|
||||
| `TestAnthropicProxy_Success` | Full Bifrost döngüsü: Anthropic→OpenAI→forward→OpenAI→Anthropic |
|
||||
| `TestAnthropicProxy_InvalidFormatPassthrough` | Geçersiz Anthropic formatı → raw passthrough |
|
||||
| `TestAnthropicProxy_UpstreamError` | Upstream kapalı → 502 Bad Gateway |
|
||||
| `TestAnthropicProxy_ModelsList` | `GET /anthropic/v1/models` → DeepSeek model listesi |
|
||||
| `TestAnthropicProxy_EmptyBody` | Boş body → varsayılan "ready" yanıtı |
|
||||
| `TestAnthropicProxy_HEAD` | `HEAD` → 404 |
|
||||
| `TestAnthropicProxy_xApiKeyConversion` | `x-api-key` header'ının `Authorization: Bearer`'a dönüşümü |
|
||||
| `TestAnthropicProxy_DefaultModelInjection` | İstekte model yoksa `OPENAI_MODEL` enjekte edilir (zaten "/" var, prefix eklenmez) |
|
||||
| `TestAnthropicProxy_DefaultModelInjection_NopWhenModelExists` | İstekte model varsa `OPENAI_MODEL` override etmez |
|
||||
| `TestAnthropicProxy_TransformErrorFallsbackToPassthrough` | Upstream yanıtı bozuksa raw passthrough |
|
||||
| `TestOpenAIProxy_Success` | OpenAI handler başarılı proxy |
|
||||
| `TestOpenAIProxy_DefaultModelInjection` | OpenAI handler'da model yoksa enjekte |
|
||||
| `TestOpenAIProxy_DefaultModelInjection_ExistingModel` | OpenAI handler'da var olan model korunur |
|
||||
| `TestOpenAIProxy_UpstreamError` | OpenAI handler upstream hatasında 502 |
|
||||
|
||||
## Çalıştırma
|
||||
|
||||
```bash
|
||||
# Tüm testler
|
||||
go test ./tests/ -v
|
||||
|
||||
# Sadece transform unit testleri
|
||||
go test ./internal/transform/ -v
|
||||
```
|
||||
|
||||
## Notlar
|
||||
|
||||
- Tüm testler DB'siz çalışır (`db = nil`) — loglama katmanı test edilmez
|
||||
- Fiber v3'ün `app.Test()` metodu ile entegre test
|
||||
- Mock upstream her test için ayrı `httptest.Server` oluşturur (izolasyon)
|
||||
Reference in New Issue
Block a user