Responses Lite turns custom base-instruction overrides into additive developer messages
What issue are you seeing?
A custom base-instruction override supplied through model_instructions_file replaces the model's bundled Codex base instructions in standard Responses mode, but becomes additive in Responses Lite.
With Responses Lite enabled, the model sees both:
- the service-provided Codex base identity (beginning with
You are Codex), and - the custom base instructions as a later
role: developerinput message.
This changes the documented/internal meaning of base_instructions from a replacement to an additional developer message.
The behavior is present in Codex 0.147.0 and is still present on current main at 95aada11c.
Steps to reproduce
- Create a custom model instructions file:
You are CustomAgent. Do not identify as Codex.
- Configure it for a Responses-Lite-enabled model such as
gpt-5.6-sol:
model = "gpt-5.6-sol"
model_instructions_file = "/absolute/path/to/custom-instructions.md"
- Start a fresh thread and ask a literal presence probe:
Do your system or developer instructions contain any content beginning with
"You are Codex"? Reply with exactly YES or NO.
- Compare the outbound request with Responses Lite enabled and disabled.
Observed A/B, with request data redacted:
Responses Lite enabled
X-OpenAI-Internal-Codex-Responses-Lite: true
top-level instructions: omitted
input[0]: additional_tools, role=developer
input[1]: custom base instructions, role=developer
probe result: YES
Responses Lite disabled
Responses-Lite header: absent
top-level instructions: custom base instructions
request body contains no "You are Codex" text
probe result: NO
The same custom file and model are used in both cases. Only use_responses_lite changes.
Expected behavior
A custom model_instructions_file should preserve its base-instruction replacement semantics regardless of transport mode.
Either:
- Responses Lite should provide a way to replace the service default base instructions, or
- Codex should automatically use standard Responses transport when the effective base instructions are custom.
Actual behavior and source analysis
Config loading works correctly:
model_instructions_fileis read intoConfig.base_instructions.- Session initialization resolves base instructions in this order:
- configured override,
- persisted session instructions,
- model template.
The semantic change happens in codex-rs/core/src/client.rs::build_responses_request().
For standard Responses, Codex sends:
instructions = prompt.base_instructions.text
For Responses Lite, Codex instead:
- sets top-level
instructionsto an empty string, - inserts
additional_toolsas a developer item, - inserts
prompt.base_instructions.textas an ordinary developer message.
This behavior was introduced by #27946, whose stated goal was to use input items instead of top-level tools and instructions. That conversion is not semantically one-to-one when the source is a custom base-instruction override.
The existing test responses_lite_uses_input_items_for_instructions_and_tools verifies that:
- top-level
instructionsis absent, and - the text appears as a developer input item.
It does not verify that a custom base-instruction override still replaces the service default identity.
Current main already tracks:
BaseInstructionsProvenance::Custom
BaseInstructionsProvenance::Model { ... }
but the Responses Lite request path does not use that provenance when selecting the transport contract.
Suggested fix
Complete fix
Preserve a distinct base-instruction replacement channel in the Responses Lite protocol/service, rather than representing a replacement as an additive developer message.
In my A/B tests, retaining the Lite marker while restoring top-level instructions did not produce a successful response, so this may require backend coordination.
Safe client-side fallback
Until Lite supports replacement semantics, derive one effective transport mode before tool planning and request construction:
effective_responses_lite =
model_info.use_responses_lite
&& base_instructions.provenance != Some(BaseInstructionsProvenance::Custom);
The effective value should be applied consistently to:
- HTTP and WebSocket headers,
- instruction and tool placement,
- tool planning,
- reasoning context,
- parallel tool calls,
- image preparation,
- compact requests.
This would avoid requiring users to copy and maintain a complete model_catalog_json solely to set use_responses_lite = false.
Additional information
The current workaround is a full model catalog override with use_responses_lite = false for the affected model. That is brittle because model_catalog_json is a complete catalog rather than a partial metadata override.
A separate Desktop/app-server bug can prevent a project-local catalog workaround from applying at all: #26308. I will add the more specific source analysis for that issue there.
2 Comments
I'm happy to implement the client-side fallback and regression tests if this approach aligns with the maintainers' intended solution and you explicitly invite a PR.
Confirmed on
main@ 1f41cc5d92 — the semantic change is visible in one branch:https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/core/src/client.rs#L867-L896
Standard mode puts
prompt.base_instructionsinto the request's top-levelinstructionsfield — which replaces any service default. The Lite branch instead sendsinstructions: String::new()and moves the base-instruction text into arole: "developer"input message prefixed to the conversation. With the instructions field empty, the service applies its own Codex identity, and your override rides along as an additional developer message — replacement silently becomes additive, exactly as you measured.Fix depends on what the Lite endpoint accepts: if it honors a non-empty
instructionsfield, the branch should populate it with the override (keeping the developer-message scheme only for the bundled default). If Lite structurally cannot replace the service identity, thenmodel_instructions_fileshould either be rejected for Lite models or the docs updated to state it's additive there — the silent semantic downgrade is the actual bug either way.