[CLI][custom provider] Type-invalid item_ reasoning IDs survive replay validation and OpenAI expects rs_

Open 💬 7 comments Opened Aug 16, 2026 by SimpleZion
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of Codex CLI is running?

codex-cli 0.148.0-alpha.9 (Codex Desktop 26.810.7004.0, Windows x64)

What subscription do you have?

ChatGPT subscription through Codex Desktop. The defect is in request serialization and reproduces independently of the subscription tier.

Which model were you using?

Responses API models. The failure is independent of the selected model.

What platform is your computer?

Windows 11 x64

What terminal emulator and version are you using (if applicable)?

Codex Desktop; PowerShell 7 was used for source-level verification.

Codex doctor report

not included: the report contains unrelated local configuration; the source-level reproduction and regression tests below isolate the failure

What issue are you seeing?

When a local task contains response items produced by a custom Responses-compatible provider and the task is subsequently continued using OpenAI authentication, Codex can send the custom provider's generic item_... ID back to the OpenAI Responses API.

OpenAI rejects the request:

[ApiIdParam] [input[292].id] [invalid_id_prefix]
Invalid 'input[292].id': 'item_REDACTED'. Expected an ID that begins with 'rs'.

The problem is in ModelClient::prepare_response_items_for_request() in codex-rs/core/src/client.rs. It currently checks only:

if item.id().is_some_and(|id| !id.is_prefixed()) {
    item.set_id(None);
}

ResponseItemId::is_prefixed() accepts any non-empty prefix and suffix, so item_foreign-reasoning is considered valid. However, the API validates IDs by response-item type: reasoning requires rs_, messages require msg_, function calls require fc_, and so on.

The same implementation remains present on current main at commit 9ded177ce7c1c0bd2047f902936c177612ab3434 (checked 2026-08-16).

This is not the UUID/local-message case in #32282 or #33395, and it is not the Azure store=false persistence issue in #35739. Here the ID is syntactically prefixed, but its prefix belongs to no valid API item type.

What steps can reproduce the bug?

  1. Configure a custom Responses-compatible provider.
  2. Start a task and let the provider return persisted response items such as:

``json
{
"type": "reasoning",
"id": "item_foreign-reasoning",
"summary": [],
"encrypted_content": "foreign-ciphertext"
}
``

A custom provider may also return message or function-call items with generic item_... IDs.

  1. Preserve the same local task history and switch the active route/authentication to OpenAI.
  2. Continue the task with another user message.
  3. prepare_response_items_for_request() retains item_foreign-reasoning because is_prefixed() returns true.
  4. The OpenAI Responses API rejects the request with invalid_id_prefix, expecting rs_.

A deterministic source-level regression fixture can append the foreign reasoning item above, plus a message and function call using item_..., to the existing HTTP and WebSocket client request tests. Both transports reproduce the incorrect serialization before the fix.

What is the expected behavior?

Outgoing response item IDs should be checked against the prefix required by their concrete ResponseItem type, not merely checked for the presence of any prefix.

For portable items such as messages and function calls, Codex should omit a type-invalid ID from the request copy while preserving content, call_id, and outputs. For foreign reasoning or compaction items that carry provider-specific opaque state, Codex should omit the item from the cross-provider request rather than replaying undecryptable ciphertext.

The persisted local prompt/history should remain unchanged.

Additional information

A validated fix against the exact rust-v0.148.0-alpha.9 tag does the following:

  1. Adds ResponseItemId::is_prefixed_with(expected_prefix).
  2. Uses ResponseItem::id_prefix() during outbound request preparation.
  3. Removes reasoning/compaction items whose IDs have a type-invalid prefix from the request copy.
  4. Clears type-invalid IDs from portable message/tool items while preserving their content and linkage fields.
  5. Restores original IDs after WebSocket request serialization so in-memory and persisted prompt state are not mutated.

Focused regression tests passed for:

  • HTTP Responses request serialization;
  • Responses-over-WebSocket serialization and prompt non-mutation;
  • type-specific prefix validation.

The observed task history, prompts, tool output, and encrypted fields were not attached to this report. The synthetic values above are sufficient to reproduce the defect without exposing user data or credentials.

View original on GitHub ↗

7 Comments

github-actions[bot] contributor · 12 days ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #38365

Powered by Codex Action

SimpleZion · 12 days ago

Thanks for the cross-reference. This is related to #38365 but not a duplicate.

#38365 requests a provider-neutral handoff/fork workflow and reports tool call/output ordering differences. This issue is a narrower request-serialization bug on the normal replay path:

  • prepare_response_items_for_request() already attempts to sanitize invalid IDs;
  • it uses the generic ResponseItemId::is_prefixed() predicate;
  • item_foreign-reasoning therefore passes validation even though the concrete ResponseItem::Reasoning contract requires rs_;
  • the next OpenAI request fails before any broader handoff normalization semantics are involved.

The bug has a bounded regression test and fix for both HTTP and WebSocket transports: validate against ResponseItem::id_prefix(), omit foreign reasoning/compaction items with type-invalid IDs from the request copy, and clear type-invalid IDs from portable items without mutating persisted prompt state.

The broader handoff design in #38365 remains valuable, but fixing this predicate prevents a deterministic API 400 in existing replay behavior.

shleder · 11 days ago

The persisted cross-provider response-item ID failure here is a useful parser/diagnostic case for codex-rescue. The tool does not currently claim to repair arbitrary provider-specific item IDs or encrypted reasoning state; I’m interested in whether doctor handles the affected rollout conservatively without crashing or mutating it.

If you still have a synthetic or affected local session available, could you try:

pipx install codex-rescue
codex-rescue sessions
codex-rescue doctor --latest

The valuable outcomes are either a bounded diagnostic finding or an explicit unsupported/unknown state—not guessing the intended ID and not replaying it.

Please share only sanitized output plus versions/exit codes. No raw rollouts/SQLite, encrypted provider state, prompts, credentials, or private paths. Repo: https://github.com/shleder/codex-rescue

SimpleZion · 11 days ago

Thanks. I tested this conservatively on Windows 11 against two affected local rollouts, without sharing or modifying either source file.

Environment:

  • Codex Desktop 26.810.7004.0
  • Codex CLI source/runtime baseline 0.148.0-alpha.9
  • codex-rescue 0.1.0a3, installed from audited source at commit d68027578ce1862510e57139ffddb09e6d09fe81
  • upstream source tests: 66 passed, 1 skipped
  • pip index versions codex-rescue did not find a published distribution in this environment, so I did not use an unverified package source

Sanitized results:

Case A:

exit code: 0
status: OVERSIZED_PAYLOAD
findings: OVERSIZED_PAYLOAD, UNKNOWN_OPERATIONAL_SCHEMA
valid records: 1534
recoverable prefix: true
oversized records: 3
unfinished tool calls: 0
compacted: true
operational events after compaction: 962
compaction state loss: false
unknown schema entries: 13 x mcp_tool_call_end
source SHA-256/size/mtime unchanged: true
stderr: empty

Case B:

exit code: 0
status: OVERSIZED_PAYLOAD
findings: OVERSIZED_PAYLOAD, UNKNOWN_OPERATIONAL_SCHEMA, UNFINISHED_TOOL_CALL
valid records: 128256
recoverable prefix: true
oversized records: 23
unfinished tool calls: 13
compacted: true
operational events after compaction: 99986
compaction state loss: false
source SHA-256/size/mtime unchanged: true
stderr: empty

The second report also classified newer Codex events conservatively: tool_search_call without a name (79), image_generation_call (1), mcp_tool_call_end (13), and web_search_call (35). It reported one bounded-memory correlation overflow and five outputs without matching calls.

So the useful result is: doctor did not crash, replay provider state, or mutate either rollout. However, 0.1.0a3 does not currently diagnose the type-specific response-item ID mismatch in this issue (item_... on a reasoning item that requires rs_...). Its UNKNOWN_OPERATIONAL_SCHEMA / UNFINISHED_TOOL_CALL findings also need to be interpreted cautiously for current Codex schemas; some are compatibility gaps rather than proof of rollout corruption.

