---
name: risky-llm-provider-isolation
description: "Переносимый паттерн: как безопасно подключить бесплатный LLM-провайдер, который ВОРУЕТ/обучается на запросах (logfare.ai, unlimited.surf) — три слоя изоляции, чтобы он мог ТОЛЬКО узкую задачу (спорт-анализ) и ничего больше. Брать при добавлении такого провайдера в любой бот без конфиденциальных данных."
metadata:
  node_type: memory
  type: reference
  originSessionId: 6620ea3d-79ed-46e4-b5ea-65828f0cc0b8
---

⚠️ **СНЕСЕНО в Делор (2026-07-13, commit 25ad82d):** `models/risky_guard.py` + `models/atessa.py` удалены — спорт-консенсус (единственный живой потребитель на момент удаления) заменён одной моделью DashScope, leaky-голос atessa/aerolink больше не нужен ([[sport-reliability-quality-pref]], [[delor-sport-dashscope-lessons]]). Grep перед удалением не нашёл других вызывающих (crypto, упомянутый ниже как второй потребитель, к этому моменту уже НЕ импортировал risky_guard — статус устарел или крипто-скоуп был снят раньше отдельно, не проверено). **Паттерн ниже остаётся валидным** как переносимый рецепт для будущего бота — просто в Делор сейчас живой реализации нет; при реюзе — писать классы заново по этому образцу, а не искать старые файлы.

**Аудит всего контура + закрытая дыра (2026-06-16, commit c2487eb).** Полная карта «домен→модель→чувствительность»: leaky (atessa/fmd-freemodel/research/gemini-web) — ТОЛЬКО спорт; chat/1С/личное — trusted (mistral/gemini-cli/siliconflow/groq/nvidia/cerebras на шине). Дыра найдена и закрыта: `llm_health` self-heal слота **l2code** (code-чат) падал на tier-пул `"code"`, начинавшийся с `freemodel-dev/*` (leaky) — добавлена trusted-группа `HEAL_CANDIDATES_BY_GROUP["l2code"]` + вычищен freemodel-dev из общего code-tier. Инвариант закреплён `tests/test_contour_safety.py` («ни один не-спорт слот не лечится на leaky» + 1С только Mistral + code-tier trusted). **1С проверен отдельно — чист:** текст→mistral-цепочка, vision по скриншотам клиента→`cloud_l2_chat` (vision-tier весь trusted), RAG→только retrieval. Эмпирическая перепроверка — через учёт квоты #1 (домен sport/chat/onec/health виден в metrics.db). Деталь — Obsidian Decision D-contour-audit.

