Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ var PreConsumedQuota = 500

var RetryTimes = 0

var EnableAutodisabledChannelOrKeyAfterMinute = 0
var EnableAutodisabledChannelOrKeyAt = -1 // UTC hour

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.

🛠️ Refactor suggestion

Validate and document scheduler knobs (UTC hour range; non-negative minutes).

Guard against misconfiguration: ensure 0 <= EnableAutodisabledChannelOrKeyAt <= 23 and EnableAutodisabledChannelOrKeyAfterMinute >= 0 on read; log and ignore invalid values.

Apply in main.go where these are read:

- common.EnableAutodisabledChannelOrKeyAfterMinute = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE", 0)
- common.EnableAutodisabledChannelOrKeyAt = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT", -1)
+ common.EnableAutodisabledChannelOrKeyAfterMinute = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE", 0)
+ common.EnableAutodisabledChannelOrKeyAt = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT", -1)
+ if common.EnableAutodisabledChannelOrKeyAt != -1 &&
+   (common.EnableAutodisabledChannelOrKeyAt < 0 || common.EnableAutodisabledChannelOrKeyAt > 23) {
+   common.SysLog(fmt.Sprintf("invalid ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT=%d, disabling fixed-time schedule",
+     common.EnableAutodisabledChannelOrKeyAt))
+   common.EnableAutodisabledChannelOrKeyAt = -1
+ }
+ if common.EnableAutodisabledChannelOrKeyAfterMinute < 0 {
+   common.SysLog(fmt.Sprintf("invalid ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE=%d, forcing to 0",
+     common.EnableAutodisabledChannelOrKeyAfterMinute))
+   common.EnableAutodisabledChannelOrKeyAfterMinute = 0
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var EnableAutodisabledChannelOrKeyAfterMinute = 0
var EnableAutodisabledChannelOrKeyAt = -1 // UTC hour
// … earlier in main.go …
// Read configuration (with defaults)
common.EnableAutodisabledChannelOrKeyAfterMinute = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE", 0)
common.EnableAutodisabledChannelOrKeyAt = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT", -1)
// Validate EnableAutodisabledChannelOrKeyAt:
// -1 means “disabled”; otherwise must be within 0–23 (UTC hour)
if common.EnableAutodisabledChannelOrKeyAt != -1 &&
(common.EnableAutodisabledChannelOrKeyAt < 0 || common.EnableAutodisabledChannelOrKeyAt > 23) {
common.SysLog(fmt.Sprintf(
"invalid ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT=%d, disabling fixed-time schedule",
common.EnableAutodisabledChannelOrKeyAt,
))
common.EnableAutodisabledChannelOrKeyAt = -1
}
// Validate EnableAutodisabledChannelOrKeyAfterMinute:
// must be non-negative (minutes after startup)
if common.EnableAutodisabledChannelOrKeyAfterMinute < 0 {
common.SysLog(fmt.Sprintf(
"invalid ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE=%d, forcing to 0",
common.EnableAutodisabledChannelOrKeyAfterMinute,
))
common.EnableAutodisabledChannelOrKeyAfterMinute = 0
}
// … rest of initialization …
🤖 Prompt for AI Agents
In common/constants.go around lines 109 to 111, the scheduler knobs lack
validation and documentation: ensure when reading these values in main.go you
validate that EnableAutodisabledChannelOrKeyAt is within 0..23 and
EnableAutodisabledChannelOrKeyAfterMinute is >= 0; if either is invalid, log a
clear warning including the invalid value and ignore it (use the safe default
from constants.go), and proceed without applying the misconfigured setting. Add
a brief comment in common/constants.go documenting the expected ranges (UTC hour
0-23, non-negative minutes) so future readers know the constraints.

//var RootUserEmail = ""

var IsMasterNode bool
Expand Down
6 changes: 3 additions & 3 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,9 +449,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
106 changes: 83 additions & 23 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,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 @@ -136,20 +157,18 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
return
}

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

preConsumedQuota, newAPIError := service.PreConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo)
if newAPIError != nil {
return
}

defer func() {
// Only return quota if downstream failed and quota was actually pre-consumed
if newAPIError != nil && preConsumedQuota != 0 {
service.ReturnPreConsumedQuota(c, relayInfo, preConsumedQuota)
}
}()

