Skip to content

Commit b564b3e

Browse files
authored
Merge pull request #1205 from open-webui/dev
2 parents 2febf55 + 8bcf0fb commit b564b3e

2 files changed

Lines changed: 65 additions & 53 deletions

File tree

docs/features/extensibility/plugin/functions/filter.mdx

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -359,69 +359,70 @@ Here's the complete flow from admin configuration to filter execution:
359359

360360
### 📡 Filter Behavior with API Requests
361361

362-
When using Open WebUI's API endpoints directly (e.g., via `curl` or external applications), filters follow the same execution model as WebUI requests — with one important caveat for `outlet()`.
362+
When using Open WebUI's API endpoints directly (e.g., via `curl` or external applications), `inlet()` and `stream()` follow the same execution model as WebUI requests. `outlet()` is the one that behaves very differently for direct API callers and is covered in detail below.
363363

364364
#### Key Behavioral Differences
365365

366-
| Function | WebUI Request | Direct API Request |
367-
|----------|--------------|-------------------|
368-
| `inlet()` | ✅ Always called | ✅ Always called |
369-
| `stream()` | ✅ Called during streaming | ✅ Called during streaming |
370-
| `outlet()` | ✅ Called after response | ⚠️ Non-streaming only, and only if `chat_id` and `id` are present (see below) |
371-
| `__event_emitter__` | ✅ Shows UI feedback | ⚠️ Runs but no UI to display |
366+
| Function | WebUI Request | Direct API — stable (`main`) | Direct API — pre-release (`dev`) |
367+
|----------|--------------|------------------------------|-----------------------------------|
368+
| `inlet()` | ✅ Always called | ✅ Always called | ✅ Always called |
369+
| `stream()` | ✅ Called during streaming | ✅ Called during streaming | ✅ Called during streaming |
370+
| `outlet()` | ✅ Called after response | ❌ Not called by `/api/chat/completions`only by `/api/chat/completed` | ⚠️ Runs inline only under narrow conditions (see below) |
371+
| `__event_emitter__` | ✅ Shows UI feedback | ⚠️ Inert for pure API callers | ⚠️ Inert for pure API callers |
372372

373-
:::warning Streaming API Requests Do Not Invoke outlet() Yet
374-
Inline `outlet()` execution during `/api/chat/completions` is currently only wired up for **non-streaming** direct API requests. For **streaming** (`"stream": true`) API requests, `outlet()` is not yet invoked by the backend — this is a known limitation that is being worked on. WebUI requests continue to run `outlet()` for both streaming and non-streaming responses as before.
373+
:::danger outlet() on Direct API Calls — Accurate Picture
374+
Earlier versions of this page claimed `outlet()` runs inline during `/api/chat/completions` for non-streaming direct API requests. That was only partially accurate and only on `dev`. Verified against the backend source, the real behavior is:
375375

376-
If you depend on `outlet()` for observability, logging, or post-processing of API traffic, either issue non-streaming requests or route the traffic through the WebUI until streaming support lands.
377-
:::
376+
**On tagged releases / `main`:** `outlet()` is **never** invoked by `/api/chat/completions`. It runs only when the caller performs the second POST to `/api/chat/completed`. API integrations that need `outlet()` must make both calls.
377+
378+
**On `dev` / pre-release builds:** `outlet()` can fire inline after `/api/chat/completions`, but only when **all** of the following hold:
378379

379-
:::warning Outlet Requires chat_id and message id
380-
For non-streaming direct API requests, `outlet()` runs inline as part of `/api/chat/completions` processing. However, the outlet handler **requires both `chat_id` and `id` (message ID) to be present** in the request body. The WebUI always sends these, but pure API callers using the standard OpenAI-compatible request shape typically omit them — in which case **outlet is silently skipped**.
380+
1. The request body includes **both** `chat_id` **and** `id` (the assistant message id). If either is missing, the backend sees `event_emitter = None` and silently skips the outlet block.
381+
2. The `chat_id` is a chat the authenticated user already owns; otherwise the request 404s before the outlet path runs. (Alternatively, send `parent_id: null` without a `chat_id` to have the server create a new chat.)
382+
3. The request is **non-streaming**. On the streaming path the server consumes the stream itself and routes content to the user's WebSocket — an HTTP streaming API caller receives effectively nothing. `outlet()` still runs DB-side, but the streaming client does not see it.
381383

382-
If your filters rely on `outlet()` for API requests (logging, observability, post-processing, etc.), you need to either:
383-
1. Include `chat_id` and `id` in your API request body, **or**
384-
2. Use a filter that synthesizes these IDs automatically (see example below)
384+
Even on the non-streaming path, **`outlet()` does not rewrite the HTTP response body**. The handler updates the persisted chat message row and emits a `chat:outlet` WebSocket event; the JSON returned to your HTTP client is the pre-outlet content. To observe `outlet()`'s output you must either read the chat record back, subscribe to the WebSocket, or use `/api/chat/completed`.
385385

386-
The legacy `/api/chat/completed` endpoint is kept for backward compatibility with older integrations, but it is no longer required for normal outlet execution.
386+
**Bottom line for filter authors:** if your filter's `outlet()` needs to be visible to pure API consumers (Continue.dev, Claude Code, Langfuse traces, custom scripts), assume `/api/chat/completed` is the supported surface today. Relying on inline `outlet()` during `/api/chat/completions` currently works only for clients shaped like the WebUI.
387387
:::
388388

389-
#### Enabling Outlet for Pure API Callers
389+
#### Inlet ↔ Outlet Correlation via `__metadata__`
390390

391-
If you need `outlet()` to run for API requests that don't include `chat_id` or `id`, you can create a filter that synthesizes temporary IDs in `inlet()`. Since `__metadata__` is a live dict reference shared across the request lifecycle, values written in `inlet()` are visible to `outlet()`.
391+
`__metadata__` is a live dict passed through the request lifecycle, so anything your filter stashes in `inlet()` is visible in `outlet()` on the *same request* — this is useful for things like start-time tracking, correlation IDs, or per-call valves, *when `outlet()` actually runs*. Given the constraints above, this pattern is primarily useful in WebUI requests and in the `/api/chat/completed` handler.
392392

393393
```python
394-
from uuid import uuid4
394+
import time
395395
from pydantic import BaseModel, Field
396396

397397
class Filter:
398398
class Valves(BaseModel):
399-
priority: int = Field(default=0, description="Run before other filters to ensure IDs are available")
399+
priority: int = Field(default=0)
400400

401401
def __init__(self):
402402
self.valves = self.Valves()
403403

404404
async def inlet(self, body: dict, __metadata__: dict = None) -> dict:
405-
if __metadata__:
406-
if not __metadata__.get("chat_id"):
407-
__metadata__["chat_id"] = f"local:{uuid4()}"
408-
if not __metadata__.get("message_id"):
409-
__metadata__["message_id"] = str(uuid4())
405+
if __metadata__ is not None:
406+
__metadata__["_my_started_at"] = time.monotonic()
410407
return body
411-
```
412408

