mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
fix(codex): also guard the auxiliary Copilot Responses adapter
_CodexCompletionsAdapter (agent/auxiliary_client.py) is a second, independent producer of Codex Responses input — used by auxiliary calls (context compression, flush_memories, MoA aggregation, session_search) that route through CodexAuxiliaryClient instead of the main agent's ResponsesApiTransport.build_kwargs. It calls _chat_messages_to_responses_input() directly without is_github_responses, so the previous commit's fix didn't cover it: an auxiliary call made against a Copilot-backed session could still replay a connection-scoped codex_message_items id and hit the same HTTP 401. Detect the Copilot host from the adapter's own client.base_url (same check the adapter already does further down for prompt_cache_key opt-out) and pass is_github_responses through, closing the gap. Still #32716. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b9146a47bc
commit
83b2a685cd
2 changed files with 102 additions and 1 deletions
|
|
@ -872,6 +872,7 @@ class _CodexCompletionsAdapter:
|
|||
# `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 utils import base_url_host_matches
|
||||
|
||||
instructions = "You are a helpful assistant."
|
||||
replay_messages: List[Dict[str, Any]] = []
|
||||
|
|
@ -883,7 +884,18 @@ class _CodexCompletionsAdapter:
|
|||
else:
|
||||
replay_messages.append(msg)
|
||||
|
||||
input_items = _chat_messages_to_responses_input(replay_messages)
|
||||
# Copilot (githubcopilot.com) binds replayed codex_message_items ids
|
||||
# to a backend "connection" that doesn't survive credential
|
||||
# rotation/gateway restarts — replaying one gets HTTP 401 "input
|
||||
# item ID does not belong to this connection" (#32716). 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.
|
||||
_host_for_input = str(getattr(self._client, "base_url", "") or "")
|
||||
_is_github_for_input = base_url_host_matches(_host_for_input, "githubcopilot.com")
|
||||
input_items = _chat_messages_to_responses_input(
|
||||
replay_messages, is_github_responses=_is_github_for_input,
|
||||
)
|
||||
|
||||
resp_kwargs: Dict[str, Any] = {
|
||||
"model": model,
|
||||
|
|
|
|||
|
|
@ -4050,6 +4050,95 @@ class TestCodexAdapterPromptCacheKey:
|
|||
assert "prompt_cache_key" not in captured
|
||||
|
||||
|
||||
class TestCodexAdapterGithubResponsesMessageIdDrop:
|
||||
"""_CodexCompletionsAdapter must drop codex_message_items ``id`` when
|
||||
talking to Copilot (githubcopilot.com), independent of the main
|
||||
transport's build_kwargs path. Auxiliary calls (context compression,
|
||||
flush_memories, MoA aggregation) route through this adapter instead of
|
||||
agent/transports/codex.py, so they need the same #32716 guard applied
|
||||
separately — Copilot binds replayed ids to a backend "connection" that
|
||||
doesn't survive credential rotation/gateway restarts, and rejects a
|
||||
stale id with HTTP 401 regardless of its length.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _build_adapter(base_url):
|
||||
from agent.auxiliary_client import _CodexCompletionsAdapter
|
||||
from types import SimpleNamespace
|
||||
|
||||
message_item = SimpleNamespace(
|
||||
type="message", role="assistant", status="completed",
|
||||
content=[SimpleNamespace(type="output_text", text="hi")],
|
||||
)
|
||||
events = [
|
||||
SimpleNamespace(type="response.created"),
|
||||
SimpleNamespace(type="response.output_item.done", item=message_item),
|
||||
SimpleNamespace(
|
||||
type="response.completed",
|
||||
response=SimpleNamespace(
|
||||
status="completed", id="resp_test",
|
||||
usage=SimpleNamespace(input_tokens=1, output_tokens=1, total_tokens=2),
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
class _FakeCreateStream:
|
||||
def __iter__(self): return iter(events)
|
||||
def close(self): pass
|
||||
|
||||
captured_kwargs = {}
|
||||
|
||||
def _create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return _FakeCreateStream()
|
||||
|
||||
real_client = MagicMock()
|
||||
real_client.base_url = base_url
|
||||
real_client.responses.create = _create
|
||||
adapter = _CodexCompletionsAdapter(real_client, "gpt-5.5")
|
||||
return adapter, captured_kwargs
|
||||
|
||||
@staticmethod
|
||||
def _replay_messages():
|
||||
return [
|
||||
{"role": "system", "content": "You are helpful."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "pong",
|
||||
"codex_message_items": [
|
||||
{
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"content": [{"type": "output_text", "text": "pong"}],
|
||||
"id": "msg_short_but_connection_scoped",
|
||||
"phase": "final_answer",
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "continue"},
|
||||
]
|
||||
|
||||
def test_drops_message_id_for_github_copilot_host(self):
|
||||
adapter, captured = self._build_adapter(base_url="https://api.githubcopilot.com")
|
||||
adapter.create(messages=self._replay_messages())
|
||||
message_item = next(
|
||||
item for item in captured["input"] if item.get("type") == "message"
|
||||
)
|
||||
assert "id" not in message_item
|
||||
assert message_item["phase"] == "final_answer"
|
||||
|
||||
def test_keeps_message_id_for_codex_backend_host(self):
|
||||
adapter, captured = self._build_adapter(
|
||||
base_url="https://chatgpt.com/backend-api/codex"
|
||||
)
|
||||
adapter.create(messages=self._replay_messages())
|
||||
message_item = next(
|
||||
item for item in captured["input"] if item.get("type") == "message"
|
||||
)
|
||||
assert message_item["id"] == "msg_short_but_connection_scoped"
|
||||
|
||||
|
||||
class TestVisionAutoSkipsKimiCoding:
|
||||
"""_resolve_auto vision branch skips providers that have no vision on
|
||||
their main endpoint (e.g. Kimi Coding Plan /coding) and falls through
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue