From 474427c67e953bb9f7d122757a756a639214e0b2 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 22 Jan 2026 03:55:07 +0400 Subject: [PATCH] enh: dynamic select options valve --- backend/open_webui/routers/functions.py | 11 +- backend/open_webui/routers/tools.py | 11 +- backend/open_webui/utils/plugin.py | 138 ++++++++++++++++++++++++ src/lib/components/common/Valves.svelte | 21 ++++ 4 files changed, 177 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index ad4731891..a31b958e2 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -19,6 +19,7 @@ from open_webui.utils.plugin import ( load_function_module_by_id, replace_imports, get_function_module_from_cache, + resolve_valves_schema_options, ) from open_webui.config import CACHE_DIR from open_webui.constants import ERROR_MESSAGES @@ -446,7 +447,10 @@ async def get_function_valves_spec_by_id( if hasattr(function_module, "Valves"): Valves = function_module.Valves - return Valves.schema() + schema = Valves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(Valves, schema, user) + return schema return None else: raise HTTPException( @@ -546,7 +550,10 @@ async def get_function_user_valves_spec_by_id( if hasattr(function_module, "UserValves"): UserValves = function_module.UserValves - return UserValves.schema() + schema = UserValves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(UserValves, schema, user) + return schema return None else: raise HTTPException( diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 03018d24a..7f9b23c7c 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -25,6 +25,7 @@ from open_webui.utils.plugin import ( load_tool_module_by_id, replace_imports, get_tool_module_from_cache, + resolve_valves_schema_options, ) from open_webui.utils.tools import get_tool_specs from open_webui.utils.auth import get_admin_user, get_verified_user @@ -553,7 +554,10 @@ async def get_tools_valves_spec_by_id( if hasattr(tools_module, "Valves"): Valves = tools_module.Valves - return Valves.schema() + schema = Valves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(Valves, schema, user) + return schema return None else: raise HTTPException( @@ -662,7 +666,10 @@ async def get_tools_user_valves_spec_by_id( if hasattr(tools_module, "UserValves"): UserValves = tools_module.UserValves - return UserValves.schema() + schema = UserValves.schema() + # Resolve dynamic options for select dropdowns + schema = resolve_valves_schema_options(UserValves, schema, user) + return schema return None else: raise HTTPException( diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index 965b0a688..79a1c0f0d 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -6,6 +6,7 @@ from importlib import util import types import tempfile import logging +from typing import Any from open_webui.env import PIP_OPTIONS, PIP_PACKAGE_INDEX_OPTIONS, OFFLINE_MODE from open_webui.models.functions import Functions @@ -14,6 +15,143 @@ from open_webui.models.tools import Tools log = logging.getLogger(__name__) +def resolve_valves_schema_options( + valves_class: type, schema: dict, user: Any = None +) -> dict: + """ + Resolve dynamic options in a Valves schema. + + For properties with `input.options`, this function handles two cases: + - List: Used directly as dropdown options + - String: Treated as method name, called to get options dynamically + + Usage in Valves: + class UserValves(BaseModel): + # Static options + priority: str = Field( + default="medium", + json_schema_extra={ + "input": { + "type": "select", + "options": ["low", "medium", "high"] + } + } + ) + + # Dynamic options (method name) + model: str = Field( + default="", + json_schema_extra={ + "input": { + "type": "select", + "options": "get_model_options" + } + } + ) + + @classmethod + def get_model_options(cls, __user__=None) -> list[dict]: + return [{"value": "gpt-4", "label": "GPT-4"}] + + Args: + valves_class: The Valves or UserValves Pydantic model class + schema: The JSON schema dict from valves_class.schema() + user: Optional user object passed to methods that accept __user__ + + Returns: + Modified schema dict with resolved options + """ + if not schema or "properties" not in schema: + return schema + + # Make a copy to avoid mutating the original + schema = dict(schema) + schema["properties"] = dict(schema.get("properties", {})) + + for prop_name, prop_schema in list(schema["properties"].items()): + # Get the original field info from the Pydantic model + if not hasattr(valves_class, "model_fields"): + continue + + field_info = valves_class.model_fields.get(prop_name) + if not field_info: + continue + + # Check json_schema_extra for options + json_schema_extra = field_info.json_schema_extra + if not json_schema_extra or not isinstance(json_schema_extra, dict): + continue + + input_config = json_schema_extra.get("input") + if not input_config or not isinstance(input_config, dict): + continue + + options = input_config.get("options") + if options is None: + continue + + resolved_options = None + + # Case 1: options is already a list - use directly + if isinstance(options, list): + resolved_options = options + + # Case 2: options is a string - treat as method name + elif isinstance(options, str) and options: + method = getattr(valves_class, options, None) + if method is None or not callable(method): + log.warning( + f"options '{options}' not found or not callable on {valves_class.__name__}" + ) + continue + + try: + import inspect + + sig = inspect.signature(method) + params = sig.parameters + + # Prepare kwargs based on what the method accepts + kwargs = {} + if "__user__" in params and user is not None: + kwargs["__user__"] = ( + user.model_dump() if hasattr(user, "model_dump") else user + ) + if "user" in params and user is not None: + kwargs["user"] = ( + user.model_dump() if hasattr(user, "model_dump") else user + ) + + resolved_options = method(**kwargs) if kwargs else method() + + # Validate return type + if not isinstance(resolved_options, list): + log.warning( + f"Method '{options}' did not return a list for {prop_name}" + ) + continue + + except Exception as e: + log.warning(f"Failed to resolve options for {prop_name}: {e}") + continue + else: + # Invalid options type - skip + continue + + # Update the schema with resolved options + schema["properties"][prop_name] = dict(prop_schema) + if "input" not in schema["properties"][prop_name]: + schema["properties"][prop_name]["input"] = {"type": "select"} + else: + schema["properties"][prop_name]["input"] = dict( + schema["properties"][prop_name].get("input", {}) + ) + schema["properties"][prop_name]["input"]["options"] = resolved_options + + return schema + + + def extract_frontmatter(content): """ Extract frontmatter as a dictionary from the provided content string. diff --git a/src/lib/components/common/Valves.svelte b/src/lib/components/common/Valves.svelte index cad66c5b7..4557ab490 100644 --- a/src/lib/components/common/Valves.svelte +++ b/src/lib/components/common/Valves.svelte @@ -116,6 +116,27 @@ }} /> + {:else if valvesSpec.properties[property]?.input?.type === 'select' && valvesSpec.properties[property]?.input?.options} + {:else if valvesSpec.properties[property]?.input?.type === 'color'}