---
name: vakhunter_web_ai_tariff
description: "ВакХантер веб — тариф-гейт ленты, кэш AI, ФИО, резюме-шаблоны, человеческий стиль AI; что где и как рестартить"
metadata: 
  node_type: memory
  type: project
  originSessionId: b11e39bd-4f6f-47c9-ba7b-680ccf041a5b
---

ВакХантер веб (2026-06-28, VakHunter `a84ce2c` + kodoceh-web `ee549b7`). См. полностью [[obsidian_documentation_rule]] → Projects/VakHunter Changelog/Decisions D36–D39.

- **Анти-ИИ-стиль:** одна константа `HUMAN_STYLE` в `utils/prompts.py` подмешана во все тексты-пользователю (cover letter, career_assistant, comparison, recommendations) — общий код бота И веба. Cover letter = `fast_model` (8b) + temp 0.75, подпись по ФИО.
- **Тариф-гейт ленты (был БАГ — веб игнорировал тариф):** `web/webapp.py` `_source_gate(uid)` режет `/api/vacancies`,`/api/hot`,`/api/filters` по `allowed_sources` (аноним=Trial; Pro=без ограничения). `_SOURCE_KEY_MAP`: ключ тарифа→строки `vacancies.source` (`avito`→{avito,avito-services}). Лента отдаёт `locked_sources` → баннер апселла.
- **Состав тарифов — ВАРИАНТ A (89b0e8c, итоговое решение, заменил вариант B):** Free(5)=telegram/trudvsem/gorodrabot/zarplata/habr · Standard(12)=+hh/superjob/rabota/fl/kwork/profi/getmatch · Pro(20)=+avito/freelancehunt/remoteok/wwr/geekjob/hh_kz/by/uz. **HH перенесён в платный Standard (был бесплатный), Avito остаётся Pro.** Гейт: Trial hh=0/avito=0/locked15, Standard hh>0/avito=0/locked8, Pro locked0. Проверено живой матрицей: все площадки отдают релевантное под разные профили (водитель/бухгалтер/повар/сварщик), НЕ только IT; IT-only = habr/geekjob/getmatch/remoteok/wwr/telegram-каналы (это их специфика). Матрица в одном burst-процессе даёт ложные 0/DNS у scrape-досок (fl/kwork/zarplata) — проверять liveness по свежести в БД, не standalone-скриптом.
- **Кэш AI:** таблица `ai_cache(user_id,kind,ref_key,...)` — один слот на (user, cover_letter|fit, vacancy). `force=true`=перегенерация. `GET /api/ai/cache?vacancy_id=` — фронт префетчит, показывает готовое при возврате в карточку.
- **ФИО:** `users.full_name`; в письмах/резюме; на фронте — поле в кабинете с явной подсказкой.
- **Резюме-шаблоны:** `web/resume_templates.py` (NEW) — схемы-«ячейки» HH/SuperJob/Habr/Универсальный, ИИ заполняет слоты (zemlya-стиль), `render_html` печатный. `POST /api/ai/resume_template`.
- **Рестарт:** web API = `launchctl kickstart -k gui/$(id -u)/local.vakhunter-web` (:20452, systemPython3.9); бот = `local.vakhunter`; фронт :20412 НЕ под launchd (см. [[kodoceh_web_build_gotcha]]). DB-миграция `full_name` идёт через `database.db.Database()` (на старте бота); `ai_cache` создаёт `web._init_db` (на старте веба).
- **getmatch-баг занятости (b22cd63):** getmatch.ru = доска ПОСТОЯННЫХ IT-вакансий (в API поля занятости нет), а парсер клал remote/any в `employment_type` → постоянка лезла в ленту юзеру с «только подработка». Фикс ДАННЫХ в источнике (не общего фильтра): getmatch → `employment_type="permanent"`, формат → ось `work_format` (`_detect_work_format` из `location_requirements[].format`); `scheduler._persist_vacancy` теперь прокидывает `work_format` (раньше пуст у ВСЕХ строк — ни один парсер не писал); бэкфилл 82 строк. Общий ленивый фильтр `matches_vacancy_employment` («неизвестный тип→пропустить») НЕ трогал — закреплён `test_matcher_undetected_passes` + soft-fallback R33 (by-design). E2E: «подработка»→getmatch=0, «any»→getmatch виден. См. D41. **Остаток:** hh/trudvsem/habr с занятостью `any` (~290) всё ещё проходят к gig-юзеру by-design — ужесточение под вопросом юзеру.
- **UX-фикс Trial (4f0d4f1):** экраны Профиль/Тарифы показывали «⏰ Тариф: не активирован» / «Бесплатно (7 дней)» (expires=NULL у вечного Trial) — выглядело сломанным. Правило: бесплатный тариф (price=0, без срока) → «♾️ бессрочно»/«Бесплатно навсегда». Приветствие «21 источник»→«20». Найдено живым проходом `tg-web-e2e` (бот мапит текст reply-кнопок на действия — слать подпись кнопки надёжнее клика по inline; компаратор тарифов — клик inline `.reply-markup-button:has-text("Сравнить")`).
- **Онбординг веба переустроен (0c4bb25/518351e, 2026-06-29, D42):** жалоба «профиль пуст / 250к→показывает 140-200к / подработка→полная / не видно что ИИ заполнил / онбординг каждый вход». Корни: (1) `web/webapp.py::ai_resume` ВЫБРАСЫВАЛ `employment_type`/`work_format` из разбора ИИ (а `parse_user_description` их даёт) → профиль занятости пуст → лента отдаёт полную; теперь отдаёт оба («any»→«»); (2) персонализ. лента не фильтровала по `desired_salary` (только тост «горячие») → добавлены `_salary_ceiling`/`_salary_ok` — политика «по потолку, мягко» (выбор юзера): прячем только если ВЕРХ < желаемой; «от X»/без-ЗП остаются; ручной `salary_min` главнее (`ctx.sal`); (3) визард `Setup` (`apps/vakhunter/app/app/page.tsx`) — был 5 шагов, ИИ-разбор только по кнопке, навыки/занятость не показаны; стал 3 шага (имя→рассказ→«Проверьте профиль»), рассказ авто-разбирается при «Далее», обзор показывает ВСЕ поля ИИ редактируемыми (+поле занятости); (4) модалка `Onboarding` рендерилась поверх визарда (дубль, «каждый вход») — теперь только после `me.specialty`. Проверено: API-repro (:20452, полная занятость 0/20, наруш. ЗП 0) + Playwright UI (:20412, визард без дубля, обзор заполнен ИИ, перезаход без визарда). Бэкап pre-fix: `~/.webapp.py.bak-onbfix`.
- **Подписки на ЗАКАЗЫ — проверены, работают (2026-06-29, аудит без правок):** «подписка на заказы» = подписка с `employment_type=freelance`/`part_time` (отдельного типа нет). Путь: `scheduler._process_subscription`→`resolve_subscription_with_user`(сохраняет явный employment подписки, пустое тянет из профиля)→`_parse_vacancies`(`filter_vacancies` строго) →`_send_vacancies`(дедуп `sent_vacancies`+fingerprint; веб-юзер uid<0→web-push). Заказ/вакансия делятся корректно — парсеры бирж (fl/kwork/profi/freelancehunt) ставят `employment_type='freelance'` на КАЖДУЮ строку → 0 утечек в обе стороны (проверено матчером + живым fetch+filter + дедупом). **By-design:** (a) Free(5) без бирж заказов → бесплатная подписка на заказы пуста (D40); (b) утренний дайджест/realtime (`filter_results_for_user`) при отсутствии свежих заказов показывает ближайшую постоянку с ЯВНОЙ плашкой «по строгому фрилансу точных нет — full-time» (R33, помечено). Детали — Obsidian Changelog 2026-06-29 (2).
- **Дисклеймер «заказы — со Standard» везде (5504133/4799508, 2026-06-29):** Free не отдаёт заказы → честно предупреждаем. Авторитетный флаг `web/webapp.py::_orders_available(uid)` (тариф ∩ {fl,kwork,profi,freelancehunt}) в `/api/filters`+`/api/subscriptions`. Веб `/app`: баннер в ленте при «Только заказы» + опция «🔒 (Standard)», заметка в форме оповещений при «фриланс». Лендинг: тариф-карточки + FAQ. Бот: сравнение тарифов (Trial «только вакансии» / Standard «🛒 заказы») + шаг занятости в подписке (гейт `get_tariff().allowed_sources`). Проверено: API-флаг F/T/T, Playwright (баннер+заметка), tg-web-e2e (сравнение тарифов).
- **LLM-чейн + безотказность (2026-06-30, D44, `8f285db`+`0cb8cac`+.env):** инцидент — NIM **llama-3.3-70b** (primary) стабильно ReadTimeout (free throttled), фолбэк-модель `zai/glm-4.7-flash` тоже легла → всё на 70b висело/None. NIM **8b** жив (на нём parse-профиля+cover_letter через `fast_model`). Фикс: (1) фолбэк → **OmniRoute :20129, ЦЕПОЧКА `FALLBACK_MODEL=mistral/mistral-small-latest,auto/fast`** (`b3deec7`, 2026-07-02: auto/fast=DeepSeek-V3.2 флапал 3/6 с 45с-таймаутами → узел mistral шины, свой офиц. ключ, 4/4 за 0.5–5.3с, поставлен первым; FALLBACK_MODEL = список через запятую, не-последним кэп 40с); ключ в .env = `sk-d5cc…opencode`. После цепочки смоук 10/10 за 0.5–4.1с. **opus НЕ использовать** (наш аккаунт). groq/cerebras на шлюзе = 403 (нет кредов). (2) **circuit-breaker** `_call_api`: фейл primary → `_primary_skip_until=monotonic()+300с` → сразу фолбэк без 30с-налога; успех сбрасывает; singleton держит состояние. Кэп primary-таймаута 30с. Смоук-скрипт `~/.claude/skills/vakhunter-e2e/tools/vh_ai_probe.py` (прямой прогон каждой модели + форс-фоллбэк + 4 сквозных функции). Все 3 порта 20127/28/29 = одна OmniRoute.
- **⚡ Свап primary на mistral-small (2026-07-03, `b58f8cd`+.env):** аудит показал — NVIDIA 70b (была primary у 7/8 функций) деградировала намертво: 21-26с когда отвечает, 3/5 таймаут 30с. mistral-small (OmniRoute) 0.5с, качество не хуже. **Теперь primary = OmniRoute mistral-small, fallback = NVIDIA 8b→70b.** РЕАЛИЗАЦИЯ: ai_assistant читает `PRIMARY_API_BASE/KEY/MODEL/MODEL_FAST` (фоллбэк на NVIDIA_* если пусты). **Старое правило «NVIDIA_* не переназначать» БОЛЬШЕ НЕ АКТУАЛЬНО так, как было** — теперь primary регулируется через `PRIMARY_*`, а `NVIDIA_*` штатно остаются на NVIDIA (их читают agents/run_agent — dev-инструмент, не в живом боте — и config/settings/bot/config, но у тех НЕТ потребителей значения). `.env` прод: PRIMARY_*=OmniRoute mistral, NVIDIA_API_KEY=nvapi (агенты+fallback). Живой прогон 9/9, коуч 20с→1-5с. Откат = очистить PRIMARY_* в .env.
- **⚡⚡ 3-уровневая цепочка ИИ (2026-07-03, `320ba4f`+.env):** добавлен 2-й фолбэк-тир. `_call_api` рефакторен → `_try_fallback_tier(prefix)` вызывается для `FALLBACK` и `FALLBACK2` (каждый = свой base/key/scheme + список моделей). **Цепочка: primary mistral-small(:20129) → tier1 `zai/glm-4.5-flash`(:20129, семья Zhipu, боевая модель CebersPost, ловит сбой mistral-апстрима, ~1.5с) → tier2 NVIDIA 8b→70b (другая инфра, на случай падения всей OmniRoute).** ГРАБЛИ glm: со `stream:false` → чистый JSON (совместимо); БЕЗ — SSE (наш non-stream клиент не парсит). **Отвергнуто как chat-фолбэк:** CebersResearch(:20130) + все web2api-мосты (gemini :1717, deepseek :1718) — стабильно пустой content (браузерные скрейперы, заточены под /research+/search, не chat); api.mistral.ai прямой — ключ мёртв 401. Провайдер-независимость держит ТОЛЬКО tier2 NVIDIA (glm на той же OmniRoute-коробке = диверсити модели, но не переживёт падение OmniRoute). Проверка: `vh_fb_chain_probe.py` (каскад 4/4) + `vh_ai_probe.py` (8 функций). Откат = убрать FALLBACK2_*, FALLBACK вернуть на NVIDIA.
- **⚡⚡⚡ Groq в tier1 (2026-07-03, .env):** разведка каталога OmniRoute (3548 моделей) — **`groq/llama-3.3-70b-versatile` идеален: 0.2-0.3с, 6/6, Groq LPU** (IP-бан groq обходится маршрутизацией через OmniRoute — прямой с mini банится, см. [[cerebras_groq_ru_ban]]). **Актуальная цепочка (2026-07-03, `4b71fce`): primary `groq/llama-3.3-70b-versatile` (0.3с) → tier1 `zai/glm-4.5-flash` → tier2 NVIDIA `8b,70b` (независимый) → tier3 `codex/gpt-5.4-mini` → tier4 `mistral/mistral-small-latest` (ПОСЛЕДНИЙ).** `_call_api` цикл = FALLBACK/FALLBACK2/FALLBACK3/FALLBACK4. PRIMARY_MODEL+FAST=groq. **codex/gpt-5.4-mini (tier3):** ChatGPT-пул юзера через OmniRoute :20129; лучшее качество в цепочке, чистый JSON/RU, thinking не течёт; латентность 2-27с (reasoning-переменная). Стоит ВЫШЕ mistral, т.к. codex-квоту НЕ делят фоновые сервисы (только ручной кодинг юзера). `codex/gpt-5.2`=HTTP 400. **mistral (tier4) в конце НАМЕРЕННО:** его квоту (`mistral/`=собственный апстрим Mistral, `owned_by:mistral`, НЕ NVIDIA!) делят постоянные фоновые сервисы — зовётся только если groq+glm+NVIDIA+codex легли. ОТВЕРГНУТО (все требуют пополнения/деградировали): RouteWay `api.routeway.ai` (ключ `sk-3__2kK…`, валиден, но 402 Insufficient funds на всех); AIHubMix Free (`aihub/*-free` в OmniRoute — заглушка «Sorry, to prevent abuse… recharge»); zyloo/r9 в каталоге OmniRoute = «No active credentials» (r9 2055 пустышек!); cerebras флапает; siliconflow 25-40с. Провайдер-независимость (переживёт падение всей OmniRoute) держит ТОЛЬКО NVIDIA.
- **⚠️ ruff pre-commit в VakHunter:** `%`-format → UP031 fail. В строках с `{}` (CSS) использовать токен+`.replace`, не f-string/.format. Py3.9: backslash в f-string запрещён.
