Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
179 lines
6.9 KiB
Go
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())
|
|
}
|
|
}
|