feat: add New API channel support

This commit is contained in:
CaIon
2026-07-27 15:20:19 +08:00
parent bc14c18f60
commit 398cdafecf
21 changed files with 408 additions and 128 deletions
+2 -3
View File
@@ -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),
}
}
+4
View File
@@ -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 == "" {
+73
View File
@@ -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 ",