Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0b8ca74
support gemini-style api SystemInstructions
mumingluan Aug 20, 2025
fc213af
Merge branch 'QuantumNous:alpha' into alpha
mumingluan Aug 22, 2025
9a8b3eb
a
mumingluan Sep 2, 2025
2290acc
fix(topup): add currency symbol to amounts in RechargeCard
kyubibii Oct 2, 2025
b0b6ab2
feat(web): add privacy policy and user agreement settings & pages
kyubibii Oct 2, 2025
f44f682
Merge branch 'main' of https://github.com/QuantumNous/new-api
kyubibii Oct 2, 2025
8a01f09
Merge pull request #1962 from QuantumNous/main
seefs001 Oct 3, 2025
5d33ec8
Merge pull request #1952 from kyubibii/main
Calcium-Ion Oct 3, 2025
3473329
Revert "fix(topup): add currency symbol to amounts in RechargeCard"
Calcium-Ion Oct 3, 2025
68d9751
Merge pull request #1966 from QuantumNous/revert-1952-main
Calcium-Ion Oct 3, 2025
813cb9c
f
mumingluan Oct 9, 2025
c948f85
Merge pull request #2007 from QuantumNous/main
seefs001 Oct 11, 2025
24bc24a
feat: matrix ci
seefs001 Oct 11, 2025
4e0c911
Merge pull request #2008 from seefs001/fix/ci
seefs001 Oct 11, 2025
a8a0da5
fix
seefs001 Oct 11, 2025
e603186
Merge pull request #2009 from seefs001/fix/ci
seefs001 Oct 11, 2025
bea8c57
fix
seefs001 Oct 11, 2025
2842ae5
Merge pull request #2010 from seefs001/fix/ci
seefs001 Oct 11, 2025
37cc5d2
ci
seefs001 Oct 11, 2025
aacdc39
Merge pull request #2013 from seefs001/fix/ci
seefs001 Oct 11, 2025
1d78e8e
1
mumingluan Dec 4, 2025
451c00a
Merge upstream/alpha and resolve conflicts
mumingluan Dec 4, 2025
74d58a0
a
mumingluan Dec 4, 2025
dca4758
DEL autoenable
mumingluan Dec 4, 2025
3fe80db
add 60-100
mumingluan Dec 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(go build:*)",
"Bash(npm run build)",
"Bash(git:*)"
],
"deny": [],
"ask": []
}
}
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ COPY ./VERSION .
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build

FROM golang:alpine AS builder2
ENV GO111MODULE=on CGO_ENABLED=0

ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}

ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux

WORKDIR /build

Expand Down
6 changes: 3 additions & 3 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,9 +586,9 @@ func testAllChannels(notify bool) error {
}

// enable channel
if !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) {
service.EnableChannel(channel.Id, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.Name)
}
//if !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) {
// service.EnableChannel(channel.Id, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.Name)
//}

channel.UpdateResponseTime(milliseconds)
time.Sleep(common.RequestInterval)
Expand Down
18 changes: 18 additions & 0 deletions controller/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,24 @@ func UpdateOption(c *gin.Context) {
})
return
}
case "TokenRateLimitGroup":
err = setting.CheckTokenRateLimitGroup(option.Value.(string))
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
case "TokenDailyRateLimitGroup":
err = setting.CheckTokenDailyRateLimitGroup(option.Value.(string))
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
case "console_setting.api_info":
err = console_setting.ValidateConsoleSettings(option.Value.(string), "ApiInfo")
if err != nil {
Expand Down
123 changes: 101 additions & 22 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,29 @@ func geminiRelayHandler(c *gin.Context, info *relaycommon.RelayInfo) *types.NewA
return err
}

func Relay(c *gin.Context, relayFormat types.RelayFormat) {
func relayToChannel(c *gin.Context, relayInfo *relaycommon.RelayInfo, channel *model.Channel) *types.NewAPIError {
requestBody, _ := common.GetRequestBody(c)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))

var newAPIError *types.NewAPIError
switch relayInfo.RelayFormat {
case types.RelayFormatOpenAIRealtime:
newAPIError = relay.WssHelper(c, relayInfo)
case types.RelayFormatClaude:
newAPIError = relay.ClaudeHelper(c, relayInfo)
case types.RelayFormatGemini:
newAPIError = geminiRelayHandler(c, relayInfo)
default:
newAPIError = relayHandler(c, relayInfo)
}

if newAPIError != nil {
processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
}
return newAPIError
}

func Relay(c *gin.Context, relayFormat types.RelayFormat) {
requestId := c.GetString(common.RequestIdKey)
group := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
originalModel := common.GetContextKeyString(c, constant.ContextKeyOriginalModel)
Expand Down Expand Up @@ -137,8 +158,6 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
return
}

// common.SetContextKey(c, constant.ContextKeyTokenCountMeta, meta)

newAPIError = service.PreConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
if newAPIError != nil {
return
Expand All @@ -151,6 +170,7 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
}
}()

// Main retry loop for selecting channels
for i := 0; i <= common.RetryTimes; i++ {
channel, err := getChannel(c, group, originalModel, i)
if err != nil {
Expand All @@ -160,28 +180,64 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
}

addUsedChannel(c, channel.Id)
requestBody, _ := common.GetRequestBody(c)
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))

