Skip to content
6 changes: 4 additions & 2 deletions common/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"io"
)

type RawMessage = json.RawMessage

func Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
Expand All @@ -22,7 +24,7 @@ func Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}

func GetJsonType(data json.RawMessage) string {
func GetJsonType(data RawMessage) string {
trimmed := bytes.TrimSpace(data)
if len(trimmed) == 0 {
return "unknown"
Expand All @@ -45,7 +47,7 @@ func GetJsonType(data json.RawMessage) string {
}

// JsonRawMessageToString returns JSON strings as their decoded value and other JSON values as raw text.
func JsonRawMessageToString(data json.RawMessage) string {
func JsonRawMessageToString(data RawMessage) string {
trimmed := bytes.TrimSpace(data)
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
return ""
Expand Down
19 changes: 19 additions & 0 deletions common/rate-limit.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ func (l *InMemoryRateLimiter) clearExpiredItems() {
func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration int64) bool {
l.mutex.Lock()
defer l.mutex.Unlock()
if maxRequestNum == 0 {
return true
}
// [old <-- new]
queue, ok := l.store[key]
now := time.Now().Unix()
Expand All @@ -68,3 +71,19 @@ func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration in
}
return true
}

// Check reports whether a request would be allowed without recording it.
// The duration parameter's unit is seconds.
func (l *InMemoryRateLimiter) Check(key string, maxRequestNum int, duration int64) bool {
l.mutex.Lock()
defer l.mutex.Unlock()
if maxRequestNum == 0 {
return true
}
queue, ok := l.store[key]
if !ok || len(*queue) < maxRequestNum {
return true
}
now := time.Now().Unix()
return now-(*queue)[0] >= duration
}
94 changes: 19 additions & 75 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"log"
"net/http"
"strings"
"time"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
Expand Down Expand Up @@ -65,6 +64,22 @@ func geminiRelayHandler(c *gin.Context, info *relaycommon.RelayInfo) *types.NewA
return err
}

func ResponsesWebSocket(c *gin.Context) {
requestId := c.GetString(common.RequestIdKey)
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer ws.Close()

if newAPIError := relay.ResponsesWebSocketHelper(c, ws); newAPIError != nil {
errorPreview := common.LocalLogPreview(newAPIError.Error())
logger.LogError(c, fmt.Sprintf("responses websocket relay error: %s", errorPreview))
newAPIError.SetMessage(common.MessageWithRequestId(newAPIError.Error(), requestId))
helper.WssError(c, ws, newAPIError.ToOpenAIError())
}
}

func Relay(c *gin.Context, relayFormat types.RelayFormat) {

requestId := c.GetString(common.RequestIdKey)
Expand Down Expand Up @@ -248,7 +263,7 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
}

var upgrader = websocket.Upgrader{
Subprotocols: []string{"realtime"}, // WS 握手支持的协议,如果有使用 Sec-WebSocket-Protocol,则必须在此声明对应的 Protocol TODO add other protocol
Subprotocols: []string{"realtime", "responses"}, // WS 握手支持的协议,如果有使用 Sec-WebSocket-Protocol,则必须在此声明对应的 Protocol
CheckOrigin: func(r *http.Request) bool {
return true // 允许跨域
},
Expand Down Expand Up @@ -322,82 +337,11 @@ func getChannel(c *gin.Context, info *relaycommon.RelayInfo, retryParam *service
}

func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) bool {
if openaiErr == nil {
return false
}
if service.ShouldSkipRetryAfterChannelAffinityFailure(c) {
return false
}
if types.IsChannelError(openaiErr) {
return true
}
if types.IsSkipRetryError(openaiErr) {
return false
}
if retryTimes <= 0 {
return false
}
if _, ok := c.Get("specific_channel_id"); ok {
return false
}
code := openaiErr.StatusCode
if code >= 200 && code < 300 {
return false
}
if code < 100 || code > 599 {
return true
}
if operation_setting.IsAlwaysSkipRetryCode(openaiErr.GetErrorCode()) {
return false
}
return operation_setting.ShouldRetryByStatusCode(code)
return service.ShouldRetryRelayError(c, openaiErr, retryTimes)
}

func processChannelError(c *gin.Context, channelError types.ChannelError, err *types.NewAPIError) {
logger.LogError(c, fmt.Sprintf("channel error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, common.LocalLogPreview(err.Error())))
// 不要使用context获取渠道信息,异步处理时可能会出现渠道信息不一致的情况
// do not use context to get channel info, there may be inconsistent channel info when processing asynchronously
if service.ShouldDisableChannel(err) && channelError.AutoBan {
gopool.Go(func() {
service.DisableChannel(channelError, err.ErrorWithStatusCode())
})
}

if constant.ErrorLogEnabled && types.IsRecordErrorLog(err) {
// 保存错误日志到mysql中
userId := c.GetInt("id")
tokenName := c.GetString("token_name")
modelName := c.GetString("original_model")
tokenId := c.GetInt("token_id")
userGroup := c.GetString("group")
channelId := c.GetInt("channel_id")
other := make(map[string]interface{})
if c.Request != nil && c.Request.URL != nil {
other["request_path"] = c.Request.URL.Path
}
other["error_type"] = err.GetErrorType()
other["error_code"] = err.GetErrorCode()
other["status_code"] = err.StatusCode
other["channel_id"] = channelId
other["channel_name"] = c.GetString("channel_name")
other["channel_type"] = c.GetInt("channel_type")
adminInfo := make(map[string]interface{})
adminInfo["use_channel"] = c.GetStringSlice("use_channel")
isMultiKey := common.GetContextKeyBool(c, constant.ContextKeyChannelIsMultiKey)
if isMultiKey {
adminInfo["is_multi_key"] = true
adminInfo["multi_key_index"] = common.GetContextKeyInt(c, constant.ContextKeyChannelMultiKeyIndex)
}
service.AppendChannelAffinityAdminInfo(c, adminInfo)
other["admin_info"] = adminInfo
startTime := common.GetContextKeyTime(c, constant.ContextKeyRequestStartTime)
if startTime.IsZero() {
startTime = time.Now()
}
useTimeSeconds := int(time.Since(startTime).Seconds())
model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.MaskSensitiveErrorWithStatusCode(), tokenId, useTimeSeconds, common.GetContextKeyBool(c, constant.ContextKeyIsStream), userGroup, other)
}

service.ProcessChannelError(c, channelError, err)
}

func RelayMidjourney(c *gin.Context) {
Expand Down
11 changes: 6 additions & 5 deletions dto/openai_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,12 @@ type Usage struct {
UsageSemantic string `json:"usage_semantic,omitempty"`
UsageSource string `json:"usage_source,omitempty"`

PromptTokensDetails InputTokenDetails `json:"prompt_tokens_details"`
CompletionTokenDetails OutputTokenDetails `json:"completion_tokens_details"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
InputTokensDetails *InputTokenDetails `json:"input_tokens_details"`
PromptTokensDetails InputTokenDetails `json:"prompt_tokens_details"`
CompletionTokenDetails OutputTokenDetails `json:"completion_tokens_details"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
InputTokensDetails *InputTokenDetails `json:"input_tokens_details"`
OutputTokensDetails *OutputTokenDetails `json:"output_tokens_details,omitempty"`

// claude cache 1h
ClaudeCacheCreation5mTokens int `json:"claude_cache_creation_5_m_tokens"`
Expand Down
40 changes: 26 additions & 14 deletions middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,20 +276,7 @@ func TokenAuthReadOnly() func(c *gin.Context) {
func TokenAuth() func(c *gin.Context) {
return func(c *gin.Context) {
// 先检测是否为ws
if c.Request.Header.Get("Sec-WebSocket-Protocol") != "" {
// Sec-WebSocket-Protocol: realtime, openai-insecure-api-key.sk-xxx, openai-beta.realtime-v1
// read sk from Sec-WebSocket-Protocol
key := c.Request.Header.Get("Sec-WebSocket-Protocol")
parts := strings.Split(key, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "openai-insecure-api-key") {
key = strings.TrimPrefix(part, "openai-insecure-api-key.")
break
}
}
c.Request.Header.Set("Authorization", "Bearer "+key)
}
applyWebSocketSubprotocolAuthorization(c.Request.Header)
// 检查path包含/v1/messages 或 /v1/models
if strings.Contains(c.Request.URL.Path, "/v1/messages") || strings.Contains(c.Request.URL.Path, "/v1/models") {
anthropicKey := c.Request.Header.Get("x-api-key")
Expand Down Expand Up @@ -406,6 +393,31 @@ func TokenAuth() func(c *gin.Context) {
}
}

func applyWebSocketSubprotocolAuthorization(header http.Header) bool {
key, ok := apiKeyFromWebSocketSubprotocol(header.Get("Sec-WebSocket-Protocol"))
if !ok {
return false
}
header.Set("Authorization", "Bearer "+key)
return true
}

func apiKeyFromWebSocketSubprotocol(protocols string) (string, bool) {
if protocols == "" {
return "", false
}
const insecureAPIKeyPrefix = "openai-insecure-api-key."
parts := strings.Split(protocols, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, insecureAPIKeyPrefix) {
key := strings.TrimPrefix(part, insecureAPIKeyPrefix)
return key, key != ""
}
}
return "", false
}

func SetupContextForToken(c *gin.Context, token *model.Token, parts ...string) error {
if token == nil {
return fmt.Errorf("token is nil")
Expand Down
86 changes: 86 additions & 0 deletions middleware/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package middleware

import (
"net/http"
"testing"
)

func TestAPIKeyFromWebSocketSubprotocol(t *testing.T) {
tests := []struct {
name string
protocols string
wantKey string
wantOK bool
}{
{
name: "responses protocol only",
protocols: "responses",
wantOK: false,
},
{
name: "realtime protocol only",
protocols: "realtime",
wantOK: false,
},
{
name: "responses with insecure key",
protocols: "responses, openai-insecure-api-key.sk-test",
wantKey: "sk-test",
wantOK: true,
},
{
name: "realtime with beta and insecure key",
protocols: "realtime, openai-insecure-api-key.sk-realtime, openai-beta.realtime-v1",
wantKey: "sk-realtime",
wantOK: true,
},
{
name: "empty insecure key",
protocols: "responses, openai-insecure-api-key.",
wantOK: false,
},
{
name: "bare insecure marker is not a key",
protocols: "openai-insecure-api-key",
wantOK: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotKey, gotOK := apiKeyFromWebSocketSubprotocol(tt.protocols)
if gotOK != tt.wantOK {
t.Fatalf("ok = %v, want %v", gotOK, tt.wantOK)
}
if gotKey != tt.wantKey {
t.Fatalf("key = %q, want %q", gotKey, tt.wantKey)
}
})
}
}

func TestApplyWebSocketSubprotocolAuthorizationDoesNotOverrideProtocolOnly(t *testing.T) {
header := http.Header{}
header.Set("Authorization", "Bearer sk-original")
header.Set("Sec-WebSocket-Protocol", "responses")

if applyWebSocketSubprotocolAuthorization(header) {
t.Fatal("authorization was unexpectedly applied")
}
if got := header.Get("Authorization"); got != "Bearer sk-original" {
t.Fatalf("Authorization = %q, want original bearer", got)
}
}

func TestApplyWebSocketSubprotocolAuthorizationOverridesWithInsecureKey(t *testing.T) {
header := http.Header{}
header.Set("Authorization", "Bearer sk-original")
header.Set("Sec-WebSocket-Protocol", "responses, openai-insecure-api-key.sk-from-protocol")

if !applyWebSocketSubprotocolAuthorization(header) {
t.Fatal("authorization was not applied")
}
if got := header.Get("Authorization"); got != "Bearer sk-from-protocol" {
t.Fatalf("Authorization = %q, want protocol bearer", got)
}
}
Loading