Skip to content

fix: preserve reasoning_content across request - #6655

Draft
seefs001 wants to merge 1 commit into
QuantumNous:mainfrom
seefs001:feature/deeseek-reasoning-content-converter
Draft

fix: preserve reasoning_content across request#6655
seefs001 wants to merge 1 commit into
QuantumNous:mainfrom
seefs001:feature/deeseek-reasoning-content-converter

Conversation

@seefs001

@seefs001 seefs001 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)

针对部分模型启用reasoning_content的转换逻辑

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

(请在此粘贴截图、关键日志或测试报告,以证明变更生效)

Summary by CodeRabbit

  • New Features

    • Preserves assistant reasoning content when converting requests between supported AI protocols.
    • Supports reasoning alongside messages, tool calls, and responses.
    • Added configurable model matching to control which upstream models preserve reasoning content.
    • Added system settings UI, validation, defaults, and localized guidance for configuring supported models.
  • Bug Fixes

    • Prevents encrypted reasoning and unconfigured reasoning content from appearing in converted requests.
    • Avoids duplicate or spurious messages during reasoning conversion.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds configurable reasoning-content preservation. Upstream model matching enables the conversion option, which propagates through request converters for OpenAI, Claude, Gemini, and Responses formats. Tests cover enabled, disabled, and tool-call scenarios. Web settings expose the model patterns.

Changes

Reasoning preservation configuration

Layer / File(s) Summary
Configuration and metadata propagation
relay/common/relay_info.go, setting/model_setting/global.go, relaykit/relayconvert/..., web/src/features/system-settings/models/*, web/src/i18n/locales/*
Model patterns configure case-insensitive upstream matching. Relay metadata sets PreserveReasoningContent, and conversion registries forward it. The web interface exposes the setting and localized labels.
Chat protocol reasoning conversion
relaykit/relayconvert/internal/claude_messages/*, relaykit/relayconvert/internal/gemini_chat/*, relaykit/relayconvert/internal/oai_chat/*
Converters preserve reasoning as OpenAI ReasoningContent, Claude thinking blocks, Gemini thought parts, or Responses reasoning items when enabled.
Responses protocol reasoning conversion
relaykit/relayconvert/internal/oai_responses/*
Responses converters accumulate reasoning around assistant messages, tool calls, and tool results. They emit the content in Claude and Chat Completions representations.
Cross-protocol validation
relaykit/relayconvert/reasoning_request_test.go, service/convert_test.go
Tests verify configured upstream-model matching, cross-protocol conversion, tool-call preservation, and omission when disabled.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Poem

A rabbit traced the thoughts through every gate,
From Claude to Gemini, the paths align.
Tool calls kept their reasoning state,
While quiet flags leave content behind.
The model list now guides the way.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: preserving reasoning_content during request conversions.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@seefs001
seefs001 marked this pull request as draft August 4, 2026 15:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go (1)

346-363: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the "..." text sentinel with placeholder suppression at the source.

Lines 260-262 inject the "..." placeholder when content is empty. Lines 361-363 then remove that placeholder again by matching the literal text. This drops a genuine assistant text part whose value is exactly "...". Skip the placeholder injection when reasoning content exists, then the sentinel match is unnecessary.

♻️ Proposed change at the placeholder source (lines 260-262)
-		if fmtMessage.Content == nil || (fmtMessage.IsStringContent() && fmtMessage.StringContent() == "") {
+		if fmtMessage.GetReasoningContent() == "" &&
+			(fmtMessage.Content == nil || (fmtMessage.IsStringContent() && fmtMessage.StringContent() == "")) {
 			fmtMessage.SetStringContent("...")
 		}

Then remove the sentinel check:

 			for _, mediaMessage := range message.ParseContent() {
 				switch mediaMessage.Type {
 				case "text":
-					if reasoningEmitted && mediaMessage.Text == "..." {
-						continue
-					}
 					if mediaMessage.Text != "" {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go` around
lines 346 - 363, The `"..."` placeholder is being injected unconditionally at
the source (lines 260-262) when content is empty, and then removed by matching
the literal text in the loop that processes mediaMessage items (the
reasoningEmitted && mediaMessage.Text == "..." check). This approach incorrectly
drops genuine assistant text parts with that exact value. Modify the placeholder
injection at the source to skip injection when reasoning content has been
emitted, then remove the sentinel-matching check in the loop around
mediaMessage.Text == "..." so the condition is no longer needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go`:
- Around line 268-279: Update attachPendingReasoningToLastAssistant so the
synthesized assistant dto.Message explicitly sets Content to an empty string
alongside Role and ReasoningContent. Leave the existing behavior unchanged for
pending reasoning and existing assistant messages.

In `@web/src/i18n/locales/fr.json`:
- Line 3661: Update the French translation value for the "Reasoning Content
Models" key in the fr.json file to fully translate "Reasoning Content" into
French instead of leaving it in English. Replace "Reasoning Content" with the
established French terminology used in the surrounding translation entries to
create a complete French-language translation.

---

Nitpick comments:
In `@relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go`:
- Around line 346-363: The `"..."` placeholder is being injected unconditionally
at the source (lines 260-262) when content is empty, and then removed by
matching the literal text in the loop that processes mediaMessage items (the
reasoningEmitted && mediaMessage.Text == "..." check). This approach incorrectly
drops genuine assistant text parts with that exact value. Modify the placeholder
injection at the source to skip injection when reasoning content has been
emitted, then remove the sentinel-matching check in the loop around
mediaMessage.Text == "..." so the condition is no longer needed.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d5933ae9-2cff-47f0-9012-bb3103933abd

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab0202 and 259d0fb.

📒 Files selected for processing (29)
  • relay/common/relay_info.go
  • relaykit/relayconvert/convmeta/options.go
  • relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go
  • relaykit/relayconvert/internal/gemini_chat/to_oai_chat_req.go
  • relaykit/relayconvert/internal/oai_chat/to_claude_messages_req.go
  • relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go
  • relaykit/relayconvert/internal/oai_chat/to_oai_responses_req_test.go
  • relaykit/relayconvert/internal/oai_responses/to_claude_messages_req.go
  • relaykit/relayconvert/internal/oai_responses/to_gemini_chat_req.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go
  • relaykit/relayconvert/internal/oai_responses/to_oai_chat_req_test.go
  • relaykit/relayconvert/reasoning_request_test.go
  • relaykit/relayconvert/request_compat.go
  • relaykit/relayconvert/request_registry.go
  • service/convert_test.go
  • setting/model_setting/global.go
  • web/src/features/models/components/drawers/model-mutate-drawer.tsx
  • web/src/features/system-settings/models/global-settings-card.tsx
  • web/src/features/system-settings/models/index.tsx
  • web/src/features/system-settings/models/section-registry.tsx
  • web/src/features/system-settings/types.ts
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/i18n/locales/zh.json

Comment on lines +268 to +279
func attachPendingReasoningToLastAssistant(messages []dto.Message, pending *string) []dto.Message {
if pending == nil || *pending == "" {
return messages
}
reasoning := *pending
*pending = ""
if len(messages) == 0 || messages[len(messages)-1].Role != "assistant" {
return append(messages, dto.Message{Role: "assistant", ReasoningContent: &reasoning})
}
appendReasoningContent(&messages[len(messages)-1], reasoning)
return messages
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm Content has no omitempty and inspect sibling assistant-message construction.
rg -nP -C2 'Content\s+any\s+`json:"content' relaykit/dto
rg -nP -C3 'dto\.Message\{Role: "assistant"' relaykit

Repository: QuantumNous/new-api

Length of output: 6119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go 2>/dev/null || true

echo "== target function and nearby converter logic =="
sed -n '240,290p' relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go
sed -n '380,415p' relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go

echo "== Message types =="
sed -n '290,315p' relaykit/dto/openai_request.go

echo "== Go JSON null behavior probe (read-only temp file only) =="
python3 - <<'PY'
import json, tempfile, pathlib
import subprocess
code = '''package main
import (
 "encoding/json"
 "fmt"
)
type Message struct {
 Role             string `"json:"role"`
 Content          any    `"json:"content"`
 ReasoningContent *string `"json:"reasoning_content,omitempty"`
}
func main() {
 pending := "think"
 msg := Message{Role: "assistant", ReasoningContent: &pending}
 b, _ := json.Marshal(msg)
 fmt.Printf("%s\n", b)
}
'''
with tempfile.NamedTemporaryFile(suffix='.go', delete=False) as f:
    f.write(code.encode())
    path=f.name
try:
    cmd = subprocess.run(["go", "run", path], text=True, capture_output=True, timeout=15)
    print("stdout:", cmd.stdout)
    print("stderr:", cmd.stderr)
    print("rc:", cmd.returncode)
    print("parsed:", json.loads(cmd.stdout.strip()))
finally:
    pathlib.Path(path).unlink(missing_ok=True)
PY

Repository: QuantumNous/new-api

Length of output: 7694


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== helper invocations around responsesRequestMessagesToChat =="
sed -n '128,242p' relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go

echo "== corrected Go null-content probe =="
tmp="$(mktemp -d)"
cat > "$tmp/probe.go" <<'GO'
package main
import (
	"encoding/json"
	"fmt"
)
type Message struct {
	Role             string `json:"role"`
	Content          any    `json:"content"`
	ReasoningContent *string `json:"reasoning_content,omitempty"`
}
func main() {
	pending := "think"
	msg := Message{Role: "assistant", ReasoningContent: &pending}
	b, _ := json.Marshal(msg)
	var v map[string]any
	_ = json.Unmarshal(b, &v)
	fmt.Printf("%s\n", b)
	fmt.Printf("content=%v contentIsNull=%t\n", v["content"], v["content"] == nil)
}
GO
go run "$tmp/probe.go"
rm -rf "$tmp"

Repository: QuantumNous/new-api

Length of output: 4297


Set an explicit content value on the synthesized assistant message.

When pending reasoning remains after an array input item and no assistant message is present, attachPendingReasoningToLastAssistant appends dto.Message{Role: "assistant", ReasoningContent: &reasoning}. dto.Message.Content has no omitempty, so that marshals as "content":null. Set Content: "" on this synthesized message so it does not include null assistant content on the Chat Completions request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relaykit/relayconvert/internal/oai_responses/to_oai_chat_req.go` around lines
268 - 279, Update attachPendingReasoningToLastAssistant so the synthesized
assistant dto.Message explicitly sets Content to an empty string alongside Role
and ReasoningContent. Leave the existing behavior unchanged for pending
reasoning and existing assistant messages.

"Reason": "Raison",
"Reason:": "Raison :",
"Reasoning": "Raisonnement",
"Reasoning Content Models": "Modèles avec Reasoning Content",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Translate the feature label into French.

"Modèles avec Reasoning Content" leaves the user-facing feature name in English. Use the established French terminology from the surrounding entries.

Proposed fix
-    "Reasoning Content Models": "Modèles avec Reasoning Content",
+    "Reasoning Content Models": "Modèles avec contenu de raisonnement",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Reasoning Content Models": "Modèles avec Reasoning Content",
"Reasoning Content Models": "Modèles avec contenu de raisonnement",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/i18n/locales/fr.json` at line 3661, Update the French translation
value for the "Reasoning Content Models" key in the fr.json file to fully
translate "Reasoning Content" into French instead of leaving it in English.
Replace "Reasoning Content" with the established French terminology used in the
surrounding translation entries to create a complete French-language
translation.

@AmPlace

AmPlace commented Aug 5, 2026

Copy link
Copy Markdown

感谢你按模型名白名单实现 reasoning 保留方案。

在合并前,希望能准确保留 #6396 / #6395@AmPlace 的原始贡献记录:Responses→Chat 的问题定位、最小复现、reasoning 缓冲与 tool call 关联实现,以及最初的回归测试,均首先在 #6396 / #6395 中提交。#6593 后续也明确说明其该方向的实现思路与测试来源于 #6395

如果最终方案会重写或整合现有实现,烦请在 PR 描述和最终合并记录中明确引用 #6395 / @AmPlace,并保留相关提交,或添加适当的 Co-authored-by

另外,当前实现中,模型匹配未启用时会静默跳过不支持的 reasoning item,而不是返回明确的 unsupported conversion error。请确认这一行为是有意设计的。

kk66615 pushed a commit to kk66615/new-api that referenced this pull request Aug 22, 2026
上游 253a74d(QuantumNous#6654) 与 7d09c69(QuantumNous#6861) 新增的测试用例调用
ResponsesRequestToChatCompletionsRequest / ChatCompletionsRequestToResponsesRequest
时使用的是旧签名,而本地 PR QuantumNous#6655 补丁已为这两个函数引入
convmeta.Meta 首参(reasoning_content 跨转换保留的核心机制)。
合并后函数定义取本地版本、测试取上游版本,导致 relaykit 模块
vet 失败。为 5 处新增用例补上 nil meta。

受影响用例:
- to_oai_chat_req_test.go: penalty 转换 2 处
- to_oai_responses_req_test.go: prompt_cache_key 2 处 / penalty 1 处

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AZbTUCaSkfaTLkrfzKGSP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants