Files
devplacepy/tests/unit/services/openai_gateway/routing.py
T
retoorandClaude Sonnet 5 569f1dcc64 Add OpenCode Zen support, model health/stats dashboard, and gateway fallback fixes
AI gateway:
- Add a generic, admin-selectable `client_profile` field on gateway_providers
  (e.g. "opencode") so a provider needing special request headers (OpenCode
  Zen's client-identity spoofing) is configured like any other provider, not
  hardcoded by name.
- Track per-(provider, model) reliability/speed/latency health in memory,
  seeded from the existing gateway_usage_ledger at startup - purely
  observational, never influences routing.
- New Stats tab on /admin/gateway: request volume, latency, per-model
  breakdowns, and reliability weight, charted with a vendored Chart.js and
  devplace's own theme tokens.
- Record which model a failed request actually fell back to
  (fallback_used_route), surfaced in the Recent Failures table.
- Stop excluding context_length errors from fallback, and skip a primary
  attempt outright when its known context window is already too small for
  the estimated request size, going straight to the fallback.
- gateway_usage_ledger's provider/fallback_used_route columns and indexes
  are ensured centrally in database/schema.py's init_db(), the single point
  of truth for this table's schema.
- Non-OpenAI upstream routing and client-model passthrough; trust only the
  upstream's own X-Gateway-Model header for served-model attribution.

Devii agent:
- Fix a real lockup: plan/verify tools could be individually disabled via
  the admin tool toggles while still being required by the protocol gate,
  permanently bricking any task that needed tools. They can no longer be
  disabled, and the gate now also checks the tool is actually offered.
- Fix compaction being silently calibrated for a 1M-token model while
  running a much smaller one: context budget is now percentage-based and
  the summarizer's own request is sized to fit the real model.
- Give a specific, actionable retry message when plan()'s own arguments get
  cut off by the output limit, and tighten its schema to discourage
  overlong plans.

Other:
- Backup service: offload completed backups to a remote Hetzner Storage Box.
- Container manager: fix orphan blob leaks from sync races, add a two-phase
  plan/execute `system prune` CLI command.
- Admin gateway UI: replace the JS-rendered model/provider tables with
  server-rendered forms and pages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhmEkvutuwtzFVcLbTrhdo
2026-09-09 07:38:24 +02:00

572 lines
19 KiB
Python

# retoor <retoor@molodetz.nl>
import pytest
from pydantic import ValidationError
from devplacepy.services.openai_gateway import routing as r
from devplacepy.services.openai_gateway.usage import pricing_from_cfg
from tests.conftest import run_async
class _FakeModelsResponse:
def __init__(self, status_code, payload=None):
self.status_code = status_code
self._payload = payload
def json(self):
return self._payload
class _FakeModelsClient:
def __init__(self, status_code, payload=None, error=None):
self.status_code = status_code
self.payload = payload
self.error = error
self.requested_url = None
self.headers = None
async def __aenter__(self):
return self
async def __aexit__(self, *exc_info):
return False
async def get(self, url):
self.requested_url = url
if self.error is not None:
raise self.error
return _FakeModelsResponse(self.status_code, self.payload)
def _patch_stealth_client(monkeypatch, fake_client):
def _factory(**kwargs):
fake_client.headers = kwargs.get("headers")
return fake_client
monkeypatch.setattr("devplacepy.stealth.stealth_async_client", _factory)
def _cleanup(providers, models):
for name in providers:
r.provider_store.remove(name)
for source in models:
r.model_store.remove(source)
def test_no_routes_pass_through(local_db):
assert r.chat_overlay("ghost-model-xyz", {}) is None
assert r.embed_overlay("ghost-embed-xyz", {}) is None
assert r.image_overlay("ghost-image-xyz", {}) is None
def test_chat_route_overlay_and_economy(local_db):
r.provider_store.set(
r.ProviderIn(
name="utor",
base_url="https://up.example/v1/chat/completions",
api_key="k1",
)
)
r.model_store.set(
r.ModelRouteIn(
source_model="ut-chat",
provider="utor",
target_model="vendor/big",
kind="chat",
context_window=64000,
price_cache_hit_per_m=1.0,
price_cache_miss_per_m=2.0,
price_output_per_m=8.0,
)
)
try:
overlay = r.chat_overlay("ut-chat", {"gateway_model_context_map": "{}"})
assert overlay["gateway_force_model"] is True
assert overlay["gateway_model"] == "vendor/big"
assert overlay["gateway_upstream_url"] == "https://up.example/v1/chat/completions"
assert overlay["gateway_api_key"] == "k1"
assert overlay["gateway_price_output_per_m"] == 8.0
assert overlay["gateway_model_context_map"]["vendor/big"] == 64000
pricing = pricing_from_cfg(overlay)
assert pricing.chat_output_per_m == 8.0
assert pricing.chat_cache_miss_per_m == 2.0
finally:
_cleanup(["utor"], ["ut-chat"])
def test_resolve_fallback_returns_none_without_a_configured_fallback(local_db):
r.model_store.set(r.ModelRouteIn(source_model="fb-primary", target_model="v/a"))
try:
assert r.resolve_fallback("fb-primary", "chat") is None
finally:
_cleanup([], ["fb-primary"])
def test_resolve_fallback_returns_none_for_an_unknown_source(local_db):
assert r.resolve_fallback("ghost-fb-source", "chat") is None
def test_resolve_fallback_rejects_self_reference(local_db):
with pytest.raises(ValidationError):
r.ModelRouteIn(
source_model="fb-self", target_model="v/a", fallback_model="fb-self"
)
def test_resolve_fallback_returns_the_configured_route(local_db):
r.model_store.set(
r.ModelRouteIn(
source_model="fb-backup", target_model="v/backup", kind="chat"
)
)
r.model_store.set(
r.ModelRouteIn(
source_model="fb-primary",
target_model="v/primary",
kind="chat",
fallback_model="fb-backup",
)
)
try:
fallback = r.resolve_fallback("fb-primary", "chat")
assert fallback is not None
assert fallback.source_model == "fb-backup"
assert fallback.target_model == "v/backup"
finally:
_cleanup([], ["fb-primary", "fb-backup"])
def test_resolve_fallback_ignores_an_inactive_fallback_route(local_db):
r.model_store.set(
r.ModelRouteIn(
source_model="fb-backup-off",
target_model="v/backup",
kind="chat",
is_active=False,
)
)
r.model_store.set(
r.ModelRouteIn(
source_model="fb-primary-2",
target_model="v/primary",
kind="chat",
fallback_model="fb-backup-off",
)
)
try:
assert r.resolve_fallback("fb-primary-2", "chat") is None
finally:
_cleanup([], ["fb-primary-2", "fb-backup-off"])
def test_resolve_fallback_ignores_a_fallback_of_a_different_kind(local_db):
r.model_store.set(
r.ModelRouteIn(
source_model="fb-backup-embed", target_model="v/e", kind="embed"
)
)
r.model_store.set(
r.ModelRouteIn(
source_model="fb-primary-3",
target_model="v/primary",
kind="chat",
fallback_model="fb-backup-embed",
)
)
try:
assert r.resolve_fallback("fb-primary-3", "chat") is None
finally:
_cleanup([], ["fb-primary-3", "fb-backup-embed"])
def test_vision_merge_overlay(local_db):
r.provider_store.set(
r.ProviderIn(
name="utvis",
base_url="https://vis.example/v1/chat/completions",
api_key="vk",
)
)
r.model_store.set(
r.ModelRouteIn(
source_model="ut-vision",
provider="utvis",
target_model="vendor/vlm",
kind="chat",
vision_model="vendor/describe",
price_input_per_m=0.5,
)
)
try:
overlay = r.chat_overlay("ut-vision", {})
assert overlay["gateway_vision_enabled"] is True
assert overlay["gateway_vision_model"] == "vendor/describe"
assert overlay["gateway_vision_url"] == "https://vis.example/v1/chat/completions"
assert overlay["gateway_vision_key"] == "vk"
assert overlay["gateway_vision_price_input_per_m"] == 0.5
finally:
_cleanup(["utvis"], ["ut-vision"])
def test_image_route_and_kind_isolation(local_db):
r.provider_store.set(
r.ProviderIn(
name="utimg",
base_url="https://img.example/v1/chat/completions",
api_key="ik",
)
)
r.model_store.set(
r.ModelRouteIn(
source_model="ut-image",
provider="utimg",
target_model="vendor/flux",
kind="image",
price_input_per_m=0.05,
)
)
try:
overlay = r.image_overlay("ut-image", {})
assert overlay["gateway_image_model"] == "vendor/flux"
assert overlay["gateway_image_url"] == "https://img.example/v1/images"
assert overlay["gateway_image_key"] == "ik"
assert overlay["gateway_image_price_per_call"] == 0.05
assert r.chat_overlay("ut-image", {}) is None
assert r.embed_overlay("ut-image", {}) is None
pricing = pricing_from_cfg(overlay)
assert pricing.image_per_call == 0.05
finally:
_cleanup(["utimg"], ["ut-image"])
def test_embed_route_and_kind_isolation(local_db):
r.provider_store.set(
r.ProviderIn(
name="utemb",
base_url="https://emb.example/v1/chat/completions",
api_key="ek",
)
)
r.model_store.set(
r.ModelRouteIn(
source_model="ut-embed",
provider="utemb",
target_model="vendor/embed",
kind="embed",
price_input_per_m=0.13,
)
)
try:
overlay = r.embed_overlay("ut-embed", {})
assert overlay["gateway_embed_model"] == "vendor/embed"
assert overlay["gateway_embed_url"] == "https://emb.example/v1/embeddings"
assert overlay["gateway_embed_key"] == "ek"
assert overlay["gateway_embed_price_input_per_m"] == 0.13
assert r.chat_overlay("ut-embed", {}) is None
finally:
_cleanup(["utemb"], ["ut-embed"])
def test_inactive_route_ignored(local_db):
r.model_store.set(
r.ModelRouteIn(
source_model="ut-off",
target_model="vendor/x",
kind="chat",
is_active=False,
)
)
try:
assert r.chat_overlay("ut-off", {}) is None
finally:
_cleanup([], ["ut-off"])
def test_blank_provider_uses_default_upstream(local_db):
r.model_store.set(
r.ModelRouteIn(
source_model="ut-default",
target_model="vendor/def",
kind="chat",
price_output_per_m=3.0,
)
)
try:
overlay = r.chat_overlay("ut-default", {})
assert overlay["gateway_model"] == "vendor/def"
assert "gateway_upstream_url" not in overlay
assert "gateway_api_key" not in overlay
assert overlay["gateway_price_output_per_m"] == 3.0
finally:
_cleanup([], ["ut-default"])
def test_provider_with_a_client_profile_gets_extra_headers(local_db):
r.provider_store.set(
r.ProviderIn(
name="zen",
base_url="https://opencode.ai/zen/v1/chat/completions",
api_key="public",
client_profile="opencode",
)
)
r.model_store.set(
r.ModelRouteIn(source_model="ut-zen", provider="zen", target_model="kimi-k3")
)
try:
overlay = r.chat_overlay("ut-zen", {})
headers = overlay["gateway_extra_request_headers"]
assert headers["x-opencode-client"]
assert "User-Agent" in headers
finally:
_cleanup(["zen"], ["ut-zen"])
def test_provider_without_a_client_profile_gets_no_extra_headers(local_db):
r.provider_store.set(
r.ProviderIn(name="plainprov", base_url="https://up.example/v1/chat/completions")
)
r.model_store.set(
r.ModelRouteIn(source_model="ut-plain", provider="plainprov", target_model="vendor/x")
)
try:
overlay = r.chat_overlay("ut-plain", {})
assert "gateway_extra_request_headers" not in overlay
finally:
_cleanup(["plainprov"], ["ut-plain"])
def test_client_profile_rejects_unknown_value():
with pytest.raises(ValidationError):
r.ProviderIn(name="badprofile", client_profile="not-a-real-profile")
def test_tier2_and_off_peak_fields_propagate_through_overlay(local_db):
r.model_store.set(
r.ModelRouteIn(
source_model="ut-tiered",
target_model="vendor/tiered",
kind="chat",
price_cache_hit_per_m=0.0028,
price_cache_miss_per_m=0.14,
price_output_per_m=0.28,
context_tier_threshold_tokens=128_000,
price_output_per_m_tier2=0.56,
off_peak_start_minute=990,
off_peak_end_minute=30,
off_peak_discount_pct=25.0,
)
)
try:
overlay = r.chat_overlay("ut-tiered", {})
assert overlay["gateway_context_tier_threshold_tokens"] == 128_000
assert overlay["gateway_price_output_per_m_tier2"] == 0.56
assert overlay["gateway_price_cache_hit_per_m_tier2"] is None
assert overlay["gateway_off_peak_start_minute"] == 990
assert overlay["gateway_off_peak_end_minute"] == 30
assert overlay["gateway_off_peak_discount_pct"] == 25.0
pricing = pricing_from_cfg(overlay)
assert pricing.context_tier_threshold_tokens == 128_000
assert pricing.chat_output_per_m_tier2 == 0.56
assert pricing.chat_cache_hit_per_m_tier2 is None
assert pricing.off_peak_start_minute == 990
finally:
_cleanup([], ["ut-tiered"])
def test_unrouted_request_never_gains_tier_or_off_peak_keys(local_db):
assert r.chat_overlay("ghost-model-untiered", {}) is None
pricing = pricing_from_cfg({})
assert pricing.context_tier_threshold_tokens == 0
assert pricing.off_peak_start_minute is None
assert pricing.off_peak_end_minute is None
assert pricing.chat_output_per_m_tier2 is None
def test_off_peak_window_requires_both_start_and_end():
with pytest.raises(ValidationError):
r.ModelRouteIn(
source_model="ut-bad-window",
target_model="vendor/x",
off_peak_start_minute=60,
)
with pytest.raises(ValidationError):
r.ModelRouteIn(
source_model="ut-bad-window",
target_model="vendor/x",
off_peak_end_minute=120,
)
def test_seed_default_image_routes_is_idempotent(local_db):
r.model_store.remove(r.IMAGE_SOURCE_MODEL)
r.seed_default_image_routes()
route = r.model_store.get(r.IMAGE_SOURCE_MODEL)
assert route is not None
assert route.kind == "image"
assert route.target_model == r.FLUX_TARGET_MODEL
r.seed_default_image_routes()
assert r.model_store.get(r.IMAGE_SOURCE_MODEL).target_model == r.FLUX_TARGET_MODEL
def test_seed_default_deepseek_routes_is_idempotent_and_preserves_customization(
local_db,
):
r.seed_default_deepseek_routes()
flash = r.model_store.get("deepseek-v4-flash")
pro = r.model_store.get("deepseek-v4-pro")
assert flash is not None and pro is not None
assert flash.price_cache_hit_per_m == 0.0028
assert flash.price_cache_miss_per_m == 0.14
assert flash.price_output_per_m == 0.28
assert flash.context_window == 1_048_576
assert pro.price_cache_hit_per_m == 0.003625
assert pro.price_cache_miss_per_m == 0.435
assert pro.price_output_per_m == 0.87
r.model_store.set(
r.ModelRouteIn(
source_model="deepseek-v4-pro",
target_model="deepseek-v4-pro",
price_output_per_m=1.23,
)
)
try:
r.seed_default_deepseek_routes()
assert r.model_store.get("deepseek-v4-pro").price_output_per_m == 1.23
finally:
r.model_store.set(
r.ModelRouteIn(
source_model="deepseek-v4-pro",
target_model="deepseek-v4-pro",
price_cache_hit_per_m=0.003625,
price_cache_miss_per_m=0.435,
price_output_per_m=0.87,
context_window=1_048_576,
)
)
def test_seed_publishes_molodetz_aliases(local_db):
r.seed_default_deepseek_routes()
try:
molodetz = r.model_store.resolve("molodetz", "chat")
molodetz_pro = r.model_store.resolve("molodetz-pro", "chat")
assert molodetz is not None
assert molodetz.target_model == "deepseek-v4-flash"
assert molodetz_pro is not None
assert molodetz_pro.target_model == "deepseek-v4-pro"
sources = {row["source_model"] for row in r.model_store.list()}
assert {"molodetz", "molodetz-pro"} <= sources
finally:
r.model_store.remove("molodetz")
r.model_store.remove("molodetz-pro")
def test_models_url_from_base_swaps_chat_completions():
assert (
r._models_url_from_base("https://x.example/v1/chat/completions")
== "https://x.example/v1/models"
)
def test_models_url_from_base_appends_when_no_chat_completions_suffix():
assert r._models_url_from_base("https://x.example/v1") == "https://x.example/v1/models"
assert r._models_url_from_base("https://x.example/v1/") == "https://x.example/v1/models"
def test_models_url_from_base_blank_is_blank():
assert r._models_url_from_base("") == ""
assert r._models_url_from_base(" ") == ""
def test_fetch_provider_models_returns_ids_on_success(local_db, monkeypatch):
r.provider_store.set(
r.ProviderIn(
name="probeprov",
base_url="https://x.example/v1/chat/completions",
api_key="sk-probe",
)
)
fake_client = _FakeModelsClient(200, {"data": [{"id": "vendor/a"}, {"id": "vendor/b"}]})
_patch_stealth_client(monkeypatch, fake_client)
try:
models = run_async(r.fetch_provider_models("probeprov"))
assert models == ["vendor/a", "vendor/b"]
assert fake_client.requested_url == "https://x.example/v1/models"
assert fake_client.headers == {"authorization": "Bearer sk-probe"}
finally:
r.provider_store.remove("probeprov")
def test_fetch_provider_models_uses_default_provider_when_blank(local_db, monkeypatch):
monkeypatch.setattr(
r,
"_default_provider_credentials",
lambda: ("https://default.example/v1/chat/completions", "sk-default"),
)
fake_client = _FakeModelsClient(200, {"data": [{"id": "default/model"}]})
_patch_stealth_client(monkeypatch, fake_client)
models = run_async(r.fetch_provider_models(""))
assert models == ["default/model"]
assert fake_client.requested_url == "https://default.example/v1/models"
def test_fetch_provider_models_none_for_unknown_provider(local_db):
assert run_async(r.fetch_provider_models("no-such-provider")) is None
def test_fetch_provider_models_none_on_non_200(local_db, monkeypatch):
r.provider_store.set(
r.ProviderIn(name="probeprov404", base_url="https://x.example/v1/chat/completions")
)
fake_client = _FakeModelsClient(404, {})
_patch_stealth_client(monkeypatch, fake_client)
try:
assert run_async(r.fetch_provider_models("probeprov404")) is None
finally:
r.provider_store.remove("probeprov404")
def test_fetch_provider_models_none_on_malformed_payload(local_db, monkeypatch):
r.provider_store.set(
r.ProviderIn(name="probeprovbad", base_url="https://x.example/v1/chat/completions")
)
fake_client = _FakeModelsClient(200, {"not_data": []})
_patch_stealth_client(monkeypatch, fake_client)
try:
assert run_async(r.fetch_provider_models("probeprovbad")) is None
finally:
r.provider_store.remove("probeprovbad")
def test_fetch_provider_models_none_on_empty_list(local_db, monkeypatch):
r.provider_store.set(
r.ProviderIn(name="probeprovempty", base_url="https://x.example/v1/chat/completions")
)
fake_client = _FakeModelsClient(200, {"data": []})
_patch_stealth_client(monkeypatch, fake_client)
try:
assert run_async(r.fetch_provider_models("probeprovempty")) is None
finally:
r.provider_store.remove("probeprovempty")
def test_fetch_provider_models_none_on_network_error(local_db, monkeypatch):
r.provider_store.set(
r.ProviderIn(name="probeprovdown", base_url="https://x.example/v1/chat/completions")
)
fake_client = _FakeModelsClient(200, error=RuntimeError("connection refused"))
_patch_stealth_client(monkeypatch, fake_client)
try:
assert run_async(r.fetch_provider_models("probeprovdown")) is None
finally:
r.provider_store.remove("probeprovdown")
def test_fetch_provider_models_none_when_provider_has_no_base_url(local_db):
r.provider_store.set(r.ProviderIn(name="probeprovnourl", base_url=""))
try:
assert run_async(r.fetch_provider_models("probeprovnourl")) is None
finally:
r.provider_store.remove("probeprovnourl")