You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/features/extensibility/plugin/functions/filter.mdx
+37-36Lines changed: 37 additions & 36 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -359,69 +359,70 @@ Here's the complete flow from admin configuration to filter execution:
359
359
360
360
### 📡 Filter Behavior with API Requests
361
361
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.
363
363
364
364
#### Key Behavioral Differences
365
365
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`)|
|`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|
372
372
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:
375
375
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:
378
379
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.
381
383
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`.
385
385
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.
387
387
:::
388
388
389
-
#### Enabling Outlet for Pure API Callers
389
+
#### Inlet ↔ Outlet Correlation via `__metadata__`
390
390
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.
392
392
393
393
```python
394
-
from uuid importuuid4
394
+
importtime
395
395
from pydantic import BaseModel, Field
396
396
397
397
classFilter:
398
398
classValves(BaseModel):
399
-
priority: int= Field(default=0, description="Run before other filters to ensure IDs are available")
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.
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.
417
418
:::
418
419
419
-
#### Legacy Compatibility: `/api/chat/completed`
420
+
#### Running outlet() for API Callers: `/api/chat/completed`
420
421
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.
422
423
423
424
```bash
424
-
#Optional legacy compatibility call:
425
+
#Step 2: run outlet() after /api/chat/completions returned a response.
425
426
curl -X POST http://localhost:3000/api/chat/completed \
426
427
-H "Authorization: Bearer YOUR_API_KEY" \
427
428
-H "Content-Type: application/json" \
@@ -436,8 +437,8 @@ curl -X POST http://localhost:3000/api/chat/completed \
436
437
}'
437
438
```
438
439
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.
441
442
:::
442
443
443
444
#### Detecting API vs WebUI Requests
@@ -1013,7 +1014,7 @@ The `outlet` function is like a **proofreader**: tidy up the AI's response (or m
1013
1014
-**Quality scoring** - Run automated quality checks on model outputs
1014
1015
1015
1016
:::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.
1017
1018
:::
1018
1019
1019
1020
💡 **Example Use Case**: Strip out sensitive API responses you don't want the user to see:
|`outlet()`| ✅ Runs |❌ Not called by `/api/chat/completions` — use `/api/chat/completed`|⚠️ Runs inline only under narrow conditions (see below) |
201
201
202
202
The `inlet()` function always executes, making it ideal for:
203
203
-**Rate limiting** - Track and limit requests per user
204
204
-**Request logging** - Log all API usage for monitoring
205
205
-**Input validation** - Reject invalid requests before they reach the model
206
206
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:
209
209
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.
212
217
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
+
:::
214
222
215
-
`outlet()` now runs inline during`/api/chat/completions` for WebUI requests and for non-streaming direct API requests (see caveat above).
`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.
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.
248
259
"""
249
260
url ='http://localhost:3000/api/chat/completed'
250
261
headers = {
@@ -263,7 +274,7 @@ If you rely on `outlet()` for observability, logging, or post-processing of stre
263
274
```
264
275
265
276
:::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).
0 commit comments