// 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 @@ -159,28 +178,59 @@ 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.
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.
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
}

// 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 @@ -236,6 +286,12 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b
if openaiErr == nil {
return false
}
if strings.Contains(openaiErr.Error(), "no response received") {
return false
}
if strings.Contains(openaiErr.Error(), "no candidates returned") {
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.

if types.IsChannelError(openaiErr) {
return true
}
Expand Down Expand Up @@ -281,7 +337,11 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t
// 不要使用context获取渠道信息,异步处理时可能会出现渠道信息不一致的情况
// 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 {
service.DisableChannel(channelError, err.Error())
reason := err.Error()
if err.GetErrorType() == "insufficient_quota" {
reason = "insufficient_quota: " + reason
}
service.DisableChannel(channelError, reason)
}
})

Expand Down
4 changes: 2 additions & 2 deletions dto/gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ type GeminiChatRequest struct {
Contents []GeminiChatContent `json:"contents"`
SafetySettings []GeminiChatSafetySettings `json:"safetySettings,omitempty"`
GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"`
Tools json.RawMessage `json:"tools,omitempty"`
SystemInstructions *GeminiChatContent `json:"systemInstruction,omitempty"`
Tools json.RawMessage `json:"tools,omitempty"`
SystemInstruction *GeminiChatContent `json:"system_instruction,omitempty"`
}

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.

💡 Verification agent

🧩 Analysis chain

Rename aligns with Gemini style, but JSON compatibility and token accounting need attention

  • Backward-compat risk: switching the JSON tag to "system_instruction" will break clients still sending "systemInstruction". We should accept both to avoid regressions.
  • Token accounting: GetTokenCountMeta ignores SystemInstruction.Parts, so system prompts won’t be counted toward prompt meta, potentially skewing prompt aggregation/usage.

Proposed implementation to accept both snake_case and camelCase without changing your external JSON shape:

// In dto/gemini.go (add below the type definition)
func (r *GeminiChatRequest) UnmarshalJSON(data []byte) error {
	type Alias GeminiChatRequest
	aux := struct {
		Alias
		SystemInstructionSnake *GeminiChatContent `json:"system_instruction,omitempty"`
		SystemInstructionCamel *GeminiChatContent `json:"systemInstruction,omitempty"`
	}{}

	if err := common.Unmarshal(data, &aux); err != nil {
		return err
	}
	*r = GeminiChatRequest(aux.Alias)
	// prefer snake_case, fallback to camelCase
	if aux.SystemInstructionSnake != nil {
		r.SystemInstruction = aux.SystemInstructionSnake
	} else if aux.SystemInstructionCamel != nil {
		r.SystemInstruction = aux.SystemInstructionCamel
	}
	return nil
}

Also, extend GetTokenCountMeta to include system instructions:

// inside GetTokenCountMeta(), after iterating r.Contents
if r.SystemInstruction != nil {
	for _, part := range r.SystemInstruction.Parts {
		if part.Text != "" {
			inputTexts = append(inputTexts, part.Text)
		}
		if part.InlineData != nil && part.InlineData.Data != "" {
			if strings.HasPrefix(part.InlineData.MimeType, "image/") {
				files = append(files, &types.FileMeta{FileType: types.FileTypeImage, OriginData: part.InlineData.Data})
			} else if strings.HasPrefix(part.InlineData.MimeType, "audio/") {
				files = append(files, &types.FileMeta{FileType: types.FileTypeAudio, OriginData: part.InlineData.Data})
			} else if strings.HasPrefix(part.InlineData.MimeType, "video/") {
				files = append(files, &types.FileMeta{FileType: types.FileTypeVideo, OriginData: part.InlineData.Data})
			} else {
				files = append(files, &types.FileMeta{FileType: types.FileTypeFile, OriginData: part.InlineData.Data})
			}
		}
	}
}

Run this to verify no stale references remain:


🏁 Script executed:

#!/bin/bash
# Search for old field/usages that may have been missed
rg -n -C2 -S '\bSystemInstructions\b|\bsystemInstruction\b|\bsystemInstructions\b'

Length of output: 84


🏁 Script executed:

#!/bin/bash
# Verify occurrences of both snake_case and camelCase variants
rg -n -C2 'systemInstruction'
rg -n -C2 'SystemInstruction'
# Locate GetTokenCountMeta to inspect if it already accounts for system instructions
rg -n -C2 'GetTokenCountMeta'

Length of output: 4842


🏁 Script executed:

#!/bin/bash
# Check if UnmarshalJSON is already implemented for GeminiChatRequest
rg -n -C2 'func .*GeminiChatRequest.*UnmarshalJSON' dto/gemini.go

# Inspect the GetTokenCountMeta implementation for GeminiChatRequest
rg -n -C10 'func (r \*GeminiChatRequest\) GetTokenCountMeta' -n dto/gemini.go

Length of output: 251


🏁 Script executed:

#!/bin/bash
# Check for existing UnmarshalJSON implementation on GeminiChatRequest
rg -n 'func .*GeminiChatRequest.*UnmarshalJSON' dto/gemini.go || echo "No UnmarshalJSON method found"

# Display the type definition and GetTokenCountMeta implementation for GeminiChatRequest
sed -n '1,80p' dto/gemini.go

Length of output: 2435


Support legacy systemInstruction and include system instructions in token accounting

To avoid breaking existing clients and ensure system prompts are counted:

• In dto/gemini.go, add an UnmarshalJSON on GeminiChatRequest that reads both
– system_instruction
– systemInstruction
and sets the single SystemInstruction field (prefer snake_case).

• In the same file’s GetTokenCountMeta, after iterating r.Contents, also append every r.SystemInstruction.Parts (text and inline data) to inputTexts and files so system prompts contribute to the returned TokenCountMeta.

Locations to update:

  • dto/gemini.go: around the type definition, implement UnmarshalJSON as proposed.
  • dto/gemini.go: inside GetTokenCountMeta, inject the loop over r.SystemInstruction.Parts.
--- a/dto/gemini.go
+++ b/dto/gemini.go
@@ type GeminiChatRequest struct {
  	SystemInstruction *GeminiChatContent `json:"system_instruction,omitempty"`
 }

