mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(settings): report live proxy configuration
This commit is contained in:
parent
5e0ce242e9
commit
e50d3cbabf
8 changed files with 221 additions and 26 deletions
|
|
@ -390,7 +390,9 @@ def proxy_pipeline_kwargs(config: object) -> dict[str, object]:
|
|||
return kwargs
|
||||
|
||||
|
||||
def seed_proxy_env_defaults(env: MutableMapping[str, str] | None = None) -> None:
|
||||
def seed_proxy_env_defaults(
|
||||
env: MutableMapping[str, str] | None = None,
|
||||
) -> frozenset[str]:
|
||||
"""Seed the process env with the savings-profile defaults (default: coding).
|
||||
|
||||
Call at proxy EXECUTABLE entry points (the ``headroom proxy`` command and the
|
||||
|
|
@ -403,9 +405,11 @@ def seed_proxy_env_defaults(env: MutableMapping[str, str] | None = None) -> None
|
|||
keep clean (unseeded) defaults and test isolation is preserved.
|
||||
"""
|
||||
target = os.environ if env is None else env
|
||||
before = set(target)
|
||||
# apply_agent_savings_env_defaults honors an explicit HEADROOM_SAVINGS_PROFILE
|
||||
# already in the env and otherwise falls back to DEFAULT_PROFILE (coding).
|
||||
apply_agent_savings_env_defaults(target)
|
||||
return frozenset(key for key in target if key not in before)
|
||||
|
||||
|
||||
def with_target_savings(
|
||||
|
|
|
|||
|
|
@ -174,6 +174,10 @@ class ProxyConfig:
|
|||
# Optimization mode: "token" (rewrite for max compression) or
|
||||
# "cache" (freeze prior turns for prefix-cache stability).
|
||||
mode: str = "token"
|
||||
# Environment keys generated by the savings profile at process startup.
|
||||
# They are runtime defaults, not explicit operator exports; the settings
|
||||
# schema uses this provenance to avoid reporting false env overrides.
|
||||
profile_seeded_env_keys: frozenset[str] = field(default_factory=frozenset, repr=False)
|
||||
|
||||
# Optimization
|
||||
optimize: bool = True
|
||||
|
|
|
|||
|
|
@ -3670,7 +3670,10 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
|
|||
)
|
||||
async def settings_schema(_request: Request):
|
||||
"""Registry + grouped fields + effective values for the settings form."""
|
||||
schema = settings_store.to_schema()
|
||||
schema = settings_store.to_schema(
|
||||
runtime_values=_settings_runtime_values(config),
|
||||
env_override_exclusions=config.profile_seeded_env_keys,
|
||||
)
|
||||
# Tell the UI whether this is a supervised (docker/service) install, where
|
||||
# manifest-baked knobs (HEADROOM_PORT/HEADROOM_HOST) are owned by the
|
||||
# install manifest and must be rendered read-only. Foreground proxies
|
||||
|
|
@ -5188,7 +5191,7 @@ def _json_ready(value: Any) -> Any:
|
|||
return {field.name: _json_ready(getattr(value, field.name)) for field in fields(value)}
|
||||
if isinstance(value, dict):
|
||||
return {str(key): _json_ready(item) for key, item in value.items()}
|
||||
if isinstance(value, list | tuple | set):
|
||||
if isinstance(value, list | tuple | set | frozenset):
|
||||
return [_json_ready(item) for item in value]
|
||||
return value
|
||||
|
||||
|
|
@ -5209,6 +5212,51 @@ def _proxy_config_payload(config: ProxyConfig) -> dict[str, Any]:
|
|||
return payload
|
||||
|
||||
|
||||
_SETTINGS_CONFIG_ALIASES: dict[str, str] = {
|
||||
"workers": "worker_processes",
|
||||
"rpm": "rate_limit_requests_per_minute",
|
||||
"tpm": "rate_limit_tokens_per_minute",
|
||||
"budget": "budget_limit_usd",
|
||||
"log_messages": "log_full_messages",
|
||||
"ccr_inline_resolve": "ccr_resolve_markers_inline",
|
||||
"subscription_poll_interval": "subscription_poll_interval_s",
|
||||
"request_timeout": "request_timeout_seconds",
|
||||
"min_evidence": "traffic_learning_min_evidence",
|
||||
"memory_project_root": "memory_project_root_override",
|
||||
"anthropic_base_url": "anthropic_api_url",
|
||||
"openai_base_url": "openai_api_url",
|
||||
"region": "bedrock_region",
|
||||
}
|
||||
|
||||
|
||||
def _settings_runtime_values(config: ProxyConfig) -> dict[str, Any]:
|
||||
"""Project the live proxy config onto the curated settings registry."""
|
||||
from headroom import settings_store
|
||||
|
||||
values: dict[str, Any] = {}
|
||||
for setting in settings_store.SETTINGS:
|
||||
attribute = _SETTINGS_CONFIG_ALIASES.get(setting.key, setting.key)
|
||||
if not hasattr(config, attribute):
|
||||
continue
|
||||
value = getattr(config, attribute)
|
||||
if isinstance(value, set | frozenset):
|
||||
value = ",".join(sorted(value))
|
||||
values[setting.key] = value
|
||||
|
||||
values.update(
|
||||
{
|
||||
"no_ccr": not (
|
||||
config.ccr_inject_tool or config.ccr_inject_marker or config.ccr_handle_responses
|
||||
),
|
||||
"no_ccr_proactive_expansion": not config.ccr_proactive_expansion,
|
||||
"no_subscription_tracking": not config.subscription_tracking_enabled,
|
||||
"no_memory_tools": not config.memory_inject_tools,
|
||||
"no_memory_context": not config.memory_inject_context,
|
||||
}
|
||||
)
|
||||
return values
|
||||
|
||||
|
||||
def _proxy_config_from_env() -> ProxyConfig:
|
||||
raw_config = os.environ.get(_MULTI_WORKER_CONFIG_ENV)
|
||||
if raw_config:
|
||||
|
|
@ -5221,6 +5269,8 @@ def _proxy_config_from_env() -> ProxyConfig:
|
|||
from headroom.rollout import RolloutSnapshot
|
||||
|
||||
values["rollout"] = RolloutSnapshot.from_internal_dict(rollout_value)
|
||||
if "profile_seeded_env_keys" in values:
|
||||
values["profile_seeded_env_keys"] = frozenset(values["profile_seeded_env_keys"])
|
||||
return ProxyConfig(**values)
|
||||
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
|
||||
logger.warning(
|
||||
|
|
@ -5296,8 +5346,12 @@ def create_app_from_env() -> FastAPI:
|
|||
# tool-search, dedupe, read protection, …). setdefault → explicit env wins.
|
||||
from headroom.agent_savings import seed_proxy_env_defaults
|
||||
|
||||
seed_proxy_env_defaults()
|
||||
return create_app(_proxy_config_from_env())
|
||||
seeded_env_keys = seed_proxy_env_defaults()
|
||||
config = _proxy_config_from_env()
|
||||
config.profile_seeded_env_keys = frozenset(
|
||||
set(config.profile_seeded_env_keys) | set(seeded_env_keys)
|
||||
)
|
||||
return create_app(config)
|
||||
|
||||
|
||||
def _get_code_aware_banner_status(config: ProxyConfig) -> str:
|
||||
|
|
@ -5364,9 +5418,12 @@ def run_server(
|
|||
# already resolved into `config` above via their inline defaults.
|
||||
from headroom.agent_savings import seed_proxy_env_defaults
|
||||
|
||||
seed_proxy_env_defaults()
|
||||
seeded_env_keys = seed_proxy_env_defaults()
|
||||
|
||||
config = config or ProxyConfig()
|
||||
config.profile_seeded_env_keys = frozenset(
|
||||
set(config.profile_seeded_env_keys) | set(seeded_env_keys)
|
||||
)
|
||||
if workers < 1:
|
||||
raise ValueError("workers must be >= 1")
|
||||
config.worker_processes = workers
|
||||
|
|
|
|||
|
|
@ -63,6 +63,20 @@ class SettingField:
|
|||
# the body-resolved HEADROOM_CODE_AWARE_ENABLED reader both accept).
|
||||
SETTINGS: tuple[SettingField, ...] = (
|
||||
# --- Compression ---
|
||||
SettingField(
|
||||
"HEADROOM_MODE",
|
||||
"mode",
|
||||
"Proxy mode",
|
||||
"Compression",
|
||||
"enum",
|
||||
default="cache",
|
||||
choices=("token", "cache"),
|
||||
help=(
|
||||
"Optimization posture: token rewrites history for maximum compression; "
|
||||
"cache preserves prior turns for prefix-cache stability."
|
||||
),
|
||||
tier="basic",
|
||||
),
|
||||
SettingField(
|
||||
"HEADROOM_SAVINGS_PROFILE",
|
||||
"savings_profile",
|
||||
|
|
@ -947,8 +961,25 @@ def apply_to_environ(values: dict[str, Any]) -> None:
|
|||
os.environ.setdefault(field.env, _serialize(field, value))
|
||||
|
||||
|
||||
def effective_values(stored: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""The value actually active now for each knob: default ← file ← environ."""
|
||||
def _coerce_runtime_values(runtime_values: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""Coerce known live-config values without exposing invalid internal state."""
|
||||
result: dict[str, Any] = {}
|
||||
for key, value in (runtime_values or {}).items():
|
||||
field = _BY_KEY.get(key)
|
||||
if field is None:
|
||||
continue
|
||||
try:
|
||||
result[key] = _coerce(field, value)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
def effective_values(
|
||||
stored: dict[str, Any] | None = None,
|
||||
runtime_values: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""The active value for each knob: default ← file ← environ ← runtime."""
|
||||
if stored is None:
|
||||
stored = load()
|
||||
result: dict[str, Any] = {}
|
||||
|
|
@ -961,6 +992,7 @@ def effective_values(stored: dict[str, Any] | None = None) -> dict[str, Any]:
|
|||
except (ValueError, TypeError):
|
||||
pass # unparseable env: keep the file/default value
|
||||
result[field.key] = value
|
||||
result.update(_coerce_runtime_values(runtime_values))
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -978,14 +1010,26 @@ def stored_values(mask_secrets: bool = True) -> dict[str, Any]:
|
|||
return {key: _mask(_BY_KEY[key], value) for key, value in values.items()}
|
||||
|
||||
|
||||
def to_schema() -> dict[str, Any]:
|
||||
def to_schema(
|
||||
*,
|
||||
runtime_values: dict[str, Any] | None = None,
|
||||
env_override_exclusions: set[str] | frozenset[str] = frozenset(),
|
||||
) -> dict[str, Any]:
|
||||
"""Registry + grouped fields + effective values for the UI. Secrets masked.
|
||||
|
||||
All curated knobs are startup-captured, so every key is restart-required;
|
||||
``needs_restart_keys`` lists them for the UI's "restart to apply" banner.
|
||||
|
||||
``runtime_values`` is the live ``ProxyConfig`` tier. It wins over values
|
||||
re-read from ``os.environ``, which can contain profile defaults that were
|
||||
seeded only after CLI options had already been resolved. Those generated
|
||||
defaults belong in ``env_override_exclusions`` so they are not presented as
|
||||
explicit operator exports.
|
||||
"""
|
||||
stored = load()
|
||||
effective = effective_values(stored)
|
||||
environment_effective = effective_values(stored)
|
||||
runtime = _coerce_runtime_values(runtime_values)
|
||||
effective = {**environment_effective, **runtime}
|
||||
fields: list[dict[str, Any]] = []
|
||||
for field in SETTINGS:
|
||||
fields.append(
|
||||
|
|
@ -1003,7 +1047,10 @@ def to_schema() -> dict[str, Any]:
|
|||
"minimum": field.minimum,
|
||||
"maximum": field.maximum,
|
||||
"tier": field.tier,
|
||||
"env_override": bool(os.environ.get(field.env)),
|
||||
"env_override": bool(os.environ.get(field.env))
|
||||
and field.env not in env_override_exclusions,
|
||||
"runtime_override": field.key in runtime
|
||||
and runtime[field.key] != environment_effective[field.key],
|
||||
"value": _mask(field, effective.get(field.key)),
|
||||
"stored": _mask(field, stored.get(field.key)),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from headroom.agent_savings import (
|
|||
apply_agent_savings_profile,
|
||||
get_agent_savings_profile,
|
||||
proxy_pipeline_kwargs,
|
||||
seed_proxy_env_defaults,
|
||||
with_target_savings,
|
||||
)
|
||||
from headroom.cli import wrap as wrap_module
|
||||
|
|
@ -171,6 +172,18 @@ def test_agent_savings_env_defaults_preserve_user_overrides() -> None:
|
|||
assert env["HEADROOM_SMART_CRUSHER_COMPACTION"] == "0"
|
||||
|
||||
|
||||
def test_seed_proxy_env_defaults_reports_only_generated_keys() -> None:
|
||||
env = {"HEADROOM_MODE": "token", "HEADROOM_LOSSLESS": "1"}
|
||||
|
||||
seeded = seed_proxy_env_defaults(env)
|
||||
|
||||
assert "HEADROOM_MODE" not in seeded
|
||||
assert "HEADROOM_LOSSLESS" not in seeded
|
||||
assert "HEADROOM_SAVINGS_PROFILE" in seeded
|
||||
assert env["HEADROOM_MODE"] == "token"
|
||||
assert env["HEADROOM_LOSSLESS"] == "1"
|
||||
|
||||
|
||||
def test_unknown_agent_savings_profile_falls_back_to_default(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -175,6 +175,45 @@ class TestEffectiveValues:
|
|||
monkeypatch.setenv("HEADROOM_SAVINGS_PROFILE", "general")
|
||||
assert settings_store.effective_values()["savings_profile"] == "general"
|
||||
|
||||
def test_runtime_values_override_environment(self, workspace, monkeypatch):
|
||||
_clear_env(monkeypatch)
|
||||
monkeypatch.setenv("HEADROOM_LOSSLESS", "0")
|
||||
|
||||
values = settings_store.effective_values(runtime_values={"lossless": True})
|
||||
|
||||
assert values["lossless"] is True
|
||||
|
||||
def test_schema_marks_runtime_difference_without_mislabeling_seeded_env(
|
||||
self, workspace, monkeypatch
|
||||
):
|
||||
_clear_env(monkeypatch)
|
||||
monkeypatch.setenv("HEADROOM_MODE", "cache")
|
||||
monkeypatch.setenv("HEADROOM_LOSSLESS", "0")
|
||||
|
||||
schema = settings_store.to_schema(
|
||||
runtime_values={"mode": "token", "lossless": True},
|
||||
env_override_exclusions={"HEADROOM_MODE", "HEADROOM_LOSSLESS"},
|
||||
)
|
||||
by_key = {field["key"]: field for field in schema["fields"]}
|
||||
|
||||
assert by_key["mode"]["value"] == "token"
|
||||
assert by_key["mode"]["runtime_override"] is True
|
||||
assert by_key["mode"]["env_override"] is False
|
||||
assert by_key["lossless"]["value"] is True
|
||||
assert by_key["lossless"]["runtime_override"] is True
|
||||
assert by_key["lossless"]["env_override"] is False
|
||||
|
||||
def test_schema_keeps_explicit_environment_override(self, workspace, monkeypatch):
|
||||
_clear_env(monkeypatch)
|
||||
monkeypatch.setenv("HEADROOM_LOSSLESS", "1")
|
||||
|
||||
schema = settings_store.to_schema(runtime_values={"lossless": True})
|
||||
lossless = next(field for field in schema["fields"] if field["key"] == "lossless")
|
||||
|
||||
assert lossless["value"] is True
|
||||
assert lossless["runtime_override"] is False
|
||||
assert lossless["env_override"] is True
|
||||
|
||||
|
||||
class TestSecretMasking:
|
||||
def test_schema_and_stored_mask_secret(self, workspace, monkeypatch):
|
||||
|
|
|
|||
|
|
@ -21,20 +21,24 @@ from headroom import settings_store # noqa: E402
|
|||
from headroom.proxy.server import ProxyConfig, create_app # noqa: E402
|
||||
|
||||
|
||||
def _make_app():
|
||||
return create_app(
|
||||
ProxyConfig(
|
||||
optimize=False,
|
||||
cache_enabled=False,
|
||||
rate_limit_enabled=False,
|
||||
cost_tracking_enabled=False,
|
||||
log_requests=False,
|
||||
ccr_inject_tool=False,
|
||||
ccr_handle_responses=False,
|
||||
ccr_context_tracking=False,
|
||||
image_optimize=False,
|
||||
)
|
||||
)
|
||||
def _make_config(**overrides):
|
||||
values = {
|
||||
"optimize": False,
|
||||
"cache_enabled": False,
|
||||
"rate_limit_enabled": False,
|
||||
"cost_tracking_enabled": False,
|
||||
"log_requests": False,
|
||||
"ccr_inject_tool": False,
|
||||
"ccr_handle_responses": False,
|
||||
"ccr_context_tracking": False,
|
||||
"image_optimize": False,
|
||||
}
|
||||
values.update(overrides)
|
||||
return ProxyConfig(**values)
|
||||
|
||||
|
||||
def _make_app(**overrides):
|
||||
return create_app(_make_config(**overrides))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -77,6 +81,28 @@ class TestSchemaAndRead:
|
|||
assert resp.status_code == 200, resp.text
|
||||
assert resp.json() == {"target_ratio": 0.5, "savings_profile": "balanced"}
|
||||
|
||||
def test_schema_reports_live_config_and_seed_provenance(self, workspace, monkeypatch):
|
||||
monkeypatch.setenv("HEADROOM_MODE", "cache")
|
||||
monkeypatch.setenv("HEADROOM_LOSSLESS", "0")
|
||||
app = _make_app(
|
||||
mode="token",
|
||||
lossless=True,
|
||||
worker_processes=2,
|
||||
profile_seeded_env_keys=frozenset({"HEADROOM_MODE", "HEADROOM_LOSSLESS"}),
|
||||
)
|
||||
client = TestClient(app, base_url="http://127.0.0.1", client=("127.0.0.1", 12345))
|
||||
|
||||
resp = client.get("/settings/schema")
|
||||
|
||||
assert resp.status_code == 200, resp.text
|
||||
by_key = {field["key"]: field for field in resp.json()["fields"]}
|
||||
assert by_key["mode"]["value"] == "token"
|
||||
assert by_key["mode"]["env_override"] is False
|
||||
assert by_key["lossless"]["value"] is True
|
||||
assert by_key["lossless"]["runtime_override"] is True
|
||||
assert by_key["lossless"]["env_override"] is False
|
||||
assert by_key["workers"]["value"] == 2
|
||||
|
||||
|
||||
class TestEndpointsGroup:
|
||||
def test_schema_includes_endpoints_group_and_secret_flags(self, client):
|
||||
|
|
|
|||
|
|
@ -223,7 +223,11 @@ def test_multi_worker_config_round_trip_preserves_typed_rollout(monkeypatch) ->
|
|||
"HEADROOM_DISABLE_FEATURES": "read_maturation",
|
||||
}
|
||||
)
|
||||
original = ProxyConfig(rollout=rollout, worker_processes=2)
|
||||
original = ProxyConfig(
|
||||
rollout=rollout,
|
||||
worker_processes=2,
|
||||
profile_seeded_env_keys=frozenset({"HEADROOM_MODE", "HEADROOM_LOSSLESS"}),
|
||||
)
|
||||
monkeypatch.setenv(_MULTI_WORKER_CONFIG_ENV, json.dumps(_proxy_config_payload(original)))
|
||||
|
||||
restored = _proxy_config_from_env()
|
||||
|
|
@ -233,6 +237,7 @@ def test_multi_worker_config_round_trip_preserves_typed_rollout(monkeypatch) ->
|
|||
assert restored.rollout.is_enabled("proxy_output_shaper") is True
|
||||
assert restored.rollout.is_enabled("read_maturation") is False
|
||||
assert restored.worker_processes == 2
|
||||
assert restored.profile_seeded_env_keys == frozenset({"HEADROOM_MODE", "HEADROOM_LOSSLESS"})
|
||||
|
||||
|
||||
def test_documented_proxy_json_without_internal_snapshot_is_preserved(monkeypatch) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue