From 8680f61f8b199206d8a63cf15853c5bf6475d39a Mon Sep 17 00:00:00 2001 From: helix4u <4317663+helix4u@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:47:14 -0600 Subject: [PATCH] fix(copilot-acp): keep acp runtime off responses path --- agent/auxiliary_client.py | 43 ++++++++++++++++++- run_agent.py | 18 +++++--- tests/agent/test_auxiliary_client.py | 40 +++++++++++++++++ .../test_run_agent_codex_responses.py | 16 +++++++ 4 files changed, 110 insertions(+), 7 deletions(-) diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 5016662d5..a04c347a9 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -1223,6 +1223,8 @@ def _to_async_client(sync_client, model: str): return AsyncCodexAuxiliaryClient(sync_client), model if isinstance(sync_client, AnthropicAuxiliaryClient): return AsyncAnthropicAuxiliaryClient(sync_client), model + if sync_client.__class__.__name__ == "CopilotACPClient": + return sync_client, model async_kwargs = { "api_key": sync_client.api_key, @@ -1467,7 +1469,11 @@ def resolve_provider_client( # ── API-key providers from PROVIDER_REGISTRY ───────────────────── try: - from hermes_cli.auth import PROVIDER_REGISTRY, resolve_api_key_provider_credentials + from hermes_cli.auth import ( + PROVIDER_REGISTRY, + resolve_api_key_provider_credentials, + resolve_external_process_provider_credentials, + ) except ImportError: logger.debug("hermes_cli.auth not available for provider %s", provider) return None, None @@ -1541,6 +1547,41 @@ def resolve_provider_client( return (_to_async_client(client, final_model) if async_mode else (client, final_model)) + if pconfig.auth_type == "external_process": + creds = resolve_external_process_provider_credentials(provider) + final_model = _normalize_resolved_model(model or _read_main_model(), provider) + if provider == "copilot-acp": + api_key = str(creds.get("api_key", "")).strip() + base_url = str(creds.get("base_url", "")).strip() + command = str(creds.get("command", "")).strip() or None + args = list(creds.get("args") or []) + if not final_model: + logger.warning( + "resolve_provider_client: copilot-acp requested but no model " + "was provided or configured" + ) + return None, None + if not api_key or not base_url: + logger.warning( + "resolve_provider_client: copilot-acp requested but external " + "process credentials are incomplete" + ) + return None, None + from agent.copilot_acp_client import CopilotACPClient + + client = CopilotACPClient( + api_key=api_key, + base_url=base_url, + command=command, + args=args, + ) + logger.debug("resolve_provider_client: %s (%s)", provider, final_model) + return (_to_async_client(client, final_model) if async_mode + else (client, final_model)) + logger.warning("resolve_provider_client: external-process provider %s not " + "directly supported", provider) + return None, None + elif pconfig.auth_type in ("oauth_device_code", "oauth_external"): # OAuth providers — route through their specific try functions if provider == "nous": diff --git a/run_agent.py b/run_agent.py index 4cc8c43c5..3069a190a 100644 --- a/run_agent.py +++ b/run_agent.py @@ -705,13 +705,19 @@ class AIAgent: except Exception: pass - # GPT-5.x models require the Responses API path — they are rejected - # on /v1/chat/completions by both OpenAI and OpenRouter. Also - # auto-upgrade for direct OpenAI URLs (api.openai.com) since all + # GPT-5.x models often require the Responses API path — they are + # rejected on /v1/chat/completions by both OpenAI and OpenRouter. + # Also auto-upgrade for direct OpenAI URLs (api.openai.com) since # newer tool-calling models prefer Responses there. - if self.api_mode == "chat_completions" and ( - self._is_direct_openai_url() - or self._model_requires_responses_api(self.model) + if ( + self.api_mode == "chat_completions" + and self.provider != "copilot-acp" + and not str(self.base_url or "").lower().startswith("acp://copilot") + and not str(self.base_url or "").lower().startswith("acp+tcp://") + and ( + self._is_direct_openai_url() + or self._model_requires_responses_api(self.model) + ) ): self.api_mode = "codex_responses" diff --git a/tests/agent/test_auxiliary_client.py b/tests/agent/test_auxiliary_client.py index d1af6e7b9..e6a9d1919 100644 --- a/tests/agent/test_auxiliary_client.py +++ b/tests/agent/test_auxiliary_client.py @@ -944,6 +944,46 @@ model: } +def test_resolve_provider_client_supports_copilot_acp_external_process(): + fake_client = MagicMock() + + with patch("agent.auxiliary_client._read_main_model", return_value="gpt-5.4-mini"), \ + patch("agent.auxiliary_client.CodexAuxiliaryClient", MagicMock()), \ + patch("agent.copilot_acp_client.CopilotACPClient", return_value=fake_client) as mock_acp, \ + patch("hermes_cli.auth.resolve_external_process_provider_credentials", return_value={ + "provider": "copilot-acp", + "api_key": "copilot-acp", + "base_url": "acp://copilot", + "command": "/usr/bin/copilot", + "args": ["--acp", "--stdio"], + }): + client, model = resolve_provider_client("copilot-acp") + + assert client is fake_client + assert model == "gpt-5.4-mini" + assert mock_acp.call_args.kwargs["api_key"] == "copilot-acp" + assert mock_acp.call_args.kwargs["base_url"] == "acp://copilot" + assert mock_acp.call_args.kwargs["command"] == "/usr/bin/copilot" + assert mock_acp.call_args.kwargs["args"] == ["--acp", "--stdio"] + + +def test_resolve_provider_client_copilot_acp_requires_explicit_or_configured_model(): + with patch("agent.auxiliary_client._read_main_model", return_value=""), \ + patch("agent.copilot_acp_client.CopilotACPClient") as mock_acp, \ + patch("hermes_cli.auth.resolve_external_process_provider_credentials", return_value={ + "provider": "copilot-acp", + "api_key": "copilot-acp", + "base_url": "acp://copilot", + "command": "/usr/bin/copilot", + "args": ["--acp", "--stdio"], + }): + client, model = resolve_provider_client("copilot-acp") + + assert client is None + assert model is None + mock_acp.assert_not_called() + + class TestAuxiliaryMaxTokensParam: def test_codex_fallback_uses_max_tokens(self, monkeypatch): """Codex adapter translates max_tokens internally, so we return max_tokens.""" diff --git a/tests/run_agent/test_run_agent_codex_responses.py b/tests/run_agent/test_run_agent_codex_responses.py index 533a85ac8..0fca9e4df 100644 --- a/tests/run_agent/test_run_agent_codex_responses.py +++ b/tests/run_agent/test_run_agent_codex_responses.py @@ -243,6 +243,22 @@ def test_api_mode_respects_explicit_openrouter_provider_over_codex_url(monkeypat assert agent.provider == "openrouter" +def test_copilot_acp_stays_on_chat_completions_for_gpt_5_models(monkeypatch): + _patch_agent_bootstrap(monkeypatch) + agent = run_agent.AIAgent( + model="gpt-5.4-mini", + base_url="acp://copilot", + provider="copilot-acp", + api_key="copilot-acp", + quiet_mode=True, + max_iterations=1, + skip_context_files=True, + skip_memory=True, + ) + assert agent.provider == "copilot-acp" + assert agent.api_mode == "chat_completions" + + def test_build_api_kwargs_codex(monkeypatch): agent = _build_agent(monkeypatch) kwargs = agent._build_api_kwargs(