new-api icon indicating copy to clipboard operation
new-api copied to clipboard

OpenAI 渠道“强制格式化”选项将 `reasoning` 字段重命名为 `reasoning_content` 并为 OpenRouter 上游渠道开启

Open ZhaoZuohong opened this issue 2 weeks ago • 15 comments

修改说明

OpenRouter 虽然与 OpenAI 兼容格式使用相同的 /v1/chat/completions 端点,然而其响应格式与一般的 OpenAI 兼容格式有些不同。除 OpenRouter 额外添加的字段外,一个明显的区别是:思考内容在 reasoning 字段而非常见的 reasoning_content 字段中。有些客户端(如 Cline)在添加“兼容 OpenAI”格式的客户端时,只处理 reasoning_content 字段、不处理 OpenRouter 的 reasoning 字段。本 PR 为 OpenRouter 渠道上游添加“转换为 OpenAI 兼容格式”的选项,允许将响应格式(尤其是思考字段)转换为 OpenAI 格式,以适配更多客户端。

image

本 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.

ZhaoZuohong avatar Nov 23 '25 08:11 ZhaoZuohong

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 reasoningreasoning_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 reasoningreasoning_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_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions: | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot] avatar Nov 23 '25 08:11 coderabbitai[bot]

私认为直接强制更改不提供选项不是一个妥当的做法,至少需要给用户选择权,有些人会使用 https://ai-sdk.dev/providers/community-providers/openrouter 之类的Provider,如果按你这样强制改的话他们就没办法获取到预期的Reasoning,而且因为需要使用OpenRouter特定的一些字段所以也不能换Provider,算是破坏性更新。

seefs001 avatar Nov 23 '25 11:11 seefs001

需要使用 OpenRouter 特定的一些字段……算是破坏性更新。

是的。我想问题的根源还是在 OpenRouter 与 OpenAI 共用 /v1/chat/completions,导致区分不开,只能二选一。

使用 community-providers/openrouter 之类的Provider……

这种情况下,用户应该是希望获得密钥聚合的功能。还需要考虑把其它格式的上游也转换成 OpenRouter 格式吗?

需要给用户选择权

你认为在 OpenRouter 类型上游的“渠道额外设置”中添加一个默认关闭的选项怎么样?能够保持与现有行为的兼容,开启后也能够完成对字段的转换。

ZhaoZuohong avatar Nov 23 '25 13:11 ZhaoZuohong

需要使用 OpenRouter 特定的一些字段……算是破坏性更新。

是的。我想问题的根源还是在 OpenRouter 与 OpenAI 共用 /v1/chat/completions,导致区分不开,只能二选一。

使用 community-providers/openrouter 之类的Provider……

这种情况下,用户应该是希望获得密钥聚合的功能。还需要考虑把其它格式的上游也转换成 OpenRouter 格式吗?

需要给用户选择权

你认为在 OpenRouter 类型上游的“渠道额外设置”中添加一个默认关闭的选项怎么样?能够保持与现有行为的兼容,开启后也能够完成对字段的转换。

我认为加个选项就够了,默认不进行转换操作。 不用考虑其他格式转Openrouter吧,这个比较小众,另外也不一定会是需要密钥聚合的用户,可能只是想给自己的帐号套一层代理分给别人用。

seefs001 avatar Nov 23 '25 13:11 seefs001

好的,那我去给 OpenRouter 类型的上游加一个选项。

ZhaoZuohong avatar Nov 23 '25 13:11 ZhaoZuohong

@seefs001 我觉得基本没问题了,麻烦你看看?

ZhaoZuohong avatar Nov 24 '25 03:11 ZhaoZuohong

@coderabbitai generate docstrings

ZhaoZuohong avatar Nov 24 '25 04:11 ZhaoZuohong

✅ Actions performed

Initiated docstring generation; will generate only if new commits exist.

coderabbitai[bot] avatar Nov 24 '25 04:11 coderabbitai[bot]

[!NOTE] Docstrings generation - SUCCESS Generated docstrings for this pull request at https://github.com/QuantumNous/new-api/pull/2285

coderabbitai[bot] avatar Nov 24 '25 04:11 coderabbitai[bot]

@coderabbitai generate docstrings

不用找它给你生成docs,看他给你提的意见修就行,我晚点时候再帮你看(

seefs001 avatar Nov 24 '25 06:11 seefs001

[!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.

coderabbitai[bot] avatar Nov 24 '25 06:11 coderabbitai[bot]

和强制格式化功能是否有重复呢

Calcium-Ion avatar Nov 26 '25 10:11 Calcium-Ion

和强制格式化功能是否有重复呢

此 PR 主要的功能是将 reasoning 字段重命名为 reasoning_content,强制格式化没有这个功能。

我看强制格式化功能只针对 OpenAI 类型的上游渠道开启,所以才给 OpenRouter 类型的上游渠道单独加了一个转换功能。如果把这个功能并入已有的强制格式化功能,同时支持 OpenAI 和 OpenRouter 渠道,也不错。

你认为是单独给 OpenRouter 渠道加一个选项好,还是把思考字段转换功能并入强制格式化功能、同时支持 OpenAI 和 OpenRouter 渠道呢?

ZhaoZuohong avatar Nov 26 '25 10:11 ZhaoZuohong

我觉得合并为一个选项更好

Calcium-Ion avatar Nov 26 '25 12:11 Calcium-Ion

那我把代码再改改

ZhaoZuohong avatar Nov 26 '25 12:11 ZhaoZuohong