Repository navigation
Memory writer sends hardcoded gpt-5.6-luna and gpt-5.6-terra requests to non-OpenAI model providers #37009
Description
What version of Codex CLI is running?
codex-cli 0.145.0
What subscription do you have?
ChatGPT Plus — not used for this session, which authed via API key through a custom model_provider.
Which model were you using?
A non-OpenAI model, selected with -m and served through a custom model_provider. The specific model does not matter — the behavior below is the same for any non-OpenAI model.
What platform is your computer?
macOS 25.5.0, arm64.
What issue are you seeing?
When Codex runs against a non-OpenAI model_provider, the memory writer ignores the active session model and sends requests using two hardcoded OpenAI model IDs. The active provider serves them, so nothing errors. The calls are only visible in the provider's activity log.
I selected a non-OpenAI model. These appeared in the provider log for the same session:
gpt-5.6-lunagpt-5.6-terra
I did not select either model. Both were billed to my account at the provider. The requests originate from my machine, but for a model I did not choose.
Root cause. All line numbers below are from main at the time of writing. codex-rs/model-provider/src/provider.rs defines three provider-blind defaults:
pub const DEFAULT_APPROVAL_REVIEW_PREFERRED_MODEL: &str = "codex-auto-review";
pub const DEFAULT_MEMORY_EXTRACTION_PREFERRED_MODEL: &str = "gpt-5.6-luna";
pub const DEFAULT_MEMORY_CONSOLIDATION_PREFERRED_MODEL: &str = "gpt-5.6-terra";The trait methods that return them are only overridden by AmazonBedrockModelProvider. Every other provider inherits the OpenAI values.
The memory writer reads them directly. codex-rs/memories/write/src/phase1.rs:193:
let model_name = config.memories.extract_model.clone().unwrap_or_else(|| {
context
.provider()
.memory_extraction_preferred_model()
.to_string()
});codex-rs/memories/write/src/phase2.rs:359:
config
.memories
.consolidation_model
.clone()
.unwrap_or_else(|| provider.memory_consolidation_preferred_model().to_string())Neither falls back to the active session model when the provider is not OpenAI-hosted. The fallback is the OpenAI constant.
This is the same class as #31255 and #31732, which cover DEFAULT_APPROVAL_REVIEW_PREFERRED_MODEL. Those report a hard failure: the provider rejects the unknown ID codex-auto-review with a 400 or 404. The memory-writer variant behaves differently, because gpt-5.6-luna and gpt-5.6-terra are real IDs that many gateways serve. There is no error, so the behavior is silent.
The memory writer reads prior thread rollouts, so this sends recorded session content to models the user did not select.
What steps can reproduce the bug?
Observed through ori 0.4.0+063b32e, OpenRouter's wrapper binary that launches the Codex CLI against the OpenRouter API.
-
Run
ori codex --model <any non-openai model>.ori codexlaunches the installed Codex CLI with these config overrides, plus-m <model>andOPENROUTER_API_KEYin the environment:
-c model_provider=openrouter
-c model_providers.openrouter.name="OpenRouter"
-c model_providers.openrouter.base_url="https://openrouter.ai/api/v1"
-c model_providers.openrouter.wire_api="responses"
-c model_providers.openrouter.auth.command="sh"
-c model_providers.openrouter.auth.args=["-c","echo $OPENROUTER_API_KEY"]
-
Leave
[memories] extract_modelandconsolidation_modelunset. -
Do some work, exit, then start Codex again so the memory startup pass runs.
-
Read the OpenRouter activity log. Requests appear for
gpt-5.6-lunaandgpt-5.6-terraalongside the requests for the selected model.
Any OpenAI-compatible gateway that serves those two model IDs should behave the same way. ori is how I hit it, not a precondition.
What is the expected behavior?
When the active provider is not OpenAI-hosted, the memory writer should use the active session model — the same intent guardian_review_session_config already carries in codex-rs/core/src/guardian/review.rs, where the non-matching branch falls back to turn.model_info.slug.
If a hardcoded default must be kept, Codex should not send an unselected model to a third-party provider without disclosing it.
The same provider gate proposed for the guardian path in #31732 applies here — resolve memory_extraction_preferred_model and memory_consolidation_preferred_model against the session model unless the provider is OpenAI-hosted, and always honor an explicit extract_model / consolidation_model.
Additional information
Outline of a potential fix, per the contributing guide's request for a high-level outline rather than a PR.
The gate already exists as a field. ModelProviderInfo.requires_openai_auth (codex-rs/model-provider/src/provider.rs:71) distinguishes first-party from custom providers, and the crate already derives a stricter form of it in provider_uses_first_party_auth_path at line 255.
Applying that gate at both memory call sites keeps the OpenAI defaults for OpenAI-hosted providers and falls back to the active session model otherwise:
// phase1.rs, build_request_context
let model_name = config.memories.extract_model.clone().unwrap_or_else(|| {
let provider = context.provider();
if provider.info().requires_openai_auth {
provider.memory_extraction_preferred_model().to_string()
} else {
// active session model, the value the guardian path reads as
// turn.model_info.slug
session_model.to_string()
}
});Phase 2 takes the same shape around memory_consolidation_preferred_model.
Three notes on this outline. An explicit extract_model or consolidation_model should keep winning, which the existing unwrap_or_else already gives. The same gate applies to the guardian path in #31255 and #31732, so one shared helper may be better than three call sites. And I have not built or tested this — the accessor for the session model inside the memories crate is the part I could not confirm from reading alone.
Workaround. Set both keys explicitly:
[memories]
extract_model = "<your model>"
consolidation_model = "<your model>"Or disable the subsystem:
[memories]
generate_memories = falseOne related note. MemoriesToml.min_rate_limit_remaining_percent is documented as "Minimum remaining percentage required in Codex rate-limit windows before memory startup runs." That gate also assumes an OpenAI backend, so the memory startup path carries more than one OpenAI assumption.
Activity
github-actions commentedon Aug 5, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
asdf8675309 commentedon Aug 5, 2026
Not a duplicate of #36094. That issue — along with #31255 and #31732 — is the
approval-review path, which reads
DEFAULT_APPROVAL_REVIEW_PREFERRED_MODEL = "codex-auto-review". It fails loudly,because the provider rejects an ID it does not have.
This issue is the memory writer, which reads two different constants:
DEFAULT_MEMORY_EXTRACTION_PREFERRED_MODEL = "gpt-5.6-luna"atcodex-rs/memories/write/src/phase1.rs:193, andDEFAULT_MEMORY_CONSOLIDATION_PREFERRED_MODEL = "gpt-5.6-terra"atphase2.rs:359. Both are real model IDs, so a gateway serves them and nothingerrors. The only signal is a charge in the provider's activity log.
Same root-cause shape, three separate constants, and this pair is silent rather
than fatal. One shared provider gate would cover all of them.
ded-furby commentedon Aug 5, 2026
I agree this looks like a provider-routing bypass. The reported default constants currently point to gpt-5.6-luna and gpt-5.6-terra, which can route provider-incompatible IDs through non-OpenAI gateways. That can create silent provider-specific failures/charges instead of a hard failure. If this remains accepted, a likely fix path is to derive memory defaults from the same provider-aware model selection path used for response model resolution, with deterministic fallback and regression coverage for unsupported provider IDs.
metrofun commentedon Aug 11, 2026
Workaround if you need to keep Codex unchanged: route the two hardcoded model names yourself.
I verified this with swobuforge/swobu:
gpt-5.6-luna→ your chosen modelgpt-5.6-terra→ your chosen modelCodex still emits those names, but the gateway controls what actually runs upstream.
Install Swobu:
curl -fsSL https://swobu.com/install.sh | shCreate routes for those two names and point them at the model you want.
MatsukataMusubi commentedon Sep 27, 2026
Confirming this on Windows with a third-party provider, and adding the billing side — which I think is the more serious half of the bug.
Setup
0.155.0-alpha.9.2(also reproduces on0.155.0-alpha.16.3), Codex Desktop26.915.x/26.917.xmodel_provider= a third-party OpenAI-compatible gateway,wire_api = "responses"[memories] generate_memories = true,use_memories = true, and neitherextract_modelnorconsolidation_modelsetObserved
model=gpt-5.6-terrasent to the third-party endpoint,cwd=~/.codex/memories, all HTTP 200. I never selected that model id.get_model_info{model="gpt-5.6-luna"}resolving during athread/resume, i.e. the extraction path hits the same constants.The part I had not seen reported: the provider bills you under a different model name
Querying the provider's per-model usage breakdown for the incident day:
gpt-5.6-terraandgpt-5.6-lunaappear nowhere in the billing breakdownSo the user is not only billed for a model they did not choose; the invoice does not even show the model id that was requested. Reconciliation is impossible from the billing side alone.
Why this is worse than "wrong model id"
The request originates from Codex, but the silent success is created by the gateway. Any client-side bug that emits an unknown model id — this one,
DEFAULT_APPROVAL_REVIEW_PREFERRED_MODEL, or a future one — will be silently billed instead of failing fast. A hard 404/400 costs nothing.Two asks
codex-auto-review,gpt-5.6-luna,gpt-5.6-terra).Notes
extract_model/consolidation_modelunset.[memories].