[eric] agents: leading-_ -> p_/public in providers+core+tools (registry/pricing/seq_log/mcp_preflight/openai_passthrough/web/ssrf_guard); promote cross-module fns public, fix monkeypatch string refs

This commit is contained in:
ciregenz
2026-06-23 18:39:11 -07:00
parent 751bb77032
commit 394dcd7492
17 changed files with 111 additions and 111 deletions
+4 -4
View File
@@ -487,7 +487,7 @@ async def probe_model(body: dict):
from backend.apps.agents.providers.registry import (
resolve_model_id_for_sdk,
get_api_type,
_find_builtin_model,
find_builtin_model,
_NINEROUTER_MODEL_PREFIXES,
)
from backend.apps.settings.settings import load_settings
@@ -495,7 +495,7 @@ async def probe_model(body: dict):
settings = load_settings()
api_type = get_api_type(short_name)
resolved = resolve_model_id_for_sdk(short_name, settings)
entry = _find_builtin_model(short_name) or {}
entry = find_builtin_model(short_name) or {}
route = entry.get("route")
connection_mode = getattr(settings, "connection_mode", "own_key")
@@ -774,14 +774,14 @@ async def list_models():
result[f"OpenRouter · {pretty}"] = entries
# Custom OpenAI-compatible providers (Ollama Cloud, Together, etc); addressed via custom/<slug>/<model_id>.
from backend.apps.agents.providers.registry import _custom_provider_slug_for_lookup
from backend.apps.agents.providers.registry import custom_provider_slug_for_lookup
for cp in (getattr(settings, "custom_providers", None) or []):
cp_name = (getattr(cp, "name", "") or "").strip()
cp_base_url = (getattr(cp, "base_url", "") or "").strip()
cp_models = getattr(cp, "models", None) or []
if not cp_name or not cp_base_url or not cp_models:
continue
slug = _custom_provider_slug_for_lookup(cp_name)
slug = custom_provider_slug_for_lookup(cp_name)
entries: list[dict] = []
for m in cp_models:
bare = (m.get("value") or m.get("id") or "").strip()
+2 -2
View File
@@ -445,7 +445,7 @@ async def run_browser_agent(
from backend.apps.settings.settings import load_settings
from backend.apps.settings.credentials import get_anthropic_client_for_model
from backend.apps.agents.providers.registry import (
_find_builtin_model,
find_builtin_model,
resolve_model_id_for_sdk,
resolve_aux_model,
)
@@ -457,7 +457,7 @@ async def run_browser_agent(
# type, etc.) through 9Router's claude→openai translator is UNVERIFIED ,
# if translation is poor, the user should manually switch this session
# back to Claude in the model picker.
if _find_builtin_model(model) is not None:
if find_builtin_model(model) is not None:
api_model = resolve_model_id_for_sdk(model, browser_settings)
else:
# Unknown model string; fall back to whatever aux model is available
+9 -9
View File
@@ -79,7 +79,7 @@ _PATH_LIKE = re.compile(r"^[./~]|/[\w\-]+/|\.[a-zA-Z]{1,5}\b")
_SHELL_PREFIX = re.compile(r"^\s*[\$!/]")
def _is_obviously_local(prompt: str) -> bool:
def p_is_obviously_local(prompt: str) -> bool:
"""True for prompts that obviously can't benefit from MCP (very short, shell-ish, single path)."""
s = prompt.strip()
if len(s) < 8:
@@ -100,21 +100,21 @@ async def run_preflight(prompt: str, timeout_s: float = 8.0, task_id: str | None
if not prompt or not prompt.strip():
return default
if _is_obviously_local(prompt):
if p_is_obviously_local(prompt):
return default
try:
settings = load_settings()
available = _build_available_shortlist(settings)
available = p_build_available_shortlist(settings)
result = await asyncio.wait_for(
_call_classifier(settings, prompt, available, task_id),
p_call_classifier(settings, prompt, available, task_id),
timeout=timeout_s,
)
# Re-validate ids against the curated shortlist so hallucinations can't reach the frontend.
valid_ids = {e["id"] for e in CURATED_SHORTLIST}
result["suggestions"] = [
_decorate(s, available) for s in result.get("suggestions", [])
p_decorate(s, available) for s in result.get("suggestions", [])
if isinstance(s, dict) and s.get("id") in valid_ids
]
result["suggestions"] = [s for s in result["suggestions"] if s is not None]
@@ -131,7 +131,7 @@ async def run_preflight(prompt: str, timeout_s: float = 8.0, task_id: str | None
return default
def _build_available_shortlist(settings) -> list[CuratedEntry]:
def p_build_available_shortlist(settings) -> list[CuratedEntry]:
"""Curated entries that are NOT currently enabled and NOT dismissed."""
try:
enabled_names = {t.name for t in load_all_tools() if getattr(t, "enabled", False)}
@@ -158,7 +158,7 @@ def offer_for_gated_server(server_name: str, settings) -> CuratedEntry | None:
# ("Google Workspace"). Match on the slug of both sides so neither form is a load-bearing string.
slug = _sanitize_server_name(server_name)
entry = next(
(e for e in _build_available_shortlist(settings) if _sanitize_server_name(e["id"]) == slug),
(e for e in p_build_available_shortlist(settings) if _sanitize_server_name(e["id"]) == slug),
None,
)
if entry is None:
@@ -166,7 +166,7 @@ def offer_for_gated_server(server_name: str, settings) -> CuratedEntry | None:
return {"id": entry["id"], "title": entry["title"], "description": entry["description"], "reason": ""}
def _decorate(llm_suggestion: dict, available: list[CuratedEntry]) -> dict | None:
def p_decorate(llm_suggestion: dict, available: list[CuratedEntry]) -> dict | None:
"""Expand an LLM-returned {id, reason} into the full frontend shape."""
entry = next((e for e in available if e["id"] == llm_suggestion["id"]), None)
if entry is None:
@@ -179,7 +179,7 @@ def _decorate(llm_suggestion: dict, available: list[CuratedEntry]) -> dict | Non
}
async def _call_classifier(settings, prompt: str, available: list[CuratedEntry], task_id: str | None = None) -> dict:
async def p_call_classifier(settings, prompt: str, available: list[CuratedEntry], task_id: str | None = None) -> dict:
"""One aux-model call, returns validated JSON {is_vague, suggestions}."""
aux_model, _base = await resolve_aux_model(settings, preferred_tier="haiku")
client = get_anthropic_client_for_model(settings, aux_model)
@@ -31,7 +31,7 @@ _HOP_HEADERS = {
}
def _is_gpt5(model: str) -> bool:
def p_is_gpt5(model: str) -> bool:
m = (model or "").strip().lower()
if not m:
return False
@@ -52,7 +52,7 @@ _GPT5_UNSUPPORTED_PARAMS = (
)
def _scrub_gpt5_params(body: bytes) -> bytes:
def scrub_gpt5_params(body: bytes) -> bytes:
"""For GPT-5: rename max_tokens→max_completion_tokens and drop the sampling
params the reasoning models reject. Bytes in/out, never raises."""
if not body:
@@ -61,7 +61,7 @@ def _scrub_gpt5_params(body: bytes) -> bytes:
parsed = json.loads(body)
except Exception:
return body
if not isinstance(parsed, dict) or not _is_gpt5(str(parsed.get("model") or "")):
if not isinstance(parsed, dict) or not p_is_gpt5(str(parsed.get("model") or "")):
return body
mutated = False
if "max_tokens" in parsed:
@@ -85,7 +85,7 @@ def _scrub_gpt5_params(body: bytes) -> bytes:
)
async def passthrough(rest: str, request: Request):
body = await request.body()
body = _scrub_gpt5_params(body)
body = scrub_gpt5_params(body)
forward_headers: dict[str, str] = {}
for k, v in request.headers.items():
+2 -2
View File
@@ -143,7 +143,7 @@ class SeqLogStore:
pass
def _default_persist_dir() -> Optional[str]:
def p_default_persist_dir() -> Optional[str]:
try:
from backend.config.paths import DATA_ROOT
return os.path.join(DATA_ROOT, "agents", "terminal_events")
@@ -151,4 +151,4 @@ def _default_persist_dir() -> Optional[str]:
return None
seq_log = SeqLogStore(persist_dir=_default_persist_dir())
seq_log = SeqLogStore(persist_dir=p_default_persist_dir())
@@ -267,7 +267,7 @@ class RunSupportMixin:
return
try:
from backend.apps.agents.providers.registry import _find_builtin_model as find_builtin_model
from backend.apps.agents.providers.registry import find_builtin_model as find_builtin_model
entry = find_builtin_model(session.model)
if not entry or entry.get("api") != "anthropic":
return # other providers handle caching automatically
+2 -2
View File
@@ -28,7 +28,7 @@ async def configure_provider_env(
from backend.apps.agents.providers.registry import _NINEROUTER_MODEL_PREFIXES as NINEROUTER_MODEL_PREFIXES
resolved_is_9router = isinstance(resolved_model, str) and resolved_model.startswith(NINEROUTER_MODEL_PREFIXES)
from backend.apps.agents.providers.registry import _find_builtin_model as find_builtin_model
from backend.apps.agents.providers.registry import find_builtin_model as find_builtin_model
model_entry = find_builtin_model(session.model)
is_pinned_api_route = (
model_entry is not None
@@ -81,7 +81,7 @@ async def configure_provider_env(
"providers need 9Router to translate the Anthropic "
"protocol, install Node.js and restart the app."
)
from backend.apps.agents.providers.registry import _find_custom_provider_for_value as find_custom_provider_for_value
from backend.apps.agents.providers.registry import find_custom_provider_for_value as find_custom_provider_for_value
cp = find_custom_provider_for_value(global_settings, session.model)
env = {
"ANTHROPIC_API_KEY": "9router",
+2 -2
View File
@@ -185,7 +185,7 @@ MODEL_TIERS: dict[str, tuple[int, int, int]] = {
}
def _heuristic_tiers(label: str, output_cost_per_1m: float, reasoning: bool) -> tuple[int, int, int]:
def heuristic_tiers(label: str, output_cost_per_1m: float, reasoning: bool) -> tuple[int, int, int]:
"""Fallback tier scoring for models not in MODEL_TIERS. Tries to
extract a parameter count from the label (8B/70B/235B/etc.) and
use that as a stronger size signal than cost alone, since open-
@@ -310,7 +310,7 @@ def compute_tiers(
if c in MODEL_TIERS:
return MODEL_TIERS[c]
return _heuristic_tiers(label, output_cost_per_1m, reasoning)
return heuristic_tiers(label, output_cost_per_1m, reasoning)
def compute_billing_kind(
+9 -9
View File
@@ -21,7 +21,7 @@ from .openrouter import (
from .pricing import (
compute_billing_kind,
compute_tiers,
_heuristic_tiers,
heuristic_tiers,
)
from .thinking import thinking_params_for
@@ -155,7 +155,7 @@ BUILTIN_MODELS: dict[str, list[dict[str, Any]]] = {
_CUSTOM_VALUE_PREFIX = "custom/"
def _custom_provider_slug_for_lookup(name: str) -> str:
def custom_provider_slug_for_lookup(name: str) -> str:
"""Mirror nine_router._custom_provider_slug; duplicated here to avoid
importing from nine_router (circular: nine_router imports from settings)."""
import re
@@ -163,7 +163,7 @@ def _custom_provider_slug_for_lookup(name: str) -> str:
return s or "custom"
def _find_custom_provider_for_value(settings, value: str):
def find_custom_provider_for_value(settings, value: str):
"""Look up the CustomProvider whose slug matches the slug encoded in a
`custom/<slug>/<model_id>` picker value. Returns None if no match."""
if not isinstance(value, str) or not value.startswith(_CUSTOM_VALUE_PREFIX):
@@ -173,12 +173,12 @@ def _find_custom_provider_for_value(settings, value: str):
if not slug:
return None
for cp in getattr(settings, "custom_providers", None) or []:
if _custom_provider_slug_for_lookup(getattr(cp, "name", "")) == slug:
if custom_provider_slug_for_lookup(getattr(cp, "name", "")) == slug:
return cp
return None
def _find_builtin_model(short_name: str) -> dict | None:
def find_builtin_model(short_name: str) -> dict | None:
"""Look up a model entry by its short `value`.
OpenRouter entries (prefixed `or:<vendor>/<model>`) and custom-provider
@@ -223,11 +223,11 @@ def _find_builtin_model(short_name: str) -> dict | None:
def get_api_type(short_name: str) -> str:
entry = _find_builtin_model(short_name)
entry = find_builtin_model(short_name)
return (entry or {}).get("api", "anthropic")
def _antigravity_connected() -> bool:
def p_antigravity_connected() -> bool:
"""True if a live Antigravity OAuth lane exists in 9Router. Synchronous
probe (this resolver is sync) with a tight timeout; any hiccup reads as
'no' so a slow/absent 9Router never blocks model resolution for long."""
@@ -249,7 +249,7 @@ def _antigravity_connected() -> bool:
def resolve_model_id_for_sdk(short_name: str, settings: AppSettings) -> str:
"""Short model name → id string for ClaudeAgentOptions."""
entry = _find_builtin_model(short_name)
entry = find_builtin_model(short_name)
if entry is None:
return short_name
if entry.get("route") == "cc":
@@ -294,7 +294,7 @@ def resolve_model_id_for_sdk(short_name: str, settings: AppSettings) -> str:
if isinstance(rid, str) and rid.startswith("gc/"):
suffix = rid[len("gc/"):]
ag_suffix = _ANTIGRAVITY_MAP.get(suffix)
if ag_suffix and _antigravity_connected():
if ag_suffix and p_antigravity_connected():
return "ag/" + ag_suffix
if getattr(settings, "google_api_key", None):
return "gemini/" + suffix
+7 -7
View File
@@ -25,9 +25,9 @@ from typing import Any, Literal, TYPE_CHECKING
from backend.apps.agents.providers.registry import (
_CUSTOM_VALUE_PREFIX,
_custom_provider_slug_for_lookup,
_find_builtin_model,
_find_custom_provider_for_value,
custom_provider_slug_for_lookup,
find_builtin_model,
find_custom_provider_for_value,
get_api_type,
)
@@ -73,9 +73,9 @@ class PoweringCredential:
def _custom_slug_for_model(model_value: str, settings: AppSettings) -> str | None:
cp = _find_custom_provider_for_value(settings, model_value)
cp = find_custom_provider_for_value(settings, model_value)
if cp is not None:
return _custom_provider_slug_for_lookup(getattr(cp, "name", ""))
return custom_provider_slug_for_lookup(getattr(cp, "name", ""))
# Fall back to the slug encoded in the picker value itself.
if isinstance(model_value, str) and model_value.startswith(_CUSTOM_VALUE_PREFIX):
slug = model_value[len(_CUSTOM_VALUE_PREFIX):].partition("/")[0]
@@ -89,7 +89,7 @@ def resolve_powering_credential(model_value: str, settings: AppSettings) -> Powe
`model_value` is the session's short model name (e.g. "opus-4-8", "sonnet-api",
"custom/lmstudio/llama"), exactly what AgentSession.model holds.
"""
entry = _find_builtin_model(model_value)
entry = find_builtin_model(model_value)
api = (entry or {}).get("api") or get_api_type(model_value)
route = (entry or {}).get("route")
mode = getattr(settings, "connection_mode", "own_key")
@@ -166,7 +166,7 @@ def _powering_custom_slug_present(new_providers: Any, slug: str) -> bool:
return False
for cp in new_providers:
name = cp.get("name") if isinstance(cp, dict) else getattr(cp, "name", None)
if name and _custom_provider_slug_for_lookup(name) == slug:
if name and custom_provider_slug_for_lookup(name) == slug:
return True
return False
+5 -5
View File
@@ -46,7 +46,7 @@ _BLOCKED_V6_NETS = [
]
async def _resolve_host_async(host: str) -> list[str]:
async def p_resolve_host_async(host: str) -> list[str]:
"""Resolve host to all IPs (v4 + v6) without blocking the event loop."""
loop = asyncio.get_event_loop()
try:
@@ -56,7 +56,7 @@ async def _resolve_host_async(host: str) -> list[str]:
return list({info[4][0] for info in infos})
def _is_forbidden_ip(ip_str: str) -> bool:
def p_is_forbidden_ip(ip_str: str) -> bool:
"""True iff this IP is in a blocked range. Loopback is allowed (see module docstring)."""
try:
ip = ipaddress.ip_address(ip_str)
@@ -88,17 +88,17 @@ async def assert_safe_url(url: str) -> str:
try:
ipaddress.ip_address(host)
if _is_forbidden_ip(host):
if p_is_forbidden_ip(host):
raise SSRFBlocked(f"URL host {host} is in a blocked range.")
return url
except ValueError:
pass
resolved = await _resolve_host_async(host)
resolved = await p_resolve_host_async(host)
if not resolved:
raise SSRFBlocked(f"No DNS records for {host}.")
for ip in resolved:
if _is_forbidden_ip(ip):
if p_is_forbidden_ip(ip):
raise SSRFBlocked(f"Host {host} resolves to forbidden IP {ip}.")
return url
+7 -7
View File
@@ -66,7 +66,7 @@ def should_register_web_mcp(
no Claude connection), and a subscription-route Claude model on a non-Pro account (the
built-in WebSearch's aux haiku call 401s). Pro pool is deliberately NOT counted for a
non-Claude primary: spending it on WebSearch would drain the user's Claude turns."""
from backend.apps.agents.providers.registry import _find_builtin_model as find_builtin_model
from backend.apps.agents.providers.registry import find_builtin_model as find_builtin_model
m = router_model_id if isinstance(router_model_id, str) else ""
primary_is_claude = m.startswith("cc/") or (
@@ -93,13 +93,13 @@ def should_register_web_mcp(
return not has_anthropic_path
def _truncate(text: str, limit: int = _MAX_OUTPUT_BYTES) -> str:
def p_truncate(text: str, limit: int = _MAX_OUTPUT_BYTES) -> str:
if len(text) > limit:
return text[:limit] + "\n... (output truncated)"
return text
def _strip_html(raw_html: str) -> str:
def p_strip_html(raw_html: str) -> str:
"""Naive but effective HTML to plain-text conversion."""
text = re.sub(r"<(script|style)[^>]*>.*?</\1>", "", raw_html, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(r"<[^>]+>", " ", text)
@@ -206,14 +206,14 @@ class WebSearchTool(BaseTool):
if "/y.js?" in raw_url or "ad_provider=" in raw_url or "ad_domain=" in raw_url:
continue
title = _strip_html(link_match.group(2)).strip()
title = p_strip_html(link_match.group(2)).strip()
snippet_match = re.search(
r'<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>(.*?)</a>',
block,
flags=re.DOTALL,
)
snippet = _strip_html(snippet_match.group(1)).strip() if snippet_match else ""
snippet = p_strip_html(snippet_match.group(1)).strip() if snippet_match else ""
# DDG wraps URLs in a redirect; extract the real one.
real_url_match = re.search(r"uddg=([^&]+)", raw_url)
@@ -291,11 +291,11 @@ class WebFetchTool(BaseTool):
except Exception:
text = None
if not text:
text = _strip_html(resp.text)
text = p_strip_html(resp.text)
else:
text = resp.text
text = _truncate(text)
text = p_truncate(text)
header = f"Contents of {url}:"
if prompt:
+1 -1
View File
@@ -71,7 +71,7 @@ def _install(monkeypatch, primary, aux):
import backend.apps.agents.agent_manager as am_mod
monkeypatch.setattr(settings_mod, "load_settings", lambda: {"fake": True}, raising=True)
monkeypatch.setattr(reg_mod, "_find_builtin_model", lambda m: object(), raising=True)
monkeypatch.setattr(reg_mod, "find_builtin_model", lambda m: object(), raising=True)
monkeypatch.setattr(reg_mod, "resolve_model_id_for_sdk", lambda m, s: "primary-x", raising=True)
async def _aux_resolve(s, preferred_tier="haiku"):
+3 -3
View File
@@ -80,7 +80,7 @@ def _stub_classifier(is_vague, ids):
def test_preflight_default_suppresses_suggestions_on_concrete_prompt(monkeypatch):
# Launch path: a concrete (non-vague) prompt must NOT interrupt with a card.
monkeypatch.setattr(pf, "load_all_tools", lambda: [])
monkeypatch.setattr(pf, "_call_classifier", _stub_classifier(False, ["Google Workspace"]))
monkeypatch.setattr(pf, "p_call_classifier", _stub_classifier(False, ["Google Workspace"]))
out = asyncio.run(run_preflight("refactor foo.ts to use the new client", timeout_s=5))
assert out["suggestions"] == []
@@ -89,7 +89,7 @@ def test_preflight_require_vague_false_keeps_suggestions(monkeypatch):
# MCPSearch path: the agent already proved it needs an integration, so keep the suggestion
# even though the prompt is concrete (is_vague False).
monkeypatch.setattr(pf, "load_all_tools", lambda: [])
monkeypatch.setattr(pf, "_call_classifier", _stub_classifier(False, ["Google Workspace"]))
monkeypatch.setattr(pf, "p_call_classifier", _stub_classifier(False, ["Google Workspace"]))
out = asyncio.run(run_preflight("check my unread emails", timeout_s=5, require_vague=False))
assert [s["id"] for s in out["suggestions"]] == ["Google Workspace"]
assert set(out["suggestions"][0].keys()) == OFFER_SHAPE
@@ -98,6 +98,6 @@ def test_preflight_require_vague_false_keeps_suggestions(monkeypatch):
def test_preflight_require_vague_false_still_drops_hallucinated_ids(monkeypatch):
# require_vague=False must NOT loosen the vetted-id revalidation: a made-up id is still dropped.
monkeypatch.setattr(pf, "load_all_tools", lambda: [])
monkeypatch.setattr(pf, "_call_classifier", _stub_classifier(False, ["TotallyFakeServer"]))
monkeypatch.setattr(pf, "p_call_classifier", _stub_classifier(False, ["TotallyFakeServer"]))
out = asyncio.run(run_preflight("do the thing", timeout_s=5, require_vague=False))
assert out["suggestions"] == []
+2 -2
View File
@@ -65,7 +65,7 @@ def _capture_env(monkeypatch, settings, api_type, resolved_model, model_entry):
monkeypatch.setattr(am, "load_settings", lambda: settings, raising=True)
monkeypatch.setattr(reg, "get_api_type", lambda model: api_type, raising=True)
monkeypatch.setattr(reg, "resolve_model_id_for_sdk", lambda model, s: resolved_model, raising=True)
monkeypatch.setattr(reg, "_find_builtin_model", lambda model: model_entry, raising=True)
monkeypatch.setattr(reg, "find_builtin_model", lambda model: model_entry, raising=True)
captured = {}
async def capturing_query(*args, **kwargs):
@@ -172,7 +172,7 @@ def test_loop_builds_direct_anthropic_key_env(monkeypatch):
monkeypatch.setattr(am, "load_settings", lambda: settings, raising=True)
monkeypatch.setattr(reg, "get_api_type", lambda model: "anthropic", raising=True)
monkeypatch.setattr(reg, "resolve_model_id_for_sdk", lambda model, s: "claude-sonnet-4-6", raising=True)
monkeypatch.setattr(reg, "_find_builtin_model", lambda model: None, raising=True)
monkeypatch.setattr(reg, "find_builtin_model", lambda model: None, raising=True)
captured = {}
+49 -49
View File
@@ -582,15 +582,15 @@ def test_resolve_sdk_gemini_prefers_antigravity_over_api_key():
from backend.apps.settings.models import AppSettings
s = AppSettings()
s.google_api_key = "ai-studio-key"
with patch.object(registry, "_antigravity_connected", return_value=True):
with patch.object(registry, "p_antigravity_connected", return_value=True):
# flash IS AG-serveable -> AG wins over the key
assert registry.resolve_model_id_for_sdk("gemini-3-flash", s) == "ag/gemini-3-flash"
with patch.object(registry, "_antigravity_connected", return_value=False):
with patch.object(registry, "p_antigravity_connected", return_value=False):
# AG not connected -> key
assert registry.resolve_model_id_for_sdk("gemini-3-flash", s) == "gemini/gemini-3-flash-preview"
# No key, no AG -> gc/ subscription lane untouched
s2 = AppSettings()
with patch.object(registry, "_antigravity_connected", return_value=False):
with patch.object(registry, "p_antigravity_connected", return_value=False):
assert registry.resolve_model_id_for_sdk("gemini-3-flash", s2) == "gc/gemini-3-flash-preview"
@@ -1254,13 +1254,13 @@ def test_get_api_type_openai():
def test_find_builtin_model_returns_none_for_unknown():
from backend.apps.agents.providers.registry import _find_builtin_model
assert _find_builtin_model("not-a-real-model-xyz") is None
from backend.apps.agents.providers.registry import find_builtin_model
assert find_builtin_model("not-a-real-model-xyz") is None
def test_find_builtin_model_returns_dict_for_known():
from backend.apps.agents.providers.registry import _find_builtin_model
sonnet = _find_builtin_model("sonnet")
from backend.apps.agents.providers.registry import find_builtin_model
sonnet = find_builtin_model("sonnet")
assert sonnet is not None
assert sonnet.get("api") == "anthropic"
@@ -1766,20 +1766,20 @@ def test_gpt5_param_scrub_drops_unsupported_sampling_knobs():
Live-confirmed the 400s against the OpenAI API 2026-06-14."""
import json
from backend.apps.agents.proxy.anthropic_proxy import _scrub_request_for_openai_gpt5
from backend.apps.agents.core.openai_passthrough import _scrub_gpt5_params
from backend.apps.agents.core.openai_passthrough import scrub_gpt5_params
dirty = json.dumps({"model": "gpt-5", "messages": [{"role": "user", "content": "hi"}],
"max_tokens": 200, "temperature": 0, "top_p": 0.9,
"frequency_penalty": 0.5, "presence_penalty": 0.1, "logprobs": True}).encode()
for fn in (_scrub_request_for_openai_gpt5, _scrub_gpt5_params):
for fn in (_scrub_request_for_openai_gpt5, scrub_gpt5_params):
out = json.loads(fn(dirty))
assert out.get("max_completion_tokens") == 200 and "max_tokens" not in out, fn.__name__
for k in ("temperature", "top_p", "frequency_penalty", "presence_penalty", "logprobs"):
assert k not in out, f"{fn.__name__} left {k}"
# temperature==1 is the one allowed value; don't over-strip it
assert json.loads(_scrub_gpt5_params(json.dumps(
assert json.loads(scrub_gpt5_params(json.dumps(
{"model": "gpt-5", "temperature": 1}).encode())).get("temperature") == 1
# non-gpt-5 models are untouched
assert json.loads(_scrub_gpt5_params(json.dumps(
assert json.loads(scrub_gpt5_params(json.dumps(
{"model": "gpt-4o", "temperature": 0, "top_p": 0.5}).encode())) == \
{"model": "gpt-4o", "temperature": 0, "top_p": 0.5}
@@ -2300,8 +2300,8 @@ def test_custom_provider_value_synthesises_route_api_entry():
api='custom' entry whose model_id is the 9Router routing string
`cp-<slug>/<bare>`. agent_manager keys on api='custom' and resolved_model
must be the cp- prefixed string for 9Router to forward correctly."""
from backend.apps.agents.providers.registry import _find_builtin_model
entry = _find_builtin_model("custom/ollama-cloud/gpt-oss:120b")
from backend.apps.agents.providers.registry import find_builtin_model
entry = find_builtin_model("custom/ollama-cloud/gpt-oss:120b")
assert entry is not None
assert entry.get("api") == "custom"
assert entry.get("route") == "api"
@@ -2320,28 +2320,28 @@ def test_custom_provider_value_with_multi_segment_model_id():
"""Model ids may contain '/' (e.g. meta-llama/llama-3-70b-instruct on
Together AI). Synthesis must use partition on the FIRST '/' so the
rest of the model id stays intact."""
from backend.apps.agents.providers.registry import _find_builtin_model
entry = _find_builtin_model("custom/together-ai/meta-llama/llama-3-70b-instruct")
from backend.apps.agents.providers.registry import find_builtin_model
entry = find_builtin_model("custom/together-ai/meta-llama/llama-3-70b-instruct")
assert entry is not None
assert entry.get("model_id") == "cp-together-ai/meta-llama/llama-3-70b-instruct"
def test_custom_provider_lookup_finds_entry_by_slug():
"""_find_custom_provider_for_value must slugify the same way as the
"""find_custom_provider_for_value must slugify the same way as the
UI/sync layer so name 'Ollama Cloud' resolves to the value
'custom/ollama-cloud/...'."""
from backend.apps.agents.providers.registry import _find_custom_provider_for_value
from backend.apps.agents.providers.registry import find_custom_provider_for_value
from backend.apps.settings.models import AppSettings, CustomProvider
s = AppSettings(custom_providers=[
CustomProvider(name="Ollama Cloud", base_url="https://ollama.com/v1", api_key="x"),
CustomProvider(name="Together AI", base_url="https://api.together.xyz/v1", api_key="y"),
])
cp = _find_custom_provider_for_value(s, "custom/ollama-cloud/gpt-oss:120b")
cp = find_custom_provider_for_value(s, "custom/ollama-cloud/gpt-oss:120b")
assert cp is not None and cp.name == "Ollama Cloud"
cp2 = _find_custom_provider_for_value(s, "custom/together-ai/meta-llama/llama-3-70b")
cp2 = find_custom_provider_for_value(s, "custom/together-ai/meta-llama/llama-3-70b")
assert cp2 is not None and cp2.name == "Together AI"
# Unknown slug → None.
assert _find_custom_provider_for_value(s, "custom/nonexistent/whatever") is None
assert find_custom_provider_for_value(s, "custom/nonexistent/whatever") is None
def test_get_context_window_custom_provider_value_format():
@@ -2365,22 +2365,22 @@ def test_custom_provider_slug_is_url_safe():
"""The slug must be alnum-and-dash only, it's used both as the 9Router
prefix and as a URL path segment. Spaces, slashes, and special chars
must all be folded to dashes."""
from backend.apps.agents.providers.registry import _custom_provider_slug_for_lookup
assert _custom_provider_slug_for_lookup("Ollama Cloud") == "ollama-cloud"
assert _custom_provider_slug_for_lookup("My/Local LM!!!") == "my-local-lm"
assert _custom_provider_slug_for_lookup("") == "custom"
assert _custom_provider_slug_for_lookup(" ") == "custom"
from backend.apps.agents.providers.registry import custom_provider_slug_for_lookup
assert custom_provider_slug_for_lookup("Ollama Cloud") == "ollama-cloud"
assert custom_provider_slug_for_lookup("My/Local LM!!!") == "my-local-lm"
assert custom_provider_slug_for_lookup("") == "custom"
assert custom_provider_slug_for_lookup(" ") == "custom"
def test_custom_provider_slug_unicode_collapses_safely():
"""Unicode names are folded to ASCII-safe dashes; emojis/accents drop."""
from backend.apps.agents.providers.registry import _custom_provider_slug_for_lookup
from backend.apps.agents.providers.registry import custom_provider_slug_for_lookup
# Accented chars get stripped (regex is [a-zA-Z0-9-] only).
assert _custom_provider_slug_for_lookup("Tögether AI 🚀") == "t-gether-ai"
assert custom_provider_slug_for_lookup("Tögether AI 🚀") == "t-gether-ai"
# Pure-emoji name → fallback "custom".
assert _custom_provider_slug_for_lookup("🚀💎") == "custom"
assert custom_provider_slug_for_lookup("🚀💎") == "custom"
# Trailing/leading dashes get stripped.
assert _custom_provider_slug_for_lookup("---weird---") == "weird"
assert custom_provider_slug_for_lookup("---weird---") == "weird"
def test_custom_provider_slug_does_not_collide_with_routing_prefixes():
@@ -2388,8 +2388,8 @@ def test_custom_provider_slug_does_not_collide_with_routing_prefixes():
built-in prefixes (cc/, cx/, gc/, ag/, gemini/, openrouter/) used by
resolved_is_9router. cp- starts with 'c' and dash so it can't be
confused with cc/, but verify the dispatch logic agrees."""
from backend.apps.agents.providers.registry import _find_builtin_model
entry = _find_builtin_model("custom/cc/whatever") # adversarial slug "cc"
from backend.apps.agents.providers.registry import find_builtin_model
entry = find_builtin_model("custom/cc/whatever") # adversarial slug "cc"
assert entry is not None
routed = entry["model_id"]
assert routed == "cp-cc/whatever"
@@ -2402,7 +2402,7 @@ def test_custom_provider_models_with_special_chars():
(deepseek 'deepseek-v3.1'), version suffixes (':free'), and slashes
(Together 'meta-llama/Llama-3-70B'). All must round-trip without
being mangled."""
from backend.apps.agents.providers.registry import _find_builtin_model
from backend.apps.agents.providers.registry import find_builtin_model
cases = [
"custom/ollama/gpt-oss:120b",
"custom/together/meta-llama/Llama-3.3-70B-Instruct",
@@ -2411,7 +2411,7 @@ def test_custom_provider_models_with_special_chars():
"custom/groq/llama-3.3-70b-versatile",
]
for v in cases:
e = _find_builtin_model(v)
e = find_builtin_model(v)
assert e is not None, f"failed: {v}"
# Bare-model portion is everything after first slash after the slug.
rest = v[len("custom/"):]
@@ -2421,12 +2421,12 @@ def test_custom_provider_models_with_special_chars():
def test_custom_provider_value_with_invalid_format_returns_none():
"""Malformed picker values (no slug, no model) must not synthesise a
bogus entry, they should miss _find_builtin_model entirely so the
bogus entry, they should miss find_builtin_model entirely so the
dispatch loop falls through to the 'unknown model' branch."""
from backend.apps.agents.providers.registry import _find_builtin_model
assert _find_builtin_model("custom/") is None
assert _find_builtin_model("custom/onlyslug") is None
assert _find_builtin_model("custom//onlymodel") is None # empty slug
from backend.apps.agents.providers.registry import find_builtin_model
assert find_builtin_model("custom/") is None
assert find_builtin_model("custom/onlyslug") is None
assert find_builtin_model("custom//onlymodel") is None # empty slug
def test_custom_provider_get_api_type_returns_custom():
@@ -2486,10 +2486,10 @@ def test_custom_provider_two_providers_get_distinct_slugs():
"""Two custom providers with different display names must produce
two different slugs / routing prefixes, otherwise 9Router will route
both to whichever connection was created last."""
from backend.apps.agents.providers.registry import _custom_provider_slug_for_lookup
a = _custom_provider_slug_for_lookup("Ollama Cloud")
b = _custom_provider_slug_for_lookup("Together AI")
c = _custom_provider_slug_for_lookup("Groq")
from backend.apps.agents.providers.registry import custom_provider_slug_for_lookup
a = custom_provider_slug_for_lookup("Ollama Cloud")
b = custom_provider_slug_for_lookup("Together AI")
c = custom_provider_slug_for_lookup("Groq")
assert len({a, b, c}) == 3
@@ -2500,10 +2500,10 @@ def test_custom_provider_slug_collision_after_sanitize():
test just documents that post-slug collisions DO collide and the
UI-level uniqueness check (in Settings.tsx) is the right enforcement
layer, backend resolution would always pick the first match."""
from backend.apps.agents.providers.registry import _custom_provider_slug_for_lookup
assert _custom_provider_slug_for_lookup("Ollama Cloud") == \
_custom_provider_slug_for_lookup("ollama-cloud") == \
_custom_provider_slug_for_lookup("OLLAMA cloud")
from backend.apps.agents.providers.registry import custom_provider_slug_for_lookup
assert custom_provider_slug_for_lookup("Ollama Cloud") == \
custom_provider_slug_for_lookup("ollama-cloud") == \
custom_provider_slug_for_lookup("OLLAMA cloud")
def test_list_models_includes_complete_custom_providers_excludes_incomplete():
@@ -2634,11 +2634,11 @@ def test_custom_provider_resolve_aux_model_unaffected():
def test_custom_provider_with_very_long_name_still_works():
"""No upper bound on name length anywhere in the pipeline. Verify a
250-char name slugs cleanly."""
from backend.apps.agents.providers.registry import _custom_provider_slug_for_lookup, _find_builtin_model
from backend.apps.agents.providers.registry import custom_provider_slug_for_lookup, find_builtin_model
long_name = "a" * 250
slug = _custom_provider_slug_for_lookup(long_name)
slug = custom_provider_slug_for_lookup(long_name)
assert slug == long_name
entry = _find_builtin_model(f"custom/{slug}/some-model")
entry = find_builtin_model(f"custom/{slug}/some-model")
assert entry is not None
assert entry["model_id"] == f"cp-{slug}/some-model"
+2 -2
View File
@@ -12,7 +12,7 @@ def _call(**kw):
base = dict(model="m", router_model_id="cc/opus", api_type="anthropic",
anthropic_api_key=None, connection_mode="own_key")
base.update(kw)
with patch("backend.apps.agents.providers.registry._find_builtin_model", return_value=None):
with patch("backend.apps.agents.providers.registry.find_builtin_model", return_value=None):
return should_register_web_mcp(**base)
@@ -40,7 +40,7 @@ def test_subscription_route_claude_non_pro_registers():
def test_direct_anthropic_api_route_uses_native_path():
entry = {"route": "api", "api": "anthropic"}
with patch("backend.apps.agents.providers.registry._find_builtin_model", return_value=entry):
with patch("backend.apps.agents.providers.registry.find_builtin_model", return_value=entry):
out = should_register_web_mcp(
model="claude-x", router_model_id="claude-3-5-api", api_type="anthropic",
anthropic_api_key="sk-ant-xxx", connection_mode="own_key",