336 lines
10 KiB
Python
336 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
# pylint: disable=protected-access,missing-function-docstring
|
|
# pylint: disable=too-few-public-methods,unused-argument
|
|
# pylint: disable=unsubscriptable-object
|
|
"""The static context-window catalog and its wiring into providers.
|
|
|
|
The compaction trigger is ``trigger_ratio * model.context_size``; before the
|
|
catalog every model inherited the 128k ``max_input_length`` default, so a
|
|
1M-context model compacted exactly like a 128k one.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from pineagents.providers.context_windows import (
|
|
DEFAULT_CONTEXT_WINDOW,
|
|
known_context_size,
|
|
resolve_context_window,
|
|
)
|
|
from pineagents.providers.provider import ModelInfo, Provider
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("model_id", "expected"),
|
|
[
|
|
# Qwen family, including the specific over the generic.
|
|
("qwen-long", 10_000_000),
|
|
("qwen3.7-max", 1_000_000),
|
|
("qwen3.7-plus-2026-01-01", 1_000_000),
|
|
("qwen3.6-plus", 1_000_000),
|
|
("qwen-plus-latest", 1_000_000),
|
|
("qwen-plus", 131_072),
|
|
("qwen-turbo-latest", 1_000_000),
|
|
("qwen-turbo", 131_072), # stable alias: conservative bound
|
|
("qwen3-max", 262_144),
|
|
("qwen-max", 131_072),
|
|
# One entry covers the same model across provider id formats.
|
|
("claude-sonnet-4-5", 200_000),
|
|
("anthropic/claude-opus-4.6", 200_000),
|
|
("us.anthropic.claude-haiku-4-5-20251001-v1:0", 200_000),
|
|
# Legacy 100k models must NOT inherit the family's 200k.
|
|
("claude-2.0", 100_000),
|
|
("anthropic/claude-2", 100_000),
|
|
("claude-instant-1.2", 100_000),
|
|
("us.anthropic.claude-instant-v1:0", 100_000),
|
|
("gpt-4.1-mini", 1_047_576),
|
|
("gpt-5-codex", 272_000),
|
|
("o3", 200_000),
|
|
("openai/o3-mini", 200_000),
|
|
# gemini: 1.5-pro (2M) must win over the family catch-all (1M).
|
|
("gemini-1.5-pro", 2_097_152),
|
|
("gemini-2.5-flash", 1_048_576),
|
|
("kimi-k2-thinking", 262_144),
|
|
("glm-5.2", 1_000_000),
|
|
("GLM-5.2[1m]", 1_000_000),
|
|
("zhipu/glm-5.2", 1_000_000),
|
|
# MiniMax: M3 is a 1M-context flagship; the M2.7 series is 204.8k.
|
|
("MiniMax-M3", 1_000_000),
|
|
("MiniMax-M2.7", 204_800),
|
|
("MiniMax-M2.7-highspeed", 204_800),
|
|
],
|
|
)
|
|
def test_known_windows(model_id: str, expected: int):
|
|
assert known_context_size(model_id) == expected
|
|
|
|
|
|
# -- resolve_context_window: the single resolution entry point ---------------
|
|
|
|
|
|
def test_resolve_explicit_config_wins():
|
|
assert (
|
|
resolve_context_window("claude-sonnet-4-5", configured=1_000_000)
|
|
== 1_000_000
|
|
)
|
|
|
|
|
|
def test_resolve_default_valued_config_falls_to_catalog():
|
|
assert (
|
|
resolve_context_window(
|
|
"claude-sonnet-4-5",
|
|
configured=DEFAULT_CONTEXT_WINDOW,
|
|
)
|
|
== 200_000
|
|
)
|
|
|
|
|
|
def test_resolve_explicit_default_valued_config_wins():
|
|
assert (
|
|
resolve_context_window(
|
|
"claude-sonnet-4-5",
|
|
configured=DEFAULT_CONTEXT_WINDOW,
|
|
configured_is_explicit=True,
|
|
)
|
|
== DEFAULT_CONTEXT_WINDOW
|
|
)
|
|
|
|
|
|
def test_resolve_without_catalog_uses_default():
|
|
# Local-serving providers opt out: family windows don't apply.
|
|
assert (
|
|
resolve_context_window("qwen3-coder:30b", use_catalog=False)
|
|
== DEFAULT_CONTEXT_WINDOW
|
|
)
|
|
# But an explicit config still wins.
|
|
assert (
|
|
resolve_context_window(
|
|
"qwen3-coder:30b",
|
|
configured=32_768,
|
|
use_catalog=False,
|
|
)
|
|
== 32_768
|
|
)
|
|
|
|
|
|
def test_resolve_unknown_model_uses_default():
|
|
assert (
|
|
resolve_context_window("totally-unknown-model")
|
|
== DEFAULT_CONTEXT_WINDOW
|
|
)
|
|
|
|
|
|
def test_unknown_model_returns_none():
|
|
assert known_context_size("totally-unknown-model") is None
|
|
assert known_context_size("") is None
|
|
|
|
|
|
def test_short_patterns_require_a_word_boundary():
|
|
# "o3" must not fire inside another token.
|
|
assert known_context_size("gpt-4o3x") is None
|
|
assert known_context_size("foo-bar-o3") == 200_000
|
|
|
|
|
|
class _CatalogProvider:
|
|
"""Minimal stand-in exposing what get_context_size touches.
|
|
|
|
Binds the real ``Provider`` methods without instantiating the abstract
|
|
``Provider`` class.
|
|
"""
|
|
|
|
_info: ModelInfo | None = None
|
|
|
|
def get_model_info(self, model_id):
|
|
return self._info
|
|
|
|
get_context_size = Provider.get_context_size
|
|
_get_context_size = Provider._get_context_size
|
|
_context_catalog_enabled = Provider._context_catalog_enabled
|
|
|
|
|
|
class _MutableCatalogProvider(_CatalogProvider):
|
|
models: list[ModelInfo]
|
|
extra_models: list[ModelInfo]
|
|
|
|
update_model_config = Provider.update_model_config
|
|
|
|
|
|
def test_context_size_prefers_explicit_user_config():
|
|
p = _CatalogProvider()
|
|
p._info = ModelInfo(
|
|
id="claude-sonnet-4-5",
|
|
name="x",
|
|
max_input_length=1_000_000,
|
|
)
|
|
assert p.get_context_size("claude-sonnet-4-5") == 1_000_000
|
|
|
|
|
|
def test_context_size_falls_back_to_catalog_when_default():
|
|
p = _CatalogProvider()
|
|
p._info = ModelInfo(id="claude-sonnet-4-5", name="x") # default 128k
|
|
assert p.get_context_size("claude-sonnet-4-5") == 200_000
|
|
|
|
|
|
def test_context_size_honors_explicit_128k_user_config():
|
|
p = _CatalogProvider()
|
|
p._info = ModelInfo(
|
|
id="claude-sonnet-4-5",
|
|
name="x",
|
|
max_input_length=DEFAULT_CONTEXT_WINDOW,
|
|
max_input_length_configured=True,
|
|
)
|
|
assert p.get_context_size("claude-sonnet-4-5") == DEFAULT_CONTEXT_WINDOW
|
|
|
|
|
|
def test_model_config_update_marks_128k_as_explicit():
|
|
p = _MutableCatalogProvider()
|
|
model = ModelInfo(id="claude-sonnet-4-5", name="x")
|
|
p.models = [model]
|
|
p.extra_models = []
|
|
|
|
assert p.update_model_config(
|
|
model.id,
|
|
{"max_input_length": DEFAULT_CONTEXT_WINDOW},
|
|
)
|
|
assert model.max_input_length_configured is True
|
|
p._info = model
|
|
assert p.get_context_size(model.id) == DEFAULT_CONTEXT_WINDOW
|
|
|
|
|
|
def test_unrelated_model_config_update_keeps_catalog_window():
|
|
p = _MutableCatalogProvider()
|
|
model = ModelInfo(id="claude-sonnet-4-5", name="x")
|
|
p.models = [model]
|
|
p.extra_models = []
|
|
|
|
assert p.update_model_config(model.id, {"max_tokens": 4096})
|
|
assert model.max_input_length_configured is False
|
|
p._info = model
|
|
assert p.get_context_size(model.id) == 200_000
|
|
|
|
|
|
def test_context_size_default_when_unknown_everywhere():
|
|
p = _CatalogProvider()
|
|
p._info = None
|
|
assert (
|
|
p.get_context_size("totally-unknown-model") == DEFAULT_CONTEXT_WINDOW
|
|
)
|
|
|
|
|
|
def test_private_alias_still_works():
|
|
# Providers call self._get_context_size internally; it must stay wired.
|
|
p = _CatalogProvider()
|
|
p._info = ModelInfo(id="claude-sonnet-4-5", name="x")
|
|
assert p._get_context_size("claude-sonnet-4-5") == 200_000
|
|
|
|
|
|
# -- Ollama: local serving opts out of the cloud catalog ----------------------
|
|
|
|
|
|
def _make_ollama(**kw):
|
|
from pineagents.providers.ollama_provider import OllamaProvider
|
|
|
|
return OllamaProvider(
|
|
id="ollama",
|
|
name="Ollama",
|
|
base_url="http://localhost:11434",
|
|
api_key="EMPTY",
|
|
chat_model="OpenAIChatModel",
|
|
**kw,
|
|
)
|
|
|
|
|
|
def test_ollama_skips_catalog():
|
|
"""A local qwen3-coder:30b must NOT get the family's cloud 262k — the
|
|
local serve truncates at num_ctx, so assuming a huge window would
|
|
disable compression while the server drops the prompt head."""
|
|
provider = _make_ollama()
|
|
assert (
|
|
provider.get_context_size("qwen3-coder:30b") == DEFAULT_CONTEXT_WINDOW
|
|
)
|
|
|
|
|
|
def test_ollama_explicit_config_still_wins():
|
|
provider = _make_ollama(
|
|
models=[
|
|
ModelInfo(
|
|
id="qwen3-coder:30b",
|
|
name="qwen3-coder",
|
|
max_input_length=32_768,
|
|
),
|
|
],
|
|
)
|
|
assert provider.get_context_size("qwen3-coder:30b") == 32_768
|
|
|
|
|
|
# -- OpenRouter: the API's context_length is authoritative --------------------
|
|
|
|
|
|
def _openrouter_payload(*rows):
|
|
return SimpleNamespace(data=list(rows))
|
|
|
|
|
|
def test_openrouter_reads_context_length():
|
|
from pineagents.providers.openrouter_provider import OpenRouterProvider
|
|
|
|
payload = _openrouter_payload(
|
|
SimpleNamespace(
|
|
id="anthropic/claude-sonnet-4.5",
|
|
name="Claude Sonnet 4.5",
|
|
pricing=None,
|
|
context_length=1_000_000,
|
|
),
|
|
SimpleNamespace( # absent → field default → catalog resolves
|
|
id="mistralai/mistral-large",
|
|
name="Mistral Large",
|
|
pricing=None,
|
|
),
|
|
SimpleNamespace( # invalid → ignored
|
|
id="foo/bar",
|
|
name="Bar",
|
|
pricing=None,
|
|
context_length="not-a-number",
|
|
),
|
|
)
|
|
models = {
|
|
m.id: m for m in OpenRouterProvider._normalize_models_payload(payload)
|
|
}
|
|
assert models["anthropic/claude-sonnet-4.5"].max_input_length == 1_000_000
|
|
assert (
|
|
models["mistralai/mistral-large"].max_input_length
|
|
== DEFAULT_CONTEXT_WINDOW
|
|
)
|
|
assert models["foo/bar"].max_input_length == DEFAULT_CONTEXT_WINDOW
|
|
|
|
|
|
# -- config display path resolves through the SAME provider method -----------
|
|
|
|
|
|
def test_get_model_max_input_length_uses_provider_resolution(monkeypatch):
|
|
"""/history, usage%%, and daemon status must report the same window the
|
|
compaction trigger uses — the display path delegates to
|
|
Provider.get_context_size instead of reading the raw field."""
|
|
from pineagents.config import config as config_mod
|
|
|
|
class _Provider:
|
|
def get_context_size(self, model_id):
|
|
assert model_id == "claude-sonnet-4-5"
|
|
return 200_000
|
|
|
|
class _Manager:
|
|
def get_provider(self, provider_id):
|
|
return _Provider()
|
|
|
|
monkeypatch.setattr(
|
|
"pineagents.providers.ProviderManager.get_instance",
|
|
staticmethod(_Manager),
|
|
)
|
|
agent_config = SimpleNamespace(
|
|
id="agent-1",
|
|
active_model=SimpleNamespace(
|
|
provider_id="anthropic",
|
|
model="claude-sonnet-4-5",
|
|
),
|
|
)
|
|
assert config_mod.get_model_max_input_length(agent_config) == 200_000
|