---
name: CebersResearch
description: meta-gateway :20130 над gemini/deepseek/qwen web2api с OpenAI-совместимым API и Bearer auth. **Прод = v0.6.3** (ddgs курированные движки yandex/brave/google + SOCKS5 :2080). v0.6.4 (full-text grounding через trafilatura) была собрана и задеплоена 2026-06-08, но **ОТКАЧЕНА** — до/после A/B на нишевых вопросах показал, что full-text без фильтра качества источников вредит (тянет мусорные блоги → фактические ошибки). Подробно — v0.6.4 SHORT ниже + Decisions D30 (reverted) — resilience layer (v0.4.x) + Phase 2 (qwen-search + SSE + Reflection) + Phase 3 (PDF/arXiv) + v0.6.1 hardening (R1-R4) + v0.6.2 self-recovery (R5 probe retry 3×2s, R6 false-down gateway kickstart на degraded, R7 silent chat-auth recovery через deep-probe). Корень флаки-deepseek-probe: proxy.py=uvicorn single-loop, кратко блокируется sync curl_cffi — решили hardening gateway, НЕ трогать чужой proxy.py (D26). Все слои E2E PASS.
metadata: 
  node_type: memory
  type: project
  originSessionId: d66da292-7e67-41c7-9f4f-d2ff61ffbf08
  modified: 2026-08-04T11:01:52.853Z
---

## G58 SHORT (2026-08-04, gemini auth-фикс + gemini-pro на 2-м аккаунте)

**Устаревшее утверждение ниже (v0.2 SHORT, «cookie не поможет — РФ привязка неустранима») — ОПРОВЕРГНУТО.** Реальная причина пустых ответов `research/gemini-flash` была тройная: устаревший `gemini_bl` (внутренняя build-версия), IP-гейт Google (кука с VPN-аккаунта ≠ exit-IP исходящего прокси mini), и баг `urllib.request.ProxyHandler` (не умеет по-настоящему SOCKS5, рвёт на строгом sing-box). Фикс — `gemini-web2api` (:1717) переведён на HTTP-прокси-инпут sing-box (:2092, тот же exit-IP что и личный VPN юзера) + свежая cookie-сессия через официальный `gemini-cookie-sync-extension`. Итог: 8/8 → надёжно. Полный разбор — investpilot Decisions.md **D54**, переиспользуемый урок про urllib/SOCKS5 — [[python_urllib_socks5_proxyhandler_gotcha]].

**Добавлена `research/gemini-pro`** (по аналогии с `gigachat-ultra` — отдельная модель на отдельной сессии/квоте). Юзер завёл ВТОРОЙ Google-аккаунт с подпиской Google AI Pro, экспортировал его cookie (auth_user=3 в URL `/u/3/app`, извлечён из скриншота модельного селектора — подтверждено наличие `3.1 Pro` в списке, не заблокировано). Отдельный инстанс `gemini-web2api-pro` (symlink на тот же `gemini_web2api.py`, свой `config.json`) на порту **:1723** (НЕ :1719 — тот зарезервирован под `QWEN_URL`/`research/qwen-search` в CebersResearch, хоть физически qwen-free-api и ретирован; чуть не создал скрытую коллизию имён, поймал до деплоя). `main.py` патч: константа `GEMINI_PRO_URL`, health-probe `gemini-pro`, новая model-entry. Живой тест: 3/3 верных ответов (17×23, 19×21, 31×29) через прямой инстанс и через полную цепочку `:20130/v1/chat/completions`. Обе кукифайла (`gemini-auth.json` — основной аккаунт, `gemini-auth-pro.json` — Pro-аккаунт) `chmod 600`, не в git.

**Экспортёр куки:** `gemini-cookie-sync-extension` из апстрима `gemini-web2api` (загружен как unpacked в Chrome юзера, dev-mode) — читает cookies+xsrf+bl локально через `chrome.cookies`/`chrome.scripting`, никуда не шлёт, экспорт через нативный `chrome.downloads.download({saveAs:true})`. Код вручную вычитан перед установкой — чисто. Извлечение auth_user из URL (`/u/N/`) иногда не срабатывает (первый экспорт Pro-аккаунта вернул `auth_user: null`, хотя вкладка была `/u/3/`) — если так, просто выставь `auth_user` в `config.json` руками по номеру из адресной строки, не полагайся слепо на автодетект расширения.

---

## G57 SHORT (2026-07-09, FreeModel-синтез восстановлен)

`/v1/research` падал `synthesis failed: freemodel | qwen` — синтез-цепочка `_do_synthesis` (FreeModel:21489→DeepSeek:1718→Qwen:1719) вся мертва: шим :21489 деинициализирован (нет порта/плиста/исходника), Qwen ретирован, DeepSeek пустой; freemodel и на OmniRoute (`fmd/`) без кредов. Юзер дал ключ `api.freemodel.dev` (`fe_oa_…`, НЕ в память/git — только `~/.codex/omniroute.env` chmod600 как `FREEMODEL_API_KEY`). Фикс: `FREEMODEL_URL` → прямой `https://api.freemodel.dev/v1/chat/completions` (OpenAI-совмест., модели gpt-5.5/5.4/5.4-mini), Bearer добавлен в `_call_upstream` (research) И `_proxy_chat` (чат). Проверено: research medium 9с/5 источников/цитаты, synth_provider=freemodel:gpt-5.4-mini; чат research/gpt-5.4-mini→«Мадрид». Бэкап `main.py.bak-pre-freemodel-direct-20260709`. ОСТАЁТСЯ: Delor voter#7 (`fmd/` через OmniRoute) без кредов — ключ надо внести в провайдер `freemodel-dev` дашборда OmniRoute :20129 (sudo/cookie). В env есть старая неиспользуемая `FREEMODEL_API_KEY2`.

## v0.6.5 SHORT (2026-07-09, фикс ежедневного degraded-флапа)

Тайл ops_dashboard мигал running→degraded→running каждый день ~19:19/~19:53. Корень — **наш собственный** `_do_search` (ddgs, ~2.3с/запрос, синхронный) звался ПРЯМО в async-хендлерах (`web_search`, `_handle_rag`, `_research_pipeline`×2) → event-loop uvicorn заморожен на время поиска → тривиальный `/healthz` не отвечает за 5с (таймаут health-check дашборда) → ложный degraded. Триггер по времени = investpilot вечерним сканером (19:15 digest, 49 бумаг) ходит на :20130 за веб-контекстом по каждой бумаге. Это ВТОРОЙ источник блокировки лупа помимо чужого proxy.py из D26 — но этот в нашем коде, чинить можно. **Фикс:** 4 вызова обёрнуты в `await asyncio.to_thread(_do_search, …)`. Проверено живьём: 8 параллельных реальных поисков (~18с суммарной блокировки) → `/healthz` держал 0.01с (под старым кодом >5с). ГРАБЛЯ проверки: `/v1/search` требует Bearer = `OMNIROUTE_API_KEY` (= INVESTPILOT_OMNIROUTE_API_KEY, сверено md5) — без него 401 не доходит до _do_search и стресс фальшиво «проходит». Бэкап `main.py.bak-pre-tothread-20260709`. Obsidian D (to_thread).

## v0.6.4 SHORT (2026-06-08, G1 full-text grounding — ⛔ ОТКАЧЕНО + coworker recon)

> **ИТОГ: full-text ОТКАЧЕН, прод на 0.6.3.** Юзер попросил до/после тесты ПЕРЕД тем как оставлять. До/после на нишевых вопросах (0.6.3 :20139 vs 0.6.4 :20130): Django 3/7→**2/7** (хуже), PG17 5/6→5/6 но AFTER **соврал** («summarizer вкл по умолчанию» — на деле `summarize_wal` off, надо включать), uv 5/7→6/7 (чуть лучше). Итог: не улучшение + латентность → откат. **Корень:** trafilatura тянет полный текст без фильтра качества → мусорный блог перевешивает доку. **Урок:** валидировать на нишевых ДО деплоя; full-text вернётся только с приоритезацией авторитетных доменов + фильтром качества + гибридом сниппет+текст. Бэкап 0.6.3 = `main.py.bak-0.6.3-20260608-093153`; trafilatura осталась в venv (инертна).

**Запрос:** юзер — не подключить ли оплаченный **coworker.ai** как провайдера research («много моделек»).

**Recon coworker (Playwright на мини, лог сети):** каталог топовый (Opus 4.8, GPT-5.5, Gemini 3.1 Pro, Kimi 2.6), Firebase-auth (вход email/пароль обходит Google-блок автоматизации; refresh_token минтит ID-token без Google-входа). **НО чат по постоянному Firestore-WebChannel — REST-эндпоинта для переигровки НЕТ** (отправка сообщения = 0 новых HTTP). Шим = реверс Firestore-схемы или хрупкий headless-браузер. **Решение D31: не интегрируем** (+ A/B уже доказал: research упирается в grounding, не модель; те же модели есть в OmniRoute чисто). coworker оставлен для ручного использования.

**Вместо шима — вложились в grounding (выбор юзера «full-text всегда вкл»):**
- **Корень потолка:** синтез видел только сниппеты (~150 симв), `_fetch_full_content` умел лишь PDF/arxiv.
- **G1 (main.py 0.6.3→0.6.4):** `trafilatura 2.1.0` для HTML main-text; `_detect_source_type`+`html`; `_enrich_with_fulltext` — параллельный (Semaphore=5) фетч топ-K с таймаутом 12с + fallback на сниппет; `_trim_to_relevant` BM25-lite (без эмбеддингов); DEPTH_CONFIG +fetch_k/per_src_chars (shallow 4/3000, medium 6/2500, deep 8/2200); Step 2.5 always-on + reflection-источники тоже; ctx-cap 4500 для full-text. env: `FULLTEXT_CONCURRENCY`, `HTML_FETCH_TIMEOUT_S`. **Decisions D30**.
- **A/B verified (prod 0.6.3 vs staging :20139):** html 4–6 источников/запрос (было 0), ответы полнее (VACUUM: +пункт про фрагментацию индексов), +10–20с латентности. Deploy подтверждён на :20130 (Python 3.13 → html:6, 27.5с).
- **Безопасность:** watcher пробит только `/healthz`+deep-probe (чаты), НЕ `/v1/research` → латентность research не вызывает ложный down. trafilatura в venv (requirements.txt в проекте нет). Бэкап `main.py.bak-0.6.3-<ts>`.

---

## v0.6.3 SHORT (2026-05-31, ddgs search quality + proxy)

**Запрос:** «research работает?» → да (не виснет, ответ 10–50с), но поиск подмешивал мусор.

**2 причины + фикс (`_do_search` в main.py):**
1. ddgs `backend="auto"` крутит grokipedia/wikipedia → фаззи-мусор (White Lotus Resort, Temu, Azure DevOps). Фикс: курированные движки `_DDGS_TEXT_BACKENDS="yandex, brave, google"` (env), пробуем ПО ОДНОМУ стоп-на-первом-непустом (не список — тот = 5× запросов → бан). yandex первый (кириллица).
2. Анти-бот блок прямого IP (yandex/google → «No results found»). Фикс: **ddgs через xray SOCKS5 `:2080`** (env `DDGS_PROXY`, дефолт on). `_do_search` = proxy-first → direct-fallback (если xray лёг). Доказано: direct «No results» 0.3с vs proxy 4 чистых 0.7с в одну секунду.

**Verified end-to-end (при активном блоке прямого IP):** /v1/search RU+EN чисто; /v1/research RU search **1.18с** (было 44–65с), 5 чистых источников, корректный ответ «ПСЖ выиграл ЛЧ 2025/26» с цитатами. Decisions **D29**. Версия 0.6.2→0.6.3.

⚠️ Я сам спровоцировал блок прямого IP интенсивным тестированием ddgs — спадёт за десятки минут. Прокси это обходит, так что прод не затронут.

---

## v0.6.2 SHORT (2026-05-31, self-recovery R5–R7)

**Запрос юзера:** «чтобы cebersresearch **сам мог восстанавливаться**» (активно пользуется).

**Реальный кейс при проверке:** gateway репортит `deepseek down (ReadTimeout 3017ms)`, но direct `curl :1718/v1/models` = 9ms, внешний Python с тем же httpx = 4ms. Висит `degraded`, ALERT есть, **ничего не чинится** (R1 kickstart только на `critical`=все3down, не на `degraded`).

**Корень (диагностика):** `proxy.py` (deepseek-free-api) = **uvicorn single event loop**, кратко блокируется sync `curl_cffi` во время чата/cleanup → `/v1/models` на секунды не отвечает. Probe в окне = timeout; внешние curl мимо окна. Интермиттентно (5 probe подряд через 31s = все ok 21-29ms). **Решение: не трогать чужой proxy.py (4560 строк, риск), hardening нашего gateway (D26).**

