# SPDX-License-Identifier: Apache-2.0 """Tests for profile/template CRUD on ModelSettingsManager.""" import json import pytest from omlx.model_profiles import InvalidProfileNameError from omlx.model_settings import ModelSettings, ModelSettingsManager @pytest.fixture def mgr(tmp_path): return ModelSettingsManager(tmp_path) class TestProfilesCRUD: def test_list_profiles_empty_by_default(self, mgr): assert mgr.list_profiles("model-a") == [] def test_save_and_list_profile(self, mgr): mgr.save_profile( model_id="model-a", name="coding", display_name="Coding", description="det.", settings={"temperature": 0.0, "top_p": 0.95, "is_pinned": True}, ) profiles = mgr.list_profiles("model-a") assert len(profiles) == 1 assert profiles[0]["name"] == "coding" assert profiles[0]["display_name"] == "Coding" assert profiles[0]["api_name"] == "coding" # is_pinned is excluded assert "is_pinned" not in profiles[0]["settings"] assert profiles[0]["settings"]["temperature"] == 0.0 def test_save_profile_rejects_duplicate_name(self, mgr): mgr.save_profile("m", "coding", "Coding", None, {"temperature": 0.0}) with pytest.raises(ValueError, match="already exists"): mgr.save_profile("m", "coding", "Coding", None, {"temperature": 0.1}) def test_save_profile_rejects_invalid_name(self, mgr): with pytest.raises(InvalidProfileNameError): mgr.save_profile("m", "Has Space", "x", None, {}) def test_get_profile_returns_none_for_missing(self, mgr): assert mgr.get_profile("m", "nope") is None def test_update_profile_metadata(self, mgr): mgr.save_profile("m", "coding", "Coding", None, {"temperature": 0.0}) mgr.update_profile( "m", "coding", display_name="Coding v2", description="new desc", settings={"temperature": 0.2}, ) p = mgr.get_profile("m", "coding") assert p["display_name"] == "Coding v2" assert p["api_name"] == "coding" assert p["description"] == "new desc" assert p["settings"]["temperature"] == 0.2 def test_update_profile_api_name(self, mgr): mgr.save_profile("m", "p-abc", "Fast Chat", None, {"temperature": 0.0}) mgr.update_profile("m", "p-abc", api_name="fast-chat-api") p = mgr.get_profile("m", "p-abc") assert p["name"] == "p-abc" assert p["display_name"] == "Fast Chat" assert p["api_name"] == "fast-chat-api" def test_rename_profile(self, mgr): mgr.save_profile("m", "coding", "Coding", None, {"temperature": 0.0}) mgr.update_profile("m", "coding", new_name="coding-v2") assert mgr.get_profile("m", "coding") is None assert mgr.get_profile("m", "coding-v2") is not None def test_rename_to_existing_fails(self, mgr): mgr.save_profile("m", "a", "A", None, {}) mgr.save_profile("m", "b", "B", None, {}) with pytest.raises(ValueError, match="already exists"): mgr.update_profile("m", "a", new_name="b") def test_delete_profile(self, mgr): mgr.save_profile("m", "coding", "Coding", None, {"temperature": 0.0}) assert mgr.delete_profile("m", "coding") is True assert mgr.get_profile("m", "coding") is None def test_delete_missing_returns_false(self, mgr): assert mgr.delete_profile("m", "nope") is False def test_profiles_persist_across_instances(self, tmp_path): m1 = ModelSettingsManager(tmp_path) m1.save_profile("m", "coding", "Coding", None, {"temperature": 0.0}) m2 = ModelSettingsManager(tmp_path) assert m2.get_profile("m", "coding") is not None def test_existing_profiles_get_display_name_based_api_name(self, tmp_path): profiles_file = tmp_path / "model_profiles.json" profiles_file.write_text( json.dumps( { "version": 1, "profiles": { "m": { "p-abc123": { "name": "p-abc123", "display_name": "Fast Chat", "description": None, "created_at": "2026-06-01T00:00:00+00:00", "updated_at": "2026-06-01T00:00:00+00:00", "settings": {"temperature": 0.1}, "source_template": None, } } }, } ) ) manager = ModelSettingsManager(tmp_path) profile = manager.get_profile("m", "p-abc123") assert profile["name"] == "p-abc123" assert profile["display_name"] == "Fast Chat" assert profile["api_name"] == "fast-chat" def test_profile_api_name_migration_dedupes_collisions(self, tmp_path): profiles_file = tmp_path / "model_profiles.json" profiles_file.write_text( json.dumps( { "version": 1, "profiles": { "m": { "p-a": { "name": "p-a", "display_name": "Fast Chat", "created_at": "2026-06-01T00:00:00+00:00", "updated_at": "2026-06-01T00:00:00+00:00", "settings": {}, }, "p-b": { "name": "p-b", "display_name": "Fast Chat", "created_at": "2026-06-01T00:00:00+00:00", "updated_at": "2026-06-01T00:00:00+00:00", "settings": {}, }, } }, } ) ) manager = ModelSettingsManager(tmp_path) api_names = [p["api_name"] for p in manager.list_profiles("m")] assert api_names == ["fast-chat", "fast-chat-2"] def test_profile_api_name_migration_avoids_random_internal_name_fallback( self, tmp_path ): profiles_file = tmp_path / "model_profiles.json" profiles_file.write_text( json.dumps( { "version": 1, "profiles": { "m": { "p-random": { "name": "p-random", "display_name": "思考モード", "created_at": "2026-06-01T00:00:00+00:00", "updated_at": "2026-06-01T00:00:00+00:00", "settings": {}, } } }, } ) ) manager = ModelSettingsManager(tmp_path) profile = manager.get_profile("m", "p-random") assert profile["api_name"] == "profile" def test_rename_cascade_persists_to_disk(self, tmp_path): m1 = ModelSettingsManager(tmp_path) m1.save_profile("m", "coding", "Coding", None, {"temperature": 0.0}) m1.apply_profile("m", "coding") m1.update_profile("m", "coding", new_name="coding-v2") m2 = ModelSettingsManager(tmp_path) assert m2.get_settings("m").active_profile_name == "coding-v2" def test_delete_cascade_persists_to_disk(self, tmp_path): m1 = ModelSettingsManager(tmp_path) m1.save_profile("m", "coding", "Coding", None, {"temperature": 0.0}) m1.apply_profile("m", "coding") m1.delete_profile("m", "coding") m2 = ModelSettingsManager(tmp_path) assert m2.get_settings("m").active_profile_name is None class TestApplyProfile: def test_apply_sets_settings_and_active_name(self, mgr): mgr.save_profile( "m", "coding", "Coding", None, {"temperature": 0.0, "top_p": 0.95} ) applied = mgr.apply_profile("m", "coding") assert applied is not None assert applied.temperature == 0.0 assert applied.top_p == 0.95 assert applied.active_profile_name == "coding" # Persisted again = mgr.get_settings("m") assert again.active_profile_name == "coding" assert again.temperature == 0.0 def test_apply_merges_leaves_unset_fields_alone(self, mgr): # Pre-existing settings pre = ModelSettings(temperature=0.9, top_p=0.5, top_k=40) mgr.set_settings("m", pre) mgr.save_profile("m", "coding", "Coding", None, {"temperature": 0.0}) mgr.apply_profile("m", "coding") s = mgr.get_settings("m") assert s.temperature == 0.0 # overwritten assert s.top_p == 0.5 # preserved assert s.top_k == 40 # preserved def test_apply_missing_profile_returns_none(self, mgr): assert mgr.apply_profile("m", "nope") is None class TestProfileFieldFiltering: def test_save_filters_excluded_fields(self, mgr): mgr.save_profile( "m", "p", "P", None, { "temperature": 0.5, "is_pinned": True, "is_default": True, "display_name": "ignored", "unknown_key": "x", }, ) p = mgr.get_profile("m", "p") assert p["settings"] == {"temperature": 0.5} class TestTemplatesCRUD: def test_list_templates_empty_by_default(self, mgr): # Shipped builtins were retired in favor of the client-side preset # bundle (`omlx/admin/static/omlx_preset.json`); the server's # /api/profile-templates surface now exposes user templates only. assert mgr.list_templates() == [] def test_save_template_universal_only(self, mgr): mgr.save_template( name="coding", display_name="Coding", description="d", settings={ "temperature": 0.0, "turboquant_kv_enabled": True, "is_pinned": True, }, ) t = mgr.get_template("coding") assert t is not None assert t["settings"] == {"temperature": 0.0} def test_save_template_rejects_duplicate(self, mgr): mgr.save_template("coding", "Coding", None, {"temperature": 0.0}) with pytest.raises(ValueError, match="already exists"): mgr.save_template("coding", "Coding", None, {"temperature": 0.1}) def test_save_template_rejects_invalid_name(self, mgr): with pytest.raises(InvalidProfileNameError): mgr.save_template("Has Space", "x", None, {}) def test_update_template(self, mgr): mgr.save_template("coding", "Coding", None, {"temperature": 0.0}) mgr.update_template( "coding", display_name="Coding v2", settings={"temperature": 0.2, "turboquant_kv_enabled": True}, ) t = mgr.get_template("coding") assert t["display_name"] == "Coding v2" assert t["settings"] == {"temperature": 0.2} def test_rename_template(self, mgr): mgr.save_template("coding", "Coding", None, {"temperature": 0.0}) mgr.update_template("coding", new_name="coding-v2") assert mgr.get_template("coding") is None assert mgr.get_template("coding-v2") is not None def test_delete_template(self, mgr): mgr.save_template("coding", "Coding", None, {"temperature": 0.0}) assert mgr.delete_template("coding") is True assert mgr.get_template("coding") is None def test_delete_missing_returns_false(self, mgr): assert mgr.delete_template("nope") is False def test_templates_persist_across_instances(self, tmp_path): m1 = ModelSettingsManager(tmp_path) m1.save_template("coding", "Coding", None, {"temperature": 0.0}) m2 = ModelSettingsManager(tmp_path) assert m2.get_template("coding") is not None class TestTemplatesPersistence: """The on-disk template file holds only user-created entries. Built-in seed templates were retired in favor of the client-side preset bundle (`omlx/admin/static/omlx_preset.json`); /api/profile-templates is now a pure user-store surface.""" def test_no_file_created_when_empty(self, tmp_path): ModelSettingsManager(tmp_path) # With no user templates and no shipped builtins, the manager must # not create the templates file proactively. assert not (tmp_path / "global_templates.json").exists() def test_user_template_persists_only_itself(self, tmp_path): m1 = ModelSettingsManager(tmp_path) m1.save_template("custom", "Custom", None, {"temperature": 0.1}) on_disk = json.loads((tmp_path / "global_templates.json").read_text()) assert set(on_disk["templates"].keys()) == {"custom"} m2 = ModelSettingsManager(tmp_path) names = {t["name"] for t in m2.list_templates()} assert names == {"custom"} # No `is_builtin` is emitted now that builtins are retired; preset # vs user classification lives on the client (preset bundle), not # on this response. assert "is_builtin" not in m2.get_template("custom") # ==================== Exposed profile models ==================== def _save_exposed_profile( manager, model_id="qwen-base", name="thinking", settings=None ): return manager.save_profile( model_id=model_id, name=name, display_name=name.title(), description=None, settings=( settings if settings is not None else {"temperature": 0.6, "enable_thinking": True} ), expose_as_model=True, ) class TestExposedProfilePersistence: def test_save_profile_can_expose_profile_as_model(self, tmp_path): manager = ModelSettingsManager(tmp_path) profile = _save_exposed_profile(manager) assert profile["expose_as_model"] is True reloaded = ModelSettingsManager(tmp_path) exposed = reloaded.list_exposed_profile_models() assert len(exposed) == 1 assert exposed[0]["model_id"] == "qwen-base:thinking" assert exposed[0]["source_model_id"] == "qwen-base" def test_list_profiles_includes_derived_model_id(self, mgr): _save_exposed_profile(mgr) profiles = mgr.list_profiles("qwen-base") assert [p["model_id"] for p in profiles] == ["qwen-base:thinking"] def test_list_profiles_derives_has_engine_fields(self, mgr): """The server classifies engine-construction overrides so UIs can warn on exposure without mirroring the field list.""" _save_exposed_profile(mgr, name="thinking") _save_exposed_profile( mgr, name="accelerated", settings={"temperature": 0.6, "dflash_enabled": True}, ) flags = { p["name"]: p["has_engine_fields"] for p in mgr.list_profiles("qwen-base") } assert flags == {"thinking": False, "accelerated": True} def test_alias_drives_advertised_model_id(self, mgr): """A base-model alias renames the advertised exposed ID, mirroring how /v1/models lists the base model under its alias.""" mgr.set_settings("qwen-base", ModelSettings(model_alias="gpt-4")) _save_exposed_profile(mgr) exposed = mgr.list_exposed_profile_models() assert [p["model_id"] for p in exposed] == ["gpt-4:thinking"] profiles = mgr.list_profiles("qwen-base") assert [p["model_id"] for p in profiles] == ["gpt-4:thinking"] # Both the alias form and the directory-name form resolve. assert mgr.get_exposed_profile_source_model_id("gpt-4:thinking") == "qwen-base" assert ( mgr.get_exposed_profile_source_model_id("qwen-base:thinking") == "qwen-base" ) def test_unexposed_profile_is_not_a_model(self, mgr): mgr.save_profile( model_id="qwen-base", name="thinking", display_name="Thinking", description=None, settings={"temperature": 0.6}, ) assert mgr.list_exposed_profile_models() == [] assert mgr.get_exposed_profile_source_model_id("qwen-base:thinking") is None def test_rename_keeps_exposure_and_preserves_model_id(self, mgr): _save_exposed_profile(mgr) mgr.update_profile("qwen-base", "thinking", new_name="reasoning") exposed = mgr.list_exposed_profile_models() assert [p["model_id"] for p in exposed] == ["qwen-base:thinking"] assert ( mgr.get_exposed_profile_source_model_id("qwen-base:thinking") == "qwen-base" ) def test_api_name_drives_exposed_model_id(self, mgr): mgr.save_profile( model_id="qwen-base", name="p-abc123", display_name="Fast Chat", description=None, settings={"temperature": 0.6}, api_name="fast-chat", expose_as_model=True, ) exposed = mgr.list_exposed_profile_models() assert [p["name"] for p in exposed] == ["p-abc123"] assert [p["api_name"] for p in exposed] == ["fast-chat"] assert [p["model_id"] for p in exposed] == ["qwen-base:fast-chat"] assert mgr.get_exposed_profile_source_model_id("qwen-base:p-abc123") is None assert ( mgr.get_exposed_profile_source_model_id("qwen-base:fast-chat") == "qwen-base" ) def test_update_api_name_updates_exposed_model_id(self, mgr): _save_exposed_profile(mgr) mgr.update_profile("qwen-base", "thinking", api_name="reasoning") exposed = mgr.list_exposed_profile_models() assert [p["model_id"] for p in exposed] == ["qwen-base:reasoning"] assert mgr.get_exposed_profile_source_model_id("qwen-base:thinking") is None assert ( mgr.get_exposed_profile_source_model_id("qwen-base:reasoning") == "qwen-base" ) def test_delete_profile_removes_exposed_model(self, mgr): _save_exposed_profile(mgr) mgr.delete_profile("qwen-base", "thinking") assert mgr.list_exposed_profile_models() == [] assert mgr.get_exposed_profile_source_model_id("qwen-base:thinking") is None def test_exposed_profile_rejects_alias_collision(self, mgr): mgr.set_settings("other", ModelSettings(model_alias="qwen-base:thinking")) with pytest.raises(ValueError, match="conflicts with model alias"): _save_exposed_profile(mgr) class TestExposedProfileRequestSettings: def test_request_settings_overlay_base_without_mutating_it(self, mgr): mgr.set_settings("qwen-base", ModelSettings(temperature=0.2, top_p=0.8)) _save_exposed_profile(mgr) settings = mgr.get_settings_for_request( "qwen-base:thinking", resolved_model_id="qwen-base", ) assert settings.temperature == 0.6 assert settings.top_p == 0.8 assert settings.enable_thinking is True assert mgr.get_settings("qwen-base").temperature == 0.2 assert mgr.get_settings("qwen-base").active_profile_name is None def test_request_settings_handle_provider_prefix(self, mgr): _save_exposed_profile(mgr) settings = mgr.get_settings_for_request( "omlx/qwen-base:thinking", resolved_model_id="qwen-base", ) assert settings.temperature == 0.6 assert ( mgr.get_exposed_profile_source_model_id("omlx/qwen-base:thinking") == "qwen-base" ) def test_engine_construction_fields_are_not_overlaid(self, mgr): """Exposed profiles overlay request-time fields only — engine knobs in the profile stay at the base model's values.""" mgr.set_settings("qwen-base", ModelSettings(temperature=0.2)) _save_exposed_profile( mgr, settings={"temperature": 0.9, "dflash_enabled": True}, ) settings = mgr.get_settings_for_request( "qwen-base:thinking", resolved_model_id="qwen-base", ) assert settings.temperature == 0.9 assert settings.dflash_enabled is False def test_runtime_settings_include_engine_fields_without_mutating_base(self, mgr): mgr.set_settings( "qwen-base", ModelSettings(temperature=0.2, mtp_enabled=False), ) _save_exposed_profile( mgr, settings={"temperature": 0.9, "mtp_enabled": True}, ) runtime = mgr.get_exposed_profile_runtime_settings_for_request( "qwen-base:thinking" ) assert runtime is not None source_model_id, settings = runtime assert source_model_id == "qwen-base" assert settings.temperature == 0.9 assert settings.mtp_enabled is True assert mgr.get_settings("qwen-base").temperature == 0.2 assert mgr.get_settings("qwen-base").mtp_enabled is False def test_request_settings_fall_back_to_resolved_physical_model(self, mgr): mgr.set_settings( "qwen-base", ModelSettings(temperature=0.2, model_alias="my-alias") ) settings = mgr.get_settings_for_request( "my-alias", resolved_model_id="qwen-base", ) assert settings.temperature == 0.2 def test_alias_form_of_exposed_profile_serves_overlay(self, mgr): """Requests to : get the profile overlay, same as the directory-name form.""" mgr.set_settings( "qwen-base", ModelSettings(temperature=0.2, model_alias="gpt-4") ) _save_exposed_profile(mgr) settings = mgr.get_settings_for_request( "gpt-4:thinking", resolved_model_id="qwen-base", ) assert settings.temperature == 0.6 assert settings.enable_thinking is True