feat: add telegram /model and /reasoning overrides (#147)
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
from takopi.model import ResumeToken
|
||||
from takopi.runners.claude import ClaudeRunner
|
||||
from takopi.runners.codex import CodexRunner
|
||||
from takopi.runners.opencode import OpenCodeRunner, OpenCodeStreamState
|
||||
from takopi.runners.pi import ENGINE as PI_ENGINE, PiRunner, PiStreamState
|
||||
from takopi.runners.run_options import EngineRunOptions, apply_run_options
|
||||
|
||||
|
||||
def test_codex_run_options_override_model_and_reasoning() -> None:
|
||||
runner = CodexRunner(codex_cmd="codex", extra_args=["-c", "notify=[]"])
|
||||
state = runner.new_state("hi", None)
|
||||
with apply_run_options(EngineRunOptions(model="gpt-4.1-mini", reasoning="low")):
|
||||
args = runner.build_args("hi", None, state=state)
|
||||
|
||||
assert args == [
|
||||
"-c",
|
||||
"notify=[]",
|
||||
"--model",
|
||||
"gpt-4.1-mini",
|
||||
"-c",
|
||||
"model_reasoning_effort=low",
|
||||
"exec",
|
||||
"--json",
|
||||
"--skip-git-repo-check",
|
||||
"--color=never",
|
||||
"-",
|
||||
]
|
||||
|
||||
|
||||
def test_claude_run_options_override_model() -> None:
|
||||
runner = ClaudeRunner(claude_cmd="claude", model="claude-sonnet")
|
||||
with apply_run_options(EngineRunOptions(model="claude-opus")):
|
||||
args = runner.build_args("hi", None, state=None)
|
||||
|
||||
assert "--model" in args
|
||||
model_idx = args.index("--model") + 1
|
||||
assert args[model_idx] == "claude-opus"
|
||||
|
||||
|
||||
def test_opencode_run_options_override_model() -> None:
|
||||
runner = OpenCodeRunner(opencode_cmd="opencode", model="claude-sonnet")
|
||||
state = OpenCodeStreamState()
|
||||
with apply_run_options(EngineRunOptions(model="gpt-4o-mini")):
|
||||
args = runner.build_args("hi", None, state=state)
|
||||
|
||||
assert "--model" in args
|
||||
model_idx = args.index("--model") + 1
|
||||
assert args[model_idx] == "gpt-4o-mini"
|
||||
|
||||
|
||||
def test_pi_run_options_override_model() -> None:
|
||||
runner = PiRunner(extra_args=[], model="pi-default", provider=None)
|
||||
state = PiStreamState(resume=ResumeToken(engine=PI_ENGINE, value="sess.jsonl"))
|
||||
with apply_run_options(EngineRunOptions(model="pi-override")):
|
||||
args = runner.build_args("hi", None, state=state)
|
||||
|
||||
assert "--model" in args
|
||||
model_idx = args.index("--model") + 1
|
||||
assert args[model_idx] == "pi-override"
|
||||
@@ -8,6 +8,8 @@ import pytest
|
||||
from takopi import commands, plugins
|
||||
from takopi.telegram.commands.executor import _CaptureTransport, _run_engine
|
||||
from takopi.telegram.commands.file_transfer import _handle_file_get, _handle_file_put
|
||||
from takopi.telegram.commands.model import _handle_model_command
|
||||
from takopi.telegram.commands.reasoning import _handle_reasoning_command
|
||||
from takopi.telegram.commands.topics import _handle_topic_command
|
||||
import takopi.telegram.loop as telegram_loop
|
||||
import takopi.telegram.topics as telegram_topics
|
||||
@@ -38,6 +40,7 @@ from takopi.telegram.render import MAX_BODY_CHARS
|
||||
from takopi.telegram.topic_state import TopicStateStore, resolve_state_path
|
||||
from takopi.telegram.chat_sessions import ChatSessionStore, resolve_sessions_path
|
||||
from takopi.telegram.chat_prefs import ChatPrefsStore, resolve_prefs_path
|
||||
from takopi.telegram.engine_overrides import EngineOverrides
|
||||
from takopi.context import RunContext
|
||||
from takopi.config import ProjectConfig, ProjectsConfig
|
||||
from takopi.runner_bridge import ExecBridgeConfig, RunningTask
|
||||
@@ -1348,6 +1351,230 @@ async def test_topic_command_recreates_stale_topic(tmp_path: Path) -> None:
|
||||
assert snapshot.context == RunContext(project="takopi", branch="master")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_model_command_show_reports_overrides(tmp_path: Path) -> None:
|
||||
transport = _FakeTransport()
|
||||
cfg = _make_cfg(transport)
|
||||
cfg = replace(cfg, topics=TelegramTopicsSettings(enabled=True, scope="main"))
|
||||
chat_prefs = ChatPrefsStore(tmp_path / "telegram_chat_prefs_state.json")
|
||||
topic_store = TopicStateStore(tmp_path / "telegram_topics_state.json")
|
||||
await chat_prefs.set_engine_override(
|
||||
123,
|
||||
CODEX_ENGINE,
|
||||
EngineOverrides(model="gpt-4.1-mini", reasoning=None),
|
||||
)
|
||||
await topic_store.set_engine_override(
|
||||
123,
|
||||
77,
|
||||
CODEX_ENGINE,
|
||||
EngineOverrides(model="gpt-4.1", reasoning=None),
|
||||
)
|
||||
msg = TelegramIncomingMessage(
|
||||
transport="telegram",
|
||||
chat_id=123,
|
||||
message_id=10,
|
||||
text="/model",
|
||||
reply_to_message_id=None,
|
||||
reply_to_text=None,
|
||||
sender_id=123,
|
||||
thread_id=77,
|
||||
)
|
||||
|
||||
await _handle_model_command(
|
||||
cfg,
|
||||
msg,
|
||||
"",
|
||||
ambient_context=None,
|
||||
topic_store=topic_store,
|
||||
chat_prefs=chat_prefs,
|
||||
resolved_scope="main",
|
||||
scope_chat_ids=frozenset({123}),
|
||||
)
|
||||
|
||||
text = transport.send_calls[-1]["message"].text
|
||||
assert "engine: codex (global default)" in text
|
||||
assert "model: gpt-4.1 (topic override)" in text
|
||||
assert "defaults: topic: gpt-4.1, chat: gpt-4.1-mini" in text
|
||||
assert "available engines: codex" in text
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_model_command_set_and_clear_chat_override(tmp_path: Path) -> None:
|
||||
transport = _FakeTransport()
|
||||
cfg = _make_cfg(transport)
|
||||
chat_prefs = ChatPrefsStore(tmp_path / "telegram_chat_prefs_state.json")
|
||||
await chat_prefs.set_engine_override(
|
||||
123,
|
||||
CODEX_ENGINE,
|
||||
EngineOverrides(model=None, reasoning="low"),
|
||||
)
|
||||
msg = TelegramIncomingMessage(
|
||||
transport="telegram",
|
||||
chat_id=123,
|
||||
message_id=10,
|
||||
text="/model set gpt-4.1-mini",
|
||||
reply_to_message_id=None,
|
||||
reply_to_text=None,
|
||||
sender_id=456,
|
||||
chat_type="supergroup",
|
||||
)
|
||||
|
||||
await _handle_model_command(
|
||||
cfg,
|
||||
msg,
|
||||
"set gpt-4.1-mini",
|
||||
ambient_context=None,
|
||||
topic_store=None,
|
||||
chat_prefs=chat_prefs,
|
||||
)
|
||||
|
||||
override = await chat_prefs.get_engine_override(123, CODEX_ENGINE)
|
||||
assert override is not None
|
||||
assert override.model == "gpt-4.1-mini"
|
||||
assert override.reasoning == "low"
|
||||
assert (
|
||||
"chat model override set to gpt-4.1-mini for codex."
|
||||
in transport.send_calls[-1]["message"].text
|
||||
)
|
||||
|
||||
msg_clear = replace(
|
||||
msg,
|
||||
message_id=11,
|
||||
text="/model clear codex",
|
||||
)
|
||||
await _handle_model_command(
|
||||
cfg,
|
||||
msg_clear,
|
||||
"clear codex",
|
||||
ambient_context=None,
|
||||
topic_store=None,
|
||||
chat_prefs=chat_prefs,
|
||||
)
|
||||
|
||||
override = await chat_prefs.get_engine_override(123, CODEX_ENGINE)
|
||||
assert override is not None
|
||||
assert override.model is None
|
||||
assert override.reasoning == "low"
|
||||
assert "chat model override cleared." in transport.send_calls[-1]["message"].text
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_reasoning_command_set_and_clear_topic_override(tmp_path: Path) -> None:
|
||||
transport = _FakeTransport()
|
||||
cfg = _make_cfg(transport)
|
||||
cfg = replace(cfg, topics=TelegramTopicsSettings(enabled=True, scope="main"))
|
||||
topic_store = TopicStateStore(tmp_path / "telegram_topics_state.json")
|
||||
await topic_store.set_engine_override(
|
||||
123,
|
||||
77,
|
||||
CODEX_ENGINE,
|
||||
EngineOverrides(model="gpt-4.1-mini", reasoning=None),
|
||||
)
|
||||
msg = TelegramIncomingMessage(
|
||||
transport="telegram",
|
||||
chat_id=123,
|
||||
message_id=10,
|
||||
text="/reasoning set High",
|
||||
reply_to_message_id=None,
|
||||
reply_to_text=None,
|
||||
sender_id=456,
|
||||
chat_type="supergroup",
|
||||
thread_id=77,
|
||||
)
|
||||
|
||||
await _handle_reasoning_command(
|
||||
cfg,
|
||||
msg,
|
||||
"set High",
|
||||
ambient_context=None,
|
||||
topic_store=topic_store,
|
||||
chat_prefs=None,
|
||||
resolved_scope="main",
|
||||
scope_chat_ids=frozenset({123}),
|
||||
)
|
||||
|
||||
override = await topic_store.get_engine_override(123, 77, CODEX_ENGINE)
|
||||
assert override is not None
|
||||
assert override.model == "gpt-4.1-mini"
|
||||
assert override.reasoning == "high"
|
||||
assert (
|
||||
"topic reasoning override set to high for codex."
|
||||
in transport.send_calls[-1]["message"].text
|
||||
)
|
||||
|
||||
msg_clear = replace(
|
||||
msg,
|
||||
message_id=11,
|
||||
text="/reasoning clear",
|
||||
)
|
||||
await _handle_reasoning_command(
|
||||
cfg,
|
||||
msg_clear,
|
||||
"clear",
|
||||
ambient_context=None,
|
||||
topic_store=topic_store,
|
||||
chat_prefs=None,
|
||||
resolved_scope="main",
|
||||
scope_chat_ids=frozenset({123}),
|
||||
)
|
||||
|
||||
override = await topic_store.get_engine_override(123, 77, CODEX_ENGINE)
|
||||
assert override is not None
|
||||
assert override.model == "gpt-4.1-mini"
|
||||
assert override.reasoning is None
|
||||
assert (
|
||||
"topic reasoning override cleared (using chat default)."
|
||||
in transport.send_calls[-1]["message"].text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_reasoning_command_show_reports_overrides(tmp_path: Path) -> None:
|
||||
transport = _FakeTransport()
|
||||
cfg = _make_cfg(transport)
|
||||
cfg = replace(cfg, topics=TelegramTopicsSettings(enabled=True, scope="main"))
|
||||
chat_prefs = ChatPrefsStore(tmp_path / "telegram_chat_prefs_state.json")
|
||||
topic_store = TopicStateStore(tmp_path / "telegram_topics_state.json")
|
||||
await chat_prefs.set_engine_override(
|
||||
123,
|
||||
CODEX_ENGINE,
|
||||
EngineOverrides(model=None, reasoning="low"),
|
||||
)
|
||||
await topic_store.set_engine_override(
|
||||
123,
|
||||
88,
|
||||
CODEX_ENGINE,
|
||||
EngineOverrides(model=None, reasoning="high"),
|
||||
)
|
||||
msg = TelegramIncomingMessage(
|
||||
transport="telegram",
|
||||
chat_id=123,
|
||||
message_id=10,
|
||||
text="/reasoning",
|
||||
reply_to_message_id=None,
|
||||
reply_to_text=None,
|
||||
sender_id=123,
|
||||
thread_id=88,
|
||||
)
|
||||
|
||||
await _handle_reasoning_command(
|
||||
cfg,
|
||||
msg,
|
||||
"",
|
||||
ambient_context=None,
|
||||
topic_store=topic_store,
|
||||
chat_prefs=chat_prefs,
|
||||
resolved_scope="main",
|
||||
scope_chat_ids=frozenset({123}),
|
||||
)
|
||||
|
||||
text = transport.send_calls[-1]["message"].text
|
||||
assert "engine: codex (global default)" in text
|
||||
assert "reasoning: high (topic override)" in text
|
||||
assert "defaults: topic: high, chat: low" in text
|
||||
assert "available levels: minimal, low, medium, high, xhigh" in text
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_send_with_resume_waits_for_token() -> None:
|
||||
transport = _FakeTransport()
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import pytest
|
||||
|
||||
from takopi.telegram.chat_prefs import ChatPrefsStore
|
||||
from takopi.telegram.engine_overrides import (
|
||||
EngineOverrides,
|
||||
merge_overrides,
|
||||
resolve_override_value,
|
||||
)
|
||||
from takopi.telegram.topic_state import TopicStateStore
|
||||
|
||||
|
||||
def test_merge_overrides_prefers_topic_values() -> None:
|
||||
topic = EngineOverrides(model=None, reasoning="high")
|
||||
chat = EngineOverrides(model="gpt-4.1-mini", reasoning=None)
|
||||
merged = merge_overrides(topic, chat)
|
||||
|
||||
assert merged is not None
|
||||
assert merged.model == "gpt-4.1-mini"
|
||||
assert merged.reasoning == "high"
|
||||
|
||||
|
||||
def test_resolve_override_value_tracks_sources() -> None:
|
||||
topic = EngineOverrides(model="gpt-4.1", reasoning=None)
|
||||
chat = EngineOverrides(model="gpt-4.1-mini", reasoning="low")
|
||||
resolution = resolve_override_value(
|
||||
topic_override=topic,
|
||||
chat_override=chat,
|
||||
field="model",
|
||||
)
|
||||
|
||||
assert resolution.value == "gpt-4.1"
|
||||
assert resolution.source == "topic_override"
|
||||
assert resolution.topic_value == "gpt-4.1"
|
||||
assert resolution.chat_value == "gpt-4.1-mini"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_chat_prefs_engine_overrides_roundtrip(tmp_path) -> None:
|
||||
path = tmp_path / "telegram_chat_prefs_state.json"
|
||||
store = ChatPrefsStore(path)
|
||||
await store.set_engine_override(
|
||||
123,
|
||||
"codex",
|
||||
EngineOverrides(model="gpt-4.1-mini", reasoning="low"),
|
||||
)
|
||||
|
||||
override = await store.get_engine_override(123, "codex")
|
||||
assert override is not None
|
||||
assert override.model == "gpt-4.1-mini"
|
||||
assert override.reasoning == "low"
|
||||
|
||||
store2 = ChatPrefsStore(path)
|
||||
override2 = await store2.get_engine_override(123, "codex")
|
||||
assert override2 is not None
|
||||
assert override2.model == "gpt-4.1-mini"
|
||||
assert override2.reasoning == "low"
|
||||
|
||||
await store2.set_engine_override(
|
||||
123,
|
||||
"codex",
|
||||
EngineOverrides(model=None, reasoning="low"),
|
||||
)
|
||||
override3 = await store2.get_engine_override(123, "codex")
|
||||
assert override3 is not None
|
||||
assert override3.model is None
|
||||
assert override3.reasoning == "low"
|
||||
|
||||
await store2.set_engine_override(
|
||||
123,
|
||||
"codex",
|
||||
EngineOverrides(model=None, reasoning=None),
|
||||
)
|
||||
override4 = await store2.get_engine_override(123, "codex")
|
||||
assert override4 is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_topic_state_engine_overrides_roundtrip(tmp_path) -> None:
|
||||
path = tmp_path / "telegram_topics_state.json"
|
||||
store = TopicStateStore(path)
|
||||
await store.set_engine_override(
|
||||
1,
|
||||
10,
|
||||
"codex",
|
||||
EngineOverrides(model="gpt-4.1", reasoning="medium"),
|
||||
)
|
||||
|
||||
override = await store.get_engine_override(1, 10, "codex")
|
||||
assert override is not None
|
||||
assert override.model == "gpt-4.1"
|
||||
assert override.reasoning == "medium"
|
||||
|
||||
store2 = TopicStateStore(path)
|
||||
override2 = await store2.get_engine_override(1, 10, "codex")
|
||||
assert override2 is not None
|
||||
assert override2.model == "gpt-4.1"
|
||||
assert override2.reasoning == "medium"
|
||||
Reference in New Issue
Block a user