fix(codex): drop bare rs_* reasoning references — stateless backend
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.
This commit is contained in:
@@ -1054,6 +1054,10 @@ pub fn patch_reasoning_effort(body: &mut Value, effort: Option<ReasoningEffort>)
|
||||
/// 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]
|
||||
|
||||
Reference in New Issue
Block a user