Codex App omits `model` field on /v1/responses follow-up requests when using a custom OpenAI-compatible provider

Resolved 💬 2 comments Opened Aug 27, 2026 by ptrsauer Closed Aug 27, 2026

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.

View original on GitHub ↗

2 Comments

ptrsauer · 1 day ago

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:

/Applications/ChatGPT.app/Contents/Resources/codex \
  -c features.code_mode_host=true app-server --analytics-default-enabled …

.../Resources/codex --version reports codex-cli 0.150.0-alpha.8, and matching
rust-v0.150.0-alpha.* tags exist in this repo. So the request-building path is in
this tree, not somewhere else.

But the request-building path cannot produce model=None. Checked against
main at 89650c6:

  • codex-rs/codex-api/src/common.rs:276ResponsesApiRequest { pub model: String, … },

no skip_serializing_if, so it is always serialized

  • codex-rs/codex-api/src/common.rs:329ResponseCreateWsRequest { pub model: &'a str, … },

same

  • codex-rs/core/src/client.rs:771 and :979 — both fed from model: model_info.slug.clone(),

and ModelInfo.slug is a plain String

  • codex-rs/models-manager/src/manager.rs:159get_default_model() takes

&Option<String> but returns a String, falling back to the catalog default

A 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 empty
string. 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 model is optional.** The only place in the public tree where it may be
omitted is the app-server protocol boundary — six occurrences of
pub model: Option<String> in
codex-rs/app-server-protocol/src/protocol/v2/{thread.rs:64,248,359,546, turn.rs:52,211},
documented as "Omission or null leaves 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 path
where 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_ids symbol in main, and the only
is_azure_responses_provider consumer
(codex-rs/model-provider/src/provider.rs:341) gates remote compaction support, not
item fields and not model. That link is not supported by the current code — please
disregard it.
The nearest provider-conditional logic is codex-rs/core/src/client.rs:940,
which is !is_openai rather than Azure-specific and touches internal metadata, not model.

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.

ptrsauer · 1 day ago

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:

proxy (leaving the app)                    gateway log (arriving)
─────────────────────────────────────────────────────────────────────
11:37:41  POST /v1/responses  512113 B     09:37:41  400 model=None
          model=gpt-5.6-luna → 502
11:37:42  POST /v1/responses  512113 B     09:37:42  400 model=None
          model=gpt-5.6-luna → 502
11:37:42  POST /v1/responses  512113 B     09:37:43  200 OK, model applied
          model=gpt-5.6-luna → 200

Byte-identical payloads, all three carrying model. Two arrive without it, the third
arrives 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:

  • The 502 bodies were HTML, not JSON (`The server returned an invalid or

incomplete response). A LiteLLM proxy answers in JSON; HTML means a load balancer
in front of it. Shortly afterwards the whole gateway returned
503 No server is available — including on /health/liveliness` — so the
infrastructure in front of it was demonstrably unstable during the capture window.

  • The proxy logged every request and every method. Proxy count and gateway count match;

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 observing
what the client actually sent. The reproduction I posted (follow-up with
previous_response_id and no model) does produce that error, but it is not what the
app 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.