Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion model/ability.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
}
}
} else {
return nil, errors.New("channel not found")
return nil, nil
Comment on lines 138 to +139

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

Critical: Breaking change in exported function may affect external callers.

This exported function now returns (nil, nil) instead of (nil, error) when no abilities are found. This breaks the expected Go error handling contract where callers check if err != nil to detect failure conditions.

Since this is an exported function, verify all callers (both internal and potentially external) can handle the new semantics. The function should either:

  1. Maintain the error return (recommended):
	} else {
-		return nil, nil
+		return nil, errors.New("channel not found")
	}
  1. Or update the function documentation to clearly indicate the new behavior and ensure all callers are updated.

Let me verify all callers of this exported function:


🏁 Script executed:

#!/bin/bash
# Search for all callers of the exported GetRandomSatisfiedChannel function
rg -A 5 -B 2 "GetRandomSatisfiedChannel\("

Length of output: 2409


Action Required: Handle nil channel return in all callers of GetRandomSatisfiedChannel

The exported function GetRandomSatisfiedChannel now returns (nil, nil) when no channel is found. While controller/relay.go already checks for a nil channel, middleware/distributor.go does not—and will panic or exhibit undefined behavior when it receives a nil channel with no error.

Please update callers (or revert the change) as follows:

• middleware/distributor.go
After invoking CacheGetRandomSatisfiedChannel, add a nil‐channel check before using channel:

   channel, selectGroup, err = model.CacheGetRandomSatisfiedChannel(c, userGroup, modelRequest.Model, 0)
   if err != nil {
       // existing error handling…
   }
+    if channel == nil {
+        return /* appropriate error or fallback, e.g.: */
+            fmt.Errorf("no channel available for group %s, model %s", selectGroup, modelRequest.Model)
+    }
   // proceed safely with non‐nil channel…

• model/channel_cache.go
In the !common.MemoryCacheEnabled branch, ensure the three‐value return matches the signature:

if !common.MemoryCacheEnabled {
-    return GetRandomSatisfiedChannel(group, model, retry)
+    ch, err := GetRandomSatisfiedChannel(group, model, retry)
+    return ch, selectGroup, err
}

• model/ability.go
Either revert to returning an error (errors.New("channel not found")) for consistency with Go conventions, or update the function’s documentation to explicitly state that (nil, nil) signals “no channel found” and that callers must guard against a nil *Channel.

Fix these locations so that no caller continues past a nil channel without proper handling.

🤖 Prompt for AI Agents
In model/ability.go at lines 138-139, the function GetRandomSatisfiedChannel
returns (nil, nil) when no channel is found, which can cause panics in callers
that do not check for nil channels. To fix this, update all callers to check for
a nil channel before use: in middleware/distributor.go, add a nil check after
calling CacheGetRandomSatisfiedChannel; in model/channel_cache.go, modify the
!common.MemoryCacheEnabled branch to capture and return the three values
(channel, selectGroup, error) from GetRandomSatisfiedChannel; and in
model/ability.go, either revert to returning an error when no channel is found
or update the function documentation to clearly state that (nil, nil) means no
channel and callers must handle it. Ensure no caller proceeds with a nil channel
without proper handling.

}
err = DB.First(&channel, "id = ?", channel.Id).Error
return &channel, err
Expand Down
2 changes: 1 addition & 1 deletion model/channel_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
channels := group2model2channels[group][model]

if len(channels) == 0 {
return nil, errors.New("channel not found")
return nil, nil
Comment on lines 132 to +133

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

Critical: Breaking change in error handling semantics may cause nil channel returns without errors.

This change modifies the function to return (nil, nil) instead of (nil, error) when no channels are found. However, the caller on lines 107-110 expects an error when no channel is available and will now incorrectly return a nil channel without an error to its caller.

The caller CacheGetRandomSatisfiedChannel at lines 107-110 needs to handle the new semantics:

	} else {
		channel, err = getRandomSatisfiedChannel(group, model, retry)
-		if err != nil {
-			return nil, group, err
-		}
+		if err != nil {
+			return nil, group, err
+		}
+		if channel == nil {
+			return nil, group, errors.New("channel not found")
+		}
	}

Let me verify all callers of this function to ensure they handle the new semantics correctly:


🏁 Script executed:

#!/bin/bash
# Search for all callers of getRandomSatisfiedChannel to verify they handle nil channel with nil error
rg -A 10 -B 2 "getRandomSatisfiedChannel"

Length of output: 1701


Handle missing channel error in CacheGetRandomSatisfiedChannel

The change in getRandomSatisfiedChannel to return (nil, nil) when no channels are found breaks the error path in the else‐branch of CacheGetRandomSatisfiedChannel. You must explicitly detect a nil channel and return an error instead of propagating a silent success.

• File: model/channel_cache.go
Location: inside else { … } starting around line 107

Suggested diff:

     } else {
-        channel, err = getRandomSatisfiedChannel(group, model, retry)
-        if err != nil {
-            return nil, group, err
-        }
+        channel, err = getRandomSatisfiedChannel(group, model, retry)
+        if err != nil {
+            return nil, group, err
+        }
+        if channel == nil {
+            return nil, group, errors.New("channel not found")
+        }
     }

This ensures callers continue to receive an error when no channel is available.

📝 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
if len(channels) == 0 {
return nil, errors.New("channel not found")
return nil, nil
} else {
channel, err = getRandomSatisfiedChannel(group, model, retry)
if err != nil {
return nil, group, err
}
if channel == nil {
return nil, group, errors.New("channel not found")
}
}
🤖 Prompt for AI Agents
In model/channel_cache.go around lines 107 and 132-133, the function
getRandomSatisfiedChannel returns (nil, nil) when no channels are found, which
causes CacheGetRandomSatisfiedChannel to silently succeed with a nil channel. To
fix this, modify CacheGetRandomSatisfiedChannel to explicitly check if the
returned channel is nil and return an appropriate error instead of nil, nil.
This ensures that callers receive an error when no channel is available,
preserving correct error handling.

}

if len(channels) == 1 {
Expand Down
12 changes: 8 additions & 4 deletions relay/channel/gemini/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"one-api/common"
"one-api/dto"
"one-api/relay/channel"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
"one-api/setting/model_setting"
Expand All @@ -21,10 +22,13 @@ import (
type Adaptor struct {
}

func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
//TODO implement me
panic("implement me")
return nil, nil
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) {
adaptor := openai.Adaptor{}
oaiReq, err := adaptor.ConvertClaudeRequest(c, info, req)
if err != nil {
return nil, err
}
return a.ConvertOpenAIRequest(c, info, oaiReq.(*dto.GeneralOpenAIRequest))
}

func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
Expand Down
61 changes: 49 additions & 12 deletions relay/channel/gemini/relay-gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"one-api/common"
"one-api/constant"
"one-api/dto"
"one-api/relay/channel/openai"
relaycommon "one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
Expand Down Expand Up @@ -736,7 +737,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C
choice := dto.ChatCompletionsStreamResponseChoice{
Index: int(candidate.Index),
Delta: dto.ChatCompletionsStreamResponseChoiceDelta{
Role: "assistant",
//Role: "assistant",

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

Verify impact of commented out assistant role assignment.

The commented out role assignment in the streaming response delta might affect API compatibility. Clients may expect the role field to be present in streaming responses.


🏁 Script executed:

#!/bin/bash
# Description: Check if role is set elsewhere in streaming responses and verify OpenAI compatibility

# Search for role assignments in streaming response handling
rg -A 5 -B 5 "Role.*assistant" --type go

# Look for Delta role assignments in other parts of the codebase
rg -A 3 -B 3 "Delta.*Role" --type go

# Check OpenAI documentation references or comments about role requirements
rg -i "role.*required\|required.*role" --type go

Length of output: 19609


Confirm explicit assistant role initialization in streaming deltas

We need to ensure that the first streamed chunk still declares "assistant" so clients parsing OpenAI‐style streams know when the assistant reply begins. In all other adapters (Cloudflare, Claude, Cohere, etc.), the very first ChatCompletionsStreamResponseChoiceDelta includes Role: "assistant". In the Gemini adapter this line is commented out:

choice := dto.ChatCompletionsStreamResponseChoice{
    Index: int(candidate.Index),
    Delta: dto.ChatCompletionsStreamResponseChoiceDelta{
        //Role: "assistant",
    },
}

Without restoring it, downstream consumers may never see the assistant role header and could ignore or misinterpret subsequent content deltas.

Please add back:

 choice := dto.ChatCompletionsStreamResponseChoice{
     Index: int(candidate.Index),
     Delta: dto.ChatCompletionsStreamResponseChoiceDelta{
-        //Role: "assistant",
+        Role: "assistant",
     },
 }

at relay/channel/gemini/relay-gemini.go around line 740 to align with OpenAI’s streaming protocol.

🤖 Prompt for AI Agents
In relay/channel/gemini/relay-gemini.go at line 740, the assistant role
initialization in the first streamed delta is commented out, which can cause
downstream clients to miss the assistant role header. Uncomment the line setting
Role: "assistant" inside the ChatCompletionsStreamResponseChoiceDelta struct to
explicitly declare the assistant role in the initial streamed chunk, ensuring
compatibility with OpenAI-style streaming protocols.

},
}
var texts []string
Expand Down Expand Up @@ -798,13 +799,36 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C
return &response, isStop, hasImage
}

