Концепция веб-консоли model-router-mcp, версия 2¶
Статус: черновик после трёх рецензий. Не реализовано.
Дата: 2026-07-26.
Предшественник: docs/UI_CONCEPT.md (v1), заменяется этим документом.
Рецензии, на которых основана переработка:
docs/reviews/UI_CONCEPT_review_api.md- API, MCP, живые провайдеры;docs/reviews/UI_CONCEPT_review_architect.md- архитектура фронтенда и реализуемость;docs/reviews/UI_CONCEPT_review_vibecoder.md- взгляд неподготовленного оператора.
Документ по-русски: он состоит из экранного текста и предназначен людям. Код и комментарии остаются английскими по AGENTS.md.
1. Что изменилось против v1¶
Коротко и по существу, чтобы критикам не пришлось сличать две версии.
| Решение v1 | Решение v2 | Почему |
|---|---|---|
| Семь пунктов навигации | Пять: Обзор, Прогон, Модели, Журнал, Подключения | «Матрица» стала режимом «Прогона», «Расход» вкладкой «Журнала», MCP вкладкой «Подключений» |
| Отдельный экран «Матрица» | Мультимодельный режим «Прогона» | Отдельный экран дублирует левую колонку формы целиком |
| Шесть вкладок инспектора | Четыре: Ответ, Шаги, JSON, Сырьё | Вкладка «Лог» заменена ссылкой, запрос и ответ объединены |
| Три режима выбора модели | Два: Авто и Выбрать самому | «Ручной» это то же поле ввода внутри второго режима |
| Фолбэк тремя контролами | Один селект человеческими словами, max_steps в «ещё настройки» |
Три контрола в строке не читаются без исходников |
| Виртуальные таблицы | Страницы по 50, content-visibility при необходимости |
331 строка не требует виртуализатора |
| SSE в третьей волне | Реестр прогонов в процессе и опрос | Работает через текущий nginx, переживает закрытие вкладки, не конфликтует с FastMCP |
POST /api/runs/batch |
N обычных прогонов с общим batch_id |
Серверный батч ничего не упрощает |
| 13 ручек без контрактов | Контракты, конверты, курсорная пагинация, /api/v1 |
Список v1 годился как перечень, не как задание |
| Цена бывает «посчитанная» и «оценочная» | Шесть состояний, включая «методики нет» и «условная по подписке» | Три модальности из пяти сегодня считаются нулём |
| Про MCP ничего | Вкладка MCP: инвентарь инструментов, проба, каналы вызовов | MCP это основной интерфейс сервиса, консоль вторична |
| Про ретраи и 429 ничего | Отдельный уровень таймлайна, поля в логе, фильтр | Повторы уже происходят и уже искажают задержку |
| Экранирование не оговорено | Жёсткое правило и пункт приёмки | В текущей консоли работающий XSS, концепция v1 расширяла поверхность |
| Три волны | Нулевая волна плюс три, 26-34 дня | Без починок первая волна выходит на прод сломанной |
Отклонено из рецензий, с обоснованием, в разделе 21.
2. Модель предметной области¶
Восемь сущностей. Интерфейс называет их одинаково везде, словарь в разделе 3.
| Сущность | Что это | Состояния |
|---|---|---|
| Поставщик сервиса | Точка подключения: openrouter, deepseek, anthropic-api, codex-oauth, deepgram, elevenlabs, codex-cli, claude-cli |
enabled (тумблер) x configured (ключ на месте) x probe (живая проверка) |
| Вендор модели | Кто сделал модель: openai, anthropic, deepseek |
- |
| Модель реестра | Строка registry_models, адресуется парой «поставщик плюс model_id» |
активна или снята; источник строки catalog/seed/manual; происхождение цены |
| Прогон | Один вызов любой модальности | queued, running, ok, error, cancelled, interrupted |
| Попытка | Одна пара «адаптер плюс модель» внутри прогона | планируется, выполняется, отказала; внутри неё повторы HTTP |
| Запись журнала | Строка request_logs |
статус прогона плюс канал вызова |
| Расход | Агрегат по пользователю, модели, поставщику, дню | разложен по источникам цены |
| Канал | Кто вызвал: mcp, api, ui |
плюс имя инструмента и метка клиента |
Модель адресуется парой, а не одним идентификатором. Сегодня поиск идёт по одному model_id и берёт первое совпадение по алфавиту поставщика, то есть при коллизии счёт зависит от случайности. В интерфейсе заголовок карточки, строка журнала и результат прогона показывают пару.
Состояния поставщика¶
enabled=1 configured=1 probe=ok рабочий, проверен зелёный
enabled=1 configured=1 probe=unauthorized ключ есть, но отозван красный
enabled=1 configured=1 probe не делалась включён, ключ на месте зелёный контур
enabled=1 configured=0 включён, ключа нет красный, диспетчер откажет
enabled=0 configured=1 выключен вручную серый
enabled=0 configured=0 не подключён серый бледный
Отдельная проба нужна потому, что configured у большинства адаптеров проверяет наличие ключа, а не его действительность. Отозванный ключ сегодня даёт зелёный статус.
Состояния цены¶
Шесть значений cost_source, и все шесть должны читаться на экране:
| Значение | Смысл | Как показываем |
|---|---|---|
computed |
ставка подтверждена (API, страница прайса, админ) | обычное число |
estimated |
ставка не подтверждена | число со значком ≈ |
unpriced |
методики нет: ставка нулевая при ненулевом потреблении | прочерк и слово «нет методики» |
provider |
сумму назвал провайдер (OpenRouter usage.cost) |
число со значком проверки |
provider_notional |
сумму назвал CLI под подпиской, деньги не списывались | число в скобках, в итог отдельной строкой |
amortized |
размазано по подписке | число со ссылкой на подписку |
cost_unknown |
read-таймаут: запрос, скорее всего, отработан и оплачен, но не учтён | прочерк и предупреждение |
Итог на любом экране расхода показывается как нижняя граница, если в выборке есть unpriced или cost_unknown.
3. Словарь: одно слово на одно понятие¶
Главная претензия неподготовленного читателя v1 была не к раскладке, а к словам. Сегодня одно и то же называется по-разному в концепции, в шаблоне реестра и в схеме БД. Ниже канон. Слева поле в данных, в центре подпись на экране, справа подпись-пояснение под контролом (не тултип, именно видимый текст).
| Поле | Подпись | Пояснение под контролом |
|---|---|---|
external_user_id |
Метка пользователя | Любая строка для группировки статистики. На доступ и маршрутизацию не влияет |
service_provider_id |
Поставщик | Через кого идёт запрос и чей ключ тратится |
model_provider |
Вендор | Кто сделал модель |
tier |
Ценовой уровень | Проставлен в реестре по сумме ставок: free, до $1 за 1M, выше |
fallback.policy |
Если модель недоступна | (варианты словами, см. 10.2) |
fallback.max_steps |
Сколько замен пробовать | В «ещё настройки» |
fallback.scope |
(внутри вариантов политики) | Отдельного контрола нет |
price_source_kind=api |
Из каталога поставщика | Цену отдал сам поставщик по API |
price_source_kind=page |
С прайс-страницы | Человек взял цену со страницы поставщика, ссылка рядом |
price_source_kind=manual |
Введена вручную | Цену ввёл администратор, синхронизация её не тронет |
price_source_kind=estimate |
Прикидка | Цену никто не подтверждал |
price_source_kind=unknown |
Цены нет | Каталог отдал модель без цены |
cost_source |
Откуда сумма | (по таблице в разделе 2) |
is_active=0 |
Снята | Пропала из каталога поставщика такого-то числа |
source=catalog/seed/manual |
Строка из каталога / из заготовки / добавлена вручную | |
finish_reason=length |
Обрыв по лимиту токенов | Модель упёрлась в max_tokens, ответ неполный |
blob ref |
Файл ответа | Кнопка «Скачать», размер, тип. Идентификатор в «Сырьё» |
channel |
Канал | Откуда пришёл вызов: MCP-клиент, API, консоль |
latency_ms |
Время ответа | Включая повторы и ожидание |
attempts |
Повторов | Сколько раз запрос повторялся из-за временных отказов |
Правила словаря:
- Термин, впервые появляющийся на экране, снабжён пояснением под контролом. Тултип годится для уточнения, не для первого объяснения.
- Одно понятие переводится одинаково в форме, в таблице, в фасете и в отчёте. Список подписей живёт в одном файле локализации, а не в трёх шаблонах.
- Английский идентификатор остаётся видимым рядом с русской подписью там, где оператор будет искать его в API или в логах:
model_id,finish_reason, имена переменных окружения.
4. Роли и сценарии¶
Роли прежние: тестировщик интеграции, администратор реестра, дежурный по сервису. Сценарии v1 (С1-С12) сохраняются, к ним добавлены пять.
С13. Понять, кто дёргает сервис. Открыть журнал, отфильтровать по каналу mcp, увидеть, какой клиент, каким инструментом и сколько раз ходил, с какой ошибкой.
С14. Первый запуск. Свежая установка: ключей нет, моделей нет, журнал пуст. Оператор должен получить список шагов, а не пустые таблицы.
С15. Разобрать медленный ответ. Запрос шёл 8 секунд. Нужно увидеть, что это два повтора по 429 плюс сон, а не медленная модель, и что провайдер просил подождать 60 секунд.
С16. Проверить MCP-подключение. Скопировать фрагмент конфигурации клиента, запустить самопробу, увидеть список инструментов со схемами и статистику вызовов.
С17. Не потратить лишнего. Перед пакетным запуском увидеть число запросов и прикидку суммы, подтвердить осознанно.
5. Управляемые параметры¶
Правило релевантности из v1 остаётся: поле существует на экране тогда, когда его читает адаптер выбранной пары «модальность плюс поставщик». К нему добавляются три уточнения.
Уточнение 1. Источник матрицы это код адаптеров. В ProviderAdapter заводится SUPPORTED_PARAMS, тот же кортеж используется адаптером при сборке payload и отдаётся в /api/v1/capabilities. Руками написанная копия в web/ разойдётся с кодом на первом новом адаптере. Изменение контракта адаптера оформляется припиской к ADR-0001.
Уточнение 2. Одной пары для ключа матрицы мало. У Anthropic max_tokens обязателен и имеет умолчание, у OpenRouter нет. reasoning имеет смысл у рассуждающих моделей. Список голосов ElevenLabs это данные поставщика. Поэтому ответ capabilities даёт схему параметров на пару, плюс переопределения на модель.
Уточнение 3. Молчаливое игнорирование запрещено. Адаптеры сегодня выбрасывают tools, response_format, stop, seed, stream без предупреждения. В ответе прогона появляется ignored_params, интерфейс показывает предупреждение «параметр tools адаптер openrouter не читает». Считается как разность ключей.
Матрица на 2026-07-26, сверена по коду:
| Модальность | Поставщик | Параметры | Фиксировано сервером |
|---|---|---|---|
| chat | openrouter | temperature, top_p, max_tokens, reasoning |
базовый URL, ключ, таймаут, повторы |
| chat | deepseek | temperature, top_p, max_tokens |
то же |
| chat | anthropic-api | temperature, top_p, max_tokens (умолчание) |
CLAUDE_API_MODEL |
| image | openrouter | как chat | - |
| image | codex-oauth | только промпт | размер, качество, обе модели |
| stt | deepgram | ничего сверх аудио и модели | DEEPGRAM_MODEL |
| tts | elevenlabs | voice, language |
модель, голос по умолчанию |
| cli | codex-cli | cwd, timeout_sec |
команда, белый список корней |
| cli | claude-cli | cwd, timeout_sec, model |
команда, CLI_DEFAULT_CWD |
Две строки матрицы сегодня недостижимы из веб-API: language для tts и model для claude-cli. Это чинится в нулевой волне добавлением params в тело /api/tts и /api/cli, иначе форма описывает поля, которых нет.
Блок «Настройки сервера». Показывается рядом с формой в режиме чтения: таймаут, число повторов, разрешённые корни cwd, максимальный размер загрузки. Значения приезжают из capabilities. Показываются имена переменных окружения и признак «задано», значения секретов и пути файловой системы не показываются никогда.
6. Что показывать в результате¶
Четыре уровня из v1 сохраняются, наполнение расширено.
Уровень 0 Итог статус · пара поставщик+модель · токены · сумма · время · канал
Уровень 1 Шаги план, попытки, повторы HTTP внутри попытки, блобы, расчёт цены
Уровень 2 JSON тело запроса к /api, тело запроса к провайдеру (под флагом), ответ
Уровень 3 Сырьё usage_raw, raw_meta, provider_request_id, finish_reason, файлы
Поля, которые обязаны доехать до экрана. Первая группа уже есть в БД и теряется по дороге: latency_ms, finish_reason, usage_raw, error_message, request_ref, response_ref, pricing_mode, cost_source, requested_model_id, fallback_reason.
Вторая группа не доезжает даже до БД и добавляется одной миграцией: channel, tool, client_id, attempts, provider_status, provider_request_id, cache_read_tokens, cache_write_tokens, reasoning_tokens, session_id (для CLI), return_code, characters (для TTS), plan_json, finished_at, client_request_id.
Токены показываются разложением. промпт 1 042 (из них из кэша 890) / ответ 318 (из них рассуждение 120). Без этого расчёт цены для Anthropic и DeepSeek врёт в разы, а не в процентах: у Anthropic input_tokens не включает кэш, у DeepSeek попадание в кэш дешевле примерно на порядок.
Строка расчёта цены пишется по слагаемым, каждое со своей ставкой:
цена = 152 x $0.27/1M (промпт)
+ 890 x $0.027/1M (чтение кэша)
+ 318 x $1.10/1M (ответ)
= $0.000412 ставки подтверждены каталогом, проверено 2026-07-25
7. Модель денег в интерфейсе¶
Отдельный раздел, потому что это место, где интерфейс легче всего врёт уверенно.
- Ни одно число в долларах не показывается без источника. Значок или подпись обязательны везде: инспектор, журнал, расход, карточка модели, прикидка у кнопки.
- Итог с примесью
unpricedилиcost_unknownподписывается как нижняя граница. Рядом строка «N запросов без методики цены» и «M запросов с неизвестной стоимостью», обе кликаются в отфильтрованный журнал. - Условная стоимость подписки не смешивается с настоящими деньгами.
provider_notionalидёт отдельной строкой итога. - Расхождение между ценой провайдера и ценой по прайсу показывается явно. Когда OpenRouter вернул
usage.cost, рядом ставится расчёт по реестру, а разница подсвечивается: это лучший индикатор протухшей цены. - Прикидка перед тратой. Кнопка отправки несёт ожидаемую сумму. Для модальностей без методики (image, stt, tts сегодня) пишется «стоимость неизвестна», а не ноль.
- Пакетный запуск требует подтверждения с числом запросов и суммой прикидки.
- Правка цены защищена от промаха на порядок. Единица внутри поля, предупреждение при отличии от прежнего значения больше чем в 10 раз, показ старого и нового значения перед сохранением.
Три дефекта учёта чинятся до того, как рисуется экран расхода, иначе он будет показывать уверенно неверные числа: нулевые ставки seed-строк для image, stt, tts; отсутствие поля «цена за символ» для ElevenLabs; нулевая seed-цена, выигрывающая автоматический подбор (страховка сегодня срабатывает только на unknown, а seed-строки имеют estimate).
8. Принципы¶
- Итог сверху, сырьё внизу, всё раскрывается на месте.
- Ничего не исчезает: отчёт синхронизации, ошибка и план остаются на экране до следующего действия.
- Состояние вместо галочки: где состояний шесть, рисуем шесть.
- Числа выравниваются по правому краю моноширинным шрифтом с одинаковым числом знаков.
- Каждое число объясняет своё происхождение.
- Форма адаптируется к выбору, а игнорируемые параметры называются вслух.
- Списки от трёхсот строк фильтруются на сервере, страницами по 50.
- Ошибка называет причину, несёт код и ссылку на строку журнала.
- Деньги под защитой: прикидка перед тратой, подтверждение на пакет, защита от промаха в цене.
- Никакого
innerHTMLс данными. Текст только черезtextContent. - Одно понятие называется одним словом во всех местах.
- Клавиатура важнее мыши, но односимвольные клавиши не тратят деньги.
9. Карта экранов¶
┌ Model Router ─────────────────────────────────────────────┐
│ Обзор · Прогон · Модели · Журнал · Подключения │
└───────────────────────────────────────────────────────────┘
Обзор здоровье, поставщики, свежесть каталогов, ошибки, список работы
Прогон одиночный и мультимодельный запрос плюс инспектор
Модели реестр: фасеты, таблица, карточка, правка цены
Журнал вкладки «Запросы» и «Расход»
Подключения вкладки «Поставщики» и «MCP»
Пять пунктов вместо семи. «Матрица» из v1 стала режимом «Прогона», «Расход» вкладкой, MCP получил место, которого в v1 не было вовсе.
10. Экран «Прогон»¶
┌─ ПРОГОН ───────────────────────────────────────── [ одна модель │ несколько ] ─┐
│ ┌── ЗАПРОС ───────────────────────────┐ ┌── РЕЗУЛЬТАТ ─────────────────────────┐│
│ │ Модальность │ │ ● ok 1.84 s $0.000412 ≈ ui #4821 ││
│ │ [chat][image][stt][tts][cli] │ │ deepseek · deepseek-chat ││
│ │ │ │ ⇄ запрошена deepseek-v4-pro (снята) ││
│ │ Метка пользователя │ │ ⚠ параметр tools адаптером не читан ││
│ │ [ web-tester ] │ ├──────────────────────────────────────┤│
│ │ Любая строка для группировки │ │ Ответ │ Шаги 5 │ JSON │ Сырьё ││
│ │ статистики │ ├──────────────────────────────────────┤│
│ │ │ │ ││
│ │ ── МОДЕЛЬ ───── [Авто │ Выбрать] ── │ │ Привет. Ниже разбор по пунктам... ││
│ │ Поставщик [ deepseek ▾ ] │ │ ││
│ │ через кого идёт запрос │ │ ││
│ │ Вендор [ deepseek ▾ ] │ │ ││
│ │ кто сделал модель │ │ ││
│ │ Модель [ deepseek-chat ▾ ] │ │ ││
│ │ Вариант [ базовая · $1.37/1M ▾ ] │ │ ││
│ │ model_id deepseek/deepseek-chat │ │ [Копировать]││
│ │ 331 модель · обновлено 12 мин назад │ │ ││
│ │ │ │ ││
│ │ Если модель недоступна │ │ ││
│ │ [ показать ошибку ▾ ] │ │ ││
│ │ ▸ ещё настройки │ │ ││
│ │ │ │ ││
│ │ ── ПАРАМЕТРЫ ГЕНЕРАЦИИ ───────────── │ │ ││
│ │ temperature [0.7] max_tokens [4096] │ │ ││
│ │ top_p [1.0] reasoning [off] │ │ ││
│ │ [ пресет: дешёвый детерминизм ▾ ]│ │ ││
│ │ │ │ ││
│ │ ── ПРОМПТ ────────── [текст│messages]│ │ ││
│ │ ┌─────────────────────────────────┐ │ │ ││
│ │ │ │ │ │ ││
│ │ └─────────────────────────────────┘ │ │ ││
│ │ черновик сохраняется автоматически │ │ ││
│ │ │ │ ││
│ │ 🔒 Сервер: таймаут 60 с, повторов 2 │ │ ││
│ │ │ │ ││
│ │ [Показать план] [Отправить ≈$0.0008]│ │ ││
│ └─────────────────────────────────────┘ └──────────────────────────────────────┘│
│ ПОСЛЕДНИЕ ПРОГОНЫ #4821 ok · #4820 ok · #4819 ошибка · #4818 ok [все →] │
└─────────────────────────────────────────────────────────────────────────────────┘
10.1 Два режима выбора модели¶
- Авто. Оператор задаёт модальность и, при желании, ценовой уровень и поставщика. Под блоком строка «сервис выберет
openai/gpt-4o-mini: совпал уровень, цена ниже остальных». Строка приезжает из плана. - Выбрать самому. Каскад из четырёх селектов плюс поле
model_idс автодополнением, оба редактируются, синхронизируются между собой. Поле подсвечивает состояние прямо при вводе: найдена, снята такого-то числа, поставщик выключен.
Дерево каскада строит сервер. Сегодня клиент режет model_id по слэшу и двоеточию, при том что model_provider лежит отдельной колонкой в базе и уже отдаётся в API. Разбор идентификатора, счётчики в узлах и сортировка уезжают на сервер.
В варианте показывается цена вместо слова «cheap»: базовая · $1.37/1M.
10.2 Фолбэк одним селектом¶
Если модель недоступна
показать ошибку (off)
подставить похожую по цене у того же поставщика (same, provider)
подставить дешевле у того же поставщика (cheaper, provider)
подставить дороже у того же поставщика (pricier, provider)
подставить похожую у любого поставщика (same, any)
▸ ещё настройки: сколько замен пробовать [1]
Скобки показаны здесь для критиков, на экране их нет. scope=vendor доступен в «ещё настройках» для тех, кому он нужен.
Отдельно в результате различаются два вида подстановки: запланированная до вызова (модель снята, поставщик выключен) и случившаяся после отказа (провайдер вернул ошибку, исчерпан лимит). Сегодня они отличаются только текстом причины, а стоят по-разному: временный лимит при политике «дороже» тихо уводит трафик на дорогую модель.
10.3 План¶
Кнопка «Показать план» с подписью «ничего не отправляем и не тратим».
ПЛАН МАРШРУТИЗАЦИИ ничего не отправлено, деньги не потрачены
Запрошено deepseek · deepseek-v4-pro
Проблема снята поставщиком 2026-07-25
Политика подставить дешевле у того же поставщика, до 1 замены
Кандидаты отобрано 12 из 331 (chat · deepseek · цена известна · адаптер настроен)
# модель цена, $/1M цена откуда почему
1 deepseek-chat 1.370 из каталога ближайшая ступень вниз
2 deepseek-reasoner 2.740 из каталога отфильтрована: дороже опорной
3 codex-oauth/gpt-image-2 0.000 цены нет исключена из лестницы
Будет вызвана #1. Прикидка на 1000/1000 токенов: $0.00164 (ставки подтверждены)
Планирование не должно ходить в сеть: сегодня получение моделей начинается с синхронизации по TTL, и «сухой прогон» может опросить каталоги. План вызывается с запретом синхронизации.
Когда исполнять нечего, план отвечает успехом с описанием проблемы, а не ошибкой. Экран «почему запрос не ушёл» обязан работать именно в этом случае.
Пересчёт плана автоматический при смене дискретных полей (модальность, поставщик, политика) с задержкой 300 мс. Полная таблица кандидатов по кнопке.
10.4 Инспектор, четыре вкладки¶
| Вкладка | Содержимое |
|---|---|
| Ответ | Текст с кнопкой копирования, картинки, плеер, вывод CLI. Кнопка «Скачать файл» вместо идентификатора блоба |
| Шаги | Таймлайн двух уровней, см. ниже |
| JSON | Два блока: тело запроса к /api и тело ответа. Тело запроса к провайдеру под флагом отладки, с усечением строк длиннее 4 КБ |
| Сырьё | usage_raw объектом, raw_meta, provider_request_id с копированием, finish_reason с расшифровкой словами, ссылки на файлы |
Вкладки «Лог» нет, вместо неё ссылка «Открыть в журнале #4821».
Таймлайн двух уровней: попытки моделей и повторы HTTP внутри попытки.
ШАГИ всего 8.42 s
● 0 ms план построен 2 попытки
● 2 ms попытка 1 deepseek-v4-pro пропущена: снята 2026-07-25
● 3 ms попытка 2 deepseek-chat POST /v1/chat/completions
○ 3 ms повтор 1/3 HTTP 429, провайдер просил 5 с
○ 5 412 ms повтор 2/3 HTTP 429, провайдер просил 3 с
○ 8 020 ms успех HTTP 200
● 8 390 ms ответ разобран обрыв по лимиту токенов: нет
● 8 400 ms файлов нет
● 8 410 ms цена посчитана по трём ставкам, см. Ответ
● 8 420 ms записан лог #4821 канал ui
Без этого уровня восьмисекундный ответ читается как медленная модель, хотя это два повтора и сон.
10.5 Долгие прогоны, прогресс и отмена¶
Прогон отправляется в реестр прогонов внутри процесса и получает идентификатор. Клиент опрашивает состояние раз в секунду, потоковые соединения не используются.
- Кнопка превращается в счётчик времени с кнопкой «Прервать».
- Таймлайн наполняется по мере поступления шагов. Для CLI шаги приходят построчно: разбор stdout как JSONL уже построчный, требуется заменить чтение целиком на чтение по строкам.
- Закрытие вкладки не теряет прогон: он виден в журнале и в ленте последних прогонов.
- Отмена для CLI завершает процесс, для HTTP отменяет задачу.
- Рестарт сервиса переводит зависшие
runningвinterruptedпри старте, чтобы не оставались вечные «выполняется».
Для чата стриминг ответа в интерфейс в эту концепцию не входит: он ломает повторы и определение обрыва по лимиту и требует отдельного архитектурного решения.
10.6 Мультимодельный режим¶
Тот же экран, переключатель в шапке. Вместо одного model_id список чипов моделей, вместо карточки результата таблица.
Модели [deepseek-chat ×][gpt-4o-mini ×][claude-sonnet-4-6 ×][+] повторов [1]
[ Запустить 3 прогона, примерно $0.0031 ]
модель статус время токены $ ответ
deepseek-chat ● ok 1.84s 1042(890к)/318 0.000412≈ Протокол MCP…
gpt-4o-mini ● ok 0.92s 1039/291 0.000198 MCP - это…
claude-sonnet-4-6 ● ошибка — — — не настроен →
Запуск с числом прогонов больше трёх требует подтверждения с суммой. Каждый прогон получает client_request_id, повторная отправка того же ключа возвращает сохранённый результат вместо второй траты.
10.7 Бытовые вещи¶
- Лента последних прогонов сессии внизу экрана, десять штук, клик открывает инспектор.
- Черновик промпта сохраняется в браузере по ключу экрана и модальности.
- Пресеты параметров: сохранить текущий набор под именем, применить, удалить. Хранятся локально.
- «Повторить последний» на самом экране, а не одним пунктом в журнале.
- Кнопка копирования у текста ответа, у
model_id, уprovider_request_id, у команды curl. - После отправки в адрес страницы кладётся
?log=4821. Перезагрузка поднимает инспектор из журнала, ссылку можно отправить коллеге.
11. Экран «Модели»¶
Три панели на широком экране, две на среднем, одна с выдвижной карточкой на узком.
┌─ МОДЕЛИ ──────────────────────────────────────────────────────────────────────────────┐
│ ┌ ФИЛЬТРЫ ────────┐ ┌ 331 модель · показано 50 ───────────┐ ┌ КАРТОЧКА ──────────────┐│
│ │ [ поиск ] │ │ поставщик model_id промпт ответ│ │ deepseek · deepseek-chat││
│ │ │ │ $/1M $/1M │ │ ││
│ │ Модальность │ │ deepseek deepseek-chat 0.270 1.100│ │ Вендор deepseek ││
│ │ ☑ chat 243 │ │ deepseek deepseek-rea… 0.550 2.190│ │ Модальности chat ││
│ │ ☐ image 61 │ │ openrout… deepseek/v3 0.280 1.140│ │ Контекст 128 000 ││
│ │ ☐ stt 3 │ │ … │ │ Уровень expensive ││
│ │ ☐ tts 2 │ │ │ │ Строка из каталога ││
│ │ ☐ cli 2 │ │ │ │ ││
│ │ │ │ │ │ ЦЕНЫ, $ за 1M токенов ││
│ │ Состояние │ │ │ │ промпт [0.270 $/1M] ││
│ │ ☑ активные 318 │ │ │ │ ответ [1.100 $/1M] ││
│ │ ☐ снятые 13 │ │ │ │ было 0.270 / 1.100 ││
│ │ │ │ │ │ ││
│ │ Цена откуда │ │ │ │ Откуда [введена вручную]││
│ │ ☐ из каталога287│ │ │ │ Ссылка [https://… ]││
│ │ ☑ прикидка 31 │ │ │ │ Проверено 2026-07-25 ││
│ │ ☑ цены нет 13 │ │ │ │ ⓘ ручная цена переживёт││
│ │ │ │ │ │ синхронизацию ││
│ │ Строка │ │ │ │ [вернуть цену каталога]││
│ │ ☐ каталог │ │ │ │ ││
│ │ ☐ вручную │ │ │ │ ЗА 30 ДНЕЙ ││
│ │ ☐ заготовка │ │ │ │ 412 запросов · $1.84 ││
│ │ │ │ │ │ ││
│ │ [ Сбросить ] │ │ [☐ выделено 0] [ ещё 50 ] │ │ [Сохранить] [Тест →] ││
│ └─────────────────┘ └─────────────────────────────────────┘ └────────────────────────┘│
└───────────────────────────────────────────────────────────────────────────────────────┘
Что важно:
- Правка цены прямо в каталожной строке. Сегодня единственный путь правки перезаписывает всю строку значениями по умолчанию: затираются контекст и даты, а снятая модель воскресает активной. Нужна частичная правка по адресу «поставщик плюс модель».
- Ручная цена переживает синхронизацию. Защита цены сегодня завязана на источник строки, а не на происхождение цены, поэтому цена, введённая в каталожную строку, затирается ближайшей синхронизацией. Критерий меняется на происхождение цены, строка остаётся каталожной и продолжает получать обновления имени, контекста и модальностей.
- Кнопка «вернуть цену каталога» делает правку обратимой.
- Ручная строка у каталожного поставщика снимается при первой синхронизации, потому что цикл деприкации её не исключает. Либо исключить, либо честно предупредить в форме добавления.
- Единицы внутри поля, старое значение под полем, предупреждение при отличии больше чем в 10 раз.
- Массовые действия с подтверждением, в котором перечислены затронутые модели, и с возможностью отмены в течение нескольких секунд.
- Счётчики фасетов приезжают вместе со строками одним ответом, иначе они будут из разных моментов времени.
- Вырожденный ценовой уровень. Порог «дешёвой» модели сегодня отсекает почти весь современный каталог в
expensive. Счётчики в фасете покажут это сразу, порог поправят по данным.
Форма добавления модели вручную живёт в правой панели по кнопке «Добавить», а не посреди страницы.
12. Экран «Подключения»¶
Две вкладки.
12.1 Поставщики¶
┌─ ПОДКЛЮЧЕНИЯ ── [ Поставщики │ MCP ] ────────────────────────────────────────────────┐
│ [ Синхронизировать каталоги ] [ Проверить все ключи ] последняя сверка 12 мин назад│
├──────────────────────────────────────────────────────────────────────────────────────┤
│ ● openrouter chat,image 331 мод. ключ есть, проверен 2 мин назад прайс [синх.] │
│ рабочий · каталог с ценами · +2 ~14 -1 │
│ ● deepseek chat 6 мод. ключ есть, проверен прайс [синх.]│
│ рабочий · каталог без цен · ⚠ 6 моделей без цены │
│ ● anthropic-api chat 9 мод. ключ есть прайс [синх.]│
│ ○ codex-oauth image 1 мод. токен на месте, срок до 12:40 [—] │
│ выключен вручную │
│ ● deepgram stt 3 мод. ключ есть прайс [—] │
│ ✕ elevenlabs tts 2 мод. КЛЮЧА НЕТ прайс [—] │
│ включён, но не настроен · переменная ELEVENLABS_API_KEY не задана │
│ ● codex-cli cli 1 мод. бинарник 0.144.1 [—] │
│ ● claude-cli cli 1 мод. бинарник 2.1.187 [—] │
└──────────────────────────────────────────────────────────────────────────────────────┘
Отчёт синхронизации разворачивается под строкой и остаётся там:
openrouter · 12 мин назад · 1.4 s
добавлено 2 openai/gpt-5.6-mini, x-ai/grok-5
обновлено 14 цены
снято 1 anthropic/claude-3-opus последний вызов 2026-06-02 → в журнал
ручные цены сохранены: 3
У кнопки синхронизации постоянная подпись «ручные цены сохраняются, снятые модели помечаются, ничего не удаляется». Выключение поставщика показывает тост с кнопкой отмены на пять секунд.
Проба ключа отдельной кнопкой, результат кэшируется, чтобы проверка не выжигала лимит. Пути файловой системы на экране не показываются, только имя переменной и признак «задано».
12.2 MCP¶
Раздела не было в v1. Между тем MCP это основной интерфейс сервиса, а консоль вспомогательная.
┌─ ПОДКЛЮЧЕНИЯ ── [ Поставщики │ MCP ] ────────────────────────────────────────────────┐
│ Эндпоинт https://router.example.com/mcp Bearer требуется [ Самопроба ] │
│ Проба: ok, 12 ms, инструментов 7, режим без сохранения состояния │
│ │
│ ИНСТРУМЕНТЫ вызовов 24ч ошибок p50 [ схема ] │
│ chat 128 3 840 ms ▸ │
│ list_models 44 0 12 ms ▸ │
│ run_cli 9 1 62.1 s ▸ │
│ … │
│ │
│ КЛИЕНТЫ │
│ telecodex последний вызов 3 мин назад 88 вызовов за сутки │
│ (без метки) последний вызов 2 ч назад 12 вызовов за сутки │
│ │
│ ФРАГМЕНТ КОНФИГУРАЦИИ КЛИЕНТА [Копировать] │
│ { "mcpServers": { "model-router": { "url": "https://…/mcp", │
│ "headers": { "Authorization": "Bearer <токен>" } } } } │
└──────────────────────────────────────────────────────────────────────────────────────┘
Схема инструмента раскрывается рядом со схемой соответствующей ручки /api, что сразу проявляет расхождения. Экрана «сессии MCP» не будет: сервер работает без сохранения состояния по ADR-0003, такой экран был бы пустым по устройству.
Различать клиентов позволяет таблица именованных токенов вместо одного общего. Метка клиента пишется в журнал вместе с каналом и именем инструмента.
13. Экран «Журнал»¶
Две вкладки: «Запросы» и «Расход».
┌─ ЖУРНАЛ ── [ Запросы │ Расход ] ──────────────────────────────────────────────────────┐
│ метка [все ▾] статус [все ▾] модальность [все ▾] модель [ ] канал [все ▾] 24ч ▾ │
│ ☐ с подстановкой ☐ были повторы ☐ цена не подтверждена [Живой хвост ●] [Выгрузить]│
├───────────────────────────────────────────────────────────────────────────────────────┤
│ id время* метка модель мод. кан. стат время токены $ │
│ 4821 14:22:07 web-tester deepseek-chat ⇄ chat ui ok 1.84s 1042/318 0.000412≈│
│ 4820 14:21:44 bot-77 gpt-4o-mini chat mcp ok 0.92s 1039/291 0.000198│
│ 4819 14:20:03 bot-77 — tts mcp ОШБК — — — │
│ elevenlabs не настроен · переменная ELEVENLABS_API_KEY · [открыть поставщика] │
│ 4818 14:19:55 web-tester claude-sonnet-4-6 cli ui ok 412.10s 8123/902 0.041000*│
│ * условная стоимость по подписке │
├───────────────────────────────────────────────────────────────────────────────────────┤
│ показано 50 из 12 418 * время местное (UTC+5) [ ещё 50 ] │
└───────────────────────────────────────────────────────────────────────────────────────┘
- Ошибка разворачивается строкой под записью, с подсказкой и кнопкой действия. Сегодня текст ошибки лежит в базе и на экран не выводится вовсе.
- Время показывается местное с явной подписью зоны. В базе оно в UTC, оператор не должен вычитать в уме.
- Пагинация курсорная по идентификатору. Смещение и живой хвост несовместимы: журнал растёт с головы, и вторая страница покажет часть первой.
- Список отдаёт только колонки таблицы. Сегодня выдаётся полная строка вместе с ответом до 4000 символов и сырым usage на каждую из пятидесяти записей.
- Карточка запроса это тот же инспектор, что на «Прогоне», плюс кнопки «Повторить» и «Повторить с другой моделью». Для stt повтор работает через сохранённый файл запроса.
13.1 Вкладка «Расход»¶
период [30 дней ▾] группировка [пользователь │ модель │ поставщик │ канал │ день]
всего не менее $12.84 · 12 418 запросов · 4.2M токенов
├ подтверждённые ставки $8.10
├ неподтверждённые ставки $3.90 ≈
├ условная стоимость подписки $0.84 не списывалась
├ без методики цены 412 запроса → в журнал
└ стоимость неизвестна (таймаут) 7 запросов → в журнал
▁▂▃▅▂▁▂▇▅▃▂▁▁▂▃▄▅▃▂▁▂▃▄▂▁▁▂▃ по дням
пользователь запросов токенов промпт/кэш/рассуждение $ подтверждено
bot-77 8 210 3 100 000 2.1M / 0.9M / 0.1M 8.12 88%
web-tester 3 004 900 000 0.8M / 0.0M / 0.1M 3.21 12% ⚠
Отдельный экран расхода в v1 заменён вкладкой: те же фильтры по периоду и пользователю, другой рендер.
14. Экран «Обзор» и первый запуск¶
┌─ ОБЗОР ───────────────────────────────────────────────────────────────────────────────┐
│ ┌ СЕРВИС ────────┐ ┌ ПОСТАВЩИКИ ──────┐ ┌ КАТАЛОГИ ────────┐ ┌ ЗА ЧАС ──────────────┐│
│ │ ● работает │ │ рабочих 6/8 │ │ сверка 12 мин │ │ 214 запросов ││
│ │ 4 д 02:11 │ │ ✕ elevenlabs │ │ следующая ч/з 5ч │ │ 3 ошибки ││
│ │ /mcp 12 ms │ │ ○ codex-oauth │ │ 331 модель │ │ не менее $0.42 ││
│ │ база 48 МБ │ │ │ │ ⚠ 13 без цены │ │ 12 без методики цены ││
│ │ файлы 812 МБ ⚠ │ │ [ подробнее ] │ │ [синхронизовать] │ │ [ журнал ] ││
│ └────────────────┘ └──────────────────┘ └──────────────────┘ └──────────────────────┘│
│ │
│ ПОСЛЕДНИЕ ОШИБКИ │
│ 14:20:03 bot-77 mcp tts elevenlabs не настроен #4819 → │
│ 13:58:11 bot-77 mcp chat обрыв по лимиту токенов: max_tokens 512 #4802 → │
│ │
│ СПИСОК РАБОТЫ │
│ 13 моделей без цены · 31 с неподтверждённой ценой · снятая модель вызывалась вчера │
│ файлы ответов занимают 812 МБ, политика хранения не настроена │
└────────────────────────────────────────────────────────────────────────────────────────┘
Каждая строка «списка работы» кликается в соответствующий фильтр.
Первый запуск. Пока в системе нет ни моделей, ни логов, «Обзор» показывает не нули, а шаги:
Сервис запущен, данных пока нет.
1. Задайте ключ хотя бы одного поставщика в .env и перезапустите сервис
задано: OPENROUTER_API_KEY нет · DEEPSEEK_API_KEY нет · …
2. Синхронизируйте каталоги [ Синхронизировать ]
3. Сделайте первый прогон [ Открыть прогон с примером ]
Что это такое: сервис отправляет запросы к разным моделям через один интерфейс,
считает стоимость и пишет журнал. Консоль нужна для проверки и настройки.
Пустые состояния предусмотрены на каждом экране: журнал без записей объясняет, откуда они возьмутся; реестр без моделей ведёт к синхронизации; «Прогон» предлагает вставить пример промпта.
15. Компоненты, адаптив, доступность¶
Компоненты¶
| Компонент | Где | Требования |
|---|---|---|
| Просмотрщик JSON | инспектор, карточка лога, отчёты | сворачивание через details, подсветка по токенам, поиск, копирование пути, порог схлопывания 256 КБ |
| Таблица ключ-значение | сырьё, карточка модели | копирование строки |
| Чип статуса | везде | цвет плюс слово, анимация под prefers-reduced-motion |
| Числовая ячейка | таблицы | правое выравнивание, tabular-nums, точное значение в title и aria-label |
| Значок происхождения цены | реестр, журнал, расход, инспектор | шесть состояний из раздела 2 |
| Таймлайн | инспектор | два уровня: попытки и повторы |
| Инспектор | Прогон, Журнал, мультирежим | один компонент поверх единого конверта |
| Каскад моделей | Прогон | дерево с сервера, счётчики в узлах |
| Панель фасетов | Модели, Журнал | счётчики вместе со строками, состояние в адресе |
| Лента прогонов | Прогон | последние десять за сессию |
Типографика и цвет¶
Интерфейсный шрифт системный. Идентификаторы, JSON, числа, пути только моноширинным: deepseek-v3 и deepseek-v3.1 должны различаться взглядом. Палитра текущая тёмная плюс семантические токены --ok, --warn, --err, вынесенные в tokens.css. Светлая тема откладывается, но токены позволяют добавить её одним блоком.
Адаптив¶
Ширина макета до 1600, но поведение прописано для трёх точек:
- от 1400: три панели на «Моделях», две колонки на «Прогоне».
- 1100-1400 (типичный ноутбук 13 дюймов): на «Моделях» фасеты сворачиваются в выдвижную панель по кнопке, карточка модели уезжает в правый выдвижной слой. На «Прогоне» две колонки сохраняются, ширина запроса фиксируется 420 пикселей.
- до 1100: одна колонка. «Прогон» складывается вертикально, результат сразу под формой. Таблицы получают горизонтальную прокрутку с закреплённой первой колонкой и скрывают второстепенные колонки (токены, канал) под раскрытие строки.
- телефон: полноценно работают «Обзор» и «Прогон» (проверить долгий прогон с дивана, отправить простой запрос). «Модели» и «Журнал» доступны в режиме списка карточек, без таблиц.
Доступность¶
Настоящие <table> с <th scope="col">, видимый :focus-visible, ловушка фокуса в выдвижной карточке и возврат фокуса на строку при закрытии, aria-live на итоговой строке результата, контраст не ниже 4.5 к 1 для всех новых оттенков.
Горячие клавиши¶
Ctrl+Enter отправить, Esc закрыть карточку, / поиск, Ctrl+R в контексте страницы не переопределяется. Односимвольные клавиши не запускают трат: повтор запроса только кнопкой. Подсказка по клавишам по ?.
16. Безопасность вывода¶
Правила, которых в v1 не было, а поверхность v1 увеличивал.
- Никакого
innerHTMLс данными. Текст черезtextContent, структура через создание узлов. Подсветка JSON строится по токенам, а не заменой в строке HTML. - Недоверенными считаются: метка пользователя, текст запроса, текст ответа, сообщение об ошибке, причина подстановки,
model_idиз эха запроса, любые поляusage_rawиraw_meta, имена моделей из каталога, отчёты синхронизации. - Файлы ответов отдаются с
X-Content-Type-Options: nosniff, сContent-Disposition: attachmentдля всего, кроме картинок и аудио, и сохраняются только с типами из белого списка. Сегодня вызывающий может положить.htmlи получить его обратно с того же адреса, что и консоль. - Тело запроса к провайдеру показывается только под флагом отладки. Заголовки клиента не сериализуются никогда: ключи живут именно там.
- Публичная проверка здоровья отдаёт только статус. Версия, список поставщиков и размеры уезжают в закрытую сводку.
- Поле
cwdдля CLI это выпадающий список разрешённых корней, а не свободный ввод, рядом постоянная плашка: прогон выполняет код без подтверждений от имени сервисного пользователя.
17. Контракты бэкенда¶
Полные схемы даны в рецензии API (docs/reviews/UI_CONCEPT_review_api.md, раздел 3). Здесь то, что обязательно для интерфейса.
17.1 Сквозные правила¶
- Префикс
/api/v1вводится сейчас, пока единственный потребитель это своя консоль. - Списки отдаются конвертом
{items, total, next_cursor, facets}, а не голым массивом. - Курсорная пагинация для журнала, смещение допустимо для реестра.
- Время в ISO 8601 с зоной, интерфейс переводит в местное.
- Деньги никогда не отдаются без
cost_source. - Поле
schema_versionв capabilities, ссылки на статику с версией: после перезапуска сервиса браузер не должен работать старым клиентом против нового API.
17.2 Единый конверт прогона¶
Один объект Run отдаётся из прогона, из журнала и из мультирежима. Сегодня формы разные по именам и вложенности, и один инспектор на них не собрать: это станет ясно на третьем экране, когда переписывать дорого.
Собирается двумя функциями в web/views.py: из результата диспетча и из строки журнала. Тест на совпадение ключей обеих функций защищает контракт.
Поля: run_id, log_id, status, channel, tool, client_id, model {provider, model_id, display_name}, requested_model, fallback {kind, trigger, reason}, usage {prompt, completion, cache_read, cache_write, reasoning, audio_seconds, images, characters, raw}, cost {value, source, breakdown[]}, latency_ms, attempts, provider_status, provider_request_id, finish_reason, steps[], attachments[], ignored_params[], error.
17.3 Конверт ошибки¶
Единый для /api и для MCP: type, code, message, provider, model_id, log_id, retriable, provider_status, retry_after_s, hint {kind, env_var, action, target}. Коды: 400 ошибка вызывающего, 404 неизвестная модель, 409 модель снята или поставщик выключен, 422 обрыв по лимиту токенов, 429 лимит провайдера, 502 ошибка провайдера, 503 поставщик не настроен, 504 таймаут.
Без log_id в ошибке переход «из ошибки в журнал» невозможен, а он заявлен в трёх экранах. Сегодня идентификатор записи создаётся и выбрасывается.
17.4 Ручки по волнам¶
Волна 0. Починки: таймаут nginx для длинных запросов; params в теле /api/tts и /api/cli; проекция колонок в списке журнала; GET /api/v1/logs/{id}; индексы под фильтры; единый конверт Run и обработчик ошибок; latency_ms и finish_reason в ответе прогона; миграция колонок журнала одним заходом.
Волна 1. GET /api/v1/capabilities (из SUPPORTED_PARAMS, плюс лимиты, плюс unsupported); POST /api/v1/plan (без обращения к сети, всегда 200); GET /api/v1/logs с фильтрами и курсором; PATCH /api/v1/registry/models/{provider}/{model_id} с оптимистической блокировкой плюс правка критерия защиты цены.
Волна 2. GET /api/v1/registry/models с фасетами и usage_30d; массовая правка; GET /api/v1/health/summary; POST /api/v1/providers/{id}/probe; GET /api/v1/mcp/summary и POST /api/v1/mcp/probe; GET /api/v1/usage с группировками и разбивкой по источнику цены; экспорт журнала.
Волна 3. POST/GET/DELETE /api/v1/runs (реестр прогонов, прогресс, отмена, client_request_id, batch_id); POST /api/v1/blobs и вложения по ссылке; status=running и восстановление в interrupted; plan_json в журнале; именованные токены.
17.5 Разделение слоёв¶
/api/* остаётся чистым JSON и стабильным контрактом. HTML-фрагменты для сервера живут отдельно под /ui/*. Это правило делает возможный переход на сборку переносом слоя представления, а не переписыванием.
18. Стек и структура фронтенда¶
Решение: остаться на Jinja2 без сборщика, добавить htmx и Alpine вендоренными файлами, вынести весь JS из шаблонов в нативные ES-модули. Сборка сегодня стоит дороже, чем экономит: прод обновляется через git pull и pip install, node на хосте нет, тестовый контур это pytest. Решение о сборке пересматривается в третьей волне по факту.
router_mcp/web/
api.py /api/v1/*, только JSON
views.py НОВОЕ: единый конверт Run, модели, фасеты
capabilities.py НОВОЕ: матрица из SUPPORTED_PARAMS, перечни, schema_version
fragments.py НОВОЕ: /ui/*, HTML-фрагменты для htmx
pages.py каркасы, читают параметры адреса и отдают первый экран заполненным
templates/{pages,partials}/
static/
vendor/htmx.min.js alpine.min.js
css/tokens.css base.css tables.css forms.css inspector.css
js/core/{api,state,format,dom,keys}.js
components/{json-view,inspector,kv-table,chips,facets,timeline,numcell}.js
pages/{run,models,logs,overview,connections}.js
tests/
test_web_views.py совпадение ключей двух конструкторов Run
test_api_filters.py фильтры, курсор, фасеты
test_capabilities.py матрица совпадает с SUPPORTED_PARAMS
test_escaping.py строка со скриптом отображается как текст
ui/ Playwright со второй волны
Правила: ни один шаблон не содержит логики в <script>; компонент не ходит в сеть и не читает адрес; HTML из данных строится только через dom.js; предметное знание (разбор идентификатора, единицы цены, релевантность, тексты причин) живёт на сервере.
Состояние в адресе. В адресе живут фильтры, поиск, страница, сортировка, выбранная строка, активная вкладка, ?log=. Не живут: живой хвост, сворачивание узлов, черновик промпта, тема. Первый рендер делает сервер, дальше фрагменты. Сегодня страницы игнорируют параметры адреса и всегда отдают пустой каркас.
19. Масштаб и производительность¶
- Реестр: 331 модель сейчас, до тысячи потом. Страницы по 50, фильтрация и сортировка на сервере, виртуализация не нужна.
- Список моделей для каскада кешируется на сессию, при смене ценового уровня фильтруется на месте без повторной загрузки.
- Журнал: курсор, проекция колонок, живой хвост запросом только новых строк раз в три секунды.
- Файлы ответов отдаются с длинным кешем и поддержкой докачки: содержимое по ссылке неизменно, аудиоплеер сможет перематывать.
- Пороги схлопывания: 8 КБ для текста, 256 КБ для JSON. Подсветка мегабайтного документа вешает вкладку.
- Одно соединение с SQLite и общий замок на запись: мультимодельный прогон на шесть моделей и живой хвост проверяются на этом до третьей волны.
- Ретеншен: политика хранения журнала и файлов отсутствует, файлы не удаляются никогда. В третьей волне нужна ручка очистки и настройка срока, на «Обзоре» уже сейчас показывается занятое место.
20. Приёмка¶
Оператор без чтения исходников может:
- Ответить, почему запрос ушёл не на ту модель, за два клика от результата, и отличить запланированную подстановку от подстановки после отказа.
- Увидеть, что восьмисекундный ответ это два повтора по лимиту, а не медленная модель.
- Скопировать JSON запроса и ответа и идентификатор запроса у провайдера.
- Найти все модели без подтверждённой цены, проставить цену и убедиться, что синхронизация её не затёрла.
- Понять, почему модальность tts отказывает, и узнать имя переменной окружения.
- Прогнать один промпт на трёх моделях, увидев сумму до запуска.
- Повторить любой запрос из журнала, включая распознавание речи.
- Отличить посчитанную цену от прикидки, от условной подписочной и от «методики нет» в любом месте, где показаны деньги.
- Понять состояние сервиса за пять секунд на первом экране, а на пустой установке получить список шагов.
- Увидеть, какой MCP-клиент каким инструментом пользуется.
- Прервать долгий прогон и вернуться к нему после перезагрузки страницы.
- Открыть журнал, в котором есть запись с меткой пользователя
<img src=x onerror=…>, и увидеть её как текст.
Пункты 2, 8, 10, 11, 12 в v1 отсутствовали.
21. Волны¶
Оценки в человеко-днях для одного разработчика с агентской поддержкой, с запасом на проверку на живом сервисе.
Волна 0. Починки и фундамент. 5-6 дней¶
| Задача | Дни |
|---|---|
| nginx: отдельный location для длинных запросов, буферизация, таймаут | 0.5 |
Устранить XSS: dom.js, переписать три текущих шаблона, тест |
0.5 |
Единый конверт Run в views.py, latency_ms и finish_reason в ответе |
1 |
Контракт ошибки, один обработчик, log_id в ошибке |
1 |
| Миграция колонок журнала одним заходом | 0.5 |
Список журнала: проекция колонок, /logs/{id}, индексы |
0.5 |
params в /api/tts и /api/cli (язык синтеза, модель CLI) |
0.25 |
Каркас статики: токены, core/*.js, вендоринг, версия в ссылках |
1 |
| Кеш и докачка для файлов ответов, белый список типов, заголовки | 0.5 |
Волна 1. Каркас правды. 9-11 дней¶
| Задача | Дни |
|---|---|
| Навигация из пяти пунктов, каркасы, состояние в адресе | 1 |
SUPPORTED_PARAMS и /api/v1/capabilities (приписка к ADR-0001) |
1 |
| «Прогон»: форма по матрице, два режима выбора, дерево с сервера, фолбэк одним селектом | 2 |
| Просмотрщик JSON | 1.5 |
| Инспектор с четырьмя вкладками и двухуровневым таймлайном | 1.5 |
POST /api/v1/plan и человекочитаемый план |
1 |
| Журнал: фильтры, курсор, разворот ошибки, карточка тем же инспектором, местное время | 1.5 |
Правка цены: PATCH плюс критерий защиты цены |
0.5 |
Бытовое вокруг прогона: лента сессии, черновик, повтор последнего, копирование, ?log= |
1 |
Волна 2. Учёт, объём, обзор. 8-10 дней¶
| Задача | Дни |
|---|---|
Учёт: кэш- и рассуждающие токены в Usage и в расчёте, unpriced, provider_notional, usage.cost у OpenRouter, цена за символ |
2 |
| «Модели»: три панели, фасеты со счётчиками, карточка, единицы и защита от промаха | 2 |
| Массовые действия с подтверждением | 0.5 |
| «Подключения»: поставщики, отчёты, проба ключей | 1 |
| Вкладка MCP: инвентарь, самопроба, каналы в журнале, именованные токены | 1.5 |
| «Обзор» и сводка здоровья, пустые состояния первого запуска | 1.5 |
| Расход как вкладка: группировки, разбивка по источникам, экспорт | 1.5 |
| Playwright, 10-12 сценариев по разделу 20 | 1 |
Волна 3. Динамика. 5-7 дней¶
| Задача | Дни |
|---|---|
| Реестр прогонов, прогресс, отмена, идемпотентность | 2 |
| Построчное чтение вывода CLI и шаги в реальном времени | 1 |
running и interrupted, восстановление при старте |
0.5 |
| Мультимодельный режим «Прогона» | 1.5 |
plan_json в журнале, восстановление таймлайна |
1 |
| Вложения по ссылке, повтор распознавания из журнала | 0.5 |
| Ретеншен: срок хранения журнала и файлов, ручка очистки | 0.5 |
Итого 27-34 дня. Против оценки архитектора добавлено 6 дней: разбор токенов и починка учёта, вкладка MCP, бытовые вещи вокруг прогона, пустые состояния.
22. Что осознанно не делаем¶
| Отклонено | Почему |
|---|---|
| SSE и стриминг ответа в чате | Ломает повторы и определение обрыва по лимиту, требует отдельного ADR. Прогресс закрывается опросом реестра прогонов |
| Виртуальные таблицы | 331 строка со страницами по 50 не требует виртуализатора |
| Серверный пакетный прогон | N обычных прогонов с общим идентификатором дают то же самое плюс частичные результаты и отмену по одному |
| Отдельные экраны «Матрица» и «Расход» | Режим и вкладка соответственно |
| Светлая тема сейчас | Один оператор, тёмная по умолчанию. Токены оставляют дверь открытой |
| Сборка фронтенда | Прод обновляется двумя командами, node на хосте нет. Пересмотр в третьей волне |
| Экран «сессии MCP» | Сервер без сохранения состояния по ADR-0003, экран был бы пустым |
| Разделение прав «смотреть» и «запускать» | Одна инсталляция, один оператор. Записано в открытые вопросы, а не в план |
| Batch API провайдеров | Отдельная функциональность сервиса, не интерфейса |
23. Ответы на открытые вопросы v1¶
| Вопрос v1 | Ответ |
|---|---|
| 1. Семь пунктов навигации | Пять. MCP добавлен вкладкой, «Матрица» и «Расход» свёрнуты |
| 2. «Матрица» отдельным экраном | Режим «Прогона» |
| 3. Правка каталожной цены | Править саму строку, защиту цены завязать на её происхождение. Отдельный слой наложений не нужен |
| 4. План по кнопке или всегда | Автоматически при смене дискретных полей с задержкой, полная таблица по кнопке. План не должен ходить в сеть |
| 5. Статус «выполняется» в журнале | Нужен: перезапуск сервиса не должен терять получасовой прогон |
| 6. Светлая тема | Отложена, токены заведены |
| 7. Показывать ли тело запроса к провайдеру | Показывать тело под флагом отладки, заголовки не сериализовать никогда |
24. Открытые вопросы v2¶
- Ретеншен: сколько хранить журнал и файлы ответов, удалять ли файлы вместе с записями журнала или отдельно по возрасту.
- Именованные токены против одного общего: готовы ли мы завести таблицу токенов ради различения MCP-клиентов, или достаточно метки, которую клиент присылает сам (и которой нельзя доверять).
- Разделение «смотреть» и «запускать»: нужно ли оно при одном операторе, учитывая, что консоль опубликована в интернет за Basic auth.
- Порог ценового уровня: сегодня почти весь каталог попадает в «дорогие». Пересчитывать порог по данным или заменить уровень на квантили каталога.
- Мультимодельный прогон и одно соединение с SQLite: шесть параллельных прогонов пишут журнал через общий замок. Проверять на нагрузке или сразу ограничить параллельность тремя.
- Прикидка стоимости у кнопки требует ожидаемого числа токенов. Брать длину промпта и типовой ответ или показывать вилку.
- Телефон: ограничиться «Обзором» и «Прогоном» или довести до списков все экраны.