Portal serves anthropic/* on two routes. The native /v1/messages wire, which
Hermes has used since 02d5e23085, re-writes the previous turn's prompt cache
on 14-20% of consecutive calls in concurrent tool loops; the chat/completions
route does not. Measured 2026-09-06, 20 concurrent sessions x 6 tool calls on
Fable 5.1, same account, same hour, same first-party pin:
Nous /v1/messages 180 pairs, 25 stuck (13.9%); 3 earlier 40-session
runs 15.0 / 17.3 / 20.2%; unchanged by the pin
Nous /v1/chat/completions 320 pairs (2 runs), 0 stuck
OpenRouter direct, pinned 161 pairs, 0 stuck
"stuck" = cache_read on call k+1 equals cache_read on call k instead of
call k's prompt size: the last write was not visible, the turn (~28K) was
written twice. Every stuck pair had byte-identical system, tools and
per-message shas, so it is not client-side mutation. At $10-20/M for
cache writes that is 15-20% of a fan-out's write bill.
The cause is inside the portal's native route (every element of its
outgoing request reproduces clean from outside; NousResearch/api#227
carries the diagnostics). Until it is fixed, anthropic/* rides
chat/completions. nous.anthropic_wire: native opts back in. Cost of
chat: prior-turn thinking travels as OpenAI-style reasoning fields, and
cache_control scopes are translated by the portal's adapter.
Tests: new test_nous_anthropic_wire_default.py reads the knob through the
real config loader in a temp HERMES_HOME and through
resolve_runtime_provider (both settings); the existing native-wire
contract suite selects native via an autouse fixture so the wire keeps
working for the flip-back. Live: a real AIAgent on this branch,
provider=nous + anthropic/claude-fable-5.1, dispatches to
chat/completions with the session_id intact.
55 lines
2.8 KiB
Python
55 lines
2.8 KiB
Python
"""``nous.anthropic_wire`` selects the Portal route for ``anthropic/*``: ``chat`` (default) rides
|
|
/v1/chat/completions, ``native`` rides /v1/messages. Read through the real config loader against a
|
|
temp HERMES_HOME (the loader is keyed on config path + mtime, so a fresh home is a fresh read), and
|
|
through ``resolve_runtime_provider`` so the api_mode a live agent gets is what is asserted."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from hermes_cli import providers as _providers
|
|
from hermes_cli import runtime_provider as rp
|
|
|
|
PORTAL = "https://inference-api.nousresearch.com/v1"
|
|
|
|
|
|
def _cfg(tmp_path, body: str, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
(tmp_path / "config.yaml").write_text(body, encoding="utf-8")
|
|
|
|
|
|
def _portal_creds(monkeypatch):
|
|
monkeypatch.setattr(rp, "resolve_nous_runtime_credentials",
|
|
lambda **kw: {"base_url": PORTAL, "api_key": "jwt", "source": "portal", "expires_at": None})
|
|
monkeypatch.setattr(rp, "_get_model_config", lambda: {"provider": "nous"})
|
|
|
|
|
|
def test_default_is_chat_for_anthropic_and_unchanged_for_everything_else(tmp_path, monkeypatch):
|
|
_cfg(tmp_path, "model:\n default: anthropic/claude-fable-5.1\n", monkeypatch)
|
|
assert _providers.nous_api_mode("anthropic/claude-fable-5.1") == "chat_completions"
|
|
assert _providers.nous_api_mode("openai/gpt-5.6-sol") == "chat_completions"
|
|
assert _providers.determine_api_mode("nous", PORTAL, "anthropic/claude-fable-5.1") == "chat_completions"
|
|
|
|
|
|
def test_native_opt_in_restores_the_messages_wire_for_anthropic_only(tmp_path, monkeypatch):
|
|
_cfg(tmp_path, "nous:\n anthropic_wire: native\n", monkeypatch)
|
|
assert _providers.nous_api_mode("anthropic/claude-fable-5.1") == "anthropic_messages"
|
|
assert _providers.nous_api_mode("openai/gpt-5.6-sol") == "chat_completions"
|
|
|
|
|
|
@pytest.mark.parametrize("raw", ["''", "CHAT", "messages", "true", "1"])
|
|
def test_anything_but_native_reads_as_chat(tmp_path, monkeypatch, raw):
|
|
_cfg(tmp_path, f"nous:\n anthropic_wire: {raw}\n", monkeypatch)
|
|
assert _providers.nous_api_mode("anthropic/claude-fable-5.1") == "chat_completions"
|
|
|
|
|
|
def test_runtime_resolution_hands_a_live_agent_the_selected_wire(tmp_path, monkeypatch):
|
|
"""The path an AIAgent takes: provider=nous + anthropic model -> api_mode, both settings."""
|
|
_portal_creds(monkeypatch)
|
|
_cfg(tmp_path, "model:\n provider: nous\n", monkeypatch)
|
|
resolved = rp.resolve_runtime_provider(requested="nous", target_model="anthropic/claude-fable-5.1")
|
|
assert (resolved["api_mode"], resolved["base_url"]) == ("chat_completions", PORTAL)
|
|
|
|
_cfg(tmp_path, "model:\n provider: nous\nnous:\n anthropic_wire: native\n", monkeypatch)
|
|
resolved = rp.resolve_runtime_provider(requested="nous", target_model="anthropic/claude-fable-5.1")
|
|
assert resolved["api_mode"] == "anthropic_messages"
|