fix(codex): scope encrypted-reasoning replay to the issuing model

Encrypted reasoning blobs are sealed to the model that minted them, not
only to the endpoint. Switching models on the same custom Responses
endpoint therefore replayed blobs the new model cannot decrypt and the
turn failed with HTTP 400.

Stamp captured reasoning items with `_issuer_model` (the canonical wire
model) alongside `_issuer_kind`, and replay an item only when both the
issuer kind and the model match the current request. Endpoint-stamped
legacy items without model provenance are dropped once the current
model is known (fail closed); ordinary assistant text stays replayable.
The transport threads the effective wire model (request_overrides win)
into conversion and normalization; the auxiliary Codex adapter stamps
and filters against its own model rather than the main agent's. The
400 classifier also recognises the custom-endpoint wording
"encrypted content could not be decrypted or parsed" so recovery strips
the replay state instead of aborting.

Hand-grafted from #95849 (final head d9cf6bcc08) onto current main; the
middleware-model-rewrite half is intentionally left out.

Closes #95834
This commit is contained in:
Fangliquan
2026-09-14 23:27:12 +05:30
committed by kshitij
parent f7b6a2b59f
commit 51ebdff570
6 changed files with 131 additions and 35 deletions
+15 -7
View File
@@ -107,10 +107,7 @@ def aux_probe_mode():
from agent.credential_pool import load_pool
from agent.model_metadata import (
MINIMUM_CONTEXT_LENGTH, get_model_context_length,
strip_codex_context_variant_suffix as _strip_codex_ctx_variant,
)
from agent.model_metadata import MINIMUM_CONTEXT_LENGTH, get_model_context_length
from hermes_cli.config import get_hermes_home
from agent.auxiliary_health import _custom_health_base_url, _unhealthy_cache_key
from hermes_constants import OPENROUTER_BASE_URL, hermes_home_key
@@ -1340,8 +1337,13 @@ class _CodexCompletionsAdapter:
# includes assistant tool_calls + role="tool" results). The shared converter encodes assistant tool
# calls as `function_call` items and tool results as `function_call_output` items with a valid
# call_id, so every Responses path normalizes tool history identically and cannot drift.
from agent.codex_responses_adapter import _chat_messages_to_responses_input
from agent.codex_responses_adapter import (
_chat_messages_to_responses_input,
_classify_responses_issuer,
_wire_model_identity,
)
model = kwargs.get("model", self._model)
wire_model = _wire_model_identity(model)
host = str(getattr(self._client, "base_url", "") or "")
is_xai = base_url_host_matches(host, "x.ai") or base_url_host_matches(host, "api.x.ai")
is_copilot = base_url_host_matches(host, "githubcopilot.com")
@@ -1363,12 +1365,18 @@ class _CodexCompletionsAdapter:
# Auxiliary calls (context compression, flush_memories, MoA aggregation) go through this adapter
# instead of agent/transports/codex.py's build_kwargs, so they need the same guard applied
# independently. See #32716.
# Aux requests run their own model; stamp/filter reasoning provenance against it, not the main agent's.
input_items = _chat_messages_to_responses_input(
replay_messages, is_github_responses=is_copilot, native_compaction_eligible=False
replay_messages, is_github_responses=is_copilot,
current_issuer_kind=_classify_responses_issuer(
is_xai_responses=is_xai, is_github_responses=is_github,
is_codex_backend=base_url_host_matches(host, "chatgpt.com"), base_url=host,
),
current_issuer_model=wire_model, native_compaction_eligible=False,
)
resp_kwargs: Dict[str, Any] = {
# Codex only knows the base slug; strip the Hermes ``-900k`` picker suffix.
"model": _strip_codex_ctx_variant(model), "instructions": instructions,
"model": wire_model, "instructions": instructions,
"input": input_items or [{"role": "user", "content": ""}], "store": False,
}
# Forward the chat.completions timeout; otherwise a Codex stream can sit behind a