From 9761362bc0ad0f016c6abc7c5b0397bd68ef6d8b Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Tue, 1 Sep 2026 02:55:39 +0700 Subject: [PATCH] =?UTF-8?q?fix(ollama):=20=D0=BE=D0=B1=D0=BB=D0=B0=D1=87?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=B0=D0=BA=D0=BA=D0=B0=D1=83=D0=BD=D1=82?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=8A=D1=8F=D0=B2=D0=BB=D1=8F=D0=BB=D1=81=D1=8F?= =?UTF-8?q?=20=D0=BD=D0=B5=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D1=8E=D1=89?= =?UTF-8?q?=D0=B8=D0=BC=20=D0=B8=20=D0=B1=D0=B5=D0=B7=D0=BB=D0=B8=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D0=BD=D1=8B=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Адрес. Адаптер всегда шёл на 127.0.0.1:11434, получал Connection refused и помечал аккаунт нерабочим — при том что каталог из девятнадцати облачных моделей у него получался. У Ollama Cloud локального сервера нет вовсе, есть только ключ. Теперь при заданном ключе и незаданном адресе адаптер идёт на ollama.com; явно указанный адрес по-прежнему в приоритете. Проверено: только ключ даёт https://ollama.com, заданный адрес — его же. Ключ. Как у NVIDIA и OpenRouter, он читался только из auth_config в router_profiles.yaml, куда мастер его не кладёт. Добавлено чтение из хранилища учётных данных. Лимиты. Провайдер ollama безусловно относился к локальным и получал подпись «Без ограничений (локальная модель)». Для облачного аккаунта это неправда: лимиты у него есть. Теперь облачный аккаунт (узнаётся по наличию ключа) показывает «Н/Д: лимиты не измерены — провайдер не сообщает их через API». Отдельно: отказ agy теперь доносит причину. Прежнее «код 1; каталог не получен» скрывало и текст ошибки, и главное — что agy запускается с HOME, подменённым на каталог профиля ради изоляции учётных данных. Если вход делался обычным agy в оболочке, ключи легли в настоящий домашний каталог, и профиль пуст. Теперь в сообщении и ответ agy, и использованный HOME. 610 passed, ruff clean, релизный гейт 10/10. Co-Authored-By: Claude Opus 5 --- src/antigravity_provider/agy_subprocess.py | 13 +++++- .../router/adapters/ollama_adapter.py | 40 ++++++++++++++++++- .../router/quota_collector.py | 25 ++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) 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",