fix(router): correct spare assignment and error presentation

This commit is contained in:
Hermes Team 2026-08-20 17:21:30 +07:00
parent 65482e8eee
commit 925e6c6687
5 changed files with 91 additions and 13 deletions

View file

@ -6,7 +6,7 @@ from typing import Any
from .agy_subprocess import agy_generate
from .hermes_provider import DEFAULT_MODEL, PLACEHOLDER_API_KEY, PLACEHOLDER_API_KEY_ENV, PROVIDER_NAME, register_provider_profile
from .runtime import ensure_provider_profile_files, openai_completion_object
from .runtime import ensure_provider_profile_files, format_antigravity_error, openai_completion_object
logger = logging.getLogger(__name__)
@ -38,14 +38,13 @@ def antigravity_llm_execution(**kwargs: Any) -> Any:
session_id = kwargs.get("session_id") or request.get("session_id")
completion = engine.route_request(request, role=role, session_id=session_id)
if isinstance(completion, dict) and "error" in completion and not completion.get("choices"):
err = completion.get("error")
err_msg = err.get("message") if isinstance(err, dict) else str(err)
err_text = format_antigravity_error(completion.get("error"))
completion = {
"model": str(request.get("model") or DEFAULT_MODEL),
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": f"Antigravity error: {err_msg}"},
"message": {"role": "assistant", "content": err_text},
"finish_reason": "stop",
}
],
@ -61,14 +60,13 @@ def antigravity_llm_execution(**kwargs: Any) -> Any:
try:
completion = agy_generate(request)
if isinstance(completion, dict) and "error" in completion and not completion.get("choices"):
err = completion.get("error")
err_msg = err.get("message") if isinstance(err, dict) else str(err)
err_text = format_antigravity_error(completion.get("error"))
completion = {
"model": str(request.get("model") or DEFAULT_MODEL),
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": f"Antigravity error: {err_msg}"},
"message": {"role": "assistant", "content": err_text},
"finish_reason": "stop",
}
],

View file

@ -210,6 +210,17 @@ class AutoAssigner:
return False, f"Профиль '{profile_id}' не найден"
clean_role = role_name.strip().lower()
if clean_role in {"spare", "резерв", "none", "unassigned"}:
# Remove profile from all active role chains to keep strictly as spare pool
for rname, rpolicy in config.roles.items():
if profile_id in rpolicy.preferred_chain:
rpolicy.preferred_chain = [p for p in rpolicy.preferred_chain if p != profile_id]
config.roles[rname] = rpolicy
pcfg.enabled = True
config.profiles[profile_id] = pcfg
save_router_config(config)
return True, f"Профиль '{profile_id}' сохранен в пуле резерва (spare)"
canonical_role = CANONICAL_ROLE_MAP.get(clean_role, clean_role)
if canonical_role not in config.roles:

View file

@ -398,11 +398,18 @@ class AddAccountWizard(HubModal):
)
# Apply role to live config
AutoAssigner.assign_profile_to_role(self.target_slot, target_role, is_primary=(chosen != "spare"))
ok, msg = AutoAssigner.assign_profile_to_role(self.target_slot, target_role, is_primary=(chosen != "spare"))
if not ok:
EventLogService.get().log(
"account",
f"Ошибка назначения профиля {self.target_slot}: {msg}",
level="warning",
)
return
EventLogService.get().log(
"account",
f"Подключён аккаунт {self.discovered_identity} ({self.selected_provider}). Назначен на роль: {target_role}.",
f"Подключён аккаунт {self.discovered_identity} ({self.selected_provider}). {msg}.",
level="success",
)
self.destroy()

View file

@ -111,20 +111,43 @@ def _namespace(value: Any) -> Any:
return value
def format_antigravity_error(err: Any) -> str:
"""Format an error message with 'Antigravity error: ' prefix without duplicate prefixes."""
msg = err.get("message") if isinstance(err, dict) else str(err or "unknown error")
msg = msg.strip()
prefixes_to_strip = [
"Antigravity error:",
"Antigravity (agy) error:",
"agy error:",
"Antigravity error",
"agy error",
]
changed = True
while changed:
changed = False
for p in prefixes_to_strip:
if msg.lower().startswith(p.lower()):
msg = msg[len(p):].strip(" :")
changed = True
break
return f"Antigravity error: {msg}" if msg else "Antigravity error: unknown error"
def openai_completion_object(completion: dict[str, Any]) -> SimpleNamespace:
"""Return an object compatible with Hermes' ChatCompletionsTransport."""
completion = dict(completion)
choices = []
# Handle error payload without crashing choices[0]
if "error" in completion and not completion.get("choices"):
err = completion.get("error")
err_msg = err.get("message") if isinstance(err, dict) else str(err)
err_text = format_antigravity_error(completion.get("error"))
choices.append({
"index": 0,
"message": {
"role": "assistant",
"content": f"Antigravity error: {err_msg}",
"content": err_text,
"tool_calls": None,
},
"finish_reason": "stop",

View file

@ -244,6 +244,45 @@ def test_p0_7_assign_role_action(tmp_path, monkeypatch):
assert "Неизвестная роль" in msg
@pytest.mark.unit
def test_n1_spare_assignment_mode(tmp_path, monkeypatch):
"""N1: Verify that selecting spare mode removes profile from active roles without creating rogue roles."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
config = get_default_router_config()
# Put ag-w1 in coder-primary
config.roles["coder-primary"].preferred_chain = ["ag-w1", "ag-w2"]
save_router_config(config)
# Assign ag-w1 to spare
ok, msg = AutoAssigner.assign_profile_to_role("ag-w1", "spare")
assert ok is True
assert "резерв" in msg.lower() or "spare" in msg.lower()
reloaded = load_router_config()
assert "ag-w1" not in reloaded.roles["coder-primary"].preferred_chain
assert "spare" not in reloaded.roles # Canonical role set unchanged
assert reloaded.profiles["ag-w1"].enabled is True
@pytest.mark.unit
def test_n2_error_formatter_deduplication():
"""N2: Verify that format_antigravity_error never creates double 'Antigravity error:' prefixes."""
from antigravity_provider.runtime import format_antigravity_error
# Case 1: Raw exception message
assert format_antigravity_error("connection refused") == "Antigravity error: connection refused"
# Case 2: Already prefixed with Antigravity error:
assert format_antigravity_error("Antigravity error: quota exceeded") == "Antigravity error: quota exceeded"
# Case 3: Nested multiple prefixes
assert format_antigravity_error("Antigravity error: Antigravity error: agy error: 429") == "Antigravity error: 429"
# Case 4: Dict error format
assert format_antigravity_error({"message": "Antigravity (agy) error: timeout"}) == "Antigravity error: timeout"
@pytest.mark.unit
def test_p0_8_wizard_role_application(tmp_path, monkeypatch):
"""P0-8: Verify Wizard step 4 role assignment is applied directly to configuration."""