170 lines
4.5 KiB
Go
170 lines
4.5 KiB
Go
package proxy
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/config"
|
||
|
|
)
|
||
|
|
|
||
|
|
// TestSystemoneRoutesByModel verifies POST /v1/systemone dispatches on the body
|
||
|
|
// "model" field instead of falling through to path-based route lookup. Before
|
||
|
|
// that path joined body dispatch, the request reached the route table and
|
||
|
|
// failed with "no routes configured" when no routes were defined.
|
||
|
|
func TestSystemoneRoutesByModel(t *testing.T) {
|
||
|
|
clefCalled := false
|
||
|
|
clefServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
clefCalled = true
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
fmt.Fprint(w, `{"decision":"allow"}`)
|
||
|
|
}))
|
||
|
|
defer clefServer.Close()
|
||
|
|
|
||
|
|
clefAddr := strings.TrimPrefix(clefServer.URL, "http://")
|
||
|
|
|
||
|
|
cfg := &config.Config{
|
||
|
|
Models: map[string]*config.ModelUpstream{
|
||
|
|
"clef-flash": {
|
||
|
|
Name: "clef-flash",
|
||
|
|
Address: clefAddr,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
Routes: make(map[string]*config.Route),
|
||
|
|
}
|
||
|
|
|
||
|
|
handler := New(cfg)
|
||
|
|
defer handler.Close()
|
||
|
|
|
||
|
|
server := httptest.NewServer(handler)
|
||
|
|
defer server.Close()
|
||
|
|
|
||
|
|
resp, err := http.Post(
|
||
|
|
server.URL+"/v1/systemone",
|
||
|
|
"application/json",
|
||
|
|
bytes.NewReader([]byte(`{"model":"clef-flash","state":"x","questions":{}}`)),
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("request failed: %v", err)
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
if !clefCalled {
|
||
|
|
t.Errorf("upstream clef-flash service was not called")
|
||
|
|
}
|
||
|
|
|
||
|
|
if resp.StatusCode != http.StatusOK {
|
||
|
|
t.Errorf("expected 200 for known systemone model, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
body, readErr := io.ReadAll(resp.Body)
|
||
|
|
if readErr != nil {
|
||
|
|
t.Fatalf("failed to read response body: %v", readErr)
|
||
|
|
}
|
||
|
|
if strings.Contains(string(body), "no routes configured") {
|
||
|
|
t.Errorf("systemone request fell through to route lookup: %s", string(body))
|
||
|
|
}
|
||
|
|
if !strings.Contains(string(body), `"decision":"allow"`) {
|
||
|
|
t.Errorf("expected upstream response body, got %s", string(body))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSystemoneUnknownModelValidationError verifies an unknown model on
|
||
|
|
// /v1/systemone returns the same modelValidationError kind as the other three
|
||
|
|
// body-dispatched paths.
|
||
|
|
func TestSystemoneUnknownModelValidationError(t *testing.T) {
|
||
|
|
cfg := &config.Config{
|
||
|
|
Models: map[string]*config.ModelUpstream{
|
||
|
|
"clef-flash": {
|
||
|
|
Name: "clef-flash",
|
||
|
|
Address: "localhost:9000",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
Routes: make(map[string]*config.Route),
|
||
|
|
}
|
||
|
|
|
||
|
|
handler := New(cfg)
|
||
|
|
defer handler.Close()
|
||
|
|
|
||
|
|
paths := []string{
|
||
|
|
"/v1/chat/completions",
|
||
|
|
"/v1/embeddings",
|
||
|
|
"/v1/rerank",
|
||
|
|
"/v1/systemone",
|
||
|
|
}
|
||
|
|
for _, path := range paths {
|
||
|
|
req := httptest.NewRequest(
|
||
|
|
"POST",
|
||
|
|
path,
|
||
|
|
strings.NewReader(`{"model":"unknown-model"}`),
|
||
|
|
)
|
||
|
|
route, err := handler.RouteRequest(req)
|
||
|
|
if route != nil {
|
||
|
|
t.Errorf("%s: expected nil route for unknown model", path)
|
||
|
|
}
|
||
|
|
var validationErr *modelValidationError
|
||
|
|
if !errors.As(err, &validationErr) {
|
||
|
|
t.Fatalf("%s: expected modelValidationError, got %v", path, err)
|
||
|
|
}
|
||
|
|
if validationErr.Kind != "unknown_model" {
|
||
|
|
t.Errorf("%s: expected kind unknown_model, got %s", path, validationErr.Kind)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestSystemoneUpstreamPathNotRewritten verifies routeByModel keeps the request
|
||
|
|
// path for /v1/systemone: upstream must see /v1/systemone, not
|
||
|
|
// /v1/chat/completions. /v1/rerank is the only endpoint with a path rewrite.
|
||
|
|
func TestSystemoneUpstreamPathNotRewritten(t *testing.T) {
|
||
|
|
upstreamPath := ""
|
||
|
|
|
||
|
|
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
upstreamPath = r.URL.Path
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
fmt.Fprint(w, `{"decision":"deny"}`)
|
||
|
|
}))
|
||
|
|
defer upstreamServer.Close()
|
||
|
|
|
||
|
|
upstreamAddr := strings.TrimPrefix(upstreamServer.URL, "http://")
|
||
|
|
|
||
|
|
cfg := &config.Config{
|
||
|
|
Models: map[string]*config.ModelUpstream{
|
||
|
|
"clef-flash": {
|
||
|
|
Name: "clef-flash",
|
||
|
|
Address: upstreamAddr,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
Routes: make(map[string]*config.Route),
|
||
|
|
}
|
||
|
|
|
||
|
|
handler := New(cfg)
|
||
|
|
defer handler.Close()
|
||
|
|
|
||
|
|
server := httptest.NewServer(handler)
|
||
|
|
defer server.Close()
|
||
|
|
|
||
|
|
resp, err := http.Post(
|
||
|
|
server.URL+"/v1/systemone",
|
||
|
|
"application/json",
|
||
|
|
bytes.NewReader([]byte(`{"model":"clef-flash","state":"x","questions":{}}`)),
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("request failed: %v", err)
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
if upstreamPath != "/v1/systemone" {
|
||
|
|
t.Errorf("expected upstream path /v1/systemone, got %s", upstreamPath)
|
||
|
|
}
|
||
|
|
if upstreamPath == "/v1/chat/completions" {
|
||
|
|
t.Errorf("upstream path was rewritten to /v1/chat/completions")
|
||
|
|
}
|
||
|
|
}
|