[feature][ai-chat] Бейдж контекста в шапке: показывать «текущий / максимум»; максимум — из настроек AI #189

Closed
opened 2026-06-25 13:23:30 +03:00 by Ghost · 0 comments

Контекст / проблема

Сейчас число в шапке плавающего окна AI-чата (бейдж справа от имени роли) меняет смысл в зависимости от состояния:

  • во время стрима ответа показывается liveTurnTokens — оценка токенов, сгенерированных в текущем ходе (reasoning + output), тикает ~8 Гц, тултип «Tokens generated this turn»;
  • в покое показывается contextTokensтекущий размер контекста (сколько диалог занимает в окне модели), тултип «Current context size».

Из-за этого при отправке большого промпта бейдж «сбрасывается на 1» и считает заново: во время хода он считает только генерацию модели (ввод пользователя туда вообще не входит) и стартует с нуля, а после хода снова показывает размер контекста. Одно и то же место без видимой подписи показывает две разные величины — это путает.

Живой фидбек «модель работает / сколько надумала» уже есть внутри тела чата — блок «Thinking · N tokens» (ReasoningBlock, тикает live), плюс готовится live-reasoning по #178. Значит дублирующий live-счётчик в шапке не нужен.

Текущее поведение (где в коде)

  • Рендер бейджа: apps/client/src/features/ai-chat/components/ai-chat-window.tsx (~строки 497–514) — ветка liveTurnTokens > 0 ? ... : contextTokens > 0 ? ....
  • Текущий контекст: тот же файл, useMemo ~290–307 — берёт metadata.contextTokens из последней ассистентской строки (фолбэк на usage.totalTokens).
  • Live-счётчик хода: apps/client/src/features/ai-chat/utils/count-stream-tokens.ts (liveTurnTokens()), продюсер — apps/client/src/features/ai-chat/components/chat-thread.tsx (эффект ~335–368, проп onLiveTurnTokens), стейт — ai-chat-window.tsx:168.
  • Источник contextTokens на сервере: apps/server/src/core/ai-chat/ai-chat.service.tsflushAssistant(..., { contextTokens }) (~612–619), сборка метаданных в flushAssistant (~1218–1256).
  • Максимального размера контекста нигде нет: модель задаётся произвольной строкой chatModel через драйверы openai / gemini / ollama (apps/server/src/integrations/ai/ai.types.ts), может переопределяться на уровне роли (role.modelConfig.chatModel), а клиент-окно вообще не знает, какая модель используется.

Желаемое поведение

Бейдж в шапке всегда показывает текущий / максимум контекста, например 572 / 200k:

  • режим live-счётчика хода ��з шапки убрать (живой фидбек остаётся в теле чата);
  • когда максимум не задан — показывать только текущий контекст (как сейчас, без знаменателя);
  • тултип переписать на смысл «размер контекста / лимит модели».

Принятое решение: источник максимума

Единого надёжного способа узнать окно контекста для всех драйверов нет (/v1/models у OpenAI обычно не отдаёт лимит; у Gemini — inputTokenLimit, у Ollama — /api/show, у OpenRouter — context_length; всё несовместимо). Поэтому максимум берётся из нового поля в настройках AI — админ задаёт число (токены) вручную. Плюсы: всегда точно, провайдер-независимо, минимум кода. Минус: ручное заполнение (приемлемо).

Отвергнутые на v1 альтернативы: статическая таблица «id модели → окно» (надо поддерживать, не угадывает кастомные id), рантайм-запрос к провайдеру (много провайдер-специфичного кода, не работает для чистого OpenAI/произвольного совместимого), гибрид (больше всего работы). При желании их можно добавить позже поверх этого поля.

Технически число доводится до клиента через метаданные ассистентского сообщения (рядом с contextTokens): сервер при финализации хода уже имеет резолвнутый конфиг (resolved в ai-chat.service.ts:338) и кладёт maxContextTokens в ту же метадату. Бейдж читает оба числа из последней строки — никакой новой клиентской логики резолва модели, и это автоматически переживёт публичные шары и (в будущем) пер-ролевые модели.

План реализации

Сервер

  1. apps/server/src/integrations/ai/ai.types.ts: добавить chatContextWindow?: number в AiProviderSettings, в PROVIDER_SETTINGS_KEYS, в MaskedAiSettings и в ResolvedAiConfig.
  2. apps/server/src/database/repos/workspace/workspace.repo.ts: добавить ключ chatContextWindow в parity-копию AI_PROVIDER_SETTINGS_ALLOWED (иначе значение молча отбрасывается на SQL-границе; покрыто ai-provider-settings-keys.spec.ts).
  3. apps/server/src/integrations/ai/dto/update-ai-settings.dto.ts: новое поле @IsOptional() @IsInt() @Min(0) chatContextWindow?: number (0/пусто = очистить лимит).
  4. apps/server/src/integrations/ai/ai-settings.service.ts: прокинуть поле в resolve() и getMasked().
  5. apps/server/src/core/ai-chat/ai-chat.service.ts: в flushAssistant расширить extra полем maxContextTokens?: number и писать metadata.maxContextTokens, когда оно > 0; в streamChat передать resolved.chatContextWindow в финализацию завершённого хода (~612–619).

Клиент

  1. apps/client/src/features/ai-chat/types/ai-chat.types.ts: добавить maxContextTokens?: number в IAiChatMessageRow.metadata (с комментарием о семантике рядом с contextTokens).
  2. apps/client/src/features/ai-chat/components/ai-chat-window.tsx: читать maxContextTokens из той же последней строки; заменить ре��дер бейджа на «текущий [/ максимум]» (formatTokens(ctx) + при max>0/ formatTokens(max)); убрать ветку liveTurnTokens, стейт liveTurnTokens и проп onLiveTurnTokens; обновить тултип.
  3. apps/client/src/features/ai-chat/components/chat-thread.tsx: удалить эффект/проп onLiveTurnTokens (~335–368, 75, 120) и импорт liveTurnTokens (строка 23).
  4. count-stream-tokens.ts: файл оставитьestimateTokens используется в ReasoningBlock; функция liveTurnTokens() остаётся без вызовов — удалить её вместе с её тестом, либо оставить как util (решить при ревью; рекомендация — удалить мёртвый код).

Настройки (UI)

  1. apps/client/src/features/workspace/services/ai-settings-service.ts: добавить chatContextWindow?: number в IAiSettings и IAiSettingsUpdate.
  2. apps/client/src/pages/settings/workspace/ai-settings.tsx: числовое поле «Context window (tokens)» с подсказкой «показывает использовано/всего в шапке чата; пусто = скрыть лимит».

i18n / Тесты

  1. Новые строки через t() (тултип бейджа, лейбл и подсказка в настройках) — в локали.
  2. Тесты: chatStreamMetadata/flushAssistant (проброс maxContextTokens), маппинг в settings-сервисе, parity-тест ключей, рендер бейджа «ctx / max» и отсутствие live-режима; обновить существующие тесты, завязанные на старый live-бейдж.

Краевые случаи

  • Лимит не задан / 0 → показываем только текущий контекст (поведение как сейчас).
  • context > max (дрейф оценки или роль с меньшей реальной моделью) → показываем как есть (210k / 200k), без клампа; можно подсветить — на усмотрение.
  • Старые строки истории без maxContextTokens → знаменатель скрыт.
  • Публичный шар-чат: отдельный путь; на v1 знаменатель там может не показываться (допустимо).

Вне скоупа (v1)

  • Пер-ролевое переопределение лимита (RoleModelConfig сейчас несёт только {driver, chatModel}).
  • Автоопределение окна (таблица/рантайм-probe) — возможное развитие поверх поля.

Критерии приёмки

  • В настройках AI есть числовое поле «context window», сохраняется и резолвится.
  • Бейдж в шапке показывает текущий / максимум (или только текущий, если лимит пуст).
  • Бейдж больше не «сбрасывается на 1» при отправке промпта (live-режим из шапки убран).
  • Живой «Thinking · N tokens» в теле чата продолжает работать.
  • Зелёные тесты, в т.ч. parity-тест ключей провайдера.
## Контекст / проблема Сейчас число в шапке плавающего окна AI-чата (бейдж справа от имени роли) **меняет смысл в зависимости от состояния**: - во время стрима ответа показывается `liveTurnTokens` — оценка токенов, сгенерированных **в текущем ходе** (reasoning + output), тикает ~8 Гц, тултип «Tokens generated this turn»; - в покое показывается `contextTokens` — **текущий размер контекста** (сколько диалог занимает в окне модели), тултип «Current context size». Из-за этого при отправке большого промпта бейдж «сбрасывается на 1» и считает заново: во время хода он считает только генерацию модели (ввод пользователя туда вообще не входит) и стартует с нуля, а после хода снова показывает размер контекста. Одно и то же место без видимой подписи показывает две разные величины — это путает. Живой фидбек «модель работает / сколько надумала» **уже есть внутри тела чата** — блок «Thinking · N tokens» (`ReasoningBlock`, тикает live), плюс готовится live-reasoning по #178. Значит дублирующий live-счётчик в шапке не нужен. ## Текущее поведение (где в коде) - Рендер бейджа: `apps/client/src/features/ai-chat/components/ai-chat-window.tsx` (~строки 497–514) — ветка `liveTurnTokens > 0 ? ... : contextTokens > 0 ? ...`. - Текущий контекст: тот же файл, `useMemo` ~290–307 — берёт `metadata.contextTokens` из последней ассистентской строки (фолбэк на `usage.totalTokens`). - Live-счётчик хода: `apps/client/src/features/ai-chat/utils/count-stream-tokens.ts` (`liveTurnTokens()`), продюсер — `apps/client/src/features/ai-chat/components/chat-thread.tsx` (эффект ~335–368, проп `onLiveTurnTokens`), стейт — `ai-chat-window.tsx:168`. - Источник `contextTokens` на сервере: `apps/server/src/core/ai-chat/ai-chat.service.ts` — `flushAssistant(..., { contextTokens })` (~612–619), сборка метаданных в `flushAssistant` (~1218–1256). - Максимального размера контекста **нигде нет**: модель задаётся произвольной строкой `chatModel` через драйверы `openai` / `gemini` / `ollama` (`apps/server/src/integrations/ai/ai.types.ts`), может переопределяться на уровне роли (`role.modelConfig.chatModel`), а клиент-окно вообще не знает, какая модель используется. ## Желаемое поведение Бейдж в шапке всегда показывает **текущий / максимум контекста**, например `572 / 200k`: - режим live-счётчика хода ��з шапки **убрать** (живой фидбек остаётся в теле чата); - когда максимум не задан — показывать только текущий контекст (как сейчас, без знаменателя); - тултип переписать на смысл «размер контекста / лимит модели». ## Принятое решение: источник максимума Единого надёжного способа узнать окно контекста для всех драйверов нет (`/v1/models` у OpenAI обычно не отдаёт лимит; у Gemini — `inputTokenLimit`, у Ollama — `/api/show`, у OpenRouter — `context_length`; всё несовместимо). Поэтому максимум берётся из **нового поля в настройках AI** — админ задаёт число (токены) вручную. Плюсы: всегда точно, провайдер-независимо, минимум кода. Минус: ручное заполнение (приемлемо). Отвергнутые на v1 альтернативы: статическая таблица «id модели → окно» (надо поддерживать, не угадывает кастомные id), рантайм-запрос к провайдеру (много провайдер-специфичного кода, не работает для чистого OpenAI/произвольного совместимого), гибрид (больше всего работы). При желании их можно добавить позже поверх этого поля. Технически число доводится до клиента через **метаданные ассистентского сообщения** (рядом с `contextTokens`): сервер при финализации хода уже имеет резолвнутый конфиг (`resolved` в `ai-chat.service.ts:338`) и кладёт `maxContextTokens` в ту же метадату. Бейдж читает оба числа из последней строки — никакой новой клиентской логики резолва модели, и это автоматически переживёт публичные шары и (в будущем) пер-ролевые модели. ## План реализации ### Сервер 1. `apps/server/src/integrations/ai/ai.types.ts`: добавить `chatContextWindow?: number` в `AiProviderSettings`, в `PROVIDER_SETTINGS_KEYS`, в `MaskedAiSettings` и в `ResolvedAiConfig`. 2. `apps/server/src/database/repos/workspace/workspace.repo.ts`: добавить ключ `chatContextWindow` в parity-копию `AI_PROVIDER_SETTINGS_ALLOWED` (иначе значение молча отбрасывается на SQL-границе; покрыто `ai-provider-settings-keys.spec.ts`). 3. `apps/server/src/integrations/ai/dto/update-ai-settings.dto.ts`: новое поле `@IsOptional() @IsInt() @Min(0) chatContextWindow?: number` (0/пусто = очистить лимит). 4. `apps/server/src/integrations/ai/ai-settings.service.ts`: прокинуть поле в `resolve()` и `getMasked()`. 5. `apps/server/src/core/ai-chat/ai-chat.service.ts`: в `flushAssistant` расширить `extra` полем `maxContextTokens?: number` и писать `metadata.maxContextTokens`, когда оно > 0; в `streamChat` передать `resolved.chatContextWindow` в финализацию завершённого хода (~612–619). ### Клиент 6. `apps/client/src/features/ai-chat/types/ai-chat.types.ts`: добавить `maxContextTokens?: number` в `IAiChatMessageRow.metadata` (с комментарием о семантике рядом с `contextTokens`). 7. `apps/client/src/features/ai-chat/components/ai-chat-window.tsx`: читать `maxContextTokens` из той же последней строки; заменить ре��дер бейджа на «текущий [/ максимум]» (`formatTokens(ctx)` + при `max>0` — `/ formatTokens(max)`); убрать ветку `liveTurnTokens`, стейт `liveTurnTokens` и проп `onLiveTurnTokens`; обновить тултип. 8. `apps/client/src/features/ai-chat/components/chat-thread.tsx`: удалить эффект/проп `onLiveTurnTokens` (~335–368, 75, 120) и импорт `liveTurnTokens` (строка 23). 9. `count-stream-tokens.ts`: файл **оставить** — `estimateTokens` используется в `ReasoningBlock`; функция `liveTurnTokens()` остаётся без вызовов — удалить её вместе с её тестом, либо оставить как util (решить при ревью; рекомендация — удалить мёртвый код). ### Настройки (UI) 10. `apps/client/src/features/workspace/services/ai-settings-service.ts`: добавить `chatContextWindow?: number` в `IAiSettings` и `IAiSettingsUpdate`. 11. `apps/client/src/pages/settings/workspace/ai-settings.tsx`: числовое поле «Context window (tokens)» с подсказкой «показывает использовано/всего в шапке чата; пусто = скрыть лимит». ### i18n / Тесты 12. Новые строки через `t()` (тултип бейджа, лейбл и подсказка в настройках) — в локали. 13. Тесты: `chatStreamMetadata`/`flushAssistant` (проброс `maxContextTokens`), маппинг в settings-сервисе, parity-тест ключей, рендер бейджа «ctx / max» и отсутствие live-режима; обновить существующие тесты, завязанные на старый live-бейдж. ## Краевые случаи - Лимит не задан / 0 → показываем только текущий контекст (поведение как сейчас). - `context > max` (дрейф оценки или роль с меньшей реальной моделью) → показываем как есть (`210k / 200k`), без клампа; можно подсветить — на усмотрение. - Старые строки истории без `maxContextTokens` → знаменатель скрыт. - Публичный шар-чат: отдельный путь; на v1 знаменатель там может не показываться (допустимо). ## Вне скоупа (v1) - Пер-ролевое переопределение лимита (`RoleModelConfig` сейчас несёт только `{driver, chatModel}`). - Автоопределение окна (таблица/рантайм-probe) — возможное развитие поверх поля. ## Критерии приёмки - [ ] В настройках AI есть числовое поле «context window», сохраняется и резолвится. - [ ] Бейдж в шапке показывает `текущий / максимум` (или только текущий, если лимит пуст). - [ ] Бейдж больше не «сбрасывается на 1» при отправке промпта (live-режим из шапки убран). - [ ] Живой «Thinking · N tokens» в теле чата продолжает работать. - [ ] Зелёные тесты, в т.ч. parity-тест ключей провайдера.
Ghost added the feature label 2026-06-25 13:23:30 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#189