38 Commits

  • fix: prevent startup crash when function/tool has null user_id (#26850)
    The Function and Tool database columns declare user_id as a nullable
    String column, but their Pydantic read-models required a non-null
    string. A record with user_id NULL therefore raised a
    pydantic ValidationError inside get_functions()/get_tools(), which run
    during install_tool_and_function_dependencies() at app startup —
    crashing the whole application and blocking all chat completions.
    
    Make user_id Optional in the read/response models so such records
    validate gracefully (user is already rendered as None downstream when
    the id has no matching user) instead of taking down startup.
    
    
    Claude-Session: https://claude.ai/code/session_01Y4RRUNq7ZUFkRWbWPkDw3m
    
    Co-authored-by: Claude <noreply@anthropic.com>
  • perf: drop redundant session.refresh calls after commit across the model layer (#27381)
    Both session factories run with expire_on_commit=False, so ORM objects keep their attribute values after commit. Every session.refresh issued right after a commit therefore re-SELECTed a row whose values the session already held, including full chat JSON blobs and user settings, purely to overwrite identical data. Fifty such calls existed across the model layer, covering nearly every write path in the app (chat inserts, title updates, pin/archive toggles, user role and settings updates, tool, prompt, function, model, file, tag, feedback, memory, automation and grant writes).
    
    All fifty are removed. The only refreshes with an actual job were the two update-then-reload paths in tools and skills, where a Core UPDATE statement bypasses the identity map; those now use session.get(..., populate_existing=True), which guarantees a fresh row in one SELECT whether or not the row was already present in the session (the previous code issued get plus refresh, two SELECTs, on the default configuration).
    
    Benchmark (real SQLite DB, per write):
    
    | write path | before | after |
    | --- | --- | --- |
    | chat title update, ~600 KB chat blob | 2.08 ms | 1.24 ms |
    | user role update, small row | 1.21 ms | 0.68 ms |
    
    On Postgres each removed refresh is additionally a network round trip. The chat-blob case also skips re-parsing the entire JSON document per write.
    
    Functionally verified against a fresh database: user insert, role and settings updates, chat insert (including the server-default meta column, which is always provided client-side), title update and pin toggle, tool insert and the Core-update reload path, tag insert and the prompt insert flow that pins version_id after history creation all return correct values and persist correctly.
  • perf: cut repeated per-model work out of model list assembly
    get_all_models runs on every models refresh and, without the base-models cache (off by default), on every /api/models request. Several of its costs multiplied by the model count for no reason:
    
    - The active action and filter id sets were derived from get_functions_by_type, which loads full function rows including plugin source and validates them, only for the ids and is_global flags. A generalized column-only query now returns (id, is_global) tuples; the existing filter-specific helper delegates to it.
    - Action priorities were computed inside the per-model sort key, constructing a pydantic Valves object per action per model; with global actions in every model's list that was models x actions constructions per refresh. Priorities are now memoized per action.
    - Global action and filter item dicts were rebuilt per model from the same modules. The item lists are now built once per function and shallow-copied per model, keeping per-model dicts independent exactly as before (nested values were already shared).
    - Deactivated base-model overrides were dropped with models.remove, a linear scan and shift per removal; removals are now collected and filtered out in one identity-based pass, preserving list.remove's exact object semantics.
    - RedisDict.set fingerprinted the payload by serializing the already-serialized mapping a second time plus a sha256; a direct dict comparison against the last written mapping has the same skip semantics without re-serializing anything.
    - /api/models did tag normalization and profile-image stripping for every model before access filtering discarded the invisible ones, and always evaluated a json.dumps debug f-string; the work now runs only on visible models and the debug line is gated on the log level. The duplicate-id dedup keeps its position before filtering so the effective-model semantics are unchanged.
    
    Benchmark:
    
    | metric | before | after |
    | --- | --- | --- |
    | model-cache fingerprint, 200 models | 45 us | 1.4 us |
    | action priority Valves builds, 200 models x 4 global actions | 0.37 ms (800 builds) | 0.002 ms (4 builds) |
    | function-table payload for id sets | full rows incl. source | (id, is_global) tuples |
    
    Functionally verified: the column-only id query matches the full-row query for actions and filters including inactive exclusion, and the fingerprint skip logic writes on first set, skips identical payloads, updates plus deletes stale keys on change and clears on empty, against a scripted fake Redis.
  • perf(models): batch-fetch function valves to eliminate N+1 queries (#22301)
    * perf(models): batch-fetch function valves to eliminate N+1 queries
    
    get_action_priority() called Functions.get_function_valves_by_id()
    individually for every action on every model — an N+1 query pattern
    that issued one DB round-trip per (action x model) pair.
    
    Add Functions.get_function_valves_by_ids() that fetches all valves in
    a single WHERE IN query, then look up each action's valves from the
    pre-fetched dict inside get_action_priority().
    
    No functional change — same priority resolution, same sort order.
    
    * Update models.py
    
    * Update models.py
  • chore/perf: Remove old SRC level log env vars with no impact (#20045)
    * Update openai.py
    
    * Update env.py
    
    * Merge pull request open-webui#19030 from open-webui/dev (#119)
    
    Co-authored-by: Tim Baek <tim@openwebui.com>
    Co-authored-by: Claude <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Tim Baek <tim@openwebui.com>
    Co-authored-by: Claude <noreply@anthropic.com>
  • chore: format
    HIDE MODELS
  • wip