mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix(config): preserve custom-provider models maps and metadata through v11->v12 migration (#40573)
Salvaged from #40410; cleaned up, re-verified against main, tests added. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
This commit is contained in:
parent
89929553b4
commit
887295ba54
3 changed files with 133 additions and 16 deletions
|
|
@ -3793,6 +3793,42 @@ def _normalize_custom_provider_entry(
|
|||
return normalized
|
||||
|
||||
|
||||
def _custom_provider_entry_to_provider_config(
|
||||
entry: Any,
|
||||
*,
|
||||
provider_key: str = "",
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Translate a legacy custom provider entry to the v12 providers shape."""
|
||||
normalized = _normalize_custom_provider_entry(
|
||||
dict(entry) if isinstance(entry, dict) else entry,
|
||||
provider_key=provider_key,
|
||||
)
|
||||
if normalized is None:
|
||||
return None
|
||||
|
||||
provider_entry: Dict[str, Any] = {"api": normalized["base_url"]}
|
||||
|
||||
for field in (
|
||||
"name",
|
||||
"api_key",
|
||||
"key_env",
|
||||
"models",
|
||||
"context_length",
|
||||
"rate_limit_delay",
|
||||
"discover_models",
|
||||
"extra_body",
|
||||
):
|
||||
if field in normalized:
|
||||
provider_entry[field] = normalized[field]
|
||||
|
||||
if "model" in normalized:
|
||||
provider_entry["default_model"] = normalized["model"]
|
||||
if "api_mode" in normalized:
|
||||
provider_entry["transport"] = normalized["api_mode"]
|
||||
|
||||
return provider_entry
|
||||
|
||||
|
||||
def providers_dict_to_custom_providers(providers_dict: Any) -> List[Dict[str, Any]]:
|
||||
"""Normalize ``providers`` config entries into the legacy custom-provider shape."""
|
||||
if not isinstance(providers_dict, dict):
|
||||
|
|
@ -4299,8 +4335,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
|
|||
if not isinstance(entry, dict):
|
||||
continue
|
||||
old_name = entry.get("name", "")
|
||||
old_url = entry.get("base_url", "") or entry.get("url", "") or ""
|
||||
old_key = entry.get("api_key", "")
|
||||
old_url = entry.get("base_url", "") or entry.get("url", "") or entry.get("api", "") or ""
|
||||
if not old_url:
|
||||
continue # skip entries with no URL
|
||||
|
||||
|
|
@ -4320,20 +4355,22 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
|
|||
key = f"endpoint-{migrated_count}"
|
||||
|
||||
# Don't overwrite existing entries
|
||||
if key in providers_dict:
|
||||
key = f"{key}-{migrated_count}"
|
||||
base_key = key
|
||||
suffix = migrated_count
|
||||
while key in providers_dict:
|
||||
key = f"{base_key}-{suffix}"
|
||||
suffix += 1
|
||||
|
||||
new_entry = {"api": old_url}
|
||||
if old_name:
|
||||
new_entry["name"] = old_name
|
||||
if old_key and old_key not in {"no-key", "no-key-required", ""}:
|
||||
new_entry["api_key"] = old_key
|
||||
|
||||
# Carry over model and api_mode if present
|
||||
if entry.get("model"):
|
||||
new_entry["default_model"] = entry["model"]
|
||||
if entry.get("api_mode"):
|
||||
new_entry["transport"] = entry["api_mode"]
|
||||
new_entry = _custom_provider_entry_to_provider_config(
|
||||
entry,
|
||||
provider_key=key,
|
||||
)
|
||||
if new_entry is None:
|
||||
continue
|
||||
if not old_name:
|
||||
new_entry.pop("name", None)
|
||||
if new_entry.get("api_key") in {"no-key", "no-key-required", ""}:
|
||||
new_entry.pop("api_key", None)
|
||||
|
||||
providers_dict[key] = new_entry
|
||||
migrated_count += 1
|
||||
|
|
|
|||
|
|
@ -706,6 +706,71 @@ class TestCustomProviderCompatibility:
|
|||
# custom_providers removed by migration — runtime reads via compat layer
|
||||
assert "custom_providers" not in raw
|
||||
|
||||
def test_v11_upgrade_preserves_custom_provider_model_metadata(self, tmp_path):
|
||||
config_path = tmp_path / "config.yaml"
|
||||
model_map = {
|
||||
"kimi-k2.6": {"context_length": 262144},
|
||||
"moonshotai/Kimi-K2.6-ACED": {"context_length": 131072},
|
||||
}
|
||||
config_path.write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"_config_version": 11,
|
||||
"custom_providers": [
|
||||
{
|
||||
"name": "Kimi Coding Plan",
|
||||
"base_url": "https://api.kimi.example.com/coding",
|
||||
"api_key_env": "KIMI_CODING_API_KEY",
|
||||
"api_mode": "anthropic_messages",
|
||||
"model": "kimi-k2.6",
|
||||
"models": model_map,
|
||||
"context_length": 262144,
|
||||
"rate_limit_delay": 0.25,
|
||||
"discover_models": False,
|
||||
"extra_body": {
|
||||
"chat_template_kwargs": {"enable_thinking": False}
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "List Models",
|
||||
"base_url": "https://list.example.com/v1",
|
||||
"models": ["alpha", "beta"],
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
|
||||
migrate_config(interactive=False, quiet=True)
|
||||
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
compatible = get_compatible_custom_providers(raw)
|
||||
|
||||
assert "custom_providers" not in raw
|
||||
provider = raw["providers"]["kimi-coding-plan"]
|
||||
assert provider["api"] == "https://api.kimi.example.com/coding"
|
||||
assert provider["key_env"] == "KIMI_CODING_API_KEY"
|
||||
assert provider["transport"] == "anthropic_messages"
|
||||
assert provider["default_model"] == "kimi-k2.6"
|
||||
assert provider["models"] == model_map
|
||||
assert provider["context_length"] == 262144
|
||||
assert provider["rate_limit_delay"] == 0.25
|
||||
assert provider["discover_models"] is False
|
||||
assert provider["extra_body"] == {
|
||||
"chat_template_kwargs": {"enable_thinking": False}
|
||||
}
|
||||
assert raw["providers"]["list-models"]["models"] == {
|
||||
"alpha": {},
|
||||
"beta": {},
|
||||
}
|
||||
|
||||
compatible_provider = next(
|
||||
entry for entry in compatible if entry["provider_key"] == "kimi-coding-plan"
|
||||
)
|
||||
assert compatible_provider["models"] == model_map
|
||||
assert compatible_provider["key_env"] == "KIMI_CODING_API_KEY"
|
||||
|
||||
def test_providers_dict_resolves_at_runtime(self, tmp_path):
|
||||
"""After migration deleted custom_providers, get_compatible_custom_providers
|
||||
still finds entries from the providers dict."""
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ def _run_migration(hermes_home: Path, **env_overrides: str) -> subprocess.Comple
|
|||
def test_docker_config_migrate_backs_up_and_migrates_legacy_config(tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "config.yaml"
|
||||
env_path = tmp_path / ".env"
|
||||
model_map = {
|
||||
"local-small": {"context_length": 8192},
|
||||
"local-large": {"context_length": 32768},
|
||||
}
|
||||
config_path.write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
|
|
@ -44,6 +48,11 @@ def test_docker_config_migrate_backs_up_and_migrates_legacy_config(tmp_path: Pat
|
|||
"name": "Local API",
|
||||
"base_url": "http://localhost:8080/v1",
|
||||
"api_key": "test-key",
|
||||
"api_mode": "chat_completions",
|
||||
"model": "local-small",
|
||||
"models": model_map,
|
||||
"context_length": 32768,
|
||||
"discover_models": False,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
@ -59,7 +68,13 @@ def test_docker_config_migrate_backs_up_and_migrates_legacy_config(tmp_path: Pat
|
|||
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert raw["_config_version"] == DEFAULT_CONFIG["_config_version"]
|
||||
assert "custom_providers" not in raw
|
||||
assert raw["providers"]["local-api"]["api"] == "http://localhost:8080/v1"
|
||||
provider = raw["providers"]["local-api"]
|
||||
assert provider["api"] == "http://localhost:8080/v1"
|
||||
assert provider["transport"] == "chat_completions"
|
||||
assert provider["default_model"] == "local-small"
|
||||
assert provider["models"] == model_map
|
||||
assert provider["context_length"] == 32768
|
||||
assert provider["discover_models"] is False
|
||||
assert list(tmp_path.glob("config.yaml.bak-*"))
|
||||
assert list(tmp_path.glob(".env.bak-*"))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue