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 — 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