From b1bfc1876229cf3be8bc993428d8d09f9e807e02 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 25 Aug 2026 22:00:34 +0200 Subject: [PATCH] perf: cache the serialized builtin tool spec instead of deep-copying it per request (#28860) Every chat request hands each builtin tool a fresh copy of its cached spec, because callers mutate what they get. That copy was a full deepcopy of a nested dict, repeated per tool per message. The builder now caches the spec already serialized, so a request only parses it back. Parsing is what produces the independent tree callers mutate, and the cached value becomes an immutable string, so a request can no longer reach the cached object at all. Measured on CPython 3.12 with a 1.1 KB spec and 20 builtin tools per request: | | before | after | |---|---|---| | stdlib json, the default | 276.2 us | 66.4 us | | orjson | 279.5 us | 37.5 us | Builtin specs are plain JSON by construction: pydantic normalizes every default before it reaches the schema, so a tuple, set, enum or datetime cannot appear in one, and an unserializable default is dropped rather than embedded. --- backend/open_webui/utils/tools.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 1bdaa33e41..cc9bba3cb4 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -984,14 +984,15 @@ def get_builtin_function_introspection(func: Callable): @cache -def build_builtin_tool_spec(func: Callable) -> dict: +def build_builtin_tool_spec_json(func: Callable) -> str: pydantic_model = convert_function_to_pydantic_model(func, get_builtin_function_introspection(func)) spec = convert_pydantic_model_to_openai_function_spec(pydantic_model) - return clean_openai_tool_schema(spec) + return JSONCodec.dumps(clean_openai_tool_schema(spec)) def get_builtin_tool_spec(func: Callable) -> dict: - return copy.deepcopy(build_builtin_tool_spec(func)) + # callers mutate the spec, so parse a fresh copy out of the cached JSON + return JSONCodec.loads(build_builtin_tool_spec_json(func)) def get_functions_from_tool(tool: object) -> list[Callable]: