Merge branch 'sub' into feature/subscription

This commit is contained in:
t0ng7u
2026-02-03 01:29:45 +08:00
10 changed files with 150 additions and 58 deletions

View File

@@ -817,6 +817,10 @@ type OpenAIResponsesRequest struct {
User string `json:"user,omitempty"`
MaxToolCalls uint `json:"max_tool_calls,omitempty"`
Prompt json.RawMessage `json:"prompt,omitempty"`
// qwen
EnableThinking json.RawMessage `json:"enable_thinking,omitempty"`
// perplexity
Preset json.RawMessage `json:"preset,omitempty"`
}
func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta {

View File

@@ -84,6 +84,8 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
fullRequestURL = fmt.Sprintf("%s/compatible-mode/v1/embeddings", info.ChannelBaseUrl)
case constant.RelayModeRerank:
fullRequestURL = fmt.Sprintf("%s/api/v1/services/rerank/text-rerank/text-rerank", info.ChannelBaseUrl)
case constant.RelayModeResponses:
fullRequestURL = fmt.Sprintf("%s/api/v2/apps/protocols/compatible-mode/v1/responses", info.ChannelBaseUrl)
case constant.RelayModeImagesGenerations:
if isSyncImageModel(info.OriginModelName) {
fullRequestURL = fmt.Sprintf("%s/api/v1/services/aigc/multimodal-generation/generation", info.ChannelBaseUrl)
@@ -210,8 +212,7 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
}
func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
//TODO implement me
return nil, errors.New("not implemented")
return request, nil
}
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {

View File

@@ -98,6 +98,19 @@ func processHeaderOverride(info *common.RelayInfo, c *gin.Context) (map[string]s
return headerOverride, nil
}
func applyHeaderOverrideToRequest(req *http.Request, headerOverride map[string]string) {
if req == nil {
return
}
for key, value := range headerOverride {
req.Header.Set(key, value)
// set Host in req
if strings.EqualFold(key, "Host") {
req.Host = value
}
}
}
func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*http.Response, error) {
fullRequestURL, err := a.GetRequestURL(info)
if err != nil {
@@ -121,9 +134,7 @@ func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody
if err != nil {
return nil, err
}
for key, value := range headerOverride {
headers.Set(key, value)
}
applyHeaderOverrideToRequest(req, headerOverride)
resp, err := doRequest(c, req, info)
if err != nil {
return nil, fmt.Errorf("do request failed: %w", err)
@@ -156,9 +167,7 @@ func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBod
if err != nil {
return nil, err
}
for key, value := range headerOverride {
headers.Set(key, value)
}
applyHeaderOverrideToRequest(req, headerOverride)
resp, err := doRequest(c, req, info)
if err != nil {
return nil, fmt.Errorf("do request failed: %w", err)

View File

@@ -437,8 +437,10 @@ func StreamResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse
}
} else {
if claudeResponse.Type == "message_start" {
response.Id = claudeResponse.Message.Id
response.Model = claudeResponse.Message.Model
if claudeResponse.Message != nil {
response.Id = claudeResponse.Message.Id
response.Model = claudeResponse.Message.Model
}
//claudeUsage = &claudeResponse.Message.Usage
choice.Delta.SetContentString("")
choice.Delta.Role = "assistant"
@@ -589,35 +591,63 @@ type ClaudeResponseInfo struct {
}
func FormatClaudeResponseInfo(requestMode int, claudeResponse *dto.ClaudeResponse, oaiResponse *dto.ChatCompletionsStreamResponse, claudeInfo *ClaudeResponseInfo) bool {
if claudeInfo == nil {
return false
}
if claudeInfo.Usage == nil {
claudeInfo.Usage = &dto.Usage{}
}
if requestMode == RequestModeCompletion {
claudeInfo.ResponseText.WriteString(claudeResponse.Completion)
} else {
if claudeResponse.Type == "message_start" {
claudeInfo.ResponseId = claudeResponse.Message.Id
claudeInfo.Model = claudeResponse.Message.Model
if claudeResponse.Message != nil {
claudeInfo.ResponseId = claudeResponse.Message.Id
claudeInfo.Model = claudeResponse.Message.Model
}
// message_start, 获取usage
claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Message.Usage.CacheReadInputTokens
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Message.Usage.CacheCreationInputTokens
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Message.Usage.GetCacheCreation5mTokens()
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Message.Usage.GetCacheCreation1hTokens()
claudeInfo.Usage.CompletionTokens = claudeResponse.Message.Usage.OutputTokens
} else if claudeResponse.Type == "content_block_delta" {
if claudeResponse.Delta.Text != nil {
claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Text)
if claudeResponse.Message != nil && claudeResponse.Message.Usage != nil {
claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Message.Usage.CacheReadInputTokens
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Message.Usage.CacheCreationInputTokens
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Message.Usage.GetCacheCreation5mTokens()
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Message.Usage.GetCacheCreation1hTokens()
claudeInfo.Usage.CompletionTokens = claudeResponse.Message.Usage.OutputTokens
}
if claudeResponse.Delta.Thinking != nil {
claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Thinking)
} else if claudeResponse.Type == "content_block_delta" {
if claudeResponse.Delta != nil {
if claudeResponse.Delta.Text != nil {
claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Text)
}
if claudeResponse.Delta.Thinking != nil {
claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Thinking)
}
}
} else if claudeResponse.Type == "message_delta" {
// 最终的usage获取
if claudeResponse.Usage.InputTokens > 0 {
// 不叠加,只取最新的
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
if claudeResponse.Usage != nil {
if claudeResponse.Usage.InputTokens > 0 {
// 不叠加,只取最新的
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
}
if claudeResponse.Usage.CacheReadInputTokens > 0 {
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens
}
if claudeResponse.Usage.CacheCreationInputTokens > 0 {
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens
}
if cacheCreation5m := claudeResponse.Usage.GetCacheCreation5mTokens(); cacheCreation5m > 0 {
claudeInfo.Usage.ClaudeCacheCreation5mTokens = cacheCreation5m
}
if cacheCreation1h := claudeResponse.Usage.GetCacheCreation1hTokens(); cacheCreation1h > 0 {
claudeInfo.Usage.ClaudeCacheCreation1hTokens = cacheCreation1h
}
if claudeResponse.Usage.OutputTokens > 0 {
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
}
claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeInfo.Usage.CompletionTokens
}
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeInfo.Usage.CompletionTokens
// 判断是否完整
claudeInfo.Done = true
@@ -657,7 +687,9 @@ func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
} else {
if claudeResponse.Type == "message_start" {
// message_start, 获取usage
info.UpstreamModelName = claudeResponse.Message.Model
if claudeResponse.Message != nil {
info.UpstreamModelName = claudeResponse.Message.Model
}
} else if claudeResponse.Type == "content_block_delta" {
} else if claudeResponse.Type == "message_delta" {
}
@@ -745,13 +777,18 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
if requestMode == RequestModeCompletion {
claudeInfo.Usage = service.ResponseText2Usage(c, claudeResponse.Completion, info.UpstreamModelName, info.GetEstimatePromptTokens())
} else {
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
claudeInfo.Usage.TotalTokens = claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Usage.GetCacheCreation5mTokens()
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Usage.GetCacheCreation1hTokens()
if claudeInfo.Usage == nil {
claudeInfo.Usage = &dto.Usage{}
}
if claudeResponse.Usage != nil {
claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens
claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens
claudeInfo.Usage.TotalTokens = claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens
claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens
claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens
claudeInfo.Usage.ClaudeCacheCreation5mTokens = claudeResponse.Usage.GetCacheCreation5mTokens()
claudeInfo.Usage.ClaudeCacheCreation1hTokens = claudeResponse.Usage.GetCacheCreation1hTokens()
}
}
var responseData []byte
switch info.RelayFormat {
@@ -766,7 +803,7 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
responseData = data
}
if claudeResponse.Usage.ServerToolUse != nil && claudeResponse.Usage.ServerToolUse.WebSearchRequests > 0 {
if claudeResponse.Usage != nil && claudeResponse.Usage.ServerToolUse != nil && claudeResponse.Usage.ServerToolUse.WebSearchRequests > 0 {
c.Set("claude_web_search_requests", claudeResponse.Usage.ServerToolUse.WebSearchRequests)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/QuantumNous/new-api/relay/channel"
"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"
@@ -42,6 +43,9 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
if info.RelayMode == relayconstant.RelayModeResponses {
return fmt.Sprintf("%s/v1/responses", info.ChannelBaseUrl), nil
}
return fmt.Sprintf("%s/chat/completions", info.ChannelBaseUrl), nil
}
@@ -71,8 +75,7 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
}
func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
// TODO implement me
return nil, errors.New("not implemented")
return request, nil
}
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {

View File

@@ -24,9 +24,9 @@ import (
)
const (
RequestModeClaude = 1
RequestModeGemini = 2
RequestModeLlama = 3
RequestModeClaude = 1
RequestModeGemini = 2
RequestModeOpenSource = 3
)
var claudeModelMap = map[string]string{
@@ -115,7 +115,7 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
} else if strings.Contains(info.UpstreamModelName, "llama") ||
// open source models
strings.Contains(info.UpstreamModelName, "-maas") {
a.RequestMode = RequestModeLlama
a.RequestMode = RequestModeOpenSource
} else {
a.RequestMode = RequestModeGemini
}
@@ -166,10 +166,9 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s
suffix,
), nil
}
} else if a.RequestMode == RequestModeLlama {
} else if a.RequestMode == RequestModeOpenSource {
return fmt.Sprintf(
"https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions",
region,
"https://aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions",
adc.ProjectID,
region,
), nil
@@ -242,7 +241,7 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
model = v
}
return a.getRequestUrl(info, model, suffix)
} else if a.RequestMode == RequestModeLlama {
} else if a.RequestMode == RequestModeOpenSource {
return a.getRequestUrl(info, "", "")
}
return "", errors.New("unsupported request mode")
@@ -340,7 +339,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
}
c.Set("request_model", request.Model)
return geminiRequest, nil
} else if a.RequestMode == RequestModeLlama {
} else if a.RequestMode == RequestModeOpenSource {
return request, nil
}
return nil, errors.New("unsupported request mode")
@@ -375,7 +374,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
} else {
return gemini.GeminiChatStreamHandler(c, info, resp)
}
case RequestModeLlama:
case RequestModeOpenSource:
return openai.OaiStreamHandler(c, info, resp)
}
} else {
@@ -391,7 +390,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
}
return gemini.GeminiChatHandler(c, info, resp)
}
case RequestModeLlama:
case RequestModeOpenSource:
return openai.OpenaiHandler(c, info, resp)
}
}

View File

@@ -231,6 +231,8 @@ func GetMimeTypeByExtension(ext string) string {
return "image/png"
case "gif":
return "image/gif"
case "jfif":
return "image/jpeg"
// Audio files
case "mp3":

View File

@@ -33,11 +33,40 @@ type ChannelAffinitySetting struct {
}
var channelAffinitySetting = ChannelAffinitySetting{
Enabled: false,
Enabled: true,
SwitchOnSuccess: true,
MaxEntries: 100_000,
DefaultTTLSeconds: 3600,
Rules: []ChannelAffinityRule{},
Rules: []ChannelAffinityRule{
{
Name: "codex trace",
ModelRegex: []string{"^gpt-.*$"},
PathRegex: []string{"/v1/responses"},
KeySources: []ChannelAffinityKeySource{
{Type: "gjson", Path: "prompt_cache_key"},
},
ValueRegex: "",
TTLSeconds: 0,
SkipRetryOnFailure: false,
IncludeUsingGroup: true,
IncludeRuleName: true,
UserAgentInclude: nil,
},
{
Name: "claude code trace",
ModelRegex: []string{"^claude-.*$"},
PathRegex: []string{"/v1/messages"},
KeySources: []ChannelAffinityKeySource{
{Type: "gjson", Path: "metadata.user_id"},
},
ValueRegex: "",
TTLSeconds: 0,
SkipRetryOnFailure: false,
IncludeUsingGroup: true,
IncludeRuleName: true,
UserAgentInclude: nil,
},
},
}
func init() {

View File

@@ -120,7 +120,7 @@ const ContentModal = ({
}
return (
<div style={{ position: 'relative' }}>
<div style={{ position: 'relative', height: '100%' }}>
{isLoading && (
<div
style={{
@@ -137,7 +137,13 @@ const ContentModal = ({
<video
src={modalContent}
controls
style={{ width: '100%' }}
style={{
width: '100%',
height: '100%',
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
}}
autoPlay
crossOrigin='anonymous'
onError={handleVideoError}
@@ -155,11 +161,13 @@ const ContentModal = ({
onCancel={() => setIsModalOpen(false)}
closable={null}
bodyStyle={{
height: isVideo ? '450px' : '400px',
height: isVideo ? '70vh' : '400px',
maxHeight: '80vh',
overflow: 'auto',
padding: isVideo && videoError ? '0' : '24px',
}}
width={800}
width={isVideo ? '90vw' : 800}
style={isVideo ? { maxWidth: 960 } : undefined}
>
{isVideo ? (
renderVideoContent()

View File

@@ -67,7 +67,7 @@ const KEY_SOURCE_TYPES = [
const RULE_TEMPLATES = {
codex: {
name: 'codex优选',
name: 'codex trace',
model_regex: ['^gpt-.*$'],
path_regex: ['/v1/responses'],
key_sources: [{ type: 'gjson', path: 'prompt_cache_key' }],
@@ -78,7 +78,7 @@ const RULE_TEMPLATES = {
include_rule_name: true,
},
claudeCode: {
name: 'claude-code优选',
name: 'claude-code trace',
model_regex: ['^claude-.*$'],
path_regex: ['/v1/messages'],
key_sources: [{ type: 'gjson', path: 'metadata.user_id' }],