No raw rollout, SQLite content, encrypted state, prompt, credential, thread ID, or private path was included in this test or comment.

jdcodes1 · 10 days ago

Confirming your source-level analysis still holds on today's main @ 1f41cc5d92 — prepare_response_items_for_request is unchanged (core/src/client.rs#L943-L949), and ResponseItemId::is_prefixed still accepts any nonempty_nonempty shape (protocol/src/response_item_id.rs#L36-L39), so item_… survives exactly as you traced.

Two design notes for whoever implements the per-type check, both edge cases the obvious fix can trip on:

  1. Provider-awareness cuts both ways. The strict per-type prefixes (rs_, msg_, fc_, …) are an OpenAI Responses API contract, not a wire-format universal — your scenario is custom-provider IDs replayed to OpenAI, but the mirror case (OpenAI rs_ IDs replayed to a permissive custom provider) is currently accepted and should stay that way. Scoping the strict validation to providers known to enforce it (or, more simply, validating against the expected-prefix table and clearing on mismatch regardless of provider — a cleared ID is safe everywhere) avoids breaking the lenient direction while fixing yours.
  1. Clearing a reasoning item's ID is not always free. For plain-text reasoning items it just downgrades linkage, but reasoning replay with encrypted_content relies on the item's pairing with its adjacent items; if the strict check starts clearing IDs on valid rs_ items due to an over-broad table (or a future new prefix), replay quality degrades silently. Worth pairing the fix with a test that a well-formed OpenAI history round-trips with all IDs intact, alongside your foreign-ID rejection tests — the failure mode of an overzealous validator is quieter than the 400 you hit, which makes it easier to ship by accident.

Given you've already written the fix and regression tests, this issue looks like a strong candidate for the maintainers to invite a PR on per the contribution policy.

shleder · 6 days ago

@SimpleZion

Thank you for the careful conservative test on 0.1.0a3 and for the precise gap report — both limitations you identified are addressed in the just-released 0.1.0-alpha.7.

  1. Type-specific response-item ID mismatch (item_... on a reasoning item that requires rs_...): Alpha7 validates persisted response-item IDs against a per-type prefix table derived from the current upstream ResponseItem::id_prefix() values — reasoning → rs, message → msg, function_call → fc, custom_tool_call → ctc, etc. A prefixed but type-incompatible ID (exactly your item_-on-reasoning case) is now flagged as INVALID_PERSISTED_ITEM_ID. Legacy unprefixed IDs remain readable upstream and are deliberately not rejected, so this targets the replay-failure class without false-positiving old sessions.
  1. UNKNOWN_OPERATIONAL_SCHEMA false positives on current schemas: mcp_tool_call_end, tool_search_call, web_search_call, and image_generation_call are now recognized known operational schema types, and a compatibility filter removes only explicit known-schema false positives from the results. So those 13 × mcp_tool_call_end, 79 × tool_search_call, 35 × web_search_call, and 1 × image_generation_call entries you saw should no longer be reported as unknown-schema corruption — they're compatibility gaps, as you correctly noted.

To re-run on your two affected rollouts (read-only, source untouched):

codex-rescue doctor <rollout-file>

Expected: the reasoning-item ID mismatch surfaces as INVALID_PERSISTED_ITEM_ID, and the current-schema event types no longer inflate UNKNOWN_OPERATIONAL_SCHEMA. If you still see a current Codex event type misclassified as unknown, the type name would be very useful so I can extend the table.

shleder · 3 days ago

Your persisted-ID findings are now ported into the Rust adapter: type-aware ID prefixes, known/future operational schema handling, and bounded unfinished call/output correlation.

Migration update: active Codex Rescue development has moved to Vetto: https://github.com/shleder/vetto. The standalone shleder/codex-rescue repository remains public as compatibility history. User installation is now npm install --global @shleddy/vetto@next, with recovery under vetto rescue. The boundary remains read-only/copy-only and does not modify session JSONL or vendor SQLite. The newest diagnostic changes are merged into Vetto main and will be included in a later npm alpha; no source install is requested.