fix: run the tool-call continuation inlet on every continuation

The opt-in inlet re-run in drain_approved_tool_calls sat inside the branch that rebuilds the message list from the database. When that rebuild is skipped the function still reports "not paused", so the request reached the model with a body no opted-in filter had seen and the feature's own promise quietly did not hold. Moving the call up one level makes it run on every continuation the drain lets through.

The flag is now called tool_call_continuation, the word the rest of the backend already uses for this, which matters because plugin authors type it into their own modules.

The chat and message id params go only to the continuation inlet instead of into the shared extra_params that existing stream filters also read, and the filter lookup picks up the ENABLE_PLUGINS guard its neighbours use. The body is handed in as a copy and copied back, so a filter returning a new dict no longer needs an identity check to avoid clearing the request.
This commit is contained in:
Classic298 2026-08-30 22:24:20 +02:00
parent 24b11a9f68
commit cc27ae6f0e
2 changed files with 40 additions and 41 deletions

View file

@ -159,7 +159,7 @@ async def process_filter_function(
filter_context,
valves_by_id,
filter_ids,
tool_turns=False,
tool_call_continuation=False,
):
filter_id = function.id
@ -170,9 +170,7 @@ async def process_filter_function(
if not handler:
return form_data, valves_by_id, None
# Tool-call turns only run filters that opt in with `tool_turns = True`, and
# re-run them on the whole body, so an opted-in handler has to be idempotent.
if tool_turns and not getattr(function_module, 'tool_turns', False):
if tool_call_continuation and not getattr(function_module, 'tool_call_continuation', False):
return form_data, valves_by_id, None
skip_files = (
@ -210,7 +208,7 @@ async def process_filter_functions(
filter_type,
form_data,
extra_params,
tool_turns=False,
tool_call_continuation=False,
):
if not ENABLE_PLUGINS:
return form_data, {}
@ -232,13 +230,13 @@ async def process_filter_functions(
filter_context,
valves_by_id,
filter_ids,
tool_turns=tool_turns,
tool_call_continuation=tool_call_continuation,
)
skip_files = skip_files or file_handler
# Handle file cleanup for inlet; the user turn already stripped them, and on a
# tool turn `metadata` is live state later tool calls still read.
if skip_files and not tool_turns:
# Handle file cleanup for inlet
# Skipped on continuations: files are already stripped, and metadata is live state later tool calls read.
if skip_files and not tool_call_continuation:
if 'files' in form_data.get('metadata', {}):
del form_data['metadata']['files']
if 'files' in form_data:

View file

@ -3389,31 +3389,30 @@ async def drain_approved_tool_calls(request, form_data, user, model, metadata) -
)
form_data['messages'] = sanitize_tool_pairs(form_data['messages'])
if not paused:
filtered_form_data, _ = await process_filter_functions(
request=request,
filter_context=None,
filter_functions=await get_filter_functions(request, model, metadata.get('filter_ids', [])),
filter_type='inlet',
form_data=form_data,
extra_params={
'__event_emitter__': event_emitter,
'__event_call__': event_caller,
'__user__': user.model_dump() if isinstance(user, UserModel) else {},
'__metadata__': metadata,
'__oauth_token__': await get_system_oauth_token(request, user),
'__request__': request,
'__model__': model,
'__chat_id__': chat_id,
'__message_id__': message_id,
},
tool_turns=True,
)
# The caller holds its own reference, so a filter that returned a new
# body has to be swapped in rather than rebound.
if filtered_form_data is not form_data:
form_data.clear()
form_data.update(filtered_form_data)
if not paused:
filtered_form_data, _ = await process_filter_functions(
request=request,
filter_context=None,
filter_functions=(
await get_filter_functions(request, model, metadata.get('filter_ids', [])) if ENABLE_PLUGINS else []
),
filter_type='inlet',
form_data=dict(form_data),
extra_params={
'__event_emitter__': event_emitter,
'__event_call__': event_caller,
'__user__': user.model_dump() if isinstance(user, UserModel) else {},
'__metadata__': metadata,
'__oauth_token__': await get_system_oauth_token(request, user),
'__request__': request,
'__model__': model,
'__chat_id__': chat_id,
'__message_id__': message_id,
},
tool_call_continuation=True,
)
form_data.clear()
form_data.update(filtered_form_data)
return paused
@ -4223,8 +4222,6 @@ async def streaming_chat_response_handler(response, ctx):
'__oauth_token__': await get_system_oauth_token(request, user),
'__request__': request,
'__model__': model,
'__chat_id__': metadata.get('chat_id'),
'__message_id__': metadata.get('message_id'),
}
filter_functions = (
@ -4245,15 +4242,19 @@ async def streaming_chat_response_handler(response, ctx):
tag_boundary_positions = {}
response_stream_task_id = metadata.get('task_id') or metadata.get('message_id')
async def apply_tool_turn_inlet(new_form_data):
async def apply_continuation_inlet(new_form_data):
new_form_data, _ = await process_filter_functions(
request=request,
filter_context=filter_context,
filter_functions=filter_functions,
filter_type='inlet',
form_data=new_form_data,
extra_params=extra_params,
tool_turns=True,
extra_params={
**extra_params,
'__chat_id__': metadata.get('chat_id'),
'__message_id__': metadata.get('message_id'),
},
tool_call_continuation=True,
)
return new_form_data
@ -5979,7 +5980,7 @@ async def streaming_chat_response_handler(response, ctx):
}
)
new_form_data = await apply_tool_turn_inlet(new_form_data)
new_form_data = await apply_continuation_inlet(new_form_data)
res = await generate_chat_completion(
request,
@ -6189,7 +6190,7 @@ async def streaming_chat_response_handler(response, ctx):
],
}
new_form_data = await apply_tool_turn_inlet(new_form_data)
new_form_data = await apply_continuation_inlet(new_form_data)
res = await generate_chat_completion(
request,