diff --git a/routes/model_routes.py b/routes/model_routes.py index fb9555438e..72ddbdea9f 100644 --- a/routes/model_routes.py +++ b/routes/model_routes.py @@ -359,6 +359,22 @@ def _truthy(value: str | None) -> bool: return (value or "").strip().lower() in ("true", "1", "yes", "on") +def _parse_optional_bool(value: Any) -> bool | None: + if value is None: + return None + if isinstance(value, bool): + return value + if isinstance(value, int): + return bool(value) + if isinstance(value, str): + s = value.strip().lower() + if s in ("true", "1", "yes", "on"): + return True + if s in ("false", "0", "no", "off"): + return False + return None + + _ENDPOINT_KINDS = {"auto", "local", "api", "proxy"} _REFRESH_MODES = {"auto", "manual", "disabled"} @@ -1852,8 +1868,7 @@ def create_model_endpoint( ep_id = str(uuid.uuid4())[:8] db = SessionLocal() try: - _st_raw = (supports_tools or "").strip().lower() - _st = True if _st_raw in ("true", "1", "yes") else (False if _st_raw in ("false", "0", "no") else None) + _st = _parse_optional_bool(supports_tools) _pinned = _normalize_model_ids(pinned_models) # Stamp owner so the picker only shows this endpoint to the admin # who added it. Pass `shared=true` to mark it null-owner (visible @@ -2208,8 +2223,7 @@ async def toggle_model_endpoint(ep_id: str, request: Request): raise HTTPException(404, "Endpoint not found") if body: if "supports_tools" in body: - v = body["supports_tools"] - ep.supports_tools = {True: True, False: False, 'true': True, 'false': False, 1: True, 0: False}.get(v) + ep.supports_tools = _parse_optional_bool(body.get("supports_tools")) if "is_enabled" in body: v_ie = body['is_enabled'] ep.is_enabled = v_ie.lower() in ('true', '1', 'yes') if isinstance(v_ie, str) else bool(v_ie) diff --git a/tests/test_model_routes.py b/tests/test_model_routes.py index de109809c7..878571e8f2 100644 --- a/tests/test_model_routes.py +++ b/tests/test_model_routes.py @@ -49,6 +49,7 @@ _parse_model_list, _normalize_refresh_mode, _truthy, + _parse_optional_bool, _speech_settings_using_endpoint, _clear_speech_settings_for_endpoint, _endpoint_settings_using_endpoint, @@ -538,6 +539,18 @@ def test_truthy_true_values(self, value): def test_truthy_false_values(self, value): assert _truthy(value) is False + @pytest.mark.parametrize("value", [True, 1, "true", "1", "yes", "on", " TRUE "]) + def test_parse_optional_bool_true(self, value): + assert _parse_optional_bool(value) is True + + @pytest.mark.parametrize("value", [False, 0, "false", "0", "no", "off", " FALSE "]) + def test_parse_optional_bool_false(self, value): + assert _parse_optional_bool(value) is False + + @pytest.mark.parametrize("value", [None, "", "unknown", "invalid", [], {}]) + def test_parse_optional_bool_none(self, value): + assert _parse_optional_bool(value) is None + def test_keyed_probe_does_not_fallback_to_curated_on_auth_failure(self, monkeypatch): monkeypatch.setattr(endpoint_resolver, "resolve_url", lambda url: url, raising=False) monkeypatch.setattr(model_routes, "_normalize_base", lambda url: url.rstrip("/"))