**3 фикса:**
1. **R5 probe retry** — `_probe_one` 3×2s (было 1×3s) + backoff 0.25s. Один transient не флипает в down. `_PROBE_ATTEMPTS=3`, `_PROBE_TIMEOUT_S=2.0`. 🧪 mock-сервер: 3s stall→ok, 5s stall→down, healthy→ok. PASS.
2. **R6 false-down kickstart** (watch.sh) — если gw репортит down, а direct-curl=200 на **2 тика** → kickstart gateway. Расширяет R1 c critical на degraded. Helpers `probe_status()`/`direct_ok()`. Counter `.falsedown_count.ts` (сброс на любой не-false-down). Guard: qwen/deepseek autologin теперь требует `*_DIRECT=0` (реальный outage), иначе R6.
3. **R7 silent chat-auth recovery** (watch.sh, после deep-probe) — парсит последнюю `deep_probe.jsonl`; если чат упал с auth-паттерном (`авторизаци|invalid_token|unauthorized|не удалось создать чат|40003|401`) И НЕ timeout → авто-релогин qwen/deepseek (reuse cooldown). **Главная дыра:** silent qwen «токен авторизации» при GET-probe=ok раньше НИКОГДА не триггерил релогин (D11/D15). 🧪 классификатор 4/4 PASS + реальный fire на крафченном auth-fail → qwen «pong».

**Bonus:** вживую вылечен реальный qwen chat-token сбой (релогин profile-cookies). Версия→0.6.2. Бэкапы `*.bak-pre-selfheal`.

**Decisions D26 (не чинить proxy.py), D27 (probe 3×2s, уточняет D11), D28 (recovery на degraded+silent, заменяет D15 частично).**

**Отложено (предложено, ждёт подтверждения юзера):** traffic-driven sentinel — на auth-ошибке в реальном чате gateway пишет sentinel → релогин за ≤5мин (vs ≤30мин R7); trade-off: трогает hot-path `_proxy_chat`.

---

## v0.6.1 SHORT (2026-05-30 evening, hardening after wedge incident)

**Реальный инцидент 30 мая:** gateway сам wedged 25 мин. Все upstream напрямую отвечали 200, но gateway probe timeouts 26s. Recovery storm (много parallel `edit_limit` → `_try_deepseek_rotate` subprocess timeout 30s в asyncio loop) видимо exhausted httpx connection pool / event loop tasks. Watcher детектил "all upstreams down" но trigger'ил qwen/deepseek autologin (бесполезно). Ручной kickstart починил.

**5 fixes (R1-R5):**

1. **R1 self-kickstart в watcher** — `.critical_count.ts` counter, при 3 ticks подряд + direct upstream probes (`/v1/models` :1717/:1718/:1719) показывают ≥2 ok → `launchctl kickstart -k CebersResearch`. Cooldown 10 мин. Log `self_kickstart.log`.

2. **R2 asyncio.Lock на recovery** — `_DS_ROTATE_LOCK` + `_DS_AUTOLOGIN_LOCK`. При `lock.locked()` → fast `return False` (вместо ждать). Bounded timeouts: rotate 30s→15s, autologin 120s→60s. Explicit `proc.kill()` при timeout.

3. **R3 middleware 200s** — `asyncio.wait_for(call_next, 200)` на все handlers. Streaming bypass убран — middleware оборачивает только handler entry-to-return, не SSE body iteration. При timeout → HTTP 504. Configurable через `REQUEST_TIMEOUT_S` env.

4. **R4 RSS memory** — `_self_process_rss_mb()` через `ps -o rss=`. В `/healthz?detail=1` поле `self_rss_mb`. При >500MB status=degraded + signal. Watcher: при >600MB preventive kickstart.

5. **R5 E2E test** — `kill -STOP gateway` → upstream direct probes still ok → watcher 3 ticks → counter=3 → wedge detect → kickstart → recovered (~15s от STOP до new PID).

**Smoke 6/6 PASS** (v0.6.1, qwen-search, SSE, reflection, legacy).

**Decisions D22 (self-kickstart logic), D23 (Lock+timeouts), D24 (middleware), D25 (memory tracking).**

---

## v0.6.0 SHORT (2026-05-30, Phase 3: PDF/arXiv parsing)

**Two additions:**

1. **`POST /v1/fetch`** — standalone PDF/arxiv extractor
   - `pymupdf 1.27.2` (lazy import `fitz`) в `~/.venvs/CebersResearch/`
   - arxiv.org/abs/X → rewrite /pdf/X.pdf (regex)
   - SHA256 cache в `logs/fetch_cache/<hash>.txt`, 100MB limit с hysteresis eviction до 80%
   - asyncio.to_thread для CPU-bound PDF parse (не блокирует event loop)
   - Возвращает {url, normalized_url, source_type=pdf|arxiv|unsupported|error, content[:max_chars], content_length, cache_hit, fetched_in_ms}
   - Smoke: arxiv 1706.03762 (Attention Is All You Need) → 39511 chars за 629ms, cache hit instant

