This commit is contained in:
Timothy Jaeryang Baek
2026-01-26 18:04:58 +04:00
parent c5c4aef7b1
commit 4a55167759
2 changed files with 167 additions and 86 deletions
+122 -62
View File
@@ -73,6 +73,7 @@ from open_webui.models.models import Models
from open_webui.retrieval.utils import get_sources_from_items from open_webui.retrieval.utils import get_sources_from_items
from open_webui.utils.sanitize import strip_markdown_code_fences
from open_webui.utils.chat import generate_chat_completion from open_webui.utils.chat import generate_chat_completion
from open_webui.utils.task import ( from open_webui.utils.task import (
get_task_model_id, get_task_model_id,
@@ -1841,9 +1842,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# Inject builtin tools for native function calling based on enabled features and model capability # Inject builtin tools for native function calling based on enabled features and model capability
# Check if builtin_tools capability is enabled for this model (defaults to True if not specified) # Check if builtin_tools capability is enabled for this model (defaults to True if not specified)
builtin_tools_enabled = ( builtin_tools_enabled = (
(model.get("info", {}).get("meta", {}).get("capabilities") or {}) model.get("info", {}).get("meta", {}).get("capabilities") or {}
.get("builtin_tools", True) ).get("builtin_tools", True)
)
if ( if (
metadata.get("params", {}).get("function_calling") == "native" metadata.get("params", {}).get("function_calling") == "native"
and builtin_tools_enabled and builtin_tools_enabled
@@ -1887,9 +1887,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# Check if file context extraction is enabled for this model (default True) # Check if file context extraction is enabled for this model (default True)
file_context_enabled = ( file_context_enabled = (
(model.get("info", {}).get("meta", {}).get("capabilities") or {}) model.get("info", {}).get("meta", {}).get("capabilities") or {}
.get("file_context", True) ).get("file_context", True)
)
if file_context_enabled: if file_context_enabled:
try: try:
@@ -2248,7 +2247,11 @@ async def process_chat_response(
"data": { "data": {
"done": True, "done": True,
"content": content, "content": content,
**({"output": response_output} if response_output else {}), **(
{"output": response_output}
if response_output
else {}
),
"title": title, "title": title,
}, },
} }
@@ -2261,7 +2264,11 @@ async def process_chat_response(
{ {
"role": "assistant", "role": "assistant",
"content": content, "content": content,
**({"output": response_output} if response_output else {}), **(
{"output": response_output}
if response_output
else {}
),
}, },
) )
@@ -2577,10 +2584,12 @@ async def process_chat_response(
if content and not content.endswith("\n"): if content and not content.endswith("\n"):
content += "\n" content += "\n"
display = html.escape("\n".join( display = html.escape(
(f"> {line}" if not line.startswith(">") else line) "\n".join(
for line in reasoning_content.splitlines() (f"> {line}" if not line.startswith(">") else line)
)) for line in reasoning_content.splitlines()
)
)
if status == "completed" or duration is not None: if status == "completed" or duration is not None:
content = f'{content}<details type="reasoning" done="true" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>\n' content = f'{content}<details type="reasoning" done="true" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>\n'
@@ -2592,9 +2601,14 @@ async def process_chat_response(
output_val = item.get("output") output_val = item.get("output")
lang = item.get("lang", "") lang = item.get("lang", "")
content_stripped, original_whitespace = split_content_and_whitespace(content) content_stripped, original_whitespace = (
split_content_and_whitespace(content)
)
if is_opening_code_block(content_stripped): if is_opening_code_block(content_stripped):
content = content_stripped.rstrip("`").rstrip() + original_whitespace content = (
content_stripped.rstrip("`").rstrip()
+ original_whitespace
)
else: else:
content = content_stripped + original_whitespace content = content_stripped + original_whitespace
@@ -2609,7 +2623,6 @@ async def process_chat_response(
return content.strip() return content.strip()
def convert_content_blocks_to_messages(content_blocks, raw=False): def convert_content_blocks_to_messages(content_blocks, raw=False):
messages = [] messages = []
@@ -2668,13 +2681,17 @@ async def process_chat_response(
if block_type == "text": if block_type == "text":
text_content = block.get("content", "").strip() text_content = block.get("content", "").strip()
if text_content: if text_content:
output_items.append({ output_items.append(
"type": "message", {
"id": block_id or next_id("msg"), "type": "message",
"status": "completed", "id": block_id or next_id("msg"),
"role": "assistant", "status": "completed",
"content": [{"type": "output_text", "text": text_content}], "role": "assistant",
}) "content": [
{"type": "output_text", "text": text_content}
],
}
)
elif block_type == "tool_calls": elif block_type == "tool_calls":
tool_calls = block.get("content", []) tool_calls = block.get("content", [])
@@ -2684,50 +2701,86 @@ async def process_chat_response(
for tool_call in tool_calls: for tool_call in tool_calls:
call_id = tool_call.get("id", "") call_id = tool_call.get("id", "")
func = tool_call.get("function", {}) func = tool_call.get("function", {})
output_items.append({ output_items.append(
"type": "function_call", {
"id": call_id or next_id("fc"), # Use call_id as item id if available "type": "function_call",
"call_id": call_id, "id": call_id
"name": func.get("name", ""), or next_id(
"arguments": func.get("arguments", "{}"), "fc"
"status": "completed" if results else "in_progress", ), # Use call_id as item id if available
}) "call_id": call_id,
"name": func.get("name", ""),
"arguments": func.get("arguments", "{}"),
"status": "completed" if results else "in_progress",
}
)
# Emit function_call_output items # Emit function_call_output items
for result in results: for result in results:
output_items.append({ output_items.append(
"type": "function_call_output", {
"id": result.get("id") or next_id("fco"), "type": "function_call_output",
"call_id": result.get("tool_call_id", ""), "id": result.get("id") or next_id("fco"),
"output": [{"type": "input_text", "text": result.get("content", "")}], "call_id": result.get("tool_call_id", ""),
"status": "completed", "output": [
**({"files": result.get("files")} if result.get("files") else {}), {
**({"embeds": result.get("embeds")} if result.get("embeds") else {}), "type": "input_text",
}) "text": result.get("content", ""),
}
],
"status": "completed",
**(
{"files": result.get("files")}
if result.get("files")
else {}
),
**(
{"embeds": result.get("embeds")}
if result.get("embeds")
else {}
),
}
)
elif block_type == "reasoning": elif block_type == "reasoning":
reasoning_content = block.get("content", "").strip() reasoning_content = block.get("content", "").strip()
duration = block.get("duration") duration = block.get("duration")
output_items.append({ output_items.append(
"type": "reasoning", {
"id": block_id or next_id("r"), "type": "reasoning",
"status": "completed" if duration is not None else "in_progress", "id": block_id or next_id("r"),
"content": [{"type": "output_text", "text": reasoning_content}] if reasoning_content else None, "status": (
"summary": None, "completed"
}) if duration is not None
else "in_progress"
),
"content": (
[{"type": "output_text", "text": reasoning_content}]
if reasoning_content
else None
),
"summary": None,
}
)
elif block_type == "code_interpreter": elif block_type == "code_interpreter":
code = block.get("content", "") code = block.get("content", "")
output_val = block.get("output") output_val = block.get("output")
attrs = block.get("attributes", {}) attrs = block.get("attributes", {})
output_items.append({ output_items.append(
"type": "open_webui:code_interpreter", {
"id": block_id or next_id("ci"), "type": "open_webui:code_interpreter",
"status": "completed" if output_val is not None else "in_progress", "id": block_id or next_id("ci"),
"lang": attrs.get("lang", ""), "status": (
"code": code, "completed"
"output": output_val, if output_val is not None
}) else "in_progress"
),
"lang": attrs.get("lang", ""),
"code": code,
"output": output_val,
}
)
return output_items return output_items
@@ -2934,13 +2987,15 @@ async def process_chat_response(
output = existing_output output = existing_output
else: else:
# Always create an initial message item (even if content is empty) # Always create an initial message item (even if content is empty)
output = [{ output = [
"type": "message", {
"id": output_id("msg"), "type": "message",
"status": "in_progress", "id": output_id("msg"),
"role": "assistant", "status": "in_progress",
"content": [{"type": "output_text", "text": content}], "role": "assistant",
}] "content": [{"type": "output_text", "text": content}],
}
]
# Keep content_blocks for backward compatibility during transition # Keep content_blocks for backward compatibility during transition
content_blocks = [ content_blocks = [
@@ -3354,7 +3409,9 @@ async def process_chat_response(
if ENABLE_REALTIME_CHAT_SAVE: if ENABLE_REALTIME_CHAT_SAVE:
# Save message in the database # Save message in the database
output = convert_content_blocks_to_output(content_blocks) output = convert_content_blocks_to_output(
content_blocks
)
Chats.upsert_message_to_chat_by_id_and_message_id( Chats.upsert_message_to_chat_by_id_and_message_id(
metadata["chat_id"], metadata["chat_id"],
metadata["message_id"], metadata["message_id"],
@@ -3692,6 +3749,9 @@ async def process_chat_response(
try: try:
if content_blocks[-1]["attributes"].get("type") == "code": if content_blocks[-1]["attributes"].get("type") == "code":
code = content_blocks[-1]["content"] code = content_blocks[-1]["content"]
# Strip markdown fences if model included them
code = strip_markdown_code_fences(code)
if CODE_INTERPRETER_BLOCKED_MODULES: if CODE_INTERPRETER_BLOCKED_MODULES:
blocking_code = textwrap.dedent( blocking_code = textwrap.dedent(
f""" f"""
+21
View File
@@ -0,0 +1,21 @@
import re
def strip_markdown_code_fences(code: str) -> str:
"""
Strip markdown code fences if present.
This is a defensive, non-breaking change — if the code doesn't
contain fences, it passes through unchanged.
Handles patterns like:
- ```python
- ```py
- ```
"""
code = code.strip()
# Remove opening fence (```python, ```py, ``` etc.)
code = re.sub(r"^```\w*\n?", "", code)
# Remove closing fence
code = re.sub(r"\n?```\s*$", "", code)
return code.strip()