Model switch mid-conversation fails: "encrypted content could not be decrypted"

Open 💬 6 comments Opened Apr 12, 2026 by caibirdme
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

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?

  1. Start a conversation using Model A (e.g. an Azure-hosted model that supports reasoning)
  2. Have a multi-turn conversation so that the history contains Reasoning items with encrypted_content
  3. Switch to Model B (e.g. gpt-5.4 on a different provider/deployment) mid-conversation via turn/start with the new model name
  4. 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:

  1. History construction (core/src/codex.rs:6257-6261): sess.clone_history().await.for_prompt(...) returns all items including Reasoning with encrypted_content.
  1. 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 strip encrypted_content.
  1. Request construction (core/src/client.rs:829-868): get_formatted_input() passes all history items (with encrypted_content) into the ResponsesApiRequest.input.
  1. The new model receives encrypted_content it 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() in history.rs — similar to how images are stripped for text-only models
  • for_prompt() in history.rs — as an additional filtering step
  • build_responses_request() in client.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/start with a different model parameter
  • Backend: litellm proxy routing to Azure OpenAI deployments

View original on GitHub ↗

6 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #15219

Powered by Codex Action

etraut-openai contributor · 3 months ago

If you haven't already done so, please report this issue to Azure.

caibirdme · 3 months ago

This is related to but distinct from #15219, which reports the same invalid_encrypted_content error 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 restore model_provider, so history is sent to the wrong endpoint | History contains encrypted_content from Model A, which is sent to Model B that cannot decrypt it |
| Fix direction | Restore model_provider during resume so the original provider is used | Strip encrypted_content from 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_content must be cleaned from the history rather than routed to the original provider.

caibirdme · 3 months ago
If you haven't already done so, please report this issue to Azure.

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.

safrano9999 · 23 days ago

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.

ShawnFumo · 4 days ago

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