mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-07 18:27:45 +02:00
[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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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"] == []
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user