first commit

This commit is contained in:
Beyhan Ogur
2026-05-11 15:08:50 +03:00
commit a408821410
47 changed files with 4670 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
package handlers
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/gofiber/fiber/v3"
"gorm.io/gorm"
"optoant/config"
"optoant/internal/logger"
"optoant/internal/proxy"
"optoant/models"
)
// OpenAIHandler handles all /v1/* requests and forwards them to OPENAI_BACKEND.
// @Summary OpenAI-compatible proxy
// @Description Forwards any /v1/* request to the configured OpenAI-compatible backend (e.g. DeepSeek).
// @Tags openai
// @Accept json
// @Produce json
// @Param path path string true "OpenAI API path (e.g. chat/completions)"
// @Success 200 {object} map[string]interface{}
// @Failure 502 {object} map[string]string
// @Router /v1/{path} [post]
func OpenAIHandler(cfg *config.Config, db *gorm.DB) fiber.Handler {
return func(c fiber.Ctx) error {
start := time.Now()
// Build target URL: OPENAI_BACKEND + full original path
targetURL := strings.TrimRight(cfg.OpenAIBackend, "/") + c.OriginalURL()
// Read body
bodyBytes := c.Body()
// Inject default model from config if request body has no model field
if cfg.OpenAIModel != "" {
bodyBytes = injectDefaultModel(bodyBytes, cfg.OpenAIModel)
}
// Convert fiber headers to net/http.Header
reqHeaders := fiberToHTTPHeaders(c)
// Auto-inject API key from .env if client didn't provide one
if cfg.OpenAIApiKey != "" && reqHeaders.Get("Authorization") == "" {
reqHeaders.Set("Authorization", "Bearer "+cfg.OpenAIApiKey)
logger.Debug("│ 🔑 Auto-injected API key from OPENAI_KEY env")
}
// ── LOG: Incoming OpenAI Request ──
logger.Info("┌─ [OPENAI] >>> %s %s | IP: %s", c.Method(), c.OriginalURL(), c.IP())
logger.Debug("│ Headers: %v", proxy.MaskSensitiveHeaders(reqHeaders))
logger.Debug("│ Body: %s", string(bodyBytes))
logger.Debug("│ 🚀 Forwarding to: %s", targetURL)
result, err := proxy.Forward(
c.Context(),
c.Method(),
targetURL,
reqHeaders,
bytes.NewReader(bodyBytes),
cfg.RequestTimeoutSeconds,
)
latency := time.Since(start).Milliseconds()
status := http.StatusBadGateway
if err == nil {
status = result.StatusCode
}
// Log to DB (fire-and-forget style; don't fail the request if logging fails)
if db != nil {
logEntry := &models.RequestLog{
Endpoint: c.OriginalURL(),
Method: c.Method(),
ClientIP: c.IP(),
RequestBody: models.TruncateBody(string(bodyBytes)),
ResponseStatus: status,
LatencyMs: latency,
}
go db.Create(logEntry) //nolint:errcheck
}
if err != nil {
logger.Warn("│ ❌ UPSTREAM ERROR: %v", err)
logger.Warn("└─ [OPENAI] <<< 502 (%dms)", latency)
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{
"error": fmt.Sprintf("upstream error: %v", err),
})
}
// ── LOG: Upstream Response ──
logger.Debug("│ <--- UPSTREAM RESPONSE (%d bytes, status %d):", len(result.Body), result.StatusCode)
logger.Debug("│ %s", string(result.Body))
logger.Info("└─ [OPENAI] <<< %d (%dms)", result.StatusCode, latency)
// Copy response headers
copyResponseHeaders(c, result.Headers)
return c.Status(result.StatusCode).Send(result.Body)
}
}
// fiberToHTTPHeaders converts Fiber request headers to stdlib net/http.Header.
func fiberToHTTPHeaders(c fiber.Ctx) http.Header {
h := make(http.Header)
c.Request().Header.VisitAll(func(k, v []byte) {
h.Add(string(k), string(v))
})
return h
}
// copyResponseHeaders writes upstream response headers back to the Fiber response.
func copyResponseHeaders(c fiber.Ctx, headers http.Header) {
skipHeaders := map[string]bool{
"Transfer-Encoding": true,
"Content-Length": true,
"Connection": true,
}
for key, vals := range headers {
if skipHeaders[key] {
continue
}
for _, v := range vals {
c.Set(key, v)
}
}
}
// injectDefaultModel forces the model field in a JSON body to the configured
// default model. This ensures Claude Code's model selection is overridden by
// the upstream model from .env (OPENAI_MODEL).
func injectDefaultModel(body []byte, defaultModel string) []byte {
if defaultModel == "" {
return body
}
var req map[string]interface{}
if err := json.Unmarshal(body, &req); err != nil {
return body
}
if req["model"] == defaultModel {
return body
}
oldModel := req["model"]
req["model"] = defaultModel
modified, err := json.Marshal(req)
if err != nil {
return body
}
logger.Info("│ 🔧 Model override: %v → %s", oldModel, defaultModel)
return modified
}