Skip to content

Commit 75df948

Browse files
authored
feat: forward client User-Agent to model backends via {{USER_AGENT}} placeholder (open-webui#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#26159
1 parent 260f3c3 commit 75df948

5 files changed

Lines changed: 32 additions & 4 deletions

File tree

backend/open_webui/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,6 +1168,7 @@ async def chat_completion(
11681168

11691169
metadata = {
11701170
'user_id': user.id,
1171+
'user_agent': request.headers.get('user-agent', '') or '',
11711172
'chat_id': form_data.pop('chat_id', None) or '',
11721173
'user_message': user_message,
11731174
'user_message_id': user_message.get('id') if user_message else None,

backend/open_webui/routers/ollama.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from open_webui.models.users import UserModel
3939
from open_webui.utils.access_control import check_model_access
4040
from open_webui.utils.auth import get_admin_user, get_verified_user
41-
from open_webui.utils.headers import include_user_info_headers
41+
from open_webui.utils.headers import get_custom_headers, include_user_info_headers
4242
from open_webui.utils.misc import calculate_sha256
4343
from open_webui.utils.payload import (
4444
apply_model_params_to_body_ollama,
@@ -99,6 +99,8 @@ async def send_request(
9999
stream: bool = False,
100100
content_type: str | None = None,
101101
metadata: dict | None = None,
102+
api_config: dict | None = None,
103+
request: Request | None = None,
102104
):
103105
r = None
104106
streaming = False
@@ -115,6 +117,10 @@ async def send_request(
115117
if metadata and metadata.get('chat_id'):
116118
headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get('chat_id')
117119

120+
# Custom per-connection headers last so admin-set headers take precedence.
121+
if api_config and api_config.get('headers'):
122+
headers.update(get_custom_headers(api_config['headers'], user, metadata, request=request))
123+
118124
r = await session.request(
119125
method,
120126
url,
@@ -1100,6 +1106,8 @@ async def generate_chat_completion(
11001106
stream=form_data.stream,
11011107
content_type='application/x-ndjson',
11021108
metadata=metadata,
1109+
api_config=api_config,
1110+
request=request,
11031111
)
11041112

11051113

@@ -1185,6 +1193,8 @@ async def generate_openai_completion(
11851193
user=user,
11861194
stream=payload.get('stream', False),
11871195
metadata=metadata,
1196+
api_config=api_config,
1197+
request=request,
11881198
)
11891199

11901200

@@ -1242,6 +1252,8 @@ async def generate_openai_chat_completion(
12421252
user=user,
12431253
stream=payload.get('stream', False),
12441254
metadata=metadata,
1255+
api_config=api_config,
1256+
request=request,
12451257
)
12461258

12471259

@@ -1294,6 +1306,8 @@ async def generate_anthropic_messages(
12941306
user=user,
12951307
stream=payload.get('stream', False),
12961308
content_type='text/event-stream' if payload.get('stream', False) else None,
1309+
api_config=api_config,
1310+
request=request,
12971311
)
12981312

12991313

@@ -1352,6 +1366,8 @@ async def generate_responses(
13521366
user=user,
13531367
stream=payload.get('stream', False),
13541368
content_type='text/event-stream' if payload.get('stream', False) else None,
1369+
api_config=api_config,
1370+
request=request,
13551371
)
13561372

13571373

backend/open_webui/routers/openai.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ async def get_headers_and_cookies(
209209
headers['Authorization'] = f'Bearer {token}'
210210

211211
if config.get('headers') and isinstance(config.get('headers'), dict):
212-
custom_headers = get_custom_headers(config.get('headers'), user, metadata)
212+
custom_headers = get_custom_headers(config.get('headers'), user, metadata, request=request)
213213
headers.update(custom_headers)
214214

215215
return headers, cookies

backend/open_webui/utils/headers.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,22 @@ def include_user_info_headers(headers: dict, user: Optional[Any] = None) -> dict
5959
}
6060

6161

62-
def get_custom_headers(custom_headers: dict, user=None, metadata: dict = None) -> dict:
62+
def get_custom_headers(custom_headers: dict, user=None, metadata: dict = None, request=None) -> dict:
6363
if not custom_headers or not isinstance(custom_headers, dict):
6464
return {}
6565

6666
metadata = metadata or {}
6767

68+
# UA from the live request; fall back to metadata for detached RAG/tool calls.
69+
user_agent = ''
70+
if request is not None:
71+
try:
72+
user_agent = request.headers.get('user-agent', '') or ''
73+
except Exception:
74+
user_agent = ''
75+
if not user_agent:
76+
user_agent = metadata.get('user_agent', '') or ''
77+
6878
# Extract user_message info for tree mapping
6979
user_message = metadata.get('user_message') or {}
7080
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) -
8393
'{{USER_NAME}}': (user.name.strip() if user else '') or '',
8494
'{{USER_EMAIL}}': (user.email.strip() if user else '') or '',
8595
'{{USER_ROLE}}': (user.role if user else '') or '',
96+
'{{USER_AGENT}}': user_agent,
8697
}
8798

8899
parsed_headers = {}

src/lib/components/AddConnectionModal.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@
436436
</div>
437437
</div>
438438

439-
{#if !ollama && !direct}
439+
{#if !direct}
440440
<div class="flex gap-2 mt-2">
441441
<div class="flex flex-col w-full">
442442
<label

0 commit comments

Comments
 (0)