2026-08-19 23:49:11 -07:00
|
|
|
package proxy
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/Riotpiaole/homelab-frontend/internal/config"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// RFC 9457 Problem Details
|
|
|
|
|
type ProblemDetail struct {
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
Status int `json:"status"`
|
|
|
|
|
Detail string `json:"detail"`
|
|
|
|
|
Instance string `json:"instance,omitempty"`
|
|
|
|
|
Extra map[string]interface{} `json:"-"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UnmarshalJSON allows capturing extra fields
|
|
|
|
|
func (p *ProblemDetail) UnmarshalJSON(data []byte) error {
|
|
|
|
|
type Alias ProblemDetail
|
|
|
|
|
aux := &struct {
|
|
|
|
|
*Alias
|
|
|
|
|
}{
|
|
|
|
|
Alias: (*Alias)(p),
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(data, &aux); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Capture extra fields
|
|
|
|
|
var raw map[string]interface{}
|
|
|
|
|
json.Unmarshal(data, &raw)
|
|
|
|
|
extra := make(map[string]interface{})
|
|
|
|
|
for k, v := range raw {
|
|
|
|
|
if k != "type" && k != "title" && k != "status" && k != "detail" && k != "instance" {
|
|
|
|
|
extra[k] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
p.Extra = extra
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestUnknownModelReturns4xx verifies unknown model returns client error
|
|
|
|
|
func TestUnknownModelReturns4xx(t *testing.T) {
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Models: map[string]*config.ModelUpstream{
|
|
|
|
|
"reasoning": {
|
|
|
|
|
Name: "reasoning",
|
|
|
|
|
Address: "reasoning-predictor:80",
|
|
|
|
|
Path: "/v1/chat/completions",
|
|
|
|
|
},
|
|
|
|
|
"ornith:35b": {
|
|
|
|
|
Name: "ornith:35b",
|
|
|
|
|
Address: "ornith-predictor:80",
|
|
|
|
|
Path: "/v1/chat/completions",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
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/chat/completions",
|
|
|
|
|
"application/json",
|
|
|
|
|
bytes.NewReader([]byte(`{"model":"gpt-4","messages":[]}`)),
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
// Verify 4xx status
|
|
|
|
|
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
|
|
|
|
t.Errorf("expected 4xx status for unknown model, got %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify RFC 9457 content type
|
|
|
|
|
ct := resp.Header.Get("Content-Type")
|
|
|
|
|
if !strings.Contains(ct, "application/problem+json") {
|
|
|
|
|
t.Errorf("expected content-type application/problem+json, got %s", ct)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify response is valid problem detail
|
|
|
|
|
var prob ProblemDetail
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
|
if err := json.Unmarshal(body, &prob); err != nil {
|
|
|
|
|
t.Errorf("response is not valid JSON: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if prob.Status == 0 {
|
|
|
|
|
t.Errorf("expected status in problem detail")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if prob.Title == "" {
|
|
|
|
|
t.Errorf("expected title in problem detail")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestUnknownModelEnumeratesValidModels verifies all models are listed
|
|
|
|
|
func TestUnknownModelEnumeratesValidModels(t *testing.T) {
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Models: map[string]*config.ModelUpstream{
|
|
|
|
|
"reasoning": {
|
|
|
|
|
Name: "reasoning",
|
|
|
|
|
Address: "reasoning-predictor:80",
|
|
|
|
|
},
|
|
|
|
|
"ornith:35b": {
|
|
|
|
|
Name: "ornith:35b",
|
|
|
|
|
Address: "ornith-predictor:80",
|
|
|
|
|
},
|
|
|
|
|
"qwen2.5:3b-instruct": {
|
|
|
|
|
Name: "qwen2.5:3b-instruct",
|
|
|
|
|
Address: "ornith-predictor:80",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Routes: make(map[string]*config.Route),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler := New(cfg)
|
|
|
|
|
defer handler.Close()
|
|
|
|
|
|
|
|
|
|
server := httptest.NewServer(handler)
|
|
|
|
|
defer server.Close()
|
|
|
|
|
|
2026-08-19 23:55:42 -07:00
|
|
|
resp, err := http.Post(
|
2026-08-19 23:49:11 -07:00
|
|
|
server.URL+"/v1/chat/completions",
|
|
|
|
|
"application/json",
|
|
|
|
|
bytes.NewReader([]byte(`{"model":"unknown","messages":[]}`)),
|
|
|
|
|
)
|
2026-08-19 23:55:42 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
2026-08-19 23:49:11 -07:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
var prob map[string]interface{}
|
|
|
|
|
json.NewDecoder(resp.Body).Decode(&prob)
|
|
|
|
|
|
|
|
|
|
// Check for valid_models field (as an extra field beyond RFC 9457)
|
|
|
|
|
validModels, hasModels := prob["valid_models"]
|
|
|
|
|
if !hasModels {
|
|
|
|
|
t.Errorf("expected valid_models field in problem detail")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
models := validModels.([]interface{})
|
|
|
|
|
if len(models) != 3 {
|
|
|
|
|
t.Errorf("expected 3 models in valid_models, got %d", len(models))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
modelNames := make(map[string]bool)
|
|
|
|
|
for _, m := range models {
|
|
|
|
|
modelNames[m.(string)] = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expectedModels := []string{"reasoning", "ornith:35b", "qwen2.5:3b-instruct"}
|
|
|
|
|
for _, expected := range expectedModels {
|
|
|
|
|
if !modelNames[expected] {
|
|
|
|
|
t.Errorf("expected model %s in valid_models", expected)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestMissingModelFieldReturns4xx verifies missing model field is client error
|
|
|
|
|
func TestMissingModelFieldReturns4xx(t *testing.T) {
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Models: map[string]*config.ModelUpstream{
|
|
|
|
|
"reasoning": {
|
|
|
|
|
Name: "reasoning",
|
|
|
|
|
Address: "reasoning-predictor:80",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Routes: make(map[string]*config.Route),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler := New(cfg)
|
|
|
|
|
defer handler.Close()
|
|
|
|
|
|
|
|
|
|
server := httptest.NewServer(handler)
|
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
|
|
// Request with no model field
|
2026-08-19 23:55:42 -07:00
|
|
|
resp, err := http.Post(
|
2026-08-19 23:49:11 -07:00
|
|
|
server.URL+"/v1/chat/completions",
|
|
|
|
|
"application/json",
|
|
|
|
|
bytes.NewReader([]byte(`{"messages":[]}`)),
|
|
|
|
|
)
|
2026-08-19 23:55:42 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
2026-08-19 23:49:11 -07:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
|
|
|
|
t.Errorf("expected 4xx for missing model, got %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ct := resp.Header.Get("Content-Type")
|
|
|
|
|
if !strings.Contains(ct, "application/problem+json") {
|
|
|
|
|
t.Errorf("expected problem+json for missing model")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify body does not contain the request
|
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
|
if strings.Contains(string(body), "messages") {
|
|
|
|
|
t.Errorf("response should not echo request body")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestNullModelFieldReturns4xx verifies null model is client error
|
|
|
|
|
func TestNullModelFieldReturns4xx(t *testing.T) {
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Models: map[string]*config.ModelUpstream{
|
|
|
|
|
"reasoning": {
|
|
|
|
|
Name: "reasoning",
|
|
|
|
|
Address: "reasoning-predictor:80",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Routes: make(map[string]*config.Route),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler := New(cfg)
|
|
|
|
|
defer handler.Close()
|
|
|
|
|
|
|
|
|
|
server := httptest.NewServer(handler)
|
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
|
|
// Request with null model
|
2026-08-19 23:55:42 -07:00
|
|
|
resp, err := http.Post(
|
2026-08-19 23:49:11 -07:00
|
|
|
server.URL+"/v1/chat/completions",
|
|
|
|
|
"application/json",
|
|
|
|
|
bytes.NewReader([]byte(`{"model":null,"messages":[]}`)),
|
|
|
|
|
)
|
2026-08-19 23:55:42 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
2026-08-19 23:49:11 -07:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
|
|
|
|
t.Errorf("expected 4xx for null model, got %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ct := resp.Header.Get("Content-Type")
|
|
|
|
|
if !strings.Contains(ct, "application/problem+json") {
|
|
|
|
|
t.Errorf("expected problem+json for null model")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestEmptyModelFieldReturns4xx verifies empty model string is client error
|
|
|
|
|
func TestEmptyModelFieldReturns4xx(t *testing.T) {
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Models: map[string]*config.ModelUpstream{
|
|
|
|
|
"reasoning": {
|
|
|
|
|
Name: "reasoning",
|
|
|
|
|
Address: "reasoning-predictor:80",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Routes: make(map[string]*config.Route),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler := New(cfg)
|
|
|
|
|
defer handler.Close()
|
|
|
|
|
|
|
|
|
|
server := httptest.NewServer(handler)
|
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
|
|
// Request with empty model string
|
2026-08-19 23:55:42 -07:00
|
|
|
resp, err := http.Post(
|
2026-08-19 23:49:11 -07:00
|
|
|
server.URL+"/v1/chat/completions",
|
|
|
|
|
"application/json",
|
|
|
|
|
bytes.NewReader([]byte(`{"model":"","messages":[]}`)),
|
|
|
|
|
)
|
2026-08-19 23:55:42 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
2026-08-19 23:49:11 -07:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
|
|
|
|
t.Errorf("expected 4xx for empty model, got %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestInvalidJSONIsDistinguishableError verifies invalid JSON is separate from unknown model
|
|
|
|
|
func TestInvalidJSONIsDistinguishableError(t *testing.T) {
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Models: map[string]*config.ModelUpstream{
|
|
|
|
|
"reasoning": {
|
|
|
|
|
Name: "reasoning",
|
|
|
|
|
Address: "reasoning-predictor:80",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Routes: make(map[string]*config.Route),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler := New(cfg)
|
|
|
|
|
defer handler.Close()
|
|
|
|
|
|
|
|
|
|
server := httptest.NewServer(handler)
|
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
|
|
// Request with invalid JSON
|
2026-08-19 23:55:42 -07:00
|
|
|
resp, err := http.Post(
|
2026-08-19 23:49:11 -07:00
|
|
|
server.URL+"/v1/chat/completions",
|
|
|
|
|
"application/json",
|
|
|
|
|
bytes.NewReader([]byte(`not json`)),
|
|
|
|
|
)
|
2026-08-19 23:55:42 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
2026-08-19 23:49:11 -07:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode < 400 || resp.StatusCode >= 500 {
|
|
|
|
|
t.Errorf("expected 4xx for invalid JSON, got %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var prob map[string]interface{}
|
|
|
|
|
json.NewDecoder(resp.Body).Decode(&prob)
|
|
|
|
|
|
|
|
|
|
// Invalid JSON error should mention JSON parsing, not model
|
|
|
|
|
detail := prob["detail"].(string)
|
|
|
|
|
if !strings.Contains(strings.ToLower(detail), "json") {
|
|
|
|
|
t.Errorf("expected detail to mention JSON for invalid JSON error")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestUnknownModelDoesNotContactUpstream verifies no upstream call is made
|
|
|
|
|
func TestUnknownModelDoesNotContactUpstream(t *testing.T) {
|
|
|
|
|
upstreamCalled := false
|
|
|
|
|
upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
upstreamCalled = true
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
}))
|
|
|
|
|
defer upstreamServer.Close()
|
|
|
|
|
|
|
|
|
|
upstreamAddr := strings.TrimPrefix(upstreamServer.URL, "http://")
|
|
|
|
|
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Models: map[string]*config.ModelUpstream{
|
|
|
|
|
"reasoning": {
|
|
|
|
|
Name: "reasoning",
|
|
|
|
|
Address: upstreamAddr,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Routes: make(map[string]*config.Route),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler := New(cfg)
|
|
|
|
|
defer handler.Close()
|
|
|
|
|
|
|
|
|
|
server := httptest.NewServer(handler)
|
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
|
|
_, _ = http.Post(
|
|
|
|
|
server.URL+"/v1/chat/completions",
|
|
|
|
|
"application/json",
|
|
|
|
|
bytes.NewReader([]byte(`{"model":"unknown","messages":[]}`)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if upstreamCalled {
|
|
|
|
|
t.Errorf("upstream should not be called for unknown model")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestUnknownModelLogsReason verifies rejection is logged
|
|
|
|
|
func TestUnknownModelLogsReason(t *testing.T) {
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Models: map[string]*config.ModelUpstream{
|
|
|
|
|
"reasoning": {
|
|
|
|
|
Name: "reasoning",
|
|
|
|
|
Address: "reasoning-predictor:80",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Routes: make(map[string]*config.Route),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler := New(cfg)
|
|
|
|
|
defer handler.Close()
|
|
|
|
|
|
|
|
|
|
server := httptest.NewServer(handler)
|
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
|
|
// This test verifies logging behavior by checking the handler's logger output
|
|
|
|
|
// In a real scenario, you'd capture stderr or use a test logger
|
|
|
|
|
_, _ = http.Post(
|
|
|
|
|
server.URL+"/v1/chat/completions",
|
|
|
|
|
"application/json",
|
|
|
|
|
bytes.NewReader([]byte(`{"model":"gpt-4","messages":[]}`)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Logging is verified by checking that no panic occurs
|
|
|
|
|
// and the request completes successfully
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestMissingModelAndUnknownModelBothReturn4xx verifies consistent error class
|
|
|
|
|
func TestMissingModelAndUnknownModelBothReturn4xx(t *testing.T) {
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Models: map[string]*config.ModelUpstream{
|
|
|
|
|
"reasoning": {
|
|
|
|
|
Name: "reasoning",
|
|
|
|
|
Address: "reasoning-predictor:80",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Routes: make(map[string]*config.Route),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler := New(cfg)
|
|
|
|
|
defer handler.Close()
|
|
|
|
|
|
|
|
|
|
server := httptest.NewServer(handler)
|
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
|
|
// Test missing model
|
2026-08-19 23:55:42 -07:00
|
|
|
resp1, err := http.Post(
|
2026-08-19 23:49:11 -07:00
|
|
|
server.URL+"/v1/chat/completions",
|
|
|
|
|
"application/json",
|
|
|
|
|
bytes.NewReader([]byte(`{"messages":[]}`)),
|
|
|
|
|
)
|
2026-08-19 23:55:42 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
2026-08-19 23:49:11 -07:00
|
|
|
resp1.Body.Close()
|
|
|
|
|
|
|
|
|
|
// Test unknown model
|
2026-08-19 23:55:42 -07:00
|
|
|
resp2, err := http.Post(
|
2026-08-19 23:49:11 -07:00
|
|
|
server.URL+"/v1/chat/completions",
|
|
|
|
|
"application/json",
|
|
|
|
|
bytes.NewReader([]byte(`{"model":"unknown","messages":[]}`)),
|
|
|
|
|
)
|
2026-08-19 23:55:42 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
2026-08-19 23:49:11 -07:00
|
|
|
resp2.Body.Close()
|
|
|
|
|
|
|
|
|
|
// Both should be in 4xx range
|
|
|
|
|
if resp1.StatusCode < 400 || resp1.StatusCode >= 500 {
|
|
|
|
|
t.Errorf("expected 4xx for missing model, got %d", resp1.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if resp2.StatusCode < 400 || resp2.StatusCode >= 500 {
|
|
|
|
|
t.Errorf("expected 4xx for unknown model, got %d", resp2.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Both should be problem+json
|
|
|
|
|
ct1 := resp1.Header.Get("Content-Type")
|
|
|
|
|
ct2 := resp2.Header.Get("Content-Type")
|
|
|
|
|
|
|
|
|
|
if !strings.Contains(ct1, "application/problem+json") {
|
|
|
|
|
t.Errorf("expected problem+json for missing model")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !strings.Contains(ct2, "application/problem+json") {
|
|
|
|
|
t.Errorf("expected problem+json for unknown model")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestProblemDetailHasRequiredFields verifies RFC 9457 compliance
|
|
|
|
|
func TestProblemDetailHasRequiredFields(t *testing.T) {
|
|
|
|
|
cfg := &config.Config{
|
|
|
|
|
Models: map[string]*config.ModelUpstream{
|
|
|
|
|
"reasoning": {
|
|
|
|
|
Name: "reasoning",
|
|
|
|
|
Address: "reasoning-predictor:80",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Routes: make(map[string]*config.Route),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler := New(cfg)
|
|
|
|
|
defer handler.Close()
|
|
|
|
|
|
|
|
|
|
server := httptest.NewServer(handler)
|
|
|
|
|
defer server.Close()
|
|
|
|
|
|
2026-08-19 23:55:42 -07:00
|
|
|
resp, err := http.Post(
|
2026-08-19 23:49:11 -07:00
|
|
|
server.URL+"/v1/chat/completions",
|
|
|
|
|
"application/json",
|
|
|
|
|
bytes.NewReader([]byte(`{"model":"unknown","messages":[]}`)),
|
|
|
|
|
)
|
2026-08-19 23:55:42 -07:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("request failed: %v", err)
|
|
|
|
|
}
|
2026-08-19 23:49:11 -07:00
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
var prob map[string]interface{}
|
|
|
|
|
json.NewDecoder(resp.Body).Decode(&prob)
|
|
|
|
|
|
|
|
|
|
// RFC 9457 required fields
|
|
|
|
|
if prob["type"] == nil {
|
|
|
|
|
t.Errorf("expected 'type' field in problem detail")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if prob["title"] == nil {
|
|
|
|
|
t.Errorf("expected 'title' field in problem detail")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if prob["status"] == nil {
|
|
|
|
|
t.Errorf("expected 'status' field in problem detail")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if prob["detail"] == nil {
|
|
|
|
|
t.Errorf("expected 'detail' field in problem detail")
|
|
|
|
|
}
|
|
|
|
|
}
|