76ba500417
CI / docker (push) Successful in 2m4s
OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress, ingress key governance, monitoring, and security controls. Co-authored-by: Cursor <cursoragent@cursor.com>
101 lines
2.9 KiB
Go
101 lines
2.9 KiB
Go
package proxy
|
|
|
|
import (
|
|
"bufio"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/rose_cat707/luminary/internal/gateway"
|
|
"github.com/rose_cat707/luminary/internal/middleware"
|
|
"github.com/rose_cat707/luminary/internal/model"
|
|
"github.com/rose_cat707/luminary/internal/provider"
|
|
)
|
|
|
|
type Handler struct {
|
|
gateway *gateway.Service
|
|
}
|
|
|
|
func New(gw *gateway.Service) *Handler {
|
|
return &Handler{gateway: gw}
|
|
}
|
|
|
|
func (h *Handler) Register(r *gin.RouterGroup) {
|
|
r.GET("/models", h.ListModels)
|
|
r.POST("/chat/completions", h.forward(provider.EndpointChatCompletion))
|
|
r.POST("/completions", h.forward(provider.EndpointTextCompletion))
|
|
r.POST("/responses", h.forward(provider.EndpointResponses))
|
|
r.POST("/embeddings", h.forward(provider.EndpointEmbeddings))
|
|
r.POST("/rerank", h.forward(provider.EndpointRerank))
|
|
}
|
|
|
|
func (h *Handler) ingressKey(c *gin.Context) (*model.IngressKey, bool) {
|
|
v, ok := c.Get("ingress_key")
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
k, ok := v.(*model.IngressKey)
|
|
return k, ok
|
|
}
|
|
|
|
func (h *Handler) ListModels(c *gin.Context) {
|
|
ingress, ok := h.ingressKey(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
models, err := h.gateway.ListModels(c.Request.Context(), ingress)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"object": "list", "data": models})
|
|
}
|
|
|
|
func (h *Handler) forward(endpoint provider.EndpointType) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
ingress, ok := h.ingressKey(c)
|
|
if !ok {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
return
|
|
}
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
|
|
return
|
|
}
|
|
if provider.IsStreamRequest(body) && provider.SupportsStreaming(endpoint) {
|
|
h.handleStream(c, ingress, endpoint, body)
|
|
return
|
|
}
|
|
clientIP := middleware.ClientIP(c)
|
|
resp, err := h.gateway.ForwardEndpoint(c.Request.Context(), ingress, endpoint, body, clientIP)
|
|
if err != nil {
|
|
c.JSON(gatewayHTTPStatus(err), gin.H{"error": gin.H{"message": err.Error(), "type": "gateway_error"}})
|
|
return
|
|
}
|
|
c.Data(resp.StatusCode, "application/json", resp.Body)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) handleStream(c *gin.Context, ingress *model.IngressKey, endpoint provider.EndpointType, body []byte) {
|
|
c.Header("Content-Type", "text/event-stream")
|
|
c.Header("Cache-Control", "no-cache")
|
|
c.Header("Connection", "keep-alive")
|
|
c.Status(http.StatusOK)
|
|
|
|
flusher, ok := c.Writer.(http.Flusher)
|
|
if !ok {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "streaming not supported"})
|
|
return
|
|
}
|
|
w := bufio.NewWriter(c.Writer)
|
|
clientIP := middleware.ClientIP(c)
|
|
err := h.gateway.ForwardStream(c.Request.Context(), ingress, endpoint, body, clientIP, w, flusher)
|
|
w.Flush()
|
|
if err != nil {
|
|
_, _ = c.Writer.Write([]byte("data: {\"error\":\"" + err.Error() + "\"}\n\n"))
|
|
flusher.Flush()
|
|
}
|
|
}
|