feat: sync deployed v1.0.0-rc.38-custom.42

Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
This commit is contained in:
cneicy committed 2026-09-26 04:49:11 +08:00
1 parent aa6c3f50f4
commit ceddfd3d6b
828 files changed
+122253 -13702

No files matched your search

+25 -28
View File
@@ -77,15 +77,7 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn
case relayconvert.ConverterNone:
return a.claudeAdaptor.ConvertClaudeRequest(c, info, request)
case relayconvert.ConverterClaudeMessagesToOpenAIChat:
result, err := service.ConvertRequestByID(c, info, converter, request)
if err != nil {
return nil, err
}
chatRequest, ok := result.Value.(*dto.GeneralOpenAIRequest)
if !ok {
return nil, fmt.Errorf("expected OpenAI chat completions request, got %T", result.Value)
}
return a.convertOpenAICompatibleRequest(c, info, chatRequest)
return a.convertCrossProtocolChatRequest(c, info, converter, request)
default:
return nil, fmt.Errorf("converter %q does not support Anthropic Messages requests", converter)
}
@@ -101,15 +93,7 @@ func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayIn
case relayconvert.ConverterNone:
return a.geminiAdaptor.ConvertGeminiRequest(c, info, request)
case relayconvert.ConverterGeminiContentToOpenAIChat:
result, err := service.ConvertRequestByID(c, info, converter, request)
if err != nil {
return nil, err
}
chatRequest, ok := result.Value.(*dto.GeneralOpenAIRequest)
if !ok {
return nil, fmt.Errorf("expected OpenAI chat completions request, got %T", result.Value)
}
return a.convertOpenAICompatibleRequest(c, info, chatRequest)
return a.convertCrossProtocolChatRequest(c, info, converter, request)
default:
return nil, fmt.Errorf("converter %q does not support Gemini generateContent requests", converter)
}
@@ -124,15 +108,7 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo
case relayconvert.ConverterNone:
return a.convertOpenAICompatibleResponsesRequest(c, info, request)
case relayconvert.ConverterOpenAIResponsesToOpenAIChat:
result, err := service.ConvertRequestByID(c, info, converter, request)
if err != nil {
return nil, err
}
chatRequest, ok := result.Value.(*dto.GeneralOpenAIRequest)
if !ok {
return nil, fmt.Errorf("expected OpenAI chat completions request, got %T", result.Value)
}
return a.convertOpenAICompatibleRequest(c, info, chatRequest)
return a.convertCrossProtocolChatRequest(c, info, converter, request)
case relayconvert.ConverterOpenAIResponsesToGemini:
result, err := service.ConvertRequestByID(c, info, converter, request)
if err != nil {
@@ -286,7 +262,7 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
if err := a.resolve(c, info); err != nil {
return nil, err
}
if !a.converted && a.converter != relayconvert.ConverterNone {
if !a.converted && !a.route.SupportsPassThroughBody() {
return nil, errors.New("advanced custom converter routes cannot be used with pass-through request body")
}
@@ -307,6 +283,8 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
}
switch a.converter {
case dto.AdvancedCustomConverterSGLangRerank:
return a.doSGLangRerankResponse(c, resp, info)
case relayconvert.ConverterNone:
return a.doNativeResponse(c, resp, info)
case relayconvert.ConverterClaudeMessagesToOpenAIChat,
@@ -534,6 +512,25 @@ func isJSONRequest(c *gin.Context) bool {
return strings.Contains(strings.ToLower(c.Request.Header.Get("Content-Type")), "application/json")
}
// convertCrossProtocolChatRequest converts a Claude, Gemini, or Responses request into an
// OpenAI chat completions request for an OpenAI-compatible upstream. Streaming requests
// always ask the upstream for usage, because the downstream protocol reports usage in its
// own format and never carries stream_options itself.
func (a *Adaptor) convertCrossProtocolChatRequest(c *gin.Context, info *relaycommon.RelayInfo, converter string, request any) (any, error) {
result, err := service.ConvertRequestByID(c, info, converter, request)
if err != nil {
return nil, err
}
chatRequest, ok := result.Value.(*dto.GeneralOpenAIRequest)
if !ok {
return nil, fmt.Errorf("expected OpenAI chat completions request, got %T", result.Value)
}
if info.SupportStreamOptions && info.IsStream {
chatRequest.StreamOptions = &dto.StreamOptions{IncludeUsage: true}
}
return a.convertOpenAICompatibleRequest(c, info, chatRequest)
}
func (a *Adaptor) convertOpenAICompatibleRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
old := info.ChannelType
info.ChannelType = constant.ChannelTypeOpenAI
@@ -829,6 +829,143 @@ func TestAdaptorConvertsGeminiRequestToOpenAIChatUpstream(t *testing.T) {
assert.Equal(t, "user", chatReq.Messages[0].Role)
}
func TestAdaptorCrossProtocolChatUpstreamRequestsStreamUsage(t *testing.T) {
claudeReq := &dto.ClaudeRequest{
Model: "gpt-test",
Messages: []dto.ClaudeMessage{{Role: "user", Content: "hello"}},
}
geminiReq := &dto.GeminiChatRequest{
Contents: []dto.GeminiChatContent{{Role: "user", Parts: []dto.GeminiPart{{Text: "hello"}}}},
}
responsesReq := dto.OpenAIResponsesRequest{
Model: "gpt-test",
Input: mustAdvancedCustomRawMessage(t, "hello"),
}
tests := []struct {
name string
route dto.AdvancedCustomRoute
relayFormat types.RelayFormat
relayMode int
requestPath string
convert func(*Adaptor, *gin.Context, *relaycommon.RelayInfo) (any, error)
isStream bool
supportUsage bool
wantUsage bool
}{
{
name: "claude stream requests usage",
route: dto.AdvancedCustomRoute{
IncomingPath: "/v1/messages",
UpstreamPath: "/v1/chat/completions",
Converter: relayconvert.ConverterClaudeMessagesToOpenAIChat,
},
relayFormat: types.RelayFormatClaude,
relayMode: relayconstant.RelayModeChatCompletions,
requestPath: "/v1/messages",
convert: func(a *Adaptor, c *gin.Context, info *relaycommon.RelayInfo) (any, error) {
return a.ConvertClaudeRequest(c, info, claudeReq)
},
isStream: true,
supportUsage: true,
wantUsage: true,
},
{
name: "gemini stream requests usage",
route: dto.AdvancedCustomRoute{
IncomingPath: "/v1beta/models/{model}:generateContent",
UpstreamPath: "/v1/chat/completions",
Converter: relayconvert.ConverterGeminiContentToOpenAIChat,
},
relayFormat: types.RelayFormatGemini,
relayMode: relayconstant.RelayModeGemini,
requestPath: "/v1beta/models/gpt-test:generateContent",
convert: func(a *Adaptor, c *gin.Context, info *relaycommon.RelayInfo) (any, error) {
return a.ConvertGeminiRequest(c, info, geminiReq)
},
isStream: true,
supportUsage: true,
wantUsage: true,
},
{
name: "responses stream requests usage",
route: dto.AdvancedCustomRoute{
IncomingPath: "/v1/responses",
UpstreamPath: "/v1/chat/completions",
Converter: relayconvert.ConverterOpenAIResponsesToOpenAIChat,
},
relayFormat: types.RelayFormatOpenAIResponses,
relayMode: relayconstant.RelayModeResponses,
requestPath: "/v1/responses",
convert: func(a *Adaptor, c *gin.Context, info *relaycommon.RelayInfo) (any, error) {
return a.ConvertOpenAIResponsesRequest(c, info, responsesReq)
},
isStream: true,
supportUsage: true,
wantUsage: true,
},
{
name: "claude non-stream leaves stream options unset",
route: dto.AdvancedCustomRoute{
IncomingPath: "/v1/messages",
UpstreamPath: "/v1/chat/completions",
Converter: relayconvert.ConverterClaudeMessagesToOpenAIChat,
},
relayFormat: types.RelayFormatClaude,
relayMode: relayconstant.RelayModeChatCompletions,
requestPath: "/v1/messages",
convert: func(a *Adaptor, c *gin.Context, info *relaycommon.RelayInfo) (any, error) {
return a.ConvertClaudeRequest(c, info, claudeReq)
},
isStream: false,
supportUsage: true,
wantUsage: false,
},
{
name: "claude stream without stream options support",
route: dto.AdvancedCustomRoute{
IncomingPath: "/v1/messages",
UpstreamPath: "/v1/chat/completions",
Converter: relayconvert.ConverterClaudeMessagesToOpenAIChat,
},
relayFormat: types.RelayFormatClaude,
relayMode: relayconstant.RelayModeChatCompletions,
requestPath: "/v1/messages",
convert: func(a *Adaptor, c *gin.Context, info *relaycommon.RelayInfo) (any, error) {
return a.ConvertClaudeRequest(c, info, claudeReq)
},
isStream: true,
supportUsage: false,
wantUsage: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
adaptor := &Adaptor{}
info := advancedCustomRelayInfo(&dto.AdvancedCustomConfig{Routes: []dto.AdvancedCustomRoute{tt.route}})
info.RelayFormat = tt.relayFormat
info.RelayMode = tt.relayMode
info.RequestURLPath = tt.requestPath
info.IsStream = tt.isStream
info.SupportStreamOptions = tt.supportUsage
c := advancedCustomGinContext(tt.requestPath)
converted, err := tt.convert(adaptor, c, info)
require.NoError(t, err)
chatReq, ok := converted.(*dto.GeneralOpenAIRequest)
require.True(t, ok)
if !tt.wantUsage {
assert.Nil(t, chatReq.StreamOptions)
return
}
require.NotNil(t, chatReq.StreamOptions)
assert.True(t, chatReq.StreamOptions.IncludeUsage)
})
}
}
func advancedCustomRelayInfo(config *dto.AdvancedCustomConfig) *relaycommon.RelayInfo {
return &relaycommon.RelayInfo{
RelayFormat: types.RelayFormatOpenAI,
@@ -0,0 +1,134 @@
package advancedcustom_test
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/relay"
"github.com/QuantumNous/new-api/relay/channel/advancedcustom"
relaycommon "github.com/QuantumNous/new-api/relay/common"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSGLangChannelProtocols(t *testing.T) {
apiType, ok := common.ChannelType2APIType(constant.ChannelTypeSGLang)
require.True(t, ok)
adaptor := relay.GetAdaptor(apiType)
require.NotNil(t, adaptor)
assert.Equal(t, "advanced_custom", adaptor.GetChannelName())
assert.Contains(t, common.GetEndpointTypesByChannelType(constant.ChannelTypeSGLang, "served-model"), constant.EndpointTypeAnthropic)
assert.Contains(t, common.GetEndpointTypesByChannelType(constant.ChannelTypeSGLang, "served-model"), constant.EndpointTypeOpenAIResponse)
info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{ChannelType: constant.ChannelTypeSGLang, ChannelBaseUrl: "https://inference.example/prefix", ApiKey: "test-key"}}
info.ChannelOtherSettings.AdvancedCustom = common.GetAdvancedCustomPreset(constant.ChannelTypeSGLang)
adaptor.Init(info)
for _, path := range []string{"/v1/chat/completions", "/v1/completions", "/v1/embeddings", "/v1/responses", "/v1/messages"} {
info.RequestURLPath = path
adaptor = &advancedcustom.Adaptor{}
adaptor.Init(info)
url, err := adaptor.GetRequestURL(info)
require.NoError(t, err)
assert.Equal(t, "https://inference.example/prefix"+path, url)
}
for _, format := range []types.RelayFormat{types.RelayFormatOpenAI, types.RelayFormatClaude} {
info.RelayFormat = format
info.RequestURLPath = "/v1/messages"
adaptor = &advancedcustom.Adaptor{}
adaptor.Init(info)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
headers := http.Header{}
require.NoError(t, adaptor.SetupRequestHeader(c, &headers, info))
assert.Equal(t, "Bearer test-key", headers.Get("Authorization"))
}
info.RequestURLPath = "/v1/chat/completions"
adaptor = &advancedcustom.Adaptor{}
adaptor.Init(info)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", nil)
request := &dto.GeneralOpenAIRequest{Model: "served-model"}
converted, err := adaptor.ConvertOpenAIRequest(c, info, request)
require.NoError(t, err)
assert.Same(t, request, converted)
info.RequestURLPath = "/v1/messages"
adaptor = &advancedcustom.Adaptor{}
adaptor.Init(info)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
claudeRequest := &dto.ClaudeRequest{Model: "served-model", Thinking: &dto.Thinking{Type: "adaptive"}}
converted, err = adaptor.ConvertClaudeRequest(c, info, claudeRequest)
require.NoError(t, err)
assert.Same(t, claudeRequest, converted)
info.RequestURLPath = "/v1beta/models/test:generateContent"
adaptor = &advancedcustom.Adaptor{}
adaptor.Init(info)
_, err = adaptor.ConvertGeminiRequest(nil, info, &dto.GeminiChatRequest{})
require.Error(t, err)
}
func TestSGLangRerankRequestAndResponse(t *testing.T) {
service.InitHttpClient()
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/prefix/v1/rerank", r.URL.Path)
assert.Equal(t, "Bearer test-key", r.Header.Get("Authorization"))
var request dto.RerankRequest
require.NoError(t, common.DecodeJson(r.Body, &request))
assert.Equal(t, "served-reranker", request.Model)
assert.Equal(t, []any{"first", "second"}, request.Documents)
_, _ = io.WriteString(w, `[{"index":1,"score":0.9,"document":"second","meta_info":{"prompt_tokens":5}}]`)
}))
defer upstream.Close()
info := &relaycommon.RelayInfo{
ChannelMeta: &relaycommon.ChannelMeta{ChannelType: constant.ChannelTypeSGLang, ChannelBaseUrl: upstream.URL + "/prefix", ApiKey: "test-key"},
RequestURLPath: "/rerank", RelayMode: relayconstant.RelayModeRerank,
RelayFormat: types.RelayFormatOpenAI,
RerankerInfo: &relaycommon.RerankerInfo{Documents: []any{"first", "second"}, ReturnDocuments: true},
}
info.SetEstimatePromptTokens(12)
adaptor := &advancedcustom.Adaptor{}
info.ChannelOtherSettings.AdvancedCustom = common.GetAdvancedCustomPreset(constant.ChannelTypeSGLang)
adaptor.Init(info)
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/rerank", nil).WithContext(context.Background())
converted, err := adaptor.ConvertRerankRequest(c, info.RelayMode, dto.RerankRequest{Model: "served-reranker", Query: "query", Documents: info.Documents})
require.NoError(t, err)
body, err := common.Marshal(converted)
require.NoError(t, err)
response, err := adaptor.DoRequest(c, info, bytes.NewReader(body))
require.NoError(t, err)
usage, apiErr := adaptor.DoResponse(c, response.(*http.Response), info)
require.Nil(t, apiErr)
assert.Equal(t, 12, usage.(*dto.Usage).TotalTokens)
var decoded dto.RerankResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &decoded))
assert.Equal(t, []dto.RerankResponseResult{{Index: 1, RelevanceScore: 0.9, Document: map[string]any{"text": "second"}}}, decoded.Results)
assert.Equal(t, 12, decoded.Usage.PromptTokens)
assert.Zero(t, decoded.Usage.CompletionTokens)
}
func TestSGLangRerankRejectsMalformedResults(t *testing.T) {
for _, body := range []string{`null`, `{}`, `[{"index":99,"score":1}]`, `[{"index":-1,"score":0}]`, `[{"index":0}]`, `[{"score":1}]`} {
t.Run(body, func(t *testing.T) {
adaptor := &advancedcustom.Adaptor{}
info := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{ChannelOtherSettings: dto.ChannelOtherSettings{AdvancedCustom: common.GetAdvancedCustomPreset(constant.ChannelTypeSGLang)}}, RequestURLPath: "/v1/rerank", RelayMode: relayconstant.RelayModeRerank, RerankerInfo: &relaycommon.RerankerInfo{Documents: []any{"only document"}}}
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
usage, err := adaptor.DoResponse(c, &http.Response{Body: io.NopCloser(strings.NewReader(body))}, info)
require.NotNil(t, err)
assert.Nil(t, usage)
assert.Empty(t, recorder.Body.String())
})
}
}
+46
View File
@@ -0,0 +1,46 @@
package advancedcustom
import (
"errors"
"net/http"
"github.com/QuantumNous/new-api/common"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
)
func (a *Adaptor) doSGLangRerankResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (any, *types.NewAPIError) {
defer service.CloseResponseBodyGracefully(resp)
// SGLang v0.5.19 returns a sorted array with score, not a Jina results envelope.
// https://github.com/sgl-project/sglang/blob/v0.5.19/python/sglang/srt/entrypoints/openai/serving_rerank.py
var results []struct {
Index *int `json:"index"`
Score *float64 `json:"score"`
}
if err := common.DecodeJson(resp.Body, &results); err != nil {
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusBadGateway)
}
if results == nil {
return nil, types.NewOpenAIError(errors.New("invalid SGLang rerank response"), types.ErrorCodeBadResponseBody, http.StatusBadGateway)
}
response := dto.RerankResponse{Results: make([]dto.RerankResponseResult, 0, len(results))}
for _, result := range results {
if result.Index == nil || result.Score == nil || *result.Index < 0 || *result.Index >= len(info.Documents) {
return nil, types.NewOpenAIError(errors.New("invalid SGLang rerank result"), types.ErrorCodeBadResponseBody, http.StatusBadGateway)
}
item := dto.RerankResponseResult{Index: *result.Index, RelevanceScore: *result.Score}
if info.ReturnDocuments {
item.Document = dto.RerankDocument{Text: info.Documents[*result.Index]}
}
response.Results = append(response.Results, item)
}
// top_n can omit scored documents, and decoder rerankers can omit meta_info.
// Per-result counts therefore cannot account for the full request; use the
// existing prompt estimate, as with other rerank providers without usage.
response.Usage = dto.Usage{PromptTokens: info.GetEstimatePromptTokens(), TotalTokens: info.GetEstimatePromptTokens()}
c.JSON(http.StatusOK, response)
return &response.Usage, nil
}