diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index a6c078dc..fe0864d6 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -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//. - 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() diff --git a/backend/apps/agents/browser/browser_agent.py b/backend/apps/agents/browser/browser_agent.py index d6d0ddb7..8edf48ac 100644 --- a/backend/apps/agents/browser/browser_agent.py +++ b/backend/apps/agents/browser/browser_agent.py @@ -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 diff --git a/backend/apps/agents/core/mcp_preflight.py b/backend/apps/agents/core/mcp_preflight.py index de58feed..b779f4eb 100644 --- a/backend/apps/agents/core/mcp_preflight.py +++ b/backend/apps/agents/core/mcp_preflight.py @@ -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) diff --git a/backend/apps/agents/core/openai_passthrough.py b/backend/apps/agents/core/openai_passthrough.py index dc5ac2fd..22edaf30 100644 --- a/backend/apps/agents/core/openai_passthrough.py +++ b/backend/apps/agents/core/openai_passthrough.py @@ -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(): diff --git a/backend/apps/agents/core/seq_log.py b/backend/apps/agents/core/seq_log.py index fcd1ea39..e52b54da 100644 --- a/backend/apps/agents/core/seq_log.py +++ b/backend/apps/agents/core/seq_log.py @@ -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()) diff --git a/backend/apps/agents/manager/RunSupportMixin.py b/backend/apps/agents/manager/RunSupportMixin.py index 2e92d5bd..bd6f57d6 100644 --- a/backend/apps/agents/manager/RunSupportMixin.py +++ b/backend/apps/agents/manager/RunSupportMixin.py @@ -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 diff --git a/backend/apps/agents/manager/provider_env.py b/backend/apps/agents/manager/provider_env.py index c0971104..527c0581 100644 --- a/backend/apps/agents/manager/provider_env.py +++ b/backend/apps/agents/manager/provider_env.py @@ -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", diff --git a/backend/apps/agents/providers/pricing.py b/backend/apps/agents/providers/pricing.py index 63035aa3..c985a655 100644 --- a/backend/apps/agents/providers/pricing.py +++ b/backend/apps/agents/providers/pricing.py @@ -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( diff --git a/backend/apps/agents/providers/registry.py b/backend/apps/agents/providers/registry.py index 3ef184a8..17e4bb6f 100644 --- a/backend/apps/agents/providers/registry.py +++ b/backend/apps/agents/providers/registry.py @@ -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//` 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:/`) 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 diff --git a/backend/apps/agents/session_credential.py b/backend/apps/agents/session_credential.py index 1facfad3..5951f7d5 100644 --- a/backend/apps/agents/session_credential.py +++ b/backend/apps/agents/session_credential.py @@ -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 diff --git a/backend/apps/agents/tools/ssrf_guard.py b/backend/apps/agents/tools/ssrf_guard.py index c641844a..099f5338 100644 --- a/backend/apps/agents/tools/ssrf_guard.py +++ b/backend/apps/agents/tools/ssrf_guard.py @@ -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 diff --git a/backend/apps/agents/tools/web.py b/backend/apps/agents/tools/web.py index 6e2a74a7..492613e9 100644 --- a/backend/apps/agents/tools/web.py +++ b/backend/apps/agents/tools/web.py @@ -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)[^>]*>.*?", "", 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']*class="[^"]*result__snippet[^"]*"[^>]*>(.*?)', 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: diff --git a/backend/tests/test_browser_agent_loop.py b/backend/tests/test_browser_agent_loop.py index 5f4bfcd9..221740c7 100644 --- a/backend/tests/test_browser_agent_loop.py +++ b/backend/tests/test_browser_agent_loop.py @@ -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"): diff --git a/backend/tests/test_mcp_offer.py b/backend/tests/test_mcp_offer.py index 6c58cc8c..9889e50a 100644 --- a/backend/tests/test_mcp_offer.py +++ b/backend/tests/test_mcp_offer.py @@ -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"] == [] diff --git a/backend/tests/test_streaming_harness.py b/backend/tests/test_streaming_harness.py index 29dcea78..af4e6110 100644 --- a/backend/tests/test_streaming_harness.py +++ b/backend/tests/test_streaming_harness.py @@ -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 = {} diff --git a/backend/tests/test_v2_invariants.py b/backend/tests/test_v2_invariants.py index 6cd0ca45..9a61513f 100644 --- a/backend/tests/test_v2_invariants.py +++ b/backend/tests/test_v2_invariants.py @@ -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-/`. 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" diff --git a/backend/tests/test_web_mcp_decision.py b/backend/tests/test_web_mcp_decision.py index b5f1b40f..fe782667 100644 --- a/backend/tests/test_web_mcp_decision.py +++ b/backend/tests/test_web_mcp_decision.py @@ -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",