413-
The `local:` prefix on `chat_id` routes the request into the temp-chat code path, which builds the outlet message list from in-memory data instead of querying the database. No database rows are created, and the event emitter is already inert for API callers (no WebSocket session), so this is safe.
409+
async def outlet(self, body: dict, __metadata__: dict = None) -> dict:
410+
if __metadata__ is not None and "_my_started_at" in __metadata__:
411+
duration = time.monotonic() - __metadata__["_my_started_at"]
412+
print(f"request took {duration:.3f}s")
413+
return body
414+
```
414415

415-
:::tip Inlet-Outlet Correlation
416-
This pattern also enables **inlet/outlet correlation**: any per-request state your filter stashes in `inlet()` keyed on `__metadata__["message_id"]` can be read back in `outlet()` using the same ID, since both see the same synthesized value.
416+
:::warning Synthesizing `chat_id` / `message_id` in `inlet()` Is Not a Reliable Workaround
417+
Older versions of this page suggested synthesizing a `local:<uuid>` `chat_id` and a random `message_id` inside `inlet()` to force `outlet()` to run for pure API callers. Do not rely on that pattern — in current backend code the chat ownership check runs before the filter pipeline, and the inline outlet handler does not rewrite the HTTP response body even when it does run. If you need `outlet()` output over HTTP for an API caller, use `/api/chat/completed` instead.
417418
:::
418419

419-
#### Legacy Compatibility: `/api/chat/completed`
420+
#### Running outlet() for API Callers: `/api/chat/completed`
420421

421-
If you maintain an older integration that still calls `/api/chat/completed`, that endpoint remains available for compatibility:
422+
The reliable, supported way to run `outlet()` for a direct API integration is to follow up `/api/chat/completions` with `POST /api/chat/completed`, passing the full conversation (including the assistant response) in `messages`. The endpoint runs pipeline outlet filters and Function `outlet()` handlers unconditionally and returns the filtered payload.
422423

423424
```bash
424-
# Optional legacy compatibility call:
425+
# Step 2: run outlet() after /api/chat/completions returned a response.
425426
curl -X POST http://localhost:3000/api/chat/completed \
426427
-H "Authorization: Bearer YOUR_API_KEY" \
427428
-H "Content-Type: application/json" \
@@ -436,8 +437,8 @@ curl -X POST http://localhost:3000/api/chat/completed \
436437
}'
437438
```
438439

439-
:::tip
440-
For new integrations, you can skip this second call and rely on inline outlet execution during `/api/chat/completions`.
440+
:::info
441+
On `dev` this endpoint is labeled deprecated in favor of inline execution, but because inline execution does not return the filtered payload over HTTP to pure API callers, `/api/chat/completed` is still the correct choice for most API integrations today.
441442
:::
442443

443444
#### Detecting API vs WebUI Requests
@@ -1013,7 +1014,7 @@ The `outlet` function is like a **proofreader**: tidy up the AI's response (or m
10131014
- **Quality scoring** - Run automated quality checks on model outputs
10141015

10151016
:::info Outlet and API Requests
1016-
`outlet()` is called for **non-streaming** direct API requests to `/api/chat/completions` when `chat_id` and `id` (message ID) are present in the request body. Streaming API requests do not yet invoke `outlet()` — support is in progress. See [Enabling Outlet for Pure API Callers](#enabling-outlet-for-pure-api-callers) for details.
1017+
`outlet()` does **not** run reliably for direct `/api/chat/completions` calls. On tagged releases it is never invoked by that endpoint. On `dev` it can run inline, but only when the caller supplies `chat_id` + `id`, owns the chat, and uses a non-streaming request — and even then the filtered content is not returned in the HTTP response. For direct API integrations that need `outlet()`, follow `/api/chat/completions` with `POST /api/chat/completed`. See [Filter Behavior with API Requests](#-filter-behavior-with-api-requests) for the full picture.
10171018
:::
10181019

10191020
💡 **Example Use Case**: Strip out sensitive API responses you don't want the user to see:

docs/reference/api-endpoints.md

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -193,31 +193,39 @@ Open WebUI accepts both **API keys** (prefixed with `sk-`) and **JWT tokens** fo
193193

194194
#### Filter Execution
195195

196-
| Filter Function | WebUI Request | Direct API Request |
197-
|----------------|--------------|-------------------|
198-
| `inlet()` | ✅ Runs | ✅ Runs |
199-
| `stream()` | ✅ Runs | ✅ Runs |
200-
| `outlet()` | ✅ Runs | ⚠️ Non-streaming only (see note) |
196+
| Filter Function | WebUI Request | Direct API — stable (`main`) | Direct API — pre-release (`dev`) |
197+
|----------------|--------------|------------------------------|-----------------------------------|
198+
| `inlet()` | ✅ Runs | ✅ Runs | ✅ Runs |
199+
| `stream()` | ✅ Runs | ✅ Runs | ✅ Runs |
200+
| `outlet()` | ✅ Runs | ❌ Not called by `/api/chat/completions` — use `/api/chat/completed` | ⚠️ Runs inline only under narrow conditions (see below) |
201201

202202
The `inlet()` function always executes, making it ideal for:
203203
- **Rate limiting** - Track and limit requests per user
204204
- **Request logging** - Log all API usage for monitoring
205205
- **Input validation** - Reject invalid requests before they reach the model
206206

207-
:::warning Streaming Requests
208-
Inline `outlet()` execution during `/api/chat/completions` is currently only wired up for **non-streaming** direct API requests. For **streaming** (`"stream": true`) API requests, `outlet()` is not yet invoked by the backend — this is a known limitation that is being worked on. WebUI requests continue to run `outlet()` for both streaming and non-streaming responses as before.
207+
:::danger Outlet Behavior for Direct API Calls — Read Carefully
208+
Earlier versions of this page said `outlet()` runs inline during `/api/chat/completions` for both WebUI and API requests. That was wrong. The accurate picture, verified in the backend source, is:
209209

210-
If you rely on `outlet()` for observability, logging, or post-processing of streamed API responses, use non-streaming requests or consume the response via the WebUI until streaming support lands.
211-
:::
210+
**On tagged releases / `main`:** `outlet()` is **not** invoked inline by `/api/chat/completions` at all. It only runs if the caller performs the second POST to `/api/chat/completed`. For now, if your integration needs `outlet()`, you must still do that second call.
211+
212+
**On `dev` / pre-release builds:** `outlet()` can run inline after `/api/chat/completions`, but only when **all** of the following are true:
213+
214+
1. The request body includes **both** `chat_id` **and** `id` (the assistant message id). If either is missing, the backend has no `event_emitter` and silently skips the outlet block.
215+
2. The `chat_id` is a chat the authenticated user already **owns**, otherwise the request 404s before the outlet path is reached. (Alternatively, send `parent_id: null` without a `chat_id` to trigger new-chat creation on the server.)
216+
3. The request is **non-streaming**. Streaming requests that satisfy (1) and (2) hit a code path designed for the WebUI: the server consumes the upstream stream itself and routes content to the user's WebSocket, so the HTTP response to a streaming API caller is effectively empty. Outlet runs, but you won't see its effect over HTTP.
212217

213-
#### Legacy Endpoint: `/api/chat/completed`
218+
Even in the non-streaming case, **`outlet()` does not rewrite the HTTP response body**. It updates the persisted chat message and emits a `chat:outlet` WebSocket event, but the JSON your client receives is the pre-outlet content. If you need the outlet-filtered text, read it back from the chat record, subscribe to the WebSocket, or keep using `/api/chat/completed`.
219+
220+
**Practical guidance:** if you are a pure API consumer (Continue.dev, Claude Code, custom scripts, Langfuse pipelines, etc.), treat `/api/chat/completed` as the supported way to run `outlet()` today. Inline execution on `dev` is primarily for WebUI-shaped clients that are already listening on the WebSocket.
221+
:::
214222

215-
`outlet()` now runs inline during `/api/chat/completions` for WebUI requests and for non-streaming direct API requests (see caveat above).
223+
#### Legacy / Supported-for-API Endpoint: `/api/chat/completed`
216224

217-
`POST /api/chat/completed` is retained for backward compatibility with older clients that still call it as a second step:
225+
`POST /api/chat/completed` is the endpoint that reliably runs `outlet()` for direct API integrations. On `dev` it is marked deprecated in favor of inline execution, but as described above, inline execution does not currently return the filtered payload to pure API callers — so in practice `/api/chat/completed` remains the right call for most API integrations today.
218226

219-
- **Endpoint**: `POST /api/chat/completed` (legacy compatibility)
220-
- **Description**: Backward-compatible outlet processing endpoint for older integrations
227+
- **Endpoint**: `POST /api/chat/completed`
228+
- **Description**: Runs `outlet()` filters (and pipeline outlet filters) unconditionally over a completed chat payload. Returns the filtered payload.
221229

222230
- **Curl Example**:
223231

@@ -243,8 +251,11 @@ If you rely on `outlet()` for observability, logging, or post-processing of stre
243251

244252
def complete_chat_with_outlet(token, model, messages, chat_id=None):
245253
"""
246-
Optional legacy call for older integrations.
247-
For new clients, outlet processing happens inline during /api/chat/completions.
254+
Second-step call that actually runs outlet() for direct API callers.
255+
On tagged releases /api/chat/completions does not run outlet inline at all.
256+
On dev it runs inline only under narrow conditions and does not rewrite
257+
the HTTP response body, so this endpoint is still the right call for
258+
most API integrations that want outlet's output over HTTP.
248259
"""
249260
url = 'http://localhost:3000/api/chat/completed'
250261
headers = {
@@ -263,7 +274,7 @@ If you rely on `outlet()` for observability, logging, or post-processing of stre
263274
```
264275

265276
:::tip
266-
For new integrations, prefer a single `/api/chat/completions` call. For more details on filter behavior, see the [Filter Function documentation](/features/extensibility/plugin/functions/filter#-filter-behavior-with-api-requests).
277+
If you need `outlet()` output over HTTP today, call `/api/chat/completions` followed by `/api/chat/completed`. Inline execution on `dev` is primarily for WebUI-shaped clients that read from the WebSocket. For more details on filter behavior, see the [Filter Function documentation](/features/extensibility/plugin/functions/filter#-filter-behavior-with-api-requests).
267278
:::
268279

269280
### 🦙 Ollama API Proxy Support

0 commit comments

Comments
 (0)