fix: global default custom_params lost when a model or request sets its own custom params

When global default model params define custom_params and a model (or an API request) also defines any custom param, the whole global custom_params dict was replaced instead of merged, so global custom params silently stopped reaching the upstream request for that model.

The layer merge now overrides params per key as before, but merges the custom_params dicts key by key across layers, so a higher layer only replaces the specific custom params it actually sets. The config default is also deep-copied at the read site, since downstream code mutates params in place and could otherwise rewrite the process-wide default from a single request.

Fixes #28241
This commit is contained in:
Classic298 2026-08-07 12:48:29 +02:00
parent 8faaf2cd1e
commit 82cfb7f9f0
2 changed files with 19 additions and 9 deletions

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import copy
import logging
import mimetypes
import os
@ -239,6 +240,7 @@ from open_webui.utils.middleware import (
process_chat_payload,
process_chat_response,
)
from open_webui.utils.misc import merge_model_params
from open_webui.utils.model_ids import strip_provider_model_prefix
from open_webui.utils.models import (
check_model_access,
@ -1096,17 +1098,15 @@ async def chat_completion(
await _set_direct_model(request, model, user)
# Model params: global defaults as base, per-model overrides win
default_model_params = await Config.get('models.default_params', {}) or {}
model_info_params = {
**default_model_params,
**(model_info.params.model_dump() if model_info and model_info.params else {}),
}
# Deep copy: downstream mutates params in place and the config default is shared
default_model_params = copy.deepcopy(await Config.get('models.default_params', {}) or {})
model_info_params = merge_model_params(
default_model_params,
model_info.params.model_dump() if model_info and model_info.params else {},
)
request_params = {key: value for key, value in (form_data.get('params') or {}).items() if value is not None}
if model_info_params or request_params:
form_data['params'] = {
**model_info_params,
**request_params,
}
form_data['params'] = merge_model_params(model_info_params, request_params)
# Check base model existence for custom models
if model_info and model_info.base_model_id:

View file

@ -29,6 +29,16 @@ def deep_update(d, u):
return d
def merge_model_params(base: dict, override: dict) -> dict:
# Params override wholesale, except custom_params: a user-defined bag merged per key
params = {**base, **override}
base_custom = base.get('custom_params')
override_custom = override.get('custom_params')
if isinstance(base_custom, dict) and (override_custom is None or isinstance(override_custom, dict)):
params['custom_params'] = {**base_custom, **(override_custom or {})}
return params
def _strip_filter_entry(entry):
# Compose list-form env syntax passes surrounding quotes through verbatim
return (entry or '').strip().strip('"\'').strip()