Skip to content

fix: sanitize reasoning items before Responses API replay - #9730

Open
Roovelrz wants to merge 1 commit into
AstrBotDevs:masterfrom
Roovelrz:fix/9724-reasoning-replay
Open

fix: sanitize reasoning items before Responses API replay#9730
Roovelrz wants to merge 1 commit into
AstrBotDevs:masterfrom
Roovelrz:fix/9724-reasoning-replay

Conversation

@Roovelrz

@Roovelrz Roovelrz commented Aug 18, 2026

Copy link
Copy Markdown

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 → GPT reproduces 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:

Responses API output
    ↓
serialize complete reasoning item
    ↓
store it in AstrBot conversation history
    ↓
restore the complete item
    ↓
append it directly to the next Responses API input

During response parsing, reasoning items are serialized with model_dump(..., exclude_none=True). This removes only fields whose value is None, so fields such as status: completed remain 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:

type
id
summary
content
encrypted_content

Fields outside this replay-safe set, including status and unknown provider-specific metadata, are not forwarded to the next provider.

The resulting flow becomes:

stored reasoning state
    ↓
restore reasoning item
    ↓
filter to replay-safe fields
    ↓
append normalized item to Responses API input

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_response stores 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:

if provider is DeepSeek

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 status

The reported error currently references:

input[N].status

However, status is 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 status and an unknown provider-specific field to verify this behavior.

Additional reproduction context

Thanks for the reports by @C10H14N2O5:

GPT
→ direct DeepSeek Responses API
→ GPT

consistently reproduces the incompatible reasoning replay error.

However:

GPT through CLIProxyAPI
→ DeepSeek through CLIProxyAPI
→ GPT through CLIProxyAPI

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 status and 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:

uv run pytest tests/test_openai_responses_source.py -q

Run Ruff validation:

uv run ruff format .
uv run ruff check .

Recommended project validation:

make pr-test-neo

Regression coverage

The regression test constructs a previously stored reasoning item containing:

id
type
summary
content
encrypted_content
status
provider_specific

After converting the stored conversation back into Responses API input, the test verifies that:

id
type
summary
content
encrypted_content

are preserved, while:

status
provider_specific

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

  • My changes have been well-tested, and verification steps are provided above.
  • A regression test covers the reported reasoning replay incompatibility.
  • No new dependencies are introduced.
  • This change does not introduce provider-specific branching.
  • Existing serialized conversation state remains readable.
  • My changes do not introduce malicious code.

Summary by Sourcery

Normalize replayed reasoning items to improve cross-provider Responses API compatibility without requiring conversation migration.

Bug Fixes:

  • Prevent incompatible or provider-specific reasoning metadata from being replayed across OpenAI-compatible Responses API providers.

Enhancements:

  • Normalize restored reasoning items to a replay-safe set of fields while preserving the existing serialized conversation format.

Tests:

  • Add regression coverage confirming that nonportable reasoning fields are excluded when stored conversation history is converted into Responses API input.

@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. labels Aug 18, 2026

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

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_KEYS explaining why only this allowlist is replayed (cross-provider compatibility boundary), so future changes don’t accidentally reintroduce non-portable fields like status.
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`.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 同一会话在不同 Responses API 模型间切换后,历史 reasoning 数据可能导致后续模型请求 400

1 participant