2. **`body.fetch_full=true` в `/v1/research`**
   - После search → filter PDF/arxiv URLs → fetch для каждого
   - В each source: replace `snippet` → truncated full content (6000 chars), preserve original в `snippet_preview`, set `source_type="pdf"`
   - On fail → `source_type="snippet"` + `fetch_error`
   - SSE events: `fetch_start{n_candidates}`, `fetch_progress{current,total,url}`, `fetch_done{n_fetched,took_s}`
   - Final done event: `fetch_full_used: bool`, `timings.fetch_s, .n_fetched`
   - E2E: query `site:arxiv.org Attention Is All You Need scaled dot product` → 1 PDF fetched → synthesizer **прoцитировал точный фрагмент** из abstract Vaswani et al.

**Также: fixed pip shebang** — был от старого research-gateway имени venv (renaming не покрыл). `sed -i` фикс на `CebersResearch/bin/python3.12`.

**Decisions D21 (pymupdf vs alternatives, arxiv URL rewrite, SHA256 file cache, threading PDF parse).**

**E2E: 4 P3 tasks complete. Final state: 5 endpoints (/v1/{models,chat/completions,search,research,fetch}), 8 models + RAG.**

**Не делали в Phase 3:** HTML scraping не-arxiv (Nature/Science), multi-page chunking + re-rank, vision/figures, citation-graph traversal, proxy для arxiv (пока ходим direct).

---

## v0.5.0 SHORT (2026-05-29 deep night, Phase 2)

**Three features added:**

1. **research/qwen-search** — Qwen3-Max с native web search (chat.qwen.ai featureConfig.auto_search=true)
   - **FreeQwenApi patched** (3 точки в src/api/{chat.js,routes.js}): bak`*.bak-pre-search`. buildPayloadV2 accepts `search=false`, sendMessage прокидывает, routes.js парсит body.search
   - В CebersResearch model spec: `extra_body: {search: true}` инжектится в upstream payload
   - Smoke: latency ~2s, X-Upstream без fallback

2. **SSE streaming /v1/research** — `body.stream=true` → text/event-stream
   - `_research_pipeline` async generator yields events: planning_start, sub_queries_ready, search_progress×N, search_done, synthesis_start, first_answer, done (+reflection_* при reflect=true)
   - Non-stream (default) collect-then-return JSON для back-compat
   - Smoke: 8 events на shallow research, 11s total

3. **Reflection multi-round /v1/research** — `body.reflect=true` (default false)
   - После first synth — Qwen-call ("SUFFICIENT" или 1-3 extra queries)
   - При extra → ddgs search round + re-synth с extended context
   - Filter дубликатов с already-searched sub-queries, max 3 extra
   - Smoke на quantum computing: first "insufficient" → 12 extra sources (6→18) → final с конкретными IBM/Google/Microsoft/Amazon/IonQ/D-Wave. 41.9s total
   - Smoke на простой запрос: reflection_sufficient event, без extra round

**Other v0.5.0:**
- Helpers `_do_synthesis(messages, model)` + `_format_sources_ctx(sources)` shared между first/reflection synth
- MODEL_INDEX supports `extra_body` field для model-specific payload injection
- response field `reflection_used: bool` + `timings.reflection_*`

**Decisions added: D18 (patch FreeQwenApi vs alternatives), D19 (SSE dual-mode endpoint), D20 (reflection opt-in)**

**E2E smoke v0.5.0: 6/6 PASS:**
- v0.5.0 healthz
- qwen-search в /v1/models (9 моделей теперь)
- qwen-search chat (X-Upstream без fallback)
- SSE streaming 8 events
- Reflection (sufficient + extra-queries cases)
- Legacy non-stream back-compat

**Не сделано в Phase 2 (отложено):**
- Gemini Web Search reverse — низкий ROI (anonymous + РФ-блок)
- Long-context map-reduce synthesis
- PDF/arXiv parsing
- Vision в RAG/research
- Embedding-based dedup

---

## v0.4.3 SHORT (2026-05-29 night)

**Resilience layer:**
- `_proxy_chat`/`_call_upstream` reactive recovery: DeepSeek edit_limit→rotate (60s cooldown), INVALID_TOKEN→autologin (30min cooldown). One retry per request.
- B1 fallback: deepseek-chat→qwen-max, deepseek-reasoner→qwen-thinking (non-stream). Header X-Fallback.
- Watcher `com.cebers.CebersResearch.watch` 5min: probes /healthz?detail=1, on qwen/deepseek down → autologin + kickstart. Cooldowns: qwen 10min, ds 30min.
- Deep probe каждые 30min: реальный mini-chat per модель → logs/deep_probe.jsonl
- ddgs retry chain: region requested → us-en → ru-ru/wt-wt с jittered backoff
- ALERT_PERSISTENT sentinel при degraded ≥15min

