feat: forward client User-Agent to model backends via {{USER_AGENT}} placeholder (#26333)

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
This commit is contained in:
Classic298 authored and GitHub committed 2026-06-29 05:58:19 -05:00
1 parent 260f3c3a22
commit 75df948f34
5 files changed
+32 -4

No files matched your search

+12 -1
View File
@@ -59,12 +59,22 @@ def include_user_info_headers(headers: dict, user: Optional[Any] = None) -> dict
}
def get_custom_headers(custom_headers: dict, user=None, metadata: dict = None) -> dict:
def get_custom_headers(custom_headers: dict, user=None, metadata: dict = None, request=None) -> dict:
if not custom_headers or not isinstance(custom_headers, dict):
return {}
metadata = metadata or {}
# UA from the live request; fall back to metadata for detached RAG/tool calls.
user_agent = ''
if request is not None:
try:
user_agent = request.headers.get('user-agent', '') or ''
except Exception:
user_agent = ''
if not user_agent:
user_agent = metadata.get('user_agent', '') or ''
# Extract user_message info for tree mapping
user_message = metadata.get('user_message') or {}
user_message_id = metadata.get('user_message_id', '') or (user_message.get('id', '') if user_message else '')
@@ -83,6 +93,7 @@ def get_custom_headers(custom_headers: dict, user=None, metadata: dict = None) -
'{{USER_NAME}}': (user.name.strip() if user else '') or '',
'{{USER_EMAIL}}': (user.email.strip() if user else '') or '',
'{{USER_ROLE}}': (user.role if user else '') or '',
'{{USER_AGENT}}': user_agent,
}
parsed_headers = {}