feat(providers): add Actual Computer inference provider
This commit is contained in:
parent
241605d1ea
commit
a9acb400ba
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
name: actual-provider
|
||||
kind: model-provider
|
||||
version: 1.0.0
|
||||
description: Actual Computer inference
|
||||
author: Actual Computer
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue