Files
jundot--omlx/tests/test_model_settings_profiles.py
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

591 lines
22 KiB
Python

# 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 <alias>:<profile> 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