2026-08-19 20:52:13 -07:00
|
|
|
// Package proxy provides request routing and forwarding.
|
|
|
|
|
package proxy
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
|
|
|
|
|
"github.com/Riotpiaole/homelab-frontend/internal/config"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// RouteRequest determines which upstream should handle the request.
|
2026-08-19 21:48:11 -07:00
|
|
|
// For /v1/chat/completions, it uses body-based dispatch (reads JSON to find "model" field).
|
|
|
|
|
// For other routes, it looks up by path prefix.
|
2026-08-19 20:52:13 -07:00
|
|
|
func (h *Handler) RouteRequest(r *http.Request) (*Route, error) {
|
|
|
|
|
// For /v1/chat/completions, use body-based dispatch
|
|
|
|
|
if r.URL.Path == "/v1/chat/completions" && r.Method == "POST" {
|
|
|
|
|
return h.routeByModel(r)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-19 21:48:11 -07:00
|
|
|
// For other paths, try to find a matching route by path
|
|
|
|
|
// Look for exact path match or path prefix match
|
|
|
|
|
for _, route := range h.routes {
|
|
|
|
|
// Check if route's pathRewrite matches request path
|
|
|
|
|
if route.Upstream.PathRewrite != "" && route.Upstream.PathRewrite == r.URL.Path {
|
|
|
|
|
return route, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no routes configured, return error
|
|
|
|
|
if len(h.routes) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("no routes configured")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return the first configured route as fallback
|
2026-08-19 20:52:13 -07:00
|
|
|
for _, route := range h.routes {
|
|
|
|
|
return route, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("no route available")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// routeByModel reads the request body to find the "model" field and routes accordingly.
|
|
|
|
|
// The body is preserved for forwarding to the upstream.
|
|
|
|
|
func (h *Handler) routeByModel(r *http.Request) (*Route, error) {
|
|
|
|
|
// If there's no body, we can't determine the model
|
|
|
|
|
if r.Body == nil {
|
|
|
|
|
return nil, fmt.Errorf("request body required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read the body to extract the model name
|
|
|
|
|
// We need to be careful to preserve the body for the upstream
|
|
|
|
|
bodyBytes, err := io.ReadAll(r.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to read request body: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Restore the body so it can be read again by the upstream
|
|
|
|
|
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
|
|
|
|
|
|
|
|
|
// Parse the JSON to find the model field
|
|
|
|
|
var payload map[string]interface{}
|
|
|
|
|
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid JSON in request body: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract the model name
|
|
|
|
|
modelName, ok := payload["model"].(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("model field missing or not a string")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Look up the model in the registry
|
|
|
|
|
modelUpstream := h.config.LookupModel(modelName)
|
|
|
|
|
if modelUpstream == nil {
|
|
|
|
|
return nil, fmt.Errorf("unknown model: %q", modelName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a route for this model with appropriate timeouts
|
|
|
|
|
// These are sensible defaults for LLM models
|
|
|
|
|
upstreamCfg := &config.Upstream{
|
|
|
|
|
Address: modelUpstream.Address,
|
|
|
|
|
PathRewrite: "/v1/chat/completions",
|
|
|
|
|
ConnectTimeout: h.defaultConnectTimeout,
|
|
|
|
|
ReadTimeout: h.defaultReadTimeout,
|
|
|
|
|
WriteTimeout: h.defaultWriteTimeout,
|
|
|
|
|
MaxBodySize: h.defaultMaxBodySize,
|
|
|
|
|
AuthRequired: false,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetURL, _ := url.Parse("http://" + modelUpstream.Address)
|
|
|
|
|
|
|
|
|
|
route := &Route{
|
|
|
|
|
Name: "v1-chat-" + modelName,
|
|
|
|
|
Upstream: upstreamCfg,
|
|
|
|
|
Transport: h.getOrCreateTransport(modelUpstream.Address, upstreamCfg),
|
|
|
|
|
Director: func(req *http.Request) {
|
|
|
|
|
directorFunc(req, targetURL, upstreamCfg)
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return route, nil
|
|
|
|
|
}
|