fix(copilot-acp): keep acp runtime off responses path
This commit is contained in:
@@ -1223,6 +1223,8 @@ def _to_async_client(sync_client, model: str):
|
|||||||
return AsyncCodexAuxiliaryClient(sync_client), model
|
return AsyncCodexAuxiliaryClient(sync_client), model
|
||||||
if isinstance(sync_client, AnthropicAuxiliaryClient):
|
if isinstance(sync_client, AnthropicAuxiliaryClient):
|
||||||
return AsyncAnthropicAuxiliaryClient(sync_client), model
|
return AsyncAnthropicAuxiliaryClient(sync_client), model
|
||||||
|
if sync_client.__class__.__name__ == "CopilotACPClient":
|
||||||
|
return sync_client, model
|
||||||
|
|
||||||
async_kwargs = {
|
async_kwargs = {
|
||||||
"api_key": sync_client.api_key,
|
"api_key": sync_client.api_key,
|
||||||
@@ -1467,7 +1469,11 @@ def resolve_provider_client(
|
|||||||
|
|
||||||
# ── API-key providers from PROVIDER_REGISTRY ─────────────────────
|
# ── API-key providers from PROVIDER_REGISTRY ─────────────────────
|
||||||
try:
|
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:
|
except ImportError:
|
||||||
logger.debug("hermes_cli.auth not available for provider %s", provider)
|
logger.debug("hermes_cli.auth not available for provider %s", provider)
|
||||||
return None, None
|
return None, None
|
||||||
@@ -1541,6 +1547,41 @@ def resolve_provider_client(
|
|||||||
return (_to_async_client(client, final_model) if async_mode
|
return (_to_async_client(client, final_model) if async_mode
|
||||||
else (client, final_model))
|
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"):
|
elif pconfig.auth_type in ("oauth_device_code", "oauth_external"):
|
||||||
# OAuth providers — route through their specific try functions
|
# OAuth providers — route through their specific try functions
|
||||||
if provider == "nous":
|
if provider == "nous":
|
||||||
|
|||||||
18
run_agent.py
18
run_agent.py
@@ -705,13 +705,19 @@ class AIAgent:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# GPT-5.x models require the Responses API path — they are rejected
|
# GPT-5.x models often require the Responses API path — they are
|
||||||
# on /v1/chat/completions by both OpenAI and OpenRouter. Also
|
# rejected on /v1/chat/completions by both OpenAI and OpenRouter.
|
||||||
# auto-upgrade for direct OpenAI URLs (api.openai.com) since all
|
# Also auto-upgrade for direct OpenAI URLs (api.openai.com) since
|
||||||
# newer tool-calling models prefer Responses there.
|
# newer tool-calling models prefer Responses there.
|
||||||
if self.api_mode == "chat_completions" and (
|
if (
|
||||||
self._is_direct_openai_url()
|
self.api_mode == "chat_completions"
|
||||||
or self._model_requires_responses_api(self.model)
|
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"
|
self.api_mode = "codex_responses"
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
class TestAuxiliaryMaxTokensParam:
|
||||||
def test_codex_fallback_uses_max_tokens(self, monkeypatch):
|
def test_codex_fallback_uses_max_tokens(self, monkeypatch):
|
||||||
"""Codex adapter translates max_tokens internally, so we return max_tokens."""
|
"""Codex adapter translates max_tokens internally, so we return max_tokens."""
|
||||||
|
|||||||
@@ -243,6 +243,22 @@ def test_api_mode_respects_explicit_openrouter_provider_over_codex_url(monkeypat
|
|||||||
assert agent.provider == "openrouter"
|
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):
|
def test_build_api_kwargs_codex(monkeypatch):
|
||||||
agent = _build_agent(monkeypatch)
|
agent = _build_agent(monkeypatch)
|
||||||
kwargs = agent._build_api_kwargs(
|
kwargs = agent._build_api_kwargs(
|
||||||
|
|||||||
Reference in New Issue
Block a user