switch relayFormat {
case types.RelayFormatOpenAIRealtime:
newAPIError = relay.WssHelper(c, relayInfo)
case types.RelayFormatClaude:
newAPIError = relay.ClaudeHelper(c, relayInfo)
case types.RelayFormatGemini:
newAPIError = geminiRelayHandler(c, relayInfo)
default:
newAPIError = relayHandler(c, relayInfo)
}

if newAPIError == nil {
return
// Inner loop for retrying keys within a multi-key channel
if channel.ChannelInfo.IsMultiKey {
key, keyIdx, keyErr := channel.GetNextEnabledKey()
if keyErr != nil {
newAPIError = keyErr
break // No keys available, break to outer loop to switch channel
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
triedKeys := make(map[int]bool) // Track tried keys for this channel
for j := 0; j < channel.ChannelInfo.MultiKeySize && j < 20; j++ {
if _, ok := triedKeys[keyIdx]; ok {
// This key has been tried, get another one
key, keyIdx, keyErr = channel.GetNextEnabledKey()
if keyErr != nil {
newAPIError = keyErr
break
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
continue
}

// Setup context with the new key
middleware.SetupContextForSelectedChannelWithKey(c, channel, originalModel, key, keyIdx)
triedKeys[keyIdx] = true

newAPIError = relayToChannel(c, relayInfo, channel)
if newAPIError == nil {
return // Success
}

// inappropriate 错误直接切换渠道,不重试同一渠道的多个密钥
if isInappropriateError(newAPIError) {
goto next_channel
}

// If the error is not retryable for a key, break the inner loop
if !shouldRetry(c, newAPIError, common.RetryTimes-i) {
goto next_channel // Break inner loop and go to the next channel
}

// Get next key for retry
key, keyIdx, keyErr = channel.GetNextEnabledKey()
if keyErr != nil {
newAPIError = keyErr
break // No more keys to try
}
}
} else {
// Single key channel logic
newAPIError = relayToChannel(c, relayInfo, channel)
if newAPIError == nil {
return // Success
}
}

processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)

next_channel:
if !shouldRetry(c, newAPIError, common.RetryTimes-i) {
break
break // Break outer loop if error is not retryable for the channel
}
}

Expand Down Expand Up @@ -233,10 +289,29 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m
return channel, nil
}

// isInappropriateError 检测错误是否包含 inappropriate 字样
// 此类错误需要强制渠道间重试,不重试同一渠道的多个密钥
func isInappropriateError(openaiErr *types.NewAPIError) bool {
if openaiErr == nil {
return false
}
return strings.Contains(strings.ToLower(openaiErr.Error()), "inappropriate")
}

func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) bool {
if openaiErr == nil {
return false
}
if strings.Contains(openaiErr.Error(), "no response received") {
return false
}
if strings.Contains(openaiErr.Error(), "no candidates reMeoWturned") {
return false
}
Comment on lines +305 to +310

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Suspicious string: "reMeoWturned" appears to be a typo or obfuscation.

The string "no candidates reMeoWturned" on line 306 looks like an intentional typo or obfuscated text. If this is meant to match error messages from the Gemini API, it should be corrected to "no candidates returned".

-	if strings.Contains(openaiErr.Error(), "no candidates reMeoWturned") {
+	if strings.Contains(openaiErr.Error(), "no candidates returned") {
🤖 Prompt for AI Agents
In controller/relay.go around lines 303 to 308, the string literal "no
candidates reMeoWturned" is a typo/obfuscation and should be corrected to match
the actual Gemini API error text; replace it with "no candidates returned"
(preserve surrounding logic and casing) so the error check works as intended and
run relevant tests or logs to confirm the condition now matches real errors.

// inappropriate 错误强制渠道间重试
if isInappropriateError(openaiErr) {
return true
}
if types.IsChannelError(openaiErr) {
return true
}
Expand Down Expand Up @@ -281,7 +356,11 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t
// do not use context to get channel info, there may be inconsistent channel info when processing asynchronously
if service.ShouldDisableChannel(channelError.ChannelId, err) && channelError.AutoBan {
gopool.Go(func() {
service.DisableChannel(channelError, err.Error())
reason := err.Error()
if err.GetErrorType() == "insufficient_quota" {
reason = "insufficient_quota: " + reason
}
service.DisableChannel(channelError, reason)
})
}

Expand Down
2 changes: 1 addition & 1 deletion dto/gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type GeminiChatRequest struct {
GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"`
Tools json.RawMessage `json:"tools,omitempty"`
ToolConfig *ToolConfig `json:"toolConfig,omitempty"`
SystemInstructions *GeminiChatContent `json:"systemInstruction,omitempty"`
SystemInstruction *GeminiChatContent `json:"system_instruction,omitempty"`
CachedContent string `json:"cachedContent,omitempty"`
}

Expand Down
4 changes: 4 additions & 0 deletions middleware/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
if newAPIError != nil {
return newAPIError
}
return SetupContextForSelectedChannelWithKey(c, channel, modelName, key, index)
}

func SetupContextForSelectedChannelWithKey(c *gin.Context, channel *model.Channel, modelName string, key string, index int) *types.NewAPIError {
if channel.ChannelInfo.IsMultiKey {
common.SetContextKey(c, constant.ContextKeyChannelIsMultiKey, true)
common.SetContextKey(c, constant.ContextKeyChannelMultiKeyIndex, index)
Expand Down
Loading