[PR #22302] [CLOSED] perf(models): deduplicate action/filter objects in /api/models response #26600

Closed
opened 2026-04-20 06:35:47 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/open-webui/open-webui/pull/22302
Author: @Classic298
Created: 3/6/2026
Status: Closed

Base: devHead: perf/dedup-action-icons


📝 Commits (1)

  • 4344937 perf(models): deduplicate action/filter objects in /api/models response

📊 Changes

2 files changed (+50 additions, -2 deletions)

View changed files

📝 backend/open_webui/main.py (+30 -1)
📝 src/lib/apis/index.ts (+20 -1)

📄 Description

Performance: Deduplicate action/filter objects in /api/models response

Why this matters

The savings scale with (models × actions/filters) and compound quickly:

  • 1 action icon + 2 models: You already see a difference — the icon
    is transmitted once instead of twice
  • 1 action icon + 10 models: You save yourself sending the same icon
    9 times
  • 3 actions + 2 filters with icons + 30 models: You save yourself
    sending 5 icon blobs 29 extra times each — easily dozens of megabytes
  • Enterprise setups with 50+ models and a dozen custom actions/filters:
    The payload reduction can exceed 100MB

This endpoint is called on every page load and every refresh, so this
is not a one-time cost — it is the tax paid every time a user opens
the app. Especially noticeable on setups with poor network conditions,
large model catalogs, or both.

Problem

The /api/models endpoint embeds full action and filter objects (including base64 icon data) directly on every model in the response. Since global actions/filters are shared across all models, this means M identical action blobs are serialized N times — once per model.

In a concrete scenario with 20 models sharing 3 global actions where each action has a ~500KB base64 icon, the JSON payload bloats to ~30MB of redundant data. This directly impacts initial page load time, network bandwidth, and time-to-interactive.

Solution

The backend extracts unique action/filter objects into top-level "actions" and "filters" dicts, keyed by ID. Each model carries only action_ids/filter_ids string arrays.

Move unique action and filter objects into top-level "actions" and "filters" dicts in the API response, keyed by their ID. Each model now carries only action_ids and filter_ids arrays (lightweight string arrays) instead of full embedded objects.

Response shape before:

{
  "data": [
    {
      "id": "gpt-4o",
      "name": "GPT-4o",
      "actions": [
        { "id": "web_search", "name": "Web Search", "description": "Search the web", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo... (500KB)" },
        { "id": "code_exec", "name": "Code Execution", "description": "Run code", "icon": "data:image/svg+xml;base64,YW5vdGhlciBsb25nIGJhc2U2NC... (500KB)" },
        { "id": "save_to_notes", "name": "Save to Notes", "description": "Save response", "icon": "data:image/png;base64,aVZCT1J3MEtHZ29BQUFBTl... (500KB)" }
      ],
      "filters": [
        { "id": "profanity_filter", "name": "Profanity Filter", "description": "Filters profanity", "icon": "data:image/svg+xml;base64,ZmlsdGVyIGljb24gYmFzZTY0... (200KB)" },
        { "id": "context_injector", "name": "Context Injector", "description": "Injects context", "icon": "data:image/svg+xml;base64,Y29udGV4dCBpbmplY3Rvcg... (200KB)" },
        { "id": "rag_pipeline", "name": "RAG Pipeline", "description": "RAG processing", "icon": "data:image/svg+xml;base64,cmFnIHBpcGVsaW5lIGljb24... (200KB)" }
      ]
    },
    {
      "id": "gpt-4o-mini",
      "name": "GPT-4o Mini",
      "actions": [
        { "id": "web_search", "name": "Web Search", "description": "Search the web", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo... (500KB)" },
        { "id": "code_exec", "name": "Code Execution", "description": "Run code", "icon": "data:image/svg+xml;base64,YW5vdGhlciBsb25nIGJhc2U2NC... (500KB)" },
        { "id": "save_to_notes", "name": "Save to Notes", "description": "Save response", "icon": "data:image/png;base64,aVZCT1J3MEtHZ29BQUFBTl... (500KB)" }
      ],
      "filters": [
        { "id": "profanity_filter", "name": "Profanity Filter", "description": "Filters profanity", "icon": "data:image/svg+xml;base64,ZmlsdGVyIGljb24gYmFzZTY0... (200KB)" },
        { "id": "context_injector", "name": "Context Injector", "description": "Injects context", "icon": "data:image/svg+xml;base64,Y29udGV4dCBpbmplY3Rvcg... (200KB)" },
        { "id": "rag_pipeline", "name": "RAG Pipeline", "description": "RAG processing", "icon": "data:image/svg+xml;base64,cmFnIHBpcGVsaW5lIGljb24... (200KB)" }
      ]
    },
    {
      "id": "claude-3.5-sonnet",
      "name": "Claude 3.5 Sonnet",
      "actions": [
        { "id": "web_search", "name": "Web Search", "description": "Search the web", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo... (500KB)" },
        { "id": "code_exec", "name": "Code Execution", "description": "Run code", "icon": "data:image/svg+xml;base64,YW5vdGhlciBsb25nIGJhc2U2NC... (500KB)" },
        { "id": "save_to_notes", "name": "Save to Notes", "description": "Save response", "icon": "data:image/png;base64,aVZCT1J3MEtHZ29BQUFBTl... (500KB)" }
      ],
      "filters": [
        { "id": "profanity_filter", "name": "Profanity Filter", "description": "Filters profanity", "icon": "data:image/svg+xml;base64,ZmlsdGVyIGljb24gYmFzZTY0... (200KB)" },
        { "id": "context_injector", "name": "Context Injector", "description": "Injects context", "icon": "data:image/svg+xml;base64,Y29udGV4dCBpbmplY3Rvcg... (200KB)" },
        { "id": "rag_pipeline", "name": "RAG Pipeline", "description": "RAG processing", "icon": "data:image/svg+xml;base64,cmFnIHBpcGVsaW5lIGljb24... (200KB)" }
      ]
    },
    { "id": "claude-3-haiku", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] },
    { "id": "gemini-2.0-flash", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] },
    { "id": "gemini-1.5-pro", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] },
    { "id": "llama3:70b", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] },
    { "id": "llama3:8b", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] },
    { "id": "mistral-large", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] },
    { "id": "deepseek-r1", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] }
  ]
}
// Total: 10 models x (1.5MB actions + 600KB filters) = ~21MB payload
// The SAME 3 actions and 3 filters repeated 10 times.

Response shape after:

{
  "data": [
    { "id": "gpt-4o", "name": "GPT-4o", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] },
    { "id": "gpt-4o-mini", "name": "GPT-4o Mini", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] },
    { "id": "claude-3.5-sonnet", "name": "Claude 3.5 Sonnet", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] },
    { "id": "claude-3-haiku", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] },
    { "id": "gemini-2.0-flash", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] },
    { "id": "gemini-1.5-pro", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] },
    { "id": "llama3:70b", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] },
    { "id": "llama3:8b", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] },
    { "id": "mistral-large", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] },
    { "id": "deepseek-r1", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] }
  ],
  "actions": {
    "web_search": { "id": "web_search", "name": "Web Search", "description": "Search the web", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo... (500KB)" },
    "code_exec": { "id": "code_exec", "name": "Code Execution", "description": "Run code", "icon": "data:image/svg+xml;base64,YW5vdGhlciBsb25nIGJhc2U2NC... (500KB)" },
    "save_to_notes": { "id": "save_to_notes", "name": "Save to Notes", "description": "Save response", "icon": "data:image/png;base64,aVZCT1J3MEtHZ29BQUFBTl... (500KB)" }
  },
  "filters": {
    "profanity_filter": { "id": "profanity_filter", "name": "Profanity Filter", "description": "Filters profanity", "icon": "data:image/svg+xml;base64,ZmlsdGVyIGljb24gYmFzZTY0... (200KB)" },
    "context_injector": { "id": "context_injector", "name": "Context Injector", "description": "Injects context", "icon": "data:image/svg+xml;base64,Y29udGV4dCBpbmplY3Rvcg... (200KB)" },
    "rag_pipeline": { "id": "rag_pipeline", "name": "RAG Pipeline", "description": "RAG processing", "icon": "data:image/svg+xml;base64,cmFnIHBpcGVsaW5lIGljb24... (200KB)" }
  }
}
// Total: 3 actions (1.5MB) + 3 filters (600KB) + 10 tiny ID arrays = ~2.1MB payload
// Down from ~21MB. Same data, 90% smaller.

