feat: add New API channel support
This commit is contained in:
@@ -271,11 +271,10 @@ func testChannel(ctx context.Context, channel *model.Channel, testUserID int, te
|
||||
|
||||
apiType, _ := common.ChannelType2APIType(channel.Type)
|
||||
if info.RelayMode == relayconstant.RelayModeResponsesCompact &&
|
||||
apiType != constant.APITypeOpenAI &&
|
||||
apiType != constant.APITypeCodex {
|
||||
!common.IsResponsesCompactAPIType(apiType) {
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: fmt.Errorf("responses compaction test only supports openai/codex channels, got api type %d", apiType),
|
||||
localErr: fmt.Errorf("responses compaction test is not supported for api type %d", apiType),
|
||||
newAPIError: types.NewError(fmt.Errorf("unsupported api type: %d", apiType), types.ErrorCodeInvalidApiType),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,6 +481,10 @@ func validateChannel(channel *model.Channel, isAdd bool) error {
|
||||
return fmt.Errorf("渠道额外设置[channel setting] 格式错误:%s", err.Error())
|
||||
}
|
||||
|
||||
if channel.Type == constant.ChannelTypeNewAPI && strings.TrimSpace(channel.GetBaseURL()) == "" {
|
||||
return fmt.Errorf("New API channel base URL cannot be empty")
|
||||
}
|
||||
|
||||
// 如果是添加操作,检查 channel 和 key 是否为空
|
||||
if isAdd {
|
||||
if channel.Key == "" {
|
||||
|
||||
@@ -56,6 +56,79 @@ func TestValidateChannelProxy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateChannelRequiresNewAPIBaseURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseURL *string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "missing", wantErr: true},
|
||||
{name: "blank", baseURL: common.GetPointer(" "), wantErr: true},
|
||||
{name: "configured", baseURL: common.GetPointer("https://new-api.example")},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
channel := &model.Channel{
|
||||
Type: constant.ChannelTypeNewAPI,
|
||||
BaseURL: test.baseURL,
|
||||
}
|
||||
|
||||
err := validateChannel(channel, false)
|
||||
|
||||
if test.wantErr {
|
||||
require.ErrorContains(t, err, "New API channel base URL cannot be empty")
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAPIChannelRegistration(t *testing.T) {
|
||||
apiType, ok := common.ChannelType2APIType(constant.ChannelTypeNewAPI)
|
||||
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, constant.APITypeNewAPI, apiType)
|
||||
assert.Equal(t, "New API", constant.GetChannelTypeName(constant.ChannelTypeNewAPI))
|
||||
require.Greater(t, len(constant.ChannelBaseURLs), constant.ChannelTypeNewAPI)
|
||||
assert.Empty(t, constant.ChannelBaseURLs[constant.ChannelTypeNewAPI])
|
||||
}
|
||||
|
||||
func TestResponsesCompactAPITypeSupport(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
apiType int
|
||||
want bool
|
||||
}{
|
||||
{name: "OpenAI", apiType: constant.APITypeOpenAI, want: true},
|
||||
{name: "Codex", apiType: constant.APITypeCodex, want: true},
|
||||
{name: "Sub2API", apiType: constant.APITypeSub2API, want: true},
|
||||
{name: "New API", apiType: constant.APITypeNewAPI, want: true},
|
||||
{name: "Anthropic", apiType: constant.APITypeAnthropic, want: false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.want, common.IsResponsesCompactAPIType(test.apiType))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiprotocolGatewayEndpointTypes(t *testing.T) {
|
||||
want := []constant.EndpointType{
|
||||
constant.EndpointTypeOpenAI,
|
||||
constant.EndpointTypeOpenAIResponse,
|
||||
constant.EndpointTypeOpenAIResponseCompact,
|
||||
constant.EndpointTypeAnthropic,
|
||||
constant.EndpointTypeGemini,
|
||||
constant.EndpointTypeOpenAIAlphaSearch,
|
||||
}
|
||||
|
||||
assert.Equal(t, want, common.GetEndpointTypesByChannelType(constant.ChannelTypeNewAPI, "gpt-5"))
|
||||
assert.Equal(t, want, common.GetEndpointTypesByChannelType(constant.ChannelTypeSub2API, "gpt-5"))
|
||||
}
|
||||
|
||||
func TestCopyChannelRejectsInvalidLegacyProxySettings(t *testing.T) {
|
||||
db := setupModelListControllerTestDB(t)
|
||||
settingBytes, err := common.Marshal(dto.ChannelSettings{
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -384,6 +385,29 @@ func TestFetchModelsUsesSharedChannelFetchBehavior(t *testing.T) {
|
||||
require.JSONEq(t, `{"success":true,"message":"","data":["claude-sonnet"]}`, recorder.Body.String())
|
||||
}
|
||||
|
||||
func TestFetchNewAPIModelsUsesOpenAIContract(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/v1/models", r.URL.Path)
|
||||
assert.Equal(t, "Bearer new-api-key", r.Header.Get("Authorization"))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err := w.Write([]byte(`{"data":[{"id":"gpt-5"},{"id":" gpt-5-mini "}]}`))
|
||||
assert.NoError(t, err)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
baseURL := server.URL
|
||||
channel := &model.Channel{
|
||||
Type: constant.ChannelTypeNewAPI,
|
||||
Key: "new-api-key",
|
||||
BaseURL: &baseURL,
|
||||
}
|
||||
|
||||
models, err := fetchChannelUpstreamModelIDs(channel)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"gpt-5", "gpt-5-mini"}, models)
|
||||
}
|
||||
|
||||
func TestNormalizeModelNames(t *testing.T) {
|
||||
result := normalizeModelNames([]string{
|
||||
" gpt-4o ",
|
||||
|
||||
Reference in New Issue
Block a user