From 6f9f5503084c7dd8196ba448caa36591cbda7dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 23 Jul 2026 00:55:23 -0400 Subject: [PATCH] =?UTF-8?q?fix(codex):=20drop=20bare=20rs=5F*=20reasoning?= =?UTF-8?q?=20references=20=E2=80=94=20stateless=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-provider audit R3: reasoning captured on stateful api.openai.com sessions (no include requested) carries a server-issued rs_* id but NO encrypted_content; replaying that bare reference to the stateless (store:false) codex backend points at server state chatgpt.com does not have. adapt_body_for_codex_backend step 3 drops such items (encrypted ones pass through verbatim). Capture-side include for the API-key path is deferred: the typed CreateResponse is shared with the xai Responses leg and changing its bytes needs separate validation. Part 3 of the cross-provider replay audit. Verified: sampling-types+sampler+chat-state+shell all green. --- .../codegen/kigi-sampling-types/src/types.rs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/codegen/kigi-sampling-types/src/types.rs b/crates/codegen/kigi-sampling-types/src/types.rs index 179394f..6c6bf8c 100644 --- a/crates/codegen/kigi-sampling-types/src/types.rs +++ b/crates/codegen/kigi-sampling-types/src/types.rs @@ -1054,6 +1054,10 @@ pub fn patch_reasoning_effort(body: &mut Value, effort: Option) /// 2. `store` is always `false` on this backend, so reasoning continuity /// is stateless: `include: ["reasoning.encrypted_content"]` is required /// for the response to carry replayable encrypted reasoning. +/// 3. For the same reason, a replayed reasoning item WITHOUT +/// `encrypted_content` (captured from a stateful api.openai.com session +/// that never requested the include) references server state +/// chatgpt.com does not have — drop it rather than 400. /// /// openai-codex-GATED at the call sites — API-key `openai` Responses /// bodies stay byte-identical. @@ -1104,6 +1108,18 @@ pub fn adapt_body_for_codex_backend(body: &mut Value) { entries.push(key); } } + + // 3. Drop reasoning items with no encrypted payload: stateless codex + // cannot resolve a bare `rs_*` reference. + if let Some(input) = body.get_mut("input").and_then(|v| v.as_array_mut()) { + input.retain(|item| { + item.get("type").and_then(|t| t.as_str()) != Some("reasoning") + || item + .get("encrypted_content") + .and_then(|v| v.as_str()) + .is_some_and(|s| !s.is_empty()) + }); + } } /// Neutralize a `reasoning.effort` echo the typed `rs` enum cannot parse @@ -1662,6 +1678,38 @@ mod tests { assert_eq!(body, before); } + /// Stateless codex cannot resolve a bare `rs_*` reference: reasoning + /// input items without an encrypted payload are dropped; items WITH + /// one pass through untouched. + #[test] + fn codex_adapter_drops_reasoning_without_encrypted_payload() { + let mut body = json!({ + "model": "gpt-5.2-codex", + "input": [ + {"type": "message", "role": "user", "content": "q"}, + {"type": "reasoning", "id": "rs_bare", "summary": []}, + {"type": "reasoning", "id": "rs_full", "summary": [], + "encrypted_content": "gAAAA-blob"}, + {"type": "message", "role": "assistant", "content": "a"} + ] + }); + adapt_body_for_codex_backend(&mut body); + let input = body["input"].as_array().unwrap(); + assert_eq!(input.len(), 3, "bare rs_* item dropped: {body:#}"); + assert!( + input + .iter() + .any(|i| i.get("id").and_then(|v| v.as_str()) == Some("rs_full")), + "encrypted reasoning passes through: {body:#}" + ); + assert!( + !input + .iter() + .any(|i| i.get("id").and_then(|v| v.as_str()) == Some("rs_bare")), + "{body:#}" + ); + } + /// No system items and no prior instructions: input untouched, no /// empty-string instructions invented, include still requested. #[test]