fix(router): correct spare assignment and error presentation
This commit is contained in:
parent
65482e8eee
commit
925e6c6687
5 changed files with 91 additions and 13 deletions
|
|
@ -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",
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue