refactor: remove browser_http_headers config and add safe_call utility for plugin hooks

- Remove browser_http_headers from model config (config, migration, UI, helpers) - Add safe_call function to extract_tools.py to safely invoke functions with filtered args/kwargs - Update call_plugin_hook to use safe_call for better parameter handling - Add _apply_defaults_from_env to apply environment variable defaults to plugin configs - Delete additional plugin asset folders when deleting plugins - Remove

frdel committed Mar 20, 2026 at 12:43 UTC 971e93ee9689e4c5eff612fed69246478fa991f5
11 files changed +76 -37
helpers/extract_tools.py
+34
@@ -5,6 +5,8 @@ from .dirty_json import DirtyJson
5 from .files import get_abs_path, deabsolute_path
6 import regex
7 from fnmatch import fnmatch
8 +import inspect
9 +
10
11 def json_parse_dirty(json:str) -> dict[str,Any] | None:
12 if not json or not isinstance(json, str):
@@ -118,3 +120,35 @@ def load_classes_from_file(file: str, base_class: type[T], one_per_file: bool =
120 break
121
122 return classes
123 +
124 +def safe_call(func, *args, **kwargs):
125 + sig = inspect.signature(func)
126 +
127 + bound_args = []
128 + bound_kwargs = {}
129 +
130 + params = sig.parameters
131 +
132 + # Check if function accepts *args / **kwargs
133 + accepts_var_args = any(p.kind == p.VAR_POSITIONAL for p in params.values())
134 + accepts_var_kwargs = any(p.kind == p.VAR_KEYWORD for p in params.values())
135 +
136 + # Handle positional args
137 + if accepts_var_args:
138 + bound_args = args
139 + else:
140 + max_positional = sum(
141 + 1 for p in params.values()
142 + if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
143 + )
144 + bound_args = args[:max_positional]
145 +
146 + # Handle kwargs
147 + if accepts_var_kwargs:
148 + bound_kwargs = kwargs
149 + else:
150 + bound_kwargs = {
151 + k: v for k, v in kwargs.items() if k in params
152 + }
153 +
154 + return func(*bound_args, **bound_kwargs)
\ No newline at end of file
helpers/plugins.py
+30 -2
@@ -14,6 +14,7 @@ from typing import (
14 TYPE_CHECKING,
15 TypedDict,
16 )
17 +from helpers.settings import get_default_value
18
19 from regex import W
20
@@ -359,10 +360,20 @@ def delete_plugin(plugin_name: str):
360 custom_plugins_dir = files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR)
361 if not files.is_in_dir(plugin_dir, custom_plugins_dir):
362 raise ValueError("Only custom plugins can be deleted")
363 +
364 + # delete additional plugin folders
365 + assets = find_plugin_assets("",plugin_name=plugin_name)
366 + for asset in assets:
367 + files.delete_dir(asset["path"])
368 +
369 +
370 send_frontend_reload_notification(
371 [plugin_name]
372 ) # send before deletion to properly check the extensions, second notification will be skipped automatically
373 +
374 + # delete main plugin folder
375 files.delete_dir(plugin_dir)
376 +
377 after_plugin_change([plugin_name])
378
379
@@ -540,6 +551,8 @@ def get_plugin_config(
551 agent_profile: str | None = None,
552 ):
553
554 + default_used = False
555 +
556 if project_name is None and agent is not None:
557 from helpers import projects
558
@@ -561,6 +574,7 @@ def get_plugin_config(
574 file_path = files.get_abs_path(
575 find_plugin_dir(plugin_name), CONFIG_DEFAULT_FILE_NAME
576 )
577 + default_used = True
578
579 result = None
580 if file_path and files.exists(file_path):
@@ -568,6 +582,9 @@ def get_plugin_config(
582 json.loads if file_path.lower().endswith(".json") else yaml_helper.loads
583 )(files.read_file(file_path))
584
585 + if default_used:
586 + _apply_defaults_from_env(plugin_name, result)
587 +
588 # call plugin hook to modify the standard result if needed
589 result = call_plugin_hook(
590 plugin_name,
@@ -838,6 +855,17 @@ def call_plugin_hook(
855 return default
856
857 if asyncio.iscoroutinefunction(hook):
841 - return asyncio.run(hook(*args, **kwargs, default=default))
858 + return asyncio.run(extract_tools.safe_call(hook, *args, default=default, **kwargs))
859 +
860 + return extract_tools.safe_call(hook, *args, default=default, **kwargs)
861 +
862 +
863 +def _apply_defaults_from_env(plugin_name: str, config: dict[str, Any]):
864 + def _apply(prefix: list[str], value: dict[str, Any]):
865 + for key, child in value.items():
866 + env_name = "__".join([plugin_name, *prefix, key])
867 + value[key] = get_default_value(env_name, child)
868 + if isinstance(value[key], dict):
869 + _apply([*prefix, key], value[key])
870
843 - return hook(*args, **kwargs, default=default)
871 + _apply([], config)
helpers/settings.py
+3 -3
@@ -262,7 +262,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
262 # normalize certain fields
263 for key, value in list(out["settings"].items()):
264 # convert kwargs dicts to .env format
265 - if (key.endswith("_kwargs") or key=="browser_http_headers") and isinstance(value, dict):
265 + if (key.endswith("_kwargs")) and isinstance(value, dict):
266 out["settings"][key] = _dict_to_env(value)
267 return out
268
@@ -281,8 +281,8 @@ def convert_in(settings: Settings) -> Settings:
281 current = get_settings()
282
283 for key, value in settings.items():
284 - # Special handling for browser_http_headers and *_kwargs (stored as .env text)
285 - if (key == "browser_http_headers" or key.endswith("_kwargs")) and isinstance(value, str):
284 + # Special handling for *_kwargs (stored as .env text)
285 + if (key.endswith("_kwargs")) and isinstance(value, str):
286 current[key] = _env_to_dict(value)
287 continue
288
plugins/_model_config/default_config.yaml
+1 -3
@@ -29,6 +29,4 @@ embedding_model:
29 api_base: ""
30 rl_requests: 0
31 rl_input: 0
32 - kwargs: {}
33 -
34 -browser_http_headers: {}
32 + kwargs: {}
\ No newline at end of file
plugins/_model_config/extensions/python/initialize_migration_start/_10_migrate_model_config.py
-4
@@ -83,7 +83,6 @@ class MigrateModelConfig(Extension):
83 "rl_input": raw.get("embed_model_rl_input", 0),
84 "kwargs": raw.get("embed_model_kwargs", {}),
85 },
86 - "browser_http_headers": raw.get("browser_http_headers", {}),
86 }
87
88 # Ensure kwargs are dicts (might be strings from .env format)
@@ -92,9 +91,6 @@ class MigrateModelConfig(Extension):
91 if isinstance(kw, str):
92 plugin_config[section]["kwargs"] = {}
93
95 - if isinstance(plugin_config["browser_http_headers"], str):
96 - plugin_config["browser_http_headers"] = {}
97 -
94 # Save as global plugin config
95 plugins.save_plugin_config("_model_config", "", "", plugin_config)
96 PrintStyle(background_color="#6734C3", font_color="white", padding=True).print(
plugins/_model_config/helpers/model_config.py
-7
@@ -117,13 +117,6 @@ def get_embedding_model_config(agent=None) -> dict:
117 cfg = get_config(agent)
118 return cfg.get("embedding_model", {})
119
120 -
121 -def get_browser_http_headers(agent=None) -> dict:
122 - """Get browser HTTP headers from config."""
123 - cfg = get_config(agent)
124 - return cfg.get("browser_http_headers", {})
125 -
126 -
120 def is_chat_override_allowed(agent=None) -> bool:
121 """Check if per-chat model override is enabled."""
122 cfg = get_config(agent)
plugins/_model_config/hooks.py
-1
@@ -1,7 +1,6 @@
1 def save_plugin_config(result=None, settings=None, **kwargs):
2 if settings and isinstance(settings, dict):
3 # Remove transient UI-only fields before persisting
4 - settings.pop("_browser_headers_text", None)
4 for section in ("chat_model", "utility_model", "embedding_model"):
5 if section in settings and isinstance(settings[section], dict):
6 settings[section].pop("_kwargs_text", None)
plugins/_model_config/webui/config.html
-4
@@ -267,10 +267,6 @@
267 Custom HTTP headers sent with browser requests. The browser agent uses the main model. Format is KEY=VALUE, one per line.
268 </div>
269 </div>
270 - <div class="field-control">
271 - <textarea x-model="config._browser_headers_text"
272 - @change="config.browser_http_headers = $store.modelConfig.textToHeaders(config._browser_headers_text)"></textarea>
273 - </div>
270 </div>
271 </template>
272
plugins/_model_config/webui/main.html
+4 -8
@@ -188,10 +188,6 @@
188 <div class="field-title">Browser HTTP Headers</div>
189 <div class="field-description">Custom HTTP headers sent with browser requests. The browser agent uses the main model. Format is KEY=VALUE, one per line.</div>
190 </div>
191 - <div class="field-control">
192 - <textarea x-model="preset.chat._browser_headers_text"
193 - @change="preset.chat.browser_http_headers = $store.modelConfig.textToHeaders(preset.chat._browser_headers_text)"></textarea>
194 - </div>
191 </div>
192
193 <div class="preset-subheader">Utility Model <span style="opacity:0.5; font-size:0.75rem;">(optional &#x2014; falls back to the configured Utility Model)</span></div>
@@ -330,21 +326,21 @@
326 @click="
327 presets = [...presets, {
328 name: 'Preset ' + (presets.length + 1),
333 - chat: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_history: 0.7, vision: true, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, browser_http_headers: {}, _kwargs_text: '', _browser_headers_text: '' },
329 + chat: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_history: 0.7, vision: true, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, _kwargs_text: '' },
330 utility: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_input: 0.7, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, _kwargs_text: '' }
331 }]">
336 - <span class="material-symbols-outlined" style="font-size:16px;">add</span>
332 + <span class="material-symbols-outlined">add</span>
333 <span>Add Preset</span>
334 </button>
335 <button class="text-button" @click="(async () => {
336 await import('/components/plugins/plugin-settings-store.js');
337 await $store.pluginSettingsPrototype.openConfig('_model_config');
338 })()">
343 - <span class="material-symbols-outlined" style="font-size:15px;">settings</span>
339 + <span class="material-symbols-outlined">settings</span>
340 <span>Settings</span>
341 </button>
342 <button class="text-button" @click="openModal('/plugins/_model_config/webui/api-keys.html')">
347 - <span class="material-symbols-outlined" style="font-size:15px;">key</span>
343 + <span class="material-symbols-outlined">key</span>
344 <span>API Keys</span>
345 </button>
346 </div>
plugins/_model_config/webui/model-config-store.js
+2 -3
@@ -70,7 +70,7 @@ export const store = createStore("modelConfig", {
70 _normalizePresets(rawPresets) {
71 return (rawPresets || []).map(p => ({
72 name: p.name || '',
73 - chat: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_history: 0.7, vision: true, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, browser_http_headers: {}, _kwargs_text: kwargsToText(p.chat?.kwargs), _browser_headers_text: Object.entries(p.chat?.browser_http_headers || {}).map(([k, v]) => k + '=' + v).join('\n'), ...(p.chat || {}) },
73 + chat: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_history: 0.7, vision: true, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, _kwargs_text: kwargsToText(p.chat?.kwargs), ...(p.chat || {}) },
74 utility: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_input: 0.7, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, _kwargs_text: kwargsToText(p.utility?.kwargs), ...(p.utility || {}) },
75 }));
76 },
@@ -117,7 +117,6 @@ export const store = createStore("modelConfig", {
117 if (config?.chat_model) config.chat_model._kwargs_text = kwargsToText(config.chat_model.kwargs);
118 if (config?.utility_model) config.utility_model._kwargs_text = kwargsToText(config.utility_model.kwargs);
119 if (config?.embedding_model) config.embedding_model._kwargs_text = kwargsToText(config.embedding_model.kwargs);
120 - if (config) config._browser_headers_text = Object.entries(config.browser_http_headers || {}).map(([k, v]) => k + '=' + v).join('\n');
120 },
121
122 // Global presets
@@ -143,7 +142,7 @@ export const store = createStore("modelConfig", {
142 const c = { name: p.name };
143 for (const slot of ['chat', 'utility']) {
144 if (p[slot]) {
146 - const { _kwargs_text, _browser_headers_text, ...rest } = p[slot];
145 + const { _kwargs_text, ...rest } = p[slot];
146 c[slot] = rest;
147 }
148 }
tools/browser_agent.py
+2 -2
@@ -44,8 +44,8 @@ class State:
44 )
45
46 def _get_browser_http_headers(self):
47 - from plugins._model_config.helpers.model_config import get_browser_http_headers
48 - return get_browser_http_headers(self.agent) or {}
47 + # ignored for now
48 + return {}
49
50 def _get_browser_vision(self):
51 from plugins._model_config.helpers.model_config import get_chat_model_config