Files
owui-tools/app/owuitools/toolconfig.py
T
2026-09-15 15:24:03 -04:00

80 lines
3.1 KiB
Python

"""Which tools exist, read from configs/tools.toml.
WHY THIS IS A REMOVAL AND NOT A FLAG
Open WebUI has a Function Name Filter List on the tool connection, and it
was the only way to turn a tool off before this file. It hides a tool
from one model. The schema is still fetched, the endpoint still answers,
and the setting lives in Open WebUI's database rather than anywhere you
can read in the repo - so "which tools does this deployment offer" had no
answer on disk at all.
This drops the route instead. An operation that is not registered is not
in the spec, so the model is never told it exists and no context is spent
describing it; and the path 404s, so a stale cached spec somewhere else
cannot call it either.
THE SEAM IS main.py, NOT toolbase.tool
`tool()` wraps the handler, which runs long after FastAPI has registered
the route and built its description. A check there could refuse a call
but could never stop the model being told about the tool - which is the
expensive half.
FAIL OPEN, DELIBERATELY
A missing or unparseable file leaves every tool on. This is config that
removes capability, so the failure mode of losing it should be the
system it replaces, not a server with no tools. Both cases log a line
saying what happened; silence here would look identical to "everything is
switched on", which is exactly the confusion to avoid.
"""
from __future__ import annotations
import logging
import os
import tomllib
log = logging.getLogger(__name__)
CONFIG_PATH = os.environ.get("OWUITOOLS_TOOLS_CONFIG", "/configs/tools.toml")
def load(path: str | None = None) -> dict[str, bool]:
"""The file's contents as {tool_name: enabled}, or {} if unreadable."""
path = path or CONFIG_PATH
try:
with open(path, "rb") as handle:
table = tomllib.load(handle).get("tools", {})
except FileNotFoundError:
log.info("no tool config at %s - every tool stays enabled", path)
return {}
except (OSError, tomllib.TOMLDecodeError) as exc:
log.warning("tool config at %s could not be read (%s) - every tool stays enabled",
path, exc)
return {}
return {str(name): bool(value) for name, value in table.items()}
def disabled(known: set[str], path: str | None = None) -> set[str]:
"""Which of `known` this deployment has switched off.
`known` is what the server actually implements. Anything named in the
file that is not in it is reported and ignored - a typo would
otherwise be indistinguishable from a tool that is switched off, and
the whole point of the file is to be able to read what is on.
A known tool the file does not mention stays ON, so a config written
against an older version never silently removes a new tool.
"""
config = load(path)
unknown = sorted(set(config) - known)
if unknown:
log.warning("tool config names %d tool(s) this server does not have: %s",
len(unknown), ", ".join(unknown))
off = {name for name, on in config.items() if not on and name in known}
if off:
log.info("tools switched off in config: %s", ", ".join(sorted(off)))
return off