enh: dynamic select options valve

This commit is contained in:
Timothy Jaeryang Baek
2026-01-22 03:55:07 +04:00
parent 00b3583dc2
commit 474427c67e
4 changed files with 177 additions and 4 deletions

View File

@@ -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(

View File

@@ -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(

View File

@@ -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.

View File

@@ -116,6 +116,27 @@
}}
/>
</div>
{:else if valvesSpec.properties[property]?.input?.type === 'select' && valvesSpec.properties[property]?.input?.options}
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100/30 dark:border-gray-850/30"
bind:value={valves[property]}
on:change={() => {
dispatch('change');
}}
>
<option value="" disabled>{valvesSpec.properties[property]?.description ?? $i18n.t('Select an option')}</option>
{#each valvesSpec.properties[property].input.options as option}
{#if typeof option === 'object' && option !== null}
<option value={option.value} selected={option.value === valves[property]}>
{option.label ?? option.value}
</option>
{:else}
<option value={option} selected={option === valves[property]}>
{option}
</option>
{/if}
{/each}
</select>
{:else if valvesSpec.properties[property]?.input?.type === 'color'}
<div class="flex items-center space-x-2">
<div class="relative size-6">