Ctrl K
ComunidadGuías…

relay/request_clone_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package relay

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http/httptest"
	"reflect"
	"testing"

	"github.com/QuantumNous/new-api/common"
	relaycommon "github.com/QuantumNous/new-api/relay/common"
	"github.com/QuantumNous/new-api/relay/helper"
	"github.com/QuantumNous/new-api/relaykit/dto"
	"github.com/gin-gonic/gin"
	"github.com/jinzhu/copier"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestRequestDeepCopyResponses(t *testing.T) {
	t.Run("nil source", func(t *testing.T) {
		clone, err := common.DeepCopy[dto.OpenAIResponsesRequest](nil)
		require.Error(t, err)
		assert.Nil(t, clone)
	})

	for _, raw := range []json.RawMessage{nil, {}, []byte(`null`), []byte(`false`), []byte(`0`), []byte(`[]`), []byte(`{"x":1}`)} {
		t.Run(fmt.Sprintf("raw_%q_nil_%t", raw, raw == nil), func(t *testing.T) {
			src := &dto.OpenAIResponsesRequest{}
			populateResponsesCloneFixture(t, reflect.ValueOf(src).Elem(), raw)
			want := &dto.OpenAIResponsesRequest{}
			populateResponsesCloneFixture(t, reflect.ValueOf(want).Elem(), raw)
			clone, err := common.DeepCopy(src)
			require.NoError(t, err)
			assert.Equal(t, src, clone, "including internal state and explicit zero pointers")

			// Mutate every reachable field, including nested pointers and raw bytes.
			// A later channel attempt must still see the original request.
			populateResponsesCloneFixture(t, reflect.ValueOf(clone).Elem(), []byte(`true`))
			assert.Equal(t, want, src)
			retry, err := common.DeepCopy(src)
			require.NoError(t, err)
			assert.Equal(t, want, retry)
		})
	}

	t.Run("absent optional fields", func(t *testing.T) {
		src := &dto.OpenAIResponsesRequest{Model: "gpt-4.1", Input: []byte(`"hello"`)}
		clone, err := common.DeepCopy(src)
		require.NoError(t, err)
		assert.Equal(t, src, clone)
		clone.Input[1] = 'H'
		assert.Equal(t, json.RawMessage(`"hello"`), src.Input)
	})

	t.Run("large input", func(t *testing.T) {
		input := bytes.Repeat([]byte("a"), 1<<20)
		input[0], input[len(input)-1] = '"', '"'
		src := &dto.OpenAIResponsesRequest{Model: "gpt-4.1", Input: input}
		clone, err := common.DeepCopy(src)
		require.NoError(t, err)
		assert.Equal(t, src, clone)
		clone.Input[len(input)/2] = 'b'
		assert.Equal(t, byte('a'), src.Input[len(input)/2])
	})

	t.Run("model mapping on successive channel attempts", func(t *testing.T) {
		src := &dto.OpenAIResponsesRequest{Model: "client-model", Input: []byte(`"hello"`)}
		for _, model := range []string{"first-channel-model", "retry-channel-model"} {
			request, err := common.DeepCopy(src)
			require.NoError(t, err)
			assert.Equal(t, json.RawMessage(`"hello"`), request.Input)
			c, _ := gin.CreateTestContext(httptest.NewRecorder())
			c.Set("model_mapping", fmt.Sprintf(`{"client-model":%q}`, model))
			info := &relaycommon.RelayInfo{OriginModelName: src.Model}
			require.NoError(t, helper.ModelMappedHelper(c, info, request))
			assert.Equal(t, model, request.Model)
			assert.Equal(t, "client-model", src.Model)
			request.Input[1] = 'H'
			assert.Equal(t, json.RawMessage(`"hello"`), src.Input)
		}
	})
}

// Populate all mutable fields so newly added request fields also exercise the
// retry-isolation contract, rather than silently escaping a hand-written list.
func populateResponsesCloneFixture(t *testing.T, value reflect.Value, raw json.RawMessage) {
	t.Helper()
	switch value.Kind() {
	case reflect.Struct:
		for i := 0; i < value.NumField(); i++ {
			populateResponsesCloneFixture(t, value.Field(i), raw)
		}
	case reflect.Pointer:
		if value.IsNil() {
			value.Set(reflect.New(value.Type().Elem()))
		}
		populateResponsesCloneFixture(t, value.Elem(), raw)
	case reflect.Slice:
		require.Equal(t, reflect.TypeFor[json.RawMessage](), value.Type())
		if value.Len() > 0 {
			value.Index(0).SetUint('!')
		}
		value.Set(reflect.ValueOf(json.RawMessage(bytes.Clone(raw))))
	case reflect.String:
		value.SetString(value.String() + "gpt-4.1")
	case reflect.Bool:
		value.SetBool(bytes.Equal(raw, []byte(`true`)))
	case reflect.Int:
		if bytes.Equal(raw, []byte(`true`)) {
			value.SetInt(1)
		}
	case reflect.Uint:
		if bytes.Equal(raw, []byte(`true`)) {
			value.SetUint(1)
		}
	case reflect.Float64:
		if bytes.Equal(raw, []byte(`true`)) {
			value.SetFloat(1)
		}
	default:
		require.FailNow(t, "extend clone fixture for new field type", "%s", value.Type())
	}
}

func TestRequestDeepCopyRawMutation(t *testing.T) {
	t.Run("edits before cloning are included", func(t *testing.T) {
		src := &dto.OpenAIResponsesRequest{Input: json.RawMessage(`"hello"`)}
		src.Input[1] = 'H'
		clone, err := common.DeepCopy(src)
		require.NoError(t, err)
		assert.Equal(t, json.RawMessage(`"Hello"`), clone.Input)
	})

	t.Run("edits after cloning are isolated in both directions", func(t *testing.T) {
		input := make(json.RawMessage, len(`"hello"`), 32)
		copy(input, `"hello"`)
		src := &dto.OpenAIResponsesRequest{Input: input}
		clone, err := common.DeepCopy(src)
		require.NoError(t, err)

		clone.Input[1] = 'H'
		assert.Equal(t, json.RawMessage(`"hello"`), src.Input)
		src.Input[2] = 'A'
		assert.Equal(t, json.RawMessage(`"Hello"`), clone.Input)
		clone.Input = append(clone.Input, ' ')
		src.Input = append(src.Input, '\n')
		assert.Equal(t, json.RawMessage("\"Hello\" "), clone.Input)
		assert.Equal(t, json.RawMessage("\"hAllo\"\n"), src.Input)
		clone.Input = json.RawMessage(`"replacement"`)
		assert.Equal(t, json.RawMessage("\"hAllo\"\n"), src.Input)
	})

	t.Run("raw map and slice entries retain nil and empty values", func(t *testing.T) {
		src := &struct {
			Fields map[string]json.RawMessage
			Items  []json.RawMessage
		}{
			Fields: map[string]json.RawMessage{"absent": nil, "empty": {}, "value": []byte(`false`)},
			Items:  []json.RawMessage{nil, {}, []byte(`0`)},
		}
		clone, err := common.DeepCopy(src)
		require.NoError(t, err)
		assert.Equal(t, src, clone)
		clone.Fields["value"][0] = '!'
		clone.Items[2][0] = '1'
		delete(clone.Fields, "absent")
		assert.Contains(t, src.Fields, "absent")
		assert.Equal(t, json.RawMessage(`false`), src.Fields["value"])
		assert.Equal(t, json.RawMessage(`0`), src.Items[2])
	})
}

