Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
398cdafe
authored
Jul 27, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add New API channel support
parent
bc14c18f
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
408 additions
and
128 deletions
+408
-128
common/api_type.go
+14
-0
common/endpoint_type.go
+2
-1
constant/api_type.go
+1
-0
constant/channel.go
+3
-0
controller/channel-test.go
+2
-3
controller/channel.go
+4
-0
controller/channel_test_internal_test.go
+73
-0
controller/channel_upstream_update_test.go
+24
-0
relay/alpha_search_handler.go
+4
-1
relay/channel/newapi/adaptor.go
+121
-0
relay/channel/newapi/constants.go
+6
-0
relay/channel/sub2api/adaptor.go
+2
-106
relay/channel/sub2api/adaptor_test.go
+19
-0
relay/common/relay_info.go
+1
-0
relay/relay_adaptor.go
+3
-0
relay/responses_handler.go
+8
-12
web/src/features/channels/constants.ts
+8
-4
web/src/features/channels/lib/__tests__/new-api-channel.test.ts
+97
-0
web/src/features/channels/lib/channel-form.ts
+5
-1
web/src/features/channels/lib/channel-type-config.ts
+10
-0
web/src/features/channels/lib/channel-utils.ts
+1
-0
No files found.
common/api_type.go
View file @
398cdafe
...
...
@@ -79,9 +79,23 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType
=
constant
.
APITypeAdvancedCustom
case
constant
.
ChannelTypeSub2API
:
apiType
=
constant
.
APITypeSub2API
case
constant
.
ChannelTypeNewAPI
:
apiType
=
constant
.
APITypeNewAPI
}
if
apiType
==
-
1
{
return
constant
.
APITypeOpenAI
,
false
}
return
apiType
,
true
}
func
IsResponsesCompactAPIType
(
apiType
int
)
bool
{
switch
apiType
{
case
constant
.
APITypeOpenAI
,
constant
.
APITypeCodex
,
constant
.
APITypeSub2API
,
constant
.
APITypeNewAPI
:
return
true
default
:
return
false
}
}
common/endpoint_type.go
View file @
398cdafe
...
...
@@ -30,10 +30,11 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
endpointTypes
=
[]
constant
.
EndpointType
{
constant
.
EndpointTypeOpenAI
,
constant
.
EndpointTypeOpenAIResponse
}
case
constant
.
ChannelTypeSora
:
endpointTypes
=
[]
constant
.
EndpointType
{
constant
.
EndpointTypeOpenAIVideo
}
case
constant
.
ChannelTypeSub2API
:
case
constant
.
ChannelTypeSub2API
,
constant
.
ChannelTypeNewAPI
:
endpointTypes
=
[]
constant
.
EndpointType
{
constant
.
EndpointTypeOpenAI
,
constant
.
EndpointTypeOpenAIResponse
,
constant
.
EndpointTypeOpenAIResponseCompact
,
constant
.
EndpointTypeAnthropic
,
constant
.
EndpointTypeGemini
,
constant
.
EndpointTypeOpenAIAlphaSearch
,
...
...
constant/api_type.go
View file @
398cdafe
...
...
@@ -38,5 +38,6 @@ const (
APITypeCodex
APITypeAdvancedCustom
APITypeSub2API
APITypeNewAPI
APITypeDummy
// this one is only for count, do not add any channel after this
)
constant/channel.go
View file @
398cdafe
...
...
@@ -57,6 +57,7 @@ const (
ChannelTypeCodex
=
57
ChannelTypeAdvancedCustom
=
58
ChannelTypeSub2API
=
59
ChannelTypeNewAPI
=
60
ChannelTypeDummy
// this one is only for count, do not add any channel after this
)
...
...
@@ -122,6 +123,7 @@ var ChannelBaseURLs = []string{
"https://chatgpt.com"
,
//57
""
,
//58
""
,
//59
""
,
//60
}
var
ChannelTypeNames
=
map
[
int
]
string
{
...
...
@@ -181,6 +183,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeCodex
:
"ChatGPT Subscription (Codex)"
,
ChannelTypeAdvancedCustom
:
"Advanced Custom"
,
ChannelTypeSub2API
:
"Sub2API"
,
ChannelTypeNewAPI
:
"New API"
,
}
func
GetChannelTypeName
(
channelType
int
)
string
{
...
...
controller/channel-test.go
View file @
398cdafe
...
...
@@ -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
),
}
}
...
...
controller/channel.go
View file @
398cdafe
...
...
@@ -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
==
""
{
...
...
controller/channel_test_internal_test.go
View file @
398cdafe
...
...
@@ -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
{
...
...
controller/channel_upstream_update_test.go
View file @
398cdafe
...
...
@@ -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 "
,
...
...
relay/alpha_search_handler.go
View file @
398cdafe
...
...
@@ -22,7 +22,10 @@ func AlphaSearchHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError
info
.
InitChannelMeta
(
c
)
switch
info
.
ChannelType
{
case
constant
.
ChannelTypeSub2API
,
constant
.
ChannelTypeCodex
,
constant
.
ChannelTypeAdvancedCustom
:
case
constant
.
ChannelTypeSub2API
,
constant
.
ChannelTypeNewAPI
,
constant
.
ChannelTypeCodex
,
constant
.
ChannelTypeAdvancedCustom
:
default
:
// Allow retry onto another channel that may support this endpoint.
return
types
.
NewError
(
...
...
relay/channel/newapi/adaptor.go
0 → 100644
View file @
398cdafe
package
newapi
import
(
"errors"
"io"
"net/http"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/relay/channel"
"github.com/QuantumNous/new-api/relay/channel/claude"
"github.com/QuantumNous/new-api/relay/channel/gemini"
"github.com/QuantumNous/new-api/relay/channel/openai"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
relayconstant
"github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
)
type
Adaptor
struct
{
openaiAdaptor
openai
.
Adaptor
claudeAdaptor
claude
.
Adaptor
geminiAdaptor
gemini
.
Adaptor
}
func
(
a
*
Adaptor
)
Init
(
info
*
relaycommon
.
RelayInfo
)
{
a
.
openaiAdaptor
.
Init
(
info
)
a
.
claudeAdaptor
.
Init
(
info
)
a
.
geminiAdaptor
.
Init
(
info
)
}
func
(
a
*
Adaptor
)
GetRequestURL
(
info
*
relaycommon
.
RelayInfo
)
(
string
,
error
)
{
if
info
.
RelayMode
==
relayconstant
.
RelayModeAlphaSearch
{
return
relaycommon
.
GetFullRequestURL
(
info
.
ChannelBaseUrl
,
"/v1/alpha/search"
,
info
.
ChannelType
),
nil
}
return
relaycommon
.
GetFullRequestURL
(
info
.
ChannelBaseUrl
,
info
.
RequestURLPath
,
info
.
ChannelType
),
nil
}
func
(
a
*
Adaptor
)
SetupRequestHeader
(
c
*
gin
.
Context
,
req
*
http
.
Header
,
info
*
relaycommon
.
RelayInfo
)
error
{
channel
.
SetupApiRequestHeader
(
info
,
c
,
req
)
req
.
Set
(
"Authorization"
,
"Bearer "
+
info
.
ApiKey
)
switch
info
.
RelayFormat
{
case
types
.
RelayFormatClaude
:
req
.
Set
(
"x-api-key"
,
info
.
ApiKey
)
if
req
.
Get
(
"anthropic-version"
)
==
""
{
anthropicVersion
:=
c
.
Request
.
Header
.
Get
(
"anthropic-version"
)
if
anthropicVersion
==
""
{
anthropicVersion
=
"2023-06-01"
}
req
.
Set
(
"anthropic-version"
,
anthropicVersion
)
}
case
types
.
RelayFormatGemini
:
req
.
Set
(
"x-goog-api-key"
,
info
.
ApiKey
)
}
return
nil
}
func
(
a
*
Adaptor
)
ConvertOpenAIRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
*
dto
.
GeneralOpenAIRequest
)
(
any
,
error
)
{
if
request
==
nil
{
return
nil
,
errors
.
New
(
"request is nil"
)
}
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertOpenAIResponsesRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
OpenAIResponsesRequest
)
(
any
,
error
)
{
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertEmbeddingRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
EmbeddingRequest
)
(
any
,
error
)
{
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertClaudeRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
*
dto
.
ClaudeRequest
)
(
any
,
error
)
{
if
request
==
nil
{
return
nil
,
errors
.
New
(
"request is nil"
)
}
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertGeminiRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
*
dto
.
GeminiChatRequest
)
(
any
,
error
)
{
if
request
==
nil
{
return
nil
,
errors
.
New
(
"request is nil"
)
}
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertImageRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
ImageRequest
)
(
any
,
error
)
{
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertRerankRequest
(
c
*
gin
.
Context
,
relayMode
int
,
request
dto
.
RerankRequest
)
(
any
,
error
)
{
return
nil
,
errors
.
New
(
"endpoint not supported"
)
}
func
(
a
*
Adaptor
)
ConvertAudioRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
AudioRequest
)
(
io
.
Reader
,
error
)
{
return
nil
,
errors
.
New
(
"endpoint not supported"
)
}
func
(
a
*
Adaptor
)
DoRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
requestBody
io
.
Reader
)
(
any
,
error
)
{
return
channel
.
DoApiRequest
(
a
,
c
,
info
,
requestBody
)
}
func
(
a
*
Adaptor
)
DoResponse
(
c
*
gin
.
Context
,
resp
*
http
.
Response
,
info
*
relaycommon
.
RelayInfo
)
(
usage
any
,
err
*
types
.
NewAPIError
)
{
switch
info
.
RelayFormat
{
case
types
.
RelayFormatClaude
:
return
a
.
claudeAdaptor
.
DoResponse
(
c
,
resp
,
info
)
case
types
.
RelayFormatGemini
:
return
a
.
geminiAdaptor
.
DoResponse
(
c
,
resp
,
info
)
default
:
return
a
.
openaiAdaptor
.
DoResponse
(
c
,
resp
,
info
)
}
}
func
(
a
*
Adaptor
)
GetModelList
()
[]
string
{
return
ModelList
}
func
(
a
*
Adaptor
)
GetChannelName
()
string
{
return
ChannelName
}
relay/channel/newapi/constants.go
0 → 100644
View file @
398cdafe
package
newapi
const
ChannelName
=
"newapi"
// ModelList is empty because models are fetched dynamically from upstream /v1/models.
var
ModelList
=
[]
string
{}
relay/channel/sub2api/adaptor.go
View file @
398cdafe
package
sub2api
import
(
"errors"
"io"
"net/http"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/relay/channel"
"github.com/QuantumNous/new-api/relay/channel/claude"
"github.com/QuantumNous/new-api/relay/channel/gemini"
"github.com/QuantumNous/new-api/relay/channel/openai"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
relayconstant
"github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
"github.com/QuantumNous/new-api/relay/channel/newapi"
)
type
Adaptor
struct
{
openaiAdaptor
openai
.
Adaptor
claudeAdaptor
claude
.
Adaptor
geminiAdaptor
gemini
.
Adaptor
}
func
(
a
*
Adaptor
)
Init
(
info
*
relaycommon
.
RelayInfo
)
{
a
.
openaiAdaptor
.
Init
(
info
)
a
.
claudeAdaptor
.
Init
(
info
)
a
.
geminiAdaptor
.
Init
(
info
)
}
func
(
a
*
Adaptor
)
GetRequestURL
(
info
*
relaycommon
.
RelayInfo
)
(
string
,
error
)
{
if
info
.
RelayMode
==
relayconstant
.
RelayModeAlphaSearch
{
return
relaycommon
.
GetFullRequestURL
(
info
.
ChannelBaseUrl
,
"/v1/alpha/search"
,
info
.
ChannelType
),
nil
}
return
relaycommon
.
GetFullRequestURL
(
info
.
ChannelBaseUrl
,
info
.
RequestURLPath
,
info
.
ChannelType
),
nil
}
func
(
a
*
Adaptor
)
SetupRequestHeader
(
c
*
gin
.
Context
,
req
*
http
.
Header
,
info
*
relaycommon
.
RelayInfo
)
error
{
channel
.
SetupApiRequestHeader
(
info
,
c
,
req
)
req
.
Set
(
"Authorization"
,
"Bearer "
+
info
.
ApiKey
)
switch
info
.
RelayFormat
{
case
types
.
RelayFormatClaude
:
req
.
Set
(
"x-api-key"
,
info
.
ApiKey
)
if
req
.
Get
(
"anthropic-version"
)
==
""
{
anthropicVersion
:=
c
.
Request
.
Header
.
Get
(
"anthropic-version"
)
if
anthropicVersion
==
""
{
anthropicVersion
=
"2023-06-01"
}
req
.
Set
(
"anthropic-version"
,
anthropicVersion
)
}
case
types
.
RelayFormatGemini
:
req
.
Set
(
"x-goog-api-key"
,
info
.
ApiKey
)
}
return
nil
}
func
(
a
*
Adaptor
)
ConvertOpenAIRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
*
dto
.
GeneralOpenAIRequest
)
(
any
,
error
)
{
if
request
==
nil
{
return
nil
,
errors
.
New
(
"request is nil"
)
}
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertOpenAIResponsesRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
OpenAIResponsesRequest
)
(
any
,
error
)
{
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertEmbeddingRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
EmbeddingRequest
)
(
any
,
error
)
{
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertClaudeRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
*
dto
.
ClaudeRequest
)
(
any
,
error
)
{
if
request
==
nil
{
return
nil
,
errors
.
New
(
"request is nil"
)
}
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertGeminiRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
*
dto
.
GeminiChatRequest
)
(
any
,
error
)
{
if
request
==
nil
{
return
nil
,
errors
.
New
(
"request is nil"
)
}
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertImageRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
ImageRequest
)
(
any
,
error
)
{
return
request
,
nil
}
func
(
a
*
Adaptor
)
ConvertRerankRequest
(
c
*
gin
.
Context
,
relayMode
int
,
request
dto
.
RerankRequest
)
(
any
,
error
)
{
return
nil
,
errors
.
New
(
"endpoint not supported"
)
}
func
(
a
*
Adaptor
)
ConvertAudioRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
AudioRequest
)
(
io
.
Reader
,
error
)
{
return
nil
,
errors
.
New
(
"endpoint not supported"
)
}
func
(
a
*
Adaptor
)
DoRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
requestBody
io
.
Reader
)
(
any
,
error
)
{
return
channel
.
DoApiRequest
(
a
,
c
,
info
,
requestBody
)
}
func
(
a
*
Adaptor
)
DoResponse
(
c
*
gin
.
Context
,
resp
*
http
.
Response
,
info
*
relaycommon
.
RelayInfo
)
(
usage
any
,
err
*
types
.
NewAPIError
)
{
switch
info
.
RelayFormat
{
case
types
.
RelayFormatClaude
:
return
a
.
claudeAdaptor
.
DoResponse
(
c
,
resp
,
info
)
case
types
.
RelayFormatGemini
:
return
a
.
geminiAdaptor
.
DoResponse
(
c
,
resp
,
info
)
default
:
return
a
.
openaiAdaptor
.
DoResponse
(
c
,
resp
,
info
)
}
newapi
.
Adaptor
}
func
(
a
*
Adaptor
)
GetModelList
()
[]
string
{
...
...
relay/channel/sub2api/adaptor_test.go
View file @
398cdafe
...
...
@@ -25,3 +25,22 @@ func TestGetRequestURLAlphaSearch(t *testing.T) {
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
"https://sub2api.example/v1/alpha/search"
,
url
)
}
func
TestAdaptorInheritsNewAPIResponsesCompactSupport
(
t
*
testing
.
T
)
{
adaptor
:=
&
Adaptor
{}
info
:=
&
relaycommon
.
RelayInfo
{
ChannelMeta
:
&
relaycommon
.
ChannelMeta
{
ChannelType
:
constant
.
ChannelTypeSub2API
,
ChannelBaseUrl
:
"https://sub2api.example"
,
},
RequestURLPath
:
"/v1/responses/compact"
,
RelayMode
:
relayconstant
.
RelayModeResponsesCompact
,
}
url
,
err
:=
adaptor
.
GetRequestURL
(
info
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
"https://sub2api.example/v1/responses/compact"
,
url
)
assert
.
Equal
(
t
,
"sub2api"
,
adaptor
.
GetChannelName
())
assert
.
Empty
(
t
,
adaptor
.
GetModelList
())
}
relay/common/relay_info.go
View file @
398cdafe
...
...
@@ -343,6 +343,7 @@ var streamSupportedChannels = map[int]bool{
constant
.
ChannelTypeSiliconFlow
:
true
,
constant
.
ChannelTypeAdvancedCustom
:
true
,
constant
.
ChannelTypeSub2API
:
true
,
constant
.
ChannelTypeNewAPI
:
true
,
constant
.
ChannelTypeTencent
:
true
,
}
...
...
relay/relay_adaptor.go
View file @
398cdafe
...
...
@@ -24,6 +24,7 @@ import (
"github.com/QuantumNous/new-api/relay/channel/mistral"
"github.com/QuantumNous/new-api/relay/channel/mokaai"
"github.com/QuantumNous/new-api/relay/channel/moonshot"
"github.com/QuantumNous/new-api/relay/channel/newapi"
"github.com/QuantumNous/new-api/relay/channel/ollama"
"github.com/QuantumNous/new-api/relay/channel/openai"
"github.com/QuantumNous/new-api/relay/channel/palm"
...
...
@@ -126,6 +127,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
return
&
advancedcustom
.
Adaptor
{}
case
constant
.
APITypeSub2API
:
return
&
sub2api
.
Adaptor
{}
case
constant
.
APITypeNewAPI
:
return
&
newapi
.
Adaptor
{}
}
return
nil
}
...
...
relay/responses_handler.go
View file @
398cdafe
...
...
@@ -7,7 +7,6 @@ import (
"strings"
"github.com/QuantumNous/new-api/common"
appconstant
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/logger"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
...
...
@@ -22,17 +21,14 @@ import (
func
ResponsesHelper
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
)
(
newAPIError
*
types
.
NewAPIError
)
{
info
.
InitChannelMeta
(
c
)
if
info
.
RelayMode
==
relayconstant
.
RelayModeResponsesCompact
{
switch
info
.
ApiType
{
case
appconstant
.
APITypeOpenAI
,
appconstant
.
APITypeCodex
:
default
:
return
types
.
NewErrorWithStatusCode
(
fmt
.
Errorf
(
"unsupported endpoint %q for api type %d"
,
"/v1/responses/compact"
,
info
.
ApiType
),
types
.
ErrorCodeInvalidRequest
,
http
.
StatusBadRequest
,
types
.
ErrOptionWithSkipRetry
(),
)
}
if
info
.
RelayMode
==
relayconstant
.
RelayModeResponsesCompact
&&
!
common
.
IsResponsesCompactAPIType
(
info
.
ApiType
)
{
return
types
.
NewErrorWithStatusCode
(
fmt
.
Errorf
(
"unsupported endpoint %q for api type %d"
,
"/v1/responses/compact"
,
info
.
ApiType
),
types
.
ErrorCodeInvalidRequest
,
http
.
StatusBadRequest
,
types
.
ErrOptionWithSkipRetry
(),
)
}
var
responsesReq
*
dto
.
OpenAIResponsesRequest
...
...
web/src/features/channels/constants.ts
View file @
398cdafe
...
...
@@ -21,6 +21,8 @@ For commercial licensing, please contact support@quantumnous.com
// All label/name values are i18n keys; use t(value) when displaying.
// ============================================================================
export
const
CHANNEL_TYPE_NEW_API
=
60
export
const
CHANNEL_TYPES
=
{
0
:
'Unknown'
,
1
:
'OpenAI'
,
...
...
@@ -78,12 +80,13 @@ export const CHANNEL_TYPES = {
57
:
'ChatGPT Subscription (Codex)'
,
58
:
'Advanced Custom'
,
59
:
'Sub2API'
,
60
:
'New API'
,
}
as
const
const
CHANNEL_TYPE_DISPLAY_ORDER
:
number
[]
=
[
1
,
14
,
33
,
24
,
43
,
3
,
41
,
48
,
58
,
42
,
34
,
20
,
4
,
40
,
27
,
25
,
17
,
26
,
15
,
46
,
23
,
18
,
45
,
31
,
35
,
49
,
19
,
47
,
37
,
38
,
39
,
11
,
8
,
57
,
59
,
22
,
21
,
44
,
2
,
5
,
36
,
50
,
51
,
52
,
53
,
54
,
55
,
56
,
1
,
14
,
33
,
24
,
43
,
3
,
41
,
48
,
60
,
58
,
42
,
34
,
20
,
4
,
40
,
27
,
25
,
17
,
26
,
15
,
46
,
23
,
18
,
45
,
31
,
35
,
49
,
19
,
47
,
37
,
38
,
39
,
11
,
8
,
57
,
59
,
22
,
21
,
44
,
2
,
5
,
36
,
50
,
51
,
52
,
53
,
54
,
55
,
56
,
]
export
const
CHANNEL_TYPE_OPTIONS
:
{
value
:
number
;
label
:
string
}[]
=
(()
=>
{
...
...
@@ -381,7 +384,7 @@ export const FIELD_DESCRIPTIONS = {
export
const
MODEL_FETCHABLE_TYPES
=
new
Set
([
1
,
4
,
14
,
17
,
20
,
23
,
24
,
25
,
26
,
27
,
31
,
34
,
35
,
40
,
42
,
43
,
47
,
48
,
57
,
58
,
59
,
59
,
60
,
])
export
const
TYPE_TO_KEY_PROMPT
:
Record
<
number
,
string
>
=
{
...
...
@@ -394,6 +397,7 @@ export const TYPE_TO_KEY_PROMPT: Record<number, string> = {
51
:
'Format: Access Key ID|Secret Access Key'
,
57
:
'Paste Codex OAuth JSON credential (access_token / refresh_token / account_id)'
,
59
:
'Enter API key for this channel'
,
60
:
'Enter API key for this channel'
,
}
export
const
CHANNEL_TYPE_WARNINGS
:
Record
<
number
,
string
>
=
{
...
...
web/src/features/channels/lib/__tests__/new-api-channel.test.ts
0 → 100644
View file @
398cdafe
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
assert
from
'node:assert/strict'
import
{
describe
,
test
}
from
'node:test'
import
{
CHANNEL_TYPE_NEW_API
,
CHANNEL_TYPE_OPTIONS
,
MODEL_FETCHABLE_TYPES
,
}
from
'../../constants'
import
{
CHANNEL_FORM_DEFAULT_VALUES
,
channelFormSchema
}
from
'../channel-form'
import
{
getChannelTypeConfig
}
from
'../channel-type-config'
import
{
getChannelTypeIcon
,
getKeyPromptForType
}
from
'../channel-utils'
function
newAPIForm
(
baseUrl
:
string
)
{
return
{
...
CHANNEL_FORM_DEFAULT_VALUES
,
name
:
'New API upstream'
,
type
:
CHANNEL_TYPE_NEW_API
,
base_url
:
baseUrl
,
key
:
'test-key'
,
models
:
'gpt-5'
,
}
}
describe
(
'New API channel'
,
()
=>
{
test
(
'registers selection, ordering, model discovery, and icon metadata'
,
()
=>
{
const
option
=
CHANNEL_TYPE_OPTIONS
.
find
(
(
item
)
=>
item
.
value
===
CHANNEL_TYPE_NEW_API
)
assert
.
deepEqual
(
option
,
{
value
:
CHANNEL_TYPE_NEW_API
,
label
:
'New API'
,
})
assert
.
equal
(
CHANNEL_TYPE_OPTIONS
.
findIndex
(
(
item
)
=>
item
.
value
===
CHANNEL_TYPE_NEW_API
)
+
1
,
CHANNEL_TYPE_OPTIONS
.
findIndex
((
item
)
=>
item
.
value
===
58
)
)
assert
.
equal
(
MODEL_FETCHABLE_TYPES
.
has
(
CHANNEL_TYPE_NEW_API
),
true
)
assert
.
equal
(
getChannelTypeIcon
(
CHANNEL_TYPE_NEW_API
),
'NewAPI'
)
assert
.
equal
(
getKeyPromptForType
(
CHANNEL_TYPE_NEW_API
),
'Enter API key for this channel'
)
assert
.
equal
(
getChannelTypeConfig
(
CHANNEL_TYPE_NEW_API
).
icon
,
'NewAPI'
)
})
test
(
'requires a non-blank Base URL'
,
()
=>
{
const
blankResult
=
channelFormSchema
.
safeParse
(
newAPIForm
(
' '
))
assert
.
equal
(
blankResult
.
success
,
false
)
if
(
!
blankResult
.
success
)
{
assert
.
equal
(
blankResult
.
error
.
issues
.
some
(
(
issue
)
=>
issue
.
path
[
0
]
===
'base_url'
&&
issue
.
message
===
'Base URL is required for this channel type'
),
true
)
}
assert
.
equal
(
channelFormSchema
.
safeParse
(
newAPIForm
(
'https://new-api.example'
))
.
success
,
true
)
})
test
(
'keeps Sub2API Base URL validation unchanged'
,
()
=>
{
const
result
=
channelFormSchema
.
safeParse
({
...
newAPIForm
(
''
),
type
:
59
,
})
assert
.
equal
(
result
.
success
,
true
)
})
})
web/src/features/channels/lib/channel-form.ts
View file @
398cdafe
...
...
@@ -19,6 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import
{
z
}
from
'zod'
import
{
CHANNEL_TYPE_NEW_API
,
CHANNEL_STATUS
,
ERROR_MESSAGES
,
MODEL_FETCHABLE_TYPES
,
...
...
@@ -247,7 +248,10 @@ export const channelFormSchema = z
upstream_model_update_ignored_models
:
z
.
string
().
optional
(),
})
.
superRefine
((
data
,
ctx
)
=>
{
if
([
3
,
8
,
36
,
45
].
includes
(
data
.
type
)
&&
!
data
.
base_url
?.
trim
())
{
if
(
[
3
,
8
,
36
,
45
,
CHANNEL_TYPE_NEW_API
].
includes
(
data
.
type
)
&&
!
data
.
base_url
?.
trim
()
)
{
addRequiredIssue
(
ctx
,
'base_url'
,
...
...
web/src/features/channels/lib/channel-type-config.ts
View file @
398cdafe
...
...
@@ -154,6 +154,16 @@ export const CHANNEL_TYPE_CONFIGS: Record<number, ChannelTypeConfig> = {
models
:
'Models fetched from upstream /v1/models'
,
},
},
60
:
{
id
:
60
,
name
:
CHANNEL_TYPES
[
60
],
icon
:
'NewAPI'
,
hints
:
{
baseUrl
:
'Base URL is required for this channel type'
,
key
:
'Enter API key for this channel'
,
models
:
'Models'
,
},
},
}
/**
...
...
web/src/features/channels/lib/channel-utils.ts
View file @
398cdafe
...
...
@@ -53,6 +53,7 @@ export function getChannelTypeIcon(type: number): string {
8
:
'OpenAI'
,
// Custom
58
:
'NewAPI'
,
// Advanced Custom
59
:
'Sub2API'
,
// Sub2API
60
:
'NewAPI'
,
// New API
3
:
'Azure'
,
// Azure
// Anthropic
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment