feat(agent): hierarchical executor with path resolve, runtime probe, quiet UI
CI / build-gateway (push) Failing after 16s
CI / sync-config (push) Failing after 0s

Make Zed Agent closer to Cursor: deterministic DevOps path index, live Traefik
port probe before blind edits, stop-after-edit, and quieter Russian progress.
This commit is contained in:
2026-08-13 11:41:27 +03:00
parent 6fd2f0e689
commit a2d238d92e
71 changed files with 10617 additions and 449 deletions
+111 -86
View File
@@ -1,6 +1,8 @@
# EventHub AI Router — Zed gateway + LiteLLM on IFT
OpenAI-compatible endpoint for [Zed](https://zed.dev) with automatic tier/lane routing (Novita AI), optional VLESS VPN, Grafana metrics, and LiteLLM Admin UI.
OpenAI-compatible endpoint for [Zed](https://zed.dev) with tier/lane routing, optional **hierarchical** orchestration (**Qwen3.8-Max** plan → cheap **Qwen3-Coder-30B** workers/executor → conditional **DeepSeek V3.2** verify), GigaChat classifier, VLESS VPN, Grafana, LiteLLM Admin UI.
Цель: качество правок близко к Cursor при экономии бюджета — дорогие модели только на plan/hard/verify, рутина на дешёвом coder.
## URLs (IFT, Calentiq TLS)
@@ -11,123 +13,146 @@ OpenAI-compatible endpoint for [Zed](https://zed.dev) with automatic tier/lane r
| **Grafana** | `https://grafana.ift.calentiq.com` |
| Legacy (self-signed) | `https://ai-router.ift.eventhub.local/v1` |
## Zed settings.json
## Клиент (канон): Zed Agent → AiRouter
**Primary:** встроенный Zed Agent (OpenAI-compatible), не Claude Code / Codex ACP — те обходят Novita-бюджет.
Пример `settings.json` (Zed 2026 openai-compatible provider):
```json
{
"openai": {
"api_url": "https://ai-router.ift.calentiq.com/v1",
"api_key": "<ROUTER_API_KEY>"
"language_models": {
"openai_compatible": {
"AiRouter": {
"api_url": "https://ai-router.ift.calentiq.com/v1",
"available_models": [
{
"name": "smart-router",
"display_name": "EventHub smart-router",
"max_tokens": 16384,
"max_output_tokens": 4096,
"capabilities": {
"tools": true,
"images": false,
"parallel_tool_calls": false
}
}
]
}
}
},
"assistant": {
"default_model": { "provider": "openai", "model": "smart-router" }
"agent": {
"default_model": {
"provider": "AiRouter",
"model": "smart-router"
}
}
}
```
API key: `ROUTER_API_KEY` в Agent Settings / provider UI.
Checklist:
- модель `smart-router`, `capabilities.tools: true`
- профиль **Write** (tools on) для правок файлов
- Ask без tools → text hierarchical / lane routing
Optional headers / metadata:
- `X-AI-Quality: auto|economy|balanced|max``max` поднимает hard на Qwen3.8-Max
- `X-AI-Orchestrate: off|auto|force` — hierarchical (`force` always; `auto` на COMPLEX/REASONING; `economy` quality пропускает hierarchical)
### Запасной клиент (тот же AiRouter)
Если native Zed Agent loop хрупкий: **OpenCode** или **Cline** с `base_url=https://ai-router.ift.calentiq.com/v1` и тем же ключом — иерархия бюджета сохраняется. Claude Code / Codex ACP в Zed — **не** primary (свой биллинг).
### Zed Agent Write + hierarchical
Если в запросе есть `tools` (профиль **Write**), gateway **не** подменяет agent-цикл текстовым синтезом:
1. **Max**-планировщик строит план (`paths` / `edit_goal` / `constraints`) → при `plan_confirm: true` ждёт **утвердить / правка / отмена**
2. После `ok`**Qwen3-Coder-30B** executor получает `tools` и отвечает **`tool_calls`** (Zed сам правит файлы)
3. Пока в истории есть `role: tool` / `tool_calls` — только executor; при повторных fail edit → escalate на DeepSeek (без synthetic tool_calls)
Чат без tools (Ask / Minimal) по-прежнему может идти через полный text hierarchical.
### Progress в Zed (hierarchical)
При `stream: true` (дефолт Zed) gateway сразу стримит блок **«Ход выполнения»**: план → волны workers → verify → синтез, затем `---` и финальный ответ. Конфиг: `hierarchical.stream_progress` / `progress_in_content` в `config/orchestration.yaml`.
## Provider profiles (`PRIMARY_PROVIDER`)
| Profile | Keys | VPN | Role |
|---------|------|-----|------|
| **`hybrid`** (default) | `NOVITA_API_KEY` | workers direct | Novita workers + **Novita** plan/verify |
| **`openrouter`** | `OPENROUTER_API_KEY` | **VPN** | Reserve (`:free` / non-Anthropic); Claude/OpenAI/Google may be region-blocked |
GigaChat (`GIGACHAT_CREDENTIALS`) — classifier. Optional: Groq / Gemini / Grok / Anthropic (если биллинг заработает).
```bash
bash scripts/switch-provider.sh hybrid
bash scripts/deploy.sh
# VPN нужен только для OpenRouter / Grok / Anthropic — не для Novita hierarchical
```
## Hierarchical flow (COMPLEX / force)
1. **`novita-planner`** (**Qwen3.8-Max**) — JSON subtasks (`simple` | `medium_code` | `hard`) + `paths` / `edit_goal`
2. **Workers / agent executor**: `simple` → Llama (text only); `medium_code`**Qwen3-Coder-30B**; `hard`**DeepSeek V3.2**; `quality=max` hard → **Qwen3.8-Max**
3. Checks; **`novita-verifier`** (DeepSeek V3.2) только на fail / hard / `quality=max`
4. **Synthesize** (text path) на `a-medium-code`
> Anthropic / OpenRouter-Claude недоступны при RU billing — plan/verify идут через Novita.
Response `x_router_meta.mode=hierarchical` / `hierarchical_agent` with `worker_calls`, `executor_model`, `role_cost`, `verify_skipped`, etc.
## Architecture
- **Gateway** (`router/`) — vision/OCR split, tier classification, lanes A/B/C orchestration, Redis session
- **LiteLLM** — named models `a-*` / `b-*` / `c-*`, Auto Router v2 fallback (`smart-router-internal`)
- **PostgreSQL** — LiteLLM Admin UI, spend logs
- **Redis** — response cache + gateway session context
- **VPN** — `vless-proxy` replicas=0 by default; `scripts/vpn-enable.sh`
- **Gateway** (`router/`) — classify, lanes A/B/C, hierarchical runner, Redis session
- **LiteLLM** — named models + Auto Router (`smart-router-internal`)
- **PostgreSQL** — spend logs / Admin UI
- **Redis** — cache + session
- **VPN** — `vless-proxy` (Anthropic, OpenRouter, Grok)
Config sources:
- `config/routing_rules.yaml` — tier keywords
- `config/model_matrix.yaml` — tier × lane → Novita models
- `config/orchestration.yaml` — start lanes, escalation, budget caps
Config: `config/providers.yaml`, `orchestration.yaml`, `routing_rules.yaml`, `model_matrix.yaml`.
## Deploy on IFT
```bash
git clone git.sabilin.com/eventhub/EventHubAiRouter /opt/ai-router-stack
cd /opt/ai-router-stack
cp .env.example .env # fill NOVITA_API_KEY, keys, POSTGRES_PASSWORD, LITELLM_SALT_KEY
cp .env.example .env # NOVITA_API_KEY; PRIMARY_PROVIDER=hybrid
bash scripts/deploy.sh
bash scripts/smoke-test.sh
sudo bash scripts/install-audit-cron.sh # daily Novita audit 03:00 MSK
SMOKE_HIERARCHICAL=1 bash scripts/smoke-test.sh
```
Prerequisites: Docker Swarm, external network `eventhub-ift-net`, Traefik routes in EventHubDevOps.
### Swarm secrets
Created automatically by `deploy.sh` from `.env`: `novita_api_key`, `litellm_master_key`, `litellm_salt_key`, `router_api_key`, `postgres_password`, optional `groq_api_key`, `gemini_api_key`, `vless_conf`.
Swarm secrets: `novita_api_key`, optional `anthropic_api_key` / `openrouter_api_key`, `gigachat_credentials`, …
**`LITELLM_SALT_KEY`** — generate once, never change after first deploy.
## VPN
```bash
bash scripts/vpn-enable.sh # scale vless + watchdog, set HTTP_PROXY on litellm
bash scripts/vpn-enable.sh
bash scripts/vpn-disable.sh
```
Novita always direct (`NO_PROXY=api.novita.ai`).
| Provider | Host | Route |
|----------|------|-------|
| Novita | `api.novita.ai` | **direct** (NO_PROXY) |
| Groq / Gemini / GigaChat | … | **direct** |
| **Anthropic** | `api.anthropic.com` | **VPN** (App unavailable without) |
| **OpenRouter** | `openrouter.ai` | **VPN** |
| **xAI Grok** | `api.x.ai` | **VPN** |
## Observability
Canon: `scripts/no-proxy-default.sh`.
- Prometheus scrape: `litellm:4000/metrics`, `ai-router:8000/metrics` (via `eventhub-ift-net`)
- Grafana: vendored LiteLLM v2 dashboard + custom `gateway-tier-lane.json` (EventHubDevOps)
- Uptime Kuma monitors — see table in plan / configure manually
## LLM classifier (GigaChat)
`CLASSIFIER_MODE=hybrid|heuristic|llm` — see `config/orchestration.yaml`. Freemium GigaChat for personal test.
## Scripts
| Script | Purpose |
|--------|---------|
| `deploy.sh` | secrets + build + stack deploy |
| `smoke-test.sh` | health, classify, chat max_tokens=16 |
| `sync-routing-config.sh` | regen `litellm_config.generated.yaml` |
| `audit-novita-pricing.sh` | daily model catalog check |
| `vendor-litellm-dashboards.sh` | fetch Grafana JSON → EventHubDevOps |
| `vpn-enable.sh` / `vpn-disable.sh` | VPN toggle |
## Quality modes
Header `X-AI-Quality: auto|economy|balanced|max` or `metadata.quality_mode` in request body. Default: `auto` (start lane A/B by tier, escalate on retry/5xx).
## LLM classifier (GigaChat, optional)
When `CLASSIFIER_MODE=hybrid` (default) and `GIGACHAT_CREDENTIALS` is set:
1. **Heuristic** classify first (0 cost)
2. If `confidence < 0.6` → one call to **GigaChat-2-Lite** via LiteLLM (`gigachat-classifier`)
3. Lane orchestration unchanged (A/B/C, Redis, budget)
| `CLASSIFIER_MODE` | Behavior |
|-------------------|----------|
| `heuristic` | Keywords only (no GigaChat) |
| `hybrid` | GigaChat only on low confidence |
| `llm` | Always GigaChat for text (except vision) |
Freemium GigaChat — для личного некомmercial теста; prod — юр. тариф Сбера.
Setup: [developers.sber.ru](https://developers.sber.ru/docs/ru/gigachat/quickstart/ind-create-project) → Authorization key → `.env` `GIGACHAT_CREDENTIALS`.
Response field: `x_router_meta.classifier_source` = `heuristic` | `gigachat` | `heuristic_fallback`.
## Response metadata
Each chat response includes `x_router_meta`:
```json
{
"tier": "MEDIUM_OPS",
"lane": "A",
"model": "a-medium-ops",
"escalation_level": 0,
"quality_mode": "auto"
}
```
## Local build
```bash
docker build -f router/Dockerfile -t ai-router-gateway:local .
```
## CI
Gitea Actions: `.gitea/workflows/ci.yml` — build gateway image, sync config check.
See `scripts/``deploy.sh`, `smoke-test.sh`, `gen-litellm-config.py`, `switch-provider.sh`, VPN helpers.