Перейти к содержанию

Концепция веб-консоли 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 Повторов Сколько раз запрос повторялся из-за временных отказов

Правила словаря:

  1. Термин, впервые появляющийся на экране, снабжён пояснением под контролом. Тултип годится для уточнения, не для первого объяснения.
  2. Одно понятие переводится одинаково в форме, в таблице, в фасете и в отчёте. Список подписей живёт в одном файле локализации, а не в трёх шаблонах.
  3. Английский идентификатор остаётся видимым рядом с русской подписью там, где оператор будет искать его в 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. Модель денег в интерфейсе

Отдельный раздел, потому что это место, где интерфейс легче всего врёт уверенно.

  1. Ни одно число в долларах не показывается без источника. Значок или подпись обязательны везде: инспектор, журнал, расход, карточка модели, прикидка у кнопки.
  2. Итог с примесью unpriced или cost_unknown подписывается как нижняя граница. Рядом строка «N запросов без методики цены» и «M запросов с неизвестной стоимостью», обе кликаются в отфильтрованный журнал.
  3. Условная стоимость подписки не смешивается с настоящими деньгами. provider_notional идёт отдельной строкой итога.
  4. Расхождение между ценой провайдера и ценой по прайсу показывается явно. Когда OpenRouter вернул usage.cost, рядом ставится расчёт по реестру, а разница подсвечивается: это лучший индикатор протухшей цены.
  5. Прикидка перед тратой. Кнопка отправки несёт ожидаемую сумму. Для модальностей без методики (image, stt, tts сегодня) пишется «стоимость неизвестна», а не ноль.
  6. Пакетный запуск требует подтверждения с числом запросов и суммой прикидки.
  7. Правка цены защищена от промаха на порядок. Единица внутри поля, предупреждение при отличии от прежнего значения больше чем в 10 раз, показ старого и нового значения перед сохранением.

Три дефекта учёта чинятся до того, как рисуется экран расхода, иначе он будет показывать уверенно неверные числа: нулевые ставки seed-строк для image, stt, tts; отсутствие поля «цена за символ» для ElevenLabs; нулевая seed-цена, выигрывающая автоматический подбор (страховка сегодня срабатывает только на unknown, а seed-строки имеют estimate).


8. Принципы

  1. Итог сверху, сырьё внизу, всё раскрывается на месте.
  2. Ничего не исчезает: отчёт синхронизации, ошибка и план остаются на экране до следующего действия.
  3. Состояние вместо галочки: где состояний шесть, рисуем шесть.
  4. Числа выравниваются по правому краю моноширинным шрифтом с одинаковым числом знаков.
  5. Каждое число объясняет своё происхождение.
  6. Форма адаптируется к выбору, а игнорируемые параметры называются вслух.
  7. Списки от трёхсот строк фильтруются на сервере, страницами по 50.
  8. Ошибка называет причину, несёт код и ссылку на строку журнала.
  9. Деньги под защитой: прикидка перед тратой, подтверждение на пакет, защита от промаха в цене.
  10. Никакого innerHTML с данными. Текст только через textContent.
  11. Одно понятие называется одним словом во всех местах.
  12. Клавиатура важнее мыши, но односимвольные клавиши не тратят деньги.

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 увеличивал.

  1. Никакого innerHTML с данными. Текст через textContent, структура через создание узлов. Подсветка JSON строится по токенам, а не заменой в строке HTML.
  2. Недоверенными считаются: метка пользователя, текст запроса, текст ответа, сообщение об ошибке, причина подстановки, model_id из эха запроса, любые поля usage_raw и raw_meta, имена моделей из каталога, отчёты синхронизации.
  3. Файлы ответов отдаются с X-Content-Type-Options: nosniff, с Content-Disposition: attachment для всего, кроме картинок и аудио, и сохраняются только с типами из белого списка. Сегодня вызывающий может положить .html и получить его обратно с того же адреса, что и консоль.
  4. Тело запроса к провайдеру показывается только под флагом отладки. Заголовки клиента не сериализуются никогда: ключи живут именно там.
  5. Публичная проверка здоровья отдаёт только статус. Версия, список поставщиков и размеры уезжают в закрытую сводку.
  6. Поле 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. Приёмка

Оператор без чтения исходников может:

  1. Ответить, почему запрос ушёл не на ту модель, за два клика от результата, и отличить запланированную подстановку от подстановки после отказа.
  2. Увидеть, что восьмисекундный ответ это два повтора по лимиту, а не медленная модель.
  3. Скопировать JSON запроса и ответа и идентификатор запроса у провайдера.
  4. Найти все модели без подтверждённой цены, проставить цену и убедиться, что синхронизация её не затёрла.
  5. Понять, почему модальность tts отказывает, и узнать имя переменной окружения.
  6. Прогнать один промпт на трёх моделях, увидев сумму до запуска.
  7. Повторить любой запрос из журнала, включая распознавание речи.
  8. Отличить посчитанную цену от прикидки, от условной подписочной и от «методики нет» в любом месте, где показаны деньги.
  9. Понять состояние сервиса за пять секунд на первом экране, а на пустой установке получить список шагов.
  10. Увидеть, какой MCP-клиент каким инструментом пользуется.
  11. Прервать долгий прогон и вернуться к нему после перезагрузки страницы.
  12. Открыть журнал, в котором есть запись с меткой пользователя <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

  1. Ретеншен: сколько хранить журнал и файлы ответов, удалять ли файлы вместе с записями журнала или отдельно по возрасту.
  2. Именованные токены против одного общего: готовы ли мы завести таблицу токенов ради различения MCP-клиентов, или достаточно метки, которую клиент присылает сам (и которой нельзя доверять).
  3. Разделение «смотреть» и «запускать»: нужно ли оно при одном операторе, учитывая, что консоль опубликована в интернет за Basic auth.
  4. Порог ценового уровня: сегодня почти весь каталог попадает в «дорогие». Пересчитывать порог по данным или заменить уровень на квантили каталога.
  5. Мультимодельный прогон и одно соединение с SQLite: шесть параллельных прогонов пишут журнал через общий замок. Проверять на нагрузке или сразу ограничить параллельность тремя.
  6. Прикидка стоимости у кнопки требует ожидаемого числа токенов. Брать длину промпта и типовой ответ или показывать вилку.
  7. Телефон: ограничиться «Обзором» и «Прогоном» или довести до списков все экраны.