Files
hermes-agent/tests/run_agent/test_opencode_free_client_headers.py
Teknium ca06b87689 feat: opencode-free is fully keyless — no env var, no account, anonymous wire
Reworks the salvaged OpenCode Free provider to match the tier's real
auth contract (verified live 2026-08-21): the Zen relay serves free
models ANONYMOUSLY and 401s any unrecognized bearer, so the provider now
declares no credentials at all and routes every model through the shared
keyless machinery from the Ox Alpha fix (empty Authorization default
header overriding the SDK bearer).

On top of the salvaged base:
- auth.py: no api_key_env_vars; drop the keyed-auth special case
- runtime_provider.py: restore the plain fail-closed path (opencode-free
  never reaches it — the keyless runtime resolves first)
- models.py: opencode-free joins the opencode family (prefix stripping,
  Zen endpoint routing incl. muse->responses); keyless predicate extended
  with unsuffixed free slugs (big-pickle); free runtime pins EVERY
  opencode-free model keyless; curated catalog replaces the models.dev
  cost==0 filter (it lags reality: deepseek-v4-flash-free stayed 'free'
  there after its promo ended and the relay began 401ing it — delisted)
- agent_runtime_helpers.py: replace the httpx transport-sharing auth-strip
  wrapper with the shared header policy (no proxy-mount loss)
- model_setup_flows.py: skip the API-key prompt for opencode-free
- plugin profile: keyless headers, no env vars
- .env.example + providers.md: keyless docs (no OPENCODE_FREE_API_KEY)
- tests rewritten to the keyless contract, incl. catalog-membership
  invariant (every curated model must satisfy the keyless predicate)

E2E: full AIAgent turns with zero keys complete on x-preview-f-free via
provider opencode-free and alias 'free', incl. a real terminal tool
round-trip; muse routes to /v1/responses; picker lists 8 keyless models.
2026-08-21 00:24:32 -07:00

97 lines
3.4 KiB
Python

"""Regression guard: opencode-free client keyless header handling.
OpenCode's free tier at ``https://opencode.ai/zen/v1`` is served ANONYMOUSLY:
requests with no recognizable Authorization bearer succeed, while any bearer
the relay doesn't recognize — placeholders included — is rejected with 401
"Invalid API key" (verified live 2026-08-21).
The client therefore must ship an EMPTY ``Authorization`` default header for
every opencode-free build, which overrides the OpenAI SDK's always-injected
``Authorization: Bearer <api_key>`` so no credential-shaped value ever
reaches the wire.
"""
from unittest.mock import MagicMock, patch
from agent.agent_runtime_helpers import create_openai_client
ZEN_V1 = "https://opencode.ai/zen/v1"
class _FakeAgent:
def __init__(self, api_key):
self.provider = "opencode-free"
self.base_url = ZEN_V1
self.api_key = api_key
self.model = "x-preview-f-free"
self.api_mode = "chat_completions"
def _client_log_context(self):
return {}
def _build_keepalive_http_client(self, base_url, verify=True):
return None
def _zen_call_headers(mock_openai):
matching = [
c for c in mock_openai.call_args_list
if c.kwargs.get("base_url") == ZEN_V1
]
assert matching, "OpenAI was never constructed with the zen base_url"
return dict(matching[-1].kwargs.get("default_headers") or {})
@patch("run_agent.OpenAI")
def test_opencode_free_blanks_authorization_header(mock_openai):
"""Whatever api_key value reaches the client build (placeholder, stale
key, empty), the Authorization default header must be blanked so the
SDK's Bearer never hits the wire."""
mock_openai.return_value = MagicMock()
for key in ("opencode-zen-free-keyless", "no-key-required", "", "sk-stale"):
mock_openai.reset_mock()
create_openai_client(
_FakeAgent(api_key=key),
{"api_key": key, "base_url": ZEN_V1},
reason="test",
shared=False,
)
headers = _zen_call_headers(mock_openai)
assert headers.get("Authorization") == "", (
f"opencode-free with api_key={key!r} must blank Authorization; "
f"got {headers!r}"
)
@patch("run_agent.OpenAI")
def test_opencode_free_sends_hermes_attribution(mock_openai):
"""Keyless requests still identify as Hermes (attribution headers match
the opencode zen/go profiles)."""
mock_openai.return_value = MagicMock()
create_openai_client(
_FakeAgent(api_key="opencode-zen-free-keyless"),
{"api_key": "opencode-zen-free-keyless", "base_url": ZEN_V1},
reason="test",
shared=False,
)
headers = _zen_call_headers(mock_openai)
assert headers.get("X-Title") == "Hermes Agent"
assert str(headers.get("User-Agent", "")).startswith("HermesAgent/")
@patch("run_agent.OpenAI")
def test_other_providers_unaffected(mock_openai):
"""The opencode-free header policy must not leak to other providers."""
mock_openai.return_value = MagicMock()
agent = _FakeAgent(api_key="sk-real")
agent.provider = "opencode-zen"
create_openai_client(
agent,
{"api_key": "sk-real", "base_url": ZEN_V1},
reason="test",
shared=False,
)
headers = _zen_call_headers(mock_openai)
assert "Authorization" not in headers, (
"opencode-zen (keyed) must not have its Authorization header blanked"
)