diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index 313e7cd865..0fd556f840 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -1260,6 +1260,7 @@ def build_api_kwargs(agent, api_messages: list, tools_for_api: list | None = Non max_tokens=agent.max_tokens, timeout=agent._resolved_api_call_timeout(), request_overrides=agent.request_overrides, + provider=getattr(agent, "provider", None), is_github_responses=is_github_responses, is_codex_backend=is_codex_backend, is_xai_responses=is_xai_responses, diff --git a/agent/transports/codex.py b/agent/transports/codex.py index 499238b513..a60e5291b5 100644 --- a/agent/transports/codex.py +++ b/agent/transports/codex.py @@ -297,6 +297,12 @@ class ResponsesApiTransport(ProviderTransport): if params.get("is_xai_responses", False): # xAI Responses tops out at high; keep generic stronger values usable. _effort_clamp.update({"xhigh": "high", "max": "high", "ultra": "high"}) + if (params.get("provider") or "").strip().lower() == "actual": + # Actual Computer relays to SGLang/vLLM backends that accept only + # none/low/medium/high/max for reasoning effort — a forwarded + # xhigh/ultra fails with a wrapped HTTP 400 ("Expecting value: + # line 1 column 1"). Clamp Hermes' wider set to the supported one. + _effort_clamp.update({"xhigh": "high", "ultra": "max"}) reasoning_effort = _effort_clamp.get(reasoning_effort, reasoning_effort) response_tools = _responses_tools(tools) diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index 24659cc03b..41c493383b 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -3334,6 +3334,22 @@ OPTIONAL_ENV_VARS = { "category": "provider", "advanced": True, }, + "ACTUAL_API_KEY": { + "description": "Actual Computer inference key (ac_...)", + "prompt": "Actual Computer inference key", + "url": "https://actual.inc/user/keys", + "password": True, + "category": "provider", + "advanced": True, + }, + "ACTUAL_BASE_URL": { + "description": "Actual Computer base URL override (set to http://127.0.0.1:8080 for the local offline daemon)", + "prompt": "Actual Computer base URL (leave empty for hosted relay)", + "url": None, + "password": False, + "category": "provider", + "advanced": True, + }, "FIREWORKS_API_KEY": { "description": "Fireworks AI API key", "prompt": "Fireworks AI API key", diff --git a/tests/hermes_cli/test_actual_provider.py b/tests/hermes_cli/test_actual_provider.py index 37d50ad8ac..2e9116c08e 100644 --- a/tests/hermes_cli/test_actual_provider.py +++ b/tests/hermes_cli/test_actual_provider.py @@ -199,3 +199,54 @@ def test_actual_provider_model_ids_use_local_profile_catalog(monkeypatch): api_key=ACTUAL_LOCAL_NOAUTH_PLACEHOLDER, base_url=DEFAULT_ACTUAL_LOCAL_BASE_URL, ) + + +def test_actual_codex_transport_clamps_reasoning_effort(): + """Actual's SGLang/vLLM backends only accept none/low/medium/high/max. + + A global xhigh/ultra reasoning_effort must be clamped before the wire, + otherwise every request fails with a wrapped HTTP 400. Regression for + the field-reported reasoning_effort trap. + """ + from agent.transports.codex import ResponsesApiTransport + + t = ResponsesApiTransport() + for requested, expected in (("xhigh", "high"), ("ultra", "max"), ("high", "high")): + kwargs = t.build_kwargs( + model="glm-5.2-nvfp4", + messages=[{"role": "user", "content": "hi"}], + tools=None, + provider="actual", + reasoning_config={"effort": requested}, + base_url=DEFAULT_ACTUAL_BASE_URL, + ) + assert kwargs["reasoning"]["effort"] == expected, requested + + # Other providers keep the wider values untouched on the generic path. + kwargs = t.build_kwargs( + model="some-model", + messages=[{"role": "user", "content": "hi"}], + tools=None, + provider="openai-codex", + reasoning_config={"effort": "xhigh"}, + ) + assert kwargs["reasoning"]["effort"] == "xhigh" + + +def test_actual_runtime_config_local_base_url_without_key(monkeypatch): + """Config-driven loopback base_url (not just env) reaches the no-auth path.""" + _clear_actual_env(monkeypatch) + monkeypatch.setattr( + rp, + "_get_model_config", + lambda: { + "provider": "actual", + "base_url": "http://localhost:8080", + "default": "actual/local-model", + }, + ) + + resolved = rp.resolve_runtime_provider(requested="actual") + + assert resolved["api_key"] == ACTUAL_LOCAL_NOAUTH_PLACEHOLDER + assert resolved["api_mode"] == "codex_responses"