fix(codex): drop the message id of Azure-trimmed reasoning turns too

_newest_reasoning_only pops older turns' codex_reasoning_items before the
converter runs, so those turns looked reasoning-free and replayed their
msg_* id with neither the reasoning item nor its rs_* id on the wire — the
exact orphan shape #97427 rejects. The trimmed row now carries a transient
codex_reasoning_trimmed marker and _replay_message_items honours it; the
single-use _turn_has_encrypted_reasoning wrapper is inlined into that
predicate. Test drives ResponsesApiTransport.build_kwargs on an Azure host.
This commit is contained in:
teknium1
2026-09-19 01:14:32 -07:00
committed by Teknium
parent 3e80025708
commit 4d1d3d05a3
3 changed files with 42 additions and 10 deletions

View File

@@ -398,11 +398,6 @@ def _replay_reasoning_items(
return replayed
def _turn_has_encrypted_reasoning(msg: Dict[str, Any]) -> bool:
"""True when the stored assistant turn produced an encrypted ``reasoning`` item alongside its message."""
return any(isinstance(ri, dict) and ri.get("encrypted_content") for ri in _as_list(msg.get("codex_reasoning_items")))
def _replay_message_items(
msg: Dict[str, Any], *, is_github_responses: bool, current_issuer_kind: Optional[str] = None,
) -> List[Dict[str, Any]]:
@@ -411,11 +406,14 @@ def _replay_message_items(
A ``msg_*`` id minted in the same response as a ``reasoning`` item is bound to that item's ``rs_*`` id,
which ``_replay_reasoning_items`` always strips (store=False). Replaying the message id alone is a
deterministic HTTP 400 ("provided without its required 'reasoning' item", #97427/#97442), so the message
id is dropped whenever its turn carried encrypted reasoning — replayed, suppressed or foreign-issuer —
and the message goes out as content/status/phase only. Reasoning-free turns keep their id.
id is dropped whenever its turn carried encrypted reasoning — replayed, suppressed, foreign-issuer or
trimmed by the transport (``codex_reasoning_trimmed``) — and the message goes out as content/status/phase
only. Reasoning-free turns keep their id.
"""
replayed: List[Dict[str, Any]] = []
linked_to_reasoning = _turn_has_encrypted_reasoning(msg)
linked_to_reasoning = bool(msg.get("codex_reasoning_trimmed")) or any(
isinstance(ri, dict) and ri.get("encrypted_content") for ri in _as_list(msg.get("codex_reasoning_items"))
)
for raw_item in _as_list(msg.get("codex_message_items")):
if not (isinstance(raw_item, dict) and raw_item.get("type") == "message" and raw_item.get("role") == "assistant"):
continue

View File

@@ -450,7 +450,8 @@ def _is_azure_responses(params: dict[str, Any]) -> bool:
def _newest_reasoning_only(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Copy of ``messages`` keeping ``codex_reasoning_items`` only on the newest assistant row that has any.
Foundry rejects a request that replays encrypted reasoning from more than one prior response (HTTP 400
"Conflicting authenticated continuation identities", #105369). ``compaction`` checkpoints stay everywhere."""
"Conflicting authenticated continuation identities", #105369). ``compaction`` checkpoints stay everywhere.
A trimmed row is marked ``codex_reasoning_trimmed`` so the converter still drops its ``msg_*`` id (#97427)."""
out: list[dict[str, Any]] = []
newest_kept = False
for msg in reversed(messages):
@@ -458,7 +459,7 @@ def _newest_reasoning_only(messages: list[dict[str, Any]]) -> list[dict[str, Any
if isinstance(items, list) and any(isinstance(i, dict) and i.get("type") != "compaction" for i in items):
if newest_kept:
checkpoints = [i for i in items if isinstance(i, dict) and i.get("type") == "compaction"]
msg = dict(msg)
msg = dict(msg, codex_reasoning_trimmed=True)
if checkpoints:
msg["codex_reasoning_items"] = checkpoints
else:

View File

@@ -481,6 +481,39 @@ class TestCodexBuildKwargs:
reasoning = [item for item in kw["input"] if item.get("type") == "reasoning"]
assert [item["encrypted_content"] for item in reasoning] == ["sealed-2"]
def test_azure_trimmed_reasoning_turn_still_drops_its_message_id(self, transport):
"""The older turn's reasoning is trimmed for Azure (#105369) but its ``msg_*`` id is still bound to a
``rs_*`` id that is no longer on the wire; the id must go with it (#97427). The newest turn's id is
dropped too (its reasoning replays without id); a reasoning-free turn keeps its id."""
def _turn(text, *, reasoning):
msg = {
"role": "assistant", "content": text,
"codex_message_items": [{
"type": "message", "role": "assistant", "status": "completed", "id": f"msg_{text}",
"content": [{"type": "output_text", "text": text}],
}],
}
if reasoning:
msg["codex_reasoning_items"] = [{"type": "reasoning", "id": f"rs_{text}", "encrypted_content": f"sealed-{text}", "summary": []}]
return msg
messages = [
{"role": "user", "content": "first"}, _turn("old", reasoning=True),
{"role": "user", "content": "second"}, _turn("plain", reasoning=False),
{"role": "user", "content": "third"}, _turn("new", reasoning=True),
{"role": "user", "content": "fourth"},
]
kw = transport.build_kwargs(
model="gpt-6-astra", messages=messages, tools=[],
base_url="https://placeholder.openai.azure.com/openai/v1", replay_encrypted_reasoning=True,
)
reasoning = [i for i in kw["input"] if i.get("type") == "reasoning"]
assert [i["encrypted_content"] for i in reasoning] == ["sealed-new"]
by_text = {i["content"][0]["text"]: i for i in kw["input"] if i.get("type") == "message" and i.get("role") == "assistant"}
assert "id" not in by_text["old"] and "id" not in by_text["new"]
assert by_text["plain"]["id"] == "msg_plain"
assert "codex_reasoning_items" in messages[1] # canonical history untouched
def test_default_responses_new_turn_replays_all_reasoning(self, transport):
"""Non-Azure Responses endpoints keep cross-turn reasoning replay."""
kw = transport.build_kwargs(