fix(providers): block Actual at Responses send sites (E-1047)
This commit is contained in:
@@ -1449,6 +1449,15 @@ class _CodexCompletionsAdapter:
|
||||
return resp_kwargs, model, timeout
|
||||
|
||||
def create(self, **kwargs) -> Any:
|
||||
from hermes_cli.providers import is_actual_route
|
||||
|
||||
if is_actual_route(
|
||||
getattr(self._client, "_hermes_aux_effective_provider", ""),
|
||||
str(getattr(self._client, "base_url", "") or ""),
|
||||
):
|
||||
raise ValueError(
|
||||
"Actual requests require Chat Completions; refusing to call /responses."
|
||||
)
|
||||
# Low-level ``responses.create(stream=True)`` and assemble the final response ourselves
|
||||
# from ``response.output_item.done``: the high-level ``responses.stream()`` rebuilds from
|
||||
# ``response.completed.response.output``, which Codex returns as ``null`` (SDK crash).
|
||||
@@ -4497,11 +4506,22 @@ def _wrap_transport(req: _ResolveRequest, client_obj: Any, final_model_str: str,
|
||||
explicit api_mode — api.openai.com + codex model. Anthropic (Messages): ``api_mode=anthropic_messages``,
|
||||
any ``/anthropic`` suffix, ``api.kimi.com/coding``, or ``api.anthropic.com``."""
|
||||
if _is_actual_auxiliary_route(req, base_url_str):
|
||||
return client_obj._real_client if isinstance(client_obj, CodexAuxiliaryClient) else client_obj
|
||||
needs_codex = not (isinstance(client_obj, CodexAuxiliaryClient) or req.raw_codex) and (
|
||||
client = (
|
||||
client_obj._real_client
|
||||
if isinstance(client_obj, CodexAuxiliaryClient)
|
||||
else client_obj
|
||||
)
|
||||
client._hermes_aux_effective_provider = "actual"
|
||||
return client
|
||||
needs_codex = not (
|
||||
isinstance(client_obj, CodexAuxiliaryClient) or req.raw_codex
|
||||
) and (
|
||||
req.api_mode == "codex_responses"
|
||||
or (not req.api_mode and base_url_hostname(base_url_str) == "api.openai.com"
|
||||
and "codex" in (final_model_str or "").lower())
|
||||
or (
|
||||
not req.api_mode
|
||||
and base_url_hostname(base_url_str) == "api.openai.com"
|
||||
and "codex" in (final_model_str or "").lower()
|
||||
)
|
||||
)
|
||||
if needs_codex:
|
||||
logger.debug("resolve_provider_client: wrapping client in CodexAuxiliaryClient "
|
||||
|
||||
@@ -918,6 +918,15 @@ def run_codex_stream(agent, api_kwargs: dict, client: Any = None, on_first_delta
|
||||
return bool(agent._interrupt_requested)
|
||||
|
||||
def _open_codex_stream(next_api_kwargs: dict[str, Any]):
|
||||
from hermes_cli.providers import is_actual_route
|
||||
|
||||
if is_actual_route(
|
||||
getattr(agent, "provider", ""),
|
||||
str(getattr(active_client, "base_url", "") or ""),
|
||||
):
|
||||
raise ValueError(
|
||||
"Actual requests require Chat Completions; refusing to call /responses."
|
||||
)
|
||||
stream_kwargs = _sanitize_consumer_codex_request(agent, next_api_kwargs)
|
||||
stream_kwargs["stream"] = True
|
||||
return active_client.responses.create(**_bypass_sdk_request_transform(stream_kwargs))
|
||||
|
||||
@@ -348,6 +348,87 @@ def test_actual_runtime_transitions_reach_chat_completions(
|
||||
agent.client.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"provider,hosted",
|
||||
[
|
||||
("actual", False),
|
||||
("aci", False),
|
||||
("actual-computer", False),
|
||||
("actualcomputer", False),
|
||||
("custom", True),
|
||||
("custom:actual-relay", True),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("send_site", ["main", "auxiliary", "async_auxiliary"])
|
||||
def test_actual_rejects_forced_responses_before_http(
|
||||
tmp_path, monkeypatch, actual_endpoint, provider, hosted, send_site
|
||||
):
|
||||
from agent.auxiliary_client import (
|
||||
AsyncCodexAuxiliaryClient,
|
||||
CodexAuxiliaryClient,
|
||||
resolve_provider_client,
|
||||
)
|
||||
from run_agent import AIAgent
|
||||
|
||||
base_url, requests = actual_endpoint
|
||||
if hosted:
|
||||
base_url = base_url.replace("127.0.0.1", "api.actual.inc")
|
||||
base_url += "/v1"
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
config = {
|
||||
"model": {"provider": provider, "base_url": base_url, "default": "test-model"},
|
||||
"providers": {
|
||||
"actual-relay": {"base_url": base_url, "transport": "codex_responses"}
|
||||
},
|
||||
}
|
||||
(tmp_path / "config.yaml").write_text(yaml.safe_dump(config), encoding="utf-8")
|
||||
if send_site == "main":
|
||||
agent = AIAgent(
|
||||
provider=provider,
|
||||
base_url=base_url,
|
||||
api_key="actual-test-key",
|
||||
model="test-model",
|
||||
enabled_toolsets=[],
|
||||
quiet_mode=True,
|
||||
skip_context_files=True,
|
||||
skip_memory=True,
|
||||
save_trajectories=False,
|
||||
)
|
||||
client = agent.client
|
||||
agent.api_mode = "codex_responses"
|
||||
|
||||
def force_responses():
|
||||
return agent._run_codex_stream(
|
||||
{"model": "test-model", "input": "Reply briefly."}, client=client
|
||||
)
|
||||
|
||||
else:
|
||||
client, model = resolve_provider_client(
|
||||
provider,
|
||||
model="test-model",
|
||||
explicit_base_url=base_url,
|
||||
explicit_api_key="actual-test-key",
|
||||
api_mode="codex_responses",
|
||||
)
|
||||
wrapper = CodexAuxiliaryClient(client, model)
|
||||
if send_site == "async_auxiliary":
|
||||
wrapper = AsyncCodexAuxiliaryClient(wrapper)
|
||||
|
||||
def force_responses():
|
||||
result = wrapper.chat.completions.create(
|
||||
model=model, messages=[{"role": "user", "content": "Reply briefly."}]
|
||||
)
|
||||
return asyncio.run(result) if send_site == "async_auxiliary" else result
|
||||
|
||||
try:
|
||||
initial_requests = list(requests)
|
||||
with pytest.raises(ValueError, match="Actual.*Chat Completions"):
|
||||
force_responses()
|
||||
assert requests == initial_requests
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["actual", "aci", "custom:actual-relay"])
|
||||
@pytest.mark.parametrize("async_mode", [False, True])
|
||||
def test_actual_auxiliary_fallback_reaches_chat_completions(
|
||||
|
||||
Reference in New Issue
Block a user