**Recovery scripts** (`scripts/`):
- `qwen_autologin.py` — Playwright async, persistent profile `~/.config/qwen-autologin-profile/`. localStorage.token sniff.
- `deepseek_autologin.py` — Playwright async, persistent profile `~/.config/deepseek-autologin-profile/`. **Network-sniff Bearer** через `context.on("request", ...)` фильтр `len≥40` (НЕ localStorage.userToken — это 30-char alias!).
- `deepseek_rotate_session.py` — curl_cffi impersonate=chrome120 POST chat_session/create. Runs в `~/.venvs/deepseek-free-api/bin/python` (имеет curl_cffi).
- `watch.sh` — orchestrator
- `deep_probe.py` — observability
- `captcha_solver/` — reuse zemlya-bot puzzle-slider (резерв, не активирован — текущие аккаунты без капчи)

**Venvs:**
- `~/.venvs/CebersResearch` — main FastAPI
- `~/.venvs/deepseek-free-api` — upstream + curl_cffi для rotate
- `~/.venvs/qwen-autologin` — Playwright 1.60 (используется обоими DS+Qwen autologin)

**Creds (chmod 600, не в git/memory/obsidian):**
- `~/.codex/qwen_creds.env`: QWEN_EMAIL/QWEN_PASSWORD
- `~/.codex/deepseek_creds.env`: DEEPSEEK_EMAIL/DEEPSEEK_PASSWORD (те же что Qwen)

**Coverage:** edit_limit + INVALID_TOKEN + WAF expire + process down (DS+Qwen) + ddgs rate-limit + any chat error + тихая поломка (port жив auth invalid). Не покрыто: mini offline, API breaking changes, IP-ban Cloudflare/AWS, РФ-блок всех движков, Gemini anonymous null (by-design).

**E2E подтверждения:**
- Live edit_limit catch → rotate → retry succeeded (51s total, X-Upstream без fallback)
- Corrupt config → INVALID_TOKEN → autologin rc=0 → recovery 30s
- Comprehensive smoke 13/13 PASS

---

## Старое (Phase 1.5) ниже — keep для контекста

## Статус (2026-05-29)

**Phase 1.5 запущена**: launchd `com.cebers.CebersResearch` v0.2.0, порт **20130**, Bearer = `$OMNIROUTE_API_KEY` (из `~/.codex/omniroute.env`). Файлы: `~/Разработки/CebersResearch/main.py` (~310 строк FastAPI), venv `~/.venvs/CebersResearch/` (+ddgs). Логи `~/Разработки/CebersResearch/logs/launchd.{out,err}.log`.

**Новое в v0.2-0.3 (2026-05-29 вечер):**
- **`POST /v1/search`** — прямой web search через `ddgs` (мета-поиск Google/Bing/Brave/DDG/Yandex). Без auth upstream, без лимитов. Body: `{"query":"...","max_results":5,"kind":"text|news|images","region":"wt-wt"}`. Latency 1-2s. Возвращает `{query, count, results:[{title, url, snippet}]}`.
- **`research/search-rag`** virtual model — single-step agentic RAG: extract user query → ddgs search 6 results → DeepSeek synthesizes with [N] citations. End-to-end 10-20s.
- **`POST /v1/research`** (v0.3) — **multi-step Deep Research orchestrator**. Body: `{"query":"...","depth":"shallow|medium|deep"}`. Шаги: Qwen-planner → N параллельных ddgs searches → DeepSeek synthesis (с fallback на Qwen при edit_limit) → JSON с answer + numbered sources + sub_queries + timings. Depth: shallow 2×3 (~12s), medium 4×5 (~30s), deep 6×6 (~60-90s, до 36 уникальных источников). **Fallback chain**: synthesis сначала пробует DeepSeek (лучше), при edit_limit автоматом Qwen3-max (без лимита, стабильно). Подтверждено в проде 2026-05-29.

**Tried and rejected:**
- ~~DuckDuckGo AI Chat (duck.ai)~~ — JS-challenge `x-vqd-hash-1`, требует Puppeteer-стек. Webscout AUTO mode перебирает 193 провайдеров, нестабильно.
- ~~Gemini AI Studio API direct~~ — `GEMINI_API_KEY` отдаёт `quota limit:0` (РФ-блок Google, тот же что в Web UI).
- ~~HuggingChat / Tavily~~ — не пробовали, требуют регистрации, отложены в пользу ddgs (быстрее, надёжнее, без auth).
- ~~SearXNG docker~~ — docker нет на mini, и ddgs покрывает use case как Python-lib без containerization.

**7 моделей** через `/v1/models` и `/v1/chat/completions` с префиксом `research/*`:
- `research/deepseek-search` — нативный web search ✅ (единственная модель с реальным search в phase 1)
- `research/deepseek-reasoner` — R1 thinking
- `research/deepseek-chat` — V3
- `research/qwen-max` (qwen3-max), `research/qwen-thinking` (qwen3.5-plus)
- `research/gemini-flash`, `research/gemini-thinking` (gemini-3.5-flash + thinking)

`/healthz` без auth. `/v1/models` и `/v1/chat/completions` требуют Bearer. Не-известные модели → 404 со списком доступных. Stream pass-through нативно (`aiter_raw`).

**Smoke**: 4/4 текстовых моделей отвечают (0.7-4s). Gemini периодически `null` (Sophomoresty Google rate-limit на anonymous, cookie не поможет — РФ привязка аккаунта неустранима), DeepSeek `edit_limit` на долгих сессиях — оба не зависят от gateway.

**DeepSeek edit_limit recovery** (когда возникнет снова):
1. Получить через curl новый chat_session: `POST chat.deepseek.com/api/v0/chat_session/create` с Bearer + cookies из `~/Разработки/deepseek-free-api/config.json`
2. Записать новый `session_id` в `accounts[0].session_id` + `headers.referer` (там старый ID), сбросить `prompt_tokens=0` в `sessions.json`
3. `launchctl unload/load -w ~/Library/LaunchAgents/com.cebers.deepseek-free-api.plist`
Подтверждено на этой сессии 2026-05-29 — сработало с первого раза.

**Phase 2 TODO** (когда нужен search в Qwen/Gemini):
- **Qwen patch**: пробросить `featureConfig.auto_search=true` в `FreeQwenApi/src/api/chat.js:buildPayloadV2` (текущий код ставит auto_search только когда `chatType='t2v'`). Также прокинуть `chatType` из request.
- **Gemini search**: реверс какого endpoint в Sophomoresty активирует Google Search (README обещает «Web Search — Built-in», в коде через grep не нашёл). Возможно через `/v1beta/...` Gemini-native route или специальную модель.
- **`/v1/research`** orchestrator (deep multi-step planner → parallel search → synthesis) — отдельно если будут юзкейсы.

## Smoke / restart

```bash
. ~/.codex/omniroute.env
curl -s -H "Authorization: Bearer $OMNIROUTE_API_KEY" http://127.0.0.1:20130/v1/models | jq '.data[].id'
curl -s -X POST http://127.0.0.1:20130/v1/chat/completions -H "Content-Type: application/json" -H "Authorization: Bearer $OMNIROUTE_API_KEY" -d '{"model":"research/deepseek-search","messages":[{"role":"user","content":"hi"}]}'
launchctl unload/load -w ~/Library/LaunchAgents/com.cebers.CebersResearch.plist
```

## Цель

Объединить три локальных web2api сервиса под единым OpenAI-совместимым endpoint **`http://127.0.0.1:20130`** специально для бесплатного web search и deep research.

**Why:** Sophomoresty/DeepSeek/Qwen web2api все умеют ходить в интернет, но через разные пути и payload'ы. Хочется один Bearer и один base_url, чтобы клиенты выбирали `research/gemini-search`, `research/deepseek-search`, `research/qwen-search` или `research/deep` (оркестрация) единообразно.

**How to apply:** при поступлении задачи на реализацию — начать с шага «Recon search payloads в каждом web2api» (см. ниже), это критично т.к. протоколы меняются.

## Архитектура

```
client (Bearer) ──► :20130 research-gateway
                       │
                       ├─ research/gemini-search    ─► :1717 (Sophomoresty, payload с tools:google_search)
                       ├─ research/deepseek-search  ─► :1718 (Fly143, model=deepseek-search)
                       ├─ research/qwen-search      ─► :1719 (FreeQwenApi, search_enabled:true)
                       │
                       └─ research/deep             ─► внутренний orchestrator
                                                       (multi-step: planner → parallel search via 1-3 → synthesis)
```

Stack: FastAPI + httpx, launchd `com.cebers.CebersResearch`, venv-reuse из gemini-web2api.

## Search-режимы (что реально доступно бесплатно)

