Skip to content
Closed
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
14 changes: 14 additions & 0 deletions gateway/slash_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1427,6 +1427,20 @@ async def _handle_model_command(self, event: MessageEvent) -> Optional[str]:
force_refresh,
is_session,
) = parse_model_flags(raw_args)

# Context-aware default: messaging platforms (Telegram, Discord, Slack, etc.)
# should default to session scope unless --global is explicitly provided.
# This prevents accidental global config changes when users pick models via
# platform menus, which cannot send --global/--session flags.
source = event.source
is_messaging_platform = (
source.platform is not None
and source.platform != Platform.LOCAL
)
if is_messaging_platform and not is_global_flag and not is_session:
# Treat as if --session was passed (session-scoped by default)
is_session = True

persist_global = resolve_persist_behavior(is_global_flag, is_session)

# --refresh: bust the disk cache so the picker shows live data.
Expand Down
24 changes: 18 additions & 6 deletions tests/gateway/test_model_command_flat_string_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,26 +159,38 @@ async def test_model_global_persists_when_config_has_proper_dict_model(tmp_path,


@pytest.mark.asyncio
async def test_model_no_flag_persists_by_default(tmp_path, monkeypatch):
"""A plain ``/model X`` (no --global) now persists to config.yaml.
async def test_model_no_flag_is_session_scoped_by_default_on_messaging_platforms(tmp_path, monkeypatch):
"""A plain ``/model X`` (no --global) on messaging platforms (Telegram, Discord, Slack, etc.)
defaults to session-scoped, not persisted to config.yaml.

This is the user-facing fix: switching models in one session survives
into the next without re-typing the switch every time.
This prevents accidental global config changes when users pick models via
platform menus, which cannot send --global/--session flags. The in-memory
session override IS applied (the switch worked), but config.yaml is untouched.

See #61458 for context.
"""
cfg_path = _setup_isolated_home(
tmp_path,
monkeypatch,
{"default": "old-model", "provider": "openai-codex"},
)

result = await _make_runner()._handle_model_command(
runner = _make_runner()
result = await runner._handle_model_command(
_make_event("/model gpt-5.5")
)

assert result is not None
assert "gpt-5.5" in result
# The session override IS applied (proves the path didn't no-op).
assert runner._session_model_overrides, "session override should be set"
assert any(
ov.get("model") == "gpt-5.5"
for ov in runner._session_model_overrides.values()
)
# But config.yaml is untouched β€” session-scoped by default on messaging platforms.
written = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert written["model"]["default"] == "gpt-5.5"
assert written["model"]["default"] == "old-model"


@pytest.mark.asyncio
Expand Down
40 changes: 27 additions & 13 deletions tests/gateway/test_model_picker_persist.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,26 +154,40 @@ async def _drive_picker(runner, event):
],
ids=["nested-dict", "flat-string"],
)
async def test_picker_tap_persists_by_default(tmp_path, monkeypatch, seed_model):
"""Tapping a model in the picker (bare /model) persists to config.yaml,
matching the typed ``/model`` default β€” this is the #49176 fix. The written
``model:`` must always end up a nested dict regardless of the seed shape."""
async def test_picker_tap_is_session_scoped_by_default_on_messaging_platforms(
tmp_path, monkeypatch, seed_model
):
"""Tapping a model in the picker (bare /model) is session-scoped by default
on messaging platforms (Telegram, Discord, Slack, etc.).

This prevents accidental global config changes when users pick models via
platform menus, which cannot send --global/--session flags. The in-memory
session override IS applied (the switch worked), but config.yaml is untouched.

See #61458 for context.
"""
adapter = _FakePickerAdapter()
cfg_path = _setup_isolated_home(tmp_path, monkeypatch, seed_model)
runner = _make_runner(adapter)

confirmation = await _drive_picker(_make_runner(adapter), _make_event("/model"))
confirmation = await _drive_picker(runner, _make_event("/model"))

assert confirmation is not None
assert "gpt-5.5" in confirmation
written = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert isinstance(written["model"], dict), (
"model: should be coerced to a dict, got %r" % (written["model"],)
# The session override IS applied (proves the path didn't no-op).
assert runner._session_model_overrides, "session override should be set"
assert any(
ov.get("model") == "gpt-5.5"
for ov in runner._session_model_overrides.values()
)
assert written["model"]["default"] == "gpt-5.5"
assert written["model"]["provider"] == "openrouter"
assert "base_url" not in written["model"]
assert "api_key" not in written["model"]
assert "api_mode" not in written["model"]
# But config.yaml is untouched β€” session-scoped by default on messaging platforms.
written = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
if isinstance(written["model"], dict):
assert written["model"]["default"] == (
"old-model" if isinstance(seed_model, dict) else "deepseek-v4-flash"
)
else:
assert written["model"] == "deepseek-v4-flash"


@pytest.mark.asyncio
Expand Down
Loading