Model switch mid-conversation fails: "encrypted content could not be decrypted"
What version of Codex CLI is running?
0.119.0
What subscription do you have?
azure
Which model were you using?
gpt-5.4-mini, gpt-5.4
What platform is your computer?
linux x64
What terminal emulator and version are you using (if applicable)?
_No response_
What issue are you seeing?
When switching models mid-conversation (e.g. via turn/start with a different model in app-server mode), the Responses API returns invalid_encrypted_content because encrypted_content from the previous model's Reasoning and Compaction items is sent verbatim to the new model, which cannot decrypt it.
What steps can reproduce the bug?
- Start a conversation using Model A (e.g. an Azure-hosted model that supports reasoning)
- Have a multi-turn conversation so that the history contains
Reasoningitems withencrypted_content - Switch to Model B (e.g.
gpt-5.4on a different provider/deployment) mid-conversation viaturn/startwith the new model name - Send a new user message in the same thread
What is the expected behavior?
The conversation should continue normally with Model B. Previous reasoning context should be handled gracefully (e.g. encrypted_content stripped, or summarized).
Additional information
Actual Behavior
The API returns an error:
litellm.ContentPolicyViolationError: AzureException - {
"error": {
"message": "The encrypted content gAAA...S6Q= could not be verified.
Reason: Encrypted content could not be decrypted or parsed.",
"type": "invalid_request_error",
"param": null,
"code": "invalid_encrypted_content"
}
}
Root Cause Analysis
The encrypted_content field in ResponseItem::Reasoning and ResponseItem::Compaction is provider-specific encrypted data. It is designed so that the same model/provider can "recall" its reasoning without re-computing it. However, a different model/provider does not have the keys to decrypt it.
The issue is that the conversation history is sent to the new model without sanitizing these fields:
- History construction (
core/src/codex.rs:6257-6261):sess.clone_history().await.for_prompt(...)returns all items includingReasoningwithencrypted_content.
for_prompt()/normalize_history()(core/src/context_manager/history.rs:120-125): Only strips images for text-only models and removes orphaned outputs. Does not stripencrypted_content.
- Request construction (
core/src/client.rs:829-868):get_formatted_input()passes all history items (withencrypted_content) into theResponsesApiRequest.input.
- The new model receives
encrypted_contentit cannot decrypt and returns an error.
Existing precedent for content stripping on model switch
The codebase already handles similar incompatibility when switching from an image-capable model to a text-only model — images are stripped and replaced with placeholder text (tested in model_change_from_image_to_text_strips_prior_image_content in core/tests/suite/model_switching.rs). The same pattern should apply to encrypted_content.
Affected Code Locations
| File | Location | Description |
|------|----------|-------------|
| codex-rs/protocol/src/models.rs:206-216 | ResponseItem::Reasoning | encrypted_content: Option<String> — provider-specific |
| codex-rs/protocol/src/models.rs:336-338 | ResponseItem::Compaction | encrypted_content: String — also provider-specific |
| codex-rs/core/src/context_manager/history.rs:120-125 | for_prompt() | Returns items without filtering encrypted_content |
| codex-rs/core/src/context_manager/history.rs:364-373 | normalize_history() | Does not handle encrypted_content |
| codex-rs/core/src/client.rs:829-868 | build_responses_request() | Includes all history items in input and adds include: ["reasoning.encrypted_content"] |
Suggested Fix
When a model switch is detected (the new model slug differs from the model that produced the history items), strip encrypted_content from prior Reasoning and Compaction items before sending them to the new model. This could be done in one of:
normalize_history()inhistory.rs— similar to how images are stripped for text-only modelsfor_prompt()inhistory.rs— as an additional filtering stepbuild_responses_request()inclient.rs— as a last-resort sanitization before the API call
The Reasoning item's summary field should be preserved so that the new model still has context about what the previous model was thinking.
Workaround
Create a new thread (thread/create) when switching models instead of switching within the same thread via turn/start. This avoids carrying over encrypted_content from the previous model.
Environment
- Using Codex in app-server mode
- Model switching via
turn/startwith a differentmodelparameter - Backend: litellm proxy routing to Azure OpenAI deployments
6 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
If you haven't already done so, please report this issue to Azure.
This is related to but distinct from #15219, which reports the same
invalid_encrypted_contenterror in a conversation resume scenario:| | #15219 (resume) | This issue (mid-conversation switch) |
|---|---|---|
| Trigger | Resuming a saved conversation when the current profile uses a different provider | Actively switching models within the same thread via
turn/start|| Root cause |
merge_persisted_resume_metadata()fails to restoremodel_provider, so history is sent to the wrong endpoint | History containsencrypted_contentfrom Model A, which is sent to Model B that cannot decrypt it || Fix direction | Restore
model_providerduring resume so the original provider is used | Stripencrypted_contentfrom history when the model/provider changes |#15219 can be fixed by routing back to the correct provider. This issue cannot — because the user intentionally switches to a different provider, so the old
encrypted_contentmust be cleaned from the history rather than routed to the original provider.I think this might be an issue of codex itself. Because when I use opencode and copilot-cli(byok mode),I can switch among different models during the conversation without any problem. And I dug into opencode's source code, and found that they have dedicated logical to clear the encryped_content when model switch happens on gpt-* series model.
I ran into this when moving an existing Codex session between Sakana Fugu and regular OpenAI GPT models. I published the small workaround I have been using here:
https://github.com/safrano9999/codex-session-cleaner
It removes only incompatible provider-specific encrypted reasoning records from the local rollout JSONL while preserving the visible conversation, tool records, session ID, file name, permissions, and timestamp. The same session can then be resumed with GPT instead of failing on encrypted content from the previous provider.
In my setup this has worked very well for switching back and forth between Sakana Fugu and regular GPT when either provider reaches its rate limit. I run the cleaner before moving a Fugu-written session back to GPT:
codex-session-cleaner last
or pass the exact session UUID.
This is an unofficial and deliberately narrow workaround: it discards hidden incompatible reasoning state rather than decrypting or translating it. Stop Codex for the affected session and back up the JSONL file before using it.
@safrano9999 thanks so much for making that and posting it! I'd switched from OpenRouter to Bedrock Mantle now that they just started supporting GPT-5.6 and then it stopped working suddenly and I tried to switch bad to OpenRouter and started getting the error. This took care of it and could continue with the session.