The functions in this repo only run inside Open WebUI, so they are tested end-to-end
against a real Open WebUI container: tests/e2e/run.sh starts a throw-away
container, installs the function files through the Open WebUI API, points them at
mock provider APIs (Gemini, Azure OpenAI / AI Foundry, Azure AI Search, n8n,
Infomaniak, Azure Log Analytics) and runs scenario suites that chat with them the way
an API client and the browser UI do.
Container and volume are removed afterwards.
- Prerequisites
- Quick start
- Options
- What is tested
- Results: PASS, FAIL, KNOWN
- Testing a fix, another branch or another Open WebUI version
- Debugging a failure
- Adding a scenario or a mock behaviour
- Static Open WebUI API check
- Real-API smoke test (manual, needs a key)
- CI
- Troubleshooting and gotchas
- Docker (Docker Desktop on Windows/macOS, Docker Engine on Linux).
- bash: Linux/macOS shell, or Git Bash on Windows.
- Network access for the first image pull (
ghcr.io/open-webui/open-webui:v0.12.0-slim, ~1 GB), forpip install google-genaiwhen the Gemini pipe is installed and, in thefilterssuite, for the tiktoken encodings (cl100k_base,o200k_base) the driver downloads into the container'sTIKTOKEN_CACHE_DIR(without them the suite fails).
No Python on the host: the mocks, the probe pipe and the test driver run inside the
Open WebUI container, whose Python already ships aiohttp, httpx,
python-socketio, openai and mcp (the last two for the tool calling scenarios).
tests/e2e/run.sh # all suites against ghcr.io/open-webui/open-webui:v0.12.0-slim
tests/e2e/run.sh gemini # one suite (gemini, azure, n8n, infomaniak, filters)
tests/e2e/run.sh azure n8n # several suites
tests/e2e/run.sh --image v0.11.4-slim gemini # another Open WebUI version
pixi run e2e gemini # same via pixi (Linux)Typical output (abridged):
starting owui-e2e-111759-1234 from ghcr.io/open-webui/open-webui:v0.12.0-slim
Open WebUI healthy after 20s (http://localhost:57765)
Open WebUI 0.12.0, suites: gemini, azure, n8n, infomaniak, filters
function versions: google_gemini.py 1.20.0, azure_ai_foundry.py 3.0.1, n8n.py 2.3.1, ...
=== gemini ===
[PASS ] gemini.load pipelines/google/google_gemini.py loads (create, import, activate)
[PASS ] gemini.api.stream API stream without websocket session: answer streamed, thinking in the <details type="reasoning" done="true" duration="N"> block
...
[PASS ] gemini.tools.builtin built-in tool, browser stream=True: Open WebUI runs it, ...
...
--- gemini: 525s
...
=== infomaniak ===
...
[PASS ] infomaniak.models.name-prefix NAME_PREFIX valve changes the model names, and changing it back restores them
...
SUMMARY: 580 PASS, 0 FAIL, 0 KNOWN in 1263s
total runtime: 1299s
output: tests/e2e/out/20261009-111759-owui-e2e-111759-1234
A full run of all suites took about 20 minutes on a shared 8-CPU Docker host (2026-10-11,
google_gemini.py 1.20.0: 1213 s on v0.11.4-slim, 1263 s on v0.12.0-slim; up to 30 minutes
while other runs share the host).
Most of it is waiting: the azure rag group adds about 5.5 minutes (query-generation
timeouts, the query-generation pause and the 45 s retrieval limit are waited for; in the
meta-test, where every mock answers HTTP 500, its requests fail at once), the gemini
tools, toolsapi, imgedit and owuitools groups about three minutes, the gemini
owui012 group about one minute, shared about one minute on 0.12, and the filters ingest
group (Logs Ingestion API) another 2.5 minutes. The
browser scenarios of the gemini thinking group (paced streams for the live block and
its duration, Stop while thinking, a tool round) take about 20 s.
Network downloads on first use come on top
(pip install google-genai when the Gemini function is created, the tiktoken encodings
the filters suite caches before its first scenario). How many checks each suite has
and how many are KNOWN today is listed under What is tested.
The exit code is 0 when there is no FAIL (KNOWN results are fine), 1 when at least
one scenario FAILs and 2 for setup errors: Docker missing, docker run failing (port
busy, image missing), container not healthy, a volume NAME-data left over from an
earlier run (see --force), an option without its value, an unknown suite, --file
path or git ref, an --only regex that selects no scenario group, a driver error, a
driver that outlived E2E_TIMEOUT and had to be stopped. After Ctrl-C (SIGINT) or
SIGTERM the script exits with 130 / 143; see Interrupted runs.
| Option | Default | Meaning |
|---|---|---|
[suite ...], -s, --suites a,b |
all |
gemini, azure, n8n, infomaniak, filters (the modules in tests/e2e/suites/), all |
-i, --image IMAGE |
$OWUI_IMAGE or ghcr.io/open-webui/open-webui:v0.12.0-slim |
image, or just a tag (v0.11.4-slim) |
--only REGEX |
– | only scenario groups whose <suite>.<group> matches (Python regex), e.g. --only gemini.api; see below |
--ref REF |
– | test the function files as of a git ref (git show REF:path): a branch, tag or commit |
--src DIR |
– | test the function files from another checkout / worktree (not together with --ref) |
--file PATH=FILE |
– | replace one function file, e.g. --file pipelines/azure/azure_ai_foundry.py=/tmp/fix.py (repeatable); PATH must be one of the function files the harness installs, anything else is an error |
-n, --name NAME |
$E2E_NAME or owui-e2e-<time>-<random> |
container name; the volume is NAME-data |
-p, --port PORT |
$E2E_PORT or a free port picked by Docker |
host port of the UI (bound to 127.0.0.1) |
-o, --out DIR |
$E2E_OUT or tests/e2e/out/<timestamp>-<name> |
output directory (git-ignored) |
-k, --keep |
off (E2E_KEEP=1 turns it on) |
keep container and volume after the run |
--reuse |
off | reuse the running container --name (implies --keep): skips the start-up, stops drivers left over from an interrupted run, copies the current files, restarts the mocks |
--force |
off | delete a volume NAME-data left over from an earlier run; without it run.sh refuses to start (exit 2) instead of silently deleting data |
-v, --verbose |
off | print details of passing scenarios too |
--strict-known |
off (E2E_STRICT_KNOWN=1 turns it on; on in CI) |
a check tagged with a known bug that passes while its marker still applies is a FAIL, see Results |
Environment variables (all optional):
| Variable | Default | Meaning |
|---|---|---|
OWUI_IMAGE, E2E_NAME, E2E_PORT, E2E_OUT, E2E_KEEP=1, E2E_STRICT_KNOWN=1 |
– | defaults of --image, --name, --port, --out, --keep, --strict-known |
E2E_TIMEOUT |
1800 |
time budget of the driver in seconds. A suite still running when it is used up is a <suite>.timeout FAIL, suites that can no longer start get the same FAIL, and results.json is written. If the driver itself hangs, run.sh stops it 60 s later (SIGTERM, then SIGKILL after another 60 s) and exits with 2 |
E2E_SUITE_TIMEOUT |
900 |
time limit of one suite in seconds (<suite>.timeout FAIL; the next suite still runs) |
E2E_HEALTH_TIMEOUT |
600 |
seconds to wait for Open WebUI's /health after the container start |
E2E_SECRET_KEY |
a fixed test key | WEBUI_SECRET_KEY of the container (encrypts the valves) |
E2E_MOCK_FAULT |
– | an HTTP status (e.g. 500) every provider route of the mocks answers with; used by the CI meta-test (CI) |
--only takes a plain Python regex. Write alternatives with an unescaped pipe inside
quotes:
tests/e2e/run.sh --only 'gemini.(api|image)' geminiThe groups of each suite are listed in the GROUPS constant (and docstring) of its
module. A regex that matches no group stops the run with exit code 2 and prints the
available groups.
Unique container names, volumes and Docker-assigned ports make parallel runs (several worktrees, several agents) safe. The harness never removes images.
Every suite installs its function(s) through POST /api/v1/functions/create, sets the
valves to the mock, and checks at least: the file loads (import + activate), the
valve names are still there (they are public API), secrets are stored encrypted
and never show up in the server log (at any level), the model list, the API
path (non-stream and stream), the browser path (saved answer, usage and status
events), a background title task and that the server log has no unexpected
ERROR / Traceback / ResourceWarning block.
| Suite | Function(s) | Mock | Groups (--only <suite>.<group>) and file-specific scenarios |
|---|---|---|---|
gemini |
pipelines/google/google_gemini.py (+ google_search_tool) |
mock_gemini.py, mock_tools.py (tool servers, Jupyter) |
models (image / video indicators, display names #172), api (thinking in the <details type="reasoning" done="true" duration="N"> block, full usage), thinking (summaries not replayed #176: the plain block of chats saved before 1.19.0 and the type="reasoning" blocks, also a stopped done="false" one; budget, level, include and strip valves; browser path: the live done="false" block through throttled replace events, no thinking status, the switch to done="true" with all thoughts on the first answer part and deltas after it, the duration up to the first answer part (mock trigger paced-thinking), Stop while thinking and the next turn without the block (slow-thinking), a tool round that clears its live block), browser, tasks (no <details> in task answers, no grounding tools for the tasks of a web_search chat), image (image models forced non-stream, exactly one saved file, the thinking block), images (thought images skipped, used as fallback, not used after IMAGE_SAFETY; dedup; two final images; image link for API clients; image history, where the limit keeps the current and the newest images and cuts more current images than it allows; optimization), nano (gemini-nano-banana-2.1), imgvalve (IMAGE_GENERATION_MODELS), imgconfig (ImageConfig valves, user valve, body), imgtools (tools per image model with web_search), nostream (GOOGLE_STREAMING_ENABLED=false with stream=true, #170), video (Veo: text and video saved, request shape, image-to-video), grounding (google_search_tool → googleSearch + urlContext, sources, [1] citations, Open WebUI's search statuses (the queries, then Searched {{count}} sites with items and no urls); no grounding without web_search), vertex (Vertex AI Search sources), errors (400, 500 with retry, blocked prompt, SAFETY finish, image error status, a streamed answer starting with data:), retry (RETRY_COUNT for streams), status (Stop leaves no running status), valves (model cache vs. valve changes, safety, whitelist, additional models, system prompt, user headers, API version, params, valve names and defaults), concurrency (forwarded user headers belong to the requesting user), streamimg (inline image in a stream), imgedit (image editing across turns, #194: in a saved chat the image of turn 1, attached as a file only, is sent with the edit request on gemini-3.1-flash-image and gemini-nano-banana-2.1; the follow-up task gets no images; uploads between generated turns in the default mode, with another image per turn (the mock's final-image-<n> trigger) so their order shows; guided regeneration, also of an image-only message and of a text contained in the guidance; no guided regeneration after an empty answer, which Open WebUI 0.12 leaves out of the request (imgedit.empty-answer, 1.19.1); IMAGE_HISTORY_MAX_REFERENCES keeps the upload of the edit message, else the newest images; an image attached again is sent once and counts at its newest place (with IMAGE_DEDUP_HISTORY=false it is sent each time); older saved forms (a data: URL image file, markdown links to a file and to a data: URL; a text file is not sent); a temporary chat and a database error (injected by a test-only filter) take the history from the request; an image file of another user in a non-admin user's chat, in a markdown link on the API path and on the Veo path is not read (logged without its id), an admin continuing the user's chat reads the chat, but not the admin's file named in the user's message, which the user may not read (up to Open WebUI 0.11; 0.12 gives an admin only read access to another user's chat and answers the admin's turn with HTTP 404 before the pipe runs, which imgedit.admin checks there instead)), shared (chats shared with "Allow replies", Open WebUI 0.12, google_gemini.py 1.20.0; see Shared chats), toolsapi (native tool calling for API clients: client tools → tool_calls + reasoning_details streamed and non-streamed with finish_reason tool_calls (also as the openai SDK reads the stream), a text answer to a request with tools (finish_reason stop), GOOGLE_STREAMING_ENABLED=false, MALFORMED_FUNCTION_CALL / UNEXPECTED_TOOL_CALL streamed and non-streamed, continuation with and without signatures and with reasoning_details under provider_specific_fields, older turns with a thinking summary, empty arguments, content parts, a numeric id and a function that is not an object, tool_choice (also an undeclared name and with Search grounding on Gemini 3), name mapping (also a trailing newline) and duplicates, the default_api. prefix, schema clean-up, synthetic owui_ ids, unchanged answers without tools), tools (native tool calling through Open WebUI's tool loop: built-in tool streamed and non-streamed, parallel calls (responses in call order), two rounds with summed usage, merged rounds without signatures, text before a call, thinking on / off, workspace Python tool, a tool result with an image, OpenAPI and MCP tool servers, direct tools of the browser, tool approval (approve, reject, two calls), follow-up turns on Gemini 3 / 2.5 / 3 and after an approved call, unknown tool, MALFORMED_FUNCTION_CALL, Search grounding with functions on Gemini 3 (the sources of both rounds, the next turn without web search) and grounding only on 2.5, title task, Legacy mode, builtin_tools off, GOOGLE_STREAMING_ENABLED=false; see Native tool calling), owuitools (Open WebUI's built-in generate_image, edit_image and execute_code called by a Gemini text model: declared only with the chat toggles and for browser sessions, edit_image only with image editing on; the image engine gemini with generateContent and with predict (Imagen), editing an upload and the image of the previous turn, the image saved with the answer and its URL sent back to Gemini; a pipe image model makes its own image; the code interpreter with pyodide (the browser runs the code) and with Jupyter; see Open WebUI's image and code tools), owui012 (Open WebUI 0.12 features with native tool calling, version-gated: on 0.11 a missing feature passes with "feature not in Open WebUI "; context compaction between tool rounds (the summary request without tools; afterwards the summary, the user message and the kept rounds with their signatures; a follow-up turn whose boundary falls into the earlier turn), a failed or empty summary request between tool rounds and before a reply (also the summary saved with the chat, also on 0.11) leaves Open WebUI's own summary (1.20.0), Tool Search (search_tools and the <available_tools> list, the found tool declared and run in the next round and in the next turn, a mapped OpenAPI name; 1.20.0), model controls refused for pipe models and the custom params a control would set, Stream Chat Response off per chat and per model in a tool turn; see Open WebUI 0.12 features) |
azure |
pipelines/azure/azure_ai_foundry.py |
mock_azure.py (requires api-version; answers prompts with a <documents> block like a plain model, without context; query generation, embeddings, tool calls, a ~300 KB and a > 4 MiB stream event, content: null), mock_search.py (Azure AI Search and an App Service managed identity token endpoint, see below) |
valves (from 3.0.0 also the AZURE_AI_SEARCH_* valves, their defaults, enums and the password input of the key, and no AZURE_AI_SEARCH_MODE), models (AZURE_AI_MODEL lists separated by ;, , or spaces with exact names, AZURE_AI_PIPELINE_PREFIX, model from an *.openai.azure.com URL, predefined and fallback models), api (api-key and Bearer header, path and api-version, allow-listed body: extra client keys dropped, tools forwarded, stream_options only for streams; JSON 400 (exactly Azure's message, nothing appended) and text/plain 500 errors), dotted (gpt-4.1, Phi-3.5-mini-instruct reach upstream intact, model in header or body), browser (full status sequence, error status with exactly Azure's message), tasks (title task, also with Azure AI Search valves, #123), rag (the pipe's own Azure AI Search retrieval, 3.0.0, #187; suites/_azure_rag.py: API and browser path, stream and non-stream, Foundry /models endpoint; search request body per query_type, query text rules, embeddings for deployment_name (v1 route, gateway prefix, Bearer) and endpoint (api-version, own key or access token, chat key only on the same scheme, host and port), integrated vectorizer; fields_mapping with select, separator and fallback, list-valued titles; filter, top_n_documents (2 x candidates), strictness (per query), the api-version valve, the document budget (auto, also with a large top_n_documents, valve, unlimited, dropping a document); prompt placement, one block and one system message, in_scope and the tool-results clause, role_information, sanitizing (also nested tags, titles and file names), list and image-only content, Open WebUI's tool-images message, an attached file (<attached_files> and RAG template around the prompt; the search text is the typed prompt from __metadata__: base_user_prompt of Open WebUI 0.12 or user_prompt of 0.11, both also checked with the staged file in the driver process, query-text.metadata); citations, context event and message.context, scores per score type and with AZURE_AI_INCLUDE_SEARCH_SCORES off; [docX] → links, also split across stream deltas (API and browser, also without a finish chunk; in API streams all text before [DONE] and nothing after it), already linked or with parentheses in the URL (stream and non-stream), with a dotted deployment name; links in the history sent back as [docX] (also links saved before 2.8.0 with ) in the URL; a hostile history line in linear time); only referenced sources saved and the show-all valve (also for an answer that cites only a document the search did not return, [doc9], or one next to [doc1]); a context event over 128 KiB, an upstream event of ~300 KB (read and passed on) and one over 4 MiB (an error, API and browser); content: null; tool rounds (one search, reused only by a tool round of the same message with unchanged valves, no duplicate sources also when the tool round references a document, a non-stream tool call), tools and stream_options forwarded, tasks without retrieval and without data_sources (also for a saved chat without a websocket session); query generation (follow-ups, always / off, transcript with Open WebUI's conversation summary, max queries, <think> with draft queries, fallback also for JSON nested too deep, partial and all-failed results, merge order by reciprocal rank fusion and reranker score, model in the body, the pause after 3 timeouts in a row: per model, reset by a success, still on after 16 s); auth: key, key valve, access token, system- and user-assigned managed identity (mi_res_id), token errors; fail-closed errors with a terminal status (HTTP 500/403/402/404/400/302/429/503, connection, retries, the 45 s retrieval limit, an answer larger than 16 MB, configuration errors, client data_sources (API stream and non-stream and the browser path as from an inlet filter: the error naming the removal of On Your Data in 3.0.0, the pipe's ERROR line and the terminal error status, nothing forwarded; an empty list is ignored), context_length_exceeded (exactly Azure's message and the budget hint)); Stop during a search and during query generation; AZURE_AI_SEARCH_MODE=on_your_data stored by a 2.9.0 pre-release or set in the environment is ignored; log.debug: the staged file run in the driver process with every logger at DEBUG logs no key or token, see below; notice.none: no On Your Data retirement notice of 2.8.1 in the server log), logs (no API key, no search key or token, no citation text, no generated queries, document titles or URLs in the server log at INFO and above) |
n8n |
pipelines/n8n/n8n.py |
mock_n8n.py |
api (request payload contract, bearer / Cloudflare headers, usage, intermediateSteps tool display with verbosity and truncation, <think> blocks, history / INPUT_FIELD / RESPONSE_FIELD valves, plain text, NDJSON, SSE streams in separate and coalesced writes with plain lines and event: / id: / retry: fields, OpenAI-style chunks, UTF-8 characters split across writes, braces inside strings, a large object trickling in as small writes (server CPU), webhook error), browser (saved answer, usage and final status for JSON, NDJSON, UTF-8, an n8n error chunk, a broken stream and a webhook error; chat context sent to the workflow for chat turns vs. background tasks; Stop during a stream and a non-stream request), tasks (title task without and with a chat id) |
infomaniak |
pipelines/infomaniak/infomaniak.py |
mock_infomaniak.py |
models (llm models only, NAME_PREFIX), api (product id and bearer key, allow-listed body, SSE stream normal, coalesced into one write and split mid-JSON; OpenAI-style and Infomaniak error.description errors with one log line each), browser (saved answer, usage and status events for those streams plus no final newline, CRLF, a broken stream and an upstream error; Stop during the stream and while waiting for the response headers), tasks |
filters |
filters/*.py + probe pipe |
mock_la.py (Azure Log Analytics: HTTP Data Collector API, Logs Ingestion API, Microsoft Entra ID token endpoint, managed identity endpoints; the suite starts it, see below) |
model / global (filters attached per model via meta.filterIds and as global filters: features.web_search → __metadata__.features.google_search_tool, vertex_ai_search + VERTEX_AI_RAG_STORE, API request without features, time_token_tracker outlet on the API path with exact token counts, its status in the browser path, background task without __event_emitter__), spec (SEND_TO_LOG_ANALYTICS env parsing, encrypted shared key and client secret, Logs Ingestion valve defaults, valve names), la (Log Analytics records through the HTTP Data Collector API: signature, headers, payload and exact counts on the API and browser path, special tokens, multi-turn averages, sending switched off, HTTP errors, a slow and a hanging endpoint do not delay the answer, estimate marker), ingest (Logs Ingestion API, #188: request shape and 204, token cache (scope and tenant in its key) / concurrency / refresh, refresh failure with a still-valid token (also inside the back-off), the 30 s token back-off (still on after 20 s) and its end, token and HTTP errors with one log line each including a second revocation, a late 401 for a replaced token and a persistent 401, a secret echoed across the cut points of an error text, undecryptable secret, https-only endpoint and authority and other invalid settings, slow / hanging / unreachable endpoints, mode selection and fallback, both (also with one side incomplete), deprecation warning, sovereign cloud valves, App Service (also its {statusCode, message, correlationId} error) / IMDS (without proxy) / workload identity, no secret or token in the log, DEBUG lines included), valves (compact status), correlation (inlet/outlet correlation when Open WebUI rewrites the last user message, concurrent identical requests), encoding (model-specific encoding, gpt-4o → o200k_base), offline (the tiktoken download hangs: estimates, one load at a time, retry, server not blocked), multimodel (multi-model chat and the features dict the models share), search (google_search_tool with features {}, null or without web_search, other feature keys kept, no per-user permission check), vertex (per-request data store, store only with the feature, features: null) |
The probe pipe (tests/e2e/probe/probe_pipe.py) answers with a JSON report of what
Open WebUI handed it (body keys, model and messages, __metadata__ features / params /
model id, __task__, whether an __event_emitter__ exists), so filter → pipe coupling
is tested without a provider. PROBE_SLEEP=<s> in the last user message delays its
answer (overlapping requests), PROBE_SLEEP[<model>]=<s> only that model's answer
(multi-model chats). PROBE_ENV=<file> sets (or, for null, removes) the environment
variables of a JSON object in that file in the server process, for an allow-list of
managed identity, workload identity and proxy variables only; the ingest group uses
it to switch between App Service, IMDS and workload identity without a restart and
puts the container's own IDENTITY_ENDPOINT / IDENTITY_HEADER (set by run.sh for
the azure suite) back at its end. The values go through the file, never through the
chat text. PROBE_LOG=<file> writes
everything the time_token_tracker logger logs, DEBUG included, to that file
(PROBE_LOG=off stops it): the server log runs at INFO, so the filters suite captures
the tracker's DEBUG output while its Log Analytics groups run (tracker_debug.log in
the output directory) and checks it for plaintext secrets (log.no-secrets-debug).
The filters suite sets up the Log Analytics mock itself: it maps the workspace host
<id>.ods.opinsights.azure.com, the login hosts login.microsoftonline.com /
login.microsoftonline.us and two DCR ingestion hosts to 127.0.0.1 and an
unreachable DCR host to 127.0.0.9 in the container's /etc/hosts (the login and DCR
lines are removed again at the end), installs a throw-away test CA into the container's
trust store (kept across --reuse runs, valid 30 days; the server certificate lists
every mapped host and is reissued when a name is missing) and starts
mocks/mock_la.py (HTTPS on 127.0.0.1:443, control routes on :9105, managed identity
endpoints on 127.0.0.1:9107 and 127.0.0.2:9107). The ingest group takes about two
and a half minutes, mostly timeout, token-expiry and token back-off waits. It also creates the user
filters-user@example.com; the gemini suite creates e2e-gemini-user@example.com,
e2e-imgedit-user@example.com and, in its shared group, the non-admin users
e2e-shared-*@example.com, a folder of the owner and the group E2E Shared Chats
(deleted at the group's end).
The offline group needs a fresh container (tiktoken keeps loaded encodings per
process), so do not rerun it with --reuse.
The azure rag group uses mocks/mock_search.py (127.0.0.1:9106, started with the
other mocks): Azure AI Search Documents - Search Post with the indexes x100-docs,
x100-custom (other field names, no vector field) and client-index (must never be
queried), Search-like validation (api-version, api-key / Bearer, select, vector
length, Content-Type), scores by rank, and trigger words in the search text
(no-hits, search-500, search-503-once, search-slow, doc-inject, ...; the
list is in the mock's docstring). It also answers GET /msi/token, the App Service
managed identity endpoint: run.sh starts the container with
IDENTITY_ENDPOINT=http://127.0.0.1:9106/msi/token and
IDENTITY_HEADER=e2e-identity-header, so azure-identity's ManagedIdentityCredential
and DefaultAzureCredential in the Open WebUI process mint their tokens there (a
user-assigned identity is selected with mi_res_id, as on App Service; a
resource id containing mi-fail gets HTTP 400). A container started by an older
run.sh lacks these variables; the managed identity checks then fail with --reuse.
The server runs at INFO, so logs.no-secrets covers INFO and above. Open WebUI at
GLOBAL_LOG_LEVEL=DEBUG logs the parameters of its database queries itself (valve
values among them), so a DEBUG server log cannot show what the pipe logs. Instead
rag.log.debug loads the staged Azure file into the driver process (with a stub
open_webui.env: SRC_LOG_LEVELS OPENAI=DEBUG), sets every logger to DEBUG and calls
pipe() against the same mocks (search keys, tokens, embeddings, managed identity,
query generation, errors, client data_sources with a key, also streamed); no log record may
contain a key or token.
Checks per suite in strict known mode on Open WebUI v0.12.0-slim, v0.11.4-slim
and v0.11.3-slim (observed 2026-10-11 with google_gemini.py 1.20.0 and the
gemini.shared and gemini.owui012 groups, azure_ai_foundry.py 3.0.1, Time Token
Tracker 2.7.0, infomaniak.py 2.2.2; before the two groups 549 checks on every version,
with google_gemini.py 1.19.1; with the thinking level checks
thinking.level-models and .params-robust and the 4 checks of the security review
fixes of 1.20.0, which run on 0.12 only):
| Suite | Checks on v0.12.0 | Checks on v0.11.4 / v0.11.3 | PASS / KNOWN |
|---|---|---|---|
gemini |
197 | 182 | all / 0 |
azure |
199 | 199 | all / 0 |
n8n |
51 | 51 | all / 0 |
infomaniak |
32 | 32 | all / 0 |
filters |
101 | 101 | all / 0 |
| all | 580 | 565 | all / 0 |
No FAIL and no obsolete marker on any of the three versions. The gemini.shared group
has 17 checks on 0.12 and two before (shared.unsupported, shared.legacy), see
Shared chats; the 12 gemini.owui012 checks run on every
version, and on 0.11 a check of a 0.12 feature checks what 0.11 does instead (see Open
WebUI 0.12 features). gemini.imgedit.admin
expects on 0.12 that Open WebUI refuses an admin's turn in another user's chat (HTTP
404), and on 0.11 that the pipe reads that chat: the user's generated image, not the
admin's file the user named in turn 1 (the user may not read it). Before the
compatibility changes, main (31a4e7d) had 542 PASS and 5 FAIL on v0.12.0-slim: the
server-log checks of azure, n8n and infomaniak (the module names), imgedit.admin and
azure.rag.query-text.attached. The checks of the 1.19.0 thinking block
and search statuses carry no marker either: with google_gemini.py 1.18.0, 12 of the 32
checks of --only 'gemini.(api|thinking|browser|grounding|image)$' FAIL
(api.nonstream / .stream, browser.stream / .nonstream, image.browser-preview /
-ga, thinking.strip-reasoning, .live, .duration, .stop, .tool-round and
grounding.status); toolsapi.unchanged checks the same block shape.
No known bug is registered (harness/known_n8n.py has no entry): the last one,
infomaniak-name-prefix (the NAME_PREFIX valve was read only once), was fixed in
infomaniak.py 2.2.2, and its marker went with the fix. The markers of the 62 bugs fixed by #182-#185 were dropped after the merge; their checks stay and
must pass. The imgedit group came with its fix and carries no marker either: with
google_gemini.py 1.18.0 before the fix (a7fc191) 15 of its 19 checks FAIL. The four
that pass check what did not change: imgedit.temporary (the fallback to the
request), imgedit.video-file (reading the user's own file) and the two
imgedit.guided-* variants (Open WebUI also puts the upload of the regenerated message
into the request). images.history and images.history-current FAIL there too (the
image limit kept the oldest images and could drop the current ones).
The two groups live in tests/e2e/suites/_gemini_tools.py (run by suites/gemini.py;
the leading underscore keeps the module out of the suite discovery); --only 'gemini.tools' runs both, --only 'gemini.tools$' the browser group only. The Gemini mock
answers with function calls, Open WebUI runs the tools (browser path) or hands the
tool_calls to the client (API path), and the scenarios check the saved output
items, what the client got and what the pipe sent upstream (the mock's record).
The MOCKTOOLS: directive. A JSON object after MOCKTOOLS: in the turn's user
message (the last user content with text that is not Open WebUI's tool-image message)
makes mock_gemini.py answer with function calls:
MOCKTOOLS:{"rounds": [[{"name": "get_current_timestamp", "args": {}}],
[{"name": "calculate_timestamp", "args": {"days_ago": 1}}]],
"text_before": "Let me check.", "split": false, "noid": false, "nosig": false,
"server_side": false, "malformed": false, "allow_undeclared": false}
| Option | Effect |
|---|---|
rounds |
round r (the number of user contents with function responses after the turn's message, at least 1 + the highest <r> of a response id mock-call-<r>-<k>, because Open WebUI merges rounds without text or reasoning in between) answers with the calls of rounds[r]; after the last round the answer is MOCK-FINAL <name>=<json response>; ... over every function response of the turn |
| names | Open WebUI names; the call uses the pipe's mapped name (_gemini_function_name, copied into the mock) and must be declared, else the answer is MOCK-ERROR undeclared function <name>; declared=[...] (HTTP 200) |
allow_undeclared |
call an undeclared tool anyway (under its own name) |
noid |
calls without id (otherwise mock-call-<r>-<k>) |
nosig |
no thought signature (otherwise gemini-3* models sign the first call of a round with base64("mock-sig|<model>|<id or name>")) |
text_before |
a text part before the calls |
split |
streaming: one chunk per call (otherwise all calls in one chunk) |
server_side |
with toolConfig.includeServerSideToolInvocations in the request: a server-side toolCall (signed) + toolResponse before the calls |
malformed |
the first request gets a candidate without content and finishReason: MALFORMED_FUNCTION_CALL, or the finish reason given as a string ("malformed": "UNEXPECTED_TOOL_CALL") |
A tool round's usage is USAGE_TOOL (21 / 5 / 26 tokens); streaming ends with a
{"text": ""} chunk with finishReason: STOP. With googleSearch in the request a
tool round cites https://example.com/tool-round and the final answer
https://example.com/a, so the sources of each round can be told apart. Like the real API, every generate
request is rejected with HTTP 400 INVALID_ARGUMENT for duplicate declaration names,
invalid names, parameters together with parametersJsonSchema, a model content with
function calls that is not directly followed by a user content with matching function
responses (count, names, ids) and, for gemini-3*, a first function call of a model
content of the current turn without the issued signature or
skip_thought_signature_validator. Each generate request records declared, decl
(the raw schema of every declaration, before any key normalisation), tool_config,
fc_mode, allowed_names, include_flag, kinds (role and part kinds per content),
fc / fr (calls with id, name, args and signature state none / skip / issued /
bad; responses with id, name and response), round, directive, answer (fc,
final, text, summary, malformed, mock-error, http-<status>), issued,
server_echoed, synthetic_ids_upstream and orphan_fr.
Tool mocks. mocks/mock_tools.py, started by serve_all.py, is an OpenAPI tool
server on 127.0.0.1:9111 (get_weather with a query parameter, convert_units with a
JSON body, and lookup.v2, an operationId Gemini does not accept as a function name)
and an optional MCP server (FastMCP streamable HTTP from the image's mcp package) on
127.0.0.1:9112 (mcp_echo, mcp_sum; Open WebUI names them <server id>_mcp_echo).
GET /__requests on 9111 lists the OpenAPI calls and the MCP tool calls. They are not
provider mocks: E2E_MOCK_FAULT does not touch them. probe/workspace_tool.py is the
workspace Python tool the tools group creates (add_numbers reports the types it
got, whoami the user, the chat and its event emitter, and emits a status;
make_image returns a PNG data URI, which Open WebUI passes on in a user message
after the tool results). The group
deletes the tool, restores the tool server connections and the chat settings and
deletes its model overrides when it ends.
Browser harness. BrowserSession sends the Socket.IO namespace sid as
session_id (sio.get_sid(), what the web UI sends as socket.id; the engine.io sid
sio.sid it sent before is unknown to Open WebUI, so every server → client call
failed with "Client session disconnected.") and a heartbeat event every 20 s like
the web UI (Open WebUI reaps a session that sent none for 120 s). It answers Open
WebUI's execute:tool calls (direct tools) with direct_tool_answer(data) as the
socket.io ack, e.g. [{"value": "client:k1"}, {"content-type": "application/json"}]
(result, headers) and records them in execute_calls. chat() takes tool_ids,
tool_servers, extra_body and until (a predicate on the saved message that ends
the wait, e.g. a call waiting for approval); wait() waits again after
owui.resolve_tool_call(...). BrowserChat adds output, function_calls
([(name, status, call_id, arguments)]), function_outputs ({call_id: text}),
reasoning_items ([(text, reasoning_details)]) and output_text; content falls
back to the output text because Open WebUI's final save of a tool turn leaves
content empty. owui.py adds create_tool / delete_tool, set_tool_servers,
chat_config / set_chat_config, resolve_tool_call, and parse_sse collects
tool_calls (merged by index), reasoning_details, reasoning_content,
finish_reasons, done_last and openai_finish_reason (the openai SDK's
ChatCompletionStreamState, which ships with the image).
Preflight and mock_ok. Both groups start with a plain API request to
gemini-2.5-flash; every detail line starts with mock_ok=<bool> (the request got
Hello from mock). The evidence of a known bug of these groups must require
mock_ok=True plus a token that the request itself got the provider's answer
(upstream=[fc,...], upstream=[mock-error], answered=<n>, ...), so that with
E2E_MOCK_FAULT it can never be KNOWN. The detail tokens are stable (evidence can
match them): http=, done=, calls=[name:status,...], outputs=<n>,
rd=[format:id,...], upstream=[answers in order], declared_n=, safe_names=,
sig_echoed=[id:sig,...], fr=[id:name:keys,...], kinds=, tool_kinds=,
include_flag=, server_echoed=, finish=[...], openai_finish=, usage= and
final= (last). WARNING lines of the pipe that mean lost tool data (Skipping duplicate tool declaration, Dropping unmatched function call, Dropping unmatched tool result, Could not restore stored model content, Invalid tool call arguments)
fail the server-log check unless a scenario provokes one (t.expect_warnings);
tools.log fails on 'callable', __signature__, Duplicate function declaration or
AFC is enabled anywhere in the server log of the group.
The two groups came with native tool calling (google_gemini.py 1.18.0) and carry no
known-bug markers, so a --ref run of an older google_gemini.py reports most of
their checks as FAIL (42 of the 48 with 1.17.0). Open WebUI 0.11.4's approval defects
(an approved call loses its result in the saved message; with two calls only the first
is asked and the second is not run) are only recorded in the detail of
tools.approval-parallel, never asserted.
The group lives in tests/e2e/suites/_gemini_owuitools.py (run by suites/gemini.py
after tools). It checks that Open WebUI's own built-in tools generate_image,
edit_image (Open WebUI's image engine gemini) and execute_code (code interpreter)
work with a Gemini text model through Open WebUI's tool loop. It switches on Admin
Settings → Images with the engine gemini against the Gemini mock
(IMAGES_GEMINI_API_BASE_URL=<mock>/v1beta, keys mock-images-key and
mock-images-edit-key, by which the mock record tells the engine's requests from the
pipe's), image editing with gemini-3.1-flash-image and the code interpreter with the
engine pyodide, and restores both settings at its end. The MOCKTOOLS: directive makes
Gemini call the tool:
| Check | What it shows |
|---|---|
owuitools.api |
API path with both feature flags on: none of the three tools is declared (Open WebUI adds built-in tools for browser sessions only); pyodide_note= records, without asserting it, that Open WebUI still adds its pyodide note to the system prompt |
owuitools.declared |
browser path: with features.image_generation and features.code_interpreter (the chat toggles) all three are declared and Open WebUI's pyodide note is in the system instruction; without them none |
owuitools.declared-noedit |
ENABLE_IMAGE_EDIT=false: generate_image without edit_image |
owuitools.generate |
generate_image with the endpoint method generateContent (gemini-2.5-flash-image): the engine request (path, key, prompt), the image saved with the answer (message files, chat:message:files) with the bytes the mock returned, the tool result with its URL in the continuation, the final answer |
owuitools.generate-predict |
the same with predict (imagen-4.0-generate-001, the mock's :predict route) |
owuitools.edit |
edit_image on an upload: the request to Gemini names the file's URL in <attached_files>, the edit engine gets the uploaded image, the edited image is saved |
owuitools.edit-followup |
edit_image in the next turn on the image of owuitools.generate: the replayed tool result holds its URL, the edit engine gets that image |
owuitools.image-model |
a pipe image model with the Image toggle on: no function declarations, its own image is saved, the engine is not called |
owuitools.code |
execute_code, engine pyodide: the browser session gets execute:python (the code, its session id), its stdout is the tool result; an image line in it is uploaded and linked |
owuitools.code-jupyter |
engine jupyter: the Jupyter mock starts a kernel, runs the code and deletes the kernel; no execute:python, no pyodide note; stdout and result reach Gemini |
The Jupyter mock is part of mocks/mock_tools.py (http://127.0.0.1:9111/jupyter/:
POST api/kernels, the kernel websocket api/kernels/<id>/channels, which answers an
execute_request with stdout JUPYTER-MOCK-STDOUT, the result 'JUPYTER-MOCK-RESULT'
and status idle, and DELETE api/kernels/<id>; the executed code is recorded with the
path jupyter:execute). BrowserSession acks execute:python with
python_answer(data) (default None, an empty ack; the web UI acks with the result of
its pyodide worker, {stdout, stderr, result}) and records the calls in
python_calls. mock_gemini.py answers the image engine: the generateContent
request of an image model like any image request (without thoughts, the engine asks for
none), :predict with predictions (the final-image-<n> trigger in the prompt picks
the PNG). Details start with mock_ok= as in the tool groups; the extra tokens are
engine=[action:model:key], file_ok= and sent_ok=. Against google_gemini.py
1.17.0, which handed the tools to google-genai's automatic function calling, 6 of the
10 checks FAIL (generate, generate-predict, edit, edit-followup, code,
code-jupyter).
The group lives in tests/e2e/suites/_gemini_shared.py (run by suites/gemini.py
after imgedit). Open WebUI 0.12 shares a chat with users or groups (POST /api/v1/chats/<id>/share with share_mode: "continue" for "Allow replies", then POST /api/v1/chats/shared/<id>/access/update with the grants) or a folder (POST /api/v1/folders/<id>/access/update). The group drives these APIs with five non-admin
users: the owner A, a participant B (user grant on chat X, "Allow replies"), a member G
of a group with a grant on X, a viewer D (chat Y shared "Clone only" with D, and Y in a
folder of A shared read-only with D) and an outsider C. Chat X starts with A's turn on
gemini-3.1-flash-image with the upload upload1 (generated image final); B, A and
G then write in it (the mock's final-image-<n> trigger gives each turn its own image).
A second chat X2 of A, shared with B with "Allow replies", covers the fixes of the
security review of 1.20.0: B names A's private file (saved) in a message, A then edits
twice with A's own link to it, B plants 30 links to files A may not use, A edits again.
A test-only global filter (e2e_chat_redirect, installed for the group) hands the pipe
another chat when an API request's prompt carries e2e-chat=<chat id>/<user message id>: it sets chat_id and user_message_id of __metadata__, as if Open WebUI had
called the pipe for that chat without its own check. So the pipe's own check is tested
also for the users Open WebUI turns away before the pipe. e2e-link=<chat id> sets
chat_id and a new message_id, so the pipe emits and links its generated images as
for a message of that chat. e2e-chat-fault=<chat id>
and e2e-file-fault=<file id> make the pipe's next call of
Chats.get_accessible_chat_by_id / has_access_to_file for that id raise, and
e2e-chat-truthy=<chat id> the next call of Chats.get_accessible_chat_by_id return
True, a truthy value that is not the chat (an inert, signature-preserving async
wrapper stays installed in the server process, so the pipe's feature detection still
sees the helpers on a later load).
| Check | What it shows |
|---|---|
shared.visible |
Open WebUI lets B read A's upload and the generated image of X (GET /api/v1/files/<id>/content): the pipe links its images to the chat |
shared.participant |
B's browser turn in X sends upload1 and final (the edit request as in imgedit) |
shared.owner |
A's next turn also sends B's generated image (gen2, a file of B) |
shared.group |
G's turn sends every image of X |
shared.refused |
D's turn in Y and C's turn in X: Open WebUI answers HTTP 404, no request reaches the mock |
shared.pipe-participant |
filter: B is handed X, the images are sent (shows that the filter works) |
shared.pipe-admin |
filter: the admin (read access to every chat in 0.12, no write access) gets none of X |
shared.pipe-viewer |
filter: D (read access to Y through the Clone only share and the read-only folder) gets none of Y |
shared.pipe-outsider |
filter: C gets none of X |
shared.pipe-truthy |
filter, e2e-chat-truthy: the chat check answers True instead of the chat for the admin: none of X is sent (only the chat itself allows) |
shared.chat-fault |
the chat check raises for B: nothing of X is sent, the error is logged, the answer completes |
shared.links |
B's API request with markdown links: A's upload of X is read; a file of the admin and a file of A outside X are not (logged twice, without their ids) |
shared.file-fault |
the file check raises for B's link to A's upload: it is not read, logged without its id |
shared.video |
Veo image-to-video: B's markdown link to A's upload of X is sent as instances[0].image |
shared.deputy |
X2: A's turn after B's message with a link to A's private file sends upload1, final and gen2, not the private file (B may not read it; logged), which stays HTTP 404 for B; A's own link to it is sent in A's turn and, from A's message, in A's next turn |
shared.history-cap |
X2: after B's 30 planted links (the admin's file and A's private file) A's turn looks at no more than 20 images of earlier messages (4 x IMAGE_HISTORY_MAX_REFERENCES): at most 20 refusals logged, only B's newest generated image sent, the answer completes |
shared.link-chat |
filter, e2e-link to X2: B's generated image is linked to X2 (A may read it), C's two images are not (HTTP 404 for A; Not linking generated files logged twice), once decided by the image history (e2e-chat=) and once by the linking itself |
On Open WebUI before 0.12 the group records shared.unsupported (PASS, feature not in Open WebUI 0.11.4) and shared.legacy: A's chat shared with B through a 0.11 share
link with a user grant, Open WebUI lets B read the upload and the generated image, but the
pipe keeps its owner / admin rule and does not read B's markdown link to the upload.
With google_gemini.py 1.19.1 on v0.12.0-slim, 10 of the first 13 checks FAIL; refused,
pipe-viewer and pipe-outsider pass (1.19.1 allowed only the owner or an admin).
With 1.20.0 before the security review fixes (3443671) the four checks of the fixes
FAIL and the other 13 pass: pipe-truthy (the admin got X's images), deputy (A's
turn sent saved), history-cap (15 refusals, but the private links were read as A's
own and filled the limit) and link-chat (A could read C's images, HTTP 200).
The group takes about one minute on 0.12 and a few seconds on 0.11.
A mutation round on the access logic (2026-10-11, --only 'gemini.(shared|imgedit)$')
killed every mutant: the chat check always allowing (pipe-admin, pipe-viewer: the
file check still refuses the outsider), permission="read" (pipe-admin,
pipe-viewer), chat check errors allowing (chat-fault), the file check allowing
after has_access_to_file (links and imgedit.foreign, .api-foreign,
.video-foreign), file check errors allowing (file-fault), the feature detection
always off (9 shared checks), an admin bypass of the chat check (pipe-admin), no
linking of generated images (visible, participant, owner, group,
pipe-participant; on 0.11.4 legacy); on v0.11.4-slim the feature detection always on
(13 imgedit checks and legacy) and has_access_to_file used without the 0.12
detection (legacy). The last two cannot differ on 0.12.
A second round on the fixes of the security review (2026-10-11, same --only, each
mutant through --file) killed every mutant as well: the author check off and the
author check using the requesting user (deputy and history-cap on v0.12.0-slim,
imgedit.admin on v0.11.4-slim), linking without the chat check (link-chat), no
cap on the images of earlier messages (history-cap: 30 refusals logged) and the chat
check back to bool(chat) (pipe-truthy).
The group lives in tests/e2e/suites/_gemini_owui012.py (run by suites/gemini.py
after owuitools). Through /api/v1/chats/config it switches on context compaction
(threshold and cap 1 token) for the compaction scenarios and Tool Search (deferral
threshold 0, get_current_timestamp always loaded, built-in tools deferred) for the
Tool Search scenarios; it creates the workspace models e2e-g012-*, and at its end it
restores the chat settings and the tool server connections and deletes the models.
A check of a 0.12 feature passes on 0.11 with "feature not in Open WebUI "
in its title and checks what that version does instead (no compaction between tool
rounds, no search_tools, model_controls stored as an ordinary parameter). Details
start with mock_ok= (the preflight of the tool groups) and owui=<version>.
| Check | What it shows |
|---|---|
owui012.compaction |
three tool rounds on gemini-3-pro-preview: 0.12 compacts before the third and the fourth request; both summary requests reach the pipe without tools (summaries=['summary', 'summary']); the next request has [CONVERSATION SUMMARY] and the mock's summary in the system instruction, the user message and the last round only (kinds=[user:text,model:fc,user:fr], the call with its issued signature, no orphan response); all rounds stay saved |
owui012.compaction-followup |
a tool turn after a turn with two rounds: the boundary falls into the earlier turn and Open WebUI puts the current user message before that turn's kept round and answer (prompt_first=True, kinds=); the request is still valid (every call with its response, first calls signed) |
owui012.summary-error, owui012.summary-empty |
the summary request between tool rounds fails (mock trigger summary-400) or has no text (summary-empty: the thought part only, MAX_TOKENS): the next request carries Open WebUI's fallback summary (- user: ...), not the pipe's error text or [No content generated] |
owui012.saved-summary |
compaction before the third reply (0.11 and 0.12), the summary request fails: the contextSummary Open WebUI saves on the second user message and the summary it sends are its fallback, not the error text |
owui012.toolsearch |
search_tools and get_current_timestamp declared, the deferred calculate_timestamp only in <available_tools>; after search_tools returned it, it is declared in the next request, called and run |
owui012.toolsearch-followup |
the next turn calls the found tool directly: it is declared from the search result in the history |
owui012.toolsearch-mapped |
the deferred OpenAPI tool lookup.v2: declared as lookup_v2_<hash> after the search, the tool server gets the call |
owui012.controls |
0.12 refuses model controls on a workspace model of a pipe model (HTTP 400 "Model controls require a server-managed provider model.") |
owui012.custom-params |
custom params of a workspace model (reasoning_effort=low, include_thoughts=false; thinking_budget=1024 on 2.5) reach the pipe and the chat's custom params win (reasoning_effort=high); reasoning_effort_25= records, without asserting it, that reasoning_effort leaves a 2.5 budget unchanged |
owui012.stream-chat |
Stream Chat Response off for the chat (stream=false, params.stream_response=false), two tool rounds: the first request is not streamed, both tools run, the turn is done |
owui012.stream-model |
Stream Chat Response off in the model's Advanced Params: Open WebUI sends stream=false; the tool round runs, the turn is done |
The Gemini mock answers Open WebUI's compaction prompt ("Summarize the conversation
history that will be compacted ...") with MOCK-SUMMARY of <call ids>, the mock's call
ids in its "Messages Being Compacted" section (answer=summary in the record); the
triggers summary-400 and summary-empty in the user message reach only that request,
because the prompt contains the chat's messages. With google_gemini.py 1.19.1 six
checks FAIL on 0.12.0: summary-error, summary-empty, saved-summary, toolsearch,
toolsearch-followup and toolsearch-mapped; on 0.11.4 saved-summary FAILs too
(stored=["Error generating content: 400 INVALID_ARGUMENT. ..."]). A copy of 1.20.0 that answers a
stream=false tool round of a chat with a chat.completion instead of a stream fails
stream-chat and stream-model on 0.12.0 (the turn is done, but the tool never runs)
and on 0.11.4 (the turn never ends): the pipe's stream for such rounds is still needed.
- API path (
OWUI.chat):POST /api/chat/completionswithoutchat_id, like an OpenAI-compatible client. The answer comes back directly (JSON or SSE); nothing is saved; outlet filters still run. - Browser path (
BrowserSession.chat): a socket.io connection plussession_id, message ids anduser_messagein the request, like the web UI. Open WebUI runs the pipe in the background with a real__event_emitter__and saves the answer, usage, sources, files andstatusHistoryinto the chat, which the driver reads back. Only this path shows whether events and saved content work. - Background tasks: title/tags/follow-ups call the pipe again with
__task__set;POST /api/v1/tasks/title/completionsruns one directly (no__event_emitter__),background_tasksin a browser-path request runs them after the answer.
PASS– the check held.FAIL– the check did not hold. The run exits with 1.KNOWN– the check did not hold because of a known bug registered intests/e2e/harness/known_<area>.py(today onlyknown_n8n.pyfor n8n + Infomaniak, with no entry at the moment; re-exported byknown.py): key, summary, issue reference, pull request with the pending fix, evidence, and the functionfileplus the versionfixed_inthat fixes it. Printed with the bug, e.g.known <key> (found by tests/e2e, no issue filed): ...; no fix yet, or for a bug with a pending fix...; fix pending in PR #<n> (not merged yet), fixed in <file> <version>. KNOWN does not fail the run.
A tagged check only counts as KNOWN when the failure looks like that bug: one of the
bug's evidence regexes matches the check's detail text, or (for checks that pass
since=mark) the server log since mark contains the bug's log signature. A tagged
check that fails in another way (an HTTP 500, a missing model, ...) is a FAIL and says
tagged known <key>, but the failure does not show it.
Version gating. A known marker only applies while the tested copy of its file
(the staged file in functions/) has a docstring version: lower than fixed_in:
| Tested file | Tagged check fails | Tagged check passes |
|---|---|---|
older than fixed_in (e.g. main before the fix is merged) |
KNOWN | PASS with the reminder no longer reproduces, but <file> is older than <fixed_in> (a FAIL in strict mode) |
fixed_in or newer (the fix branch, main after the merge, a mutant of the fixed file) |
FAIL regression of known <key> (fixed in <file> <version>) |
PASS, listed under Obsolete known markers: drop marker in summary.md and obsolete_markers in results.json (not a failure) |
So main stays green while a fix is pending, the fix branch is protected against
regressions of exactly that bug, and the markers switch off by themselves once the fix
(with its version bump) is merged. A bug without a fix has fixed_in="" and is never
gated.
Strict mode (--strict-known or E2E_STRICT_KNOWN=1, on in CI): a tagged check that
passes while its marker still applies is a FAIL. It catches a fix without a version bump
and evidence that no longer describes the bug.
The fixing branch named in a KNOWN line may only exist as an open pull request (or not be published yet) until the fix is merged; the issue link, where there is one, is the stable reference.
The server-log check fails on every ERROR / Traceback / ResourceWarning block logged
while the suite ran, on plaintext values of password valves (any log level), and on
WARNING blocks a suite registered with fail_on_warnings, except blocks provoked on
purpose (expect_errors / expect_warnings with the signature of the provoked error or
warning) and ERROR or WARNING blocks that match the narrow log signature of a known bug
that reproduced in this suite (function name plus error, e.g. Error in outlet filter time_token_tracker + 'NoneType' object is not callable). A different error in the
same function, or the same error in another function, still fails it.
The driver adds a few checks of its own; they only show up when something is wrong:
| Check | FAIL when |
|---|---|
<suite>.timeout |
the suite ran longer than E2E_SUITE_TIMEOUT, or the run's budget E2E_TIMEOUT was used up (also for suites that could not start any more); the detail names the last check the suite recorded |
<suite>.crash |
the suite raised an exception (also CancelledError, KeyboardInterrupt or SystemExit from suite code); the other suites still run |
<suite>.no-checks |
the suite recorded no check at all |
<suite>.interrupted |
Ctrl-C / SIGTERM stopped the run during the suite (partial results) |
<suite>.server-log |
also recorded by the driver for a suite that ended before its own log scan (crash, timeout, return after a failed install) |
run.server-log |
an unexpected error block or a plaintext secret in a part of the server log no suite's server-log check read: before the first suite, between suites (the driver waits 1 s after each suite so late lines are not charged to the next one) and after the last one (late background tasks). The detail names the window (after azure) and the function_<id> that logged it |
run.mocks-log |
an error in the output of the provider mocks (mocks.txt). A mock answer that was cut short because the client (the pipe) closed the connection is listed as a note in summary.md, not a FAIL |
The output directory contains:
| File | Content |
|---|---|
driver.txt |
console output of the run |
results.json |
meta (image, Open WebUI version, function sources and their versions, strict_known, time limits, completed, interrupted / setup_error / driver_error when the run was cut short, mock_notes), summary, obsolete_markers, one entry per scenario (suite, id, title, status, detail, known, marker) |
summary.md |
Markdown summary (used as GitHub Actions job summary), with notes on partial results and cut-short mock answers |
server.log |
output of the Open WebUI server (/tmp/e2e/server.log in the container, the same text as docker logs) |
mocks.txt |
output of the mock servers |
mock_la.txt, tracker_debug.log |
filters suite only: output of the Log Analytics mock, and what time_token_tracker logged (DEBUG included) while the Log Analytics groups ran |
functions/ |
the exact function files that were tested (converted to LF line endings), plus SOURCES.txt (where they came from) |
results.json and summary.md are also written when the run is cut short (Ctrl-C,
SIGTERM, E2E_TIMEOUT, a driver error); meta.completed is then false.
tests/e2e/run.sh --ref my-fix-branch azure # a branch, tag or commit
tests/e2e/run.sh --ref origin/main azure # e.g. compare with main
tests/e2e/run.sh --src ../other-worktree gemini # files of another checkout
tests/e2e/run.sh --file pipelines/n8n/n8n.py=/tmp/n8n_fix.py n8n
tests/e2e/run.sh --image v0.11.3-slim gemini # A/B against an older release
OWUI_IMAGE=ghcr.io/open-webui/open-webui:main tests/e2e/run.shA fix is complete when its KNOWN scenarios turn into PASS on the fix branch. The fix
bumps the file's version:, and fixed_in of the bug's KnownIssue names that version:
from then on the markers are off for every file with that version (a regression is a
FAIL), while main keeps reporting KNOWN until the fix is merged. Once it is merged,
drop the markers listed under "Obsolete known markers" in summary.md (the known=
arguments and the KnownIssue entry). To test fixes from several branches together,
export each file (git show my-fix-branch:pipelines/azure/azure_ai_foundry.py > /tmp/azure.py) and pass one --file per file. functions/SOURCES.txt in the output
directory records where each tested file came from.
Function files are staged with LF line endings whatever their source: a Windows checkout
(core.autocrlf=true) has CRLF, --ref gives the file as stored in git (n8n.py,
infomaniak.py and google_search_tool.py are stored with CRLF) and the Open WebUI
editor gives LF. SOURCES.txt says CRLF converted to LF for a converted file.
-
Read the scenario's detail line and
server.log/driver.txtin the output dir. -
Rerun only the interesting part and keep the container:
tests/e2e/run.sh --keep --name owui-dbg --only 'azure.rag' azureThe script prints the URL (
http://localhost:<port>, loginadmin@example.com/Passw0rd!e2e). The mocks keep running, so you can chat with the functions in the browser. -
Iterate without waiting for the start-up again:
tests/e2e/run.sh --reuse --name owui-dbg azure(stops a driver left over from an interrupted run, copies the current files, restarts the mocks, reruns;--onlyworks here too). -
Look at what reached a mock (Git Bash: prefix with
MSYS_NO_PATHCONV=1):docker exec owui-dbg curl -s http://127.0.0.1:9102/__requests # 9101 gemini, 9102 azure, 9103 n8n, 9104 infomaniak, 9106 Azure AI Search, 9111 tools docker exec owui-dbg tail -n 100 /tmp/e2e/server.log
-
Clean up:
docker rm -f owui-dbg && docker volume rm owui-dbg-data.
Ctrl-C (SIGINT) or SIGTERM during a run is handled at once, also while the driver runs:
run.sh asks the driver in the container to stop (it records <suite>.interrupted
and writes the partial results.json / summary.md), copies the output directory,
removes container and volume (unless --keep / --reuse) and exits with 130 / 143.
That takes a few seconds; a second Ctrl-C does not cut the cleanup short. The browser
session's socket.io client is created with handle_sigint=False: otherwise engine.io
replaces the driver's SIGINT handler once a browser-path scenario ran (its handler cancels
every task and stops the event loop), and the stop was not recorded as interrupted.
If run.sh itself is killed (SIGKILL, a closed terminal on some systems, a CI job
cancelled without grace), nothing can clean up:
-
the container and its volume stay; remove them with
docker rm -f <name> && docker volume rm <name>-data. A later run with the same--namestops at the existing container, and at a leftover volume unless you pass--force; -
the driver keeps running inside the container (an orphan) and would share the mocks and the server log with the next run there.
--reusestops such orphans before it starts (stopped 1 driver(s) left over from an earlier run). To look for them by hand (the image has nops):MSYS_NO_PATHCONV=1 docker exec owui-dbg bash -c \ 'for p in /proc/[0-9]*; do tr "\0" " " <$p/cmdline 2>/dev/null; echo; done | grep "[e]2e.py"'
E2E_TIMEOUT (default 1800 s) bounds a run that hangs: the suite that is still running
becomes a <suite>.timeout FAIL and the driver writes its results. A driver that does
not react is stopped by timeout in the container 60 s later (exit code 2).
Layout of tests/e2e/:
run.sh host entry point (Docker + bash only)
check_owui_api.sh static Open WebUI API compatibility check
e2e.py in-container driver entry point
harness/ driver library: owui.py (REST client, API-path chat, valves, models),
browser.py (socket.io + saved chats), logs.py (server log),
mocks.py, results.py, known.py + known_<area>.py (known bugs),
suite.py, config.py
suites/ one module per suite: GROUPS + async def run(t: Suite); _*.py are
helper modules of a suite, not suites (_azure_rag.py: the azure rag
group; _gemini_tools.py: gemini.tools / toolsapi;
_gemini_owuitools.py: gemini.owuitools;
_gemini_shared.py: gemini.shared;
_gemini_owui012.py: gemini.owui012)
mocks/ aiohttp provider mocks + serve_all.py (127.0.0.1:9101-9104 and :9106 in
the container), tool servers mock_tools.py (OpenAPI and Jupyter :9111,
MCP :9112, no E2E_MOCK_FAULT); mock_la.py (Log Analytics) is started
by the filters suite (:443, :9105, :9107)
probe/probe_pipe.py test-only pipe reporting what Open WebUI passes to a pipe
probe/workspace_tool.py test-only workspace Python tool (gemini.tools)
A scenario is a few lines in a suite module:
async def api(t: Suite, mock) -> None:
await mock.reset()
r = await t.owui.chat("gemini.gemini-2.5-flash", "Hello", stream=True)
req = await mock.last() # what the pipe sent upstream
t.check(
"api.stream", # id -> "gemini.api.stream"
"API stream: answer streamed",
r.status == 200 and "Hello from mock" in r.content,
r.brief(), # printed for FAIL/KNOWN
known=known.MY_BUG, # KnownIssue from harness/known_*.py,
# only while a known bug breaks it
)- Browser path:
async with t.browser() as b: c = await b.chat(model, "text", stream=True)then assert onc.content,c.usage,c.sources,c.files,c.status_history,c.events,c.title; for tool turnsc.function_calls,c.function_outputs,c.reasoning_items,c.output_text(b.chat(..., tool_ids=[...], tool_servers=[...], until=...), see Native tool calling). A follow-up turn in the same chat passeschat_id=c.chat_id, parent_id=c.message_id: Open WebUI then rebuilds the history from the saved chat, like for the web UI.t.owui.upload_file(name, data, content_type)uploads a file like the web UI andb.chat(..., user_files=[item])attaches it to the user message (the file item the web UI saves, with the file id asurl; see_upload_pnginsuites/gemini.py). - Valves: use
t.owui.update_valves(fid, NAME=value)(merges, see gotchas). - Server log:
mark = t.mark()before,t.log.errors(mark)after;t.expect_errors(mark, signature)for an error you provoke on purpose (only blocks matching the signature, e.g.("function_azure:pipe", "request: 400"), are ignored; anything else in the window still failsserver-log). Open WebUI 0.12 logs a function's module asfunction_<id>_<32 hex digits>(a new name for every load);harness/logs.pyturns it back intofunction_<id>(andtool_<id>_<hex>intotool_<id>) in everything it reads, so write signatures withfunction_<id>:for every version.server.login the output directory keeps the names as logged.t.fail_on_warnings(signature)makes matching WARNING blocks failserver-log(t.expect_warnings(mark, signature)for one you provoke on purpose), andt.assert_no_secrets(value)checks that a secret never shows up in the log. Passsince=markto a check taggedknown=when the bug shows in the server log rather than in the detail text (e.g. a failing background task). - Groups: wrap related scenarios in
if t.selected("group"):so--onlycan select them, and list the group in the module'sGROUPS(an unlisted group raises). - Mock behaviour: mocks pick behaviour from the request (model name, webhook path or a
trigger word in the last user message, e.g.
force-400, or the Gemini mock'sMOCKTOOLS:directive). Add a branch intests/e2e/mocks/mock_<provider>.py; requests are recorded automatically. - New known bug: add a
KnownIssueto the area'sharness/known_<area>.py(a new area module needs its own import at the bottom ofknown.py) and pass it asknown=. Give itevidence(regexes that match the failing check's detail and nothing else: include proof that the request itself worked, e.g.HTTP 200, or in the tool groupsmock_ok=Trueplus the mock's answer, so an unrelated failure stays FAIL),log_patternswhen it logs errors or failing warnings (a string or a tuple of strings that must all occur in one block; include the function, e.g.function_gemini:pipe),fileandfixed_in(the function file and the version that fixes it,""while no fix exists), the fixing branch (or"") and an issueref. The CI meta-test fails when the evidence also matches a failing upstream (see CI). Only a bug that can show nothing but the request sent upstream goes intoREQUEST_SIDE_KNOWNin.github/workflows/e2e.yml(empty today). - New suite: add
tests/e2e/suites/<name>.py(withGROUPSandasync def run(t));run.shand the driver discover every module there (names starting with_are skipped),allruns new suites after the existing ones in alphabetical order. For a new function file add it toFUNCTION_FILESinrun.sh. Mention the suite in this guide, inCLAUDE.md("What to run") and, if it should appear there, in thesuitesinput description of.github/workflows/e2e.yml.
Python under tests/e2e/ follows the repo's Ruff settings:
uvx ruff@0.11.10 format tests/e2e && uvx ruff@0.11.10 check tests/e2e (or pixi run lint).
tests/e2e/check_owui_api.sh # against v0.12.0
tests/e2e/check_owui_api.sh latest # against the latest Open WebUI release
tests/e2e/check_owui_api.sh v0.11.4 # any tag or branchIt reads every from open_webui... import ... and import open_webui... in
pipelines/ and filters/, fetches the corresponding Open WebUI backend modules at that
tag (gh api when authenticated, otherwise curl from raw.githubusercontent.com) and
checks that each imported module and symbol, each method called on it
(Users.get_user_by_id, ...) and each injected __param__ still exists. An imported
name may also be a submodule (from open_webui.utils.access_control import files); then
the functions called on it (or on its as alias) are checked like methods. Definitions
marked legacy/deprecated are reported as WARN (for example SRC_LOG_LEVELS is an empty
legacy dict since 0.10, so per-module log levels have no effect), and so is a missing
method that the function looks up with getattr(obj, "name") first and works without
(Chats.get_accessible_chat_by_id, new in 0.12, is missing on 0.11.x). It also prints the
frontmatter of every function, the event types Open WebUI persists and the versions of
shared dependencies. A fetch that fails for another reason than "not found" is tried
3 times; if it still fails the result is incomplete (exit 2) rather than a missing
API. Run it whenever Open WebUI publishes a release, then run the Docker suites with the
new image.
It checks names only, not keys of __metadata__ or the body, signatures, fields of the
returned models or behaviour. Open WebUI 0.12.0 passed it, yet renamed
__metadata__["user_prompt"] to base_user_prompt (the Azure pipeline searched with the
RAG template until 3.0.1), stopped admins from continuing other users' chats and leaves
every empty answer out of the history it rebuilds (google_gemini.py 1.19.1). The Docker
suites found the first two; the third came from reading Open WebUI's diff and is now
gemini.imgedit.empty-answer.
The suites run against mocks, so they cannot show what the real Gemini API accepts.
tests/e2e/smoke/gemini-tools.sh checks the server-side assumptions of the Gemini
pipe's native tool calling (risks R1-R15 of the 1.18.0 design) against
generativelanguage.googleapis.com. It is opt-in and never runs in CI: it needs a key
and makes billed requests (about 40 small ones on flash models, with the defaults).
export GOOGLE_API_KEY=... # from the environment only, never as an argument
tests/e2e/smoke/gemini-tools.sh # v0.12.0-slim, gemini-3-flash-preview + gemini-2.5-flash
tests/e2e/smoke/gemini-tools.sh --image v0.11.3-slim --only R3,R14 # some risks, another image
tests/e2e/smoke/gemini-tools.sh --dry-run-mock # no key: the same run against the e2e Gemini mockHow it works:
- A fresh Open WebUI container is started with
run.sh's helpers (run.shonly defines its functions when it is sourced).google_gemini.pyandgoogle_search_tool.pycome from the worktree (--src DIRfor another checkout). The key goes into the encryptedGOOGLE_API_KEYvalve. The tools are deterministic: Open WebUI's built-in tools, the e2e workspace tool (probe/workspace_tool.py), the OpenAPI and MCP tool mocks and client tools on the API path. - The pipe's
BASE_URLpoints at a recording pass-through proxy (smoke/proxy.py, 127.0.0.1:9120 in the container). It forwards every request unchanged to the real API and streams the answer back. For each request it records the declared functions, tool kinds,toolConfig, the part kinds of every content, whether replayed function calls carry a signature (yes/skip/none), the HTTP status, the error message, finish reasons, the returned function calls with their signature flags, server-side tool parts, text lengths and usage. It never records headers, the query string, signature values or request texts.--dry-run-mockforwards to the e2e Gemini mock instead and adds theMOCKTOOLS:directive to the prompts; the key is then a dummy. - Each scenario tells the model exactly which tool to call. A scenario whose model did
not do what was asked (no call, one call instead of two, no search in the tool round
of R3) or that got HTTP 429 / 5xx is repeated, at most
--retriestimes (default 2). Every request carriesmax_tokens(default 2048; on the browser path Open WebUI keeps it for the continuations). The valves areTHINKING_LEVEL=low(Gemini 3) andTHINKING_BUDGET=512(Gemini 2.5, whose thinking counts againstmaxOutputTokens). The runner prints an estimate at the start (about 39 generate requests with the defaults, about 117 if every scenario used all its retries) and the hard cap, and the actual count and token usage at the end. - Request budget:
--max-requests N(default twice the printed maximum, 234 with the defaults) caps the generate requests the proxy forwards. Beyond it the proxy answers HTTP 429 itself, without forwarding; the scenario is not retried, the remaining risks are SKIP, andsummary.md/smoke.jsonreportbudget_hitplus aBUDGETFAIL row (exit 1). TheLOGrow fails as well, because the pipe logs the proxy's 429 as an ERROR (a bad key likewise fails the server-log check with the model listing's 400s). The model listing is not capped (not billed); R9's Vertex requests do not pass through the proxy and are not capped either. - Results:
PASS,FAILorSKIPper risk, with the evidence lines (oneupstream #nline per request), plusLOG(the server log scan of the suites, which also fails on the pipe's WARNINGs for lost tool data) andSECRETS. Exit code 0 = no FAIL, 1 = a FAIL, 2 = setup error (install, the model list, or the preflight: one plain request per model, which catches an unavailable model). A model that the API does not list is added withMODEL_ADDITIONAL. A bad key already fails the model listing, so neither model shows up: the setup error, in the output and insummary.md, then names the failing upstream requests, e.g.gemini: models [...] missing from Open WebUI; failing upstream requests: GET /v1alpha/models http=400 x4: API key not valid. Please pass a valid API key. - The key is never printed. The secrets are the key and, with
--vertex-credentials, the secret values of that file (refresh_token,client_secret,private_key_idand each base64 line ofprivate_key; values under 16 characters are left out). The runner redacts them from its stdout, stderr and output files, andSECRETSfails when one shows up in the server log, the mocks' output or the runner's output. After the run the script scans every output file for the same values, redacts any hit in place and exits 1. - Container and volume are removed on exit, also after Ctrl-C (the runner writes partial
results).
--keepkeeps them: the volume holds the key (encrypted), and with--vertex-credentialsthe container holds the credentials file in plain text; the script says so and prints the cleanup command. Images are never removed. Output:tests/e2e/out/<time>-<name>/withdriver.txt,summary.md,smoke.json,upstream.jsonl(the proxy record),server.log,mocks.txtand the stagedfunctions/.
| Risk | Scenario | PASS when |
|---|---|---|
| R1 | Browser turn on both models with the workspace tool and the OpenAPI and MCP servers (27 built-ins + 8 tools declared). API request on both models with client tools in the schema styles of pydantic (title, default, anyOf + null), OpenAPI (format, integer enum) and MCP ($defs, $ref, additionalProperties: false), plus oneOf / const, type arrays and a tool without parameters |
Every request gets HTTP 200. Built-ins that another Open WebUI version lacks are listed |
| R2 | Browser, stream=false, Gemini 3: get_current_timestamp |
The tool is declared without a schema and called, the continuation gets 200, the answer is saved |
| R3 | Gemini 3 with google_search_tool and web search: Search and get_current_timestamp in one round, continued; the next turn with web search off; API tool_choice required and none with web search |
googleSearch + functions + includeServerSideToolInvocations accepted, the call made, the server-side parts sent back within the turn and not in the next one, mode ANY gives a call and NONE none. If Gemini still did not search in the tool round after the retries, the result is PASS with a note that the replay was not exercised |
| R4 | API continuation without reasoning_details on Gemini 3 (placeholder signature), Gemini 2.5 (no signature) and --model-latest (default gemini-flash-latest) when the API lists it |
200 and an answer |
| R5 | Two calls in one round | At least 2 calls in one round, replayed as one model content and one user content with as many responses, 200 |
| R6 | make_image (a tool result with an image) |
The continuation has the function responses followed by Open WebUI's image message, 200 |
| R7 | A tool turn, then a turn with function_calling=legacy |
The second request declares no functions and gets 200 (the evidence says whether Open WebUI sent the old call) |
| R8 | Gemini 2.5 with thinking: a tool turn, a second tool turn, a plain turn | Every request 200 (the evidence counts the signatures on thought parts that are not sent back) |
| R9 | Vertex AI: a tool turn, a 64-character tool name, web search with tools, a Vertex AI Search data store (--vertex-rag-store) |
Each turn done without an error. SKIP without credentials |
| R10 | Gemini 2.5 with web search | googleSearch without functionDeclarations, 200 |
| R11 | Note only: the installed google-genai version |
SKIP with the note; FAIL when it is older than 1.68.0 after the function was saved |
| R12 | Streamed tool turn on Gemini 3 | No partial function call parts, the final text is not empty |
| R13 | --turns API tool turns (default 4), plus every answer of the run |
Counts the finish reasons, MALFORMED_FUNCTION_CALL / UNEXPECTED_TOOL_CALL and default_api. names. FAIL only when such an answer did not become the pipe's error text |
| R14 | Tool approval (ask) with two calls, approve | The turn pauses, the continuation gets 200, also when Open WebUI 0.11.4 replays fewer calls than Gemini made |
| R15 | INCLUDE_THOUGHTS=false tool turn on Gemini 3 |
The call carries a thought signature and the saved reasoning item has it |
Read the evidence, not only the status: it shows which risks were exercised (for example
server_parts_in_round=0 in R3, history_fc=False in R7) and what to change if one fails.
For example, a 400 in R7 means that function-call history has to be rendered as text
when no functions are declared.
Vertex AI (R9). --vertex-credentials FILE (a service account key or the JSON of
gcloud auth application-default login; default $GOOGLE_APPLICATION_CREDENTIALS) and
--vertex-project ID (default $GOOGLE_CLOUD_PROJECT, else the file's project_id),
plus --vertex-location (default global). The file is streamed into the container as
its Application Default Credentials file (never into the output directory; it stays
there in plain text until the container is removed), and a
second copy of the pipe, gemini_vertex, runs with USE_VERTEX_AI=true. Its requests do
not go through the proxy, so the evidence is what Open WebUI saved plus the server log.
This path has not run yet: no Vertex project was available. A run with a fake service
account file and a fake key (it stops at the model list) checked the secret scan: the
runner and the script both took 10 values from the file and found none in the output.
Dry run. --dry-run-mock checks the runner itself without a key: every scenario runs
end to end against the e2e Gemini mock, which answers the MOCKTOOLS: directive instead
of following the prompt. Observed 2026-10-09 on v0.11.4-slim (and 2026-10-10 with
google_gemini.py 1.19.0, 2026-10-11 with 1.20.0): 15 PASS (R1-R8, R10, R12-R15,
LOG, SECRETS), 0 FAIL, 2 SKIP (R9 without Vertex credentials, R11 note), 38
generate requests (the mock received the same 38), 76-160 s including the container
start. The mock always follows the directive, so no retry happens. Its answers prove
only the mechanics: real results can differ. With --max-requests 5 the proxy forwarded
the two preflight requests and three of R1, answered R1's fourth with HTTP 429 (the mock
received 5), R1 was FAIL, the other risks SKIP, BUDGET and LOG FAIL (the pipe logs
the 429 as an ERROR), exit 1. A fake key gave
exit 2 with the API key not valid setup error above, and the key was in no output.
.github/workflows/e2e.yml runs on pull requests and on pushes to main / dev that
touch pipelines/**, filters/**, tests/** or the workflow, every Monday, and on
demand (Actions → E2E → Run workflow, with an image tag and a suites input). Jobs:
| Job | What it does |
|---|---|
e2e |
run.sh with all suites in strict known mode (E2E_STRICT_KNOWN=1) against the default image, which is read from the DEFAULT_IMAGE= line of run.sh (the only place it is defined). The weekly run adds ghcr.io/open-webui/open-webui:latest-slim; a manual run uses the image tag input. Output directory as artifact, summary.md as job summary |
meta |
Meta-test: main's function files (--ref origin/main) with every provider mock answering HTTP 500 (E2E_MOCK_FAULT=500), suites gemini azure n8n infomaniak. Nearly everything fails, and it must give no KNOWN: a KNOWN means the evidence of that known bug also matches an unrelated failure and would hide it. REQUEST_SIDE_KNOWN in the workflow may list bugs that can only show in the request the pipe sends upstream (they reproduce whatever the mock answers); it is empty, because the evidence of every registered bug also needs proof that the upstream answered (e.g. HTTP 200 and the mock's answer). Observed 2026-10-09 (main 53b8495): 38 PASS / 197 FAIL / 0 KNOWN; on 2026-10-10 with main 3ff6cf9 (Azure pipeline 2.8.1) and the harness of the Azure pipeline 3.0.0 (azure rag group): 35 PASS / 327 FAIL / 0 KNOWN (466 s), and with main 4b80e24 (Azure pipeline 3.0.0, google_gemini.py 1.17.0) and the suites with the gemini.tools / toolsapi / imgedit groups: 57 PASS / 370 FAIL / 0 KNOWN (557 s); with main 2fc7bcc (google_gemini.py 1.18.0, infomaniak.py 2.2.2, no known bug registered) and the suites with the owuitools group and the checks of 1.19.0: 57 PASS / 386 FAIL / 0 KNOWN (545 s); on 2026-10-11 with main 31a4e7d (google_gemini.py 1.19.0, Azure pipeline 3.0.0) and the harness of the Open WebUI 0.12 changes (2 more checks): 57 PASS / 388 FAIL / 0 KNOWN (518 s); with main 6b99dcf (google_gemini.py 1.19.1) and the harness with the gemini.shared group (2 more checks on the default image): 58 PASS / 389 FAIL / 0 KNOWN (536 s); with main 6b99dcf (google_gemini.py 1.19.1) and the harness with the gemini.owui012 group (12 more checks): 58 PASS / 399 FAIL / 0 KNOWN (527 s; owui012.controls passes, it needs no provider answer); with main 6b99dcf and the harness of google_gemini.py 1.20.0 (thinking.level-models, thinking.params-robust and the shared-chat checks of the security review): 59 PASS / 402 FAIL / 0 KNOWN (595 s). The filters suite is left out because it uses no provider mock (its known bugs reproduce for real) |
api |
check_owui_api.sh latest |
E2E_TIMEOUT and the steps' timeout-minutes bound every job, so a hanging scenario
ends as a <suite>.timeout FAIL with results instead of a cancelled job. The jobs are
informational: they are not required checks and do not block merging.
valves/updatereplaces all valves.POST /api/v1/functions/id/{id}/valves/updatestores exactly what you send; every valve you leave out falls back to its default (secrets included). Always send the full set (update_valvesmerges for you).- Refresh the model list after model changes.
/api/modelsis cached; callGET /api/models?refresh=trueafter creating workspace models (filterIds) or changing valves that influencepipes(). - The data volume beats environment variables. Settings saved in the database
(
ENABLE_WEB_SEARCH, task settings, ...) override-evariables on later starts. The harness uses a fresh volume per run; with--reusethe old settings stay. - Events and saved content need the browser path. On the API path nothing is saved and status/source/files events go nowhere; usage/sources/files bugs only show up in browser-path scenarios.
- Background tasks have no event emitter.
/api/v1/tasks/*/completionscall the pipe with__task__set and__event_emitter__=None; pipes and filters must handleNone. - A saved status can get lost in a new chat with a title task (Open WebUI race).
For a new saved chat, Open WebUI 0.11.4 runs the title generation as its own task,
in parallel with the answer, and
Chats.update_chat_title_by_idwrites back the whole chat JSON it read before. A status event the pipe saves in between (Chats.add_message_status_to_chat_by_id_and_message_id) is then overwritten. Both writes started together lose the status in about half of the attempts (reproduced in the container, 20 of 40). The checkno-session.sources(then in the azureoydgroup, nowazure.rag.no-session.sources) failed once that way (statuses=[('Request completed', True, False)]: the first status missing) and passed in the next four runs. If only a status is missing and the server log shows a normal request, it is this race; rerun the group to confirm. - First Gemini install is slow. Creating the Gemini function pip-installs
google-genai(~30-60 s); the create request waits for it. - Git Bash on Windows: MSYS rewrites arguments that look like paths (
/e2e→C:/Program Files/Git/e2e).run.shruns everydockercommand withMSYS_NO_PATHCONV=1and streams host files withtarinstead of passing host paths todocker.exe. A globally exportedMSYS_NO_PATHCONV/MSYS2_ARG_CONV_EXCLwould breakgit -C /c/...(--ref), sorun.shunsets both for itself. Prefix your own commands per call instead of exporting it:MSYS_NO_PATHCONV=1 docker exec ....*.shfiles are checked out with LF endings (.gitattributes), otherwise bash fails with$'\r'. - Never remove images to "clean up" – only the container and its volume are throw-away; the image is shared with other runs.
- Slow or unhealthy start: the first start of a fresh volume runs all database
migrations (~60-75 s on Docker Desktop, several minutes on a busy machine). The wait
is limited to 600 s (
E2E_HEALTH_TIMEOUT=900to raise it); if the container exits, seeserver.login the output dir. - Older images and the embedding model. Without further settings
v0.11.3-slimdownloadssentence-transformers/all-MiniLM-L6-v2from huggingface.co before/healthanswers (1522 s once on a busy network, more than the 600 s limit). The suites do not use RAG, sorun.shstarts the container withRAG_EMBEDDING_ENGINE=openai: no download, andv0.11.3-slimbecame healthy after 158 s on a busy machine, with the same results asv0.11.4-slim. A container you start by hand needs the same variable. - tiktoken encodings:
time_token_trackerloads its tiktoken encoding on first use (a download fromopenaipublic.blob.core.windows.net). Thefilterssuite loadscl100k_baseando200k_basein the driver first, which fills the container'sTIKTOKEN_CACHE_DIR, so the tracker reads them from the cache and its first counts are exact. Without network access the suite fails. WEBUI_SECRET_KEYis set by the harness; without it Open WebUI generates one andEncryptedStrvalves are still encrypted, but a manual container without a stable key cannot decrypt valves after a restart.