Skip to content
Closed
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
3 changes: 3 additions & 0 deletions agent/context_compressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ def update_model(
api_key: str = "",
provider: str = "",
api_mode: str = "",
threshold_percent: float | None = None,
) -> None:
"""Update model info after a model switch or fallback activation."""
self.model = model
Expand All @@ -361,6 +362,8 @@ def update_model(
self.provider = provider
self.api_mode = api_mode
self.context_length = context_length
if threshold_percent is not None:
self.threshold_percent = threshold_percent
self.threshold_tokens = max(
int(context_length * self.threshold_percent),
MINIMUM_CONTEXT_LENGTH,
Expand Down
4 changes: 4 additions & 0 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2372,6 +2372,7 @@ def switch_model(self, new_model, new_provider, api_key='', base_url='', api_mod
api_key=getattr(self, "api_key", ""),
provider=self.provider,
api_mode=self.api_mode,
threshold_percent=self.context_compressor.threshold_percent,
)

# ── Invalidate cached system prompt so it rebuilds next turn ──
Expand All @@ -2394,6 +2395,7 @@ def switch_model(self, new_model, new_provider, api_key='', base_url='', api_mod
"compressor_provider": getattr(_cc, "provider", self.provider) if _cc else self.provider,
"compressor_context_length": _cc.context_length if _cc else 0,
"compressor_threshold_tokens": _cc.threshold_tokens if _cc else 0,
"compressor_threshold_percent": _cc.threshold_percent if _cc else 0.50,
}
if api_mode == "anthropic_messages":
self._primary_runtime.update({
Expand Down Expand Up @@ -7611,6 +7613,7 @@ def _try_activate_fallback(self, reason: "FailoverReason | None" = None) -> bool
base_url=self.base_url,
api_key=getattr(self, "api_key", ""),
provider=self.provider,
threshold_percent=self.context_compressor.threshold_percent,
)

self._emit_status(
Expand Down Expand Up @@ -7690,6 +7693,7 @@ def _restore_primary_runtime(self) -> bool:
base_url=rt["compressor_base_url"],
api_key=rt["compressor_api_key"],
provider=rt["compressor_provider"],
threshold_percent=rt.get("compressor_threshold_percent"),
)

# ── Reset fallback chain for the new turn ──
Expand Down
71 changes: 71 additions & 0 deletions tests/agent/test_update_model_threshold_percent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Tests for ContextCompressor.update_model() threshold_percent handling.

Regression test for #18617: threshold_percent not updated on model switch.
"""
import pytest
from unittest.mock import patch


def _make_compressor(model="model-a", threshold_percent=0.70, context_length=200000):
"""Create a ContextCompressor with controlled context length."""
from agent.context_compressor import ContextCompressor
with patch("agent.context_compressor.get_model_context_length", return_value=context_length):
return ContextCompressor(
model=model,
threshold_percent=threshold_percent,
quiet_mode=True,
)


class TestUpdateModelThresholdPercent:
"""update_model() should accept and apply threshold_percent."""

def test_update_model_preserves_threshold_percent_when_not_passed(self):
"""When threshold_percent is not passed, the existing value is kept."""
cc = _make_compressor(threshold_percent=0.70)
assert cc.threshold_percent == 0.70

cc.update_model(model="model-b", context_length=100000)
# threshold_percent unchanged
assert cc.threshold_percent == 0.70
# threshold_tokens recalculated from new context_length
# 100000 * 0.70 = 70000 > MINIMUM_CONTEXT_LENGTH (64000)
assert cc.threshold_tokens == 70000

def test_update_model_accepts_threshold_percent_parameter(self):
"""update_model() should accept threshold_percent and update it."""
cc = _make_compressor(threshold_percent=0.70)
cc.update_model(model="model-b", context_length=100000, threshold_percent=0.50)
assert cc.threshold_percent == 0.50

def test_update_model_recalculates_threshold_tokens_with_new_percent(self):
"""After threshold_percent update, threshold_tokens uses the new value."""
cc = _make_compressor(threshold_percent=0.70, context_length=200000)
assert cc.threshold_percent == 0.70
# 200000 * 0.70 = 140000
assert cc.threshold_tokens == 140000

cc.update_model(model="model-b", context_length=100000, threshold_percent=0.80)
# 100000 * 0.80 = 80000 > MINIMUM_CONTEXT_LENGTH
assert cc.threshold_percent == 0.80
assert cc.threshold_tokens == 80000

def test_update_model_preserves_percent_on_context_change_only(self):
"""Model switch with same percent should still recalculate tokens."""
cc = _make_compressor(threshold_percent=0.60, context_length=200000)
assert cc.threshold_tokens == 120000

cc.update_model(model="model-b", context_length=300000)
assert cc.threshold_percent == 0.60
# 300000 * 0.60 = 180000
assert cc.threshold_tokens == 180000

def test_threshold_floor_still_applies(self):
"""MINIMUM_CONTEXT_LENGTH floor should still apply after percent update."""
cc = _make_compressor(threshold_percent=0.70, context_length=200000)
# Switch to small model with low percent that would be below floor
cc.update_model(model="small-model", context_length=80000, threshold_percent=0.50)
# 80000 * 0.50 = 40000 < 64000 (MINIMUM_CONTEXT_LENGTH)
# Floor should kick in
from agent.model_metadata import MINIMUM_CONTEXT_LENGTH
assert cc.threshold_tokens == MINIMUM_CONTEXT_LENGTH