feat: publish v1.0.0-rc.36-custom.1
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
This commit is contained in:
commit
bb7968b612
2381 files changed
+551494
No files matched your search
@@ -0,0 +1,306 @@
|
||||
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.([]any)[0].(map[string]any)["input_schema"].(map[string]any)["type"] = "array"
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user