setting refactor - backend conversions

frdel committed Dec 22, 2025 at 13:47 UTC ef82fc8bb9b663df0096a5ca854befe619e473b3
5 files changed +111 -22
models.py
+2 -2
@@ -22,7 +22,7 @@ from litellm.types.utils import ModelResponse
22 from python.helpers import dotenv
23 from python.helpers import settings, dirty_json
24 from python.helpers.dotenv import load_dotenv
25 -from python.helpers.providers import get_provider_config
25 +from python.helpers.providers import ModelType as ProviderModelType, get_provider_config
26 from python.helpers.rate_limiter import RateLimiter
27 from python.helpers.tokens import approximate_tokens
28 from python.helpers import dirty_json, browser_use_monkeypatch
@@ -844,7 +844,7 @@ def _adjust_call_args(provider_name: str, model_name: str, kwargs: dict):
844
845
846 def _merge_provider_defaults(
847 - provider_type: str, original_provider: str, kwargs: dict
847 + provider_type: ProviderModelType, original_provider: str, kwargs: dict
848 ) -> tuple[str, dict]:
849 # Normalize .env-style numeric strings (e.g., "timeout=30") into ints/floats for LiteLLM
850 def _normalize_values(values: dict) -> dict:
python/api/settings_get.py
+3 -2
@@ -4,8 +4,9 @@ from python.helpers import settings
4
5 class GetSettings(ApiHandler):
6 async def process(self, input: dict, request: Request) -> dict | Response:
7 - set = settings.convert_out(settings.get_settings())
8 - return {"settings": set}
7 + backend = settings.get_settings()
8 + out = settings.convert_out(backend)
9 + return dict(out)
10
11 @classmethod
12 def get_methods(cls) -> list[str]:
python/api/settings_set.py
+5 -3
@@ -7,6 +7,8 @@ from typing import Any
7
8 class SetSettings(ApiHandler):
9 async def process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response:
10 - set = settings.convert_in(input)
11 - set = settings.set_settings(set)
12 - return {"settings": set}
10 + frontend = input.get("settings", input)
11 + backend = settings.convert_in(settings.Settings(**frontend))
12 + backend = settings.set_settings(backend)
13 + out = settings.convert_out(backend)
14 + return dict(out)
python/helpers/providers.py
+9 -9
@@ -1,7 +1,8 @@
1 import yaml
2 from python.helpers import files
3 -from typing import List, Dict, Optional, TypedDict
3 +from typing import List, Dict, Optional, TypedDict, Literal
4
5 +ModelType = Literal["chat", "embedding"]
6
7 # Type alias for UI option items
8 class FieldOption(TypedDict):
@@ -68,16 +69,15 @@ class ProviderManager:
69 opts.append({"value": pid, "label": name})
70 self._options[p_type] = opts
71
71 - def get_providers(self, provider_type: str) -> List[FieldOption]:
72 + def get_providers(self, provider_type: ModelType) -> List[FieldOption]:
73 """Returns a list of providers for a given type (e.g., 'chat', 'embedding')."""
74 return self._options.get(provider_type, []) if self._options else []
75
75 -
76 - def get_raw_providers(self, provider_type: str) -> List[Dict[str, str]]:
76 + def get_raw_providers(self, provider_type: ModelType) -> List[Dict[str, str]]:
77 """Return raw provider dictionaries for advanced use-cases."""
78 return self._raw.get(provider_type, []) if self._raw else []
79
80 - def get_provider_config(self, provider_type: str, provider_id: str) -> Optional[Dict[str, str]]:
80 + def get_provider_config(self, provider_type: ModelType, provider_id: str) -> Optional[Dict[str, str]]:
81 """Return the metadata dict for a single provider id (case-insensitive)."""
82 provider_id_low = provider_id.lower()
83 for p in self.get_raw_providers(provider_type):
@@ -86,16 +86,16 @@ class ProviderManager:
86 return None
87
88
89 -def get_providers(provider_type: str) -> List[FieldOption]:
89 +def get_providers(provider_type: ModelType) -> List[FieldOption]:
90 """Convenience function to get providers of a specific type."""
91 return ProviderManager.get_instance().get_providers(provider_type)
92
93
94 -def get_raw_providers(provider_type: str) -> List[Dict[str, str]]:
94 +def get_raw_providers(provider_type: ModelType) -> List[Dict[str, str]]:
95 """Return full metadata for providers of a given type."""
96 return ProviderManager.get_instance().get_raw_providers(provider_type)
97
98
99 -def get_provider_config(provider_type: str, provider_id: str) -> Optional[Dict[str, str]]:
99 +def get_provider_config(provider_type: ModelType, provider_id: str) -> Optional[Dict[str, str]]:
100 """Return metadata for a single provider (None if not found)."""
101 - return ProviderManager.get_instance().get_provider_config(provider_type, provider_id)
\ No newline at end of file
101 + return ProviderManager.get_instance().get_provider_config(provider_type, provider_id)
\ No newline at end of file
python/helpers/settings.py
+92 -6
@@ -10,7 +10,7 @@ import models
10 from python.helpers import runtime, whisper, defer, git
11 from . import files, dotenv
12 from python.helpers.print_style import PrintStyle
13 -from python.helpers.providers import get_providers
13 +from python.helpers.providers import get_providers, FieldOption as ProvidersFO
14 from python.helpers.secrets import get_default_secrets_manager
15 from python.helpers import dirty_json
16
@@ -156,7 +156,6 @@ class FieldOption(TypedDict):
156 value: str
157 label: str
158
159 -
159 class SettingsField(TypedDict, total=False):
160 id: str
161 title: str
@@ -188,9 +187,20 @@ class SettingsSection(TypedDict, total=False):
187 fields: list[SettingsField]
188 tab: str # Indicates which tab this section belongs to
189
190 +class ModelProvider(ProvidersFO):
191 + pass
192 +
193 +class SettingsOutputAdditional(TypedDict):
194 + chat_providers: list[ModelProvider]
195 + embedding_providers: list[ModelProvider]
196 + shell_interfaces: list[FieldOption]
197 + agent_subdirs: list[FieldOption]
198 + stt_models: list[FieldOption]
199 + is_dockerized: bool
200
201 class SettingsOutput(TypedDict):
193 - sections: list[SettingsSection]
202 + settings: Settings
203 + additional: SettingsOutputAdditional
204
205
206 PASSWORD_PLACEHOLDER = "****PSWD****"
@@ -199,8 +209,55 @@ API_KEY_PLACEHOLDER = "************"
209 SETTINGS_FILE = files.get_abs_path("tmp/settings.json")
210 _settings: Settings | None = None
211
202 -
212 def convert_out(settings: Settings) -> SettingsOutput:
213 + out = SettingsOutput(
214 + settings = settings.copy(),
215 + additional = SettingsOutputAdditional(
216 + chat_providers=get_providers("chat"),
217 + embedding_providers=get_providers("embedding"),
218 + shell_interfaces=[{"value": "local", "label": "Local Python TTY"}, {"value": "ssh", "label": "SSH"}],
219 + is_dockerized=runtime.is_dockerized(),
220 + agent_subdirs=[{"value": subdir, "label": subdir}
221 + for subdir in files.get_subdirectories("agents")
222 + if subdir != "_example"],
223 + stt_models=[
224 + {"value": "tiny", "label": "Tiny (39M, English)"},
225 + {"value": "base", "label": "Base (74M, English)"},
226 + {"value": "small", "label": "Small (244M, English)"},
227 + {"value": "medium", "label": "Medium (769M, English)"},
228 + {"value": "large", "label": "Large (1.5B, Multilingual)"},
229 + {"value": "turbo", "label": "Turbo (Multilingual)"},
230 + ]
231 +
232 + )
233 + )
234 +
235 + # load auth from dotenv
236 + out["settings"]["auth_login"] = dotenv.get_dotenv_value(dotenv.KEY_AUTH_LOGIN) or ""
237 + out["settings"]["auth_password"] = (
238 + PASSWORD_PLACEHOLDER if dotenv.get_dotenv_value(dotenv.KEY_AUTH_PASSWORD) else ""
239 + )
240 + out["settings"]["rfc_password"] = (
241 + PASSWORD_PLACEHOLDER if dotenv.get_dotenv_value(dotenv.KEY_RFC_PASSWORD) else ""
242 + )
243 +
244 + #secrets
245 + secrets_manager = get_default_secrets_manager()
246 + try:
247 + out["settings"]["secrets"] = secrets_manager.get_masked_secrets()
248 + except Exception:
249 + out["settings"]["secrets"] = ""
250 +
251 + # normalize certain fields
252 + for key, value in list(out["settings"].items()):
253 + # convert kwargs dicts to .env format
254 + if (key.endswith("_kwargs") or key=="browser_http_headers") and isinstance(value, dict):
255 + out["settings"][key] = _dict_to_env(value)
256 + return out
257 +
258 +
259 +#TODO just for reference, remove
260 +def _convert_out_old(settings: Settings) -> SettingsOutput:
261 default_settings = get_default_settings()
262
263 # main model section
@@ -1347,7 +1404,34 @@ def _get_api_key_field(settings: Settings, provider: str, title: str) -> Setting
1404 }
1405
1406
1350 -def convert_in(settings: dict) -> Settings:
1407 +def convert_in(settings: Settings) -> Settings:
1408 + current = get_settings()
1409 +
1410 + # api keys
1411 + if "api_keys" in settings and isinstance(settings["api_keys"], dict):
1412 + for provider, value in settings["api_keys"].items():
1413 + if value != API_KEY_PLACEHOLDER:
1414 + current["api_keys"][provider] = value
1415 +
1416 + # all other fields
1417 + for key, value in settings.items():
1418 + if key == "api_keys":
1419 + continue
1420 +
1421 + # Skip saving if value is a placeholder
1422 + if value == PASSWORD_PLACEHOLDER or value == API_KEY_PLACEHOLDER:
1423 + continue
1424 +
1425 + # Special handling for browser_http_headers and *_kwargs (stored as .env text)
1426 + if (key == "browser_http_headers" or key.endswith("_kwargs")) and isinstance(value, str):
1427 + current[key] = _env_to_dict(value)
1428 + continue
1429 +
1430 + current[key] = value
1431 + return current
1432 +
1433 +#TODO just for reference, remove
1434 +def _convert_in_old(settings: dict) -> Settings:
1435 current = get_settings()
1436 for section in settings["sections"]:
1437 if "fields" in section:
@@ -1368,6 +1452,7 @@ def convert_in(settings: dict) -> Settings:
1452 current[field["id"]] = field["value"]
1453 return current
1454
1455 +
1456 def get_settings() -> Settings:
1457 global _settings
1458 if not _settings:
@@ -1385,12 +1470,13 @@ def set_settings(settings: Settings, apply: bool = True):
1470 _write_settings_file(_settings)
1471 if apply:
1472 _apply_settings(previous)
1473 + return _settings
1474
1475
1476 def set_settings_delta(delta: dict, apply: bool = True):
1477 current = get_settings()
1478 new = {**current, **delta}
1393 - set_settings(new, apply) # type: ignore
1479 + return set_settings(new, apply) # type: ignore
1480
1481
1482 def merge_settings(original: Settings, delta: dict) -> Settings: