fix: sanitize reasoning items before Responses API replay - #9730
Open
Roovelrz wants to merge 1 commit into
Open
Conversation
Contributor
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- Consider extracting the reasoning-item sanitization dict comprehension into a small helper (e.g.,
_sanitize_reasoning_item) so the replay-safe allowlist logic is easier to understand, reuse, and unit-test independently if needed. - It may be helpful to add a brief inline comment near
_REASONING_REPLAY_SAFE_KEYSexplaining why only this allowlist is replayed (cross-provider compatibility boundary), so future changes don’t accidentally reintroduce non-portable fields likestatus.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider extracting the reasoning-item sanitization dict comprehension into a small helper (e.g., `_sanitize_reasoning_item`) so the replay-safe allowlist logic is easier to understand, reuse, and unit-test independently if needed.
- It may be helpful to add a brief inline comment near `_REASONING_REPLAY_SAFE_KEYS` explaining why only this allowlist is replayed (cross-provider compatibility boundary), so future changes don’t accidentally reintroduce non-portable fields like `status`.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #9724
Summary
This PR fixes a cross-provider compatibility issue when replaying serialized reasoning items in the OpenAI-compatible Responses API provider.
AstrBot currently stores complete reasoning output items in conversation history and restores them directly into subsequent Responses API input. When a conversation switches between different Responses-compatible providers, provider-specific or output-only metadata may therefore be replayed to another provider that does not accept the same schema.
The reported case is triggered by a reasoning item containing
status: completed, which is accepted in the originating provider response but rejected when replayed to another Responses-compatible upstream.Thanks @C10H14N2O5 for the detailed investigation and the additional reproduction result. The observation that
GPT → direct DeepSeek Responses → GPTreproduces the issue, while routing both providers through CLIProxyAPI avoids it, strongly supports treating this as a cross-provider reasoning replay normalization issue rather than a DeepSeek-specific request bug. The CLIProxyAPI behavior also suggests that normalizing the payload before replay is an effective compatibility boundary.Root Cause
The current reasoning state flow is:
During response parsing, reasoning items are serialized with
model_dump(..., exclude_none=True). This removes only fields whose value isNone, so fields such asstatus: completedremain in the stored reasoning state.When the conversation history is later reconstructed, AstrBot restores the saved dictionary without filtering and appends the complete reasoning item to
response_input.This works as long as both providers tolerate the same reasoning item schema. It can fail when a reasoning item generated by one OpenAI-compatible Responses implementation contains fields that another implementation does not accept as input.
Approach
This PR normalizes restored reasoning items at the replay boundary.
Before a stored reasoning item is appended to the next Responses API request, only the fields needed for reasoning replay are retained:
Fields outside this replay-safe set, including
statusand unknown provider-specific metadata, are not forwarded to the next provider.The resulting flow becomes:
The stored reasoning state format itself remains unchanged.
Why filter during replay instead of serialization
The filtering is intentionally performed when restoring the reasoning state rather than only when
_parse_responsestores it.If filtering were applied only during serialization, conversations that already contain reasoning items with incompatible fields would remain broken after users upgrade AstrBot.
Filtering at replay time also sanitizes existing stored conversations, so no conversation migration or manual context reset is required.
It additionally provides a single compatibility boundary regardless of which Responses-compatible provider originally generated the reasoning item.
Why this is not a DeepSeek-specific workaround
The observed failure involves a reasoning item produced through one provider and replayed to another provider, but the underlying problem is not specific to DeepSeek.
Different OpenAI-compatible Responses implementations may expose slightly different output schemas or tolerate different subsets of fields when those items are later reused as input.
Adding logic such as:
would only address the currently observed provider combination and would leave the same replay path vulnerable to future provider-specific fields.
Normalizing reasoning items at the common replay boundary addresses the compatibility problem independently of the originating and target providers.
Why not only remove
statusThe reported error currently references:
However,
statusis only the field that exposed the problem in this reproduction.The current implementation replays the complete stored dictionary, so any additional provider-specific field could cause the same class of failure when switching to another Responses-compatible provider.
For that reason, this PR uses a replay-safe allowlist rather than removing only the currently failing field.
The regression test also includes both
statusand an unknown provider-specific field to verify this behavior.Additional reproduction context
Thanks for the reports by @C10H14N2O5:
consistently reproduces the incompatible reasoning replay error.
However:
does not reproduce it.
This indicates that CLIProxyAPI likely performs some payload normalization before forwarding Responses input. This behavior is consistent with the normalization performed by this PR, while keeping the fix inside AstrBot so users do not need to depend on a specific proxy configuration.
Modifications
Normalize restored reasoning items before adding them to Responses API input.
Preserve only replay-safe reasoning fields.
Prevent
statusand unknown provider-specific metadata from being replayed across providers.Keep the existing serialized reasoning state format unchanged.
Preserve compatibility with reasoning state already stored in existing conversations.
Add a regression test covering incompatible reasoning metadata during replay.
This is NOT a breaking change.
Verification
Run the Responses provider tests:
Run Ruff validation:
Recommended project validation:
Regression coverage
The regression test constructs a previously stored reasoning item containing:
After converting the stored conversation back into Responses API input, the test verifies that:
are preserved, while:
are not replayed.
This verifies both the concrete failure reported in #9724 and the broader cross-provider compatibility case.
Expected behavior after the fix
A user should be able to switch between OpenAI-compatible Responses providers within the same conversation without incompatible reasoning output metadata being blindly forwarded to the next provider.
Existing conversations that already contain such reasoning metadata should also recover without requiring the user to clear the conversation context.
Checklist
Summary by Sourcery
Normalize replayed reasoning items to improve cross-provider Responses API compatibility without requiring conversation migration.
Bug Fixes:
Enhancements:
Tests: