mirror of
https://github.com/open-webui/open-webui.git
synced 2026-07-23 09:10:55 -05:00
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:
@@ -1168,6 +1168,7 @@ async def chat_completion(
|
||||
|
||||
metadata = {
|
||||
'user_id': user.id,
|
||||
'user_agent': request.headers.get('user-agent', '') or '',
|
||||
'chat_id': form_data.pop('chat_id', None) or '',
|
||||
'user_message': user_message,
|
||||
'user_message_id': user_message.get('id') if user_message else None,
|
||||
|
||||
@@ -38,7 +38,7 @@ from open_webui.models.models import Models
|
||||
from open_webui.models.users import UserModel
|
||||
from open_webui.utils.access_control import check_model_access
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.headers import include_user_info_headers
|
||||
from open_webui.utils.headers import get_custom_headers, include_user_info_headers
|
||||
from open_webui.utils.misc import calculate_sha256
|
||||
from open_webui.utils.payload import (
|
||||
apply_model_params_to_body_ollama,
|
||||
@@ -99,6 +99,8 @@ async def send_request(
|
||||
stream: bool = False,
|
||||
content_type: str | None = None,
|
||||
metadata: dict | None = None,
|
||||
api_config: dict | None = None,
|
||||
request: Request | None = None,
|
||||
):
|
||||
r = None
|
||||
streaming = False
|
||||
@@ -115,6 +117,10 @@ async def send_request(
|
||||
if metadata and metadata.get('chat_id'):
|
||||
headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get('chat_id')
|
||||
|
||||
# Custom per-connection headers last so admin-set headers take precedence.
|
||||
if api_config and api_config.get('headers'):
|
||||
headers.update(get_custom_headers(api_config['headers'], user, metadata, request=request))
|
||||
|
||||
r = await session.request(
|
||||
method,
|
||||
url,
|
||||
@@ -1100,6 +1106,8 @@ async def generate_chat_completion(
|
||||
stream=form_data.stream,
|
||||
content_type='application/x-ndjson',
|
||||
metadata=metadata,
|
||||
api_config=api_config,
|
||||
request=request,
|
||||
)
|
||||
|
||||
|
||||
@@ -1185,6 +1193,8 @@ async def generate_openai_completion(
|
||||
user=user,
|
||||
stream=payload.get('stream', False),
|
||||
metadata=metadata,
|
||||
api_config=api_config,
|
||||
request=request,
|
||||
)
|
||||
|
||||
|
||||
@@ -1242,6 +1252,8 @@ async def generate_openai_chat_completion(
|
||||
user=user,
|
||||
stream=payload.get('stream', False),
|
||||
metadata=metadata,
|
||||
api_config=api_config,
|
||||
request=request,
|
||||
)
|
||||
|
||||
|
||||
@@ -1294,6 +1306,8 @@ async def generate_anthropic_messages(
|
||||
user=user,
|
||||
stream=payload.get('stream', False),
|
||||
content_type='text/event-stream' if payload.get('stream', False) else None,
|
||||
api_config=api_config,
|
||||
request=request,
|
||||
)
|
||||
|
||||
|
||||
@@ -1352,6 +1366,8 @@ async def generate_responses(
|
||||
user=user,
|
||||
stream=payload.get('stream', False),
|
||||
content_type='text/event-stream' if payload.get('stream', False) else None,
|
||||
api_config=api_config,
|
||||
request=request,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ async def get_headers_and_cookies(
|
||||
headers['Authorization'] = f'Bearer {token}'
|
||||
|
||||
if config.get('headers') and isinstance(config.get('headers'), dict):
|
||||
custom_headers = get_custom_headers(config.get('headers'), user, metadata)
|
||||
custom_headers = get_custom_headers(config.get('headers'), user, metadata, request=request)
|
||||
headers.update(custom_headers)
|
||||
|
||||
return headers, cookies
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -436,7 +436,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !ollama && !direct}
|
||||
{#if !direct}
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
|
||||
Reference in New Issue
Block a user