7.9 KiB
OpenWebUI Plugin Development Guide
This guide consolidates official documentation, SDK details, and best practices to provide a systematic tutorial for developers, from beginner to expert.
📚 Table of Contents
- Quick Start
- Core Concepts & SDK Details
- Deep Dive into Plugin Types
- Advanced Development Patterns
- Best Practices & Design Principles
- Troubleshooting
1. Quick Start
1.1 What are OpenWebUI Plugins?
OpenWebUI Plugins (officially called "Functions") are the primary way to extend the platform's capabilities. Running in a backend Python environment, they allow you to:
- 🔌 Integrate New Models: Connect to Claude, Gemini, or custom RAGs via Pipes.
- 🎨 Enhance Interaction: Add buttons (e.g., "Export", "Generate Chart") next to messages via Actions.
- 🔧 Intervene in Processes: Modify data before requests or after responses (e.g., inject context, filter sensitive words) via Filters.
1.2 Your First Plugin (Hello World)
Save the following code as hello.py and upload it to the Functions panel in OpenWebUI:
"""
title: Hello World Action
author: Demo
version: 1.0.0
"""
from pydantic import BaseModel, Field
from typing import Optional
class Action:
class Valves(BaseModel):
greeting: str = Field(default="Hello", description="Greeting message")
def __init__(self):
self.valves = self.Valves()
async def action(
self,
body: dict,
__event_emitter__=None,
__user__=None
) -> Optional[dict]:
user_name = __user__.get("name", "Friend") if __user__ else "Friend"
if __event_emitter__:
await __event_emitter__({
"type": "notification",
"data": {"type": "success", "content": f"{self.valves.greeting}, {user_name}!"}
})
return body
2. Core Concepts & SDK Details
2.1 ⚠️ Important: Sync vs Async
OpenWebUI plugins run within an asyncio event loop.
- Principle: All I/O operations (database, file, network) must be non-blocking.
- Pitfall: Calling synchronous methods directly (e.g.,
time.sleep,requests.get) will freeze the entire server. - Solution: Wrap synchronous calls using
await asyncio.to_thread(sync_func, ...).
2.2 Core Parameters
All plugin methods (inlet, outlet, pipe, action) support injecting the following special parameters:
| Parameter | Type | Description |
|---|---|---|
body |
dict |
Core Data. Contains request info like messages, model, stream. |
__user__ |
dict |
Current User. Contains id, name, role, valves (user config), etc. |
__metadata__ |
dict |
Metadata. Contains chat_id, message_id. The variables field holds preset variables like {{USER_NAME}}, {{CURRENT_TIME}}. |
__request__ |
Request |
FastAPI Request Object. Access app.state for cross-plugin communication. |
__event_emitter__ |
func |
One-way Notification. Used to send Toast notifications or status bar updates. |
__event_call__ |
func |
Two-way Interaction. Used to execute JS code, show confirmation dialogs, or input boxes on the frontend. |
2.3 Configuration System (Valves)
Valves: Global admin configuration.UserValves: User-level configuration (higher priority, overrides global).
class Filter:
class Valves(BaseModel):
API_KEY: str = Field(default="", description="Global API Key")
class UserValves(BaseModel):
API_KEY: str = Field(default="", description="User Private API Key")
def inlet(self, body, __user__):
# Prioritize user's Key
user_valves = __user__.get("valves", self.UserValves())
api_key = user_valves.API_KEY or self.valves.API_KEY
3. Deep Dive into Plugin Types
3.1 Action
Role: Adds buttons below messages that trigger upon user click.
Advanced Usage: Execute JavaScript on Frontend (File Download Example)
import base64
async def action(self, body, __event_call__):
# 1. Generate content on backend
content = "Hello OpenWebUI".encode()
b64 = base64.b64encode(content).decode()
# 2. Send JS to frontend for execution
js = f"""
const blob = new Blob([atob('{b64}')], {{type: 'text/plain'}});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'hello.txt';
a.click();
"""
await __event_call__({"type": "execute", "data": {"code": js}})
3.2 Filter
Role: Middleware that intercepts and modifies requests/responses.
inlet: Before request. Used for injecting context, modifying model parameters.outlet: After response. Used for formatting output, logging.stream: During streaming. Used for real-time sensitive word filtering.
Example: Injecting Environment Variables
async def inlet(self, body, __metadata__):
vars = __metadata__.get("variables", {})
context = f"Current Time: {vars.get('{{CURRENT_DATETIME}}')}"
# Inject into System Prompt or first message
if body.get("messages"):
body["messages"][0]["content"] += f"\n\n{context}"
return body
3.3 Pipe
Role: Custom Model/Agent.
Example: Simple OpenAI Wrapper
import requests
class Pipe:
def pipes(self):
return [{"id": "my-gpt", "name": "My GPT Wrapper"}]
def pipe(self, body):
# Modify body here, e.g., force add prompt
headers = {"Authorization": f"Bearer {self.valves.API_KEY}"}
r = requests.post("https://api.openai.com/v1/chat/completions", json=body, headers=headers, stream=True)
return r.iter_lines()
4. Advanced Development Patterns
4.1 Pipe & Filter Collaboration
Use __request__.app.state to share data between plugins.
- Pipe:
__request__.app.state.search_results = [...] - Filter (Outlet): Read
search_resultsand format them as citation links appended to the response.
4.2 Async Background Tasks
Execute time-consuming operations (e.g., summarization, database storage) in the background without blocking the user response.
import asyncio
async def outlet(self, body, __metadata__):
asyncio.create_task(self.background_job(__metadata__["chat_id"]))
return body
async def background_job(self, chat_id):
# Execute time-consuming operation...
pass
5. Best Practices & Design Principles
5.1 Naming & Positioning
- Short & Punchy: e.g., "FlashCard", "DeepRead". Avoid generic terms like "Text Analysis Assistant".
- Complementary: Don't reinvent the wheel; clarify what specific problem your plugin solves.
5.2 User Experience (UX)
- Timely Feedback: Send a
notification("Generating...") before time-consuming operations. - Visual Appeal: When Action outputs HTML, use modern CSS (rounded corners, shadows, gradients).
- Smart Guidance: If text is too short, prompt the user: "Suggest entering more content for better results".
5.3 Error Handling
Never let a plugin fail silently. Catch exceptions and inform the user via __event_emitter__.
try:
# Business logic
except Exception as e:
await __event_emitter__({
"type": "notification",
"data": {"type": "error", "content": f"Processing failed: {str(e)}"}
})
6. Troubleshooting
- HTML not showing? Ensure it's wrapped in a
```html ... ```code block. - Database error? Check if you called synchronous DB methods directly in an
asyncfunction; useasyncio.to_thread. - Parameters not working? Check if
Valvesare defined correctly and if they are being overridden byUserValves.