Skip to content

Commit c53f53f

Browse files
committed
fix: handle OpenRouter reasoning_details pass-through
1 parent 13447a4 commit c53f53f

2 files changed

Lines changed: 72 additions & 18 deletions

File tree

backend/open_webui/utils/middleware.py

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,23 @@ def split_json_objects(raw: str) -> list[str]:
219219
return expanded
220220

221221

222+
def _merge_reasoning_detail(details: list, new: dict) -> None:
223+
"""Merge a reasoning_details delta chunk into the accumulated list by (type, index)."""
224+
dtype = new.get('type', '')
225+
idx = new.get('index')
226+
if idx is not None:
227+
for entry in details:
228+
if entry.get('type') == dtype and entry.get('index') == idx:
229+
if dtype == 'reasoning.text':
230+
entry['text'] = (entry.get('text') or '') + (new.get('text') or '')
231+
elif dtype == 'reasoning.summary':
232+
entry['summary'] = (entry.get('summary') or '') + (new.get('summary') or '')
233+
elif dtype == 'reasoning.encrypted':
234+
entry['data'] = new.get('data', entry.get('data', ''))
235+
return
236+
details.append(dict(new))
237+
238+
222239
def get_citation_source_from_tool_result(
223240
tool_name: str, tool_params: dict, tool_result: str, tool_id: str = ''
224241
) -> list[dict]:
@@ -3125,8 +3142,14 @@ async def non_streaming_chat_response_handler(response, ctx):
31253142
)
31263143

31273144
choices = response_data.get('choices', [])
3128-
if choices and choices[0].get('message', {}).get('content'):
3129-
content = response_data['choices'][0]['message']['content']
3145+
if choices:
3146+
message_obj = choices[0].get('message', {})
3147+
content = (
3148+
message_obj.get('content')
3149+
or message_obj.get('reasoning')
3150+
or message_obj.get('reasoning_content')
3151+
or ''
3152+
)
31303153

31313154
if content:
31323155
await event_emitter(
@@ -3809,7 +3832,18 @@ async def flush_pending_delta_data(threshold: int = 0):
38093832
or delta.get('reasoning')
38103833
or delta.get('thinking')
38113834
)
3812-
if reasoning_content:
3835+
reasoning_details = delta.get('reasoning_details') or []
3836+
3837+
# Extract display text from reasoning_details if no plain text arrived
3838+
if not reasoning_content and reasoning_details:
3839+
for detail in reasoning_details:
3840+
if detail.get('type') == 'reasoning.text' and detail.get('text'):
3841+
reasoning_content = (reasoning_content or '') + detail['text']
3842+
elif detail.get('type') == 'reasoning.summary' and detail.get('summary'):
3843+
reasoning_content = (reasoning_content or '') + detail['summary']
3844+
# reasoning.encrypted has no displayable text
3845+
3846+
if reasoning_content or reasoning_details:
38133847
if not output or output[-1].get('type') != 'reasoning':
38143848
reasoning_item = {
38153849
'type': 'reasoning',
@@ -3820,23 +3854,31 @@ async def flush_pending_delta_data(threshold: int = 0):
38203854
'attributes': {'type': 'reasoning_content'},
38213855
'content': [],
38223856
'summary': None,
3857+
'reasoning_details': [],
38233858
'started_at': time.time(),
38243859
}
38253860
output.append(reasoning_item)
38263861
else:
38273862
reasoning_item = output[-1]
38283863

3829-
# Append to reasoning content
3830-
parts = reasoning_item.get('content', [])
3831-
if parts and parts[-1].get('type') == 'output_text':
3832-
parts[-1]['text'] += reasoning_content
3833-
else:
3834-
reasoning_item['content'] = [
3835-
{
3836-
'type': 'output_text',
3837-
'text': reasoning_content,
3838-
}
3839-
]
3864+
# Merge reasoning_details chunks into the item for pass-through
3865+
if reasoning_details:
3866+
existing = reasoning_item.setdefault('reasoning_details', [])
3867+
for detail in reasoning_details:
3868+
_merge_reasoning_detail(existing, detail)
3869+
3870+
# Append display text for UI rendering
3871+
if reasoning_content:
3872+
parts = reasoning_item.get('content', [])
3873+
if parts and parts[-1].get('type') == 'output_text':
3874+
parts[-1]['text'] += reasoning_content
3875+
else:
3876+
reasoning_item['content'] = [
3877+
{
3878+
'type': 'output_text',
3879+
'text': reasoning_content,
3880+
}
3881+
]
38403882

38413883
data = {'content': serialize_output(full_output())}
38423884

backend/open_webui/utils/misc.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,19 +148,25 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
148148
messages = []
149149
pending_tool_calls = []
150150
pending_content = []
151+
pending_reasoning_text = ''
152+
pending_reasoning_details = []
151153

152154
def flush_pending():
153-
nonlocal pending_content, pending_tool_calls
154-
if pending_content or pending_tool_calls:
155+
nonlocal pending_content, pending_tool_calls, pending_reasoning_text, pending_reasoning_details
156+
if pending_content or pending_tool_calls or pending_reasoning_text or pending_reasoning_details:
155157
messages.append(
156158
{
157159
'role': 'assistant',
158160
'content': '\n'.join(pending_content) if pending_content else '',
159161
**({'tool_calls': pending_tool_calls} if pending_tool_calls else {}),
162+
**({'reasoning': pending_reasoning_text} if pending_reasoning_text else {}),
163+
**({'reasoning_details': pending_reasoning_details} if pending_reasoning_details else {}),
160164
}
161165
)
162166
pending_content = []
163167
pending_tool_calls = []
168+
pending_reasoning_text = ''
169+
pending_reasoning_details = []
164170

165171
for item in output:
166172
item_type = item.get('type', '')
@@ -232,7 +238,6 @@ def flush_pending():
232238

233239
elif item_type == 'reasoning':
234240
if raw:
235-
# Include reasoning with original tags for LLM re-processing
236241
reasoning_text = ''
237242
source_list = item.get('summary', []) or item.get('content', [])
238243
for part in source_list:
@@ -241,7 +246,14 @@ def flush_pending():
241246
elif 'text' in part:
242247
reasoning_text += part.get('text', '')
243248

244-
if reasoning_text:
249+
reasoning_details_stored = item.get('reasoning_details')
250+
251+
if reasoning_details_stored:
252+
# Store as top-level fields so encrypted blobs survive multi-turn
253+
pending_reasoning_text += reasoning_text
254+
pending_reasoning_details.extend(reasoning_details_stored)
255+
elif reasoning_text:
256+
# Fallback to <think> tags for models without reasoning_details
245257
start_tag = item.get('start_tag', '<think>')
246258
end_tag = item.get('end_tag', '</think>')
247259
pending_content.append(f'{start_tag}{reasoning_text}{end_tag}')

0 commit comments

Comments
 (0)