From 82191b111389f7fd5c2834467ef5c31b333bb6b4 Mon Sep 17 00:00:00 2001 From: Zn <175176992+loading2332@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:13:02 +0800 Subject: [PATCH] fix(config): preserve model overlays across equivalent ids Merge partial metadata updates by model identity and remove equivalent overlay keys while preserving the effective row precedence. Co-authored-by: Codex --- raven/config/update_providers.py | 8 +++- tests/test_config_update_providers.py | 54 +++++++++++++++++++++++++++ tests/test_rpc_model.py | 22 +++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/raven/config/update_providers.py b/raven/config/update_providers.py index c6bf7ba96..9845d5569 100644 --- a/raven/config/update_providers.py +++ b/raven/config/update_providers.py @@ -967,7 +967,13 @@ def _apply(_text: str | None) -> tuple[str | None, list[str]]: # has no parameter to be restated with at all, so a wholesale write # loses it every time. Re-adding corrects the tags it names and # leaves the rest of the row alone. - merged = {**(overlays.get(model) or {}), **overlay} + previous = {} + target = merge_key(name, model) + for stored in list(overlays): + if merge_key(name, stored) == target: + # Match the reader's last-row precedence for existing aliases. + previous = overlays.pop(stored) or {} + merged = {**previous, **overlay} # An empty string is how a caller clears a field it can otherwise # only restate; None would be "unstated" and leave it alone. merged = {k: v for k, v in merged.items() if v != ""} diff --git a/tests/test_config_update_providers.py b/tests/test_config_update_providers.py index b2184d605..b9be155ef 100644 --- a/tests/test_config_update_providers.py +++ b/tests/test_config_update_providers.py @@ -1074,6 +1074,60 @@ def test_an_overlay_merges_into_the_row_it_already_had(cfg_path: Path) -> None: assert row["capabilities"] == ["reasoning"] +@pytest.mark.parametrize( + ("existing", "incoming"), + [("team-model", "hosted-vllm/team-model"), ("hosted-vllm/team-model", "team-model")], +) +@pytest.mark.parametrize( + "patch", + [{"capabilities": ["reasoning"]}, {"label": ""}, {"capabilities": []}], +) +def test_overlay_updates_match_model_identity(cfg_path: Path, existing: str, incoming: str, patch: dict) -> None: + original = {"label": "Team model", "description": "Keep this description", "capabilities": ["function-call"]} + add_provider_model("hosted_vllm", existing, overlay=original, config_path=cfg_path) + add_provider_model("hosted_vllm", "other-model", overlay={"label": "Other model"}, config_path=cfg_path) + + models = add_provider_model("hosted_vllm", incoming, overlay=patch, config_path=cfg_path) + + overlays = _read(cfg_path)["providers"]["hosted_vllm"]["modelOverlay"] + assert models == [existing, "other-model"] + assert set(overlays) == {incoming, "other-model"} + for field, value in (original | patch).items(): + assert overlays[incoming][field] == value + assert overlays["other-model"]["label"] == "Other model" + + +@pytest.mark.parametrize("clear", [False, True]) +def test_overlay_alias_cleanup_preserves_effective_row(cfg_path: Path, clear: bool) -> None: + cfg_path.write_text( + json.dumps( + { + "providers": { + "hosted_vllm": { + "models": ["team-model"], + "modelOverlay": { + "hosted-vllm/team-model": {"label": "Stale name", "description": "Stale description"}, + "team-model": {"label": "Current name", "description": ""}, + }, + } + } + } + ) + ) + + patch = {"label": ""} if clear else {"capabilities": ["reasoning"]} + add_provider_model("hosted_vllm", "hosted-vllm/team-model", overlay=patch, config_path=cfg_path) + + overlays = _read(cfg_path)["providers"]["hosted_vllm"]["modelOverlay"] + if clear: + assert overlays == {} + else: + assert list(overlays) == ["hosted-vllm/team-model"] + assert overlays["hosted-vllm/team-model"]["label"] == "Current name" + assert overlays["hosted-vllm/team-model"]["description"] == "" + assert overlays["hosted-vllm/team-model"]["capabilities"] == ["reasoning"] + + def test_re_adding_corrects_the_field_it_names(cfg_path: Path) -> None: """Merging must not turn a correction into an append: re-adding is how a person fixes a tag they got wrong, so a restated field replaces.""" diff --git a/tests/test_rpc_model.py b/tests/test_rpc_model.py index 89efe2f06..6f00dfeeb 100644 --- a/tests/test_rpc_model.py +++ b/tests/test_rpc_model.py @@ -2042,3 +2042,25 @@ def no_everos(name: str, *args, **kwargs): monkeypatch.setattr(builtins, "__import__", no_everos) result = await model_save_key({"slug": "deepseek", "api_key": "new-key"}) assert result["provider"]["slug"] == "deepseek" + + +async def test_add_model_preserves_metadata_under_a_bare_id(fake_home: Path) -> None: + _write_config( + fake_home, + { + "providers": { + "hosted_vllm": { + "apiBase": "http://localhost:9999/v1", + "models": ["team-model"], + "modelOverlay": {"team-model": {"label": "Team model", "description": "Keep this description"}}, + } + } + }, + ) + + result = await model_add_model({"slug": "hosted_vllm", "model": "team-model", "capabilities": ["reasoning"]}) + + row = result["provider"]["model_labels"]["team-model"] + assert row["label"] == "Team model" + assert row["description"] == "Keep this description" + assert row["capabilities"] == ["reasoning"]