feat(providers): add Actual Computer inference provider

This commit is contained in:
Justin Bennington 2026-05-15 13:44:39 -04:00 committed by Teknium
parent 241605d1ea
commit a9acb400ba
10 changed files with 417 additions and 5 deletions

View File

@ -509,6 +509,9 @@ _PROVIDER_ALIASES = {
"moonshot-cn": "kimi-coding-cn",
"gmi-cloud": "gmi",
"gmicloud": "gmi",
"actual-computer": "actual",
"actualcomputer": "actual",
"aci": "actual",
"minimax-china": "minimax-cn",
"minimax_cn": "minimax-cn",
"claude": "anthropic",
@ -5802,6 +5805,8 @@ def resolve_provider_client(
return False
if raw_codex:
return False
if provider == "actual":
return True
if api_mode == "codex_responses":
return True
# Auto-detect: api.openai.com + codex model name pattern
@ -6219,6 +6224,22 @@ def resolve_provider_client(
# credential is registered for this provider alias.
if explicit_api_key:
api_key = explicit_api_key.strip() or api_key
raw_base_url = str(creds.get("base_url", "")).strip().rstrip("/") or pconfig.inference_base_url
if explicit_base_url:
raw_base_url = explicit_base_url.strip().rstrip("/")
if provider == "actual":
try:
from hermes_cli.auth import (
ACTUAL_LOCAL_NOAUTH_PLACEHOLDER,
is_actual_local_base_url,
normalize_actual_base_url,
)
raw_base_url = normalize_actual_base_url(raw_base_url)
if not api_key and is_actual_local_base_url(raw_base_url):
api_key = ACTUAL_LOCAL_NOAUTH_PLACEHOLDER
except Exception:
pass
if not api_key:
tried_sources = list(pconfig.api_key_env_vars)
if provider == "copilot":
@ -6228,7 +6249,6 @@ def resolve_provider_client(
provider, ", ".join(tried_sources))
return None, None
raw_base_url = str(creds.get("base_url", "")).strip().rstrip("/") or pconfig.inference_base_url
base_url = _to_openai_base_url(raw_base_url)
# Honour an explicit base_url override from the caller — used when a
# fallback_model entry (or custom_providers lookup) routes through a

View File

@ -97,6 +97,8 @@ DEFAULT_QWEN_BASE_URL = "https://portal.qwen.ai/v1"
DEFAULT_GITHUB_MODELS_BASE_URL = "https://api.githubcopilot.com"
DEFAULT_COPILOT_ACP_BASE_URL = "acp://copilot"
DEFAULT_OLLAMA_CLOUD_BASE_URL = "https://ollama.com/v1"
DEFAULT_ACTUAL_BASE_URL = "https://api.actual.inc/v1"
DEFAULT_ACTUAL_LOCAL_BASE_URL = "http://127.0.0.1:8080/v1"
STEPFUN_STEP_PLAN_INTL_BASE_URL = "https://api.stepfun.ai/step_plan/v1"
STEPFUN_STEP_PLAN_CN_BASE_URL = "https://api.stepfun.com/step_plan/v1"
CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
@ -150,6 +152,39 @@ SERVICE_PROVIDER_NAMES: Dict[str, str] = {
# provider as configured. This sentinel is sent only to LM Studio, never to
# any remote service.
LMSTUDIO_NOAUTH_PLACEHOLDER = "dummy-lm-api-key"
ACTUAL_LOCAL_NOAUTH_PLACEHOLDER = "dummy-actual-local-api-key"
def is_actual_local_base_url(base_url: str) -> bool:
"""Return True for Actual's loopback local API endpoint."""
try:
host = (urlparse(base_url or "").hostname or "").lower().rstrip(".")
except Exception:
return False
return host in {"localhost", "127.0.0.1", "::1", "0.0.0.0"}
def normalize_actual_base_url(base_url: str) -> str:
"""Return Actual's OpenAI-compatible base URL.
Actual hosted inference is exposed at api.actual.inc, while the Actual
client's offline local server binds a loopback host. Both use a /v1 API
surface for Hermes' Responses transport.
"""
url = str(base_url or "").strip().rstrip("/")
if not url:
return DEFAULT_ACTUAL_BASE_URL
try:
parsed = urlparse(url)
host = (parsed.hostname or "").lower().rstrip(".")
path = parsed.path.rstrip("/")
except Exception:
return url
if host == "api.actual.inc" and path in {"", "/"}:
return url + "/v1"
if is_actual_local_base_url(url) and path in {"", "/"}:
return url + "/v1"
return url
# =============================================================================
@ -290,6 +325,14 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
api_key_env_vars=("GMI_API_KEY",),
base_url_env_var="GMI_BASE_URL",
),
"actual": ProviderConfig(
id="actual",
name="Actual Computer",
auth_type="api_key",
inference_base_url=DEFAULT_ACTUAL_BASE_URL,
api_key_env_vars=("ACTUAL_API_KEY",),
base_url_env_var="ACTUAL_BASE_URL",
),
"minimax": ProviderConfig(
id="minimax",
name="MiniMax",
@ -1968,6 +2011,7 @@ def resolve_provider(
"step": "stepfun", "stepfun-coding-plan": "stepfun",
"arcee-ai": "arcee", "arceeai": "arcee",
"gmi-cloud": "gmi", "gmicloud": "gmi",
"actual-computer": "actual", "actualcomputer": "actual", "aci": "actual",
"minimax-china": "minimax-cn", "minimax_cn": "minimax-cn",
"minimax-portal": "minimax-oauth", "minimax-global": "minimax-oauth", "minimax_oauth": "minimax-oauth",
"alibaba_coding": "alibaba-coding-plan", "alibaba-coding": "alibaba-coding-plan",
@ -6937,13 +6981,22 @@ def get_api_key_provider_status(provider_id: str) -> Dict[str, Any]:
else:
base_url = pconfig.inference_base_url
if provider_id == "actual":
base_url = normalize_actual_base_url(base_url)
actual_local_noauth = (
provider_id == "actual"
and not api_key
and is_actual_local_base_url(base_url)
)
return {
"configured": bool(api_key),
"configured": bool(api_key) or actual_local_noauth,
"provider": provider_id,
"name": pconfig.name,
"key_source": key_source,
"key_source": key_source or ("local-offline" if actual_local_noauth else ""),
"base_url": base_url,
"logged_in": bool(api_key), # compat with OAuth status shape
"logged_in": bool(api_key) or actual_local_noauth, # compat with OAuth status shape
}
@ -7149,12 +7202,19 @@ def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]:
if provider_id == "lmstudio":
base_url = _normalize_lmstudio_runtime_base_url(base_url)
if provider_id == "actual":
base_url = normalize_actual_base_url(base_url)
# Last-resort guard: an API-key provider must never hand back an empty
# base URL (a set-but-empty COPILOT_API_BASE_URL or similar env override
# otherwise wedges chat inference — #50252).
if not (isinstance(base_url, str) and base_url.strip()):
base_url = pconfig.inference_base_url
if not api_key and provider_id == "actual" and is_actual_local_base_url(base_url):
api_key = ACTUAL_LOCAL_NOAUTH_PLACEHOLDER
key_source = key_source or "local-offline"
return {
"provider": provider_id,
"api_key": api_key,

View File

@ -52,6 +52,8 @@ _PROVIDER_ENV_HINTS = (
"KIMI_CN_API_KEY",
"GMI_API_KEY",
"FIREWORKS_API_KEY",
"ACTUAL_API_KEY",
"ACTUAL_BASE_URL",
"MINIMAX_API_KEY",
"MINIMAX_CN_API_KEY",
"KILOCODE_API_KEY",

View File

@ -1309,6 +1309,9 @@ _PROVIDER_ALIASES = {
"gmicloud": "gmi",
"fireworks-ai": "fireworks",
"fw": "fireworks",
"actual-computer": "actual",
"actualcomputer": "actual",
"aci": "actual",
"minimax-china": "minimax-cn",
"minimax_cn": "minimax-cn",
"minimax-portal": "minimax-oauth",

View File

@ -205,6 +205,12 @@ HERMES_OVERLAYS: Dict[str, HermesOverlay] = {
extra_env_vars=("FIREWORKS_API_KEY",),
base_url_override="https://api.fireworks.ai/inference/v1",
),
"actual": HermesOverlay(
transport="codex_responses",
extra_env_vars=("ACTUAL_API_KEY", "ACTUAL_BASE_URL"),
base_url_override="https://api.actual.inc/v1",
base_url_env_var="ACTUAL_BASE_URL",
),
"upstage": HermesOverlay(
transport="openai_chat",
extra_env_vars=("UPSTAGE_API_KEY",),
@ -383,6 +389,11 @@ ALIASES: Dict[str, str] = {
# upstage
"solar": "upstage",
# Actual Computer
"actual-computer": "actual",
"actualcomputer": "actual",
"aci": "actual",
# Local server aliases → virtual "local" concept (resolved via user config)
"lmstudio": "lmstudio",
"lm-studio": "lmstudio",
@ -408,6 +419,7 @@ _LABEL_OVERRIDES: Dict[str, str] = {
"xiaomi": "Xiaomi MiMo",
"gmi": "GMI Cloud",
"upstage": "Upstage Solar",
"actual": "Actual Computer",
"tencent-tokenhub": "Tencent TokenHub",
"lmstudio": "LM Studio",
"local": "Local endpoint",

View File

@ -20,6 +20,7 @@ from agent.credential_pool import (
)
from agent.secret_scope import get_secret as _get_secret
from hermes_cli.auth import (
ACTUAL_LOCAL_NOAUTH_PLACEHOLDER,
AuthError,
DEFAULT_CODEX_BASE_URL,
DEFAULT_QWEN_BASE_URL,
@ -36,6 +37,8 @@ from hermes_cli.auth import (
resolve_api_key_provider_credentials,
resolve_external_process_provider_credentials,
has_usable_secret,
is_actual_local_base_url,
normalize_actual_base_url,
)
from hermes_cli.config import (
get_compatible_custom_providers,
@ -131,6 +134,8 @@ def _detect_api_mode_for_url(base_url: str) -> Optional[str]:
# providers.is_official_openai_host for the spoof-rejection contract.
if is_official_openai_host(base_url):
return "codex_responses"
if hostname == "api.actual.inc":
return "codex_responses"
# Direct native Anthropic host: realign with providers.determine_api_mode,
# which already maps this host to anthropic_messages. The exact-hostname
# match rejects lookalike subdomains (api.anthropic.com.attacker.test) and
@ -1607,12 +1612,17 @@ def _resolve_explicit_runtime(
else:
base_url = env_url or pconfig.inference_base_url
if provider == "actual":
base_url = normalize_actual_base_url(base_url)
api_key = explicit_api_key
if not api_key:
creds = resolve_api_key_provider_credentials(provider)
api_key = creds.get("api_key", "")
if not base_url:
base_url = creds.get("base_url", "").rstrip("/")
if provider == "actual":
base_url = normalize_actual_base_url(base_url)
api_mode = "chat_completions"
if provider == "copilot":
@ -1623,6 +1633,8 @@ def _resolve_explicit_runtime(
)
elif provider == "xai":
api_mode = "codex_responses"
elif provider == "actual":
api_mode = "codex_responses"
else:
configured_provider = str(model_cfg.get("provider") or "").strip().lower()
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
@ -1635,6 +1647,9 @@ def _resolve_explicit_runtime(
provider, base_url, target_model or model_cfg.get("default", "")
)
if provider == "actual" and not api_key and is_actual_local_base_url(base_url):
api_key = ACTUAL_LOCAL_NOAUTH_PLACEHOLDER
return {
"provider": provider,
"api_mode": api_mode,
@ -2196,6 +2211,8 @@ def resolve_runtime_provider(
if cfg_provider == provider:
cfg_base_url = (model_cfg.get("base_url") or "").strip().rstrip("/")
base_url = cfg_base_url or creds.get("base_url", "").rstrip("/")
if provider == "actual":
base_url = normalize_actual_base_url(base_url)
api_mode = "chat_completions"
if provider == "copilot":
api_mode = _copilot_runtime_api_mode(
@ -2205,6 +2222,8 @@ def resolve_runtime_provider(
)
elif provider == "xai":
api_mode = "codex_responses"
elif provider == "actual":
api_mode = "codex_responses"
else:
configured_provider = str(model_cfg.get("provider") or "").strip().lower()
# Only honor persisted api_mode when it belongs to the same provider family.
@ -2236,11 +2255,14 @@ def resolve_runtime_provider(
base_url = normalize_opencode_base_url(provider, api_mode, base_url)
if provider == "lmstudio":
base_url = auth_mod._normalize_lmstudio_runtime_base_url(base_url)
api_key = creds.get("api_key", "")
if provider == "actual" and not api_key and is_actual_local_base_url(base_url):
api_key = ACTUAL_LOCAL_NOAUTH_PLACEHOLDER
return {
"provider": provider,
"api_mode": api_mode,
"base_url": base_url,
"api_key": creds.get("api_key", ""),
"api_key": api_key,
"source": creds.get("source", "env"),
"requested_provider": requested_provider,
}

View File

@ -0,0 +1,88 @@
"""Actual Computer provider profile."""
from __future__ import annotations
import json
import logging
import os
from urllib.parse import urlparse
import urllib.request
from providers import register_provider
from providers.base import ProviderProfile, _profile_user_agent
logger = logging.getLogger(__name__)
DEFAULT_ACTUAL_BASE_URL = "https://api.actual.inc/v1"
DEFAULT_ACTUAL_LOCAL_BASE_URL = "http://127.0.0.1:8080/v1"
def _normalize_actual_base_url(base_url: str) -> str:
url = str(base_url or "").strip().rstrip("/")
if not url:
return DEFAULT_ACTUAL_BASE_URL
try:
parsed = urlparse(url)
host = (parsed.hostname or "").lower().rstrip(".")
path = parsed.path.rstrip("/")
except Exception:
return url
if host == "api.actual.inc" and path in {"", "/"}:
return url + "/v1"
if host in {"localhost", "127.0.0.1", "::1", "0.0.0.0"} and path in {"", "/"}:
return url + "/v1"
return url
class ActualProfile(ProviderProfile):
"""Actual Computer provider.
Hosted inference defaults to api.actual.inc. Local inference is exposed by
the Actual client only when it runs in offline mode, so users opt into it by
setting ACTUAL_BASE_URL to the local API URL.
"""
def fetch_models(
self,
*,
api_key: str | None = None,
timeout: float = 8.0,
) -> list[str] | None:
base_url = _normalize_actual_base_url(
os.getenv("ACTUAL_BASE_URL", "").strip() or self.base_url
)
if not base_url:
return None
req = urllib.request.Request(base_url + "/models")
if api_key:
req.add_header("Authorization", f"Bearer {api_key}")
req.add_header("Accept", "application/json")
req.add_header("User-Agent", _profile_user_agent())
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read().decode())
items = data if isinstance(data, list) else data.get("data", [])
return [m["id"] for m in items if isinstance(m, dict) and "id" in m]
except Exception as exc:
logger.debug("fetch_models(actual): %s", exc)
return None
actual = ActualProfile(
name="actual",
aliases=("actual-computer", "actualcomputer", "aci"),
display_name="Actual Computer",
description=(
"Actual Computer - hosted inference via api.actual.inc, or local "
"offline inference via ACTUAL_BASE_URL"
),
signup_url="https://actual.inc",
env_vars=("ACTUAL_API_KEY", "ACTUAL_BASE_URL"),
base_url=DEFAULT_ACTUAL_BASE_URL,
auth_type="api_key",
api_mode="codex_responses",
)
register_provider(actual)

View File

@ -0,0 +1,5 @@
name: actual-provider
kind: model-provider
version: 1.0.0
description: Actual Computer inference
author: Actual Computer

View File

@ -1586,6 +1586,8 @@ class AIAgent:
) -> bool:
"""Return True when this provider/model pair should use Responses API."""
normalized_provider = (provider or "").strip().lower()
if normalized_provider == "actual":
return True
# Nous serves GPT-5.x models via its OpenAI-compatible chat
# completions endpoint; its /v1/responses endpoint returns 404.
if normalized_provider == "nous":

View File

@ -0,0 +1,198 @@
"""Regression tests for the Actual Computer provider wiring."""
from __future__ import annotations
import json
from unittest.mock import patch
from agent.auxiliary_client import _normalize_aux_provider
from hermes_cli import runtime_provider as rp
from hermes_cli.auth import (
ACTUAL_LOCAL_NOAUTH_PLACEHOLDER,
DEFAULT_ACTUAL_BASE_URL,
DEFAULT_ACTUAL_LOCAL_BASE_URL,
get_api_key_provider_status,
normalize_actual_base_url,
resolve_api_key_provider_credentials,
resolve_provider,
)
from hermes_cli.models import normalize_provider as normalize_model_provider
from hermes_cli.models import provider_model_ids
from hermes_cli.providers import determine_api_mode
from hermes_cli.providers import get_label
from hermes_cli.providers import normalize_provider as normalize_overlay_provider
from providers import get_provider_profile
def _clear_actual_env(monkeypatch):
monkeypatch.delenv("ACTUAL_API_KEY", raising=False)
monkeypatch.delenv("ACTUAL_BASE_URL", raising=False)
def test_actual_aliases_and_profile_metadata():
profile = get_provider_profile("actual-computer")
assert profile is not None
assert profile.name == "actual"
assert profile.display_name == "Actual Computer"
assert profile.base_url == DEFAULT_ACTUAL_BASE_URL
assert profile.api_mode == "codex_responses"
assert profile.auth_type == "api_key"
assert profile.env_vars == ("ACTUAL_API_KEY", "ACTUAL_BASE_URL")
assert normalize_overlay_provider("aci") == "actual"
assert normalize_model_provider("actualcomputer") == "actual"
assert resolve_provider("actual-computer") == "actual"
assert _normalize_aux_provider("aci") == "actual"
assert get_label("actual") == "Actual Computer"
assert determine_api_mode("actual", "https://api.actual.inc") == "codex_responses"
def test_actual_base_url_normalization():
assert normalize_actual_base_url("https://api.actual.inc") == DEFAULT_ACTUAL_BASE_URL
assert normalize_actual_base_url("https://api.actual.inc/v1") == DEFAULT_ACTUAL_BASE_URL
assert normalize_actual_base_url("http://127.0.0.1:8080") == DEFAULT_ACTUAL_LOCAL_BASE_URL
assert normalize_actual_base_url("http://127.0.0.1:8080/v1") == DEFAULT_ACTUAL_LOCAL_BASE_URL
assert normalize_actual_base_url("http://localhost:8080/") == "http://localhost:8080/v1"
def test_actual_credentials_default_to_hosted_api(monkeypatch):
_clear_actual_env(monkeypatch)
monkeypatch.setenv("ACTUAL_API_KEY", "actual-test-key")
creds = resolve_api_key_provider_credentials("actual")
assert creds["provider"] == "actual"
assert creds["api_key"] == "actual-test-key"
assert creds["base_url"] == DEFAULT_ACTUAL_BASE_URL
def test_actual_local_loopback_allows_no_auth(monkeypatch):
_clear_actual_env(monkeypatch)
monkeypatch.setenv("ACTUAL_BASE_URL", "http://127.0.0.1:8080")
creds = resolve_api_key_provider_credentials("actual")
status = get_api_key_provider_status("actual")
assert creds["api_key"] == ACTUAL_LOCAL_NOAUTH_PLACEHOLDER
assert creds["base_url"] == DEFAULT_ACTUAL_LOCAL_BASE_URL
assert creds["source"] == "local-offline"
assert status["configured"] is True
assert status["logged_in"] is True
assert status["key_source"] == "local-offline"
assert status["base_url"] == DEFAULT_ACTUAL_LOCAL_BASE_URL
def test_actual_runtime_uses_hosted_default(monkeypatch):
_clear_actual_env(monkeypatch)
monkeypatch.setenv("ACTUAL_API_KEY", "actual-test-key")
monkeypatch.setattr(
rp,
"_get_model_config",
lambda: {"provider": "actual", "default": "actual/test-model"},
)
resolved = rp.resolve_runtime_provider(requested="actual")
assert resolved["provider"] == "actual"
assert resolved["api_mode"] == "codex_responses"
assert resolved["api_key"] == "actual-test-key"
assert resolved["base_url"] == DEFAULT_ACTUAL_BASE_URL
def test_actual_runtime_uses_local_env_without_key(monkeypatch):
_clear_actual_env(monkeypatch)
monkeypatch.setenv("ACTUAL_BASE_URL", "http://127.0.0.1:8080")
monkeypatch.setattr(
rp,
"_get_model_config",
lambda: {"provider": "actual", "default": "actual/local-model"},
)
resolved = rp.resolve_runtime_provider(requested="actual")
assert resolved["provider"] == "actual"
assert resolved["api_mode"] == "codex_responses"
assert resolved["api_key"] == ACTUAL_LOCAL_NOAUTH_PLACEHOLDER
assert resolved["base_url"] == DEFAULT_ACTUAL_LOCAL_BASE_URL
def test_actual_runtime_uses_local_config_without_key(monkeypatch):
_clear_actual_env(monkeypatch)
monkeypatch.setattr(
rp,
"_get_model_config",
lambda: {
"provider": "actual",
"base_url": "http://127.0.0.1:8080",
"default": "actual/local-model",
},
)
resolved = rp.resolve_runtime_provider(requested="actual")
assert resolved["provider"] == "actual"
assert resolved["api_mode"] == "codex_responses"
assert resolved["api_key"] == ACTUAL_LOCAL_NOAUTH_PLACEHOLDER
assert resolved["base_url"] == DEFAULT_ACTUAL_LOCAL_BASE_URL
def test_actual_runtime_normalizes_explicit_hosted_base_url(monkeypatch):
_clear_actual_env(monkeypatch)
monkeypatch.setattr(
rp,
"_get_model_config",
lambda: {"provider": "actual", "default": "actual/test-model"},
)
resolved = rp.resolve_runtime_provider(
requested="actual",
explicit_api_key="actual-test-key",
explicit_base_url="https://api.actual.inc",
)
assert resolved["provider"] == "actual"
assert resolved["api_mode"] == "codex_responses"
assert resolved["api_key"] == "actual-test-key"
assert resolved["base_url"] == DEFAULT_ACTUAL_BASE_URL
assert resolved["source"] == "explicit"
def test_actual_profile_fetch_models_normalizes_env_base_url(monkeypatch):
_clear_actual_env(monkeypatch)
monkeypatch.setenv("ACTUAL_BASE_URL", "http://127.0.0.1:8080")
profile = get_provider_profile("actual")
seen = {}
class _Response:
def __enter__(self):
return self
def __exit__(self, *args):
return None
def read(self):
return json.dumps({"data": [{"id": "actual/local-model"}]}).encode()
def _urlopen(req, timeout=0):
seen["url"] = req.full_url
seen["auth"] = req.get_header("Authorization")
seen["timeout"] = timeout
return _Response()
monkeypatch.setattr("urllib.request.urlopen", _urlopen)
assert profile.fetch_models(api_key=None, timeout=1.5) == ["actual/local-model"]
assert seen["url"] == DEFAULT_ACTUAL_LOCAL_BASE_URL + "/models"
assert seen["auth"] is None
assert seen["timeout"] == 1.5
def test_actual_provider_model_ids_use_local_profile_catalog(monkeypatch):
_clear_actual_env(monkeypatch)
monkeypatch.setenv("ACTUAL_BASE_URL", "http://127.0.0.1:8080")
profile = get_provider_profile("actual")
with patch.object(profile, "fetch_models", return_value=["actual/local-model"]) as fetch:
assert provider_model_ids("actual") == ["actual/local-model"]
fetch.assert_called_once_with(api_key=ACTUAL_LOCAL_NOAUTH_PLACEHOLDER)