diff --git a/litellm/proxy/_experimental/mcp_server/toolset_db.py b/litellm/proxy/_experimental/mcp_server/toolset_db.py index ca6234b8a5b..48bad178927 100644 --- a/litellm/proxy/_experimental/mcp_server/toolset_db.py +++ b/litellm/proxy/_experimental/mcp_server/toolset_db.py @@ -136,14 +136,14 @@ async def update_mcp_toolset( tool list, so a null ``toolset_name`` or ``tools`` is a no-op rather than a clear; emptying the tool selection is an explicit ``[]``, which cannot be mistaken for a caller that left the field out.""" - data_dict: Final = data.model_dump(exclude_unset=True, exclude={"toolset_id"}) - if data_dict.get("toolset_name", "") is None: - _ = data_dict.pop("toolset_name") - if data_dict.get("tools", "") is None: - _ = data_dict.pop("tools") - if "tools" in data_dict: - data_dict["tools"] = json.dumps(data_dict["tools"]) - data_dict["updated_by"] = touched_by + data_dict: Final = dict( # mutable-ok: Prisma requires a plain dict for JSON query serialization + ( + (field, json.dumps(value) if field == "tools" else value) + for field, value in data.model_dump(exclude_unset=True).items() + if field != "toolset_id" and (field not in ("toolset_name", "tools") or value is not None) + ), + updated_by=touched_by, + ) try: row: Final = await _toolset_table(prisma_client).update( where={"toolset_id": data.toolset_id}, diff --git a/tests/e2e/models.py b/tests/e2e/models.py index b4fd1d0ac1b..62810e6cfd9 100644 --- a/tests/e2e/models.py +++ b/tests/e2e/models.py @@ -607,7 +607,7 @@ class ToolsetRow(BaseModel): toolset_id: str toolset_name: str description: str | None = None - tools: list[ToolsetTool] = [] + tools: list[ToolsetTool] = Field(default_factory=list) class ToolsetListResponse(RootModel[list[ToolsetRow]]): diff --git a/tests/e2e/proxy_client.py b/tests/e2e/proxy_client.py index e50dcc5ce68..1bac5116a9d 100644 --- a/tests/e2e/proxy_client.py +++ b/tests/e2e/proxy_client.py @@ -12,6 +12,7 @@ import time import warnings from collections.abc import Callable, Mapping from dataclasses import dataclass +from functools import reduce from datetime import datetime from types import MappingProxyType from typing import Final @@ -296,9 +297,14 @@ def await_everywhere[T]( """`_last_answer` against every replica in turn, each with the full budget, so a write counts as visible only once the last replica reflects it, and stop at the first replica that never converges. Clock and sleep are injected.""" - answers: dict[str, T] = {} - for replica, read in reads.items(): - answer = _last_answer( + def read_replica( + outcome: EverywhereConverged[T] | NeverConvergedOn[T], + item: tuple[str, ReplicaRead[T]], + ) -> EverywhereConverged[T] | NeverConvergedOn[T]: + if isinstance(outcome, NeverConvergedOn): + return outcome + replica, read = item + answer: Final = _last_answer( read, settled=settled, timeout=timeout, @@ -309,8 +315,10 @@ def await_everywhere[T]( ) if not settled(answer): return NeverConvergedOn(replica=replica, last=answer) - answers[replica] = answer - return EverywhereConverged(answers=MappingProxyType(answers)) + return EverywhereConverged(answers=MappingProxyType({**outcome.answers, replica: answer})) + + initial: Final[EverywhereConverged[T] | NeverConvergedOn[T]] = EverywhereConverged(answers=MappingProxyType({})) + return reduce(read_replica, reads.items(), initial) def _is_not_found[R: BaseModel](result: Result[R]) -> bool: