Custom base instructions are duplicated inside <model_switch> after switching models on a later turn
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
What issue are you seeing?
When a thread uses custom base instructions, switching models after at least one completed turn duplicates those instructions inside a developer <model_switch> message.
The same custom instructions remain present in the top-level Responses API instructions field, so the second request contains the instructions twice and under two different roles.
Observed request shape:
instructions:
custom base instructions
<model_switch>
The user was previously using a different model. Please continue the conversation according to the following instructions:
custom base instructions
</model_switch>
Environment:
openai/codex commit: 5fc7840cf6d085a7a7b3438d69a2beb934a2a5f4
platform: macOS 26.3.2 arm64 (Darwin 25.3.0)
rustc: 1.94.0
I reproduced this with a request-level codex-core integration test on the latest origin/main. The local nextest profile retried the failure once, and the duplicated custom instructions were observed 2/2 times.
What steps can reproduce the bug?
- Start a thread with a custom
config.base_instructionsvalue. - Complete one turn using model A.
- Switch the thread to model B and complete another turn.
- Inspect the second Responses API request.
- Verify that the custom instructions remain in the top-level
instructionsfield. - Inspect developer input for a
<model_switch>message.
I added a local integration test named:
model_change_does_not_wrap_custom_base_instructions
The essential sequence is:
let custom_base_instructions = "custom base instructions";
let mut builder = test_codex()
.with_model("gpt-5.2")
.with_config(move |config| {
config.base_instructions = Some(custom_base_instructions.to_string());
});
let test = builder.build_with_auto_env(&server).await?;
submit_model_turn(
&test.codex,
"gpt-5.2",
ThreadSettingsOverrides::default(),
)
.await?;
submit_model_turn(
&test.codex,
"gpt-5.4",
ThreadSettingsOverrides::default(),
)
.await?;
I ran:
just test -p codex-core model_change_does_not_wrap_custom_base_instructions
The top-level instructions assertion passes, but the assertion that no model-switch developer message exists fails with:
[
"<model_switch>
The user was previously using a different model. Please continue the conversation according to the following instructions:
custom base instructions
</model_switch>",
]
What is the expected behavior?
Custom base instructions should remain only in the top-level Responses API instructions field when the model changes.
No <model_switch> developer message should be generated when the effective base instructions have BaseInstructionsProvenance::Custom.
Only model-derived instructions should be eligible for inclusion in <model_switch>. Existing behavior for BaseInstructionsProvenance::Model { .. } should remain unchanged so that a model change can still inject the target model's native instructions.
Additional information
Root-cause hypothesis
This is a code-path hypothesis rather than a maintainer-confirmed root cause:
Config::to_models_manager_config()preserves custom base instructions as an explicit model-info override while filtering only model-derived instructions.with_config_overrides()writes that custom value into the effectivemodel_messages.instructions_template.Session::build_world_state_for_step()reads model instructions from that effectiveModelInfo.- After a completed turn,
previous_turn_settings()supplies the previous model before the provenance fallback is evaluated. ModelInstructionsStatereceives the current model, previous model, and effective instructions, but not base-instruction provenance.- Its diff renderer emits
<model_switch>whenever the model changed and the effective instructions are non-empty.
This means the first-turn custom-instruction path is protected by provenance inference, but later turns bypass that protection once previous_turn_settings exists.
Possible direction
A focused direction would be to preserve model identity in the world-state snapshot while gating the model-switch instruction fragment on base-instruction provenance:
BaseInstructionsProvenance::Model { .. }: allow<model_switch>generation.BaseInstructionsProvenance::Custom: do not generate<model_switch>.- Missing provenance: handle explicitly and conservatively after legacy provenance inference.
This keeps the existing model-derived behavior intact while preventing effective custom instructions from being mistaken for target-model-native instructions.
Regression coverage should include:
- custom instructions + completed model-A turn + switch to model B → no
<model_switch>; - custom instructions remain in top-level
instructions; - model-derived model A → model B still injects model B's native instructions;
- missing/legacy provenance behavior remains explicit.
I searched open and closed Issues and PRs for custom base instructions, model switching, <model_switch>, and base-instruction provenance and did not find an equivalent report.
I have traced the relevant model-resolution and world-state paths and have a deterministic request-level regression test for this behavior. If this is considered a bug and the focused direction matches the intended semantics, I would be happy to implement the fix. Would the Codex team be willing to invite me to open a PR for it?
Disclosure: this analysis was produced with significant assistance from OpenAI Codex. It is based only on the public openai/codex repository and a synthetic integration test; it contains no private code, data, prompts, roadmaps, or product-specific examples.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with codex-rs/core/src/session/world_state.rs and codex-rs/core/src/context/world_state/model.rs, then review the related configuration paths in codex-rs/core/src/config/mod.rs and codex-rs/models-manager/src/model_info.rs. Run the named model_change_does_not_wrap_custom_base_instructions integration test. Done means custom instructions remain only in top-level instructions after a model switch, while model-derived instructions retain existing behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100