@@ -944,7 +944,12 @@ def _router_config_for(kompress_disabled: bool) -> ContentRouterConfig:
944944 self ._code_aware_status = "lazy" if config .code_aware_enabled else "disabled"
945945
946946 _intercept_prefix : list = []
947- if os .environ .get ("HEADROOM_INTERCEPT_ENABLED" ):
947+ if os .environ .get ("HEADROOM_INTERCEPT_ENABLED" , "" ).strip ().lower () in (
948+ "1" ,
949+ "true" ,
950+ "yes" ,
951+ "on" ,
952+ ):
948953 from headroom .proxy .interceptors import ToolResultInterceptorTransform
949954
950955 _intercept_prefix = [ToolResultInterceptorTransform ()]
@@ -2387,12 +2392,23 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
23872392
23882393 # Defensive re-apply of file-backed settings for embedded/non-CLI callers
23892394 # that construct the app without going through the `headroom` CLI entrypoint
2390- # (which already applies them before Click parsing). setdefault keeps
2391- # explicit env exports authoritative; fail-open so it never blocks startup.
2395+ # (which already applies them before Click parsing). settings.json wins over
2396+ # a shell-exported env var here — highest priority below an explicit CLI arg;
2397+ # manifest_managed knobs keep the export. Fail-open so it never blocks startup.
23922398 try :
23932399 from headroom import settings_store
23942400
2395- settings_store .apply_to_environ (settings_store .load ())
2401+ stored = settings_store .load ()
2402+ settings_store .apply_to_environ (stored )
2403+ # Live (Output Shaping) knobs are intentionally NOT written to os.environ
2404+ # (that would env-lock them in the GUI). Seed them into the hot-reload
2405+ # override store so persisted values stay active after a restart yet
2406+ # remain editable. The override wins over os.environ, so the stored
2407+ # value takes precedence over a shell export, matching settings-first.
2408+ _live_seed = settings_store .runtime_overrides (
2409+ settings_store .live_keys (list (stored )), stored
2410+ )
2411+ runtime_env .set_overrides (_live_seed )
23962412 except Exception : # noqa: BLE001 — settings load must never break startup
23972413 pass
23982414
@@ -3359,6 +3375,15 @@ async def settings_schema(_request: Request):
33593375 schema ["supervised" ] = mode != "foreground"
33603376 except Exception : # noqa: BLE001 — schema must render even if detection fails
33613377 schema ["supervised" ] = False
3378+ # Live (runtime_env) knobs apply without a restart, so their true current
3379+ # value is the hot-reload override — not the launch-time environ the store
3380+ # read. Reflect that so the form shows what is actually active right now.
3381+ for field_schema in schema ["fields" ]:
3382+ if field_schema .get ("live" ):
3383+ field_schema ["value" ] = settings_store .coerce_env_value (
3384+ field_schema ["key" ], runtime_env .getenv (field_schema ["env" ])
3385+ )
3386+ schema ["values" ] = {f ["key" ]: f ["value" ] for f in schema ["fields" ]}
33623387 return JSONResponse (status_code = 200 , content = schema )
33633388
33643389 @app .get ("/settings" , dependencies = [Depends (_require_loopback )])
@@ -3399,6 +3424,20 @@ async def settings_post(request: Request):
33993424 )
34003425 after = settings_store .load ()
34013426 changed_keys = sorted (k for k in set (before ) | set (after ) if before .get (k ) != after .get (k ))
3427+ # Live (Output Shaping) knobs hot-reload with no restart: push their new
3428+ # values to the runtime-env override store so they take effect on the
3429+ # next request. Every other knob is startup-captured and needs a restart.
3430+ live_changed = settings_store .live_keys (changed_keys )
3431+ if live_changed :
3432+ runtime_env .set_overrides (settings_store .runtime_overrides (live_changed , after ))
3433+ # A live knob cleared to its default is absent from `after`; drop its
3434+ # override so the reader falls back to the adaptive default right away.
3435+ for key in live_changed :
3436+ if key not in after :
3437+ env = settings_store .env_for (key )
3438+ if env :
3439+ runtime_env .clear_override (env )
3440+ restart_changed = [k for k in changed_keys if k not in live_changed ]
34023441 record_admin_action (
34033442 request = request ,
34043443 action = "settings_update" ,
@@ -3407,7 +3446,12 @@ async def settings_post(request: Request):
34073446 )
34083447 return JSONResponse (
34093448 status_code = 200 ,
3410- content = {"ok" : True , "needs_restart" : bool (changed_keys ), "changed_keys" : changed_keys },
3449+ content = {
3450+ "ok" : True ,
3451+ "needs_restart" : bool (restart_changed ),
3452+ "changed_keys" : changed_keys ,
3453+ "live_keys" : live_changed ,
3454+ },
34113455 )
34123456
34133457 @app .post (
0 commit comments