Skip to content

Commit 8958b64

Browse files
committed
refac
1 parent 21f9e52 commit 8958b64

2 files changed

Lines changed: 137 additions & 4 deletions

File tree

backend/open_webui/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939

4040
async def seed_registered_defaults():
41+
await Config.repair_flattened_dict_configs()
4142
await Config.seed_defaults(DEFAULT_CONFIG)
4243

4344

backend/open_webui/models/config.py

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,76 @@
1515
from typing import Any, ClassVar
1616

1717
from open_webui.internal.db import Base, get_async_db
18-
from sqlalchemy import JSON, BigInteger, Column, Text, select
18+
from sqlalchemy import JSON, BigInteger, Column, Text, delete, select
1919

2020
log = logging.getLogger(__name__)
2121

22+
API_CONFIG_KEYS = ('openai.api_configs', 'ollama.api_configs')
23+
DICT_CONFIG_KEY_ALIASES = {
24+
'openai.api_configs': ('OPENAI_API_CONFIGS',),
25+
'ollama.api_configs': ('OLLAMA_API_CONFIGS',),
26+
'rag.mineru_params': ('MINERU_PARAMS',),
27+
'rag.docling_params': ('DOCLING_PARAMS',),
28+
'rag.web.search.linkup_search_params': ('LINKUP_SEARCH_PARAMS',),
29+
'image_generation.automatic1111.api_params': ('AUTOMATIC1111_PARAMS',),
30+
'image_generation.openai.params': ('IMAGES_OPENAI_API_PARAMS',),
31+
'audio.tts.openai.params': ('AUDIO_TTS_OPENAI_PARAMS',),
32+
'models.default_metadata': ('DEFAULT_MODEL_METADATA',),
33+
'models.default_params': ('DEFAULT_MODEL_PARAMS',),
34+
'user.permissions': ('USER_PERMISSIONS',),
35+
}
36+
DICT_CONFIG_KEYS = tuple(DICT_CONFIG_KEY_ALIASES)
37+
API_CONFIG_FIELDS = (
38+
'enable',
39+
'key',
40+
'prefix_id',
41+
'tags',
42+
'model_ids',
43+
'connection_type',
44+
'provider',
45+
'auth_type',
46+
'headers',
47+
'azure',
48+
'api_version',
49+
'extra_params',
50+
)
51+
52+
53+
def _split_api_config_fragment(fragment: str) -> tuple[str, list[str]] | None:
54+
if not fragment:
55+
return None
56+
57+
first, _, rest = fragment.partition('.')
58+
if first.isdigit() and rest:
59+
return first, rest.split('.')
60+
61+
match: tuple[int, str] | None = None
62+
for field in API_CONFIG_FIELDS:
63+
marker = f'.{field}'
64+
marker_index = fragment.rfind(marker)
65+
if marker_index != -1 and (match is None or marker_index > match[0]):
66+
match = (marker_index, field)
67+
68+
if match:
69+
marker_index, field = match
70+
connection_key = fragment[:marker_index]
71+
field_path = fragment[marker_index + 1 :]
72+
if connection_key:
73+
return connection_key, field_path.split('.')
74+
75+
return None
76+
77+
78+
def _assign_path(target: dict, path: list[str], value: Any) -> None:
79+
current = target
80+
for part in path[:-1]:
81+
next_value = current.get(part)
82+
if not isinstance(next_value, dict):
83+
next_value = {}
84+
current[part] = next_value
85+
current = next_value
86+
current[path[-1]] = value
87+
2288

2389
# ── Model ────────────────────────────────────────────────────────────────────
2490

@@ -147,10 +213,8 @@ async def delete(key: str) -> bool:
147213
@staticmethod
148214
async def clear() -> None:
149215
"""Delete all config rows."""
150-
from sqlalchemy import delete as sa_delete
151-
152216
async with get_async_db() as db:
153-
await db.execute(sa_delete(Config))
217+
await db.execute(delete(Config))
154218
await db.commit()
155219

156220
@staticmethod
@@ -175,3 +239,71 @@ async def seed_defaults(defaults: dict) -> None:
175239
if new_count:
176240
await db.commit()
177241
log.info('Seeded %d new config defaults', new_count)
242+
243+
@staticmethod
244+
async def repair_flattened_dict_configs() -> None:
245+
"""Reassemble dict config values flattened by the per-key migration."""
246+
if not Config.PERSISTENT_ENABLED:
247+
return
248+
249+
async with get_async_db() as db:
250+
repaired_keys: list[str] = []
251+
orphan_keys: list[str] = []
252+
253+
for config_key, aliases in DICT_CONFIG_KEY_ALIASES.items():
254+
prefixes = (config_key, *aliases)
255+
rows = []
256+
for key_prefix in prefixes:
257+
result = await db.execute(select(Config).where(Config.key.like(f'{key_prefix}.%')))
258+
rows.extend(result.scalars().all())
259+
if not rows:
260+
continue
261+
262+
existing = await db.get(Config, config_key)
263+
repaired = existing.value if existing and isinstance(existing.value, dict) else {}
264+
265+
repaired_any = False
266+
for row in rows:
267+
fragment = None
268+
for key_prefix in prefixes:
269+
prefix = f'{key_prefix}.'
270+
if row.key.startswith(prefix):
271+
fragment = row.key.removeprefix(prefix)
272+
break
273+
if fragment is None:
274+
continue
275+
276+
if config_key in API_CONFIG_KEYS:
277+
split = _split_api_config_fragment(fragment)
278+
if not split:
279+
continue
280+
object_key, field_path = split
281+
else:
282+
object_key, field_path = None, fragment.split('.')
283+
284+
target = repaired
285+
if object_key is not None:
286+
target = repaired.setdefault(object_key, {})
287+
if not isinstance(target, dict):
288+
continue
289+
290+
_assign_path(target, field_path, row.value)
291+
orphan_keys.append(row.key)
292+
repaired_any = True
293+
294+
if not repaired_any:
295+
continue
296+
297+
if existing:
298+
existing.value = repaired
299+
existing.updated_at = int(time.time())
300+
else:
301+
db.add(Config(key=config_key, value=repaired, updated_at=int(time.time())))
302+
repaired_keys.append(config_key)
303+
304+
if orphan_keys:
305+
await db.execute(delete(Config).where(Config.key.in_(orphan_keys)))
306+
307+
if repaired_keys or orphan_keys:
308+
await db.commit()
309+
log.info('Repaired flattened dict config rows for %s', ', '.join(repaired_keys))

0 commit comments

Comments
 (0)