The frontend API layer rehydrates these back into model.actions and model.filters arrays using object references, so no downstream component changes are needed. Since JavaScript passes objects by reference, the rehydrated models all point to the same action objects in memory — there is no duplication on the client side either.

Impact

Payload reduction of 90%+ for action/filter data (from N x M duplicated blobs to M unique blobs)
Zero breaking changes for frontend components — model.actions and model.filters are still accessible as before
Backend-only response transformation, no changes to the model computation logic

Changes

Backend (main.py):
After building the final model list, extract unique actions and filters into top-level dicts
Replace per-model actions/filters arrays with action_ids/filter_ids string arrays

Frontend (apis/index.ts):
After receiving the response, rehydrate action_ids/filter_ids back to full objects using the top-level dicts
Gracefully handles responses without the new fields (backwards compatible)

Contributor License Agreement

By submitting this pull request, I confirm that I have read and fully agree to the Contributor License Agreement (CLA), and I am providing my contributions under its terms.

Note

Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/open-webui/open-webui/pull/22302 **Author:** [@Classic298](https://github.com/Classic298) **Created:** 3/6/2026 **Status:** ❌ Closed **Base:** `dev` ← **Head:** `perf/dedup-action-icons` --- ### 📝 Commits (1) - [`4344937`](https://github.com/open-webui/open-webui/commit/4344937fd4c1530cda51d997c55bebe2f801ae6d) perf(models): deduplicate action/filter objects in /api/models response ### 📊 Changes **2 files changed** (+50 additions, -2 deletions) <details> <summary>View changed files</summary> 📝 `backend/open_webui/main.py` (+30 -1) 📝 `src/lib/apis/index.ts` (+20 -1) </details> ### 📄 Description ## Performance: Deduplicate action/filter objects in /api/models response ### Why this matters The savings scale with (models × actions/filters) and compound quickly: - **1 action icon + 2 models**: You already see a difference — the icon is transmitted once instead of twice - **1 action icon + 10 models**: You save yourself sending the same icon 9 times - **3 actions + 2 filters with icons + 30 models**: You save yourself sending 5 icon blobs 29 extra times each — easily dozens of megabytes - **Enterprise setups with 50+ models and a dozen custom actions/filters**: The payload reduction can exceed 100MB This endpoint is called on every page load and every refresh, so this is not a one-time cost — it is the tax paid every time a user opens the app. Especially noticeable on setups with poor network conditions, large model catalogs, or both. ### Problem The /api/models endpoint embeds full action and filter objects (including base64 icon data) directly on every model in the response. Since global actions/filters are shared across all models, this means M identical action blobs are serialized N times — once per model. In a concrete scenario with 20 models sharing 3 global actions where each action has a ~500KB base64 icon, the JSON payload bloats to ~30MB of redundant data. This directly impacts initial page load time, network bandwidth, and time-to-interactive. ### Solution The backend extracts unique action/filter objects into top-level "actions" and "filters" dicts, keyed by ID. Each model carries only action_ids/filter_ids string arrays. Move unique action and filter objects into top-level "actions" and "filters" dicts in the API response, keyed by their ID. Each model now carries only action_ids and filter_ids arrays (lightweight string arrays) instead of full embedded objects. **Response shape before:** ```json { "data": [ { "id": "gpt-4o", "name": "GPT-4o", "actions": [ { "id": "web_search", "name": "Web Search", "description": "Search the web", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo... (500KB)" }, { "id": "code_exec", "name": "Code Execution", "description": "Run code", "icon": "data:image/svg+xml;base64,YW5vdGhlciBsb25nIGJhc2U2NC... (500KB)" }, { "id": "save_to_notes", "name": "Save to Notes", "description": "Save response", "icon": "data:image/png;base64,aVZCT1J3MEtHZ29BQUFBTl... (500KB)" } ], "filters": [ { "id": "profanity_filter", "name": "Profanity Filter", "description": "Filters profanity", "icon": "data:image/svg+xml;base64,ZmlsdGVyIGljb24gYmFzZTY0... (200KB)" }, { "id": "context_injector", "name": "Context Injector", "description": "Injects context", "icon": "data:image/svg+xml;base64,Y29udGV4dCBpbmplY3Rvcg... (200KB)" }, { "id": "rag_pipeline", "name": "RAG Pipeline", "description": "RAG processing", "icon": "data:image/svg+xml;base64,cmFnIHBpcGVsaW5lIGljb24... (200KB)" } ] }, { "id": "gpt-4o-mini", "name": "GPT-4o Mini", "actions": [ { "id": "web_search", "name": "Web Search", "description": "Search the web", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo... (500KB)" }, { "id": "code_exec", "name": "Code Execution", "description": "Run code", "icon": "data:image/svg+xml;base64,YW5vdGhlciBsb25nIGJhc2U2NC... (500KB)" }, { "id": "save_to_notes", "name": "Save to Notes", "description": "Save response", "icon": "data:image/png;base64,aVZCT1J3MEtHZ29BQUFBTl... (500KB)" } ], "filters": [ { "id": "profanity_filter", "name": "Profanity Filter", "description": "Filters profanity", "icon": "data:image/svg+xml;base64,ZmlsdGVyIGljb24gYmFzZTY0... (200KB)" }, { "id": "context_injector", "name": "Context Injector", "description": "Injects context", "icon": "data:image/svg+xml;base64,Y29udGV4dCBpbmplY3Rvcg... (200KB)" }, { "id": "rag_pipeline", "name": "RAG Pipeline", "description": "RAG processing", "icon": "data:image/svg+xml;base64,cmFnIHBpcGVsaW5lIGljb24... (200KB)" } ] }, { "id": "claude-3.5-sonnet", "name": "Claude 3.5 Sonnet", "actions": [ { "id": "web_search", "name": "Web Search", "description": "Search the web", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo... (500KB)" }, { "id": "code_exec", "name": "Code Execution", "description": "Run code", "icon": "data:image/svg+xml;base64,YW5vdGhlciBsb25nIGJhc2U2NC... (500KB)" }, { "id": "save_to_notes", "name": "Save to Notes", "description": "Save response", "icon": "data:image/png;base64,aVZCT1J3MEtHZ29BQUFBTl... (500KB)" } ], "filters": [ { "id": "profanity_filter", "name": "Profanity Filter", "description": "Filters profanity", "icon": "data:image/svg+xml;base64,ZmlsdGVyIGljb24gYmFzZTY0... (200KB)" }, { "id": "context_injector", "name": "Context Injector", "description": "Injects context", "icon": "data:image/svg+xml;base64,Y29udGV4dCBpbmplY3Rvcg... (200KB)" }, { "id": "rag_pipeline", "name": "RAG Pipeline", "description": "RAG processing", "icon": "data:image/svg+xml;base64,cmFnIHBpcGVsaW5lIGljb24... (200KB)" } ] }, { "id": "claude-3-haiku", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] }, { "id": "gemini-2.0-flash", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] }, { "id": "gemini-1.5-pro", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] }, { "id": "llama3:70b", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] }, { "id": "llama3:8b", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] }, { "id": "mistral-large", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] }, { "id": "deepseek-r1", "actions": ["... same 3 actions (1.5MB) ..."], "filters": ["... same 3 filters (600KB) ..."] } ] } // Total: 10 models x (1.5MB actions + 600KB filters) = ~21MB payload // The SAME 3 actions and 3 filters repeated 10 times. ``` Response shape after: ```json { "data": [ { "id": "gpt-4o", "name": "GPT-4o", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] }, { "id": "gpt-4o-mini", "name": "GPT-4o Mini", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] }, { "id": "claude-3.5-sonnet", "name": "Claude 3.5 Sonnet", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] }, { "id": "claude-3-haiku", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] }, { "id": "gemini-2.0-flash", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] }, { "id": "gemini-1.5-pro", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] }, { "id": "llama3:70b", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] }, { "id": "llama3:8b", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] }, { "id": "mistral-large", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] }, { "id": "deepseek-r1", "action_ids": ["web_search", "code_exec", "save_to_notes"], "filter_ids": ["profanity_filter", "context_injector", "rag_pipeline"] } ], "actions": { "web_search": { "id": "web_search", "name": "Web Search", "description": "Search the web", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo... (500KB)" }, "code_exec": { "id": "code_exec", "name": "Code Execution", "description": "Run code", "icon": "data:image/svg+xml;base64,YW5vdGhlciBsb25nIGJhc2U2NC... (500KB)" }, "save_to_notes": { "id": "save_to_notes", "name": "Save to Notes", "description": "Save response", "icon": "data:image/png;base64,aVZCT1J3MEtHZ29BQUFBTl... (500KB)" } }, "filters": { "profanity_filter": { "id": "profanity_filter", "name": "Profanity Filter", "description": "Filters profanity", "icon": "data:image/svg+xml;base64,ZmlsdGVyIGljb24gYmFzZTY0... (200KB)" }, "context_injector": { "id": "context_injector", "name": "Context Injector", "description": "Injects context", "icon": "data:image/svg+xml;base64,Y29udGV4dCBpbmplY3Rvcg... (200KB)" }, "rag_pipeline": { "id": "rag_pipeline", "name": "RAG Pipeline", "description": "RAG processing", "icon": "data:image/svg+xml;base64,cmFnIHBpcGVsaW5lIGljb24... (200KB)" } } } // Total: 3 actions (1.5MB) + 3 filters (600KB) + 10 tiny ID arrays = ~2.1MB payload // Down from ~21MB. Same data, 90% smaller. ``` The frontend API layer rehydrates these back into model.actions and model.filters arrays using object references, so <ins>**no downstream component changes are needed**</ins>. Since <ins>**JavaScript passes objects by reference**</ins>, the rehydrated models all point to the same action objects in memory — <ins>**there is no duplication on the client side either.**</ins> ### Impact **<ins>Payload reduction of 90%+ for action/filter data (from N x M duplicated blobs to M unique blobs)</ins>** Zero breaking changes for frontend components — model.actions and model.filters are still accessible as before **Backend-only response transformation, no changes to the model computation logic** ### Changes **Backend (main.py):** After building the final model list, extract unique actions and filters into top-level dicts Replace per-model actions/filters arrays with action_ids/filter_ids string arrays **Frontend (apis/index.ts):** After receiving the response, rehydrate action_ids/filter_ids back to full objects using the top-level dicts Gracefully handles responses without the new fields (backwards compatible) ### Contributor License Agreement <!-- 🚨 DO NOT DELETE THE TEXT BELOW 🚨 Keep the "Contributor License Agreement" confirmation text intact. Deleting it will trigger the CLA-Bot to INVALIDATE your PR. --> By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms. > [!NOTE] > Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-04-20 06:35:47 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/open-webui#26600