func TestRequestDeepCopyOtherProtocols(t *testing.T) {
	t.Run("chat nested fields remain isolated", func(t *testing.T) {
		assertRequestCloneIsolation(t, `{
			"model":"gpt-4.1","max_tokens":0,"stream":false,"thinking_budget":128,
			"messages":[{"role":"assistant","content":[{"type":"text","text":"hello"}],"tool_calls":[{"id":"call_1"}]}],
			"response_format":{"type":"json_schema","json_schema":{"strict":false}}
		}`, func(clone *dto.GeneralOpenAIRequest) {
			clone.Messages[0].ToolCalls[0] = '!'
			clone.Messages[0].Content.([]any)[0].(map[string]any)["text"] = "changed"
			clone.ResponseFormat.JsonSchema[0] = '!'
			*clone.MaxTokens = 1
			*clone.Stream = true
		})
	})

	t.Run("claude dynamic tools and raw fields remain isolated", func(t *testing.T) {
		assertRequestCloneIsolation(t, `{
			"model":"claude-sonnet-4-5","max_tokens":1024,"temperature":0,
			"tools":[{"name":"lookup","input_schema":{"type":"object"}}],
			"metadata":{"user_id":"test"},"messages":[{"role":"user","content":"hello"}]
		}`, func(clone *dto.ClaudeRequest) {
			clone.Metadata[0] = '!'
			clone.Tools.(json.RawMessage)[0] = '!'
			clone.Messages[0].Content = "changed"
			*clone.Temperature = 1
		})
	})

	t.Run("gemini nested parts and configuration remain isolated", func(t *testing.T) {
		assertRequestCloneIsolation(t, `{
			"contents":[{"role":"model","parts":[{"text":"hello","thoughtSignature":"signature","inlineData":{"mimeType":"text/plain","data":"aGk="}}]}],
			"tools":[{"googleSearch":{}}],"generationConfig":{"responseJsonSchema":{"type":"object"}}
		}`, func(clone *dto.GeminiChatRequest) {
			clone.Contents[0].Parts[0].ThoughtSignature[1] = '!'
			clone.Contents[0].Parts[0].InlineData.Data = "changed"
			clone.Tools[0] = '!'
			clone.GenerationConfig.ResponseJsonSchema[0] = '!'
		})
	})

	t.Run("image extra parameters remain isolated", func(t *testing.T) {
		assertRequestCloneIsolation(t, `{
			"model":"gpt-image-1","prompt":"a cat","n":1,"watermark":false,
			"image":["https://example.com/image.png"],"parameters":{"seed":0}
		}`, func(clone *dto.ImageRequest) {
			clone.Image[0] = '!'
			clone.Extra["parameters"][0] = '!'
			clone.Extra["new_field"] = json.RawMessage(`true`)
			*clone.N = 2
			*clone.Watermark = true
		})
	})

	t.Run("audio raw parameters and speed remain isolated", func(t *testing.T) {
		assertRequestCloneIsolation(t, `{
			"model":"tts-1","input":"hello","voice":"alloy","speed":1,"metadata":{"tag":"test"},"ref_audio":"sample"
		}`, func(clone *dto.AudioRequest) {
			clone.Metadata[0] = '!'
			clone.RefAudio[1] = '!'
			*clone.Speed = 2
		})
	})

	t.Run("embedding dynamic input and dimensions remain isolated", func(t *testing.T) {
		assertRequestCloneIsolation(t, `{"model":"text-embedding-3-small","input":[[1,2],[3]],"dimensions":256}`,
			func(clone *dto.EmbeddingRequest) {
				clone.Input.([]any)[0].([]any)[0] = float64(9)
				*clone.Dimensions = 512
			})
	})

	t.Run("rerank documents and options remain isolated", func(t *testing.T) {
		assertRequestCloneIsolation(t, `{
			"model":"rerank-v3.5","query":"hello","documents":["first","second"],"top_n":1,"return_documents":false
		}`, func(clone *dto.RerankRequest) {
			clone.Documents[0] = "changed"
			*clone.TopN = 2
			*clone.ReturnDocuments = true
		})
	})
}

// A channel attempt may mutate its clone without changing the request used by
// later attempts. Decode the expected request independently to avoid aliases.
func assertRequestCloneIsolation[T any](t *testing.T, body string, mutate func(*T)) {
	t.Helper()
	var src, want T
	require.NoError(t, common.UnmarshalJsonStr(body, &src))
	require.NoError(t, common.UnmarshalJsonStr(body, &want))
	clone, err := common.DeepCopy(&src)
	require.NoError(t, err)
	require.Equal(t, &want, clone)
	wantJSON, err := common.Marshal(&want)
	require.NoError(t, err)
	cloneJSON, err := common.Marshal(clone)
	require.NoError(t, err)
	assert.Equal(t, string(wantJSON), string(cloneJSON), "preserve the outbound JSON")
	mutate(clone)
	assert.Equal(t, want, src, "a channel attempt must not mutate the original request")
	retry, err := common.DeepCopy(&src)
	require.NoError(t, err)
	assert.Equal(t, &want, retry)
}

func BenchmarkRequestDeepCopy(b *testing.B) {
	for _, size := range []int{256, 4 << 10, 1 << 20, 10 << 20, 40 << 20} {
		input := bytes.Repeat([]byte("a"), size)
		input[0], input[len(input)-1] = '"', '"'
		src := &dto.OpenAIResponsesRequest{Model: "gpt-4.1", Input: input}
		for _, method := range []struct {
			name string
			copy func(*dto.OpenAIResponsesRequest) (*dto.OpenAIResponsesRequest, error)
		}{
			{"reflect", func(src *dto.OpenAIResponsesRequest) (*dto.OpenAIResponsesRequest, error) {
				var dst dto.OpenAIResponsesRequest
				err := copier.CopyWithOption(&dst, src, copier.Option{DeepCopy: true, IgnoreEmpty: true})
				return &dst, err
			}},
			{"raw_bytes", common.DeepCopy[dto.OpenAIResponsesRequest]},
		} {
			b.Run(fmt.Sprintf("%dB/%s", size, method.name), func(b *testing.B) {
				b.ReportAllocs()
				b.SetBytes(int64(size))
				for b.Loop() {
					clone, err := method.copy(src)
					require.NoError(b, err)
					require.Len(b, clone.Input, size)
				}
			})
		}
	}
}