This commit is contained in:
Timothy Jaeryang Baek 2026-09-14 15:46:31 -04:00
parent 113c56fc8c
commit 924a4a10fb

View file

@ -128,6 +128,11 @@ from open_webui.utils.payload import apply_params_to_form_data, apply_system_pro
from open_webui.utils.plugin import load_function_module_by_id
from open_webui.utils.response import merge_usage, normalize_usage
from open_webui.utils.sanitize import sanitize_code
from open_webui.utils.skills import (
apply_skills_create_prompt,
extract_skill_ids_from_messages,
has_prior_real_chat_content,
strip_skill_mentions,
)
from open_webui.utils.task import (
get_task_model_id,
@ -313,6 +318,19 @@ def tool_result_content(tool_result: Any) -> str:
def append_to_text_field(item: dict, key: str, value: str) -> None:
# Default: keep the existing field intact until concatenation succeeds.
# Non-string values and str subclasses also use this path to preserve errors.
if not ENABLE_CHAT_RESPONSE_STREAM_INPLACE_APPEND or type(item[key]) is not str or type(value) is not str:
item[key] += value
return
# Opt-in: dropping the dict's reference lets CPython extend an unshared str
# in place. An allocation failure can leave the field empty.
text = item[key]
item[key] = ''
text += value
item[key] = text
def merge_streamed_reasoning_details(target: list, details) -> None:
items = details if isinstance(details, list) else [details]
for item in items:
@ -331,7 +349,7 @@ def merge_streamed_reasoning_details(target: list, details) -> None:
for key, value in item.items():
if key in ('text', 'summary') and isinstance(value, str) and isinstance(existing.get(key), str):
existing[key] += value
append_to_text_field(existing, key, value)
else:
existing[key] = value
@ -2298,51 +2316,6 @@ def sanitize_tool_pairs(messages: list[dict]) -> list[dict]:
return sanitized
# Match DB skill IDs and terminal skill IDs created by the $ picker.
SKILL_ID_RE = r'(?:[a-z0-9_-]+|terminal:[^|>\s]+)'
SKILL_MENTION_RE = re.compile(rf'<(?:\$({SKILL_ID_RE})(?:\|[^>]*)?|/({SKILL_ID_RE})\|[^>]*)>')
def _get_text_parts(message: dict) -> list[str]:
"""Return all text segments from a message's content."""
content = message.get('content')
if isinstance(content, str):
return [content]
if isinstance(content, list):
return [p.get('text', '') for p in content if isinstance(p, dict) and p.get('type') == 'text']
return []
def extract_skill_ids_from_messages(messages: list[dict]) -> set[str]:
"""Extract skill IDs from <$skillId|label> and </skillId|label> mention tags."""
ids: set[str] = set()
for message in messages:
for text in _get_text_parts(message):
ids.update(m.group(1) or m.group(2) for m in SKILL_MENTION_RE.finditer(text))
return ids
SKILL_MENTION_STRIP_RE = re.compile(rf'<(?:\$({SKILL_ID_RE})(?:\|([^>]*))?|/({SKILL_ID_RE})\|([^>]*))>')
def strip_skill_mentions(messages: list[dict], skill_ids: set[str]) -> None:
"""Replace mentions of resolved skills with their label, preserving all other text."""
def label(match):
if (match.group(1) or match.group(3)) not in skill_ids:
return match.group(0)
return match.group(2) or match.group(4) or ''
for message in messages:
content = message.get('content')
if isinstance(content, str) and SKILL_MENTION_STRIP_RE.search(content):
message['content'] = SKILL_MENTION_STRIP_RE.sub(label, content)
elif isinstance(content, list):
for part in content:
if isinstance(part, dict) and part.get('type') == 'text':
text = part.get('text', '')
if SKILL_MENTION_STRIP_RE.search(text):
part['text'] = SKILL_MENTION_STRIP_RE.sub(label, text)
async def connect_mcp_server(
@ -2758,6 +2731,13 @@ async def process_chat_payload(request, form_data, user, metadata, model):
files = form_data.pop('files', None)
form_data.pop('folder_id', None)
metadata['terminal_id'] = terminal_id
skill_authoring_allowed = bool(terminal_id) and has_prior_real_chat_content(form_data.get('messages', []))
skill_create_denial_reason = 'empty_chat' if terminal_id else 'disabled'
apply_skills_create_prompt(
form_data.get('messages', []),
allowed=skill_authoring_allowed,
denial_reason=skill_create_denial_reason,
)
# If the original caller provided tools, use them as-is (skip resolution).
# Otherwise, save any tools that filter inlets added for merging later.
@ -3650,7 +3630,7 @@ def update_assistant_message_from_stream(assistant_message, raw):
def append_output_text(item, text):
parts = item.setdefault('content', [])
if parts and parts[-1].get('type') == 'output_text':
parts[-1]['text'] += text
append_to_text_field(parts[-1], 'text', text)
else:
parts.append({'type': 'output_text', 'text': text})
@ -3725,7 +3705,10 @@ def update_assistant_message_from_stream(assistant_message, raw):
append_output_text(output[-1], content)
assistant_message['content'] = assistant_message.get('content', '') + content
if 'content' in assistant_message:
append_to_text_field(assistant_message, 'content', content)
else:
assistant_message['content'] = '' + content
async def get_system_oauth_token(request, user):
@ -4945,7 +4928,7 @@ async def streaming_chat_response_handler(response, ctx):
and isinstance(last_delta_data.get('delta'), str)
and isinstance(delta_data.get('delta'), str)
):
last_delta_data['delta'] += delta_data['delta']
append_to_text_field(last_delta_data, 'delta', delta_data['delta'])
delta_count += 1
else:
if last_delta_data and (last_delta_type != delta_type or last_delta_key != delta_key):
@ -5251,8 +5234,10 @@ async def streaming_chat_response_handler(response, ctx):
str,
):
current_response_tool_call['function']['arguments'] = ''
current_response_tool_call['function']['arguments'] += (
delta_arguments
append_to_text_field(
current_response_tool_call['function'],
'arguments',
delta_arguments,
)
# Emit pending tool calls in real-time as Responses events.
@ -5421,7 +5406,7 @@ async def streaming_chat_response_handler(response, ctx):
# Append to reasoning content
parts = reasoning_item.get('content', [])
if parts and parts[-1].get('type') == 'output_text':
parts[-1]['text'] += reasoning_content
append_to_text_field(parts[-1], 'text', reasoning_content)
else:
reasoning_item['content'] = [
{
@ -5528,7 +5513,7 @@ async def streaming_chat_response_handler(response, ctx):
elif last_item_type == 'reasoning':
parts = last_item.get('content', [])
if parts and parts[-1].get('type') == 'output_text':
parts[-1]['text'] += value
append_to_text_field(parts[-1], 'text', value)
else:
last_item['content'] = [
{
@ -5540,7 +5525,7 @@ async def streaming_chat_response_handler(response, ctx):
# solution or other _tag_type message
msg_parts = last_item.get('content', [])
if msg_parts and msg_parts[-1].get('type') == 'output_text':
msg_parts[-1]['text'] += value
append_to_text_field(msg_parts[-1], 'text', value)
else:
last_item['content'] = [
{
@ -5568,7 +5553,7 @@ async def streaming_chat_response_handler(response, ctx):
# Append value to last message item's text
msg_parts = output[-1].get('content', [])
if msg_parts and msg_parts[-1].get('type') == 'output_text':
msg_parts[-1]['text'] += value
append_to_text_field(msg_parts[-1], 'text', value)
else:
output[-1]['content'] = [
{