new-api
new-api copied to clipboard
OpenAI 渠道“强制格式化”选项将 `reasoning` 字段重命名为 `reasoning_content` 并为 OpenRouter 上游渠道开启
修改说明
OpenRouter 虽然与 OpenAI 兼容格式使用相同的 /v1/chat/completions 端点,然而其响应格式与一般的 OpenAI 兼容格式有些不同。除 OpenRouter 额外添加的字段外,一个明显的区别是:思考内容在 reasoning 字段而非常见的 reasoning_content 字段中。有些客户端(如 Cline)在添加“兼容 OpenAI”格式的客户端时,只处理 reasoning_content 字段、不处理 OpenRouter 的 reasoning 字段。本 PR 为 OpenRouter 渠道上游添加“转换为 OpenAI 兼容格式”的选项,允许将响应格式(尤其是思考字段)转换为 OpenAI 格式,以适配更多客户端。
本 PR 添加的功能不会和“思考内容转换”选项(将 reasoning_content 转换为 <think> 标签拼接到内容中)冲突:
- 非流式下,“思考内容转换”选项无效,只有本选项生效;
- 在流式下,只要开启“思考内容转换”选项,推理内容就在
<think>标签中;关闭“思考内容转换”选项时,字段受本选项控制。
其它信息
- Cerebras.ai 的推理内容也在
reasoning而不是reasoning_content字段中。
数据格式
修改前,流式
data: {"id":"xxx","provider":"xxx","model":"xxx","object":"chat.completion.chunk","created":111,"choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning":"The","reasoning_details":[{"type":"reasoning.text","text":"xxx","format":"unknown","index":0}]},"finish_reason":null,"native_finish_reason":null,"logprobs":null}]}
修改后,流式
data: {"id":"xxx","object":"chat.completion.chunk","created":111,"model":"xxx","system_fingerprint":null,"choices":[{"delta":{"content":"","reasoning_content":"xxx","role":"assistant"},"logprobs":null,"finish_reason":null,"index":0}],"usage":null}
修改前,非流式
{
"id": "xxx",
"provider": "xxx",
"model": "xxx",
"object": "chat.completion",
"created": 111,
"choices": [
{
"logprobs": null,
"finish_reason": "stop",
"native_finish_reason": "stop",
"index": 0,
"message": {
"role": "assistant",
"content": "xxx",
"refusal": null,
"reasoning": "xxx",
"reasoning_details": [
{
"format": "unknown",
"index": 0,
"type": "reasoning.text",
"text": "xxx"
}
]
}
}
],
"usage": {
"prompt_tokens": 111,
"completion_tokens": 111,
"total_tokens": 111,
"cost": 111,
"is_byok": false,
"prompt_tokens_details": {
"cached_tokens": 111,
"audio_tokens": 0,
"video_tokens": 0
},
"cost_details": {
"upstream_inference_cost": null,
"upstream_inference_prompt_cost": 0,
"upstream_inference_completions_cost": 0
},
"completion_tokens_details": {
"reasoning_tokens": 111,
"image_tokens": 0
}
}
}
修改后,非流式
{
"id": "xxx",
"model": "xxx",
"object": "chat.completion",
"created": 111,
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "xxx",
"reasoning_content": "xxx"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 111,
"completion_tokens": 111,
"total_tokens": 111,
"prompt_tokens_details": {
"cached_tokens": 111,
"text_tokens": 0,
"audio_tokens": 0,
"image_tokens": 0
},
"completion_tokens_details": {
"text_tokens": 0,
"audio_tokens": 0,
"reasoning_tokens": 111
},
"input_tokens": 0,
"output_tokens": 0,
"input_tokens_details": null,
"claude_cache_creation_5_m_tokens": 0,
"claude_cache_creation_1_h_tokens": 0,
"cost": 0
}
}
Summary by CodeRabbit
-
Improvements
- OpenRouter responses (streaming and non‑streaming) are normalized to OpenAI-style outputs so reasoning content is preserved and mapped consistently; streaming chunks are handled correctly and token usage inference is improved.
-
New Features
- Channel-level toggle (default off) in the channel edit modal to enable automatic OpenRouter → OpenAI format conversion; setting is persisted and reset on modal close.
-
Localization
- Added UI text and helper descriptions for the new toggle in en, fr, ja, ru, vi and zh.
✏️ Tip: You can customize this high-level summary in your review settings.
Walkthrough
Adds getters/setters for reasoning and reasoning_content on DTOs, a ReasoningHolder interface and ConvertReasoningField generic, applies conversion for OpenRouter responses (stream and non-stream) when an opt-in channel flag is enabled, exposes that flag in the channel edit UI, and adds translations for the new UI strings.
Changes
| Cohort / File(s) | Summary |
|---|---|
DTO: Request Accessors dto/openai_request.go |
Adds GetReasoning(), SetReasoning(string), GetReasoningContent(), SetReasoningContent(string) on Message. |
DTO: Stream Response Delta dto/openai_response.go |
Adds GetReasoning() string, SetReasoning(string), and SetReasoningToNil() on ChatCompletionsStreamResponseChoiceDelta. |
Reasoning Conversion Infra relay/channel/openai/reasoning_converter.go |
Adds public ReasoningHolder interface and exported generic ConvertReasoningField[T ReasoningHolder](holder T) that moves reasoning → reasoning_content and clears reasoning (prefers SetReasoningToNil() when available). |
Server: OpenRouter conversion (stream & non-stream) relay/channel/openai/helper.go, relay/channel/openai/relay-openai.go |
When ChannelTypeOpenRouter and OpenRouterConvertToOpenAI are enabled, unmarshal OpenRouter responses (streamed and simple), run conversion across choices/messages via ConvertReasoningField, re-marshal, and replace outgoing JSON so downstream sees reasoning_content. |
Channel Setting DTO dto/channel_settings.go |
Adds OpenRouterConvertToOpenAI bool to ChannelOtherSettings (serialized openrouter_convert_to_openai). |
Channel Edit UI web/src/components/table/channels/modals/EditChannelModal.jsx |
Adds openrouter_convert_to_openai state, loads/parses it from backend settings, shows a Form.Switch for OpenRouter channels, and persists it on submit; handles reset/close. |
Localization web/src/i18n/locales/{en,fr,ja,ru,vi,zh}.json |
Adds two translation keys for the new switch: label and helper text describing conversion of reasoning → reasoning_content (localized across listed files). |
Sequence Diagram(s)
sequenceDiagram
participant OR as OpenRouter API
participant Relay as Relay OpenAI Handler
participant Conv as ConvertReasoningField
participant Down as Downstream Consumer
Note right of Relay: Gate: ChannelTypeOpenRouter && OpenRouterConvertToOpenAI
OR->>Relay: response (stream or non‑stream, may include "reasoning")
alt Streaming response
Relay->>Relay: Unmarshal ChatCompletionsStreamResponse
Relay->>Conv: convertOpenRouterReasoningFieldsStream (iterate choices/deltas)
Conv->>Conv: GetReasoning() → SetReasoningContent(...) → SetReasoningToNil()/SetReasoning("")
Relay->>Relay: Re-marshal updated stream chunks
else Non-stream response
Relay->>Relay: Unmarshal simple response (choices → messages)
Relay->>Conv: convertOpenRouterReasoningFields (iterate messages)
Conv->>Conv: GetReasoning() → SetReasoningContent(...) → SetReasoning("")
Relay->>Relay: Re-marshal updated response JSON
end
Relay->>Down: response (contains reasoning_content, original reasoning cleared)
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes
- Pay special attention to:
- Generic constraint correctness and pointer vs value receiver semantics in ConvertReasoningField.
- Nil-handling and JSON serialization differences between
SetReasoningToNil()and setting"". - Streaming handler integration (chunk boundaries and partial JSON reserialization) in
relay/channel/openai/helper.go. - Frontend state initialization/reset and ensuring the flag is only shown/applied for OpenRouter channels.
Possibly related PRs
- QuantumNous/new-api#1577 — modifies OpenRouter-to-OpenAI reasoning-field handling and is strongly related to this conversion work.
Suggested reviewers
- seefs001
- Calcium-Ion
Poem
🐇 I hop through fields both old and new,
I tuck reasoning where content grew.
I nil a pointer, shift thought with care,
OpenRouter speaks OpenAI's air.
Hooray — a tidy rabbit's share.
Pre-merge checks and finishing touches
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | ⚠️ Warning | Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. | You can run @coderabbitai generate docstrings to improve docstring coverage. |
✅ Passed checks (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The PR title correctly describes the main change: adding an optional 'convert to OpenAI compatible format' feature for OpenRouter channels that renames the reasoning field to reasoning_content. The title accurately reflects the primary objective and scope of the changeset. |
✨ Finishing touches
- [ ] 📝 Generate docstrings
🧪 Generate unit tests (beta)
- [ ] Create PR with unit tests
- [ ] Post copyable unit tests in a comment
[!TIP]
📝 Customizable high-level summaries are now available in beta!
You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
- Provide your own instructions using the
high_level_summary_instructionssetting.- Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
- Use
high_level_summary_in_walkthroughto move the summary from the description to the walkthrough section.Example instruction:
"Divide the high-level summary into five sections:
- 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
- 📓 References — List relevant issues, discussions, documentation, or related PRs.
- 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
- 📊 Contributor Summary — Include a Markdown table showing contributions:
| Contributor | Lines Added | Lines Removed | Files Changed |- ✔️ Additional Notes — Add any extra reviewer context. Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
私认为直接强制更改不提供选项不是一个妥当的做法,至少需要给用户选择权,有些人会使用 https://ai-sdk.dev/providers/community-providers/openrouter 之类的Provider,如果按你这样强制改的话他们就没办法获取到预期的Reasoning,而且因为需要使用OpenRouter特定的一些字段所以也不能换Provider,算是破坏性更新。
需要使用 OpenRouter 特定的一些字段……算是破坏性更新。
是的。我想问题的根源还是在 OpenRouter 与 OpenAI 共用 /v1/chat/completions,导致区分不开,只能二选一。
使用 community-providers/openrouter 之类的Provider……
这种情况下,用户应该是希望获得密钥聚合的功能。还需要考虑把其它格式的上游也转换成 OpenRouter 格式吗?
需要给用户选择权
你认为在 OpenRouter 类型上游的“渠道额外设置”中添加一个默认关闭的选项怎么样?能够保持与现有行为的兼容,开启后也能够完成对字段的转换。
需要使用 OpenRouter 特定的一些字段……算是破坏性更新。
是的。我想问题的根源还是在 OpenRouter 与 OpenAI 共用
/v1/chat/completions,导致区分不开,只能二选一。使用 community-providers/openrouter 之类的Provider……
这种情况下,用户应该是希望获得密钥聚合的功能。还需要考虑把其它格式的上游也转换成 OpenRouter 格式吗?
需要给用户选择权
你认为在 OpenRouter 类型上游的“渠道额外设置”中添加一个默认关闭的选项怎么样?能够保持与现有行为的兼容,开启后也能够完成对字段的转换。
我认为加个选项就够了,默认不进行转换操作。 不用考虑其他格式转Openrouter吧,这个比较小众,另外也不一定会是需要密钥聚合的用户,可能只是想给自己的帐号套一层代理分给别人用。
好的,那我去给 OpenRouter 类型的上游加一个选项。
@seefs001 我觉得基本没问题了,麻烦你看看?
@coderabbitai generate docstrings
✅ Actions performed
Initiated docstring generation; will generate only if new commits exist.
[!NOTE] Docstrings generation - SUCCESS Generated docstrings for this pull request at https://github.com/QuantumNous/new-api/pull/2285
@coderabbitai generate docstrings
不用找它给你生成docs,看他给你提的意见修就行,我晚点时候再帮你看(
[!TIP] For best results, initiate chat on the files or code changes.
@seefs001: Understood! I'll be here to help with any questions or if additional review is needed. Looking forward to your review when you have time.
和强制格式化功能是否有重复呢
和强制格式化功能是否有重复呢
此 PR 主要的功能是将 reasoning 字段重命名为 reasoning_content,强制格式化没有这个功能。
我看强制格式化功能只针对 OpenAI 类型的上游渠道开启,所以才给 OpenRouter 类型的上游渠道单独加了一个转换功能。如果把这个功能并入已有的强制格式化功能,同时支持 OpenAI 和 OpenRouter 渠道,也不错。
你认为是单独给 OpenRouter 渠道加一个选项好,还是把思考字段转换功能并入强制格式化功能、同时支持 OpenAI 和 OpenRouter 渠道呢?
我觉得合并为一个选项更好
那我把代码再改改