Codex App omits `model` field on /v1/responses follow-up requests when using a custom OpenAI-compatible provider
What version of the Codex App are you using (From “About Codex” dialog)?
26.820.71523 (build 7226)
What subscription do you have?
Not relevant to this report: this session does not run against OpenAI-hosted models. It uses a custom model provider configured via model_provider in config.toml.
What platform is your computer?
Darwin 25.6.0 arm64 arm
What issue are you seeing?
When Codex is configured against a custom OpenAI-compatible provider (a self-hosted LiteLLM proxy, wire_api = "responses"), some requests reach the server with no model field at all. The server rejects them:
HTTP 400
error_class: ProxyModelNotFoundError
error_message: "/responses: Invalid model name passed in model=None.
Call `/v1/models` to view available models for your key."
model is set correctly in ~/.codex/config.toml, and the majority of requests do carry it. In one 10-minute window on the server side I counted 13 successful requests (all with the configured model) alongside 62 requests with model=None — from the same session.
In the UI this does not surface as a client error. It shows up as Reconnecting 5/5 followed by:
stream disconnected before completion: error sending request for url (.../v1/responses)
so the app appears to treat the 400 like a network failure and retries, which multiplies the failed request count.
Anecdotally it happens on follow-up turns where the app continues on its own (after a tool call), not on the first turn of a conversation.
What steps can reproduce the bug?
The server-side trigger reproduces without Codex, against any OpenAI-compatible endpoint that requires model (three calls):
1) Create a stored response
POST /v1/responses
{"model": "gpt-5.6-luna", "input": "Remember the number 42.", "store": true, "max_output_tokens": 32}
2) Follow up WITH model → HTTP 200
{"model": "gpt-5.6-luna", "previous_response_id": "resp_...", "input": "Which number?"}
3) Follow up WITHOUT model → HTTP 400
{"previous_response_id": "resp_...", "input": "Which number?"}
"Invalid model name passed in model=None"
This matches what the server receives from the app. Per the OpenAI docs on conversation state, previous_response_id carries conversation state but not the model selection (top-level instructions also have to be resent), so model is still required on follow-up requests — meaning the server's 400 is correct and the omission is on the client side.
Config used:
model = "gpt-5.6-luna"
model_provider = "litellm"
[model_providers.litellm]
base_url = "https://<self-hosted-litellm>/v1"
wire_api = "responses"
requires_openai_auth = false
What is the expected behavior?
Every request the app sends to /v1/responses should include the configured model, including follow-up turns that reference previous_response_id.
Secondary: a 400 from the provider should surface as a client/config error rather than as stream disconnected with connection retries. Retrying an identical malformed request five times cannot succeed and hides the actual cause.
Additional information
This may be related to #12230, which documents that Codex drops fields on second-turn requests against non-Azure endpoints because the item-attachment logic sits behind an Azure-only guard. That issue lists id and status as the dropped fields and explicitly not model, so this may be a different field on the same code path. Noting it in case it helps narrow things down.
I could not find an existing issue covering the missing model field specifically — searched for "model=None", "Invalid model name passed", "missing model responses" and "previous_response_id model" in this repo. Happy to be pointed at a duplicate.
Not a blocker for anyone using the app against OpenAI directly; it only shows up against a custom provider that enforces the model field.
2 Comments
Follow-up with source analysis — including a correction to my own report.
The app runs the public Rust core. On macOS the ChatGPT app spawns the bundled
Codex binary in app-server mode:
.../Resources/codex --versionreportscodex-cli 0.150.0-alpha.8, and matchingrust-v0.150.0-alpha.*tags exist in this repo. So the request-building path is inthis tree, not somewhere else.
But the request-building path cannot produce
model=None. Checked againstmainat 89650c6:codex-rs/codex-api/src/common.rs:276—ResponsesApiRequest { pub model: String, … },no
skip_serializing_if, so it is always serializedcodex-rs/codex-api/src/common.rs:329—ResponseCreateWsRequest { pub model: &'a str, … },same
codex-rs/core/src/client.rs:771and:979— both fed frommodel: model_info.slug.clone(),and
ModelInfo.slugis a plainStringcodex-rs/models-manager/src/manager.rs:159—get_default_model()takes&Option<String>but returns aString, falling back to the catalog defaultA detail that narrows this down further. The gateway distinguishes the two cases
in its error text:
| sent | error message |
|---|---|
|
"model": ""|Invalid model name passed in model=||
"model": null|Invalid model name passed in model=None|| field absent |
Invalid model name passed in model=None|The observed error is
model=None, so the field is absent or null — not an emptystring. That rules out "the config resolved to an empty model" and rules out both
structs above, since they can only ever emit
""at worst.**Where
modelis optional.** The only place in the public tree where it may beomitted is the app-server protocol boundary — six occurrences of
pub model: Option<String>incodex-rs/app-server-protocol/src/protocol/v2/{thread.rs:64,248,359,546, turn.rs:52,211},documented as "Omission or
nullleaves the model unchanged."That omission is supposed to be resolved to a concrete string before the request is
built (via
get_default_model). So the most likely shape of this bug is a pathwhere a client-side omission reaches request construction without that resolution
step running — which would sit in the app shell rather than in this repo.
---
Correction to my original report: I referenced #12230 as possibly related, based on
its description of an Azure-only guard around item-attachment logic. Having now checked
the source: there is no
attach_item_idssymbol inmain, and the onlyis_azure_responses_providerconsumer(
codex-rs/model-provider/src/provider.rs:341) gates remote compaction support, notitem fields and not
model. That link is not supported by the current code — pleasedisregard it. The nearest provider-conditional logic is
codex-rs/core/src/client.rs:940,which is
!is_openairather than Azure-specific and touches internal metadata, notmodel.Happy to capture an actual request dump if that would help — a small local proxy in
front of the endpoint can record exactly which fields the client sends on the failing
turns.
Closing this — the client is not at fault. My original report was wrong, and here is
the measurement that shows it.
I put a local proxy between the app and the gateway to capture what actually leaves the
client. Result: the app always sends
model. The field goes missing in transit.Same request, retried three times, captured on both sides:
Byte-identical payloads, all three carrying
model. Two arrive without it, the thirdarrives intact. That is not a client emitting an inconsistent request — that is a
request being damaged in transit.
Two further observations that point the same way:
502bodies were HTML, not JSON (`The server returned an invalid orincomplete response
). A LiteLLM proxy answers in JSON; HTML means a load balancer
503 No server is availablein front of it. Shortly afterwards the whole gateway returned
— including on/health/liveliness` — so theinfrastructure in front of it was demonstrably unstable during the capture window.
nothing bypassed it. So the missing field is not a second code path either.
What I got wrong: I inferred a client bug from server-side logs alone. The gateway
reported
model=None, and I concluded the client omitted it — without ever observingwhat the client actually sent. The reproduction I posted (follow-up with
previous_response_idand nomodel) does produce that error, but it is not what theapp does.
Also, for the record: the reference to #12230 in my first comment was already retracted
there, and my request that you disregard it stands.
Sorry for the noise. Nothing to fix on your side — the fault is in our own
infrastructure, and it is being handled there.