Move configuration to YAML, and make the tuning knobs configurable #43

Merged
dongar merged 4 commits from yaml-config into main 2026-09-23 23:49:44 +00:00
5 changed files with 52 additions and 24 deletions
Showing only changes of commit 58b439088f - Show all commits

No files matched your search

-4
View File
2
@@ -12,7 +12,3 @@ NISSE_GITEA_TOKEN=your-scoped-service-account-token
# Where the config file is, if not ./nisse-dev.yaml. Where this file is, if # Where the config file is, if not ./nisse-dev.yaml. Where this file is, if
# not ./.env, is NISSE_ENV, which has to be set in the real environment. # not ./.env, is NISSE_ENV, which has to be set in the real environment.
# NISSE_CONFIG=~/nisse-demo-workspace/nisse-dev.yaml # NISSE_CONFIG=~/nisse-demo-workspace/nisse-dev.yaml
# Opt-in for the capability matrix: models to measure, comma separated.
# Spends real tokens across every model listed. See docs/Tuning-Guide.md.
# NISSE_CAPABILITY_MODELS=local-fast,local-coder
+2 -2
View File
@@ -248,8 +248,8 @@ change one thing at a time.
1. **Model and quantization.** Start with whether a candidate holds the 1. **Model and quantization.** Start with whether a candidate holds the
tool-call contract at all: tool-call contract at all:
NISSE_CAPABILITY_MODELS=alias-a,alias-b \ uv run pytest -m integration tests/integration/test_model_capability.py \
uv run pytest -m integration tests/integration/test_model_capability.py --capability-models=alias-a,alias-b
A model that falls back to prose instead of calling tools is disqualified, A model that falls back to prose instead of calling tools is disqualified,
however good its benchmarks look. Before comparing aliases, check what however good its benchmarks look. Before comparing aliases, check what
+20
View File
@@ -0,0 +1,20 @@
"""Command-line options for the repository-level test suites.
Declared here rather than beside the tests that use them: pytest only reads
options from conftest files it loads at startup, and this directory is one
of the configured test paths.
"""
import pytest
def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption(
"--capability-models",
default="",
help=(
"comma-separated LiteLLM aliases to measure with the capability "
"matrix (tests/integration/test_model_capability.py). Spends real "
"tokens on every model listed; the matrix skips when this is empty."
),
)
+2 -2
View File
@@ -21,8 +21,8 @@ from nisse_dev.llm import AnthropicPassthroughClient, LiteLLMClient
REPO = Path(__file__).resolve().parents[2] REPO = Path(__file__).resolve().parents[2]
TEST_CONFIG = Path(__file__).parent / "nisse-test.yaml" TEST_CONFIG = Path(__file__).parent / "nisse-test.yaml"
# At import, so per-run switches such as NISSE_CAPABILITY_MODELS can be set # The secrets nisse-test.yaml refers to, for local runs. CI sets them as real
# in .env too. Real environment variables win over the file. # environment variables, which win over the file.
load_env_file(REPO / ".env") load_env_file(REPO / ".env")
+28 -16
View File
@@ -9,11 +9,15 @@ What it measures is the thing that actually breaks: whether a model can hold
the tool-call contract our loops depend on. llama3.2 could not — it returned the tool-call contract our loops depend on. llama3.2 could not — it returned
`{` as an entire plan — and no amount of prompt work fixed it. `{` as an entire plan — and no amount of prompt work fixed it.
Opt in with NISSE_CAPABILITY_MODELS, comma separated. Skipped otherwise, Opt in by naming the models to measure, for one run:
because it spends real tokens across several models.
"""
import os uv run pytest -m integration tests/integration/test_model_capability.py \
--capability-models=local-fast,local-coder
Skipped otherwise, because it spends real tokens across every model listed.
Which models to measure is a question for a particular run, so it is an
argument, not configuration.
"""
import pytest import pytest
@@ -31,15 +35,28 @@ from nisse_dev.llm import AnthropicPassthroughClient, LiteLLMClient, LLMError
from nisse_dev.planner import PLANNER_TOOLS from nisse_dev.planner import PLANNER_TOOLS
from nisse_dev.tools import EXECUTOR_TOOLS from nisse_dev.tools import EXECUTOR_TOOLS
SENTINEL = "NISSE_CAPABILITY_MODELS" pytestmark = pytest.mark.integration
MODELS = [m.strip() for m in os.environ.get(SENTINEL, "").split(",") if m.strip()]
pytestmark = [
pytest.mark.integration, def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
pytest.mark.skipif( if "model" not in metafunc.fixturenames:
not MODELS, reason=f"set {SENTINEL} to a comma-separated model list" return
raw = metafunc.config.getoption("--capability-models")
models = [m.strip() for m in raw.split(",") if m.strip()]
if models:
metafunc.parametrize("model", models)
else:
metafunc.parametrize(
"model",
[
pytest.param(
None,
marks=pytest.mark.skip(
reason="no models to measure; pass --capability-models"
), ),
] )
],
)
@pytest.fixture @pytest.fixture
@@ -60,7 +77,6 @@ def user(text: str) -> list[StoredEvent]:
return [StoredEvent(id=1, ts="t", event=UserMessage(text=text, source="probe"))] return [StoredEvent(id=1, ts="t", event=UserMessage(text=text, source="probe"))]
@pytest.mark.parametrize("model", MODELS)
def test_model_answers_at_all(model: str, catalog: ModelCatalog, routes) -> None: def test_model_answers_at_all(model: str, catalog: ModelCatalog, routes) -> None:
normalized, passthrough = routes normalized, passthrough = routes
client = select_client(model, normalized, passthrough, catalog) client = select_client(model, normalized, passthrough, catalog)
@@ -71,7 +87,6 @@ def test_model_answers_at_all(model: str, catalog: ModelCatalog, routes) -> None
assert texts, f"{model} produced no text: {produced!r}" assert texts, f"{model} produced no text: {produced!r}"
@pytest.mark.parametrize("model", MODELS)
def test_model_holds_the_planner_contract( def test_model_holds_the_planner_contract(
model: str, catalog: ModelCatalog, routes model: str, catalog: ModelCatalog, routes
) -> None: ) -> None:
@@ -100,7 +115,6 @@ def test_model_holds_the_planner_contract(
) )
@pytest.mark.parametrize("model", MODELS)
def test_model_round_trips_a_tool_result( def test_model_round_trips_a_tool_result(
model: str, catalog: ModelCatalog, routes model: str, catalog: ModelCatalog, routes
) -> None: ) -> None:
@@ -130,7 +144,6 @@ def test_model_round_trips_a_tool_result(
assert follow_up, f"{model} produced nothing after its tool result" assert follow_up, f"{model} produced nothing after its tool result"
@pytest.mark.parametrize("model", MODELS)
def test_model_holds_the_prompt_level_json_contract( def test_model_holds_the_prompt_level_json_contract(
model: str, catalog: ModelCatalog, routes model: str, catalog: ModelCatalog, routes
) -> None: ) -> None:
@@ -154,7 +167,6 @@ def test_model_holds_the_prompt_level_json_contract(
) )
@pytest.mark.parametrize("model", MODELS)
def test_route_selection_matches_the_model( def test_route_selection_matches_the_model(
model: str, catalog: ModelCatalog, routes model: str, catalog: ModelCatalog, routes
) -> None: ) -> None: