Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 72 additions & 7 deletions agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,58 @@ def _normalized_custom_base_url(value: Any) -> str:
return value.strip().rstrip("/")


def _resolve_per_model_threshold(
per_model: Any, model: Any
) -> Optional[float]:
"""Resolve a per-model compression threshold from the config map.

``per_model`` is the ``compression.per_model_threshold`` config value -- a
mapping of model-id -> threshold fraction. ``model`` is the active model id.

Lookup is exact-match first, then case-insensitive. The threshold is the
fraction of the model's context window that must be consumed before
compression triggers; valid values are in (0, 1]. Out-of-range or
non-numeric values are ignored (return ``None``) so resolution falls
through to the built-in default and then the global config threshold.

Returns the matched fraction as a float, or ``None`` when there is no
usable per-model entry for ``model``.
"""
if not isinstance(per_model, dict) or not per_model:
return None
if not isinstance(model, str) or not model:
return None

raw = per_model.get(model)
if raw is None:
lowered = model.lower()
for key, value in per_model.items():
if isinstance(key, str) and key.lower() == lowered:
raw = value
break
if raw is None:
return None

try:
threshold = float(raw)
except (TypeError, ValueError):
logger.warning(
"Ignoring non-numeric compression.per_model_threshold for %r: %r",
model,
raw,
)
return None
if not (0.0 < threshold <= 1.0):
logger.warning(
"Ignoring out-of-range compression.per_model_threshold for %r: %r "
"(must be in (0, 1])",
model,
threshold,
)
return None
return threshold


def _custom_provider_model_matches(agent_model: str, entry: Dict[str, Any]) -> bool:
provider_model = str(entry.get("model", "") or "").strip().lower()
if not provider_model:
Expand Down Expand Up @@ -1227,13 +1279,26 @@ def init_agent(
if not isinstance(_compression_cfg, dict):
_compression_cfg = {}
compression_threshold = float(_compression_cfg.get("threshold", 0.50))
try:
from agent.auxiliary_client import _compression_threshold_for_model as _cthresh_fn
_model_cthresh = _cthresh_fn(agent.model)
if _model_cthresh is not None:
compression_threshold = _model_cthresh
except Exception:
pass
# Per-model threshold override. Resolution order (first hit wins):
# 1. config ``compression.per_model_threshold`` map (user-tunable)
# 2. built-in per-model default (``_compression_threshold_for_model``)
# 3. global ``compression.threshold`` (set above)
# 4. hardcoded 0.50 default (the ``.get`` fallback above)
# The threshold is a fraction of the model's context window in (0, 1];
# the absolute trigger (context_length * threshold) is already model-aware
# and re-derived on every model switch in ContextCompressor.update_model.
_per_model = _compression_cfg.get("per_model_threshold")
_cfg_model_thresh = _resolve_per_model_threshold(_per_model, agent.model)
if _cfg_model_thresh is not None:
compression_threshold = _cfg_model_thresh
else:
try:
from agent.auxiliary_client import _compression_threshold_for_model as _cthresh_fn
_model_cthresh = _cthresh_fn(agent.model)
if _model_cthresh is not None:
compression_threshold = _model_cthresh
except Exception:
pass
compression_enabled = str(_compression_cfg.get("enabled", True)).lower() in {"true", "1", "yes"}
compression_target_ratio = float(_compression_cfg.get("target_ratio", 0.20))
compression_protect_last = int(_compression_cfg.get("protect_last_n", 20))
Expand Down
80 changes: 80 additions & 0 deletions tests/agent/test_per_model_compression_threshold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""Tests for the per-model compression-threshold config resolver.

``compression.per_model_threshold`` is an optional config map of
model-id -> threshold fraction. It overrides the global
``compression.threshold`` for the matched model, and takes precedence over
the built-in ``_compression_threshold_for_model`` default.

The resolver (``agent.agent_init._resolve_per_model_threshold``) must:
- match exact model id, then case-insensitively
- accept fractions in (0, 1] only; ignore out-of-range / non-numeric
- return None for empty/missing maps, non-dict input, or no-match, so the
caller falls through to the built-in default and then the global config.
"""

from __future__ import annotations

import pytest

from agent.agent_init import _resolve_per_model_threshold


def test_exact_match_wins() -> None:
per_model = {"claude-opus-4-8": 0.85, "gpt-5.5": 0.7}
assert _resolve_per_model_threshold(per_model, "claude-opus-4-8") == 0.85
assert _resolve_per_model_threshold(per_model, "gpt-5.5") == 0.7


def test_case_insensitive_match() -> None:
per_model = {"Claude-Opus-4-8": 0.85}
assert _resolve_per_model_threshold(per_model, "claude-opus-4-8") == 0.85
assert _resolve_per_model_threshold(per_model, "CLAUDE-OPUS-4-8") == 0.85


def test_exact_match_preferred_over_case_fold() -> None:
# Two keys that fold to the same lowercase: the exact one must win.
per_model = {"claude-opus-4-8": 0.6, "Claude-Opus-4-8": 0.9}
assert _resolve_per_model_threshold(per_model, "claude-opus-4-8") == 0.6


def test_no_match_returns_none() -> None:
per_model = {"gpt-5.5": 0.7}
assert _resolve_per_model_threshold(per_model, "claude-opus-4-8") is None


@pytest.mark.parametrize("value", [1.0, 0.5, 0.01])
def test_boundary_values_accepted(value: float) -> None:
assert _resolve_per_model_threshold({"m": value}, "m") == value


@pytest.mark.parametrize("value", [0.0, -0.1, 1.1, 2.0, 100])
def test_out_of_range_ignored(value) -> None:
# Out of (0, 1] -> None so resolution falls through.
assert _resolve_per_model_threshold({"m": value}, "m") is None


@pytest.mark.parametrize("value", ["high", None, [], {}, object()])
def test_non_numeric_ignored(value) -> None:
assert _resolve_per_model_threshold({"m": value}, "m") is None


def test_numeric_string_is_coerced() -> None:
# YAML can hand us a quoted number; float() coercion accepts it.
assert _resolve_per_model_threshold({"m": "0.8"}, "m") == 0.8


@pytest.mark.parametrize("per_model", [None, {}, [], "not-a-dict", 42])
def test_empty_or_non_dict_map_returns_none(per_model) -> None:
assert _resolve_per_model_threshold(per_model, "claude-opus-4-8") is None


@pytest.mark.parametrize("model", [None, "", 123, []])
def test_invalid_model_returns_none(model) -> None:
assert _resolve_per_model_threshold({"claude-opus-4-8": 0.85}, model) is None


def test_non_string_keys_skipped_gracefully() -> None:
# A non-string key must not crash the case-insensitive fallback scan.
per_model = {123: 0.9, "claude-opus-4-8": 0.85}
assert _resolve_per_model_threshold(per_model, "claude-opus-4-8") == 0.85
assert _resolve_per_model_threshold(per_model, "other-model") is None
Loading