func handleStream(c *gin.Context, info *relaycommon.RelayInfo, resp *dto.ChatCompletionsStreamResponse) error {
streamData, err := common.Marshal(resp)
if err != nil {
return fmt.Errorf("failed to marshal stream response: %w", err)
}
err = openai.HandleStreamFormat(c, info, string(streamData), info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent)
if err != nil {
return fmt.Errorf("failed to handle stream format: %w", err)
}
return nil
}

func handleFinalStream(c *gin.Context, info *relaycommon.RelayInfo, resp *dto.ChatCompletionsStreamResponse) error {
streamData, err := common.Marshal(resp)
if err != nil {
return fmt.Errorf("failed to marshal stream response: %w", err)
}
openai.HandleFinalResponse(c, info, string(streamData), resp.Id, resp.Created, resp.Model, resp.GetSystemFingerprint(), resp.Usage, info.ShouldIncludeUsage)
return nil
}

func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
// responseText := ""
id := helper.GetResponseID(c)
createAt := common.GetTimestamp()
var usage = &dto.Usage{}
var imageCount int

respCount := 0

helper.StreamScannerHandler(c, resp, info, func(data string) bool {
var geminiResponse GeminiChatResponse
err := common.UnmarshalJsonStr(data, &geminiResponse)
Expand Down Expand Up @@ -833,18 +857,31 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
}
}
}
err = helper.ObjectData(c, response)

if respCount == 0 {
// send first response
err = handleStream(c, info, helper.GenerateStartEmptyResponse(id, createAt, info.UpstreamModelName, nil))
if err != nil {
common.LogError(c, err.Error())
}
}

err = handleStream(c, info, response)
if err != nil {
common.LogError(c, err.Error())
}
if isStop {
response := helper.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop)
helper.ObjectData(c, response)
_ = handleStream(c, info, helper.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop))
}
respCount++
return true
})

