@@ -481,9 +481,9 @@ def serialize_output(output: list) -> str:
481481 )
482482
483483 if status == 'completed' or duration is not None or not is_last_item :
484- 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 '
484+ 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 '
485485 else :
486- content = f'{ content } <details type="reasoning" done="false">\n <summary>Thinking…</summary>\n { display } \n </details>\n '
486+ content = f'{ content } <details type="reasoning" done="false" id=" { item . get ( "id" , "" ) } " >\n <summary>Thinking…</summary>\n { display } \n </details>\n '
487487
488488 elif item_type == 'open_webui:code_interpreter' :
489489 content_stripped , original_whitespace = split_content_and_whitespace (content )
@@ -519,9 +519,9 @@ def serialize_output(output: list) -> str:
519519 output_attr = f' output="{ html .escape (output_json )} "'
520520
521521 if status == 'completed' or duration is not None or not is_last_item :
522- content += f'<details type="code_interpreter" done="true" duration="{ duration or 0 } "{ output_attr } >\n <summary>Analyzed</summary>\n { display } \n </details>\n '
522+ 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 '
523523 else :
524- content += f'<details type="code_interpreter" done="false"{ output_attr } >\n <summary>Analyzing…</summary>\n { display } \n </details>\n '
524+ content += f'<details type="code_interpreter" done="false" id=" { item . get ( "id" , "" ) } " { output_attr } >\n <summary>Analyzing…</summary>\n { display } \n </details>\n '
525525
526526 return content .strip ()
527527
@@ -2066,15 +2066,23 @@ async def convert_url_images_to_base64(form_data):
20662066 return form_data
20672067
20682068
2069- def load_messages_from_db (chat_id : str , message_id : str ) -> Optional [list [dict ]]:
2069+ def load_messages_from_db (
2070+ chat_id : str , message_id : str , require_done : bool = False
2071+ ) -> Optional [list [dict ]]:
20702072 """
20712073 Load the message chain from DB up to message_id,
20722074 keeping only LLM-relevant fields (role, content, output).
2075+
2076+ If require_done is True, returns None when the anchor message is not
2077+ marked done — prevents loading a partial mid-stream output as context.
20732078 """
20742079 messages_map = Chats .get_messages_map_by_chat_id (chat_id )
20752080 if not messages_map :
20762081 return None
20772082
2083+ if require_done and not messages_map .get (message_id , {}).get ('done' ):
2084+ return None
2085+
20782086 db_messages = get_message_list (messages_map , message_id )
20792087 if not db_messages :
20802088 return None
@@ -2088,6 +2096,8 @@ def process_messages_with_output(messages: list[dict]) -> list[dict]:
20882096
20892097 For assistant messages with 'output' field, produces properly formatted
20902098 OpenAI-style messages (tool_calls + tool results). Strips 'output' before LLM.
2099+ The DB is kept consistent at write-time (update_chat_by_id), so output can
2100+ be used directly as the source of truth here.
20912101 """
20922102 processed = []
20932103
@@ -2146,37 +2156,47 @@ async def process_chat_payload(request, form_data, user, metadata, model):
21462156 # Load messages from DB when available — DB preserves structured 'output' items
21472157 # which the frontend strips, causing tool calls to be merged into content.
21482158 chat_id = metadata .get ('chat_id' )
2159+ message_id = metadata .get ('message_id' )
21492160 parent_message_id = metadata .get ('parent_message_id' )
2150-
2151- if chat_id and parent_message_id and not chat_id .startswith ('local:' ):
2152- db_messages = load_messages_from_db (chat_id , parent_message_id )
2153- if db_messages :
2154- system_message = get_system_message (form_data .get ('messages' , []))
2155- form_data ['messages' ] = [system_message , * db_messages ] if system_message else db_messages
2156-
2157- # Inject image files into content as image_url parts (mirrors frontend logic)
2158- for message in form_data ['messages' ]:
2159- image_files = [
2160- f
2161- for f in message .get ('files' , [])
2162- if f .get ('type' ) == 'image' or (f .get ('content_type' ) or '' ).startswith ('image/' )
2163- ]
2164- if message .get ('role' ) == 'user' and image_files :
2165- text_content = message .get ('content' , '' )
2166- if isinstance (text_content , str ):
2167- message ['content' ] = [
2168- {'type' : 'text' , 'text' : text_content },
2169- * [
2170- {
2171- 'type' : 'image_url' ,
2172- 'image_url' : {'url' : f ['url' ]},
2173- }
2174- for f in image_files
2175- if f .get ('url' )
2176- ],
2177- ]
2178- # Strip files field — it's been incorporated into content
2179- message .pop ('files' , None )
2161+ db_messages = None
2162+
2163+ if chat_id and not chat_id .startswith ('local:' ):
2164+ # For "Continue", message_id is the existing assistant message already in the
2165+ # DB — load from it so the reconciled output is included in context.
2166+ # For a new generation, message_id is a fresh UUID not yet saved, so
2167+ # load_messages_from_db returns None and we fall through to parent_message_id.
2168+ if message_id :
2169+ db_messages = load_messages_from_db (chat_id , message_id , require_done = True )
2170+ if db_messages is None and parent_message_id :
2171+ db_messages = load_messages_from_db (chat_id , parent_message_id )
2172+
2173+ if db_messages :
2174+ system_message = get_system_message (form_data .get ('messages' , []))
2175+ form_data ['messages' ] = [system_message , * db_messages ] if system_message else db_messages
2176+
2177+ # Inject image files into content as image_url parts (mirrors frontend logic)
2178+ for message in form_data ['messages' ]:
2179+ image_files = [
2180+ f
2181+ for f in message .get ('files' , [])
2182+ if f .get ('type' ) == 'image' or (f .get ('content_type' ) or '' ).startswith ('image/' )
2183+ ]
2184+ if message .get ('role' ) == 'user' and image_files :
2185+ text_content = message .get ('content' , '' )
2186+ if isinstance (text_content , str ):
2187+ message ['content' ] = [
2188+ {'type' : 'text' , 'text' : text_content },
2189+ * [
2190+ {
2191+ 'type' : 'image_url' ,
2192+ 'image_url' : {'url' : f ['url' ]},
2193+ }
2194+ for f in image_files
2195+ if f .get ('url' )
2196+ ],
2197+ ]
2198+ # Strip files field — it's been incorporated into content
2199+ message .pop ('files' , None )
21802200
21812201 # Process messages with OR-aligned output items for clean LLM messages
21822202 form_data ['messages' ] = process_messages_with_output (form_data .get ('messages' , []))
0 commit comments