Compare commits

...

1 Commits

Author SHA1 Message Date
coderabbitai[bot]
8aedbb29c3 📝 Add docstrings to main
Docstrings generation was requested by @ZhaoZuohong.

* https://github.com/QuantumNous/new-api/pull/2279#issuecomment-3568907431

The following files were modified:

* `relay/channel/openai/helper.go`
* `relay/channel/openai/reasoning_converter.go`
* `relay/channel/openai/relay-openai.go`
2025-11-24 04:41:04 +00:00
3 changed files with 121 additions and 1 deletions

View File

@@ -5,6 +5,7 @@ import (
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/logger"
relaycommon "github.com/QuantumNous/new-api/relay/common"
@@ -18,10 +19,26 @@ import (
"github.com/gin-gonic/gin"
)
// 辅助函数
// HandleStreamFormat processes a streaming response payload according to the provided RelayInfo and forwards it to the appropriate format-specific handler.
//
// It increments info.SendResponseCount, optionally converts OpenRouter "reasoning" fields to "reasoning_content" when the channel is OpenRouter and OpenRouterConvertToOpenAI is enabled, and then dispatches the (possibly modified) JSON string to the handler for the configured RelayFormat (OpenAI, Claude, or Gemini). It returns any error produced by the selected handler or nil if no handler is invoked.
func HandleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error {
info.SendResponseCount++
// OpenRouter reasoning 字段转换reasoning -> reasoning_content
// 仅当启用转换为OpenAI兼容格式时执行
if info.ChannelType == constant.ChannelTypeOpenRouter && info.ChannelOtherSettings.OpenRouterConvertToOpenAI {
var streamResponse dto.ChatCompletionsStreamResponse
if err := common.Unmarshal(common.StringToByteSlice(data), &streamResponse); err == nil {
convertOpenRouterReasoningFieldsStream(&streamResponse)
// 重新序列化为JSON
newData, err := common.Marshal(streamResponse)
if err == nil {
data = string(newData)
}
}
}
switch info.RelayFormat {
case types.RelayFormatOpenAI:
return sendStreamData(c, info, data, forceFormat, thinkToContent)
@@ -253,9 +270,26 @@ func HandleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStream
}
}
// sendResponsesStreamData sends a non-empty data chunk for the given stream response to the client.
// If data is empty, it returns without sending anything.
func sendResponsesStreamData(c *gin.Context, streamResponse dto.ResponsesStreamResponse, data string) {
if data == "" {
return
}
helper.ResponseChunkData(c, streamResponse, data)
}
// convertOpenRouterReasoningFieldsStream converts each choice's `Delta` in a streaming ChatCompletions response
// by normalizing any `reasoning` fields into `reasoning_content`.
// It applies ConvertReasoningField to every choice's Delta and is a no-op if `response` is nil or has no choices.
func convertOpenRouterReasoningFieldsStream(response *dto.ChatCompletionsStreamResponse) {
if response == nil || len(response.Choices) == 0 {
return
}
// 遍历所有choices对每个Delta使用统一的泛型函数进行转换
for i := range response.Choices {
choice := &response.Choices[i]
ConvertReasoningField(&choice.Delta)
}
}

View File

@@ -0,0 +1,35 @@
package openai
// ReasoningHolder 定义一个通用的接口用于操作包含reasoning字段的结构体
type ReasoningHolder interface {
// 获取reasoning字段的值
GetReasoning() string
// 设置reasoning字段的值
SetReasoning(reasoning string)
// 获取reasoning_content字段的值
GetReasoningContent() string
// 设置reasoning_content字段的值
SetReasoningContent(reasoningContent string)
}
// ConvertReasoningField 通用的reasoning字段转换函数
// 将reasoning字段的内容移动到reasoning_content字段
// ConvertReasoningField moves the holder's reasoning into its reasoning content and clears the original reasoning field.
// If GetReasoning returns an empty string, the holder is unchanged. When clearing, types that implement SetReasoningToNil()
// will have that method invoked; otherwise SetReasoning("") is used.
func ConvertReasoningField[T ReasoningHolder](holder T) {
reasoning := holder.GetReasoning()
if reasoning != "" {
holder.SetReasoningContent(reasoning)
}
// 使用类型断言来智能清理reasoning字段
switch h := any(holder).(type) {
case interface{ SetReasoningToNil() }:
// 流式响应指针类型设为nil
h.SetReasoningToNil()
default:
// 非流式响应:值类型,设为空字符串
holder.SetReasoning("")
}
}

View File

@@ -194,6 +194,25 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re
return usage, nil
}
// OpenaiHandler processes an upstream OpenAI-like HTTP response, normalizes or infers token usage,
// optionally converts OpenRouter reasoning fields to OpenAI-compatible `reasoning_content`, adapts
// the response to the configured relay format (OpenAI, Claude, or Gemini), writes the final body
// to the client, and returns the computed usage.
//
// It will:
// - Handle OpenRouter enterprise wrapper responses when the channel is OpenRouter Enterprise.
// - Unmarshal the upstream body into an internal simple response and, when configured,
// convert OpenRouter `reasoning` fields into `reasoning_content`.
// - If usage prompt tokens are missing, infer completion tokens by counting tokens in choices
// (falling back to per-choice text token counting) and set Prompt/Completion/Total tokens.
// - Apply channel-specific post-processing to usage (cached token adjustments).
// - Depending on RelayFormat and channel settings, inject updated usage into the body,
// reserialize the converted simple response when ForceFormat is enabled or when OpenRouter
// conversion was applied, or convert the response to Claude/Gemini formats.
// - Write the final response body to the client via a graceful copy helper.
//
// Returns the final usage (possibly inferred or modified) or a NewAPIError describing any failure
// encountered while reading, parsing, or transforming the upstream response.
func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
defer service.CloseResponseBodyGracefully(resp)
@@ -226,6 +245,12 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
}
// OpenRouter reasoning 字段转换reasoning -> reasoning_content
// 仅当启用转换为OpenAI兼容格式时执行修改现有无条件转换
if info.ChannelType == constant.ChannelTypeOpenRouter && info.ChannelOtherSettings.OpenRouterConvertToOpenAI {
convertOpenRouterReasoningFields(&simpleResponse)
}
if oaiError := simpleResponse.GetOpenAIError(); oaiError != nil && oaiError.Type != "" {
return nil, types.WithOpenAIError(*oaiError, resp.StatusCode)
}
@@ -271,6 +296,13 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
} else {
// 对于 OpenRouter仅在执行转换后重新序列化
if info.ChannelType == constant.ChannelTypeOpenRouter && info.ChannelOtherSettings.OpenRouterConvertToOpenAI {
responseBody, err = common.Marshal(simpleResponse)
if err != nil {
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
}
}
break
}
case types.RelayFormatClaude:
@@ -672,6 +704,10 @@ func applyUsagePostProcessing(info *relaycommon.RelayInfo, usage *dto.Usage, res
}
}
// extractCachedTokensFromBody extracts a cached token count from a JSON response body.
// It looks for cached token values in the following fields (in order): `usage.prompt_tokens_details.cached_tokens`,
// `usage.cached_tokens`, and `usage.prompt_cache_hit_tokens`. It returns the first found value and `true`;
// if none are present or the body cannot be parsed, it returns 0 and `false`.
func extractCachedTokensFromBody(body []byte) (int, bool) {
if len(body) == 0 {
return 0, false
@@ -702,3 +738,18 @@ func extractCachedTokensFromBody(body []byte) (int, bool) {
}
return 0, false
}
// convertOpenRouterReasoningFields 转换OpenRouter响应中的reasoning字段为reasoning_content
// convertOpenRouterReasoningFields converts OpenRouter-style `reasoning` fields into `reasoning_content` for every choice's message in the provided OpenAITextResponse.
// It modifies the response in place and is a no-op if `response` is nil or contains no choices.
func convertOpenRouterReasoningFields(response *dto.OpenAITextResponse) {
if response == nil || len(response.Choices) == 0 {
return
}
// 遍历所有choices对每个Message使用统一的泛型函数进行转换
for i := range response.Choices {
choice := &response.Choices[i]
ConvertReasoningField(&choice.Message)
}
}