✅ **ПЕРЕИСПОЛЬЗОВАНО для aerolink (2026-06-17, commit c355c53).** aerolink = сторонний серый **Claude**-Code-прокси (`capi.aerolink.lat`, Anthropic `/v1/messages`, каталог ТОЛЬКО Claude: opus-4-7/8, sonnet-4-6, haiku-4-5, fable-5; не-Claude → 400; ошибки на китайском; 5h/weekly). Маркетинг-сайт `aerolink.lat` за Cloudflare-Turnstile (недостижим даже headless-Chromium), но **API-хост `capi.aerolink.lat` открыт даже с RU-IP мини** (прямой GET /v1/models = 200). Владелец смягчил «no Claude»: Claude от 3rd-party провайдера МОЖНО (не личная подписка), но «аккуратно — чтобы не читал/менял ПК». Решение — тот же паттерн, что atessa: **`AtessaClient` обобщён параметром `provider_label`** (одна изолированная Anthropic-реализация на оба серых шлюза); aerolink заведён спорт-**voter#8** `claude-sonnet-4-6`, off-bus + `assert_sport_only(provider_label)` (fail-closed вне scope). Проверено реально: вне `sport_analysis_scope` → отказ «aerolink: запрещён вне sport_analysis_scope», внутри → 4.0s «Футбол»; 1077 тестов зелёные. **Авто-ротация ключей (commit 9a1305c, как пул аккаунтов OmniRoute):** `AtessaClient.api_key` держит НЕСКОЛЬКО ключей (comma/space/newline) → `_KeyPool` с per-key cooldown; на 429/quota ключ уходит в cooldown (парсим «Resets at …», иначе 3600s quota/90s rate) и вызов прозрачно ретраится на следующем живом ключе; жёсткая ошибка (400) не выжигает пул. aerolink заряжен 3 ключами в plist `DELOR_AEROLINK_API_KEY` через запятую. 5 unit-тестов (`tests/test_key_rotation.py`) + live (пул=3). Один ключ → как раньше; механизм общий и для atessa. Ключи только в plist (com.cebers.delor + delor-sport). ⚠️ **launchd-грабли: `kickstart -k` НЕ перечитывает env из plist** — после добавления ключа нужен `launchctl bootout`+`bootstrap` (иначе voter молча выключается: `if model and key` → key пуст). Заодно **gemini-cli выведен из спорта** (умирал): primary→cerebras/zai-glm-4.7, live→cerebras/gpt-oss-120b (был БЕЗ фоллбэка!), fallback3→siliconflow/Qwen3-235B, deep_reviewer_fb→cerebras/zai-glm-4.7; `cloud_l2_chat` (vision/1С) переведён отдельно (commit 60b149d, 2026-06-17): gemini-cli → trusted **NVIDIA VLM** (primary `nvidia/meta/llama-3.2-11b-vision-instruct` ~1s, fallback `…90b-vision`, text-only `…llama-3.3-70b`), vision heal-пул тоже на NVIDIA. 1С=клиентские данные → только trusted, aerolink/серые шлюзы СЮДА НЕЛЬЗЯ (выбор владельца NVIDIA-only). ⚠️ NIM free-tier 404-троттлит при частых повторах (бурст ~1/3-1/4 успеха; одиночные сparse-скрины ок) — отсюда primary+fallback VLM + текстовая страховка. Проверено live: обе модели читают RU-скрин формы. Хелс спорт-голосов 2026-06-17: z.ai✅ fmd✅ cerebras✅ aerolink✅; siliconflow таймаутил (транзиент); atessa 429 (квота до 2026-06-18); gemini-web флапает; research-tail :20130 мёртв. См. [[cebers-pulse-llm-rules]].

Отработано и проверено в Делор (commit b78c884, 2026-06-14). Применимо к ЛЮБОМУ боту, где есть несекретная узкая задача и хочется бесплатные модели у провайдера, который сливает/обучается на запросах.

✅ **ПЕРЕСОБРАНО для atessa.top** (commit 30ebb78, 2026-06-15): паттерн вернулся в Делор в минимальном виде. `models/risky_guard.py` (булев `sport_analysis_scope()` + `assert_sport_only()`, без multi-purpose), `models/atessa.py` (Anthropic `/v1/messages`, парсит text/пропускает thinking), scope ставится в `sport_analyzer._ask`. atessa = спорт **voter#6** (аддитивный голос консенсуса, off-bus, ключ `DELOR_ATESSA_API_KEY` в plist; модель `deepseek-v4-pro` — единственная в пробе дала чистый text без thinking-проблем). Проверено реально: вне scope → RiskyProviderError без сети, внутри → живой ответ. ⚠️ долгую доступность atessa НЕ мерили — voter аддитивен, при отказе консенсус жив на остальных. Отличие от 2026-06-14: тогда рискованный был ЛИД-звеном цепочки; теперь — отдельный независимый голос.

⚠️ **СТАТУС (старое, logfare/unlimited): откатано** (commit 7e81eff, 2026-06-14, в тот же день). Не потому что паттерн плох — он отработал штатно (изоляция держала, 1С/чат к рискованным были недостижимы), а потому что сами бесплатные провайдеры (logfare.ai, unlimited.surf) в проде оказались **слишком ненадёжны**: доступность скакала 0/6…1/3 на тех же моделях за пару часов. Юзер: «снесём от греха подальше». Вывод-для-переиспользования: **этот способ безопасен и годен — но прежде чем строить вокруг бесплатного провайдера, замерь его реальную доступность за несколько часов; если она скачет до нуля, маржинальная выгода не стоит даже изолированного риска.** Паттерн ниже остаётся валидным для следующего бота.

**Принцип: рискованный провайдер должен СТРУКТУРНО мочь только одну задачу. Три независимых слоя:**

1. **Off-bus.** НЕ регистрировать его в общей LLM-шине (OmniRoute и т.п.). Иначе ротация ключей / auto-heal / другой проект случайно зароутят в него запрос → утечка. Если уже добавил узлом — удалить (`DELETE /api/providers/<connId>` → `DELETE /api/provider-nodes/<nodeId>`).

2. **Отдельный клиент, один конструктор.** Свой класс в `models/<provider>.py` с тем же интерфейсом, что и основной клиент (в Делор — `chat_text(*, model, messages, max_tokens, temperature) -> str`), бьёт НАПРЯМУЮ в API провайдера. Конструируется ровно в одном месте — обвязке нужной задачи (в Делор: спорт voter#1 в main.py). Больше нигде.

3. **Runtime-гард (ключевой слой) — `models/risky_guard.py`:** `contextvars.ContextVar` (хранит имя разрешённой задачи) + `risky_provider_scope(purpose)` + обёртки на задачу (`sport_analysis_scope()`, `crypto_analysis_scope()`) + `assert_risky_allowed(provider)` (raise `RiskyProviderError`, если scope не стоит). Каждый рискованный клиент в начале `chat_text` зовёт `assert_risky_allowed()`. Скоуп ставится в точках вызова моделей РАЗРЕШЁННЫХ задач (Делор: `sport_analyzer._ask` для спорта; `crypto_scanner._llm_signal` + `service._extract_news`/`_codex_narrative` для крипты). contextvars пробрасываются в `asyncio.gather`-таски, созданные внутри скоупа. Несколько задач — несколько `*_analysis_scope()`; **конфиденциальные задачи (1С/чат) просто не входят ни в один scope** → жёсткий блок. Итог: клиент, подключённый в неразрешённую задачу, **падает на вызове до отправки**. Проверка: вне любого scope RAISE, внутри sport/crypto — работает (unit-тест без сети: гард раньше HTTP). **Добавить новую разрешённую задачу = добавить `<task>_analysis_scope()` + обернуть её вызовы; ничего больше не трогать.**

**Применено к 2 задачам (Делор, 2026-06-14):** спорт (commit b78c884) + крипта (ea8007c). Обе — публичные данные. Рискованный лид → надёжный бэкстоп в обеих (sport voter#1, crypto chain[0]). 1С остаётся на freemodel-dev + отдельный хард-гард.

**Грабли клиентов (реальные):**
- **Стрим через основную шину может рвать соединение** (`STREAM_EARLY_EOF`/502). Для прямого клиента — **non-stream** или построчный стрим.
- **Нестандартный API** (unlimited.surf): body `{"message": "<одна строка>", "model", "effort"}` (без ролей — схлопывать system+user в строку), ответ кастомный SSE `data:{"delta":"..."}` → `done/finish`. Читать построчно через `resp.content` и **останавливаться на done/finish** — `resp.text()` ВИСНЕТ (сервер держит SSE-соединение открытым) → таймаут.
- **Модели флапают per-model 503** (не IP/гео — прокси НЕ помогает, проверено сменой egress). Лечится цепочкой: рискованная-модель-1 → рискованная-модель-2 → … → надёжный бэкстоп. Брать БЫСТРЫЕ модели (tier fast ~4с), не reasoning-флагманы (40с).
- **Доступность сильно скачет** (замер: один час 5/6–6/6, другой 0/3–1/3 на тех же слотах). Бесплатное = upside «когда живо», полагаться нельзя — ОБЯЗАТЕЛЕН надёжный платный бэкстоп.
- **КРИТИЧНО — короткий таймаут на рискованном клиенте** (Делор: 12с). Отказы бывают двух видов: быстрый 503 (~1.6с) и **зависание-таймаут**. При N рискованных ступенях перед платным длинный таймаут × N = десятки-сотни секунд впустую на вызов (был 45с×3≈135с — тормозило дренаж). Ставить timeout ~12с (живой ответ всегда ≤10с проходит, зависший быстро уступает платному).
- **Ключи** — в launchd-plist (`EnvironmentVariables`) / env, НИКОГДА в git/Obsidian/память. pydantic-поле с `alias="<PREFIX>_..._API_KEY"`.

**Надёжность+качество (предпочтение владельца, [[sport-reliability-quality-pref]]):** рискованный провайдер ставить ЛИД-звеном цепочки, а в хвосте — надёжный платный/стабильный (бэкстоп). Любой сбой рискованного → откат вниз, задача не падает. Консенсус нескольких независимых моделей НЕ ослаблять ради объёма.

Конкретика Делор/OmniRoute — [[omniroute-20129-openai]]; спорт-слоты — [[cerebras-groq-ru-ban]].
