hermes-agent/tests/agent/test_billing_links.py
Brooklyn Nicholson 960d339f86 feat(billing): shared cross-surface out-of-credits signal
Detect a billing wall once (agent/error_classifier → FailoverReason.billing) and
map it to a recovery link + label in one place, then carry that structured
BillingBlock to every surface instead of re-parsing free-form error text per
surface.

- agent/billing_links.py: provider-agnostic slug/host → (label, billing URL)
  table (single source of truth), Nous-aware (is_nous routes to the in-app flow);
  unknown providers degrade to a readable label with no invented URL.
- conversation_loop: both billing exit paths return a billing_block through one
  helper; the guidance message carries the derived URL for every provider.
- gateway forwards billing_block on message.complete (it was dropped).
- @hermes/shared: BillingBlock type shared by desktop + TUI.
2026-07-22 18:08:59 -05:00

103 lines
3.2 KiB
Python

"""Tests for provider-agnostic billing recovery links (agent/billing_links.py).
Behavior/invariant tests — no snapshotting of the exact URL strings beyond the
few that are the whole point of the mapping (the host they must land on).
"""
from __future__ import annotations
from agent.billing_links import (
BillingBlock,
build_billing_block,
is_nous_inference_route,
)
def test_nous_route_by_provider_slug():
block = build_billing_block(provider="nous", base_url="", model="hermes-4")
assert block.is_nous is True
assert block.provider_label == "Nous Portal"
# Nous always resolves an in-app/portal billing URL as a fallback.
assert block.billing_url and "nousresearch.com" in block.billing_url
def test_nous_route_by_base_url_host():
block = build_billing_block(
provider="openai_compatible",
base_url="https://inference-api.nousresearch.com/v1",
model="hermes-4",
)
assert block.is_nous is True
def test_is_nous_inference_route_helper():
assert is_nous_inference_route("nous", "") is True
assert is_nous_inference_route("", "https://inference-api.nousresearch.com/v1") is True
assert is_nous_inference_route("openai", "https://api.openai.com/v1") is False
def test_known_provider_by_slug_resolves_label_and_url():
block = build_billing_block(provider="openai", base_url="", model="gpt-5")
assert block.is_nous is False
assert block.provider_label == "OpenAI"
assert block.billing_url is not None
assert "openai.com" in block.billing_url
def test_openrouter_resolves_credits_page():
block = build_billing_block(
provider="openrouter",
base_url="https://openrouter.ai/api/v1",
model="anthropic/claude",
)
assert block.is_nous is False
assert block.billing_url is not None
assert "openrouter.ai" in block.billing_url
def test_unknown_provider_via_base_url_host_fallback():
# Provider slug is a generic bucket; the host reveals the real upstream.
block = build_billing_block(
provider="custom",
base_url="https://api.deepseek.com/v1",
model="deepseek-chat",
)
assert block.provider_label == "DeepSeek"
assert block.billing_url is not None
assert "deepseek.com" in block.billing_url
def test_unknown_provider_degrades_without_url():
block = build_billing_block(
provider="my_local_llm",
base_url="http://localhost:1234/v1",
model="llama",
)
assert block.is_nous is False
# No invented URL for an unknown provider — but a readable label survives.
assert block.billing_url is None
assert block.provider_label # non-empty, humanized
def test_message_is_carried_through_unchanged():
block = build_billing_block(
provider="openai",
base_url="",
model="gpt-5",
message="You are out of credits.",
)
assert block.message == "You are out of credits."
def test_to_dict_round_trips_all_fields():
block = build_billing_block(provider="openai", base_url="", model="gpt-5")
data = block.to_dict()
assert set(data) == {
"provider",
"provider_label",
"model",
"billing_url",
"is_nous",
"message",
}
assert isinstance(block, BillingBlock)