Skip to content
Merged
172 changes: 172 additions & 0 deletions controller/channel_test_astra_request_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package controller

import (
"net/http/httptest"
"testing"

"github.com/MAX-API-Next/MAX-API/common"
"github.com/MAX-API-Next/MAX-API/constant"
"github.com/MAX-API-Next/MAX-API/dto"
"github.com/MAX-API-Next/MAX-API/model"
"github.com/MAX-API-Next/MAX-API/relay/channel/openai"
relaycommon "github.com/MAX-API-Next/MAX-API/relay/common"
"github.com/MAX-API-Next/MAX-API/relay/helper"
"github.com/MAX-API-Next/MAX-API/setting/model_setting"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)

// Exercise model mapping and outbound serialization, including the request
// generated by the console's channel test, without calling a paid provider.
func astraOutboundChat(t *testing.T, req *dto.GeneralOpenAIRequest, upstream string, channelType int) ([]byte, *relaycommon.RelayInfo) {
t.Helper()
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequestWithContext(t.Context(), "POST", "/v1/chat/completions", nil)
if upstream != req.Model {
mapping, err := common.Marshal(map[string]string{req.Model: upstream})
require.NoError(t, err)
c.Set("model_mapping", string(mapping))
}
info := &relaycommon.RelayInfo{
OriginModelName: req.Model,
ChannelMeta: &relaycommon.ChannelMeta{
ChannelType: channelType, UpstreamModelName: req.Model,
},
}
require.NoError(t, helper.ModelMappedHelper(c, info, req))
converted, err := (&openai.Adaptor{}).ConvertOpenAIRequest(c, info, req)
require.NoError(t, err)
encoded, err := common.Marshal(converted)
require.NoError(t, err)
return encoded, info
}

func TestAstraChannelTestRequest(t *testing.T) {
for _, channelType := range []int{constant.ChannelTypeOpenAI, constant.ChannelTypeAzure} {
for _, stream := range []bool{false, true} {
for _, name := range []string{"gpt-6-astra", "astra-alias"} {
req, ok := buildTestRequest(name, string(constant.EndpointTypeOpenAI), &model.Channel{}, stream).(*dto.GeneralOpenAIRequest)
require.True(t, ok)
body, _ := astraOutboundChat(t, req, "gpt-6-astra", channelType)
require.False(t, gjson.GetBytes(body, "max_tokens").Exists())
require.Equal(t, int64(16), gjson.GetBytes(body, "max_completion_tokens").Int())
require.Equal(t, stream, gjson.GetBytes(body, "stream").Bool())
if stream {
require.True(t, gjson.GetBytes(body, "stream_options.include_usage").Bool())
}
}
}
}
}

func TestAstraTokenLimitPresence(t *testing.T) {
for _, tt := range []struct{ name, input, want string }{
{"omitted", `{}`, `{}`},
{"legacy", `{"max_tokens":100}`, `{"max_completion_tokens":100}`},
{"completion", `{"max_completion_tokens":50}`, `{"max_completion_tokens":50}`},
{"both", `{"max_tokens":100,"max_completion_tokens":50}`, `{"max_completion_tokens":50}`},
{"legacy zero", `{"max_tokens":0}`, `{"max_completion_tokens":0}`},
{"completion zero", `{"max_completion_tokens":0}`, `{"max_completion_tokens":0}`},
{"explicit zero wins", `{"max_tokens":100,"max_completion_tokens":0}`, `{"max_completion_tokens":0}`},
{"both zero", `{"max_tokens":0,"max_completion_tokens":0}`, `{"max_completion_tokens":0}`},
} {
t.Run(tt.name, func(t *testing.T) {
req := &dto.GeneralOpenAIRequest{Model: "gpt-6-astra"}
require.NoError(t, common.UnmarshalJsonStr(tt.input, req))
body, _ := astraOutboundChat(t, req, req.Model, constant.ChannelTypeOpenAI)
want := &dto.GeneralOpenAIRequest{Model: req.Model}
require.NoError(t, common.UnmarshalJsonStr(tt.want, want))
wantJSON, err := common.Marshal(want)
require.NoError(t, err)
require.JSONEq(t, string(wantJSON), string(body))
})
}
}

