From 8ef58eaa905e64fa54601ddc72cd7e53a9e7d7c3 Mon Sep 17 00:00:00 2001 From: Xuehao Xu Date: Sun, 20 Sep 2026 16:13:58 +0800 Subject: [PATCH] feat(models): manage shared models from Settings (#5596) * feat(models): add admin UI for shared model management * docs(gateway): keep model guidance within size budget --- README.md | 25 ++ backend/app/gateway/AGENTS.md | 1 + backend/app/gateway/app.py | 3 + backend/app/gateway/routers/managed_models.py | 94 +++++ .../harness/deerflow/config/AGENTS.md | 6 + .../harness/deerflow/config/app_config.py | 9 +- .../harness/deerflow/config/managed_models.py | 147 +++++++ .../harness/deerflow/models/AGENTS.md | 23 ++ .../tests/blocking_io/test_managed_models.py | 23 ++ backend/tests/test_managed_models.py | 212 ++++++++++ docs/pr-evidence/model-management.png | Bin 0 -> 94675 bytes frontend/AGENTS.md | 10 + .../settings/model-settings-page.tsx | 377 ++++++++++++++++++ .../workspace/settings/settings-dialog.tsx | 10 + .../workspace-settings-deep-link.tsx | 1 + frontend/src/core/i18n/locales/en-US.ts | 43 ++ frontend/src/core/i18n/locales/types.ts | 38 ++ frontend/src/core/i18n/locales/zh-CN.ts | 41 ++ frontend/src/core/models/management.ts | 89 +++++ frontend/tests/e2e/model-management.spec.ts | 85 ++++ .../components/workspace/lazy-panels.test.ts | 2 +- .../settings/model-settings-page.dom.test.tsx | 134 +++++++ 22 files changed, 1370 insertions(+), 3 deletions(-) create mode 100644 backend/app/gateway/routers/managed_models.py create mode 100644 backend/packages/harness/deerflow/config/managed_models.py create mode 100644 backend/tests/blocking_io/test_managed_models.py create mode 100644 backend/tests/test_managed_models.py create mode 100644 docs/pr-evidence/model-management.png create mode 100644 frontend/src/components/workspace/settings/model-settings-page.tsx create mode 100644 frontend/src/core/models/management.ts create mode 100644 frontend/tests/e2e/model-management.spec.ts create mode 100644 frontend/tests/unit/components/workspace/settings/model-settings-page.dom.test.tsx diff --git a/README.md b/README.md index 8816f8d9b..0eef072f4 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,31 @@ It is disabled by default; see the linked guide to enable it. DeerFlow disables Console cost estimates when currencies are mixed rather than presenting an invalid aggregate. + Administrators can also open **Settings → Models** to add, edit, test, and + enable/disable shared OpenAI-compatible Chat Completions models without editing + `config.yaml`. Enter a unique name, base URL, model ID, and optional API key; + saving refreshes the chat model list. Connection testing sends a short streaming + tool-call request and may incur provider charges. It does not save the draft or + verify image support; set image support and token limits from provider documentation. + Native provider adapters and advanced reasoning settings remain YAML-configured. + + YAML models remain read-only in this page and take precedence on name conflicts. + Managed models are appended after YAML models; edits apply to new configuration + snapshots, while active runs retain their existing snapshot. Disabling a model + removes it from future selection/resolution, so update any custom-agent or scheduled + task definitions that explicitly reference it before disabling it. + Managed models are shared by the deployment, not personal API-key profiles, and + remain subject to the existing model authorization policy. + + The encrypted catalog and a generated local encryption key are stored in + `$DEER_FLOW_HOME/managed-models/` (default `.deer-flow/managed-models/`). Persist + and back up the **whole directory**, restrict filesystem access, and share it + across Gateway workers/replicas that should use the same catalog. The local key + is protected by filesystem permissions; encryption does not protect against + someone who can read both files. Losing the key requires restoring the backup. + Reads and writes fail if the catalog cannot be decrypted, rather than replacing it. + This storage is independent of the SQL backend and works with read-only YAML mounts. + When several models are configured, open either model picker and use the star beside a model to favorite it. Favorites appear first in both the main chat and Side Chat pickers without changing either chat's selected or diff --git a/backend/app/gateway/AGENTS.md b/backend/app/gateway/AGENTS.md index 4b9e8c4a7..a6d36713f 100644 --- a/backend/app/gateway/AGENTS.md +++ b/backend/app/gateway/AGENTS.md @@ -91,6 +91,7 @@ owner-scoped assistant version selection remains enabled. | Router | Endpoints | |--------|-----------| +| **Managed Models** (`/api/managed-models`) | Admin-only GET catalog, PUT create/update with revision checks, POST `/test` streaming tool-call probe. YAML names are reserved; credentials stay private. | | **Models** (`/api/models`) | `GET /` - list models; `GET /{name}` - model details | | **Features** (`/api/features`) | `GET /` - UI capabilities: hot-reloaded agents, guarded browser, startup MCP tasks, separate batch repository/worker states so history stays readable without a worker, `conversation_references` (whether `read_conversation` is configured, plus the per-run reference cap), and knowledge scope selection | | **Knowledge** (`/api/knowledge/retrieval-catalog`) | Authenticated, allowlist-safe, read-only dataset/document catalog used only by main and custom-agent chat scope selection; knowledge management remains in RAGFlow | diff --git a/backend/app/gateway/app.py b/backend/app/gateway/app.py index 4a7e95fb0..f1097e588 100644 --- a/backend/app/gateway/app.py +++ b/backend/app/gateway/app.py @@ -905,6 +905,9 @@ This gateway provides runtime endpoints for agent runs plus custom endpoints for # Include routers # Models API is mounted at /api/models + from app.gateway.routers import managed_models + + app.include_router(managed_models.router) app.include_router(models.router) # Features API is mounted at /api/features diff --git a/backend/app/gateway/routers/managed_models.py b/backend/app/gateway/routers/managed_models.py new file mode 100644 index 000000000..9ab72ead1 --- /dev/null +++ b/backend/app/gateway/routers/managed_models.py @@ -0,0 +1,94 @@ +"""Admin-only shared model management. Credentials never leave the server.""" + +import asyncio + +from fastapi import APIRouter, HTTPException, Request +from langchain_core.messages import HumanMessage +from pydantic import BaseModel, ConfigDict + +from app.gateway.deps import require_admin_user +from deerflow.config.app_config import get_app_config +from deerflow.config.managed_models import ManagedModel, ManagedModelStore + +router = APIRouter(prefix="/api/managed-models", tags=["models"]) +_ADMIN = "Admin privileges are required to manage shared models." + + +class SaveModelRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + config: ManagedModel + expected_revision: str | None = None + + +def _catalog(): + config = get_app_config() + yaml_models = [item for item in config.models if item.name not in config._managed_model_names] + yaml_names = {item.name for item in yaml_models} + return { + "models": [{"name": item.name, "display_name": item.display_name or item.name, "model": item.model, "source": "config", "enabled": True} for item in yaml_models] + + [{**item.public(), "conflict": item.name in yaml_names} for item in ManagedModelStore().list()] + } + + +@router.get("") +async def list_managed_models(request: Request): + await require_admin_user(request, detail=_ADMIN) + try: + return await asyncio.to_thread(_catalog) + except ValueError: + raise HTTPException(503, "Managed model storage is unavailable; check the catalog and encryption key") from None + + +def _save(body: SaveModelRequest): + config = get_app_config() + if any(item.name == body.config.name and item.name not in config._managed_model_names for item in config.models): + raise HTTPException(409, "This model name is reserved by config.yaml") + try: + return ManagedModelStore().save(body.config, expected_revision=body.expected_revision).public() + except FileExistsError: + raise HTTPException(409, "Model changed or already exists; reload before saving") from None + except FileNotFoundError: + raise HTTPException(404, "Model no longer exists") from None + except (ValueError, OSError): + raise HTTPException(503, "Managed model storage is unavailable") from None + + +@router.put("") +async def save_model(request: Request, body: SaveModelRequest): + await require_admin_user(request, detail=_ADMIN) + return await asyncio.to_thread(_save, body) + + +def _probe_config(body: SaveModelRequest): + profile = body.config + if body.expected_revision is not None: + previous = next((item for item in ManagedModelStore().list() if item.name == profile.name), None) + if previous is None or previous.revision != body.expected_revision: + raise HTTPException(409, "Model changed; reload before testing") + if profile.api_key is None: + profile = profile.model_copy(update={"api_key": previous.api_key}) + return profile.runtime_config() + + +@router.post("/test") +async def test_model(request: Request, body: SaveModelRequest): + """Send a bounded streaming tool-call probe without saving the profile.""" + await require_admin_user(request, detail=_ADMIN) + try: + config = await asyncio.to_thread(_probe_config, body) + from langchain_openai import ChatOpenAI + + model = ChatOpenAI(model=config.model, base_url=config.base_url, api_key=config.api_key, timeout=15, max_retries=0, max_tokens=32) + probe = model.bind_tools([{"type": "function", "function": {"name": "connection_check", "description": "Check the connection", "parameters": {"type": "object", "properties": {}}}}], tool_choice="connection_check") + response = None + async with asyncio.timeout(20): + async for chunk in probe.astream([HumanMessage(content="Call connection_check.")], config={"callbacks": []}): + response = chunk if response is None else response + chunk + if response is None or not any(call["name"] == "connection_check" and call["args"] == {} for call in response.tool_calls): + return {"ok": False, "message": "tool_call_missing"} + return {"ok": True, "message": "success"} + except HTTPException: + raise + except Exception: + # Provider exception strings may contain keys, URLs or response bodies. + return {"ok": False, "message": "connection_failed"} diff --git a/backend/packages/harness/deerflow/config/AGENTS.md b/backend/packages/harness/deerflow/config/AGENTS.md index d592c6670..639a4ecc0 100644 --- a/backend/packages/harness/deerflow/config/AGENTS.md +++ b/backend/packages/harness/deerflow/config/AGENTS.md @@ -107,3 +107,9 @@ Extensions are optional only in the fallback *search* mode (priority 3-4 above): Gateway API endpoints and `DeerFlowClient` methods can modify MCP servers and skill state at runtime; their `extensions_config.json` writes use the shared atomic replacement helper, while `middlewares` remains an operator-controlled config-file extension point. Values beginning with `$` are resolved from the environment when the file is loaded, and an unset variable becomes `""`. Runtime writers (MCP router, skill toggle, `DeerFlowClient`) therefore read the raw file with `read_raw_extensions_config`, merge into it (`set_raw_skill_enabled` for skill state), check the candidate with `validate_raw_extensions_config`, and write that raw dict. They never serialize an `ExtensionsConfig` model back to disk: its resolved values would persist secrets in plaintext and erase the references. When the file does not exist yet, the Gateway skill toggle seeds only the cached skill states. `tests/test_extensions_config_raw_writes.py` and the placeholder tests in `tests/test_client.py` pin this. + +The file-backed singleton entrypoints additionally merge administrator-managed shared +models from the encrypted runtime-home catalog. YAML entries win name conflicts; +managed changes create new effective snapshots and do not alter an active runtime +or an explicitly injected AppConfig. See `../models/AGENTS.md` for storage and reload +boundaries. `AppConfig.from_file()` remains YAML-only. diff --git a/backend/packages/harness/deerflow/config/app_config.py b/backend/packages/harness/deerflow/config/app_config.py index 02f0cdcba..3982bed23 100644 --- a/backend/packages/harness/deerflow/config/app_config.py +++ b/backend/packages/harness/deerflow/config/app_config.py @@ -358,6 +358,7 @@ class AppConfig(BaseModel): # ``_build_name_indexes``. They make ``get_model_config`` / ``get_tool_config`` # / ``get_tool_group_config`` O(1) instead of an O(n) ``next(...)`` scan per # call. Private attrs are excluded from serialization. + _managed_model_names: set[str] = PrivateAttr(default_factory=set) _models_by_name: dict[str, ModelConfig] = PrivateAttr(default_factory=dict) _tools_by_name: dict[str, ToolConfig] = PrivateAttr(default_factory=dict) _tool_groups_by_name: dict[str, ToolGroupConfig] = PrivateAttr(default_factory=dict) @@ -717,7 +718,9 @@ def get_app_config() -> AppConfig: elif _app_config_path == resolved_path and _app_config_signature != current_signature: logger.info("Config file content signature changed, reloading AppConfig") _load_and_cache_app_config(str(resolved_path)) - return _app_config + from deerflow.config.managed_models import merge_managed_models + + return merge_managed_models(_app_config) def reload_app_config(config_path: str | None = None) -> AppConfig: @@ -733,7 +736,9 @@ def reload_app_config(config_path: str | None = None) -> AppConfig: Returns: The newly loaded AppConfig instance. """ - return _load_and_cache_app_config(config_path) + from deerflow.config.managed_models import merge_managed_models + + return merge_managed_models(_load_and_cache_app_config(config_path)) def reset_app_config() -> None: diff --git a/backend/packages/harness/deerflow/config/managed_models.py b/backend/packages/harness/deerflow/config/managed_models.py new file mode 100644 index 000000000..6771ea406 --- /dev/null +++ b/backend/packages/harness/deerflow/config/managed_models.py @@ -0,0 +1,147 @@ +"""Administrator-managed OpenAI-compatible model profiles, separate from YAML. + +The encrypted catalog and its local key live in the persistent runtime home. +Writers are serialized across threads/processes; readers see atomic snapshots. +""" + +from __future__ import annotations + +import json +import os +import tempfile +import threading +from pathlib import Path +from typing import TYPE_CHECKING, Literal +from urllib.parse import urlsplit +from uuid import uuid4 + +from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator + +from deerflow.config.extensions_config import extensions_config_file_lock +from deerflow.config.file_signature import get_config_signature +from deerflow.config.model_config import ModelConfig +from deerflow.config.runtime_paths import runtime_home + +if TYPE_CHECKING: + from deerflow.config.app_config import AppConfig + +_lock = threading.RLock() +_cache: tuple | None = None + + +class ManagedModel(BaseModel): + model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) + name: str = Field(pattern=r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$") + provider: Literal["openai-compatible"] = "openai-compatible" + display_name: str = Field(default="", max_length=100) + model: str = Field(min_length=1, max_length=200) + base_url: str = Field(max_length=2048) + api_key: SecretStr | None = None + enabled: bool = True + supports_vision: bool = False + context_window: int | None = Field(default=None, gt=0) + max_tokens: int | None = Field(default=None, gt=0) + revision: str | None = None + + @field_validator("base_url") + @classmethod + def valid_endpoint(cls, value: str) -> str: + try: + url = urlsplit(value) + port = url.port + except ValueError: + raise ValueError("Invalid endpoint URL") from None + if url.scheme not in {"https", "http"} or not url.hostname or url.username is not None or url.password is not None or url.query or url.fragment or (port is not None and port == 0): + raise ValueError("Use an HTTP(S) base URL without credentials, query or fragment") + return value.rstrip("/") + + def public(self) -> dict: + return {**self.model_dump(exclude={"api_key"}), "has_api_key": bool(self.api_key and self.api_key.get_secret_value()), "source": "managed"} + + def runtime_config(self) -> ModelConfig: + return ModelConfig( + name=self.name, + display_name=self.display_name or self.name, + use="langchain_openai:ChatOpenAI", + model=self.model, + base_url=self.base_url, + api_key=self.api_key.get_secret_value() if self.api_key and self.api_key.get_secret_value() else "not-required", + supports_vision=self.supports_vision, + context_window=self.context_window, + max_tokens=self.max_tokens, + ) + + +class ManagedModelStore: + def __init__(self): + self.path = runtime_home() / "managed-models" / "catalog.enc" + self.key_path = self.path.with_name("key") + + def _cipher(self, *, create: bool = False): + from cryptography.fernet import Fernet + + if not self.key_path.exists(): + if not create or self.path.exists(): + raise ValueError("Managed model encryption key is missing; restore it from backup") + self._write(self.key_path, Fernet.generate_key()) + return Fernet(self.key_path.read_bytes()) + + def list(self) -> list[ManagedModel]: + if not self.path.exists(): + return [] + try: + raw = json.loads(self._cipher().decrypt(self.path.read_bytes())) + return [ManagedModel.model_validate(item) for item in raw] + except Exception: + # Never include provider secrets or decrypted validation inputs. + raise ValueError("Cannot read managed models; check the catalog and encryption key") from None + + def save(self, config: ManagedModel, *, expected_revision: str | None) -> ManagedModel: + with _lock, extensions_config_file_lock(self.path): + records = self.list() + previous = next((item for item in records if item.name == config.name), None) + if previous is None and expected_revision is not None: + raise FileNotFoundError("Model no longer exists") + if previous is not None and (expected_revision is None or previous.revision != expected_revision): + raise FileExistsError("Model changed or already exists; reload before saving") + secret = config.api_key if config.api_key is not None else (previous.api_key if previous else None) + saved = config.model_copy(update={"api_key": secret, "revision": uuid4().hex}) + records = [saved if item.name == saved.name else item for item in records] if previous else [*records, saved] + payload = [{**item.model_dump(exclude={"api_key"}), "api_key": item.api_key.get_secret_value() if item.api_key else None} for item in records] + cipher = self._cipher(create=True) + self._write(self.path, cipher.encrypt(json.dumps(payload).encode("utf-8"))) + return saved + + @staticmethod + def _write(path: Path, content: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary: Path | None = None + try: + with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as handle: + temporary = Path(handle.name) + handle.write(content) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, path) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + + +def merge_managed_models(config: AppConfig) -> AppConfig: + """Return a fresh effective snapshot on changes; never mutate an active run.""" + global _cache + store = ManagedModelStore() + signature = (str(store.path), get_config_signature(store.path), get_config_signature(store.key_path)) + if signature[1] is None: + return config + with _lock: + if _cache is not None and _cache[0] is config and _cache[1] == signature: + return _cache[2] + yaml_names = {item.name for item in config.models} + managed = [item.runtime_config() for item in store.list() if item.enabled and item.name not in yaml_names] + result = config.model_copy(update={"models": [*config.models, *managed]}) + result._models_by_name = {**config._models_by_name, **{item.name: item for item in managed}} + result._managed_model_names = {item.name for item in managed} + _cache = (config, signature, result) + return result diff --git a/backend/packages/harness/deerflow/models/AGENTS.md b/backend/packages/harness/deerflow/models/AGENTS.md index 2bf3fbf53..e61ebdf1e 100644 --- a/backend/packages/harness/deerflow/models/AGENTS.md +++ b/backend/packages/harness/deerflow/models/AGENTS.md @@ -28,3 +28,26 @@ while handing off to the wait queue. - Preserves vLLM's non-standard assistant `reasoning` field on full responses, streaming deltas, and follow-up tool-call turns - Designed for configs that enable thinking through `extra_body.chat_template_kwargs.enable_thinking` on vLLM 0.19.0 Qwen reasoning models, while accepting the older `thinking` alias - `cumulative_stream_usage` is an opt-in model setting (default `false`) for endpoints that repeat cumulative token totals on each streaming chunk. The provider converts snapshots to deltas only when a stable completion id is present, isolates interleaved streams by id, and leaves the original usage untouched otherwise. Per-model tracking is lock-protected and cleared on the trailing empty-`choices` frame whether or not that frame carries usage. A soft cap of 1024 ids evicts only entries idle for at least one hour; active streams may temporarily exceed the cap so eviction cannot corrupt their deltas. Regression coverage lives in `tests/test_vllm_provider.py`. + +### Managed shared models (`config/managed_models.py`) + +`ManagedModelStore` persists a Fernet-encrypted catalog plus its generated local key +under `runtime_home()/managed-models`. Files are atomically replaced with temporary +file permissions; complete read/modify/write transactions hold the process lock and +cross-process sidecar lock. Missing keys and invalid catalogs fail closed. Backups +and shared deployments must include both files. SQL storage does not replicate this +catalog. Admin-supplied endpoints can address local providers; only trusted admins +may create or probe them. + +`get_app_config()` and `reload_app_config()` merge enabled managed models after YAML +profiles, with YAML names winning conflicts. A cached effective snapshot uses the +base config identity and content signatures of both files. Never mutate a previously +returned AppConfig: runtime-scoped and explicitly injected configurations remain +authoritative. `_managed_model_names` is private source metadata, not provider kwargs. +Direct `AppConfig.from_file()` continues to read only operator configuration. + +The MVP uses only the registered `langchain_openai:ChatOpenAI` adapter. Full updates +require the current revision; omission means create, an omitted API key retains the +saved key and an empty string clears it. Never serialize SecretStr masking as a saved +key. Read APIs return `has_api_key`, never a credential. Tests live in +`tests/test_managed_models.py` and `tests/blocking_io/test_managed_models.py`. diff --git a/backend/tests/blocking_io/test_managed_models.py b/backend/tests/blocking_io/test_managed_models.py new file mode 100644 index 000000000..da7b2c949 --- /dev/null +++ b/backend/tests/blocking_io/test_managed_models.py @@ -0,0 +1,23 @@ +"""Managed model encryption, persistence and config merging stay off the loop.""" + +from types import SimpleNamespace + +import pytest + +from app.gateway.routers import managed_models as router +from deerflow.config.app_config import AppConfig +from deerflow.config.managed_models import ManagedModel + + +@pytest.mark.asyncio +async def test_admin_catalog_round_trip_offloads_storage(tmp_path, monkeypatch): + monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path)) + config = AppConfig.model_validate({"sandbox": {"use": "test"}}) + monkeypatch.setattr(router, "get_app_config", lambda: config) + request = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(system_role="admin"))) + body = router.SaveModelRequest(config=ManagedModel(name="test", model="test", base_url="https://example.com/v1", api_key="secret")) + result = await router.save_model(request, body) + assert result["has_api_key"] is True + catalog = await router.list_managed_models(request) + assert catalog["models"][0]["name"] == "test" + assert "secret" not in str(catalog) diff --git a/backend/tests/test_managed_models.py b/backend/tests/test_managed_models.py new file mode 100644 index 000000000..519db0b3a --- /dev/null +++ b/backend/tests/test_managed_models.py @@ -0,0 +1,212 @@ +"""Managed models: persistence, snapshot resolution and administrator boundaries.""" + +from types import SimpleNamespace + +import pytest +from fastapi import HTTPException +from pydantic import ValidationError + +from deerflow.config.app_config import AppConfig +from deerflow.config.managed_models import ManagedModel, ManagedModelStore, merge_managed_models + + +def profile(**kwargs): + return ManagedModel(name="managed-test", model="test-model", base_url="https://example.com/v1", api_key="test-secret", **kwargs) + + +@pytest.fixture +def store(tmp_path, monkeypatch): + monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path)) + return ManagedModelStore() + + +def test_persist_encrypt_preserve_secret_and_revision(store): + with pytest.raises(FileNotFoundError): + store.save(profile(), expected_revision="missing") + first = store.save(profile(), expected_revision=None) + assert b"test-secret" not in store.path.read_bytes() + assert ManagedModelStore().list()[0].api_key.get_secret_value() == "test-secret" + updated = store.save(profile(enabled=False).model_copy(update={"api_key": None}), expected_revision=first.revision) + assert updated.api_key.get_secret_value() == "test-secret" + assert updated.revision != first.revision + with pytest.raises(FileExistsError): + store.save(profile(), expected_revision=first.revision) + with pytest.raises(FileExistsError): + store.save(profile(), expected_revision=None) + + +def test_snapshot_merge_yaml_precedence_and_disable(store): + base = AppConfig.model_validate({"sandbox": {"use": "test"}, "models": [{"name": "yaml", "model": "yaml", "use": "test"}]}) + first = store.save(profile(), expected_revision=None) + merged = merge_managed_models(base) + assert [m.name for m in merged.models] == ["yaml", "managed-test"] + assert merged.get_model_config("managed-test").api_key == "test-secret" + assert base.get_model_config("managed-test") is None + store.save(profile(enabled=False), expected_revision=first.revision) + assert merge_managed_models(base).get_model_config("managed-test") is None + assert merged.get_model_config("managed-test") is not None + store.save(profile().model_copy(update={"name": "yaml"}), expected_revision=None) + assert merge_managed_models(base).get_model_config("yaml").use == "test" + + +def test_missing_encryption_key_never_replaced(store): + store.save(profile(), expected_revision=None) + store.key_path.unlink() + with pytest.raises(ValueError, match="key"): + store.list() + assert not store.key_path.exists() + + +@pytest.mark.parametrize("url", ["file:///tmp/test", "https://user:pass@example.com/v1", "https://example.com/v1?key=abc", "https://example.com/#fragment"]) +def test_endpoint_validation(url): + with pytest.raises(ValidationError): + ManagedModel(name="test", model="test", base_url=url) + + +@pytest.mark.asyncio +async def test_admin_gate_and_response_redaction(store, monkeypatch): + from app.gateway.routers import managed_models as router + + config = AppConfig.model_validate({"sandbox": {"use": "test"}}) + monkeypatch.setattr(router, "get_app_config", lambda: config) + admin = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(system_role="admin"))) + member = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(system_role="user"))) + body = router.SaveModelRequest(config=profile()) + with pytest.raises(HTTPException) as exc: + await router.save_model(member, body) + assert exc.value.status_code == 403 + result = await router.save_model(admin, body) + assert result["has_api_key"] is True + assert "api_key" not in result + assert "test-secret" not in str(await router.list_managed_models(admin)) + with pytest.raises(HTTPException) as exc: + await router.list_managed_models(member) + assert exc.value.status_code == 403 + + +def test_config_loader_sees_changes_without_yaml_write(store, tmp_path, monkeypatch): + from deerflow.config.app_config import get_app_config, reset_app_config + + path = tmp_path / "config.yaml" + original = "sandbox:\n use: test\nmodels: []\n" + path.write_text(original, encoding="utf-8") + monkeypatch.setenv("DEER_FLOW_CONFIG_PATH", str(path)) + monkeypatch.delenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", raising=False) + reset_app_config() + try: + assert not get_app_config().models + saved = store.save(profile(), expected_revision=None) + snapshot = get_app_config() + assert snapshot.get_model_config("managed-test").model == "test-model" + assert get_app_config() is snapshot + store.save(profile(enabled=False), expected_revision=saved.revision) + assert not get_app_config().models + assert snapshot.get_model_config("managed-test") is not None + assert path.read_text(encoding="utf-8") == original + finally: + reset_app_config() + + +def test_parallel_writes_preserve_all_models(store): + from concurrent.futures import ThreadPoolExecutor + + def save(index): + store.save(profile().model_copy(update={"name": f"model-{index}"}), expected_revision=None) + + with ThreadPoolExecutor(max_workers=8) as executor: + list(executor.map(save, range(16))) + assert len(store.list()) == 16 + + +def test_clear_key_and_corrupt_catalog_fail_closed(store): + from pydantic import SecretStr + + previous = store.save(profile(), expected_revision=None) + saved = store.save(profile().model_copy(update={"api_key": SecretStr("")}), expected_revision=previous.revision) + assert saved.public()["has_api_key"] is False + assert store.list()[0].runtime_config().api_key == "not-required" + store.path.write_bytes(b"broken") + with pytest.raises(ValueError): + store.save(profile(), expected_revision=None) + assert store.path.read_bytes() == b"broken" + + +@pytest.mark.asyncio +async def test_yaml_name_reserved_and_pat_denied(store, monkeypatch): + from app.gateway.auth_disabled import AUTH_SOURCE_PAT + from app.gateway.routers import managed_models as router + + config = AppConfig.model_validate({"sandbox": {"use": "test"}, "models": [{"name": "managed-test", "model": "yaml", "use": "test"}]}) + monkeypatch.setattr(router, "get_app_config", lambda: config) + request = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(system_role="admin"))) + body = router.SaveModelRequest(config=profile()) + with pytest.raises(HTTPException) as exc: + await router.save_model(request, body) + assert exc.value.status_code == 409 + request.state.auth_source = AUTH_SOURCE_PAT + for operation in (router.save_model, router.test_model): + with pytest.raises(HTTPException) as exc: + await operation(request, body) + assert exc.value.status_code == 403 + assert not store.path.exists() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("mode,expected", [("tool", "success"), ("text", "tool_call_missing"), ("error", "connection_failed")]) +async def test_connection_probe_is_bounded_redacted_and_does_not_save(store, monkeypatch, mode, expected): + import langchain_openai + from langchain_core.messages import AIMessageChunk + + from app.gateway.routers import managed_models as router + + captured = {} + + class FakeModel: + def __init__(self, **kwargs): + captured.update(kwargs) + + def bind_tools(self, tools, **kwargs): + assert kwargs["tool_choice"] == "connection_check" + return self + + async def astream(self, *args, **kwargs): + if mode == "error": + raise RuntimeError("test-secret should never be returned") + yield AIMessageChunk(content="", tool_call_chunks=[{"name": "connection_check", "args": "{}", "id": "call-1", "index": 0}] if mode == "tool" else []) + + monkeypatch.setattr(langchain_openai, "ChatOpenAI", FakeModel) + request = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(system_role="admin"))) + result = await router.test_model(request, router.SaveModelRequest(config=profile())) + assert result["message"] == expected + assert "test-secret" not in str(result) + assert captured["max_retries"] == 0 + assert captured["timeout"] == 15 + assert not store.path.exists() + + +def test_separate_process_writers_share_catalog_lock(store): + import os + import subprocess + import sys + + script = """ +import sys +from deerflow.config.managed_models import ManagedModelStore, ManagedModel +store = ManagedModelStore() +for index in range(5): + store.save(ManagedModel(name=f'{sys.argv[1]}-{index}', model='test', base_url='https://example.com/v1', api_key='secret'), expected_revision=None) +""" + processes = [subprocess.Popen([sys.executable, "-c", script, str(index)], env=os.environ.copy(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) for index in range(3)] + try: + for process in processes: + output, errors = process.communicate(timeout=30) + assert process.returncode == 0, (output, errors) + finally: + for process in processes: + if process.poll() is None: + process.kill() + process.wait() + assert len(store.list()) == 15 + if os.name == "posix": + assert store.path.stat().st_mode & 0o777 == 0o600 + assert store.key_path.stat().st_mode & 0o777 == 0o600 diff --git a/docs/pr-evidence/model-management.png b/docs/pr-evidence/model-management.png new file mode 100644 index 0000000000000000000000000000000000000000..4093841be9b24409d31d1746bb5c798baa320394 GIT binary patch literal 94675 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 + +### Shared model settings + +Settings → Models (`?settings=models`) offers administrator-only catalog management +through `/api/managed-models`. YAML entries are read-only. `core/models/management.ts` +whitelists editable fields so source metadata and `has_api_key` never get posted. +Draft credentials stay in editor state, never query cache or browser storage; blank +keeps the saved key, explicit removal sends an empty key. Saving invalidates both the +admin catalog and `MODELS_QUERY_KEY`. Editor unmount aborts probes and fences late +callbacks. Static demos and non-admin users must not query the management API. diff --git a/frontend/src/components/workspace/settings/model-settings-page.tsx b/frontend/src/components/workspace/settings/model-settings-page.tsx new file mode 100644 index 000000000..e79c7621e --- /dev/null +++ b/frontend/src/components/workspace/settings/model-settings-page.tsx @@ -0,0 +1,377 @@ +"use client"; + +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { useAuth } from "@/core/auth/AuthProvider"; +import { useI18n } from "@/core/i18n/hooks"; +import { MODELS_QUERY_KEY } from "@/core/models/hooks"; +import { + loadManagedModels, + modelDraft, + saveManagedModel, + testManagedModel, + type ManagedModel, +} from "@/core/models/management"; +import { isStaticWebsiteOnly } from "@/core/static-mode"; + +import { SettingsSection } from "./settings-section"; + +export function ModelSettingsPage() { + const { user } = useAuth(); + const { t } = useI18n(); + const text = t.settings.models; + const client = useQueryClient(); + const canManage = user?.system_role === "admin" && !isStaticWebsiteOnly(); + const queryKey = ["managed-models", user?.id]; + const catalog = useQuery({ + queryKey, + queryFn: ({ signal }) => loadManagedModels(signal), + enabled: canManage, + }); + const [editing, setEditing] = useState(null); + const [pending, setPending] = useState(false); + async function refresh() { + await Promise.all([ + client.invalidateQueries({ queryKey: ["managed-models"] }), + client.invalidateQueries({ queryKey: MODELS_QUERY_KEY }), + ]); + } + async function toggle(model: ManagedModel) { + setPending(true); + try { + await saveManagedModel({ + config: { ...modelDraft(model), enabled: !model.enabled }, + expected_revision: model.revision, + }); + await refresh(); + } catch (error) { + toast.error(error instanceof Error ? error.message : text.failed); + } finally { + setPending(false); + } + } + return ( + + {!canManage ? ( +

{text.adminOnly}

+ ) : ( +
+
+ + +
+ {catalog.isLoading &&

{text.loading}

} + {catalog.error && ( +
+

{text.failed}

+
+ )} + {catalog.data?.models.length === 0 &&

{text.empty}

} + {catalog.data?.models.map((model) => ( +
+
+

+ {model.display_name || model.name} +

+

+ {model.model} ·{" "} + {model.source === "config" + ? text.yaml + : model.enabled + ? text.enabled + : text.disabled} +

+ {model.source === "managed" && model.conflict && ( +

{text.conflict}

+ )} +
+ {model.source === "managed" && ( +
+ + +
+ )} +
+ ))} + {editing && ( + setEditing(null)} + saved={refresh} + /> + )} +
+ )} +
+ ); +} + +function ModelEditor({ + model, + close, + saved, +}: { + model?: ManagedModel; + close: () => void; + saved: () => Promise; +}) { + const { t } = useI18n(); + const text = t.settings.models; + const [draft, setDraft] = useState(() => modelDraft(model)); + const [key, setKey] = useState(""); + const [clearKey, setClearKey] = useState(false); + const [pending, setPending] = useState(false); + const [result, setResult] = useState(""); + const active = useRef(true); + const abort = useRef(null); + useEffect(() => { + active.current = true; + return () => { + active.current = false; + abort.current?.abort(); + }; + }, []); + function body() { + return { + config: { + ...draft, + ...(clearKey ? { api_key: "" } : key ? { api_key: key } : {}), + }, + expected_revision: model?.revision ?? null, + }; + } + async function submit(test: boolean) { + setPending(true); + setResult(""); + try { + if (test) { + abort.current = new AbortController(); + const response = await testManagedModel(body(), abort.current.signal); + if (active.current) setResult(text[response.message]); + } else { + await saveManagedModel(body()); + await saved(); + if (active.current) { + toast.success(text.saved); + close(); + } + } + } catch (error) { + if (active.current) + setResult(error instanceof Error ? error.message : text.failed); + } finally { + if (active.current) setPending(false); + } + } + return ( + { + if (!open && !pending) close(); + }} + > + + + {model ? text.edit : text.add} + {text.formDescription} + +
{ + event.preventDefault(); + void submit(false); + }} + > +
+
+

{text.provider}: OpenAI compatible

+ + + + + + {model?.has_api_key && ( + + )} + + + +
+
+ {result && ( +

+ {result} +

+ )} + + + + + +
+
+
+ ); +} diff --git a/frontend/src/components/workspace/settings/settings-dialog.tsx b/frontend/src/components/workspace/settings/settings-dialog.tsx index 2f454db96..355ea3420 100644 --- a/frontend/src/components/workspace/settings/settings-dialog.tsx +++ b/frontend/src/components/workspace/settings/settings-dialog.tsx @@ -1,6 +1,7 @@ "use client"; import { + BotIcon, BellIcon, CableIcon, InfoIcon, @@ -72,6 +73,11 @@ const SubagentSettingsPage = dynamic( ), { loading: SettingsPageLoading }, ); +const ModelSettingsPage = dynamic( + () => + import("./model-settings-page").then((module) => module.ModelSettingsPage), + { loading: SettingsPageLoading }, +); const AboutSettingsPage = dynamic( () => import("./about-settings-page").then((module) => module.AboutSettingsPage), @@ -79,6 +85,7 @@ const AboutSettingsPage = dynamic( ); export type SettingsSection = + | "models" | "account" | "appearance" | "channels" @@ -107,6 +114,7 @@ export function SettingsDialog(props: SettingsDialogProps) { const sections = useMemo( () => [ + { id: "models", label: t.settings.sections.models, icon: BotIcon }, { id: "account", label: t.settings.sections.account, @@ -140,6 +148,7 @@ export function SettingsDialog(props: SettingsDialogProps) { { id: "about", label: t.settings.sections.about, icon: InfoIcon }, ], [ + t.settings.sections.models, t.settings.sections.account, t.settings.sections.appearance, t.settings.sections.channels, @@ -191,6 +200,7 @@ export function SettingsDialog(props: SettingsDialogProps) {
+ {activeSection === "models" && } {activeSection === "account" && } {activeSection === "appearance" && } {activeSection === "memory" && } diff --git a/frontend/src/components/workspace/workspace-settings-deep-link.tsx b/frontend/src/components/workspace/workspace-settings-deep-link.tsx index deda4936a..90bcf69f2 100644 --- a/frontend/src/components/workspace/workspace-settings-deep-link.tsx +++ b/frontend/src/components/workspace/workspace-settings-deep-link.tsx @@ -10,6 +10,7 @@ import { } from "./settings"; const SETTINGS_SECTIONS = new Set([ + "models", "account", "appearance", "channels", diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts index de0f2cafc..23737fb4a 100644 --- a/frontend/src/core/i18n/locales/en-US.ts +++ b/frontend/src/core/i18n/locales/en-US.ts @@ -1209,7 +1209,50 @@ export const enUS: Translations = { settings: { title: "Settings", description: "Adjust how DeerFlow looks and behaves for you.", + models: { + title: "Models", + description: + "Manage shared models available to users. Models from the server configuration are read-only.", + adminOnly: + "Only administrators can manage shared models. This feature is unavailable in demos.", + add: "Add model", + loading: "Loading models…", + failed: "Could not complete the request.", + reload: "Reload", + empty: "No models configured.", + yaml: "Server configuration · read-only", + enabled: "Enabled", + disabled: "Disabled", + conflict: "This name is reserved by the server configuration.", + edit: "Edit model", + enable: "Enable", + disable: "Disable", + formDescription: + "Connect an OpenAI-compatible endpoint. Testing sends a short streaming tool-call request and may incur provider charges.", + provider: "Provider", + name: "Unique name", + displayName: "Display name", + endpoint: "Base URL", + modelId: "Model ID", + keepKey: "Leave blank to keep the saved key", + optionalKey: "Optional for endpoints without authentication", + clearKey: "Remove the saved API key", + contextWindow: "Context window (optional)", + maxTokens: "Maximum output tokens (optional)", + vision: "Supports image input", + cancel: "Cancel", + test: "Test connection", + working: "Working…", + save: "Save", + saved: "Model saved", + success: "Streaming and tool-call test passed.", + tool_call_missing: + "The endpoint responded, but did not return a tool call. Check the model’s tool support.", + connection_failed: + "Connection test failed. Check the endpoint, credentials, model ID and streaming/tool support.", + }, sections: { + models: "Models", account: "Account", appearance: "Appearance", channels: "Channels", diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts index c7c0d3adc..eb5a96f6b 100644 --- a/frontend/src/core/i18n/locales/types.ts +++ b/frontend/src/core/i18n/locales/types.ts @@ -1022,7 +1022,45 @@ export interface Translations { settings: { title: string; description: string; + models: { + title: string; + description: string; + adminOnly: string; + add: string; + loading: string; + failed: string; + reload: string; + empty: string; + yaml: string; + enabled: string; + disabled: string; + conflict: string; + edit: string; + enable: string; + disable: string; + formDescription: string; + provider: string; + name: string; + displayName: string; + endpoint: string; + modelId: string; + keepKey: string; + optionalKey: string; + clearKey: string; + contextWindow: string; + maxTokens: string; + vision: string; + cancel: string; + test: string; + working: string; + save: string; + saved: string; + success: string; + tool_call_missing: string; + connection_failed: string; + }; sections: { + models: string; account: string; appearance: string; channels: string; diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index 127d09c08..ef478e388 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -1140,7 +1140,48 @@ export const zhCN: Translations = { settings: { title: "设置", description: "根据你的偏好调整 DeerFlow 的界面和行为。", + models: { + title: "模型", + description: "管理供用户选择的共享模型。服务器配置中的模型为只读。", + adminOnly: "只有管理员可以管理共享模型,演示模式不支持此功能。", + add: "添加模型", + loading: "正在加载模型…", + failed: "请求未能完成。", + reload: "重新加载", + empty: "尚未配置模型。", + yaml: "服务器配置 · 只读", + enabled: "已启用", + disabled: "已停用", + conflict: "此名称已被服务器配置占用。", + edit: "编辑模型", + enable: "启用", + disable: "停用", + formDescription: + "接入 OpenAI 兼容接口。测试将发送简短的流式工具调用请求,可能产生模型调用费用。", + provider: "接口类型", + name: "唯一名称", + displayName: "显示名称", + endpoint: "接口基础地址", + modelId: "模型 ID", + keepKey: "留空以保留已保存的密钥", + optionalKey: "无认证接口可以留空", + clearKey: "移除已保存的 API Key", + contextWindow: "上下文窗口(可选)", + maxTokens: "最大输出 Token 数(可选)", + vision: "支持图片输入", + cancel: "取消", + test: "测试连接", + working: "处理中…", + save: "保存", + saved: "模型已保存", + success: "流式输出和工具调用测试通过。", + tool_call_missing: + "接口已响应,但未返回工具调用,请检查模型的工具调用能力。", + connection_failed: + "连接测试失败,请检查接口地址、凭据、模型 ID 以及流式输出和工具调用支持。", + }, sections: { + models: "模型", account: "账号", appearance: "外观", channels: "渠道", diff --git a/frontend/src/core/models/management.ts b/frontend/src/core/models/management.ts new file mode 100644 index 000000000..e68fd4284 --- /dev/null +++ b/frontend/src/core/models/management.ts @@ -0,0 +1,89 @@ +import { throwGatewayApiError } from "@/core/api/errors"; +import { fetch } from "@/core/api/fetcher"; +import { getBackendBaseURL } from "@/core/config"; + +export type ModelDraft = { + name: string; + display_name: string; + provider: "openai-compatible"; + model: string; + base_url: string; + api_key?: string; + enabled: boolean; + supports_vision: boolean; + context_window: number | null; + max_tokens: number | null; +}; +export type ManagedModel = Omit & { + revision: string; + source: "managed"; + has_api_key: boolean; + conflict?: boolean; +}; +export type ConfigModel = { + name: string; + display_name: string; + model: string; + source: "config"; + enabled: boolean; +}; +export type SaveModelRequest = { + config: ModelDraft; + expected_revision: string | null; +}; + +async function request( + suffix: string, + method: string, + body?: SaveModelRequest, + signal?: AbortSignal, +): Promise { + const response = await fetch( + `${getBackendBaseURL()}/api/managed-models${suffix}`, + { + method, + signal, + ...(body + ? { + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + } + : {}), + }, + ); + if (!response.ok) + await throwGatewayApiError(response, "Model management request failed"); + return response.json() as Promise; +} + +export const loadManagedModels = (signal?: AbortSignal) => + request<{ models: (ManagedModel | ConfigModel)[] }>( + "", + "GET", + undefined, + signal, + ); +export const saveManagedModel = (body: SaveModelRequest) => + request("", "PUT", body); +export const testManagedModel = ( + body: SaveModelRequest, + signal?: AbortSignal, +) => + request<{ + ok: boolean; + message: "success" | "tool_call_missing" | "connection_failed"; + }>("/test", "POST", body, signal); + +export function modelDraft(model?: ManagedModel): ModelDraft { + return { + name: model?.name ?? "", + display_name: model?.display_name ?? "", + provider: "openai-compatible", + model: model?.model ?? "", + base_url: model?.base_url ?? "", + enabled: model?.enabled ?? true, + supports_vision: model?.supports_vision ?? false, + context_window: model?.context_window ?? null, + max_tokens: model?.max_tokens ?? null, + }; +} diff --git a/frontend/tests/e2e/model-management.spec.ts b/frontend/tests/e2e/model-management.spec.ts new file mode 100644 index 000000000..508d98d35 --- /dev/null +++ b/frontend/tests/e2e/model-management.spec.ts @@ -0,0 +1,85 @@ +import { expect, test } from "@playwright/test"; + +import type { ManagedModel, SaveModelRequest } from "@/core/models/management"; + +import { mockLangGraphAPI, MOCK_THREAD_ID } from "./utils/mock-api"; + +test("administrator adds, tests, edits and disables a shared model", async ({ + page, +}, testInfo) => { + mockLangGraphAPI(page, { threads: [{ thread_id: MOCK_THREAD_ID }] }); + let models: ManagedModel[] = []; + let probes = 0; + let catalogReads = 0; + const saves: SaveModelRequest[] = []; + await page.route("**/api/models", (route) => { + catalogReads++; + return route.fulfill({ + json: { + models: models.filter((model) => model.enabled), + token_usage: { enabled: false }, + }, + }); + }); + await page.route("**/api/managed-models", async (route) => { + if (route.request().method() === "GET") + return route.fulfill({ json: { models } }); + const body = route.request().postDataJSON() as SaveModelRequest; + saves.push(body); + const { api_key, ...config } = body.config; + models = [ + { + ...config, + source: "managed", + has_api_key: + api_key === undefined ? (models[0]?.has_api_key ?? false) : !!api_key, + revision: String(saves.length), + }, + ]; + await route.fulfill({ json: models[0] }); + }); + await page.route("**/api/managed-models/test", (route) => { + probes++; + return route.fulfill({ json: { ok: true, message: "success" } }); + }); + await page.goto(`/workspace/chats/${MOCK_THREAD_ID}?settings=models`); + await page.getByRole("button", { name: "Add model", exact: true }).click(); + await page.getByLabel("Unique name").fill("demo-model"); + await page.getByLabel("Display name").fill("Demo model"); + await page.getByLabel("Base URL").fill("https://example.com/v1"); + await page.getByLabel("Model ID").fill("demo"); + await page.getByLabel("API Key", { exact: true }).fill("demo-key"); + await page + .getByRole("button", { name: "Test connection", exact: true }) + .click(); + await expect( + page.getByText("Streaming and tool-call test passed."), + ).toBeVisible(); + expect(probes).toBe(1); + expect(saves).toHaveLength(0); + await page.screenshot({ path: testInfo.outputPath("model-editor.png") }); + const readsBeforeSave = catalogReads; + await page.getByRole("button", { name: "Save", exact: true }).click(); + await expect( + page + .getByRole("dialog", { name: "Settings", exact: true }) + .getByText("Demo model", { exact: true }), + ).toBeVisible(); + await expect.poll(() => catalogReads).toBeGreaterThan(readsBeforeSave); + await page.getByRole("button", { name: "Edit model", exact: true }).click(); + await expect(page.getByLabel("API Key", { exact: true })).toHaveValue(""); + await page.getByLabel("Display name").fill("Updated model"); + await page.getByRole("button", { name: "Save", exact: true }).click(); + await expect( + page + .getByRole("dialog", { name: "Settings", exact: true }) + .getByText("Updated model", { exact: true }), + ).toBeVisible(); + expect(saves[1]?.config).not.toHaveProperty("api_key"); + expect(saves[1]?.expected_revision).toBe("1"); + await page.getByRole("button", { name: "Disable", exact: true }).click(); + await expect( + page.getByRole("button", { name: "Enable", exact: true }), + ).toBeVisible(); + expect(saves[2]?.config.enabled).toBe(false); +}); diff --git a/frontend/tests/unit/components/workspace/lazy-panels.test.ts b/frontend/tests/unit/components/workspace/lazy-panels.test.ts index 3c7097c9e..17cdd1c56 100644 --- a/frontend/tests/unit/components/workspace/lazy-panels.test.ts +++ b/frontend/tests/unit/components/workspace/lazy-panels.test.ts @@ -23,7 +23,7 @@ describe("interaction-only bundle boundaries", () => { const dialog = read( "src/components/workspace/settings/settings-dialog.tsx", ); - expect(dialog.match(/dynamic\(/g)).toHaveLength(7); + expect(dialog.match(/dynamic\(/g)).toHaveLength(8); expect(dialog).not.toMatch( /import \{ \w+SettingsPage \} from "@\/components\/workspace\/settings\//, ); diff --git a/frontend/tests/unit/components/workspace/settings/model-settings-page.dom.test.tsx b/frontend/tests/unit/components/workspace/settings/model-settings-page.dom.test.tsx new file mode 100644 index 000000000..91634e3bc --- /dev/null +++ b/frontend/tests/unit/components/workspace/settings/model-settings-page.dom.test.tsx @@ -0,0 +1,134 @@ +import { afterEach, beforeEach, expect, test, rs } from "@rstest/core"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { + cleanup, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; + +import { ModelSettingsPage } from "@/components/workspace/settings/model-settings-page"; +import { useAuth } from "@/core/auth/AuthProvider"; +import { enUS } from "@/core/i18n/locales/en-US"; +import type * as Management from "@/core/models/management"; +import { + loadManagedModels, + modelDraft, + saveManagedModel, + testManagedModel, + type ManagedModel, +} from "@/core/models/management"; + +rs.mock("@/core/auth/AuthProvider", () => ({ useAuth: rs.fn() })); +rs.mock("@/core/i18n/hooks", () => ({ useI18n: () => ({ t: enUS }) })); +rs.mock("@/core/models/management", () => ({ + ...rs.requireActual("@/core/models/management"), + loadManagedModels: rs.fn(), + saveManagedModel: rs.fn(), + testManagedModel: rs.fn(), +})); + +const existing: ManagedModel = { + ...modelDraft(), + name: "custom", + model: "test", + base_url: "https://example.com/v1", + display_name: "Custom", + source: "managed", + has_api_key: true, + revision: "v1", +}; +const auth = (role: "admin" | "user") => + ({ user: { id: "admin-id", system_role: role } }) as ReturnType< + typeof useAuth + >; + +beforeEach(() => { + rs.mocked(useAuth).mockReturnValue(auth("admin")); + rs.mocked(loadManagedModels).mockResolvedValue({ models: [existing] }); + rs.mocked(saveManagedModel).mockResolvedValue({ + ...existing, + revision: "v2", + }); +}); +afterEach(() => { + cleanup(); + rs.clearAllMocks(); +}); + +function mount() { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + client.setQueryData(["models"], { models: [] }); + render( + + + , + ); + return client; +} + +test("non-admin cannot load credentials configuration or add models", () => { + rs.mocked(useAuth).mockReturnValue(auth("user")); + mount(); + expect(screen.queryByRole("button", { name: "Add model" })).toBeNull(); + expect(loadManagedModels).not.toHaveBeenCalled(); +}); + +test("editing preserves saved key and refreshes chat catalog after saving", async () => { + const client = mount(); + fireEvent.click(await screen.findByRole("button", { name: "Edit model" })); + const input = screen.getByLabelText("API Key"); + expect(input.value).toBe(""); + fireEvent.change(screen.getByLabelText("Display name"), { + target: { value: "Updated" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Save" })); + await waitFor(() => expect(saveManagedModel).toHaveBeenCalledTimes(1)); + expect(rs.mocked(saveManagedModel).mock.calls[0]?.[0]).toMatchObject({ + config: { display_name: "Updated" }, + expected_revision: "v1", + }); + expect( + rs.mocked(saveManagedModel).mock.calls[0]?.[0].config, + ).not.toHaveProperty("api_key"); + await waitFor(() => + expect(client.getQueryState(["models"])?.isInvalidated).toBe(true), + ); +}); + +test("failed save keeps draft open and displays conflict", async () => { + rs.mocked(saveManagedModel).mockRejectedValueOnce( + new Error("Model changed; reload before saving"), + ); + mount(); + fireEvent.click(await screen.findByRole("button", { name: "Edit model" })); + fireEvent.change(screen.getByLabelText("API Key"), { + target: { value: "replacement" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Save" })); + expect( + await screen.findByText("Model changed; reload before saving"), + ).toBeTruthy(); + expect(screen.getByLabelText("API Key").value).toBe( + "replacement", + ); +}); + +test("connection test never saves and supports explicit key removal", async () => { + rs.mocked(testManagedModel).mockResolvedValueOnce({ + ok: true, + message: "success", + }); + mount(); + fireEvent.click(await screen.findByRole("button", { name: "Edit model" })); + fireEvent.click(screen.getByLabelText("Remove the saved API key")); + fireEvent.click(screen.getByRole("button", { name: "Test connection" })); + expect(await screen.findByText(enUS.settings.models.success)).toBeTruthy(); + expect(rs.mocked(testManagedModel).mock.calls[0]?.[0].config.api_key).toBe( + "", + ); + expect(saveManagedModel).not.toHaveBeenCalled(); +});