From fcc130c9bbd703dcf992b1f9b95a75fba99d038e Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Tue, 11 Aug 2026 07:30:46 +0200 Subject: [PATCH] fix: send stream_options.include_usage for backend-initiated chats (#27661) Only the frontend added `stream_options: {include_usage: true}` to the completion payload, gated on the model's `usage` capability. Every backend-initiated run builds its own payload (automations, timers, subagents, channels) and omitted it, so those responses came back without token counts and never rendered the usage block, even with the capability enabled on the model. Set it in `chat_completion` instead, the single handler all of those callers go through, and drop the two duplicate copies (the Anthropic-compat handler and the frontend). Capabilities are read from the resolved model before the custom-model fallback can rebind it, and the flag is applied after the model's `stream_response` override so a non-streaming model is unaffected. Fixes #27653 --- backend/open_webui/main.py | 14 +++++++------- src/lib/components/chat/Chat.svelte | 10 +--------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 725c9a20d7..fb558cfe63 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1098,6 +1098,9 @@ async def chat_completion( model = model_item await _set_direct_model(request, model, user) + # Read before the fallback below can rebind model to a different one. + model_capabilities = ((model.get('info') or {}).get('meta') or {}).get('capabilities') or {} + # Model params: global defaults as base, per-model overrides win default_model_params = copy.deepcopy(await Config.get('models.default_params', {}) or {}) model_info_params = merge_model_params( @@ -1135,6 +1138,10 @@ async def chat_completion( if model_info_params.get('stream_response') is not None: form_data['stream'] = model_info_params.get('stream_response') + # Providers only report token counts when asked, so ask on every caller's behalf. + if form_data.get('stream') and model_capabilities.get('usage'): + form_data['stream_options'] = {**(form_data.get('stream_options') or {}), 'include_usage': True} + if model_info_params.get('stream_delta_chunk_size'): stream_delta_chunk_size = model_info_params.get('stream_delta_chunk_size') @@ -1946,13 +1953,6 @@ async def generate_messages( # Convert Anthropic payload to OpenAI format openai_payload = convert_anthropic_to_openai_payload(form_data, passthrough_params) - model_meta = model_info.meta.model_dump() if model_info and model_info.meta else {} - if (model_meta.get('capabilities') or {}).get('usage') is True: - if openai_payload.get('stream'): - stream_options = openai_payload.get('stream_options') - if not isinstance(stream_options, dict): - stream_options = {} - openai_payload['stream_options'] = {**stream_options, 'include_usage': True} # Route through the existing chat_completion handler response = await chat_completion(request, openai_payload, user) diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index 480b358272..6b918293fa 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -3343,15 +3343,7 @@ } : {}), follow_up_generation: $settings?.autoFollowUps ?? true - }, - - ...(stream && (model.info?.meta?.capabilities?.usage ?? false) - ? { - stream_options: { - include_usage: true - } - } - : {}) + } }, `${WEBUI_BASE_URL}/api` ).catch(async (error) => {