|
97 | 97 | convert_logit_bias_input_to_json, |
98 | 98 | get_content_from_message, |
99 | 99 | convert_output_to_messages, |
| 100 | + filter_output_by_content, |
100 | 101 | strip_empty_content_blocks, |
101 | 102 | ) |
102 | 103 | from open_webui.utils.tools import ( |
@@ -475,9 +476,9 @@ def serialize_output(output: list) -> str: |
475 | 476 | ) |
476 | 477 |
|
477 | 478 | if status == 'completed' or duration is not None or not is_last_item: |
478 | | - 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' |
| 479 | + content = f'{content}<details type="reasoning" done="true" id="{item.get("id", "")}" duration="{duration or 0}">\n<summary>Thought for {duration or 0} seconds</summary>\n{display}\n</details>\n' |
479 | 480 | else: |
480 | | - content = f'{content}<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{display}\n</details>\n' |
| 481 | + content = f'{content}<details type="reasoning" done="false" id="{item.get("id", "")}">\n<summary>Thinking…</summary>\n{display}\n</details>\n' |
481 | 482 |
|
482 | 483 | elif item_type == 'open_webui:code_interpreter': |
483 | 484 | content_stripped, original_whitespace = split_content_and_whitespace(content) |
@@ -513,9 +514,9 @@ def serialize_output(output: list) -> str: |
513 | 514 | output_attr = f' output="{html.escape(output_json)}"' |
514 | 515 |
|
515 | 516 | if status == 'completed' or duration is not None or not is_last_item: |
516 | | - content += f'<details type="code_interpreter" done="true" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>\n' |
| 517 | + content += f'<details type="code_interpreter" done="true" id="{item.get("id", "")}" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>\n' |
517 | 518 | else: |
518 | | - content += f'<details type="code_interpreter" done="false"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>\n' |
| 519 | + content += f'<details type="code_interpreter" done="false" id="{item.get("id", "")}"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>\n' |
519 | 520 |
|
520 | 521 | return content.strip() |
521 | 522 |
|
@@ -2040,11 +2041,40 @@ def process_messages_with_output(messages: list[dict]) -> list[dict]: |
2040 | 2041 |
|
2041 | 2042 | for message in messages: |
2042 | 2043 | if message.get('role') == 'assistant' and message.get('output'): |
2043 | | - # Use output items for clean OpenAI-format messages |
2044 | | - output_messages = convert_output_to_messages(message['output'], raw=True) |
| 2044 | + # Drop output items for <details> blocks removed from content |
| 2045 | + output = filter_output_by_content( |
| 2046 | + message['output'], message.get('content', '') |
| 2047 | + ) |
| 2048 | + |
| 2049 | + # Use content for text (respects edits), output for structured items |
| 2050 | + content = re.sub( |
| 2051 | + r'<details\b[^>]*>.*?</details>', '', |
| 2052 | + message.get('content', ''), flags=re.S, |
| 2053 | + ).strip() |
| 2054 | + non_message_items = [ |
| 2055 | + i for i in output if i.get('type') != 'message' |
| 2056 | + ] |
| 2057 | + output_messages = convert_output_to_messages(non_message_items, raw=True) |
| 2058 | + |
2045 | 2059 | if output_messages: |
| 2060 | + # Prepend edited text to first assistant message |
| 2061 | + for om in output_messages: |
| 2062 | + if om.get('role') == 'assistant': |
| 2063 | + om['content'] = ( |
| 2064 | + (content + '\n' + om['content']).strip() |
| 2065 | + if om.get('content') |
| 2066 | + else content |
| 2067 | + ) |
| 2068 | + content = '' |
| 2069 | + break |
| 2070 | + if content: |
| 2071 | + output_messages.insert( |
| 2072 | + 0, {'role': 'assistant', 'content': content} |
| 2073 | + ) |
2046 | 2074 | processed.extend(output_messages) |
2047 | | - continue |
| 2075 | + elif content: |
| 2076 | + processed.append({'role': 'assistant', 'content': content}) |
| 2077 | + continue |
2048 | 2078 |
|
2049 | 2079 | # Strip 'output' field before adding (LLM shouldn't see it) |
2050 | 2080 | clean_message = {k: v for k, v in message.items() if k != 'output'} |
@@ -2635,10 +2665,6 @@ async def tool_function(**kwargs): |
2635 | 2665 | } |
2636 | 2666 | ) |
2637 | 2667 |
|
2638 | | - # Strip empty text content blocks from multimodal messages |
2639 | | - # to prevent errors from providers like Gemini and Claude |
2640 | | - form_data['messages'] = strip_empty_content_blocks(form_data.get('messages', [])) |
2641 | | - |
2642 | 2668 | return form_data, metadata, events |
2643 | 2669 |
|
2644 | 2670 |
|
|
0 commit comments