Three defects found in review of the first head (@ehz0ah): * The mid-request hop read the PERSISTED provider from disk. A live `/model xai-oauth` session over `model.provider: auto` therefore still reached the discovery chain and billed Nous. _try_payment_fallback now takes the route's main_runtime snapshot; disk is the fallback only when no session runtime exists. * After a configured fallback was quarantined mid-request (401, refresh failed), the second pass went straight to the discovery chain, which the new gate refuses — so later CONFIGURED entries never ran and the original error was re-raised. The second pass now re-walks the task chain and main chain (the quarantined entry is unhealthy and skipped) before discovery. * current_provider_owns_vendor dropped ids detect_vendor could not classify, so Bedrock's 15-id catalog (14 unclassified `us.anthropic…`) looked exclusively DeepSeek and `/model deepseek-v4-pro` stuck on Bedrock. An unclassified id now counts as evidence of a multi-vendor catalog: ownership requires every id to classify to the one vendor.
40 lines
1.8 KiB
Python
40 lines
1.8 KiB
Python
"""A first-party session never re-routes its own vendor's model when the live catalog cannot vouch.
|
|
|
|
Second half of the Astra incident (#97487): the live-catalog guard only helps when the fetch
|
|
succeeds. A transient Codex outage, or a static fallback that lags an early-access rollout, left
|
|
``/model gpt-6-astra`` on ``openai-codex`` walking the ladder to OpenRouter (which relists every
|
|
vendor) and silently rebuilding the session on a metered aggregator.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from hermes_cli import models, models_detect
|
|
|
|
|
|
@pytest.fixture
|
|
def ladder_would_hijack(monkeypatch):
|
|
"""Live catalog unavailable; OpenRouter lists everything; the user holds an OpenRouter key."""
|
|
monkeypatch.setattr(models, "cached_provider_model_ids", lambda provider, **_: [])
|
|
monkeypatch.setattr(models, "_find_openrouter_slug", lambda name: f"vendor/{name}")
|
|
monkeypatch.setattr(models_detect, "provider_has_credentials", lambda p: p == "openrouter")
|
|
|
|
|
|
@pytest.mark.parametrize("provider,model", [
|
|
("openai-codex", "gpt-6-astra"),
|
|
("xai-oauth", "grok-5-preview"),
|
|
("anthropic", "claude-opus-5-early"),
|
|
])
|
|
def test_own_vendor_id_stays_when_live_catalog_is_empty(ladder_would_hijack, provider, model):
|
|
assert models.detect_provider_for_model(model, provider) is None
|
|
|
|
|
|
@pytest.mark.parametrize("provider,model", [
|
|
("openai-codex", "claude-opus-4.7"), # other vendor's id on a single-vendor provider
|
|
("bedrock", "deepseek-v4-pro"), # multi-vendor catalog whose non-deepseek ids the
|
|
# classifier cannot place: never "exclusively deepseek"
|
|
])
|
|
def test_non_owned_id_still_remaps_to_keyed_aggregator(ladder_would_hijack, provider, model):
|
|
assert models.detect_provider_for_model(model, provider) == ("openrouter", f"vendor/{model}")
|