| Сервис | Модель/флаг | Реализация |
|---|---|---|
| **DeepSeek** :1718 | model=`deepseek-search` | Нативный web search через chat.deepseek.com, возвращает inline-цитаты. Уже доступен сам по себе. AWS WAF cookie может протухать. |
| **Qwen** :1719 | `chat_type: "search"` или payload `search_enabled: true` | Нужно проверить — FreeQwenApi прокинуть параметр через `/api/chat/completions`. Может потребовать патч `src/api/chat.js`. |
| **Gemini** :1717 | `tools: [{type:"google_search"}]` или модель-алиас | Sophomoresty README: «Web Search — Built-in internet access (Gemini's native search)». Подключение через Google AI Studio backend. Проверить чем `gemini-3.5-flash` + search отличается от без — может префлоу. |
| **Deep Research** | orchestrator | planner (Gemini thinking) → batch search в 1-3 параллельно → синтез. См. Anthropic/Perplexity дизайн. |

## /v1/models — расширенный каталог

```json
{
  "data": [
    {"id": "research/gemini-search", "owned_by": "research-gateway",
     "capabilities": {"web_search": true, "citations": true}},
    {"id": "research/deepseek-search", "owned_by": "research-gateway",
     "capabilities": {"web_search": true, "citations": true}},
    {"id": "research/qwen-search", "owned_by": "research-gateway",
     "capabilities": {"web_search": true}},
    {"id": "research/deep", "owned_by": "research-gateway",
     "capabilities": {"web_search": true, "multi_step": true, "agentic": true}}
  ]
}
```

## /v1/chat/completions — стандартный роутер

Префикс модели определяет upstream + payload-transformation:
- `research/gemini-*` → :1717 `/v1/chat/completions` + добавить `tools: [{type:"google_search"}]`
- `research/deepseek-search` → :1718 как есть
- `research/qwen-*` → :1719 `/api/chat/completions` + `search_enabled: true`
- `research/deep` → внутренний orchestrator (см. ниже)

## /v1/research — Deep Research orchestrator (опционально, фаза 2)

```
POST /v1/research
{
  "query": "...",
  "depth": "shallow|medium|deep",  // 1-2-4 цикла
  "providers": ["gemini", "deepseek", "qwen"],  // какие можно использовать
  "stream": true
}
```

Workflow:
1. **Planner** (Gemini Flash Thinking) — разбивает query на под-запросы
2. **Parallel search** — каждый sub-query через random/выбранный провайдер с web search
3. **Reflection** — Gemini Thinking смотрит результаты, может запросить уточнения
4. **Synthesis** — финальный compact answer с inline citations

SSE-stream выдаёт промежуточные шаги (`planning`, `searching`, `reflecting`, `synthesizing`) — UX как в Perplexity.

## Auth

Bearer = тот же `OMNIROUTE_API_KEY` для удобства клиентов, ИЛИ отдельный `sk-research-...`. Если bearer не совпадает с конфигом — 401.

## Implementation steps (когда вернёмся)

1. **Recon search payloads** — прочитать source каждого web2api на предмет реальных параметров search:
   - `~/Разработки/gemini-web2api/gemini_web2api.py` — где `google_search` или `web_search` упоминается
   - `~/Разработки/deepseek-free-api/proxy.py` — endpoint для search-модели
   - `~/Разработки/FreeQwenApi/src/api/chat.js` — есть ли `search_enabled` параметр
2. **Smoke each** — `curl` через Bearer none с search-флагом, убедиться что upstream работает и возвращает citations
3. **FastAPI скаффолд** `~/Разработки/CebersResearch/main.py`:
   - `/v1/models` — статический каталог
   - `/v1/chat/completions` — роутер с payload transformation
4. **launchd plist** `com.cebers.CebersResearch`, порт 20130, KeepAlive
5. **/v1/research** orchestrator (опционально, фаза 2)
6. **Smoke E2E** — клиент через Bearer → meta-gateway → каждый из 4 paths
7. **Update memory** [[deepseek_qwen_free_api]] + [[gemini_web2api_overview]] с упоминанием meta-gateway

## Известные риски

- **DeepSeek WAF cookie expire** — search-модель упадёт раньше всех. Нужен механизм auto-reimport cURL (admin-UI алерт + полудоступный manual flow).
- **Qwen search_enabled может работать только на v1 API** — текущий FreeQwenApi на v2. Возможно потребуется fallback или патч.
- **Gemini Web Search через Sophomoresty** — может требовать активную подписку Google AI Studio (anonymous может не работать с tools).
- **Latency Deep Research** — 4 цикла × 2-5s каждый = 10-25s. Только SSE-stream даст приемлемый UX.

## Связи

- [[gemini_web2api_overview]] :1717
- [[deepseek_qwen_free_api]] :1718, :1719
- [[omniroute_cloud_bus]] :20129 — не задействован, существует параллельно
