Files

227 lines
8.4 KiB
Go

package dialect
import (
"bytes"
"encoding/json"
"net/http"
"reflect"
"testing"
)
func TestModelRewritersDeriveRequestsWithoutMutatingOriginal(t *testing.T) {
tests := []struct {
name string
rewriter ModelRewriter
request *ParsedRequest
upstream string
wantPath string
wantModel string
}{
{
name: "openai", rewriter: NewOpenAI(), upstream: "provider-openai",
request: &ParsedRequest{Method: http.MethodPost, Path: "/v1/chat/completions", Header: http.Header{"X-Test": {"one"}}, Body: []byte(`{"model":"public","metadata":{"n":9007199254740993}}`)},
wantPath: "/v1/chat/completions", wantModel: "provider-openai",
},
{
name: "openai responses",
rewriter: NewOpenAIResponses(),
upstream: "provider-responses",
request: &ParsedRequest{
Method: http.MethodPost,
Path: "/v1/responses",
Header: http.Header{"X-Test": {"one"}},
Body: []byte(`{"model":"public","input":"ping"}`),
},
wantPath: "/v1/responses",
wantModel: "provider-responses",
},
{
name: "anthropic", rewriter: NewAnthropic(), upstream: "provider-claude",
request: &ParsedRequest{Method: http.MethodPost, Path: "/v1/messages", Body: []byte(`{"model":"public","messages":[]}`)},
wantPath: "/v1/messages", wantModel: "provider-claude",
},
{
name: "gemini", rewriter: NewGemini(), upstream: "provider-gemini",
request: &ParsedRequest{Method: http.MethodPost, Path: "/v1beta/models/public:generateContent", Body: []byte(`{}`)},
wantPath: "/v1beta/models/provider-gemini:generateContent",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
original := cloneParsedRequestForTest(test.request)
derived, err := test.rewriter.RewriteRequestModel(test.request, test.upstream)
if err != nil {
t.Fatalf("RewriteRequestModel() error = %v", err)
}
if derived == test.request {
t.Fatal("RewriteRequestModel() returned the caller-owned request")
}
if derived.Path != test.wantPath {
t.Fatalf("derived path = %q, want %q", derived.Path, test.wantPath)
}
if test.wantModel != "" {
metadata, err := test.rewriter.(Dialect).InspectRequest(derived)
if err != nil {
t.Fatalf("InspectRequest(derived) error = %v", err)
}
if metadata.Model == nil || *metadata.Model != test.wantModel {
t.Fatalf("derived model = %#v, want %q", metadata.Model, test.wantModel)
}
}
if !reflect.DeepEqual(test.request, original) {
t.Fatalf("original request changed after rewrite:\n got %#v\nwant %#v", test.request, original)
}
if derived.Header == nil {
derived.Header = make(http.Header)
}
derived.Header.Set("X-Derived", "changed")
if len(derived.Body) > 0 {
derived.Body[0] ^= 0xff
}
if !reflect.DeepEqual(test.request, original) {
t.Fatalf("original request aliases derived request:\n got %#v\nwant %#v", test.request, original)
}
})
}
}
func TestModelRewritersRewriteResponseFields(t *testing.T) {
tests := []struct {
name string
rewriter ModelRewriter
body string
path []string
}{
{name: "openai", rewriter: NewOpenAI(), body: `{"model":"real","n":9007199254740993}`, path: []string{"model"}},
{name: "openai responses object", rewriter: NewOpenAIResponses(), body: `{"type":"response","model":"real","n":9007199254740993}`, path: []string{"model"}},
{name: "openai responses event", rewriter: NewOpenAIResponses(), body: `{"type":"response.completed","response":{"model":"real"},"n":9007199254740993}`, path: []string{"response", "model"}},
{name: "anthropic response", rewriter: NewAnthropic(), body: `{"type":"message","model":"real"}`, path: []string{"model"}},
{name: "anthropic message_start", rewriter: NewAnthropic(), body: `{"type":"message_start","message":{"model":"real"}}`, path: []string{"message", "model"}},
{name: "gemini", rewriter: NewGemini(), body: `{"modelVersion":"real"}`, path: []string{"modelVersion"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.rewriter.RewriteResponseModel([]byte(test.body), "public")
if err != nil {
t.Fatalf("RewriteResponseModel() error = %v", err)
}
payload := decodeJSONWithNumbers(t, got)
var selected any = payload
for _, part := range test.path {
object, ok := selected.(map[string]any)
if !ok {
t.Fatalf("response path %v reached %T", test.path, selected)
}
selected = object[part]
}
if selected != "public" {
t.Fatalf("response path %v = %#v, want public", test.path, selected)
}
if number, exists := payload["n"]; exists {
gotNumber, ok := number.(json.Number)
if !ok || gotNumber.String() != "9007199254740993" {
t.Fatalf("untouched integer = %#v", number)
}
}
})
}
}
func TestOpenAIResponsesResponseModelRewriteTreatsNullResponseAsAbsent(t *testing.T) {
body := []byte(`{"type":"response.output_text.delta","response":null,"delta":"hello"}`)
got, err := NewOpenAIResponses().RewriteResponseModel(body, "public")
if err != nil {
t.Fatalf("RewriteResponseModel() error = %v", err)
}
if !bytes.Equal(got, body) {
t.Fatalf("RewriteResponseModel() = %q, want %q", got, body)
}
}
func TestModelRewritersRejectInvalidTargetModels(t *testing.T) {
rewriters := []struct {
name string
value ModelRewriter
req *ParsedRequest
}{
{name: "openai", value: NewOpenAI(), req: &ParsedRequest{Body: []byte(`{"model":"public"}`)}},
{name: "openai responses", value: NewOpenAIResponses(), req: &ParsedRequest{Body: []byte(`{"model":"public"}`)}},
{name: "anthropic", value: NewAnthropic(), req: &ParsedRequest{Body: []byte(`{"model":"public"}`)}},
{name: "gemini", value: NewGemini(), req: &ParsedRequest{Path: "/v1beta/models/public:generateContent"}},
}
for _, test := range rewriters {
t.Run(test.name, func(t *testing.T) {
for _, model := range []string{"", " public", "public "} {
if _, err := test.value.RewriteRequestModel(test.req, model); err == nil {
t.Fatalf("RewriteRequestModel(%q) error = nil", model)
}
if _, err := test.value.RewriteResponseModel([]byte(`{"model":"real","modelVersion":"real"}`), model); err == nil {
t.Fatalf("RewriteResponseModel(%q) error = nil", model)
}
}
})
}
gemini := NewGemini()
if _, err := gemini.RewriteRequestModel(&ParsedRequest{Path: "/v1beta/models/public:generateContent"}, "models/provider"); err == nil {
t.Fatal("Gemini RewriteRequestModel() accepted slash in model")
}
if _, err := gemini.RewriteResponseModel([]byte(`{"modelVersion":"real"}`), "models/provider"); err == nil {
t.Fatal("Gemini RewriteResponseModel() accepted slash in model")
}
}
func TestModelRewritersRequireRequestModelAndCloneAbsentResponse(t *testing.T) {
for _, test := range []struct {
name string
rewriter ModelRewriter
request *ParsedRequest
response []byte
}{
{name: "openai", rewriter: NewOpenAI(), request: &ParsedRequest{Body: []byte(`{}`)}, response: []byte(`{"id":"one"}`)},
{name: "openai responses", rewriter: NewOpenAIResponses(), request: &ParsedRequest{Body: []byte(`{}`)}, response: []byte(`{"id":"resp_one"}`)},
{name: "anthropic", rewriter: NewAnthropic(), request: &ParsedRequest{Body: []byte(`{}`)}, response: []byte(`{"type":"content_block_delta"}`)},
{name: "gemini", rewriter: NewGemini(), request: &ParsedRequest{Path: "/v1beta/models/:generateContent"}, response: []byte(`{"candidates":[]}`)},
} {
t.Run(test.name, func(t *testing.T) {
if _, err := test.rewriter.RewriteRequestModel(test.request, "provider"); err == nil {
t.Fatal("RewriteRequestModel() error = nil for absent model")
}
got, err := test.rewriter.RewriteResponseModel(test.response, "public")
if err != nil {
t.Fatalf("RewriteResponseModel() error = %v", err)
}
if !bytes.Equal(got, test.response) {
t.Fatalf("absent response rewrite = %q, want %q", got, test.response)
}
if len(got) > 0 {
got[0] ^= 0xff
if bytes.Equal(got, test.response) {
t.Fatal("absent response rewrite aliases input bytes")
}
}
})
}
}
func cloneParsedRequestForTest(request *ParsedRequest) *ParsedRequest {
clone := *request
clone.Header = request.Header.Clone()
clone.Body = bytes.Clone(request.Body)
return &clone
}
func decodeJSONWithNumbers(t *testing.T, body []byte) map[string]any {
t.Helper()
decoder := json.NewDecoder(bytes.NewReader(body))
decoder.UseNumber()
var payload map[string]any
if err := decoder.Decode(&payload); err != nil {
t.Fatalf("decode response: %v", err)
}
return payload
}