Skip to content
Open
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
8 changes: 8 additions & 0 deletions model/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,14 @@ func (channel *Channel) ValidateSettings() error {
return err
}
}
if err := channelOtherSettings.ContentToReasoning.Validate(); err != nil {
return err
}
if channelOtherSettings.ContentToReasoning != nil &&
channelOtherSettings.ContentToReasoning.Enabled &&
channelParams.ThinkingToContent {
return fmt.Errorf("thinking_to_content and content_to_reasoning cannot both be enabled")
}
if channel.Type == constant.ChannelTypeAdvancedCustom && channelOtherSettings.UpstreamModelUpdateCheckEnabled {
if _, ok := channelOtherSettings.AdvancedCustom.ModelListRoute(); !ok {
return fmt.Errorf("advanced custom channels require a %s route when upstream model update checks are enabled", dto.AdvancedCustomModelListPath)
Expand Down
90 changes: 90 additions & 0 deletions model/channel_settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,93 @@ func TestAdvancedCustomChannelRequiresModelListRouteOnlyWhenUpdateChecksEnabled(
})
}
}

func TestContentToReasoningSettingsValidation(t *testing.T) {
tests := []struct {
name string
channel *Channel
wantErr string
}{
{
name: "enabled with default markers",
channel: func() *Channel {
channel := &Channel{}
channel.SetOtherSettings(dto.ChannelOtherSettings{
ContentToReasoning: &dto.ContentToReasoningSettings{Enabled: true},
})
return channel
}(),
},
{
name: "enabled with paired markers",
channel: func() *Channel {
channel := &Channel{}
channel.SetOtherSettings(dto.ChannelOtherSettings{
ContentToReasoning: &dto.ContentToReasoningSettings{
Enabled: true,
Markers: []dto.ContentToReasoningMarkerPair{
{Start: "<think>", End: "</think>"},
{Start: "[think]", End: "[/think]"},
},
},
})
return channel
}(),
},
{
name: "incomplete marker rejected",
channel: func() *Channel {
channel := &Channel{}
channel.SetOtherSettings(dto.ChannelOtherSettings{
ContentToReasoning: &dto.ContentToReasoningSettings{
Enabled: true,
Markers: []dto.ContentToReasoningMarkerPair{
{Start: "<think>", End: ""},
},
},
})
return channel
}(),
wantErr: "both start and end",
},
{
name: "disabled with invalid marker is tolerated",
channel: func() *Channel {
channel := &Channel{}
channel.SetOtherSettings(dto.ChannelOtherSettings{
ContentToReasoning: &dto.ContentToReasoningSettings{
Enabled: false,
Markers: []dto.ContentToReasoningMarkerPair{
{Start: "<think>", End: ""},
},
},
})
return channel
}(),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.channel.ValidateSettings()
if tt.wantErr == "" {
require.NoError(t, err)
return
}
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
})
}
}

func TestContentToReasoningConflictsWithThinkingToContent(t *testing.T) {
channel := &Channel{}
channel.SetSetting(dto.ChannelSettings{ThinkingToContent: true})
channel.SetOtherSettings(dto.ChannelOtherSettings{
ContentToReasoning: &dto.ContentToReasoningSettings{Enabled: true},
})

err := channel.ValidateSettings()
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot both be enabled")
}
1 change: 1 addition & 0 deletions relay/channel/claude/relay-claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
switch info.RelayFormat {
case types.RelayFormatOpenAI:
openaiResponse := ResponseClaude2OpenAI(&claudeResponse)
info.TransformContentToReasoningFull(openaiResponse)
openaiResponse.Usage = buildOpenAIStyleUsageFromClaudeUsage(claudeInfo.Usage)
responseData, err = common.Marshal(openaiResponse)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions relay/channel/gemini/relay-gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *
if err != nil {
return usage, err
}
openai.FlushContentToReasoning(c, info)

response := helper.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage)
if info.RelayFormat == types.RelayFormatClaude && info.ClaudeConvertInfo != nil && !info.ClaudeConvertInfo.Done {
Expand Down Expand Up @@ -363,6 +364,7 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R
usage := buildUsageFromGeminiResponse(c, info, &geminiResponse)

fullTextResponse.Usage = usage
info.TransformContentToReasoningFull(fullTextResponse)

switch info.RelayFormat {
case types.RelayFormatOpenAI:
Expand Down
39 changes: 39 additions & 0 deletions relay/channel/openai/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,29 @@ import (

// 辅助函数
func HandleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error {
if info.ContentToReasoningEnabled() {
responses, err := info.TransformContentToReasoningStream(data)
if err != nil {
return err
}
for _, response := range responses {
responseData, err := common.Marshal(response)
if err != nil {
return err
}
info.SendResponseCount++
if err := handleStreamFormat(c, info, string(responseData), forceFormat, false); err != nil {
return err
}
}
return nil
}
Comment thread
somnifex marked this conversation as resolved.

info.SendResponseCount++
return handleStreamFormat(c, info, data, forceFormat, thinkToContent)
}

func handleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error {
switch info.RelayFormat {
case types.RelayFormatOpenAI:
return sendStreamData(c, info, data, forceFormat, thinkToContent)
Expand All @@ -34,6 +55,24 @@ func HandleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string
return nil
}

// FlushContentToReasoning emits buffered unclosed reasoning after the upstream
// stream has ended.
func FlushContentToReasoning(c *gin.Context, info *relaycommon.RelayInfo) {
if info == nil || !info.ContentToReasoningEnabled() {
return
}
responses, _ := info.ContentToReasoningFlush()
for _, response := range responses {
responseData, err := common.Marshal(response)
if err != nil {
continue
}
if err := handleStreamFormat(c, info, string(responseData), info.ChannelSetting.ForceFormat, false); err != nil {
common.SysLog("error flushing content_to_reasoning: " + err.Error())
}
}
}

func handleClaudeFormat(c *gin.Context, data string, info *relaycommon.RelayInfo) error {
var streamResponse dto.ChatCompletionsStreamResponse
if err := common.Unmarshal(common.StringToByteSlice(data), &streamResponse); err != nil {
Expand Down
12 changes: 10 additions & 2 deletions relay/channel/openai/relay-openai.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,16 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re

if info.RelayFormat == types.RelayFormatOpenAI {
if shouldSendLastResp {
_ = sendStreamData(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent)
if info.ContentToReasoningEnabled() {
_ = HandleStreamFormat(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent)
} else {
_ = sendStreamData(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent)
}
}
}

FlushContentToReasoning(c, info)

if !containStreamUsage {
usage = service.ResponseText2Usage(c, responseTextBuilder.String(), info.UpstreamModelName, info.GetEstimatePromptTokens())
usage.CompletionTokens += toolCount * 7
Expand Down Expand Up @@ -266,6 +272,8 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
}
}

c2rChanged := info.TransformContentToReasoningFull(&simpleResponse)

forceFormat := false
if info.ChannelSetting.ForceFormat {
forceFormat = true
Expand Down Expand Up @@ -301,7 +309,7 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
bodyMap["usage"] = simpleResponse.Usage
responseBody, _ = common.Marshal(bodyMap)
}
if forceFormat {
if forceFormat || c2rChanged {
responseBody, err = common.Marshal(simpleResponse)
if err != nil {
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
Expand Down
Loading