perf: batch and deduplicate per-request DB reads in the chat middleware (#27223)

Several spots in the chat pipeline issued sequential single-key config
SELECTs, or fetched the same key twice back-to-back, on every request:

- chat_completion_tools_handler: task model default/external and the
  tools prompt template were four sequential Config round trips (the
  template was fetched twice). One batched Config.get_many now serves
  all of them.
- chat_completion_files_handler: the six RAG settings (top_k,
  top_k_reranker, relevance_threshold, hybrid_bm25_weight,
  enable_hybrid_search, full_context) were six sequential round trips
  inside the retrieval call. Batched into one get_many.
- Voice and code-interpreter prompt templates were each fetched twice
  within one conditional; fetch once and reuse. The code-interpreter
  engine was likewise fetched twice per execution.
- Skill resolution fetched the accessible-skills list, kept only the
  ids, then re-fetched each mentioned skill by id (N+1). Reuse the
  rows from the access query.

Value semantics are identical: get_many applies the same defaults as
the individual gets, and the pre-existing truthiness/empty-string
checks on templates are preserved exactly.


Claude-Session: https://claude.ai/code/session_01MHg5zs1VBjvRWQ54qHpfYD

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Classic298 2026-07-23 20:21:29 +02:00 committed by GitHub
parent ca2d7c9deb
commit e64acf1c0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1172,10 +1172,16 @@ async def chat_completion_tools_handler(
event_emitter = extra_params['__event_emitter__']
metadata = extra_params['__metadata__']
# One batched SELECT instead of four sequential round trips.
task_config = await Config.get_many(
'task.model.default',
'task.model.external',
'task.tools.prompt_template',
)
task_model_id = get_task_model_id(
body['model'],
await Config.get('task.model.default'),
await Config.get('task.model.external'),
task_config.get('task.model.default'),
task_config.get('task.model.external'),
models,
)
@ -1185,8 +1191,9 @@ async def chat_completion_tools_handler(
specs = [tool['spec'] for tool in tools.values()]
tools_specs = json.dumps(specs, ensure_ascii=False)
if await Config.get('task.tools.prompt_template') != '':
template = await Config.get('task.tools.prompt_template')
tools_prompt_template = task_config.get('task.tools.prompt_template')
if tools_prompt_template != '':
template = tools_prompt_template
else:
template = DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
@ -1859,6 +1866,15 @@ async def chat_completion_files_handler(
queries = [get_last_user_message(body['messages']) or '']
try:
# One batched SELECT instead of six sequential round trips.
rag_config = await Config.get_many(
'rag.top_k',
'rag.top_k_reranker',
'rag.relevance_threshold',
'rag.hybrid_bm25_weight',
'rag.enable_hybrid_search',
'rag.full_context',
)
# Directly await async get_sources_from_items (no thread needed - fully async now)
sources = await get_sources_from_items(
request=request,
@ -1867,17 +1883,17 @@ async def chat_completion_files_handler(
embedding_function=lambda query, prefix: request.app.state.EMBEDDING_FUNCTION(
query, prefix=prefix, user=user
),
k=await Config.get('rag.top_k'),
k=rag_config.get('rag.top_k'),
reranking_function=(
(lambda query, documents: request.app.state.RERANKING_FUNCTION(query, documents, user=user))
if request.app.state.RERANKING_FUNCTION
else None
),
k_reranker=await Config.get('rag.top_k_reranker'),
r=await Config.get('rag.relevance_threshold'),
hybrid_bm25_weight=await Config.get('rag.hybrid_bm25_weight'),
hybrid_search=await Config.get('rag.enable_hybrid_search'),
full_context=all_full_context or await Config.get('rag.full_context'),
k_reranker=rag_config.get('rag.top_k_reranker'),
r=rag_config.get('rag.relevance_threshold'),
hybrid_bm25_weight=rag_config.get('rag.hybrid_bm25_weight'),
hybrid_search=rag_config.get('rag.enable_hybrid_search'),
full_context=all_full_context or rag_config.get('rag.full_context'),
user=user,
)
except Exception as e:
@ -2492,9 +2508,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
if features:
if 'voice' in features and features['voice']:
if await Config.get('task.voice.prompt.enable'):
if await Config.get('task.voice.prompt_template'):
template = await Config.get('task.voice.prompt_template')
else:
template = await Config.get('task.voice.prompt_template')
if not template:
template = DEFAULT_VOICE_MODE_PROMPT_TEMPLATE
form_data['messages'] = add_or_update_system_message(
@ -2521,11 +2536,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# Skip XML-tag prompt injection when native FC is enabled —
# execute_code will be injected as a builtin tool instead
if metadata.get('params', {}).get('function_calling') == 'legacy':
prompt = (
await Config.get('code_interpreter.prompt_template')
if await Config.get('code_interpreter.prompt_template') != ''
else DEFAULT_CODE_INTERPRETER_PROMPT
)
ci_prompt_template = await Config.get('code_interpreter.prompt_template')
prompt = ci_prompt_template if ci_prompt_template != '' else DEFAULT_CODE_INTERPRETER_PROMPT
# Append filesystem awareness only for pyodide engine
if engine != 'jupyter':
@ -2605,12 +2617,13 @@ async def process_chat_payload(request, form_data, user, metadata, model):
if skill_ids:
from open_webui.models.skills import Skills as SkillsModel
accessible_skill_ids = {s.id for s in await SkillsModel.get_skills_by_user_id(user.id, 'read')}
# Reuse the rows from the access query instead of re-fetching each
# skill by id.
accessible_skills = {s.id: s for s in await SkillsModel.get_skills_by_user_id(user.id, 'read')}
for sid in skill_ids:
if sid in accessible_skill_ids:
s = await SkillsModel.get_skill_by_id(sid)
if s and s.is_active:
available_skills.append(s)
s = accessible_skills.get(sid)
if s and s.is_active:
available_skills.append(s)
skill_manifest = ''
for skill in available_skills:
@ -5192,7 +5205,8 @@ async def streaming_chat_response_handler(response, ctx):
""")
code = blocking_code + '\n' + code
if await Config.get('code_interpreter.engine') == 'pyodide':
ci_engine = await Config.get('code_interpreter.engine')
if ci_engine == 'pyodide':
ci_output = await event_caller(
{
'type': 'execute:python',
@ -5204,7 +5218,7 @@ async def streaming_chat_response_handler(response, ctx):
},
}
)
elif await Config.get('code_interpreter.engine') == 'jupyter':
elif ci_engine == 'jupyter':
ci_output = await execute_code_jupyter(
await Config.get('code_interpreter.jupyter.url'),
code,