var response *dto.ChatCompletionsStreamResponse
if respCount == 0 {
// 空补全,报错不计费
// empty response, throw an error
return nil, types.NewOpenAIError(errors.New("no response received from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError)
}

if imageCount != 0 {
if usage.CompletionTokens == 0 {
Expand All @@ -855,14 +892,14 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
usage.PromptTokensDetails.TextTokens = usage.PromptTokens
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens

if info.ShouldIncludeUsage {
response = helper.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage)
err := helper.ObjectData(c, response)
if err != nil {
common.SysError("send final response failed: " + err.Error())
}
response := helper.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage)
err := handleFinalStream(c, info, response)
if err != nil {
common.SysError("send final response failed: " + err.Error())
}
helper.Done(c)
//if info.RelayFormat == relaycommon.RelayFormatOpenAI {
// helper.Done(c)
//}
//resp.Body.Close()
return usage, nil
}
Expand Down
4 changes: 2 additions & 2 deletions relay/channel/openai/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
)

// 辅助函数
func handleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error {
func HandleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error {
info.SendResponseCount++
switch info.RelayFormat {
case relaycommon.RelayFormatOpenAI:
Expand Down Expand Up @@ -158,7 +158,7 @@ func handleLastResponse(lastStreamData string, responseId *string, createAt *int
return nil
}

func handleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStreamData string,
func HandleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStreamData string,
responseId string, createAt int64, model string, systemFingerprint string,
usage *dto.Usage, containStreamUsage bool) {

Expand Down
21 changes: 4 additions & 17 deletions relay/channel/openai/relay-openai.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,24 +123,11 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re
var toolCount int
var usage = &dto.Usage{}
var streamItems []string // store stream items
var forceFormat bool
var thinkToContent bool

if info.ChannelSetting.ForceFormat {
forceFormat = true
}

if info.ChannelSetting.ThinkingToContent {
thinkToContent = true
}

var (
lastStreamData string
)
var lastStreamData string

helper.StreamScannerHandler(c, resp, info, func(data string) bool {
if lastStreamData != "" {
err := handleStreamFormat(c, info, lastStreamData, forceFormat, thinkToContent)
err := HandleStreamFormat(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent)
if err != nil {
common.SysError("error handling stream format: " + err.Error())
}
Expand All @@ -161,7 +148,7 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re

if info.RelayFormat == relaycommon.RelayFormatOpenAI {
if shouldSendLastResp {
_ = sendStreamData(c, info, lastStreamData, forceFormat, thinkToContent)
_ = sendStreamData(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent)
}
}

Expand All @@ -180,7 +167,7 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re
}
}
}
handleFinalResponse(c, info, lastStreamData, responseId, createAt, model, systemFingerprint, usage, containStreamUsage)
HandleFinalResponse(c, info, lastStreamData, responseId, createAt, model, systemFingerprint, usage, containStreamUsage)

return usage, nil
}
Expand Down
18 changes: 18 additions & 0 deletions relay/helper/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,24 @@ func GetLocalRealtimeID(c *gin.Context) string {
return fmt.Sprintf("evt_%s", logID)
}

func GenerateStartEmptyResponse(id string, createAt int64, model string, systemFingerprint *string) *dto.ChatCompletionsStreamResponse {
return &dto.ChatCompletionsStreamResponse{
Id: id,
Object: "chat.completion.chunk",
Created: createAt,
Model: model,
SystemFingerprint: systemFingerprint,
Choices: []dto.ChatCompletionsStreamResponseChoice{
{
Delta: dto.ChatCompletionsStreamResponseChoiceDelta{
Role: "assistant",
Content: common.GetPointer(""),
},
},
},
}
}

func GenerateStopResponse(id string, createAt int64, model string, finishReason string) *dto.ChatCompletionsStreamResponse {
return &dto.ChatCompletionsStreamResponse{
Id: id,
Expand Down
1 change: 1 addition & 0 deletions types/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const (
ErrorCodeBadResponseStatusCode ErrorCode = "bad_response_status_code"
ErrorCodeBadResponse ErrorCode = "bad_response"
ErrorCodeBadResponseBody ErrorCode = "bad_response_body"
ErrorCodeEmptyResponse ErrorCode = "empty_response"

// sql error
ErrorCodeQueryDataError ErrorCode = "query_data_error"
Expand Down