func TestAstraModelAndSamplingCompatibility(t *testing.T) {
for _, tt := range []struct {
name, source, target, wire, effort string
astra bool
}{
{"base", "gpt-6-astra", "gpt-6-astra", "gpt-6-astra", "", true},
{"snapshot", "gpt-6-astra-2026-09-03", "gpt-6-astra-2026-09-03", "gpt-6-astra-2026-09-03", "", true},
{"effort alias", "gpt-6-astra-high", "gpt-6-astra-high", "gpt-6-astra", "high", true},
{"max effort alias", "gpt-6-astra-max", "gpt-6-astra-max", "gpt-6-astra", "max", true},
{"mapped snapshot effort", "alias", "gpt-6-astra-2026-09-03-xhigh", "gpt-6-astra-2026-09-03", "xhigh", true},
{"unknown variant", "gpt-6-astra-pro", "gpt-6-astra-pro", "gpt-6-astra-pro", "", false},
{"invalid date", "gpt-6-astra-2026-02-30", "gpt-6-astra-2026-02-30", "gpt-6-astra-2026-02-30", "", false},
{"unknown effort variant", "gpt-6-astra-pro-high", "gpt-6-astra-pro-high", "gpt-6-astra-pro-high", "", false},
{"other gpt6", "gpt-6-other", "gpt-6-other", "gpt-6-other", "", false},
{"mapped away", "gpt-6-astra", "gpt-4.1", "gpt-4.1", "", false},
} {
t.Run(tt.name, func(t *testing.T) {
req := &dto.GeneralOpenAIRequest{Model: tt.source, Messages: []dto.Message{{Role: "system", Content: "Be concise"}, {Role: "user", Content: "hi"}}}
require.NoError(t, common.UnmarshalJsonStr(`{"temperature":0,"top_p":0,"logprobs":false,"top_logprobs":0,"seed":0,"frequency_penalty":0,"stream":false}`, req))
body, info := astraOutboundChat(t, req, tt.target, constant.ChannelTypeOpenAI)
require.Equal(t, tt.wire, gjson.GetBytes(body, "model").String())
require.Equal(t, tt.effort, gjson.GetBytes(body, "reasoning_effort").String())
require.Equal(t, tt.effort, info.ReasoningEffort)
role := "system"
if tt.astra {
role = "developer"
}
require.Equal(t, role, gjson.GetBytes(body, "messages.0.role").String())
for _, key := range []string{"temperature", "top_p", "logprobs", "top_logprobs"} {
require.Equal(t, !tt.astra, gjson.GetBytes(body, key).Exists(), key)
}
for _, key := range []string{"seed", "frequency_penalty", "stream"} {
require.True(t, gjson.GetBytes(body, key).Exists(), key)
}
})
}
}

func TestGPT5NonReasoningSamplingCompatibility(t *testing.T) {
req := &dto.GeneralOpenAIRequest{Model: "gpt-5.4", ReasoningEffort: "none"}
require.NoError(t, common.UnmarshalJsonStr(`{"temperature":0.2,"top_p":0.8,"logprobs":true,"top_logprobs":5}`, req))
body, _ := astraOutboundChat(t, req, req.Model, constant.ChannelTypeOpenAI)
for _, key := range []string{"temperature", "top_p", "logprobs"} {
require.True(t, gjson.GetBytes(body, key).Exists(), key)
}
require.Equal(t, int64(5), gjson.GetBytes(body, "top_logprobs").Int())
}

func TestAstraPreservesOtherRequestFields(t *testing.T) {
const payload = `{"model":"gpt-6-astra","messages":[{"role":"user","content":[{"type":"text","text":"Describe this"},{"type":"image_url","image_url":{"url":"https://example.com/image.png"}}]}],"max_completion_tokens":128,"reasoning_effort":"max","temperature":0.2,"top_p":0.8,"logprobs":true,"top_logprobs":5,"tools":[{"type":"function","function":{"name":"lookup","parameters":{"type":"object"}}}],"tool_choice":"auto","parallel_tool_calls":false,"seed":0}`
var req dto.GeneralOpenAIRequest
require.NoError(t, common.UnmarshalJsonStr(payload, &req))
body, info := astraOutboundChat(t, &req, req.Model, constant.ChannelTypeOpenAI)
for _, key := range []string{"messages", "tools", "tool_choice", "parallel_tool_calls", "seed", "max_completion_tokens", "reasoning_effort"} {
require.JSONEq(t, gjson.Get(payload, key).Raw, gjson.GetBytes(body, key).Raw, key)
}
for _, key := range []string{"temperature", "top_p", "logprobs", "top_logprobs"} {
require.False(t, gjson.GetBytes(body, key).Exists(), key)
}
require.Equal(t, "max", info.ReasoningEffort)
}

func TestAstraRespectsPreservedSuffix(t *testing.T) {
settings := model_setting.GetGlobalSettings()
before := settings.ThinkingModelBlacklist
settings.ThinkingModelBlacklist = []string{"gpt-6-astra-high"}
t.Cleanup(func() { settings.ThinkingModelBlacklist = before })
req := &dto.GeneralOpenAIRequest{Model: "gpt-6-astra-high"}
require.NoError(t, common.UnmarshalJsonStr(`{"max_tokens":16,"temperature":0.2}`, req))
body, _ := astraOutboundChat(t, req, req.Model, constant.ChannelTypeOpenAI)
require.Equal(t, "gpt-6-astra-high", gjson.GetBytes(body, "model").String())
require.Equal(t, int64(16), gjson.GetBytes(body, "max_tokens").Int())
require.Equal(t, 0.2, gjson.GetBytes(body, "temperature").Float())
require.False(t, gjson.GetBytes(body, "reasoning_effort").Exists())
}

func TestAstraDirectResponsesKeepsExistingParameters(t *testing.T) {
const body = `{"model":"gpt-6-astra","input":"hi","max_output_tokens":100,"temperature":0.2,"top_p":0.8,"top_logprobs":5,"include":["message.output_text.logprobs"],"reasoning":{"effort":"high"}}`
var req dto.OpenAIResponsesRequest
require.NoError(t, common.UnmarshalJsonStr(body, &req))
converted, err := (&openai.Adaptor{}).ConvertOpenAIResponsesRequest(nil, &relaycommon.RelayInfo{}, req)
require.NoError(t, err)
encoded, err := common.Marshal(converted)
require.NoError(t, err)
require.JSONEq(t, body, string(encoded))
}
Loading
Loading