fix(config): resolve compact thresholds and base URL regressions
This commit is contained in:
@@ -323,6 +323,8 @@ class AuthManager:
|
||||
last_model: str | None = None,
|
||||
credential_slot: str | None = None,
|
||||
allowed_models: list[str] | None = None,
|
||||
context_window_tokens: int | None = None,
|
||||
auto_compact_threshold_tokens: int | None = None,
|
||||
) -> None:
|
||||
"""Update a profile in-place."""
|
||||
profiles = self.settings.merged_profiles()
|
||||
@@ -341,6 +343,16 @@ class AuthManager:
|
||||
"last_model": last_model if last_model is not None else current.last_model,
|
||||
"credential_slot": credential_slot if credential_slot is not None else current.credential_slot,
|
||||
"allowed_models": allowed_models if allowed_models is not None else current.allowed_models,
|
||||
"context_window_tokens": (
|
||||
context_window_tokens
|
||||
if context_window_tokens is not None
|
||||
else current.context_window_tokens
|
||||
),
|
||||
"auto_compact_threshold_tokens": (
|
||||
auto_compact_threshold_tokens
|
||||
if auto_compact_threshold_tokens is not None
|
||||
else current.auto_compact_threshold_tokens
|
||||
),
|
||||
}
|
||||
profiles[name] = current.model_copy(update=updates)
|
||||
updated = self.settings.model_copy(update={"profiles": profiles})
|
||||
|
||||
@@ -1007,6 +1007,8 @@ def provider_add(
|
||||
base_url: str | None = typer.Option(None, "--base-url", help="Optional base URL"),
|
||||
credential_slot: str | None = typer.Option(None, "--credential-slot", help="Optional profile-specific credential slot"),
|
||||
allowed_models: list[str] | None = typer.Option(None, "--allowed-model", help="Allowed model values for this profile"),
|
||||
context_window_tokens: int | None = typer.Option(None, "--context-window-tokens", help="Optional context window override for auto-compact"),
|
||||
auto_compact_threshold_tokens: int | None = typer.Option(None, "--auto-compact-threshold-tokens", help="Optional explicit auto-compact threshold override"),
|
||||
) -> None:
|
||||
"""Create a provider profile."""
|
||||
from openharness.auth.manager import AuthManager
|
||||
@@ -1025,6 +1027,8 @@ def provider_add(
|
||||
base_url=base_url,
|
||||
credential_slot=credential_slot or _default_credential_slot_for_profile(name, auth_source),
|
||||
allowed_models=allowed_models or ([model] if credential_slot or _default_credential_slot_for_profile(name, auth_source) else []),
|
||||
context_window_tokens=context_window_tokens,
|
||||
auto_compact_threshold_tokens=auto_compact_threshold_tokens,
|
||||
),
|
||||
)
|
||||
print(f"Saved provider profile: {name}", flush=True)
|
||||
@@ -1041,6 +1045,8 @@ def provider_edit(
|
||||
base_url: str | None = typer.Option(None, "--base-url", help="Optional base URL"),
|
||||
credential_slot: str | None = typer.Option(None, "--credential-slot", help="Optional profile-specific credential slot"),
|
||||
allowed_models: list[str] | None = typer.Option(None, "--allowed-model", help="Allowed model values for this profile"),
|
||||
context_window_tokens: int | None = typer.Option(None, "--context-window-tokens", help="Optional context window override for auto-compact"),
|
||||
auto_compact_threshold_tokens: int | None = typer.Option(None, "--auto-compact-threshold-tokens", help="Optional explicit auto-compact threshold override"),
|
||||
) -> None:
|
||||
"""Edit a provider profile."""
|
||||
from openharness.auth.manager import AuthManager
|
||||
@@ -1058,6 +1064,8 @@ def provider_edit(
|
||||
base_url=base_url,
|
||||
credential_slot=credential_slot,
|
||||
allowed_models=allowed_models,
|
||||
context_window_tokens=context_window_tokens,
|
||||
auto_compact_threshold_tokens=auto_compact_threshold_tokens,
|
||||
)
|
||||
except ValueError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
|
||||
@@ -61,6 +61,8 @@ class MemorySettings(BaseModel):
|
||||
enabled: bool = True
|
||||
max_files: int = 5
|
||||
max_entrypoint_lines: int = 200
|
||||
context_window_tokens: int | None = None
|
||||
auto_compact_threshold_tokens: int | None = None
|
||||
|
||||
|
||||
class SandboxNetworkSettings(BaseModel):
|
||||
@@ -114,6 +116,8 @@ class ProviderProfile(BaseModel):
|
||||
last_model: str | None = None
|
||||
credential_slot: str | None = None
|
||||
allowed_models: list[str] = Field(default_factory=list)
|
||||
context_window_tokens: int | None = None
|
||||
auto_compact_threshold_tokens: int | None = None
|
||||
|
||||
@property
|
||||
def resolved_model(self) -> str:
|
||||
@@ -429,6 +433,8 @@ class Settings(BaseModel):
|
||||
max_tokens: int = 16384
|
||||
base_url: str | None = None
|
||||
timeout: float = 30.0
|
||||
context_window_tokens: int | None = None
|
||||
auto_compact_threshold_tokens: int | None = None
|
||||
api_format: str = "anthropic" # "anthropic", "openai", or "copilot"
|
||||
provider: str = ""
|
||||
active_profile: str = "claude-api"
|
||||
@@ -490,6 +496,8 @@ class Settings(BaseModel):
|
||||
"provider": profile.provider,
|
||||
"api_format": profile.api_format,
|
||||
"base_url": profile.base_url,
|
||||
"context_window_tokens": profile.context_window_tokens,
|
||||
"auto_compact_threshold_tokens": profile.auto_compact_threshold_tokens,
|
||||
"model": resolve_model_setting(
|
||||
configured_model,
|
||||
profile.provider,
|
||||
@@ -510,6 +518,16 @@ class Settings(BaseModel):
|
||||
next_provider = (self.provider or "").strip() or profile.provider
|
||||
next_api_format = (self.api_format or "").strip() or profile.api_format
|
||||
next_base_url = self.base_url if self.base_url is not None else profile.base_url
|
||||
next_context_window_tokens = (
|
||||
self.context_window_tokens
|
||||
if self.context_window_tokens is not None
|
||||
else profile.context_window_tokens
|
||||
)
|
||||
next_auto_compact_threshold_tokens = (
|
||||
self.auto_compact_threshold_tokens
|
||||
if self.auto_compact_threshold_tokens is not None
|
||||
else profile.auto_compact_threshold_tokens
|
||||
)
|
||||
flat_model = (self.model or "").strip()
|
||||
resolved_profile_model = resolve_model_setting(
|
||||
(profile.last_model or "").strip() or profile.default_model,
|
||||
@@ -533,6 +551,8 @@ class Settings(BaseModel):
|
||||
"base_url": next_base_url,
|
||||
"auth_source": next_auth_source,
|
||||
"last_model": next_model,
|
||||
"context_window_tokens": next_context_window_tokens,
|
||||
"auto_compact_threshold_tokens": next_auto_compact_threshold_tokens,
|
||||
}
|
||||
)
|
||||
profiles = self.merged_profiles()
|
||||
@@ -698,7 +718,17 @@ class Settings(BaseModel):
|
||||
merged = self.model_copy(update=updates)
|
||||
if not updates:
|
||||
return merged
|
||||
profile_keys = {"model", "base_url", "api_format", "provider", "api_key", "active_profile", "profiles"}
|
||||
profile_keys = {
|
||||
"model",
|
||||
"base_url",
|
||||
"api_format",
|
||||
"provider",
|
||||
"api_key",
|
||||
"active_profile",
|
||||
"profiles",
|
||||
"context_window_tokens",
|
||||
"auto_compact_threshold_tokens",
|
||||
}
|
||||
profile_updates = profile_keys.intersection(updates)
|
||||
if not profile_updates:
|
||||
return merged
|
||||
@@ -736,6 +766,14 @@ def _apply_env_overrides(settings: Settings) -> Settings:
|
||||
if max_turns:
|
||||
updates["max_turns"] = int(max_turns)
|
||||
|
||||
context_window_tokens = os.environ.get("OPENHARNESS_CONTEXT_WINDOW_TOKENS")
|
||||
if context_window_tokens:
|
||||
updates["context_window_tokens"] = int(context_window_tokens)
|
||||
|
||||
auto_compact_threshold_tokens = os.environ.get("OPENHARNESS_AUTO_COMPACT_THRESHOLD_TOKENS")
|
||||
if auto_compact_threshold_tokens:
|
||||
updates["auto_compact_threshold_tokens"] = int(auto_compact_threshold_tokens)
|
||||
|
||||
api_key = os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
||||
if api_key:
|
||||
updates["api_key"] = api_key
|
||||
|
||||
@@ -86,6 +86,8 @@ class QueryContext:
|
||||
model: str
|
||||
system_prompt: str
|
||||
max_tokens: int
|
||||
context_window_tokens: int | None = None
|
||||
auto_compact_threshold_tokens: int | None = None
|
||||
permission_prompt: PermissionPrompt | None = None
|
||||
ask_user_prompt: AskUserPrompt | None = None
|
||||
max_turns: int | None = 200
|
||||
@@ -435,6 +437,8 @@ async def run_query(
|
||||
trigger=trigger,
|
||||
hook_executor=context.hook_executor,
|
||||
carryover_metadata=context.tool_metadata,
|
||||
context_window_tokens=context.context_window_tokens,
|
||||
auto_compact_threshold_tokens=context.auto_compact_threshold_tokens,
|
||||
)
|
||||
)
|
||||
while True:
|
||||
|
||||
@@ -29,6 +29,8 @@ class QueryEngine:
|
||||
model: str,
|
||||
system_prompt: str,
|
||||
max_tokens: int = 4096,
|
||||
context_window_tokens: int | None = None,
|
||||
auto_compact_threshold_tokens: int | None = None,
|
||||
max_turns: int | None = 8,
|
||||
permission_prompt: PermissionPrompt | None = None,
|
||||
ask_user_prompt: AskUserPrompt | None = None,
|
||||
@@ -42,6 +44,8 @@ class QueryEngine:
|
||||
self._model = model
|
||||
self._system_prompt = system_prompt
|
||||
self._max_tokens = max_tokens
|
||||
self._context_window_tokens = context_window_tokens
|
||||
self._auto_compact_threshold_tokens = auto_compact_threshold_tokens
|
||||
self._max_turns = max_turns
|
||||
self._permission_prompt = permission_prompt
|
||||
self._ask_user_prompt = ask_user_prompt
|
||||
@@ -158,6 +162,8 @@ class QueryEngine:
|
||||
model=self._model,
|
||||
system_prompt=self._system_prompt,
|
||||
max_tokens=self._max_tokens,
|
||||
context_window_tokens=self._context_window_tokens,
|
||||
auto_compact_threshold_tokens=self._auto_compact_threshold_tokens,
|
||||
max_turns=self._max_turns,
|
||||
permission_prompt=self._permission_prompt,
|
||||
ask_user_prompt=self._ask_user_prompt,
|
||||
@@ -185,6 +191,8 @@ class QueryEngine:
|
||||
model=self._model,
|
||||
system_prompt=self._system_prompt,
|
||||
max_tokens=self._max_tokens,
|
||||
context_window_tokens=self._context_window_tokens,
|
||||
auto_compact_threshold_tokens=self._auto_compact_threshold_tokens,
|
||||
max_turns=max_turns if max_turns is not None else self._max_turns,
|
||||
permission_prompt=self._permission_prompt,
|
||||
ask_user_prompt=self._ask_user_prompt,
|
||||
|
||||
@@ -918,8 +918,10 @@ class AutoCompactState:
|
||||
# Context window helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_context_window(model: str) -> int:
|
||||
def get_context_window(model: str, *, context_window_tokens: int | None = None) -> int:
|
||||
"""Return the context window size for a model (conservative defaults)."""
|
||||
if context_window_tokens is not None and context_window_tokens > 0:
|
||||
return int(context_window_tokens)
|
||||
m = model.lower()
|
||||
if "opus" in m:
|
||||
return 200_000
|
||||
@@ -931,9 +933,16 @@ def get_context_window(model: str) -> int:
|
||||
return _DEFAULT_CONTEXT_WINDOW
|
||||
|
||||
|
||||
def get_autocompact_threshold(model: str) -> int:
|
||||
def get_autocompact_threshold(
|
||||
model: str,
|
||||
*,
|
||||
context_window_tokens: int | None = None,
|
||||
auto_compact_threshold_tokens: int | None = None,
|
||||
) -> int:
|
||||
"""Calculate the token count at which auto-compact fires."""
|
||||
context_window = get_context_window(model)
|
||||
if auto_compact_threshold_tokens is not None and auto_compact_threshold_tokens > 0:
|
||||
return int(auto_compact_threshold_tokens)
|
||||
context_window = get_context_window(model, context_window_tokens=context_window_tokens)
|
||||
reserved = min(MAX_OUTPUT_TOKENS_FOR_SUMMARY, 20_000)
|
||||
effective = context_window - reserved
|
||||
return effective - AUTOCOMPACT_BUFFER_TOKENS
|
||||
@@ -943,12 +952,19 @@ def should_autocompact(
|
||||
messages: list[ConversationMessage],
|
||||
model: str,
|
||||
state: AutoCompactState,
|
||||
*,
|
||||
context_window_tokens: int | None = None,
|
||||
auto_compact_threshold_tokens: int | None = None,
|
||||
) -> bool:
|
||||
"""Return True when the conversation should be auto-compacted."""
|
||||
if state.consecutive_failures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES:
|
||||
return False
|
||||
token_count = estimate_message_tokens(messages)
|
||||
threshold = get_autocompact_threshold(model)
|
||||
threshold = get_autocompact_threshold(
|
||||
model,
|
||||
context_window_tokens=context_window_tokens,
|
||||
auto_compact_threshold_tokens=auto_compact_threshold_tokens,
|
||||
)
|
||||
return token_count >= threshold
|
||||
|
||||
|
||||
@@ -1330,6 +1346,8 @@ async def auto_compact_if_needed(
|
||||
trigger: CompactTrigger = "auto",
|
||||
hook_executor: HookExecutor | None = None,
|
||||
carryover_metadata: dict[str, Any] | None = None,
|
||||
context_window_tokens: int | None = None,
|
||||
auto_compact_threshold_tokens: int | None = None,
|
||||
) -> tuple[list[ConversationMessage], bool]:
|
||||
"""Check if auto-compact should fire, and if so, compact.
|
||||
|
||||
@@ -1338,7 +1356,13 @@ async def auto_compact_if_needed(
|
||||
Returns:
|
||||
(messages, was_compacted) — if compacted, messages is the new list.
|
||||
"""
|
||||
if not force and not should_autocompact(messages, model, state):
|
||||
if not force and not should_autocompact(
|
||||
messages,
|
||||
model,
|
||||
state,
|
||||
context_window_tokens=context_window_tokens,
|
||||
auto_compact_threshold_tokens=auto_compact_threshold_tokens,
|
||||
):
|
||||
return messages, False
|
||||
|
||||
log.info("Auto-compact triggered (failures=%d)", state.consecutive_failures)
|
||||
@@ -1361,7 +1385,13 @@ async def auto_compact_if_needed(
|
||||
token_count=estimate_message_tokens(messages),
|
||||
details={"tokens_freed": tokens_freed},
|
||||
)
|
||||
if tokens_freed > 0 and not should_autocompact(messages, model, state):
|
||||
if tokens_freed > 0 and not should_autocompact(
|
||||
messages,
|
||||
model,
|
||||
state,
|
||||
context_window_tokens=context_window_tokens,
|
||||
auto_compact_threshold_tokens=auto_compact_threshold_tokens,
|
||||
):
|
||||
log.info("Microcompact freed ~%d tokens, auto-compact no longer needed", tokens_freed)
|
||||
return messages, True
|
||||
|
||||
@@ -1396,7 +1426,13 @@ async def auto_compact_if_needed(
|
||||
token_count=estimate_message_tokens(messages),
|
||||
),
|
||||
)
|
||||
if not force and not should_autocompact(messages, model, state):
|
||||
if not force and not should_autocompact(
|
||||
messages,
|
||||
model,
|
||||
state,
|
||||
context_window_tokens=context_window_tokens,
|
||||
auto_compact_threshold_tokens=auto_compact_threshold_tokens,
|
||||
):
|
||||
return messages, True
|
||||
|
||||
session_memory = try_session_memory_compaction(
|
||||
|
||||
@@ -281,6 +281,11 @@ async def build_runtime(
|
||||
model=settings.model,
|
||||
system_prompt=system_prompt_text,
|
||||
max_tokens=settings.max_tokens,
|
||||
context_window_tokens=settings.context_window_tokens or settings.memory.context_window_tokens,
|
||||
auto_compact_threshold_tokens=(
|
||||
settings.auto_compact_threshold_tokens
|
||||
or settings.memory.auto_compact_threshold_tokens
|
||||
),
|
||||
max_turns=engine_max_turns,
|
||||
permission_prompt=permission_prompt,
|
||||
ask_user_prompt=ask_user_prompt,
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import httpx
|
||||
|
||||
import pytest
|
||||
|
||||
from openharness.api.client import ApiMessageRequest
|
||||
@@ -248,6 +250,43 @@ class _FakeOpenAIClient:
|
||||
self.chat = _FakeChat()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_openai_client_uses_full_base_url_path_for_requests():
|
||||
seen_urls: list[str] = []
|
||||
|
||||
def _handler(request: httpx.Request) -> httpx.Response:
|
||||
seen_urls.append(str(request.url))
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "x",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [],
|
||||
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
||||
},
|
||||
)
|
||||
|
||||
transport = httpx.MockTransport(_handler)
|
||||
http_client = httpx.AsyncClient(transport=transport)
|
||||
client = OpenAICompatibleClient(
|
||||
api_key="test-key",
|
||||
base_url="https://jarodfund.xyz/openai/v1",
|
||||
)
|
||||
client._client._client = http_client
|
||||
|
||||
request = ApiMessageRequest(
|
||||
model="gpt-4o-mini",
|
||||
messages=[ConversationMessage.from_user_text("Explain the codebase")],
|
||||
)
|
||||
events = [event async for event in client.stream_message(request)]
|
||||
|
||||
assert events
|
||||
assert seen_urls == ["https://jarodfund.xyz/openai/v1/chat/completions"]
|
||||
await http_client.aclose()
|
||||
|
||||
|
||||
def test_openai_client_init_normalizes_base_url(monkeypatch):
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
|
||||
@@ -103,6 +103,15 @@ class TestSettings:
|
||||
s = load_settings(path)
|
||||
assert s.base_url == "https://relay.example.com/v1"
|
||||
|
||||
def test_env_overrides_pick_up_compact_threshold_settings(self, tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setenv("OPENHARNESS_CONTEXT_WINDOW_TOKENS", "123456")
|
||||
monkeypatch.setenv("OPENHARNESS_AUTO_COMPACT_THRESHOLD_TOKENS", "120000")
|
||||
path = tmp_path / "settings.json"
|
||||
path.write_text(json.dumps({}))
|
||||
s = load_settings(path)
|
||||
assert s.context_window_tokens == 123456
|
||||
assert s.auto_compact_threshold_tokens == 120000
|
||||
|
||||
def test_anthropic_base_url_takes_precedence_over_openai(self, tmp_path: Path, monkeypatch):
|
||||
"""ANTHROPIC_BASE_URL should take precedence over OPENAI_BASE_URL."""
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
@@ -201,6 +210,27 @@ class TestLoadSaveSettings:
|
||||
assert materialized.api_format == "openai"
|
||||
assert materialized.model == "gpt-5"
|
||||
|
||||
def test_materialize_active_profile_projects_compact_threshold_settings(self):
|
||||
settings = Settings(
|
||||
active_profile="openai-compatible",
|
||||
profiles={
|
||||
"openai-compatible": ProviderProfile(
|
||||
label="OpenAI-Compatible API",
|
||||
provider="openai",
|
||||
api_format="openai",
|
||||
auth_source="openai_api_key",
|
||||
default_model="gpt-5.4",
|
||||
context_window_tokens=100000,
|
||||
auto_compact_threshold_tokens=90000,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
materialized = settings.materialize_active_profile()
|
||||
|
||||
assert materialized.context_window_tokens == 100000
|
||||
assert materialized.auto_compact_threshold_tokens == 90000
|
||||
|
||||
def test_merge_cli_active_profile_does_not_inherit_flat_provider_fields(self):
|
||||
settings = Settings(
|
||||
active_profile="moonshot",
|
||||
@@ -239,6 +269,43 @@ class TestLoadSaveSettings:
|
||||
assert profile.provider == "openai_codex"
|
||||
assert profile.auth_source == "codex_subscription"
|
||||
|
||||
def test_merge_cli_active_profile_keeps_profile_compact_threshold_settings(self):
|
||||
settings = Settings(
|
||||
active_profile="moonshot",
|
||||
context_window_tokens=64000,
|
||||
auto_compact_threshold_tokens=60000,
|
||||
profiles={
|
||||
"moonshot": ProviderProfile(
|
||||
label="Moonshot",
|
||||
provider="moonshot",
|
||||
api_format="openai",
|
||||
auth_source="moonshot_api_key",
|
||||
default_model="kimi-k2.5",
|
||||
last_model="kimi-k2.5",
|
||||
base_url="https://api.moonshot.cn/v1",
|
||||
context_window_tokens=64000,
|
||||
auto_compact_threshold_tokens=60000,
|
||||
),
|
||||
"openai-compatible": ProviderProfile(
|
||||
label="OpenAI-Compatible API",
|
||||
provider="openai",
|
||||
api_format="openai",
|
||||
auth_source="openai_api_key",
|
||||
default_model="gpt-5.4",
|
||||
last_model="gpt-5.4",
|
||||
base_url="https://relay.example.com/v1",
|
||||
context_window_tokens=200000,
|
||||
auto_compact_threshold_tokens=180000,
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
updated = settings.merge_cli_overrides(active_profile="openai-compatible")
|
||||
|
||||
assert updated.base_url == "https://relay.example.com/v1"
|
||||
assert updated.context_window_tokens == 200000
|
||||
assert updated.auto_compact_threshold_tokens == 180000
|
||||
|
||||
def test_claude_profile_materializes_alias_to_concrete_model(self):
|
||||
settings = Settings(
|
||||
active_profile="claude-subscription",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_openharness_package_layout_does_not_shadow_stdlib_types(monkeypatch):
|
||||
shadow_path = str((Path(__file__).resolve().parents[2] / "src" / "openharness"))
|
||||
monkeypatch.syspath_prepend(shadow_path)
|
||||
sys.modules.pop("types", None)
|
||||
|
||||
module = importlib.import_module("types")
|
||||
|
||||
assert getattr(module, "__file__", "").endswith("types.py")
|
||||
assert "site-packages" not in getattr(module, "__file__", "")
|
||||
@@ -22,6 +22,8 @@ from openharness.services import (
|
||||
from openharness.services.compact import (
|
||||
AutoCompactState,
|
||||
auto_compact_if_needed,
|
||||
get_autocompact_threshold,
|
||||
should_autocompact,
|
||||
try_context_collapse,
|
||||
try_session_memory_compaction,
|
||||
)
|
||||
@@ -326,3 +328,22 @@ async def test_auto_compact_if_needed_returns_original_messages_after_timeout(mo
|
||||
|
||||
assert was_compacted is False
|
||||
assert result == messages
|
||||
|
||||
|
||||
def test_get_autocompact_threshold_respects_manual_override():
|
||||
assert get_autocompact_threshold(
|
||||
"claude-sonnet-4-6",
|
||||
auto_compact_threshold_tokens=12345,
|
||||
) == 12345
|
||||
|
||||
|
||||
def test_should_autocompact_uses_custom_context_window():
|
||||
messages = [
|
||||
ConversationMessage(role="user", content=[TextBlock(text="alpha " * 6000)]),
|
||||
]
|
||||
assert should_autocompact(
|
||||
messages,
|
||||
"claude-sonnet-4-6",
|
||||
AutoCompactState(),
|
||||
context_window_tokens=4000,
|
||||
) is True
|
||||
|
||||
Reference in New Issue
Block a user