+// UnmarshalJSON accepts both snake_case and camelCase for SystemInstruction
+func (r *GeminiChatRequest) UnmarshalJSON(data []byte) error {
+	type Alias GeminiChatRequest
+	aux := struct {
+		Alias
+		SystemInstructionSnake *GeminiChatContent `json:"system_instruction,omitempty"`
+		SystemInstructionCamel *GeminiChatContent `json:"systemInstruction,omitempty"`
+	}{}
+
+	if err := common.Unmarshal(data, &aux); err != nil {
+		return err
+	}
+	*r = GeminiChatRequest(aux.Alias)
+	if aux.SystemInstructionSnake != nil {
+		r.SystemInstruction = aux.SystemInstructionSnake
+	} else if aux.SystemInstructionCamel != nil {
+		r.SystemInstruction = aux.SystemInstructionCamel
+	}
+	return nil
+}
@@ func (r *GeminiChatRequest) GetTokenCountMeta() *types.TokenCountMeta {
 	inputText := strings.Join(inputTexts, "\n")
+	// include system instruction parts in token count
+	if r.SystemInstruction != nil {
+		for _, part := range r.SystemInstruction.Parts {
+			if part.Text != "" {
+				inputTexts = append(inputTexts, part.Text)
+			}
+			if part.InlineData != nil && part.InlineData.Data != "" {
+				mt := part.InlineData.MimeType
+				var t types.FileType
+				switch {
+				case strings.HasPrefix(mt, "image/"):
+					t = types.FileTypeImage
+				case strings.HasPrefix(mt, "audio/"):
+					t = types.FileTypeAudio
+				case strings.HasPrefix(mt, "video/"):
+					t = types.FileTypeVideo
+				default:
+					t = types.FileTypeFile
+				}
+				files = append(files, &types.FileMeta{FileType: t, OriginData: part.InlineData.Data})
+			}
+		}
+		inputText = strings.Join(inputTexts, "\n")
+	}
 	return &types.TokenCountMeta{
 		CombineText: inputText,
 		Files:       files,
 		MaxTokens:   maxTokens,
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Tools json.RawMessage `json:"tools,omitempty"`
SystemInstruction *GeminiChatContent `json:"system_instruction,omitempty"`
}
// --- dto/gemini.go
type GeminiChatRequest struct {
Tools json.RawMessage `json:"tools,omitempty"`
SystemInstruction *GeminiChatContent `json:"system_instruction,omitempty"`
}
// UnmarshalJSON accepts both snake_case and camelCase for SystemInstruction
func (r *GeminiChatRequest) UnmarshalJSON(data []byte) error {
type Alias GeminiChatRequest
aux := struct {
Alias
SystemInstructionSnake *GeminiChatContent `json:"system_instruction,omitempty"`
SystemInstructionCamel *GeminiChatContent `json:"systemInstruction,omitempty"`
}{}
if err := common.Unmarshal(data, &aux); err != nil {
return err
}
*r = GeminiChatRequest(aux.Alias)
if aux.SystemInstructionSnake != nil {
r.SystemInstruction = aux.SystemInstructionSnake
} else if aux.SystemInstructionCamel != nil {
r.SystemInstruction = aux.SystemInstructionCamel
}
return nil
}
func (r *GeminiChatRequest) GetTokenCountMeta() *types.TokenCountMeta {
// existing setup...
inputTexts := []string{}
for _, c := range r.Contents {
inputTexts = append(inputTexts, c.PartsText()...)
}
files := []*types.FileMeta{}
// ...
inputText := strings.Join(inputTexts, "\n")
// include system instruction parts in token count
if r.SystemInstruction != nil {
for _, part := range r.SystemInstruction.Parts {
if part.Text != "" {
inputTexts = append(inputTexts, part.Text)
}
if part.InlineData != nil && part.InlineData.Data != "" {
mt := part.InlineData.MimeType
var t types.FileType
switch {
case strings.HasPrefix(mt, "image/"):
t = types.FileTypeImage
case strings.HasPrefix(mt, "audio/"):
t = types.FileTypeAudio
case strings.HasPrefix(mt, "video/"):
t = types.FileTypeVideo
default:
t = types.FileTypeFile
}
files = append(files, &types.FileMeta{FileType: t, OriginData: part.InlineData.Data})
}
}
inputText = strings.Join(inputTexts, "\n")
}
return &types.TokenCountMeta{
CombineText: inputText,
Files: files,
MaxTokens: maxTokens,
}
}
🤖 Prompt for AI Agents
In dto/gemini.go around the GeminiChatRequest type definition (near lines 16-18)
implement UnmarshalJSON that accepts both "system_instruction" and legacy
"systemInstruction", unmarshals either key into the single SystemInstruction
field (prefer "system_instruction" when both present) and preserves other
fields; then in GetTokenCountMeta (same file) after iterating r.Contents add
logic to, if r.SystemInstruction != nil, iterate r.SystemInstruction.Parts and
append each part's text to inputTexts and any inline data references to files so
system prompts are included in token accounting; ensure you handle nils and
empty parts safely.


func (r *GeminiChatRequest) GetTokenCountMeta() *types.TokenCountMeta {
Expand Down
10 changes: 10 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ func main() {
model.InitBatchUpdater()
}

common.EnableAutodisabledChannelOrKeyAfterMinute = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE", 0)
common.EnableAutodisabledChannelOrKeyAt = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT", -1)
if common.EnableAutodisabledChannelOrKeyAt != -1 {
common.SysLog(fmt.Sprintf("auto-disabled channels/keys will be re-enabled at %d:00 UTC daily", common.EnableAutodisabledChannelOrKeyAt))
go service.CheckAndReEnableChannels()
} else if common.EnableAutodisabledChannelOrKeyAfterMinute > 0 {
common.SysLog(fmt.Sprintf("auto-disabled channels/keys will be re-enabled after %d minutes", common.EnableAutodisabledChannelOrKeyAfterMinute))
go service.CheckAndReEnableChannels()
}

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.

🛠️ Refactor suggestion

Validate env values and log precedence.

Clamp hour to [0,23], minutes to >=0, and log that “AT” takes precedence over “AFTER_MINUTE” to avoid confusion.

- common.EnableAutodisabledChannelOrKeyAfterMinute = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE", 0)
- common.EnableAutodisabledChannelOrKeyAt = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT", -1)
+ common.EnableAutodisabledChannelOrKeyAfterMinute = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE", 0)
+ common.EnableAutodisabledChannelOrKeyAt = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT", -1)
+ if common.EnableAutodisabledChannelOrKeyAt != -1 &&
+   (common.EnableAutodisabledChannelOrKeyAt < 0 || common.EnableAutodisabledChannelOrKeyAt > 23) {
+   common.SysLog(fmt.Sprintf("invalid ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT=%d; disabling fixed-time schedule",
+     common.EnableAutodisabledChannelOrKeyAt))
+   common.EnableAutodisabledChannelOrKeyAt = -1
+ }
+ if common.EnableAutodisabledChannelOrKeyAfterMinute < 0 {
+   common.SysLog(fmt.Sprintf("invalid ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE=%d; forcing 0",
+     common.EnableAutodisabledChannelOrKeyAfterMinute))
+   common.EnableAutodisabledChannelOrKeyAfterMinute = 0
+ }
+ if common.EnableAutodisabledChannelOrKeyAt != -1 && common.EnableAutodisabledChannelOrKeyAfterMinute > 0 {
+   common.SysLog("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT takes precedence over ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE")
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
common.EnableAutodisabledChannelOrKeyAfterMinute = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE", 0)
common.EnableAutodisabledChannelOrKeyAt = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT", -1)
if common.EnableAutodisabledChannelOrKeyAt != -1 {
common.SysLog(fmt.Sprintf("auto-disabled channels/keys will be re-enabled at %d:00 UTC daily", common.EnableAutodisabledChannelOrKeyAt))
go service.CheckAndReEnableChannels()
} else if common.EnableAutodisabledChannelOrKeyAfterMinute > 0 {
common.SysLog(fmt.Sprintf("auto-disabled channels/keys will be re-enabled after %d minutes", common.EnableAutodisabledChannelOrKeyAfterMinute))
go service.CheckAndReEnableChannels()
}
common.EnableAutodisabledChannelOrKeyAfterMinute = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE", 0)
common.EnableAutodisabledChannelOrKeyAt = common.GetEnvOrDefault("ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT", -1)
// Clamp hour to [0,23]; if out of range, disable fixed-time schedule
if common.EnableAutodisabledChannelOrKeyAt != -1 && (common.EnableAutodisabledChannelOrKeyAt < 0 || common.EnableAutodisabledChannelOrKeyAt > 23) {
common.SysLog(fmt.Sprintf(
"invalid ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT=%d; disabling fixed-time schedule",
common.EnableAutodisabledChannelOrKeyAt,
))
common.EnableAutodisabledChannelOrKeyAt = -1
}
// Clamp minutes to >=0
if common.EnableAutodisabledChannelOrKeyAfterMinute < 0 {
common.SysLog(fmt.Sprintf(
"invalid ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE=%d; forcing 0",
common.EnableAutodisabledChannelOrKeyAfterMinute,
))
common.EnableAutodisabledChannelOrKeyAfterMinute = 0
}
// If both are set, AT takes precedence over AFTER_MINUTE
if common.EnableAutodisabledChannelOrKeyAt != -1 && common.EnableAutodisabledChannelOrKeyAfterMinute > 0 {
common.SysLog(
"ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT takes precedence over ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE",
)
}
// Original scheduling logic
if common.EnableAutodisabledChannelOrKeyAt != -1 {
common.SysLog(fmt.Sprintf(
"auto-disabled channels/keys will be re-enabled at %d:00 UTC daily",
common.EnableAutodisabledChannelOrKeyAt,
))
go service.CheckAndReEnableChannels()
} else if common.EnableAutodisabledChannelOrKeyAfterMinute > 0 {
common.SysLog(fmt.Sprintf(
"auto-disabled channels/keys will be re-enabled after %d minutes",
common.EnableAutodisabledChannelOrKeyAfterMinute,
))
go service.CheckAndReEnableChannels()
}
🤖 Prompt for AI Agents
In main.go around lines 118 to 127, validate and clamp the environment inputs
and make precedence explicit: parse ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AT into
an int and clamp it to the range 0..23 (if parse fails treat as -1), parse
ENABLE_AUTODISABLED_CHANNEL_OR_KEY_AFTER_MINUTE into an int and coerce to a
minimum of 0 (if parse fails treat as 0), then if AT != -1 log that "AT takes
precedence over AFTER_MINUTE" when both are provided and use the clamped AT
value to schedule re-enable; otherwise if AFTER_MINUTE > 0 log the minutes and
use that value; ensure you only launch the goroutine once after choosing the
validated value and include the validated/clamped values in the log messages for
clarity.

if os.Getenv("ENABLE_PPROF") == "true" {
gopool.Go(func() {
log.Println(http.ListenAndServe("0.0.0.0:8005", nil))
Expand Down
4 changes: 4 additions & 0 deletions middleware/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,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
17 changes: 17 additions & 0 deletions model/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,10 @@ func handlerMultiKeyUpdate(channel *Channel, usingKey string, status int, reason
}
if status == common.ChannelStatusEnabled {
delete(channel.ChannelInfo.MultiKeyStatusList, keyIndex)
// If the channel was auto-disabled, re-enable it since at least one key is now enabled.
if channel.Status == common.ChannelStatusAutoDisabled {
channel.Status = common.ChannelStatusEnabled
}
} else {
channel.ChannelInfo.MultiKeyStatusList[keyIndex] = status
if channel.ChannelInfo.MultiKeyDisabledReason == nil {
Expand Down Expand Up @@ -963,3 +967,16 @@ func CountChannelsGroupByType() (map[int64]int64, error) {
}
return counts, nil
}

func GetAutoDisabledChannels() ([]*Channel, error) {
var channels []*Channel
err := DB.Where("status = ?", common.ChannelStatusAutoDisabled).Find(&channels).Error
return channels, err
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

func GetChannelsWithDisabledKeys() ([]*Channel, error) {
var channels []*Channel
// Find channels that are not manually disabled and have disabled keys
err := DB.Where("status != ? AND JSON_LENGTH(channel_info -> '$.multi_key_status_list') > 0", common.ChannelStatusManuallyDisabled).Find(&channels).Error
return channels, err
}

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

MySQL-only JSON expression breaks on PostgreSQL/SQLite; add dialect-specific query.

Current JSON_LENGTH(channel_info -> '$.multi_key_status_list') is MySQL-specific and will fail elsewhere.

-func GetChannelsWithDisabledKeys() ([]*Channel, error) {
-  var channels []*Channel
-  // Find channels that are not manually disabled and have disabled keys
-  err := DB.Where("status != ? AND JSON_LENGTH(channel_info -> '$.multi_key_status_list') > 0", common.ChannelStatusManuallyDisabled).Find(&channels).Error
-  return channels, err
-}
+func GetChannelsWithDisabledKeys() ([]*Channel, error) {
+  var channels []*Channel
+  q := DB.Omit("key").Where("status != ?", common.ChannelStatusManuallyDisabled)
+  if common.UsingMySQL {
+    // object length > 0
+    q = q.Where("JSON_LENGTH(JSON_EXTRACT(channel_info, '$.multi_key_status_list')) > 0")
+  } else if common.UsingPostgreSQL {
+    // channel_info is json/jsonb; cast to jsonb then object length
+    q = q.Where(`jsonb_object_length((channel_info->'multi_key_status_list')::jsonb) > 0`)
+  } else {
+    // SQLite JSON1 fallback: '{}' has length 2; any non-empty object > 2
+    q = q.Where(`json_extract(channel_info, '$.multi_key_status_list') IS NOT NULL AND length(json_extract(channel_info, '$.multi_key_status_list')) > 2`)
+  }
+  err := q.Find(&channels).Error
+  return channels, err
+}

Additionally, consider filtering to multi-key channels only (channel_info->is_multi_key), and adding an index (PostgreSQL: expression GIN on (channel_info)) for performance.

Committable suggestion skipped: line range outside the PR's diff.

2 changes: 1 addition & 1 deletion relay/channel/gemini/relay-gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i
}

if len(system_content) > 0 {
geminiRequest.SystemInstructions = &dto.GeminiChatContent{
geminiRequest.SystemInstruction = &dto.GeminiChatContent{
Parts: []dto.GeminiPart{
{
Text: strings.Join(system_content, "\n"),
Expand Down
6 changes: 3 additions & 3 deletions relay/gemini_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,16 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
adaptor.Init(info)

// Clean up empty system instruction
if request.SystemInstructions != nil {
if request.SystemInstruction != nil {
hasContent := false
for _, part := range request.SystemInstructions.Parts {
for _, part := range request.SystemInstruction.Parts {
if part.Text != "" {
hasContent = true
break
}
}
if !hasContent {
request.SystemInstructions = nil
request.SystemInstruction = nil
}
}

Expand Down
Loading