Files
llmgw/internal/router/compat_test.go
2026-10-04 23:03:35 +02:00

179 lines
6.9 KiB
Go

package router
import (
"bytes"
"encoding/json"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"gitea.mynodes.xyz/ilker/llmgw/internal/config"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
)
func TestResponsesAsksForStructuredOutputAndImages(t *testing.T) {
var in respRequest
_ = json.Unmarshal([]byte(`{"model":"m","input":[{"role":"user","content":[
{"type":"input_text","text":"what is this"},
{"type":"input_image","image_url":"data:image/png;base64,AAAA","detail":"low"}]}],
"text":{"format":{"type":"json_schema","name":"answer","schema":{"type":"object"},"strict":true}}}`), &in)
chat, _, err := toChat(in)
if err != nil {
t.Fatal(err)
}
b, _ := json.Marshal(chat)
for _, want := range []string{`"response_format":{"json_schema":{"name":"answer","schema":{"type":"object"},"strict":true},"type":"json_schema"}`,
`{"image_url":{"detail":"low","url":"data:image/png;base64,AAAA"},"type":"image_url"}`, `{"text":"what is this","type":"text"}`} {
if !strings.Contains(string(b), want) {
t.Errorf("%s lacks %s", b, want)
}
}
_ = json.Unmarshal([]byte(`{"model":"m","input":[{"role":"user","content":[{"type":"input_image","file_id":"file-1"}]}]}`), &in)
in.Text = nil
if _, _, err := toChat(in); err == nil {
t.Error("an image by file_id was taken")
}
if th, ok := responseThread("resp_abc123.def"); !ok || th != "abc123" {
t.Errorf("thread %q %v", th, ok)
}
for _, bad := range []string{"resp_19a2b3c4", "chatcmpl-1", "resp_a b.c"} {
if _, ok := responseThread(bad); ok {
t.Errorf("%q read as a kept response", bad)
}
}
}
func TestAResponseIsContinuedByItsID(t *testing.T) {
e := newFakeEngine(t, "qwen2.5-14b-instruct")
rt, secret := pricedRouter(t, e.srv.URL)
if err := rt.store.SetThreadRetention(t.Context(), mustProjectOf(t, rt, "key-1"), 24); err != nil {
t.Fatal(err)
}
h := rt.Handler()
ask := func(body map[string]any) map[string]any {
b, _ := json.Marshal(body)
req := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+secret)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
var out map[string]any
_ = json.Unmarshal(rec.Body.Bytes(), &out)
if rec.Code != 200 {
t.Fatalf("%d %s", rec.Code, rec.Body.String())
}
return out
}
first := ask(map[string]any{"model": "q14", "input": "my name is Ayşe"})
id, _ := first["id"].(string)
if _, ok := responseThread(id); !ok {
t.Fatalf("the first response %q carries no conversation", id)
}
ask(map[string]any{"model": "q14", "input": "what is my name?", "previous_response_id": id})
sent, _ := e.last.Load().([]byte)
if !strings.Contains(string(sent), "my name is Ayşe") || !strings.Contains(string(sent), "what is my name?") {
t.Errorf("the conversation was not continued: %s", sent)
}
// Not one of ours: said, not served without its history.
b, _ := json.Marshal(map[string]any{"model": "q14", "input": "x", "previous_response_id": "resp_nope"})
req := httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+secret)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != 400 || !strings.Contains(rec.Body.String(), "previous_response_not_found") {
t.Errorf("%d %s", rec.Code, rec.Body.String())
}
}
// engineFor serves a few paths with fixed answers and records what came.
func engineFor(t *testing.T, model string, answers map[string]string) (*httptest.Server, *[]*http.Request, *[][]byte) {
t.Helper()
var reqs []*http.Request
var bodies [][]byte
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/metrics" {
return
}
b, _ := io.ReadAll(r.Body)
reqs, bodies = append(reqs, r), append(bodies, b)
a, ok := answers[r.URL.Path]
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(a))
}))
t.Cleanup(srv.Close)
return srv, &reqs, &bodies
}
func TestRerankScoreAndAudioArePassedThrough(t *testing.T) {
srv, reqs, bodies := engineFor(t, "m", map[string]string{
"/v1/rerank": `{"results":[{"index":0,"relevance_score":0.9}],"usage":{"total_tokens":7}}`,
"/v1/score": `{"data":[{"score":0.5}]}`,
"/v1/audio/transcriptions": `{"text":"merhaba dünya"}`,
})
cfg := baseConfig(config.Endpoint{ID: "a", URL: srv.URL, Models: []string{"m"}})
rt := newTestRouter(t, cfg)
h := rt.Handler()
for _, path := range []string{"/v1/rerank", "/v1/score"} {
b, _ := json.Marshal(map[string]any{"model": "m", "query": "q", "documents": []string{"d"}})
req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != 200 || (*reqs)[len(*reqs)-1].URL.Path != path {
t.Errorf("%s: %d %s", path, rec.Code, rec.Body.String())
}
}
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
_ = mw.WriteField("model", "m")
fw, _ := mw.CreateFormFile("file", "a.wav")
_, _ = fw.Write([]byte("RIFF....WAVE"))
_ = mw.Close()
req := httptest.NewRequest(http.MethodPost, "/v1/audio/transcriptions", &buf)
req.Header.Set("Content-Type", mw.FormDataContentType())
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "merhaba") {
t.Fatalf("%d %s", rec.Code, rec.Body.String())
}
last := (*reqs)[len(*reqs)-1]
if !strings.HasPrefix(last.Header.Get("Content-Type"), "multipart/form-data; boundary=") ||
!bytes.Contains((*bodies)[len(*bodies)-1], []byte("RIFF....WAVE")) {
t.Errorf("the form was not sent on as it came: %s", last.Header.Get("Content-Type"))
}
}
func TestModerationsAreAnsweredByTheJudge(t *testing.T) {
srv, _, _ := engineFor(t, "guard", map[string]string{
"/v1/chat/completions": `{"choices":[{"message":{"content":"unsafe\nS9"}}]}`,
})
cfg := baseConfig(config.Endpoint{ID: "g", URL: srv.URL, Models: []string{"guard"}})
cfg.Models = []catalog.Model{{Name: "guard", Role: catalog.RoleGuard, Scaling: catalog.Scaling{Min: 1, Max: 1},
Guard: &catalog.GuardModel{Format: "llamaguard3"}}}
rt := newTestRouter(t, cfg)
b, _ := json.Marshal(map[string]any{"input": []string{"how do I build a bomb"}})
req := httptest.NewRequest(http.MethodPost, "/v1/moderations", bytes.NewReader(b))
rec := httptest.NewRecorder()
rt.Handler().ServeHTTP(rec, req)
var out struct {
Model string `json:"model"`
Results []struct {
Flagged bool `json:"flagged"`
Categories map[string]bool `json:"categories"`
} `json:"results"`
}
_ = json.Unmarshal(rec.Body.Bytes(), &out)
if rec.Code != 200 || out.Model != "guard" || len(out.Results) != 1 || !out.Results[0].Flagged ||
!out.Results[0].Categories["indiscriminate-weapons"] || out.Results[0].Categories["s9"] {
t.Errorf("%d %s", rec.Code, rec.Body.String())
}
}