mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-05 07:57:06 +00:00
fix: hand stream filters a parsed event on direct API calls
A stream() filter that reads its event as a dictionary, which is what every other path gives it and what the documentation describes, raised on the first chunk when the caller used /api/chat/completions without a websocket session. That branch had no exception handling at all, so the exception escaped the streaming generator and the response died mid-answer. The fallback branch now parses each SSE data line before handing it to the filters and re-serialises the result, matching the path that has an event emitter. A filter that raises, returns nothing, or returns something JSON cannot encode drops that event and the stream continues, which is what the event-emitter path already does. Chunks carrying no JSON object, separators, comments and [DONE], are forwarded untouched, and with no filters installed the stream is still forwarded byte for byte. It also passes __body__ to stream filters on this path. The other path already does, so a filter declaring that parameter worked everywhere except here.
This commit is contained in:
parent
29541cbb52
commit
d5abc52ab8
1 changed files with 62 additions and 23 deletions
|
|
@ -3018,6 +3018,29 @@ def build_response_object(response, response_data):
|
|||
return response
|
||||
|
||||
|
||||
def parse_sse_data(raw_line):
|
||||
"""Return the JSON object carried by an SSE data line, or None if it carries none.
|
||||
|
||||
The `data:` prefix is required on purpose: re-framing a bare JSON line would turn an
|
||||
ndjson stream into an SSE one. `update_assistant_message_from_stream` below only reads,
|
||||
so it accepts both.
|
||||
"""
|
||||
line = raw_line.decode('utf-8', 'replace') if isinstance(raw_line, bytes) else raw_line
|
||||
if not isinstance(line, str) or not line.startswith('data:'):
|
||||
return None
|
||||
|
||||
payload = line[len('data:') :].strip()
|
||||
if not payload or payload == '[DONE]':
|
||||
return None
|
||||
|
||||
try:
|
||||
data = JSONCodec.loads(payload)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return data if isinstance(data, dict) else None
|
||||
|
||||
|
||||
def update_assistant_message_from_stream(assistant_message, raw):
|
||||
line = raw.decode('utf-8', 'replace') if isinstance(raw, bytes) else raw
|
||||
if not isinstance(line, str):
|
||||
|
|
@ -5563,34 +5586,50 @@ async def streaming_chat_response_handler(response, ctx):
|
|||
except Exception:
|
||||
has_api_outlet_filters = True
|
||||
|
||||
filter_extra_params = {'__body__': form_data, **extra_params}
|
||||
|
||||
async def apply_stream_filters(event):
|
||||
"""Return the filtered event as an SSE frame, or None if it is to be dropped."""
|
||||
try:
|
||||
event, _ = await process_filter_functions(
|
||||
request=request,
|
||||
filter_context=filter_context,
|
||||
filter_functions=filter_functions,
|
||||
filter_type='stream',
|
||||
form_data=event,
|
||||
extra_params=filter_extra_params,
|
||||
)
|
||||
return wrap_item(JSONCodec.dumps(event)) if event else None
|
||||
except Exception as e:
|
||||
# Drop the event, as the event-emitter path does
|
||||
log.debug('Dropped a stream event: %s', e)
|
||||
return None
|
||||
|
||||
for event in events:
|
||||
event, _ = await process_filter_functions(
|
||||
request=request,
|
||||
filter_context=filter_context,
|
||||
filter_functions=filter_functions,
|
||||
filter_type='stream',
|
||||
form_data=event,
|
||||
extra_params=extra_params,
|
||||
)
|
||||
|
||||
if event:
|
||||
yield wrap_item(JSONCodec.dumps(event))
|
||||
|
||||
async for data in original_generator:
|
||||
data, _ = await process_filter_functions(
|
||||
request=request,
|
||||
filter_context=filter_context,
|
||||
filter_functions=filter_functions,
|
||||
filter_type='stream',
|
||||
form_data=data,
|
||||
extra_params=extra_params,
|
||||
)
|
||||
data = await apply_stream_filters(event)
|
||||
|
||||
if data:
|
||||
if has_api_outlet_filters:
|
||||
update_assistant_message_from_stream(assistant_message, data)
|
||||
yield data
|
||||
|
||||
async for data in original_generator:
|
||||
# Filters take a parsed event dict, so a chunk that carries none is forwarded raw
|
||||
event = parse_sse_data(data) if filter_functions else None
|
||||
|
||||
if event is not None:
|
||||
data = await apply_stream_filters(event)
|
||||
|
||||
if not data:
|
||||
continue
|
||||
|
||||
if has_api_outlet_filters:
|
||||
try:
|
||||
update_assistant_message_from_stream(assistant_message, data)
|
||||
except Exception as e:
|
||||
# Never worth killing the response over, the outlet just sees less
|
||||
log.debug('Failed to accumulate the assistant message: %s', e)
|
||||
|
||||
yield data
|
||||
|
||||
if has_api_outlet_filters and assistant_message:
|
||||
ctx['assistant_message'] = assistant_message
|
||||
await outlet_filter_handler(ctx)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue