From 8aedbb29c30202d331f9ff3f95eecbebd9d63180 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 04:41:04 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`main`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` --- relay/channel/openai/helper.go | 36 ++++++++++++++- relay/channel/openai/reasoning_converter.go | 35 ++++++++++++++ relay/channel/openai/relay-openai.go | 51 +++++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 relay/channel/openai/reasoning_converter.go diff --git a/relay/channel/openai/helper.go b/relay/channel/openai/helper.go index 69731d4d2..ebdb89217 100644 --- a/relay/channel/openai/helper.go +++ b/relay/channel/openai/helper.go @@ -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) + } +} \ No newline at end of file diff --git a/relay/channel/openai/reasoning_converter.go b/relay/channel/openai/reasoning_converter.go new file mode 100644 index 000000000..fe41bc205 --- /dev/null +++ b/relay/channel/openai/reasoning_converter.go @@ -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("") + } +} diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index eafb11d99..f2100e29d 100644 --- a/relay/channel/openai/relay-openai.go +++ b/relay/channel/openai/relay-openai.go @@ -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) + } +} \ No newline at end of file