diff --git a/src/antigravity_provider/agy_subprocess.py b/src/antigravity_provider/agy_subprocess.py index 2961a29..d2d42e8 100644 --- a/src/antigravity_provider/agy_subprocess.py +++ b/src/antigravity_provider/agy_subprocess.py @@ -228,7 +228,18 @@ def discover_models(profile_id: str | None = None) -> dict[str, str]: raw = result.stdout.strip() if not raw or result.returncode != 0: if profile_id: - raise RuntimeError(f"agy models: код {result.returncode}; каталог не получен") + # Прежнее сообщение «код 1; каталог не получен» скрывало причину. + # agy пишет её в stderr, и без неё непонятно главное: он + # запускается с HOME, подменённым на каталог профиля (ради + # изоляции учётных данных между аккаунтами). Если вход + # выполнялся обычным agy в оболочке, ключи легли в настоящий + # домашний каталог, и профиль пуст — отсюда отказ. + detail = (result.stderr or "").strip() or (result.stdout or "").strip() + detail = detail.splitlines()[-1][:300] if detail else "вывод пуст" + raise RuntimeError( + f"agy models: код {result.returncode}. Ответ agy: {detail}. " + f"Запуск с HOME={env.get('HOME') or env.get('USERPROFILE') or 'не задан'}" + ) return dict(_AGY_MODEL_CACHE or {}) except Exception: if profile_id: diff --git a/src/antigravity_provider/router/adapters/ollama_adapter.py b/src/antigravity_provider/router/adapters/ollama_adapter.py index dd567e4..90f1bf4 100644 --- a/src/antigravity_provider/router/adapters/ollama_adapter.py +++ b/src/antigravity_provider/router/adapters/ollama_adapter.py @@ -15,6 +15,7 @@ from .local_adapter import LocalLLMAdapter logger = logging.getLogger("hermes.router.adapter.ollama") DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434" +CLOUD_OLLAMA_BASE_URL = "https://ollama.com" DEFAULT_OLLAMA_MODELS = ["llama3:latest"] @@ -25,11 +26,34 @@ class OllamaAdapter(LocalLLMAdapter): and native Ollama endpoints (/api/tags). """ + def _stored_base_url(self, profile: RouterProfileConfig) -> str: + """Адрес из хранилища учётных данных, если владелец его задавал.""" + try: + from antigravity_provider.router.profile_manager import ProfileAuthManager + + stored = ProfileAuthManager.load_profile_auth(profile.provider, profile.profile_id) or {} + return str(stored.get("base_url") or "").strip() + except Exception: + return "" + def _resolve_base_url(self, profile: RouterProfileConfig) -> str: - """Resolve base_url from profile custom_base_url, auth_config, or environment.""" - url = ( + """Адрес сервера: локальный либо облачный. + + У аккаунта Ollama Cloud локального сервера нет вовсе — только ключ. + Раньше адаптер всегда шёл на 127.0.0.1:11434, получал «Connection + refused» и объявлял облачный аккаунт неработающим, хотя каталог из + девятнадцати моделей у него получался. + """ + explicit = ( profile.custom_base_url or profile.auth_config.get("base_url") + or self._stored_base_url(profile) + ) + if not explicit and self._resolve_api_key(profile): + return CLOUD_OLLAMA_BASE_URL + + url = ( + explicit or os.environ.get("OLLAMA_BASE_URL") or os.environ.get("OLLAMA_HOST") or DEFAULT_OLLAMA_BASE_URL @@ -45,6 +69,18 @@ class OllamaAdapter(LocalLLMAdapter): if key: return str(key).strip() + # Ключ, введённый в мастере, лежит в хранилище учётных данных, а не в + # auth_config из router_profiles.yaml. + try: + from antigravity_provider.router.profile_manager import ProfileAuthManager + + stored = ProfileAuthManager.load_profile_auth(profile.provider, profile.profile_id) or {} + key = stored.get("api_key") or stored.get("token") + if key: + return str(key).strip() + except Exception: + pass + suffix = profile.profile_id.upper().replace("-", "_") for candidate in (f"OLLAMA_API_KEY_{suffix}", "OLLAMA_API_KEY", "OLLAMA_TOKEN"): val = os.environ.get(candidate, "").strip() diff --git a/src/antigravity_provider/router/quota_collector.py b/src/antigravity_provider/router/quota_collector.py index 1c42a12..8dfc607 100644 --- a/src/antigravity_provider/router/quota_collector.py +++ b/src/antigravity_provider/router/quota_collector.py @@ -918,6 +918,31 @@ class AccountQuotaService: def _generate_baseline_snapshot(self, provider: str, profile_id: str) -> QuotaSnapshot: """Truthful offline baseline with provider-specific independent limit pools.""" now = _utc_now() + # У облачного аккаунта Ollama лимиты есть, и объявлять их + # отсутствующими нельзя. Локальный сервер узнаётся по отсутствию + # ключа: облачный доступ без ключа невозможен. + if provider == "ollama": + try: + from .profile_manager import ProfileAuthManager + + auth = ProfileAuthManager.load_profile_auth(provider, profile_id) or {} + is_cloud = bool((auth.get("api_key") or auth.get("token") or "").strip()) + except Exception: + is_cloud = False + if is_cloud: + return QuotaSnapshot( + account_id=profile_id, + provider=provider, + buckets=[], + fetched_at=now, + source="cloud_provider", + unavailable_reason=( + "Н/Д: лимиты облачного аккаунта Ollama не измерены — " + "провайдер не сообщает их через API" + ), + is_loading=False, + ) + if provider in ("local", "local-llm", "llama.cpp", "ollama", "vllm"): b_unlimited = QuotaBucket( id="local.unlimited",