diff --git a/config/router_profiles.example.yaml b/config/router_profiles.example.yaml index 68dde1c..9d94e0f 100644 --- a/config/router_profiles.example.yaml +++ b/config/router_profiles.example.yaml @@ -1,21 +1,21 @@ # Hermes Hub: Multi-Provider Account Router Default Profiles Template -# Copy this file to %LOCALAPPDATA%\hermes\config\router_profiles.yaml if not present. +# Clean initial configuration (0 profiles, 13 canonical roles with empty chains). +# Accounts are dynamically created and mapped as you connect them. router: enabled: true - default_role: "orchestrator" + default_role: "manager" max_failover_attempts: 3 cooldown_base_seconds: 300 cooldown_max_seconds: 3600 session_affinity_ttl_seconds: 1800 + quota_threshold_percent: 10.0 + quota_threshold_action: "notify" roles: - orchestrator: - role_name: "orchestrator" - preferred_chain: - - "codex-orch" - - "ag-orch-fallback" - - "opengo-3" + manager: + role_name: "manager" + preferred_chain: [] fallback_capabilities: - "orchestrator" - "reasoning" @@ -23,281 +23,114 @@ roles: session_affinity_enabled: true default_model: "gemini-3.7-flash" - coder-primary: - role_name: "coder-primary" - preferred_chain: - - "codex-worker-1" - - "ag-w1" - - "opengo-3" + developer-1: + role_name: "developer-1" + preferred_chain: [] fallback_capabilities: - "coding" max_failover_attempts: 3 session_affinity_enabled: true + default_model: "gemini-3.7-flash" - coder-secondary: - role_name: "coder-secondary" - preferred_chain: - - "codex-worker-2" - - "ag-w2" - - "opengo-2" + developer-2: + role_name: "developer-2" + preferred_chain: [] fallback_capabilities: - "coding" - "reviewer" max_failover_attempts: 3 session_affinity_enabled: true + default_model: "gemini-3.1-pro-high" - reviewer: - role_name: "reviewer" - preferred_chain: - - "codex-worker-2" - - "opengo-2" - - "ag-w2" + code-reviewer: + role_name: "code-reviewer" + preferred_chain: [] fallback_capabilities: - "reviewer" - "coding" max_failover_attempts: 3 session_affinity_enabled: true + default_model: "claude-opus-4-6-thinking" - research: - role_name: "research" - preferred_chain: - - "opengo-1" - - "ag-w3" - - "ag-w4" + researcher: + role_name: "researcher" + preferred_chain: [] fallback_capabilities: - "research" - - "fast" + - "search" max_failover_attempts: 3 session_affinity_enabled: true - fast: - role_name: "fast" - preferred_chain: - - "opengo-1" - - "ag-w4" - - "ag-spare-1" + tester: + role_name: "tester" + preferred_chain: [] fallback_capabilities: - "fast" + - "testing" + max_failover_attempts: 3 + session_affinity_enabled: true + + tech-writer: + role_name: "tech-writer" + preferred_chain: [] + fallback_capabilities: + - "documentation" + - "reasoning" + max_failover_attempts: 3 + session_affinity_enabled: true + + analyst: + role_name: "analyst" + preferred_chain: [] + fallback_capabilities: + - "reasoning" + - "research" + max_failover_attempts: 3 + session_affinity_enabled: true + + guardian: + role_name: "guardian" + preferred_chain: [] + fallback_capabilities: + - "guardian" + - "security" + max_failover_attempts: 0 + session_affinity_enabled: true + + cost-controller: + role_name: "cost-controller" + preferred_chain: [] + fallback_capabilities: + - "cost-controller" + - "budget" + max_failover_attempts: 0 + session_affinity_enabled: true + + integration-expert: + role_name: "integration-expert" + preferred_chain: [] + fallback_capabilities: + - "integration" - "coding" max_failover_attempts: 3 - session_affinity_enabled: false + session_affinity_enabled: true -profiles: - # Antigravity (10 accounts) - ag-orch-fallback: - profile_id: "ag-orch-fallback" - provider: "antigravity" - account_id: "ag-acc-orch" - enabled: true - capabilities: ["orchestrator", "reasoning", "coding"] - preferred_models: ["gemini-3.7-flash", "claude-sonnet-4-6", "gemini-3.5-flash"] - max_concurrency: 2 + security-expert: + role_name: "security-expert" + preferred_chain: [] + fallback_capabilities: + - "security" + - "code-reviewer" + max_failover_attempts: 3 + session_affinity_enabled: true - ag-w1: - profile_id: "ag-w1" - provider: "antigravity" - account_id: "ag-acc-w1" - enabled: true - capabilities: ["coding", "coder-primary", "reasoning"] - preferred_models: ["gemini-3.7-flash", "claude-sonnet-4-6", "gemini-3.5-flash"] - max_concurrency: 2 + dependency-agent: + role_name: "dependency-agent" + preferred_chain: [] + fallback_capabilities: + - "dependency-agent" + - "preflight" + max_failover_attempts: 3 + session_affinity_enabled: true - ag-w2: - profile_id: "ag-w2" - provider: "antigravity" - account_id: "ag-acc-w2" - enabled: true - capabilities: ["coding", "coder-secondary", "reviewer", "review"] - preferred_models: ["gemini-3.7-flash", "gemini-3.5-flash"] - max_concurrency: 2 - - ag-w3: - profile_id: "ag-w3" - provider: "antigravity" - account_id: "ag-acc-w3" - enabled: true - capabilities: ["research", "reasoning", "search"] - preferred_models: ["gemini-3.7-flash", "claude-sonnet-4-6"] - max_concurrency: 2 - - ag-w4: - profile_id: "ag-w4" - provider: "antigravity" - account_id: "ag-acc-w4" - enabled: true - capabilities: ["coding", "reasoning", "fast"] - preferred_models: ["gemini-3.5-flash", "gemini-3.7-flash"] - max_concurrency: 2 - - ag-spare-1: - profile_id: "ag-spare-1" - provider: "antigravity" - account_id: "ag-acc-sp1" - enabled: true - capabilities: ["hot-spare", "coding", "reasoning", "orchestrator", "research", "fast"] - preferred_models: ["gemini-3.7-flash", "gemini-3.5-flash"] - max_concurrency: 2 - - ag-spare-2: - profile_id: "ag-spare-2" - provider: "antigravity" - account_id: "ag-acc-sp2" - enabled: true - capabilities: ["hot-spare", "coding", "reasoning", "orchestrator", "research", "fast"] - preferred_models: ["gemini-3.7-flash", "gemini-3.5-flash"] - max_concurrency: 2 - - ag-cold-1: - profile_id: "ag-cold-1" - provider: "antigravity" - account_id: "ag-acc-cold1" - enabled: false - capabilities: ["cold-spare"] - preferred_models: [] - max_concurrency: 1 - - ag-cold-2: - profile_id: "ag-cold-2" - provider: "antigravity" - account_id: "ag-acc-cold2" - enabled: false - capabilities: ["cold-spare"] - preferred_models: [] - max_concurrency: 1 - - ag-cold-3: - profile_id: "ag-cold-3" - provider: "antigravity" - account_id: "ag-acc-cold3" - enabled: false - capabilities: ["cold-spare"] - preferred_models: [] - max_concurrency: 1 - - # OpenAI Codex (3 accounts) - codex-orch: - profile_id: "codex-orch" - provider: "openai-codex" - account_id: "codex-acc-1" - enabled: true - capabilities: ["orchestrator", "coding", "reasoning"] - preferred_models: ["gpt-4o", "o3-mini", "codex"] - max_concurrency: 2 - - codex-worker-1: - profile_id: "codex-worker-1" - provider: "openai-codex" - account_id: "codex-acc-2" - enabled: true - capabilities: ["coding", "coder-primary", "reasoning"] - preferred_models: ["gpt-4o", "o3-mini", "codex"] - max_concurrency: 2 - - codex-worker-2: - profile_id: "codex-worker-2" - provider: "openai-codex" - account_id: "codex-acc-3" - enabled: true - capabilities: ["coding", "coder-secondary", "reviewer", "review"] - preferred_models: ["gpt-4o", "o3-mini", "codex"] - max_concurrency: 2 - - # OpenCode Go (3 accounts) - opengo-1: - profile_id: "opengo-1" - provider: "opencode-go" - account_id: "opengo-acc-1" - enabled: true - capabilities: ["research", "search", "fast", "review"] - preferred_models: ["qwen3.8-max", "glm-5.3", "deepseek-v4-flash", "grok-4.5"] - max_concurrency: 3 - - opengo-2: - profile_id: "opengo-2" - provider: "opencode-go" - account_id: "opengo-acc-2" - enabled: true - capabilities: ["reviewer", "review", "coding", "reasoning"] - preferred_models: ["deepseek-v4-pro", "grok-4.5", "qwen3.7-max"] - max_concurrency: 3 - - opengo-3: - profile_id: "opengo-3" - provider: "opencode-go" - account_id: "opengo-acc-3" - enabled: true - capabilities: ["coder-fallback", "orchestrator", "coding", "reasoning"] - preferred_models: ["kimi-k2.7-code", "deepseek-v4-pro", "qwen3.8-max"] - max_concurrency: 3 - - # Claude / Anthropic (3 accounts) - claude-orch: - profile_id: "claude-orch" - provider: "claude" - account_id: "claude-acc-1" - enabled: true - capabilities: ["orchestrator", "coding", "reasoning"] - preferred_models: ["claude-3-7-sonnet", "claude-3-5-haiku", "claude-sonnet-4-6"] - max_concurrency: 2 - - claude-worker-1: - profile_id: "claude-worker-1" - provider: "claude" - account_id: "claude-acc-2" - enabled: true - capabilities: ["coding", "coder-primary", "reasoning"] - preferred_models: ["claude-3-7-sonnet", "claude-3-5-haiku", "claude-sonnet-4-6"] - max_concurrency: 2 - - claude-worker-2: - profile_id: "claude-worker-2" - provider: "claude" - account_id: "claude-acc-3" - enabled: true - capabilities: ["coding", "coder-secondary", "reviewer", "review"] - preferred_models: ["claude-3-7-sonnet", "claude-3-5-haiku"] - max_concurrency: 2 - - # Grok / xAI (3 accounts) - grok-orch: - profile_id: "grok-orch" - provider: "grok" - account_id: "grok-acc-1" - enabled: true - capabilities: ["orchestrator", "coding", "reasoning"] - preferred_models: ["grok-3", "grok-3-mini", "grok-4.5"] - max_concurrency: 2 - - grok-worker-1: - profile_id: "grok-worker-1" - provider: "grok" - account_id: "grok-acc-2" - enabled: true - capabilities: ["coding", "coder-primary", "reasoning"] - preferred_models: ["grok-3", "grok-3-mini", "grok-4.5"] - max_concurrency: 2 - - grok-worker-2: - profile_id: "grok-worker-2" - provider: "grok" - account_id: "grok-acc-3" - enabled: true - capabilities: ["research", "reasoning", "fast"] - preferred_models: ["grok-3", "grok-3-mini"] - max_concurrency: 2 - -# Optional: User Model Pricing Table (USD per 1M tokens) -# Telemetry will compute call cost in USD only if a model price is defined below. -pricing: - gemini-2.5-pro: - input_cost_per_m: 1.25 - output_cost_per_m: 5.00 - gemini-2.5-flash: - input_cost_per_m: 0.15 - output_cost_per_m: 0.60 - gpt-4o: - input_cost_per_m: 2.50 - output_cost_per_m: 10.00 - claude-3-7-sonnet: - input_cost_per_m: 3.00 - output_cost_per_m: 15.00 +profiles: {} diff --git a/scripts/verify_multi_provider_router.py b/scripts/verify_multi_provider_router.py index 7a89d89..35f1b2f 100644 --- a/scripts/verify_multi_provider_router.py +++ b/scripts/verify_multi_provider_router.py @@ -18,7 +18,13 @@ for p in [ if p.is_dir() and str(p) not in sys.path: sys.path.insert(0, str(p)) -from antigravity_provider.router.router_config import get_default_router_config, load_router_config +from antigravity_provider.router.router_config import ( + RolePolicy, + RouterConfig, + RouterProfileConfig, + get_default_router_config, + load_router_config, +) from antigravity_provider.router.health_tracker import ( HEALTHY, QUOTA_EXHAUSTED, @@ -46,48 +52,29 @@ def run_checks() -> int: # 1. Config inventory print("1. Checking profile inventory and provider counts...") - config = get_default_router_config() - # Проверка структурная, а не пересчёт. Раньше здесь стояло «ровно 16 - # профилей» и дословные цепочки от 20 августа. Миграция законно довела - # конфигурацию до 22 профилей, добавив claude и grok, — и установка стала - # падать с кодом 12 на любой машине, где миграция отработала. Смысл этой - # проверки в том, работоспособна ли маршрутизация, а не совпадает ли - # конфигурация с зафиксированной когда-то. - counts = {} - for prof in config.profiles.values(): - counts[prof.provider] = counts.get(prof.provider, 0) + 1 - assert config.profiles, "В конфигурации нет ни одного профиля" - for required in ("openai-codex", "antigravity", "opencode-go"): - assert counts.get(required), f"Нет ни одного профиля провайдера {required}" - summary = ", ".join(f"{prov}: {n}" for prov, n in sorted(counts.items())) - print(f" [PASS] Профилей: {len(config.profiles)} ({summary})") + config = load_router_config() + if config.profiles: + counts = {} + for prof in config.profiles.values(): + counts[prof.provider] = counts.get(prof.provider, 0) + 1 + summary = ", ".join(f"{prov}: {n}" for prov, n in sorted(counts.items())) + print(f" [PASS] Профилей: {len(config.profiles)} ({summary})") + else: + print(" [PASS] Чистая конфигурация по умолчанию (0 профилей, добавление по мере подключения)") passed += 1 # 2. Role Fallback Chains print("2. Checking role fallback policies...") - # Имя оркестрирующей роли меняется вместе с реестром: в A28 orchestrator - # стал manager. Дословная проверка старого имени пережила миграцию и - # роняла установку на Windows с кодом 12 — на Linux этот скрипт не - # запускается, поэтому там всё ставилось. Спрашиваем актуальное имя у - # реестра, а не помним его в скрипте. assert config.roles, "В конфигурации нет ни одной роли" try: from antigravity_provider.router.role_registry import RoleRegistry orchestrating_role = RoleRegistry.resolve_canonical_role("orchestrator") except Exception: - orchestrating_role = "orchestrator" + orchestrating_role = "manager" assert orchestrating_role in config.roles, ( f"Оркестрирующая роль {orchestrating_role!r} отсутствует; есть: {sorted(config.roles)}" ) - # Цепочки настраиваются владельцем и меняются — дословно их сверять нельзя. - # Проверяем то, что действительно ломает маршрутизацию: цепочка непуста и - # каждый профиль в ней существует. - # Пустая цепочка — не поломка сама по себе. В A28 появились роли, - # объявленные без реализации (guardian, cost-controller): аккаунтов у них - # ещё нет, и требовать цепочку — значит ронять установку из-за роли, - # которой никто не пользуется. Ломает маршрутизацию другое: ссылка на - # несуществующий профиль и пустая цепочка у ОРКЕСТРИРУЮЩЕЙ роли. empty_chains = [] for role_name, policy in config.roles.items(): chain = policy.preferred_chain or [] @@ -97,12 +84,9 @@ def run_checks() -> int: assert pid in config.profiles, ( f"Роль {role_name} ссылается на несуществующий профиль {pid}" ) - assert config.roles[orchestrating_role].preferred_chain, ( - f"У оркестрирующей роли {orchestrating_role!r} пустая цепочка — маршрутизация работать не будет" - ) if empty_chains: - print(f" [INFO] Без аккаунтов пока: {', '.join(sorted(empty_chains))}") - print(f" [PASS] Цепочки {len(config.roles)} ролей ссылаются только на существующие профили") + print(f" [INFO] Роли с пустыми цепочками (чистый старт / не назначены): {', '.join(sorted(empty_chains))}") + print(f" [PASS] Все {len(config.roles)} ролей валидны и ссылаются только на существующие профили") passed += 1 # 3. Model family extraction @@ -178,29 +162,9 @@ def run_checks() -> int: # 9. Full failover execution loop print("9. Checking full failover execution loop...") - engine = RouterEngine(config=config) - engine.health.clear_cooldown() - - # Проверяется МЕХАНИЗМ отказоустойчивости, а не расстановка аккаунтов. - # - # Прежняя версия зашивала порядок codex -> antigravity -> opengo-3 и - # конкретные идентификаторы профилей. Но порядок в цепочке — это выбор - # владельца, он его меняет мышью в интерфейсе. Любая перестановка роняла - # проверку, а с ней и установку на Windows с кодом 12. - # - # Здесь: берём настоящую цепочку оркестрирующей роли, роняем все профили - # кроме последнего и убеждаемся, что маршрутизатор дошёл именно до него. - # Учитываем предел попыток: если цепочка длиннее, до её хвоста - # маршрутизатор просто не дойдёт, и ожидать этого нельзя. - full_chain = list(config.roles[orchestrating_role].preferred_chain) - max_attempts = getattr(config.roles[orchestrating_role], "max_failover_attempts", 0) or len(full_chain) + full_chain = list(config.roles.get(orchestrating_role, RolePolicy(role_name=orchestrating_role)).preferred_chain) + max_attempts = getattr(config.roles.get(orchestrating_role, RolePolicy(role_name=orchestrating_role)), "max_failover_attempts", 0) or len(full_chain) chain = full_chain[:max_attempts] - assert len(chain) >= 2, ( - f"В цепочке роли {orchestrating_role!r} меньше двух профилей — " - "отказоустойчивость проверить нечем" - ) - last_pid = chain[-1] - last_provider = config.profiles[last_pid].provider adapter_by_provider = { "openai-codex": CodexAdapter, @@ -211,9 +175,48 @@ def run_checks() -> int: "local": LocalLLMAdapter, } + if len(chain) >= 2 and all(pid in config.profiles for pid in chain): + test_engine = RouterEngine(config=config) + test_engine.health.clear_cooldown() + test_role = orchestrating_role + test_chain = chain + test_profiles = config.profiles + else: + synth_config = RouterConfig( + enabled=True, + default_role="manager", + roles={ + "manager": RolePolicy( + role_name="manager", + preferred_chain=["synth-codex", "synth-ag"], + max_failover_attempts=2, + ) + }, + profiles={ + "synth-codex": RouterProfileConfig( + profile_id="synth-codex", + provider="openai-codex", + capabilities=["coding", "orchestrator"], + ), + "synth-ag": RouterProfileConfig( + profile_id="synth-ag", + provider="antigravity", + capabilities=["coding", "orchestrator"], + ), + }, + ) + test_engine = RouterEngine(config=synth_config) + test_engine.health.clear_cooldown() + test_role = "manager" + test_chain = ["synth-codex", "synth-ag"] + test_profiles = synth_config.profiles + + last_pid = test_chain[-1] + last_provider = test_profiles[last_pid].provider + expected = {"id": "ok", "choices": [{"message": {"role": "assistant", "content": "from-last-in-chain"}}]} - failing = {cls for pid in chain[:-1] - if (cls := adapter_by_provider.get(config.profiles[pid].provider)) is not None} + failing = {cls for pid in test_chain[:-1] + if (cls := adapter_by_provider.get(test_profiles[pid].provider)) is not None} winner = adapter_by_provider.get(last_provider) if winner is None or winner in failing: @@ -224,9 +227,9 @@ def run_checks() -> int: for cls in failing: stack.enter_context(patch.object(cls, "invoke", side_effect=RuntimeError("Insufficient quota"))) stack.enter_context(patch.object(winner, "invoke", return_value=expected)) - res = engine.route_request( + res = test_engine.route_request( {"messages": [{"role": "user", "content": "test"}]}, - role=orchestrating_role, + role=test_role, session_id="verify-failover", ) assert res["choices"][0]["message"]["content"] == "from-last-in-chain", ( @@ -235,7 +238,7 @@ def run_checks() -> int: assert res["router_metadata"]["profile_id"] == last_pid, ( f"Ожидался профиль {last_pid}, получен {res['router_metadata']['profile_id']}" ) - print(f" [PASS] Отказоустойчивость прошла цепочку {' -> '.join(chain)}") + print(f" [PASS] Отказоустойчивость прошла цепочку {' -> '.join(test_chain)}") passed += 1 # 10. Passthrough & Graceful fallback diff --git a/src/antigravity_provider/router/action_handler.py b/src/antigravity_provider/router/action_handler.py index 91e4cb1..9b3f314 100644 --- a/src/antigravity_provider/router/action_handler.py +++ b/src/antigravity_provider/router/action_handler.py @@ -38,6 +38,10 @@ def do_set_orchestrator(profile_id: str) -> Tuple[bool, str]: def do_test_profile(provider: str, profile_id: str) -> Dict[str, Any]: config = load_router_config() pcfg = config.get_profile(profile_id) + if not pcfg: + AutoAssigner.ensure_profile_definition(provider, profile_id) + config = load_router_config() + pcfg = config.get_profile(profile_id) if not pcfg: return {'success': False, 'error': f"Профиль '{profile_id}' не найден"} @@ -467,6 +471,65 @@ def generate_quotas_export(format: str = "json") -> Any: } +def do_reset_router_config(actor: str = "user:web") -> Dict[str, Any]: + """Reset router configuration to clean default state (0 profiles, 13 canonical roles with empty chains). + + Guaranteed to create a timestamped backup of router_profiles.yaml first. + Strictly preserves all credentials in ~/.hermes/agy_profiles, codex_profiles, + opengo_profiles, claude_profiles, grok_profiles, and hub_settings.json. + """ + import shutil + from pathlib import Path + from antigravity_provider.router.router_config import ( + get_default_router_config, + save_router_config, + ) + + env_config = os.environ.get("HERMES_ROUTER_CONFIG", "").strip() + if env_config: + config_path = Path(env_config).expanduser() + else: + config_path = paths.get_router_profiles_path() + + backup_name = None + if config_path.exists(): + try: + backup_path = config_path.with_name(f"{config_path.name}.bak_{int(time.time())}") + shutil.copy2(config_path, backup_path) + backup_name = backup_path.name + except Exception as exc: + logger.warning("Не удалось создать резервную копию перед сбросом конфигурации: %s", exc) + + clean_cfg = get_default_router_config() + saved = save_router_config(clean_cfg, config_path) + if not saved: + return { + "ok": False, + "message": "Не удалось записать чистую конфигурацию в файл", + } + + try: + from antigravity_provider.router.state_store import HubStateStore + HubStateStore.get().refresh(force_scan=True) + except Exception: + pass + + EventLogService.get().log( + "routing", + f"Конфигурация маршрутизатора сброшена в исходное состояние (резервная копия: {backup_name or 'нет'}). Учётные данные и подключенные аккаунты сохранены.", + level="warning", + actor=actor, + action="reset_router_config", + outcome="success", + ) + + return { + "ok": True, + "message": "Конфигурация маршрутизатора сброшена. Учётные данные и подключенные аккаунты сохранены.", + "backup_file": backup_name, + } + + class ActionExecutor: """Shared execution layer for Desktop and Web actions.""" @@ -490,6 +553,28 @@ class ActionExecutor: prov = _pcfg.provider except Exception: pass + if not prov: + for prefix, p_name in [ + ("ag-", "antigravity"), + ("codex-", "openai-codex"), + ("opengo-", "opencode-go"), + ("claude-", "claude"), + ("grok-", "grok"), + ("local-", "local"), + ("openrouter-", "openrouter"), + ("nvidia-", "nvidia"), + ("ollama-", "ollama"), + ("vllm-", "vllm"), + ]: + if pid.startswith(prefix): + prov = p_name + break + if not prov: + from antigravity_provider.router.profile_manager import get_profile_auth_path + for candidate_prov in ("antigravity", "openai-codex", "opencode-go", "claude", "grok", "local", "openrouter", "nvidia", "ollama", "vllm"): + if get_profile_auth_path(candidate_prov, pid).is_file(): + prov = candidate_prov + break if action in { 'create_agent', @@ -927,5 +1012,9 @@ class ActionExecutor: 'data': {'format': 'json', 'report': res, 'filename': 'hermes_quotas_export.json'} } + elif action in ['reset_router_config', 'reset_to_empty_config']: + res = do_reset_router_config(actor=actor) + return {'ok': res.get('ok', False), 'message': res.get('message', ''), 'data': res} + else: return {'ok': False, 'message': f'Неизвестное действие: {action}', 'unknown': True} diff --git a/src/antigravity_provider/router/auto_assigner.py b/src/antigravity_provider/router/auto_assigner.py index 1ff937b..dfa2335 100644 --- a/src/antigravity_provider/router/auto_assigner.py +++ b/src/antigravity_provider/router/auto_assigner.py @@ -14,6 +14,7 @@ import logging from pathlib import Path from typing import Any, Dict, List, Optional, Tuple +from antigravity_provider import paths from antigravity_provider.router.profile_manager import ProfileAuthManager, mask_email, mask_id from antigravity_provider.router.role_registry import RoleRegistry from antigravity_provider.router.router_config import ( @@ -67,6 +68,22 @@ class AutoAssigner: """Get human-readable display name, logical role, and tier for a profile.""" if profile_id in DEFAULT_SLOT_ROLES: return DEFAULT_SLOT_ROLES[profile_id] + prov_map = { + "ag": "Antigravity", + "codex": "Codex", + "opengo": "OpenCode", + "claude": "Claude", + "grok": "Grok", + "local": "Локальный сервер", + "openrouter": "OpenRouter", + "nvidia": "NVIDIA NIM", + "ollama": "Ollama", + "vllm": "vLLM", + } + parts = profile_id.split("-") + if len(parts) == 2 and parts[0] in prov_map and parts[1].isdigit(): + clean_name = f"{prov_map[parts[0]]} {parts[1]}" + return (clean_name, "worker", "primary") clean_name = profile_id.replace("-", " ").title() return (clean_name, "worker", "primary") @@ -104,91 +121,57 @@ class AutoAssigner: def find_free_slot(provider: str, requested_role: str = "auto") -> Optional[str]: """Find the optimal free internal profile slot for a provider. - When all predefined candidate slots are authenticated/occupied, dynamically - generates the next free profile ID without capping account count (P0-1). + Generates clean numbered slots starting from 1 (codex-1, ag-1, claude-1, + grok-1, opengo-1, local-1, openrouter-1, nvidia-1, ollama-1) upon first addition. """ config = load_router_config() provider_norm = (provider or "").strip().lower() - provider_slots = { - "antigravity": [ - "ag-orch-fallback", "ag-w1", "ag-w2", "ag-w3", "ag-w4", - "ag-spare-1", "ag-spare-2", "ag-cold-1", "ag-cold-2", "ag-cold-3" - ], - "openai-codex": ["codex-orch", "codex-worker-1", "codex-worker-2"], - "codex": ["codex-orch", "codex-worker-1", "codex-worker-2"], - "opencode-go": ["opengo-1", "opengo-2", "opengo-3"], - "opencode": ["opengo-1", "opengo-2", "opengo-3"], - "claude": ["claude-orch", "claude-worker-1", "claude-worker-2"], - "anthropic": ["claude-orch", "claude-worker-1", "claude-worker-2"], - "grok": ["grok-orch", "grok-worker-1", "grok-worker-2"], - "xai": ["grok-orch", "grok-worker-1", "grok-worker-2"], - "local": ["local-1", "local-2"], - "local-llm": ["local-1", "local-2"], - "llama.cpp": ["local-1", "local-2"], - "ollama": ["ollama-1", "ollama-2"], - "vllm": ["local-1", "local-2"], - } + def _get_prefix(prov: str) -> str: + p = (prov or "").strip().lower() + if p in ("antigravity", "google-antigravity", "agy"): + return "ag" + if p in ("openai-codex", "codex", "openai"): + return "codex" + if p in ("opencode-go", "opencode", "opengo"): + return "opengo" + if p in ("claude", "anthropic"): + return "claude" + if p in ("grok", "xai"): + return "grok" + if p in ("local", "local-llm", "llama.cpp"): + return "local" + if p in ("openrouter",): + return "openrouter" + if p in ("nvidia", "nvidia-nim"): + return "nvidia" + if p in ("ollama",): + return "ollama" + if p in ("vllm",): + return "vllm" + return p - candidates = list(provider_slots.get(provider_norm, [])) + prefix = _get_prefix(provider_norm) - # Priority based on requested role - if requested_role == "manager": - if provider_norm in ("openai-codex", "codex") and "codex-orch" in candidates: - candidates.remove("codex-orch") - candidates.insert(0, "codex-orch") - elif provider_norm == "antigravity" and "ag-orch-fallback" in candidates: - candidates.remove("ag-orch-fallback") - candidates.insert(0, "ag-orch-fallback") # 1. First check existing candidate profiles already in config - for pid in candidates: - pcfg = config.get_profile(pid) - if not pcfg: - continue - status = ProfileAuthManager.get_profile_status(provider, pid) - if not status.get("authenticated"): - return pid + for pid, pcfg in config.profiles.items(): + if _get_prefix(pcfg.provider) == prefix or pcfg.provider == provider_norm: + status = ProfileAuthManager.get_profile_status(pcfg.provider, pid) + if not status.get("authenticated"): + return pid - # 2. Check remaining predefined slots and ensure definition - for pid in candidates: - status = ProfileAuthManager.get_profile_status(provider, pid) + # 2. Dynamically generate clean numbered slots (codex-1, ag-1, claude-1, etc.) + for i in range(1, 200): + candidate_pid = f"{prefix}-{i}" + if candidate_pid in config.profiles: + pcfg = config.profiles[candidate_pid] + status = ProfileAuthManager.get_profile_status(pcfg.provider, candidate_pid) + if status.get("authenticated"): + continue + status = ProfileAuthManager.get_profile_status(provider, candidate_pid) if not status.get("authenticated"): - AutoAssigner.ensure_profile_definition(provider, pid) - return pid - - # 3. Predefined slots occupied: dynamically generate unlimited candidates - def _generate_candidates(prov: str): - p = prov.lower() - if p == "antigravity": - for i in range(5, 200): - yield f"ag-w{i}" - elif p in ("openai-codex", "codex"): - for i in range(4, 200): - yield f"codex-{i}" - elif p in ("opencode-go", "opencode"): - for i in range(4, 200): - yield f"opengo-{i}" - elif p in ("claude", "anthropic"): - for i in range(3, 200): - yield f"claude-worker-{i}" - elif p in ("grok", "xai"): - for i in range(3, 200): - yield f"grok-worker-{i}" - elif p == "ollama": - for i in range(3, 200): - yield f"ollama-{i}" - elif p in ("local", "local-llm", "llama.cpp", "vllm"): - for i in range(3, 200): - yield f"local-{i}" - else: - for i in range(1, 200): - yield f"{p}-{i}" - - for dynamic_pid in _generate_candidates(provider_norm): - status = ProfileAuthManager.get_profile_status(provider, dynamic_pid) - if not status.get("authenticated"): - AutoAssigner.ensure_profile_definition(provider, dynamic_pid) - return dynamic_pid + AutoAssigner.ensure_profile_definition(provider, candidate_pid) + return candidate_pid return None @@ -209,15 +192,23 @@ class AutoAssigner: capabilities_map = { "grok": ["reasoning", "coding", "research"], + "xai": ["reasoning", "coding", "research"], "claude": ["reasoning", "coding", "review"], + "anthropic": ["reasoning", "coding", "review"], "opencode-go": ["coding", "research", "fast"], + "opencode": ["coding", "research", "fast"], "openai-codex": ["coding", "reasoning"], + "codex": ["coding", "reasoning"], "antigravity": ["coding", "reasoning", "research", "fast"], + "google-antigravity": ["coding", "reasoning", "research", "fast"], "local": ["reviewer", "coding", "reasoning", "fast", "research"], "local-llm": ["reviewer", "coding", "reasoning", "fast", "research"], "llama.cpp": ["reviewer", "coding", "reasoning", "fast", "research"], "ollama": ["reviewer", "coding", "reasoning", "fast", "research"], "vllm": ["reviewer", "coding", "reasoning", "fast", "research"], + "openrouter": ["coding", "reasoning", "research", "fast"], + "nvidia": ["coding", "reasoning", "fast"], + "nvidia-nim": ["coding", "reasoning", "fast"], } capabilities = capabilities_map.get(provider, ["coding", "reasoning"]) @@ -276,6 +267,28 @@ class AutoAssigner: """Assign a profile to a canonical router role, updating fallback chains and persisting config.""" config = load_router_config() pcfg = config.get_profile(profile_id) + if not pcfg: + inferred_prov = None + for prefix, prov in [ + ("ag-", "antigravity"), + ("codex-", "openai-codex"), + ("opengo-", "opencode-go"), + ("claude-", "claude"), + ("grok-", "grok"), + ("local-", "local"), + ("openrouter-", "openrouter"), + ("nvidia-", "nvidia"), + ("ollama-", "ollama"), + ("vllm-", "vllm"), + ]: + if profile_id.startswith(prefix): + inferred_prov = prov + break + if inferred_prov: + AutoAssigner.ensure_profile_definition(inferred_prov, profile_id) + config = load_router_config() + pcfg = config.get_profile(profile_id) + if not pcfg: return False, f"Профиль '{profile_id}' не найден" @@ -314,6 +327,31 @@ class AutoAssigner: @staticmethod def auto_assign_all() -> Dict[str, Any]: """Automatically distribute all authenticated profiles across canonical router roles (P0-4).""" + config = load_router_config() + # Scan on-disk profiles and ensure their definition in config.profiles + prov_prefix_map = { + "antigravity": "agy_profiles", + "openai-codex": "codex_profiles", + "opencode-go": "opengo_profiles", + "claude": "claude_profiles", + "grok": "grok_profiles", + "local": "local_profiles", + "openrouter": "openrouter_profiles", + "nvidia": "nvidia_profiles", + "ollama": "ollama_profiles", + "vllm": "vllm_profiles", + } + for prov, subdir in prov_prefix_map.items(): + base_dir = paths.get_hermes_home() / subdir + if base_dir.is_dir(): + for p_entry in base_dir.iterdir(): + if p_entry.is_dir(): + pid = p_entry.name + if pid not in config.profiles: + st = ProfileAuthManager.get_profile_status(prov, pid) + if st.get("authenticated"): + AutoAssigner.ensure_profile_definition(prov, pid) + config = load_router_config() authenticated_profiles: List[Tuple[str, RouterProfileConfig]] = [] for pid, pcfg in config.profiles.items(): diff --git a/src/antigravity_provider/router/cli_commands.py b/src/antigravity_provider/router/cli_commands.py index dcefffe..2f622a0 100644 --- a/src/antigravity_provider/router/cli_commands.py +++ b/src/antigravity_provider/router/cli_commands.py @@ -34,6 +34,9 @@ def print_router_status() -> int: print("-" * 80) for rname, rpolicy in sorted(config.roles.items()): + if not rpolicy.preferred_chain: + print(f"{rname:<16} {'(не назначен)':<18} {'-':<15} {'not_configured':<18} {'-':<10}") + continue for idx, pid in enumerate(rpolicy.preferred_chain): pconfig = config.get_profile(pid) if not pconfig: @@ -222,11 +225,6 @@ def test_profile_cli(profile_id: str) -> int: def simulate_quota_cli(profile_id: str, model_family: Optional[str] = None, duration: int = 600) -> int: """Simulate quota exhaustion on a profile for testing.""" engine = get_router_engine() - pconfig = engine.config.get_profile(profile_id) - if not pconfig: - print(f"[ERROR] Profile '{profile_id}' not found.") - return 1 - engine.health.simulate_quota(profile_id, model_family=model_family, duration=duration) print(f"[OK] Simulated quota exhaustion activated for profile '{profile_id}' for {duration} seconds.") print("Use `hermes router clear-cooldown` to restore normal state.") diff --git a/src/antigravity_provider/router/profile_manager.py b/src/antigravity_provider/router/profile_manager.py index 2beeb3a..1eb22e4 100644 --- a/src/antigravity_provider/router/profile_manager.py +++ b/src/antigravity_provider/router/profile_manager.py @@ -322,7 +322,11 @@ class ProfileAuthManager: if base_url: try: from antigravity_provider.router.router_config import load_router_config, save_router_config + from antigravity_provider.router.auto_assigner import AutoAssigner rcfg = load_router_config() + if profile_id not in rcfg.profiles: + AutoAssigner.ensure_profile_definition(provider, profile_id) + rcfg = load_router_config() if profile_id in rcfg.profiles: rcfg.profiles[profile_id].custom_base_url = str(base_url).strip() if auth_data.get("models") and isinstance(auth_data["models"], list): diff --git a/src/antigravity_provider/router/role_registry.py b/src/antigravity_provider/router/role_registry.py index 7d98987..3781b72 100644 --- a/src/antigravity_provider/router/role_registry.py +++ b/src/antigravity_provider/router/role_registry.py @@ -39,7 +39,7 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = { is_orchestrator=True, capabilities=["orchestrator", "reasoning", "management", "planning"], fallback_capabilities=["orchestrator", "reasoning"], - default_preferred_chain=["codex-orch", "ag-orch-fallback", "claude-orch", "grok-orch", "opengo-3"], + default_preferred_chain=[], default_model="gemini-3.7-flash", max_failover_attempts=3, tier="core", @@ -52,7 +52,7 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = { is_implemented=True, capabilities=["coding", "developer-1", "reasoning", "tools", "structured_output"], fallback_capabilities=["coding"], - default_preferred_chain=["ag-w1", "codex-worker-1", "claude-worker-1", "opengo-1"], + default_preferred_chain=[], default_model="gemini-3.7-flash", max_failover_attempts=3, tier="core", @@ -65,7 +65,7 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = { is_implemented=True, capabilities=["coding", "developer-2", "reviewer", "tools"], fallback_capabilities=["coding", "reviewer"], - default_preferred_chain=["ag-w2", "codex-worker-2", "grok-worker-1", "opengo-3"], + default_preferred_chain=[], default_model="gemini-3.1-pro-high", max_failover_attempts=3, tier="core", @@ -78,7 +78,7 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = { is_implemented=True, capabilities=["code-reviewer", "reviewer", "coding", "security_analysis"], fallback_capabilities=["reviewer", "coding"], - default_preferred_chain=["ag-w3", "ag-w2", "claude-worker-2", "codex-worker-2", "opengo-2"], + default_preferred_chain=[], default_model="claude-opus-4-6-thinking", max_failover_attempts=3, tier="core", @@ -91,7 +91,7 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = { is_implemented=True, capabilities=["researcher", "research", "search", "long_context"], fallback_capabilities=["research", "search"], - default_preferred_chain=["ag-w3", "grok-worker-2", "opengo-2", "opengo-1"], + default_preferred_chain=[], max_failover_attempts=3, tier="core", ), @@ -103,7 +103,7 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = { is_implemented=True, capabilities=["tester", "testing", "fast", "automation"], fallback_capabilities=["fast", "testing"], - default_preferred_chain=["ag-w4", "opengo-1", "ag-spare-1"], + default_preferred_chain=[], max_failover_attempts=3, tier="qa_doc", ), @@ -115,7 +115,7 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = { is_implemented=True, capabilities=["tech-writer", "documentation", "reasoning", "structured_output"], fallback_capabilities=["documentation", "reasoning"], - default_preferred_chain=["claude-worker-2", "ag-w3", "ag-w2"], + default_preferred_chain=[], max_failover_attempts=3, tier="qa_doc", ), @@ -127,7 +127,7 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = { is_implemented=True, capabilities=["analyst", "reasoning", "research", "planning"], fallback_capabilities=["reasoning", "research"], - default_preferred_chain=["grok-worker-2", "opengo-2", "ag-w3"], + default_preferred_chain=[], max_failover_attempts=3, tier="qa_doc", ), @@ -165,7 +165,7 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = { is_implemented=True, capabilities=["integration-expert", "integration", "coding", "networking"], fallback_capabilities=["integration", "coding"], - default_preferred_chain=["opengo-3", "codex-worker-1", "ag-w4"], + default_preferred_chain=[], max_failover_attempts=3, tier="expert", ), @@ -177,7 +177,7 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = { is_implemented=True, capabilities=["security-expert", "security", "code-reviewer", "audit"], fallback_capabilities=["security", "code-reviewer"], - default_preferred_chain=["claude-worker-1", "codex-worker-2", "ag-w2"], + default_preferred_chain=[], max_failover_attempts=3, tier="expert", ), @@ -189,7 +189,7 @@ CANONICAL_ROLES: Dict[str, RoleDefinition] = { is_implemented=True, capabilities=["dependency-agent", "preflight", "environment", "system_checks", "fast"], fallback_capabilities=["dependency-agent", "preflight"], - default_preferred_chain=["opengo-1", "ag-w1", "codex-worker-1"], + default_preferred_chain=[], max_failover_attempts=3, tier="qa_doc", ), diff --git a/src/antigravity_provider/router/router_config.py b/src/antigravity_provider/router/router_config.py index f423521..05ac164 100644 --- a/src/antigravity_provider/router/router_config.py +++ b/src/antigravity_provider/router/router_config.py @@ -68,219 +68,14 @@ class RouterConfig: def get_default_router_config() -> RouterConfig: - """Generate default built-in multi-provider configuration (16 profiles across 3 providers).""" - profiles: dict[str, RouterProfileConfig] = { - # 1. Codex Pool (3 accounts) - "codex-orch": RouterProfileConfig( - profile_id="codex-orch", - provider="openai-codex", - account_id="codex-acc-1", - capabilities=["orchestrator", "coding", "reasoning"], - preferred_models=["gpt-4o", "o3-mini", "codex"], - fallback_models=["gpt-4o-mini"], - max_concurrency=1, - ), - "codex-worker-1": RouterProfileConfig( - profile_id="codex-worker-1", - provider="openai-codex", - account_id="codex-acc-2", - capabilities=["coding", "coder-primary", "reasoning"], - preferred_models=["gpt-4o", "o3-mini", "codex"], - max_concurrency=1, - ), - "codex-worker-2": RouterProfileConfig( - profile_id="codex-worker-2", - provider="openai-codex", - account_id="codex-acc-3", - capabilities=["coding", "coder-secondary", "reviewer", "review"], - preferred_models=["gpt-4o", "o3-mini", "codex"], - max_concurrency=1, - ), - # 2. Antigravity Pool (10 accounts, 7 active, 3 cold) - "ag-orch-fallback": RouterProfileConfig( - profile_id="ag-orch-fallback", - provider="antigravity", - account_id="ag-acc-orch", - capabilities=["orchestrator", "reasoning", "coding"], - preferred_models=["gemini-3.7-flash", "claude-sonnet-4-6", "gemini-3.5-flash"], - max_concurrency=1, - ), - "ag-w1": RouterProfileConfig( - profile_id="ag-w1", - provider="antigravity", - account_id="ag-acc-w1", - capabilities=["coding", "coder-primary", "reasoning"], - preferred_models=["gemini-3.7-flash", "claude-sonnet-4-6", "gemini-3.5-flash"], - max_concurrency=1, - ), - "ag-w2": RouterProfileConfig( - profile_id="ag-w2", - provider="antigravity", - account_id="ag-acc-w2", - capabilities=["coding", "coder-secondary", "reviewer", "review"], - preferred_models=["gemini-3.7-flash", "gemini-3.5-flash"], - max_concurrency=1, - ), - "ag-w3": RouterProfileConfig( - profile_id="ag-w3", - provider="antigravity", - account_id="ag-acc-w3", - capabilities=["research", "reasoning", "search"], - preferred_models=["gemini-3.7-flash", "claude-sonnet-4-6"], - max_concurrency=1, - ), - "ag-w4": RouterProfileConfig( - profile_id="ag-w4", - provider="antigravity", - account_id="ag-acc-w4", - capabilities=["coding", "reasoning", "fast"], - preferred_models=["gemini-3.5-flash", "gemini-3.7-flash"], - max_concurrency=1, - ), - "ag-spare-1": RouterProfileConfig( - profile_id="ag-spare-1", - provider="antigravity", - account_id="ag-acc-sp1", - capabilities=["hot-spare", "coding", "reasoning", "orchestrator", "research", "fast"], - preferred_models=["gemini-3.7-flash", "gemini-3.5-flash"], - max_concurrency=1, - ), - "ag-spare-2": RouterProfileConfig( - profile_id="ag-spare-2", - provider="antigravity", - account_id="ag-acc-sp2", - capabilities=["hot-spare", "coding", "reasoning", "orchestrator", "research", "fast"], - preferred_models=["gemini-3.7-flash", "gemini-3.5-flash"], - max_concurrency=1, - ), - "ag-cold-1": RouterProfileConfig( - profile_id="ag-cold-1", - provider="antigravity", - account_id="ag-acc-c1", - capabilities=["cold-spare", "coding", "reasoning"], - preferred_models=["gemini-3.5-flash"], - enabled=False, - max_concurrency=1, - ), - "ag-cold-2": RouterProfileConfig( - profile_id="ag-cold-2", - provider="antigravity", - account_id="ag-acc-c2", - capabilities=["cold-spare", "coding", "reasoning"], - preferred_models=["gemini-3.5-flash"], - enabled=False, - max_concurrency=1, - ), - "ag-cold-3": RouterProfileConfig( - profile_id="ag-cold-3", - provider="antigravity", - account_id="ag-acc-c3", - capabilities=["cold-spare", "coding", "reasoning"], - preferred_models=["gemini-3.5-flash"], - enabled=False, - max_concurrency=1, - ), - # 3. OpenCode Go Pool (3 accounts) - "opengo-1": RouterProfileConfig( - profile_id="opengo-1", - provider="opencode-go", - account_id="opengo-acc-1", - capabilities=["coding", "fast", "multimodal"], - preferred_models=["deepseek-r1", "qwen-2.5-coder-32b", "deepseek-v3"], - max_concurrency=5, - ), - "opengo-2": RouterProfileConfig( - profile_id="opengo-2", - provider="opencode-go", - account_id="opengo-acc-2", - capabilities=["research", "coding", "multimodal"], - preferred_models=["deepseek-v3", "qwen-2.5-coder-32b", "deepseek-r1"], - max_concurrency=5, - ), - "opengo-3": RouterProfileConfig( - profile_id="opengo-3", - provider="opencode-go", - account_id="opengo-acc-3", - capabilities=["fallback", "coding", "reasoning"], - preferred_models=["deepseek-r1", "deepseek-v3", "qwen-2.5-coder-32b"], - max_concurrency=5, - ), - # 4. Claude Pool (3 accounts) - "claude-orch": RouterProfileConfig( - profile_id="claude-orch", - provider="claude", - account_id="claude-acc-1", - capabilities=["orchestrator", "coding", "reasoning"], - preferred_models=["claude-3-7-sonnet", "claude-3-5-haiku", "claude-sonnet-4-6"], - max_concurrency=2, - ), - "claude-worker-1": RouterProfileConfig( - profile_id="claude-worker-1", - provider="claude", - account_id="claude-acc-2", - capabilities=["coding", "coder-primary", "reasoning"], - preferred_models=["claude-3-7-sonnet", "claude-3-5-haiku", "claude-sonnet-4-6"], - max_concurrency=2, - ), - "claude-worker-2": RouterProfileConfig( - profile_id="claude-worker-2", - provider="claude", - account_id="claude-acc-3", - capabilities=["coding", "coder-secondary", "reviewer", "review"], - preferred_models=["claude-3-7-sonnet", "claude-3-5-haiku"], - max_concurrency=2, - ), - # 5. Grok Pool (3 accounts) - "grok-orch": RouterProfileConfig( - profile_id="grok-orch", - provider="grok", - account_id="grok-acc-1", - capabilities=["orchestrator", "coding", "reasoning"], - preferred_models=["grok-3", "grok-3-mini", "grok-4.5"], - max_concurrency=2, - ), - "grok-worker-1": RouterProfileConfig( - profile_id="grok-worker-1", - provider="grok", - account_id="grok-acc-2", - capabilities=["coding", "coder-primary", "reasoning"], - preferred_models=["grok-3", "grok-3-mini", "grok-4.5"], - max_concurrency=2, - ), - "grok-worker-2": RouterProfileConfig( - profile_id="grok-worker-2", - provider="grok", - account_id="grok-acc-3", - capabilities=["research", "reasoning", "fast"], - preferred_models=["grok-3", "grok-3-mini"], - max_concurrency=2, - ), - # 6. Local LLM Pool (2 accounts) - "local-1": RouterProfileConfig( - profile_id="local-1", - provider="local", - account_id="local-acc-1", - capabilities=["code-reviewer", "reviewer", "coder-secondary", "reasoning", "coding"], - preferred_models=["Qwen3.8-27B-Q4_K_M.gguf", "default"], - max_concurrency=1, - ), - "local-2": RouterProfileConfig( - profile_id="local-2", - provider="local", - account_id="local-acc-2", - capabilities=["tester", "fast", "research", "coding"], - preferred_models=["Qwen3-4B-Instruct-2507-Q4_K_M.gguf", "default"], - max_concurrency=1, - ), - } - + """Generate default clean multi-provider configuration (0 profiles, 13 canonical roles with empty chains).""" roles = RoleRegistry.get_default_role_policies() return RouterConfig( enabled=True, default_role="manager", roles=roles, - profiles=profiles, + profiles={}, ) @@ -364,24 +159,18 @@ def load_router_config(config_path: Optional[Path] = None) -> RouterConfig: if quota_threshold_action not in ("notify", "switch"): quota_threshold_action = "notify" - # Automatic Idempotent Migration (P0-0.1) - # Merge missing default profiles and roles into loaded user configuration + # Automatic Idempotent Migration (A41 Clean Install) + # Merge missing default roles into loaded user configuration without injecting fake profiles default_cfg = get_default_router_config() migration_needed = False - new_profiles_added: list[str] = [] new_roles_added: list[str] = [] if not profiles: - profiles = default_cfg.profiles - else: - for def_pid, def_pcfg in default_cfg.profiles.items(): - if def_pid not in profiles: - profiles[def_pid] = def_pcfg - new_profiles_added.append(def_pid) - migration_needed = True + profiles = {} if not roles: roles = default_cfg.roles + migration_needed = True else: # Сначала переименование старых ролей в канонические, и только # потом дополнение недостающими. diff --git a/src/antigravity_provider/router/unified_health.py b/src/antigravity_provider/router/unified_health.py index 24e7cd1..13499ce 100644 --- a/src/antigravity_provider/router/unified_health.py +++ b/src/antigravity_provider/router/unified_health.py @@ -353,7 +353,8 @@ class UnifiedHealthService: tag = f"{rname} (primary)" if idx == 0 else f"{rname} (fallback {idx})" role_assignments.setdefault(pid, []).append(tag) - orch_primary = config.roles.get("orchestrator", None) + orch_role_name = RoleRegistry.resolve_canonical_role("orchestrator") + orch_primary = config.roles.get(orch_role_name, None) orch_primary_id = orch_primary.preferred_chain[0] if orch_primary and orch_primary.preferred_chain else "" now = time.time() @@ -714,6 +715,26 @@ class UnifiedHealthService: continue if not chain: + agents.append(AgentViewModel( + role_id=rname, + role_name_ru=rname_ru, + role_description_ru=rdesc_ru or "Роль готова к назначению аккаунтов", + assigned_profile_id=None, + assigned_display_name=None, + provider="N/A", + provider_display_name="Не назначен", + model="-", + account_identity="Аккаунт не назначен", + routing_position="Не назначен", + status="not_configured", + status_label_ru="Не настроен", + is_active=False, + is_main_orchestrator=(rname == "manager"), + cooldown_remaining_sec=0, + session_id=None, + active_quota_status="unavailable", + active_quota_label="Не применяется", + )) continue primary_pid = chain[0] @@ -763,6 +784,27 @@ class UnifiedHealthService: active_quota_status=active_quota_st, active_quota_label=active_quota_lbl, )) + else: + agents.append(AgentViewModel( + role_id=rname, + role_name_ru=rname_ru, + role_description_ru=rdesc_ru or "Роль готова к назначению аккаунтов", + assigned_profile_id=primary_pid, + assigned_display_name=primary_pid, + provider="N/A", + provider_display_name="Не назначен", + model="-", + account_identity="Аккаунт не найден", + routing_position="Primary", + status="not_configured", + status_label_ru="Не настроен", + is_active=False, + is_main_orchestrator=(rname == "manager"), + cooldown_remaining_sec=0, + session_id=None, + active_quota_status="unavailable", + active_quota_label="Не применяется", + )) return agents diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index 8f4742c..75893ca 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -188,6 +188,12 @@ function initEventListeners() { if (btnPreflight) { btnPreflight.addEventListener('click', () => runPreflightChecks()); } + + // Reset router config listener (P0-4) + const btnResetConfig = document.getElementById('btn-reset-router-config'); + if (btnResetConfig) { + btnResetConfig.addEventListener('click', () => openResetConfigModal()); + } } @@ -1368,6 +1374,67 @@ async function saveHubServerSettings() { } } +// ── RESET CONFIGURATION (P0-2 & P0-4) ── +function openResetConfigModal() { + elements.modalTitle.textContent = 'Начать настройку заново'; + elements.modalBody.innerHTML = ` +
router_profiles.yaml (0 профилей).router_profiles.yaml.bak_....~/.hermes/agy_profiles/, codex_profiles/ и др.hub_settings.json.