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
65 changes: 65 additions & 0 deletions src/tmq/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,69 @@ def append_event(
return event_id


# ── Model resolution ──────────────────────────────────────────────

# Map model → provider from the known fleet configuration.
# See institutional memory: data-driven-model-selection.md.
# Refines tms/lib/tms/events.py:MODEL_TO_PROVIDER (tms#73).
# tmq#7's resolver is more aggressive: known fleet models always
# resolve to their mapped provider, overriding any caller-supplied
# provider (intentional divergence from tms#73, which preserves
# explicit providers even for known models).
MODEL_TO_PROVIDER = {
"deepseek-v4-pro": "deepseek",
"MiniMax-M3": "minimax",
"MiniMax-M3.5": "minimax",
"glm-5.2": "zai",
}


def _resolve_default_model() -> tuple[str, str]:
"""Resolve the actually-served model from pi's settings file.

When tmq dispatches pi without --provider/--model flags, the agent
uses the default from ~/.pi/agent/settings.json. We resolve this
at event-write time so the dispatch record carries the real model,
not an empty string. Returns (provider, model) tuple.
"""
import json as _json

settings_path = os.path.expanduser("~/.pi/agent/settings.json")
try:
with open(settings_path) as f:
settings = _json.load(f)
except (FileNotFoundError, _json.JSONDecodeError, OSError):
return ("", "")

model = settings.get("defaultModel", "")
if not model:
return ("", "")

provider = MODEL_TO_PROVIDER.get(model, "unknown")
return (provider, model)


def _resolve_dispatch_model(provider: str, model: str) -> tuple[str, str]:
"""Resolve event provenance from explicit flags, then pi defaults.

Refines tms#73: an explicit model determines its provider from the
fleet map, and the mapping is authoritative — a known model always
resolves to its mapped provider, overriding any caller-supplied
provider (tms#73 preserves explicit providers even for known models;
this divergence is intentional so stale defaults can't leak).
For unknown models, the explicit provider (if any) is preserved.
Defaults are consulted only when the invocation supplies no model.
"""
if model:
resolved = MODEL_TO_PROVIDER.get(model)
if resolved:
return (resolved, model)
return (provider or "unknown", model)

resolved_provider, resolved_model = _resolve_default_model()
return (provider or resolved_provider, resolved_model)


def log_dispatch(
*,
repo: str,
Expand All @@ -185,6 +248,7 @@ def log_dispatch(
so the metrics rows distinguish them; aoe_id is the 8-char prefix
the bash tool computed via ``aoe session show --json``.
"""
provider, model = _resolve_dispatch_model(provider, model)
return append_event(
event_type="dispatch",
repo=repo,
Expand Down Expand Up @@ -215,6 +279,7 @@ def log_dispatch_failed(
"""The failure path: tms#39 (cc root refusal), aoe add failed, aoe
session start failed, etc. ``reason`` is the actionable hint.
"""
provider, model = _resolve_dispatch_model(provider, model)
return append_event(
event_type="dispatch_failed",
repo=repo,
Expand Down
174 changes: 174 additions & 0 deletions tests/test_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""Tests for src/tmq/events.py — provider/model provenance.

Mirrors tms#73's parameterized tests for _resolve_dispatch_model.
Tests are ported to the bogocat-tmq API surface (log_dispatch /
log_dispatch_failed) but assert the same resolution contract.

Database isolation: events.py writes to postgres via psycopg2.
Tests patch psycopg2.connect to capture the INSERT SQL + params
so we assert what the row would contain without needing a real DB.
"""

from unittest.mock import MagicMock, patch

import pytest


# ── Shared fixtures ───────────────────────────────────────────────


def _capture_insert(mock_connect):
"""Set up mock psycopg2 so we can capture the INSERT params."""
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_conn.__enter__.return_value = mock_conn
mock_conn.cursor.return_value.__enter__.return_value = mock_cursor
mock_connect.return_value = mock_conn
return mock_cursor


def _last_insert_params(mock_cursor):
"""Return the params tuple from the most recent execute() call."""
call_args = mock_cursor.execute.call_args
if call_args is None:
return None
return call_args[0][1] # args[0] = (sql, params)


# ── Tests ─────────────────────────────────────────────────────────


class TestLogDispatchProviderResolution:
"""Provider/model provenance for log_dispatch (tms#83)."""

def test_explicit_model_derives_provider(self):
"""A --model without --provider derives provider from the fleet map."""
with patch("psycopg2.connect") as mock_connect:
cursor = _capture_insert(mock_connect)
from tmq.events import log_dispatch

log_dispatch(
repo="tms", issue=83, agent="pi",
provider="", model="MiniMax-M3",
dispatch_type="fix", cwd="/tmp/wt", session_name="fix-tms#83",
)
params = _last_insert_params(cursor)
# provider column (index 7 in INSERT: id,created_at,event_ts,event_type,
# repo,issue,agent,provider,model,...)
assert params[7] == "minimax"
assert params[8] == "MiniMax-M3"

def test_explicit_provider_and_model_preserved(self):
"""Explicit provider + model passed through unchanged."""
with patch("psycopg2.connect") as mock_connect:
cursor = _capture_insert(mock_connect)
from tmq.events import log_dispatch

log_dispatch(
repo="tms", issue=83, agent="pi",
provider="custom-p", model="custom-m",
dispatch_type="fix", cwd="/tmp/wt", session_name="fix-tms#83",
)
params = _last_insert_params(cursor)
assert params[7] == "custom-p"
assert params[8] == "custom-m"

@pytest.mark.parametrize(
("model", "expected_provider"),
[
("deepseek-v4-pro", "deepseek"),
("MiniMax-M3", "minimax"),
("MiniMax-M3.5", "minimax"),
("glm-5.2", "zai"),
],
)
def test_each_known_model_gets_correct_provider(self, model, expected_provider):
"""Every fleet model maps to its known provider (tms#73 ports)."""
with patch("psycopg2.connect") as mock_connect:
cursor = _capture_insert(mock_connect)
from tmq.events import log_dispatch

log_dispatch(
repo="tms", issue=83, agent="pi",
provider="", model=model,
dispatch_type="fix", cwd="/tmp/wt", session_name="fix-tms#83",
)
params = _last_insert_params(cursor)
assert params[7] == expected_provider
assert params[8] == model

def test_explicit_model_ignores_default_provider(self):
"""An explicit --model must NOT combine with a stale default provider.

If the caller sets provider='stale-default' (from settings) but no
explicit --model, defaults are consulted. But with an explicit model,
the provider is derived from the model, not the stale default.
"""
with patch("psycopg2.connect") as mock_connect:
cursor = _capture_insert(mock_connect)
from tmq.events import log_dispatch

# Simulate: settings.pi_provider='stale-default' but args.model='MiniMax-M3'
log_dispatch(
repo="tms", issue=83, agent="pi",
provider="stale-default", model="MiniMax-M3",
dispatch_type="fix", cwd="/tmp/wt", session_name="fix-tms#83",
)
params = _last_insert_params(cursor)
assert params[7] == "minimax", (
"explicit model should override stale default provider"
)
assert params[8] == "MiniMax-M3"

def test_empty_both_resolves_from_defaults(self):
"""Empty provider + model resolves from pi settings defaults."""
with patch("psycopg2.connect") as mock_connect:
cursor = _capture_insert(mock_connect)
with patch("tmq.events._resolve_default_model",
return_value=("deepseek", "deepseek-v4-pro")):
from tmq.events import log_dispatch

log_dispatch(
repo="tms", issue=83, agent="pi",
provider="", model="",
dispatch_type="fix", cwd="/tmp/wt", session_name="fix-tms#83",
)
params = _last_insert_params(cursor)
assert params[7] == "deepseek"
assert params[8] == "deepseek-v4-pro"


class TestLogDispatchFailedProviderResolution:
"""Provider/model provenance for log_dispatch_failed (tms#83)."""

def test_explicit_model_derives_provider_on_failure(self):
"""dispatch_failed must also resolve provider from model."""
with patch("psycopg2.connect") as mock_connect:
cursor = _capture_insert(mock_connect)
from tmq.events import log_dispatch_failed

log_dispatch_failed(
repo="tms", issue=83, agent="pi",
provider="", model="MiniMax-M3.5",
dispatch_type="fix", reason="aoe add failed",
)
params = _last_insert_params(cursor)
assert params[7] == "minimax"
assert params[8] == "MiniMax-M3.5"

def test_failed_uses_defaults_when_empty(self):
"""Failed dispatch with no model resolves from pi settings."""
with patch("psycopg2.connect") as mock_connect:
cursor = _capture_insert(mock_connect)
with patch("tmq.events._resolve_default_model",
return_value=("zai", "glm-5.2")):
from tmq.events import log_dispatch_failed

log_dispatch_failed(
repo="tms", issue=83, agent="pi",
provider="", model="",
dispatch_type="fix", reason="timeout",
)
params = _last_insert_params(cursor)
assert params[7] == "zai"
assert params[8] == "glm-5.2"
Loading