Files
hermes-agent/tests/test_copilot_initiator.py
Teknium 2a95791992 simplify(compat): run_agent/model_tools/toolsets/acp/providers — drop 42 re-exports/aliases, repoint 15 callers + 99 test files
run_agent.py: delete the `# noqa: F401` re-export block (agent.process_bootstrap
OpenAI/_SafeWriter/_get_proxy_*, model_tools get_tool_definitions/
handle_function_call/check_toolset_requirements, FailoverReason,
_qwen_portal_headers/_routermint_headers, session_persistence names,
estimate_request_tokens_rough, ContextCompressor + friends, jittered_backoff,
prompt_builder names, message_sanitization names, tool_dispatch_helpers
names) — 41 names run_agent never used itself — and the `_STREAM_DIAG_HEADERS`
back-compat class alias (no in-tree reader). run_agent now imports only what
it uses (get_toolset_for_tool, is_local_endpoint, coalesce/uniquify tool-call
ids, cleanup_vm/get_active_env from terminal_tool_lifecycle).

agent/*: `_ra().X` late-binds that only reached a re-export now import the
defining module directly (agent_runtime_helpers -> process_bootstrap.OpenAI,
model_tools.handle_function_call, session_persistence._safe_session_filename_component;
agent_init -> model_tools.get_tool_definitions/check_toolset_requirements,
_lazy_headers("agent.client_lifecycle", ...) for qwen/routermint;
system_prompt -> agent.prompt_builder / model_tools directly, dropping its
own _ra() shim and the `_r` parameter threading). `_ra()` stays for
run_agent-resident names (logger, AIAgent, _hermes_home, _set_interrupt, ...).

toolsets.py: remove resolve_multiple_toolsets (shim-only, restored by
34abf954bd); tests/test_toolsets.py pins the same union behavior via
resolve_toolset over each name.

providers/__init__.py: drop the OMIT_TEMPERATURE re-export (no callers via the
package); ProviderProfile stays because __init__ uses it for annotations —
2 tests repointed to providers.base.

agent/iteration_budget.py: drop the "run_agent re-exports the class"
docstring pointer; 4 tests import IterationBudget from its home.

model_tools.py (arg_coercion names), agent/tool_executor.py, and
hermes_cli/cli_session_mixin.py repoints landed via a sibling commit on this
shared worktree.

Callers repointed: gateway/run.py, hermes_cli/cli_chat_turn_mixin.py,
hermes_cli/cli_tui_mixin.py, tui_gateway/session_workdir.py,
agent/transports/codex.py (one-line imports) + comment pointers in
tools/file_state.py, tools/schema_sanitizer.py, scripts/tool_search_livetest.py.
Tests: patch("run_agent.X") / monkeypatch.setattr(run_agent, "X") /
`from run_agent import X` -> defining module across 99 test files.
2026-09-03 13:28:22 -07:00

140 lines
5.1 KiB
Python

"""Tests for per-turn Copilot x-initiator header injection (issue #3040).
Copilot bills "premium requests" only when a request is marked as
user-initiated via the ``x-initiator: user`` header. Hermes previously sent
``x-initiator: agent`` on every request (client-level default headers), so
user prompts never consumed premium requests and were throttled as agent
traffic. The fix marks the FIRST API call of each user turn as "user" and
lets tool-loop follow-ups keep the "agent" default.
Salvaged from PR #4097 (@tjp2021); adapted to the post-refactor layout
(conversation_loop.py owns the injection site, the codex transport now
accepts extra_headers).
"""
import pytest
from run_agent import AIAgent
def _tool_defs(*names):
return [
{"type": "function", "function": {"name": n, "description": n, "parameters": {}}}
for n in names
]
class _FakeOpenAI:
def __init__(self, **kw):
self.api_key = kw.get("api_key", "test")
self.base_url = kw.get("base_url", "http://test")
def close(self):
pass
def _make_agent(monkeypatch, base_url, api_mode="chat_completions"):
"""Create an AIAgent pointing at the given base_url."""
monkeypatch.setattr("model_tools.get_tool_definitions", lambda **kw: _tool_defs("web_search"))
monkeypatch.setattr("model_tools.check_toolset_requirements", lambda: {})
monkeypatch.setattr("agent.process_bootstrap.OpenAI", _FakeOpenAI)
return AIAgent(
api_key="test-key",
base_url=base_url,
provider="copilot" if "githubcopilot" in base_url else "openrouter",
api_mode=api_mode,
max_iterations=4,
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
)
def _inject(agent, api_kwargs):
"""Mirror the injection block in agent/conversation_loop.py."""
if getattr(agent, "_is_user_initiated_turn", False) and agent._is_copilot_url():
_xh = dict(api_kwargs.get("extra_headers") or {})
_xh["x-initiator"] = "user"
api_kwargs["extra_headers"] = _xh
agent._is_user_initiated_turn = False
return api_kwargs
class TestIsCopilotUrl:
"""_is_copilot_url() detects GitHub Copilot endpoints."""
def test_standard_copilot_url(self, monkeypatch):
agent = _make_agent(monkeypatch, "https://api.githubcopilot.com")
assert agent._is_copilot_url() is True
def test_github_models_url(self, monkeypatch):
agent = _make_agent(monkeypatch, "https://models.github.ai/inference")
assert agent._is_copilot_url() is True
def test_openrouter_url(self, monkeypatch):
agent = _make_agent(monkeypatch, "https://openrouter.ai/api/v1")
assert agent._is_copilot_url() is False
def test_case_insensitive(self, monkeypatch):
agent = _make_agent(monkeypatch, "https://API.GITHUBCOPILOT.COM")
assert agent._is_copilot_url() is True
class TestUserInitiatedTurnFlag:
"""_is_user_initiated_turn lifecycle."""
def test_default_is_false(self, monkeypatch):
agent = _make_agent(monkeypatch, "https://api.githubcopilot.com")
assert agent._is_user_initiated_turn is False
def test_reset_session_clears_flag(self, monkeypatch):
agent = _make_agent(monkeypatch, "https://api.githubcopilot.com")
agent._is_user_initiated_turn = True
agent.reset_session_state()
assert agent._is_user_initiated_turn is False
class TestFlagFlipOnInjection:
"""Flag flips immediately on injection so tool-loop calls use 'agent'."""
def test_first_call_injects_user_initiator(self, monkeypatch):
agent = _make_agent(monkeypatch, "https://api.githubcopilot.com")
agent._is_user_initiated_turn = True
kwargs = _inject(agent, {})
assert kwargs["extra_headers"] == {"x-initiator": "user"}
assert agent._is_user_initiated_turn is False
def test_second_call_has_no_injection(self, monkeypatch):
agent = _make_agent(monkeypatch, "https://api.githubcopilot.com")
agent._is_user_initiated_turn = True
kwargs1 = _inject(agent, {})
kwargs2 = _inject(agent, {})
assert "extra_headers" in kwargs1
assert "extra_headers" not in kwargs2
def test_non_copilot_flag_not_flipped(self, monkeypatch):
agent = _make_agent(monkeypatch, "https://openrouter.ai/api/v1")
agent._is_user_initiated_turn = True
kwargs = _inject(agent, {})
assert "extra_headers" not in kwargs
# Flag unchanged — non-Copilot path doesn't touch it
assert agent._is_user_initiated_turn is True
class TestHeaderValues:
"""copilot_default_headers(is_agent_turn=...) sets x-initiator correctly."""
def test_default_is_agent(self):
from hermes_cli.models import copilot_default_headers
assert copilot_default_headers()["x-initiator"] == "agent"
def test_user_turn(self):
from hermes_cli.models import copilot_default_headers
assert copilot_default_headers(is_agent_turn=False)["x-initiator"] == "user"
def test_agent_turn_explicit(self):
from hermes_cli.models import copilot_default_headers
assert copilot_default_headers(is_agent_turn=True)["x-initiator"] == "agent"