Skip to content

fix(openai): recover from invalid reasoning signatures - #6392

Open
ygcaicn wants to merge 4 commits into
QuantumNous:mainfrom
ygcaicn:codex/fix-thinking-signature-invalid
Open

fix(openai): recover from invalid reasoning signatures#6392
ygcaicn wants to merge 4 commits into
QuantumNous:mainfrom
ygcaicn:codex/fix-thinking-signature-invalid

Conversation

@ygcaicn

@ygcaicn ygcaicn commented Jul 21, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

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

📝 变更描述 / Description

OpenAI Responses 在 store: false 场景会由客户端回传历史 reasoning item。生产请求中观察到上游返回 thinking_signature_invalid:历史 reasoning.encrypted_content 无法由当前上游验证,而普通重试会原样发送相同内容,因此无法恢复。

本改动仅对 OpenAI API 类型的 Responses 与 Responses Compact 生效,并且需要在渠道的“渠道额外设置”中主动开启“恢复无效推理签名”(默认关闭):

  • 首次收到 thinking_signature_invalid 时,用第一个非空 reasoning.encrypted_content 的 SHA-256 作为会话标识。
  • 写入带 24 小时滑动 TTL 的混合缓存;Redis 可用时使用 Redis,否则使用进程内 LRU,缓存中不保存原始加密内容。
  • 重试发送前删除所有 reasoning item 的 encrypted_content,并保证一次不受全局重试次数限制的恢复重试。
  • 如果恢复重试重新选到了另一个未开启开关的 OpenAI 渠道,本次已经启动的恢复仍会继续删除 encrypted_content,避免跨渠道 fallback 使恢复失效。
  • 后续独立请求仍按实际选中渠道的开关决定是否读取会话 cache;未开启的渠道不会被其他渠道学习到的 cache 长期影响。
  • 已经应用过降级的请求不会重复触发强制重试;其他错误、非 OpenAI 类型和非 Responses 请求保持原行为。
  • 开关复用渠道已有的 setting JSON,不新增数据库字段,也不需要数据库迁移。

实现和说明由 AI 辅助整理,合并前请维护者复核。

🚀 变更类型 / Type of change

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

🔗 关联任务 / Related Issue

  • 无公开 Issue;问题来自生产请求 dump,已确认上游错误码为 thinking_signature_invalid

✅ 提交前检查项 / Checklist

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

📸 运行证明 / Proof of Work

go test ./...
PASS

tsgo -b
PASS

oxlint -c .oxlintrc.json <changed files>
PASS

node scripts/format-with-protected-headers.mjs --check
PASS

rsbuild build
PASS

新增回归测试覆盖:开关关闭时不触发恢复、开关开启后的首次错误学习、强制恢复重试、跨渠道 fallback 后继续清理、后续独立请求继续尊重实际渠道开关、所有 reasoning 加密字段清理、后续缓存命中、首项哈希隔离,以及非 OpenAI/非 Responses 请求不触发降级。前端开关及其说明已同步到全部 7 个 locale。

Summary by CodeRabbit

  • New Features
    • Added an advanced channel setting to recover from invalid OpenAI “reasoning” signatures by removing reasoning.encrypted_content and retrying once.
  • Bug Fixes
    • Improved retry handling for thinking_signature_invalid, including a targeted retry-limit adjustment.
    • Ensures request forwarding preserves payloads when no removals occur, and updates the JSON body when removals are needed.
    • Adds conversation-aware caching to avoid repeated encrypted-content removals.
  • Tests
    • Added unit tests for fallback/retry behavior, caching, and non-matching scenarios.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a cached OpenAI reasoning fallback that removes encrypted reasoning content after an invalid signature, rebuilds sanitized pass-through requests, permits one immediate retry for Responses and ResponsesCompact modes, and exposes the behavior through channel settings.

Changes

OpenAI reasoning signature fallback

Layer / File(s) Summary
Reasoning fallback cache and error contract
types/error.go, service/openai_reasoning_fallback.go, service/openai_reasoning_fallback_test.go
Adds the thinking_signature_invalid error code, hybrid cache state, encrypted-content removal, one-time retry marking, and tests for cache and input-handling behavior.
Sanitized upstream request construction
relay/responses_handler.go
Prepares OpenAI reasoning input and reconstructs pass-through JSON bodies when encrypted reasoning fields are removed.
Relay retry orchestration
controller/relay.go, controller/relay_reasoning_retry_test.go
Adds mutable retry limits, detects eligible invalid-signature errors, activates the fallback retry, and validates supported and unsupported cases.
Channel setting and UI configuration
dto/channel_settings.go, web/src/features/channels/..., web/src/i18n/locales/*
Adds the fallback setting to channel data, form handling, advanced-setting state, the channel drawer, and translations.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OpenAI
  participant Relay
  participant ResponsesHelper
  participant ReasoningFallback
  OpenAI-->>Relay: thinking_signature_invalid
  Relay->>ReasoningFallback: mark invalid signature
  ReasoningFallback-->>Relay: enable one retry
  Relay->>ResponsesHelper: prepare retry request
  ResponsesHelper->>ReasoningFallback: remove encrypted reasoning content
  ReasoningFallback-->>ResponsesHelper: sanitized input
  ResponsesHelper->>OpenAI: retry request
Loading

Poem

I’m a rabbit with a retrying tune,
Snipping sealed thoughts beneath the moon.
A cache remembers, the relay hops,
One fresh attempt when the first one stops.
Clean JSON sails through the sky—
Hoppy code goes by! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 matches the main change: recovery from invalid OpenAI reasoning signatures.
✨ Finishing Touches
🧪 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.

@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: 1

🧹 Nitpick comments (1)
relay/responses_handler.go (1)

110-113: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove the redundant byte slice clone.

The sjson library functions, including sjson.SetRawBytes, do not mutate the input byte slice; they allocate and return a newly sized slice containing the modified JSON. Therefore, manually cloning jsonData beforehand is redundant and wastes memory, especially on large request payloads.

♻️ Proposed fix
-			// BodyStorage owns the returned bytes. Keep its original request body
-			// intact for later retries that may select a non-OpenAI API type.
-			jsonData = append([]byte(nil), jsonData...)
 			jsonData, err = sjson.SetRawBytes(jsonData, "input", request.Input)
🤖 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 `@relay/responses_handler.go` around lines 110 - 113, Remove the redundant
append-based byte slice clone in the response handling flow before
sjson.SetRawBytes, while keeping the SetRawBytes update to the "input" field
unchanged. Rely on SetRawBytes to return the newly allocated modified JSON slice
and preserve the existing error handling and retry behavior.
🤖 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 `@controller/relay.go`:
- Around line 235-243: Update the reasoningSignatureRetry branch in the relay
retry flow to increment retryLimit unconditionally whenever
shouldRetryOpenAIReasoningSignatureInvalid returns true. Remove the retry-count
condition while preserving the existing continue behavior and normal shouldRetry
path.

---

Nitpick comments:
In `@relay/responses_handler.go`:
- Around line 110-113: Remove the redundant append-based byte slice clone in the
response handling flow before sjson.SetRawBytes, while keeping the SetRawBytes
update to the "input" field unchanged. Rely on SetRawBytes to return the newly
allocated modified JSON slice and preserve the existing error handling and retry
behavior.
🪄 Autofix (Beta)

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

Run ID: 98ab15ce-c0f9-43fc-8168-5fa972612c69

📥 Commits

Reviewing files that changed from the base of the PR and between 1721144 and 050667e.

📒 Files selected for processing (6)
  • controller/relay.go
  • controller/relay_reasoning_retry_test.go
  • relay/responses_handler.go
  • service/openai_reasoning_fallback.go
  • service/openai_reasoning_fallback_test.go
  • types/error.go

Comment thread controller/relay.go

@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: 1

🤖 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 `@web/src/i18n/locales/fr.json`:
- Around line 5218-5219: Update the French translation for “Remove reasoning
encrypted_content and retry once when the upstream returns
thinking_signature_invalid” to explicitly preserve the nested field path
“reasoning.encrypted_content,” matching the actual field removed during
recovery.
🪄 Autofix (Beta)

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

Run ID: b0583bb5-1867-44ad-b75a-ce86e6bbc339

📥 Commits

Reviewing files that changed from the base of the PR and between 050667e and 30478c5.

📒 Files selected for processing (15)
  • controller/relay.go
  • controller/relay_reasoning_retry_test.go
  • dto/channel_settings.go
  • relay/responses_handler.go
  • web/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/src/features/channels/lib/channel-form-errors.ts
  • web/src/features/channels/lib/channel-form.ts
  • web/src/features/channels/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
🚧 Files skipped from review as they are similar to previous changes (3)
  • controller/relay_reasoning_retry_test.go
  • relay/responses_handler.go
  • controller/relay.go

Comment on lines +5218 to +5219
"Recover invalid reasoning signatures": "Récupérer les signatures de raisonnement invalides",
"Remove reasoning encrypted_content and retry once when the upstream returns thinking_signature_invalid": "Supprimer reasoning.encrypted_content et réessayer une fois lorsque le service en amont renvoie thinking_signature_invalid"

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the exact reasoning.encrypted_content field path.

The French description currently says reasoning encrypted_content, which is ambiguous and does not match the actual nested field removed during recovery.

Suggested wording
-    "Remove reasoning encrypted_content and retry once when the upstream returns thinking_signature_invalid": "Supprimer reasoning.encrypted_content et réessayer une fois lorsque le service en amont renvoie thinking_signature_invalid"
+    "Remove reasoning encrypted_content and retry once when the upstream returns thinking_signature_invalid": "Supprimer le champ reasoning.encrypted_content et réessayer une fois lorsque le service en amont renvoie thinking_signature_invalid"
📝 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
"Recover invalid reasoning signatures": "Récupérer les signatures de raisonnement invalides",
"Remove reasoning encrypted_content and retry once when the upstream returns thinking_signature_invalid": "Supprimer reasoning.encrypted_content et réessayer une fois lorsque le service en amont renvoie thinking_signature_invalid"
"Recover invalid reasoning signatures": "Récupérer les signatures de raisonnement invalides",
"Remove reasoning encrypted_content and retry once when the upstream returns thinking_signature_invalid": "Supprimer le champ reasoning.encrypted_content et réessayer une fois lorsque le service en amont renvoie thinking_signature_invalid"
🤖 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` around lines 5218 - 5219, Update the French
translation for “Remove reasoning encrypted_content and retry once when the
upstream returns thinking_signature_invalid” to explicitly preserve the nested
field path “reasoning.encrypted_content,” matching the actual field removed
during recovery.

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

🧹 Nitpick comments (3)
service/openai_reasoning_fallback_test.go (3)

29-29: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the exact SHA-256 conversation key.

The test only verifies that a hash exists, so regressions to the hash algorithm or input could pass. Compare the context key with the hex-encoded SHA-256 of "conversation-a-first" to protect the cache-key contract.

🤖 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 `@service/openai_reasoning_fallback_test.go` at line 29, Update the test around
PrepareOpenAIResponsesReasoningInput to assert that the conversation context key
exactly matches the hex-encoded SHA-256 digest of "conversation-a-first",
replacing the current presence-only assertion while preserving the existing
result checks.

66-66: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Cover an empty leading reasoning item.

The fixture never places an empty reasoning item before "conversation-b-first", so a regression that selects the first reasoning item instead of the first non-empty encrypted_content would pass. Add that case and retain the expected conversation-key behavior.

🤖 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 `@service/openai_reasoning_fallback_test.go` at line 66, Add a leading
reasoning item with empty encrypted_content to the fixture used by
PrepareOpenAIResponsesReasoningInput, followed by the existing non-empty
reasoning item before "conversation-b-first". Keep the assertion verifying that
the returned conversation key still corresponds to the first non-empty
encrypted_content.

93-97: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Add coverage for the Redis-backed cache path.

All tests in this file disable Redis, so the hybrid-cache contract can regress in the Redis get/set path without failing this suite. Add a deterministic Redis-backed or mock-backend test covering learning and subsequent cache hits.

🤖 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 `@service/openai_reasoning_fallback_test.go` around lines 93 - 97, The tests in
service/openai_reasoning_fallback_test.go only exercise the non-Redis cache
path. Add a deterministic test alongside the existing reasoning fallback tests
that keeps common.RedisEnabled enabled and uses a Redis-backed or mock backend
to verify the first request learns/stores the result and a subsequent identical
request is served from cache.
🤖 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.

Nitpick comments:
In `@service/openai_reasoning_fallback_test.go`:
- Line 29: Update the test around PrepareOpenAIResponsesReasoningInput to assert
that the conversation context key exactly matches the hex-encoded SHA-256 digest
of "conversation-a-first", replacing the current presence-only assertion while
preserving the existing result checks.
- Line 66: Add a leading reasoning item with empty encrypted_content to the
fixture used by PrepareOpenAIResponsesReasoningInput, followed by the existing
non-empty reasoning item before "conversation-b-first". Keep the assertion
verifying that the returned conversation key still corresponds to the first
non-empty encrypted_content.
- Around line 93-97: The tests in service/openai_reasoning_fallback_test.go only
exercise the non-Redis cache path. Add a deterministic test alongside the
existing reasoning fallback tests that keeps common.RedisEnabled enabled and
uses a Redis-backed or mock backend to verify the first request learns/stores
the result and a subsequent identical request is served from cache.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5a147631-9709-418e-8bf2-719b94901e78

📥 Commits

Reviewing files that changed from the base of the PR and between 30478c5 and cabc256.

📒 Files selected for processing (4)
  • controller/relay_reasoning_retry_test.go
  • relay/responses_handler.go
  • service/openai_reasoning_fallback.go
  • service/openai_reasoning_fallback_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • controller/relay_reasoning_retry_test.go
  • relay/responses_handler.go
  • service/openai_reasoning_fallback.go

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.

1 participant