mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-05 07:57:06 +00:00
fix: stop replaying encrypted reasoning_details to a different model after a mid-chat model switch
Switching models mid-chat after a reasoning model responded breaks the chat permanently: stored reasoning_details of type reasoning.encrypted (or carrying a signature) are cryptographically bound to the model that produced them, and the new model's provider rejects every subsequent request, including retries and forks. Each assistant message already records which model produced it. On request build, when that model differs from the current target, the bound entries are now dropped from its reasoning_details while plain text and summary entries are kept, so the new model still sees what the previous one was thinking. Same-model requests are untouched, since Anthropic-family models require their signed reasoning blocks back and replaying reasoning saves tokens. The filter sits in process_messages_with_output, the single point every outbound message list passes through, so persisted chats, continue-generation and temporary chats are all covered. Filtering at request time rather than stripping at save keeps the stored data intact: switching back to the original model replays its reasoning again. Messages saved before this change have no model recorded and are left as-is. Fixes #28240
This commit is contained in:
parent
29541cbb52
commit
d8ceb45baa
2 changed files with 40 additions and 15 deletions
|
|
@ -1980,6 +1980,9 @@ async def convert_url_images_to_base64(form_data, user=None):
|
|||
return form_data
|
||||
|
||||
|
||||
PERSISTED_MESSAGE_KEYS = ('id', 'role', 'content', 'output', 'files', 'contextSummary', 'usage', 'model')
|
||||
|
||||
|
||||
async def load_messages_from_db(chat_id: str, message_id: str) -> Optional[list[dict]]:
|
||||
"""
|
||||
Load the message chain from DB up to message_id,
|
||||
|
|
@ -1993,10 +1996,7 @@ async def load_messages_from_db(chat_id: str, message_id: str) -> Optional[list[
|
|||
if not db_messages:
|
||||
return None
|
||||
|
||||
return [
|
||||
{k: v for k, v in msg.items() if k in ('id', 'role', 'content', 'output', 'files', 'contextSummary', 'usage')}
|
||||
for msg in db_messages
|
||||
]
|
||||
return [{k: v for k, v in msg.items() if k in PERSISTED_MESSAGE_KEYS} for msg in db_messages]
|
||||
|
||||
|
||||
def get_reasoning_format(model: dict) -> str | None:
|
||||
|
|
@ -2016,8 +2016,32 @@ def get_reasoning_format(model: dict) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def strip_bound_reasoning_details(output: list) -> list:
|
||||
"""Drop model-bound reasoning_details entries; providers reject them replayed to another model (#28240)."""
|
||||
stripped = []
|
||||
for item in output:
|
||||
details = item.get('reasoning_details')
|
||||
if item.get('type') == 'reasoning' and details:
|
||||
if not isinstance(details, list):
|
||||
details = [details]
|
||||
portable = [
|
||||
detail
|
||||
for detail in details
|
||||
if not (
|
||||
isinstance(detail, dict) and (detail.get('type') == 'reasoning.encrypted' or 'signature' in detail)
|
||||
)
|
||||
]
|
||||
if len(portable) != len(details):
|
||||
item = {k: v for k, v in item.items() if k != 'reasoning_details'}
|
||||
if portable:
|
||||
item['reasoning_details'] = portable
|
||||
stripped.append(item)
|
||||
return stripped
|
||||
|
||||
|
||||
def process_messages_with_output(
|
||||
messages: list[dict],
|
||||
model_id: str,
|
||||
reasoning_format: str | None = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
|
|
@ -2025,14 +2049,20 @@ def process_messages_with_output(
|
|||
|
||||
For assistant messages with 'output' field, produces properly formatted
|
||||
OpenAI-style messages (tool_calls + tool results). Strips 'output' before LLM.
|
||||
Model-bound reasoning_details from a different model are dropped.
|
||||
"""
|
||||
processed = []
|
||||
|
||||
for message in messages:
|
||||
if message.get('role') == 'assistant' and message.get('output'):
|
||||
output = message['output']
|
||||
source_model = message.get('model')
|
||||
if source_model and source_model != model_id and isinstance(output, list):
|
||||
output = strip_bound_reasoning_details(output)
|
||||
|
||||
# Use output items for clean OpenAI-format messages
|
||||
output_messages = convert_output_to_messages(
|
||||
message['output'],
|
||||
output,
|
||||
raw=True,
|
||||
reasoning_format=reasoning_format,
|
||||
flatten_tool_images=True,
|
||||
|
|
@ -2041,8 +2071,8 @@ def process_messages_with_output(
|
|||
processed.extend(output_messages)
|
||||
continue
|
||||
|
||||
# Strip 'output' field before adding (LLM shouldn't see it)
|
||||
clean_message = {k: v for k, v in message.items() if k != 'output'}
|
||||
# Strip 'output' and 'model' before adding (LLM shouldn't see them)
|
||||
clean_message = {k: v for k, v in message.items() if k not in ('output', 'model')}
|
||||
processed.append(clean_message)
|
||||
|
||||
return processed
|
||||
|
|
@ -2249,13 +2279,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
if assistant_message_id:
|
||||
assistant_message = await Chats.get_message_by_id_and_message_id(chat_id, assistant_message_id)
|
||||
if assistant_message and (assistant_message.get('content') or assistant_message.get('output')):
|
||||
db_messages.append(
|
||||
{
|
||||
k: v
|
||||
for k, v in assistant_message.items()
|
||||
if k in ('id', 'role', 'content', 'output', 'files', 'contextSummary', 'usage')
|
||||
}
|
||||
)
|
||||
db_messages.append({k: v for k, v in assistant_message.items() if k in PERSISTED_MESSAGE_KEYS})
|
||||
|
||||
system_message = get_system_message(form_data.get('messages', []))
|
||||
form_data['messages'] = [system_message, *db_messages] if system_message else db_messages
|
||||
|
|
@ -2323,6 +2347,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
# Process messages with OR-aligned output items for clean LLM messages
|
||||
form_data['messages'] = process_messages_with_output(
|
||||
form_data.get('messages', []),
|
||||
model_id=model['id'],
|
||||
reasoning_format=get_reasoning_format(model),
|
||||
)
|
||||
form_data['messages'] = sanitize_tool_pairs(form_data['messages'])
|
||||
|
|
|
|||
|
|
@ -3220,7 +3220,7 @@
|
|||
);
|
||||
|
||||
if (message.output && message.role === 'assistant') {
|
||||
return { role: message.role, output: message.output };
|
||||
return { role: message.role, model: message.model, output: message.output };
|
||||
}
|
||||
|
||||
if (message.role === 'user' && imageFiles.length > 0) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue