Adds a {{USER_AGENT}} custom-header placeholder that relays the inbound
client's User-Agent to upstream model backends, so providers see the real
client instead of Open WebUI's internal aiohttp UA. This makes upstream
usage/cost attribution and backend telemetry possible, and is opt-in
per-connection (no global flag): admins add {{USER_AGENT}} to a connection's
custom headers in Admin > Settings > Connections.
The placeholder is sourced from the live inbound request (with a metadata
fallback for detached RAG/tool calls), so it resolves on every prompt-sending
path, not just chat completions:
- OpenAI completions, Responses API, and proxy — all route through
get_headers_and_cookies, which now passes the request into get_custom_headers.
- Anthropic Messages API (/api/v1/messages) — already covered, it delegates
to the chat completion handler.
- Ollama (/api/chat, /v1/completions, /v1/chat/completions, /v1/messages,
/v1/responses) — previously had no custom-header support at all; send_request
now applies per-connection custom headers (with templating) for every
Ollama prompt endpoint.
Custom headers are applied after the built-in user-info headers so explicit
admin-configured headers take precedence. The other existing placeholders
({{CHAT_ID}}, {{USER_ID}}, ...) now also work on the newly covered paths.
Frontend: the connection editor's Headers field is now shown for Ollama
connections too (previously gated to non-Ollama), so the placeholder can be
configured there.
Ref: open-webui/open-webui#26159
routers/openai.py and routers/ollama.py decorated get_all_models with
`@cached(key=lambda _, user: ...)`. In aiocache 0.12, `key=` is a STATIC
cache key: get_cache_key returns `self.key` verbatim (the lambda object)
without calling it, so every caller collides to ONE shared entry within
the TTL. The intended per-user namespacing never happened — one user's
permission-filtered model list could be served to another user (or an
anonymous caller) during the cache window.
The per-call hook is `key_builder=` (called as key_builder(func, *args,
**kwargs)). Switch both sites to key_builder with a (func, request,
user=None) signature so the key is built per call. user=None mirrors
ollama's optional-user signature and stays correct whether user is passed
positionally, as a kwarg, or omitted.
Verified offline: old form returns the same key object for distinct users
(collision); new form yields distinct openai_all_models_<id> /
ollama_all_models_<id> keys, and the unauthenticated base key when no user.
These two were the only @cached(key=lambda ...) sites in the backend.
bypass_system_prompt is an internal flag used by utils/middleware.py and utils/chat.py to skip applying the model system prompt on recursive base-model calls, but it was still declared as a positional argument on the openai/ollama chat-completion route handlers, so FastAPI bound it from the query string. Move it to request.state so external clients cannot set it, matching how bypass_filter is handled.
Drop the argument from both route signatures and read getattr(request.state, 'bypass_system_prompt', False); utils/chat.py sets request.state.bypass_system_prompt alongside bypass_filter and drops the kwarg from the two route-handler calls (the recursive self-calls keep it). Mirrors c0385f60b.
Co-authored-by: anishgirianish <161533316+anishgirianish@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The catch-all /{path:path} proxy forwards any request to the upstream OpenAI-compatible API with the admin's API key and no access control. This is an intentional proxy but should be opt-in.
Adds ENABLE_OPENAI_API_PASSTHROUGH env var (defaults to False). When disabled, the catch-all returns 403. No other routers (Ollama, responses) have catch-all proxies.
The model name from user input was interpolated directly into Azure deployment URL paths without validation. A user could send a model name like '../../management/foo' to traverse the URL path and hit unintended Azure endpoints with the admin's API key.
Adds _sanitize_model_for_url that rejects path separators and traversal sequences, and percent-encodes the name. Applied at convert_to_azure_payload (covers chat completions + proxy) and the responses endpoint's direct URL construction.
The /responses proxy endpoint only required authentication via
get_verified_user but did not check per-model access grants. This
allowed any authenticated user to access any model through this
endpoint, bypassing the access control system.
Extract a shared check_model_access helper into utils/access_control
and replace all inline access control blocks across openai.py and
ollama.py (7 locations) with calls to this helper. This eliminates
code duplication and prevents future policy drift between endpoints.
CWE-862: Missing Authorization
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:H (6.5 Medium)
Azure offers two URL formats: the legacy deployment-based format
(/openai/deployments/{model}/...) and the newer v1 format
(/openai/v1/...) where the model stays in the payload body and no
api-version query parameter is needed.
Previously, the code always ran convert_to_azure_payload which
rewrites the URL to the deployment format, causing 404 errors for
users with v1-style base URLs. Now, when the base URL contains
'/openai/v1', we skip deployment URL construction and route
directly.
Applied consistently across all three Azure routing paths:
generate_chat_completion, /responses proxy, and generic proxy.