The floating chat window's header badge flipped meaning — a live per-turn token counter while streaming, the persisted context size at rest — so it "reset to 1" on each prompt and conflated two different numbers. Replace it with a stable "current / max" context badge (e.g. `572 / 200k`). The live "Thinking · N tokens" inside the chat body stays; only the duplicate live counter is removed from the header. Max comes from a new admin setting "Context window (tokens)". The server resolves it and attaches `maxContextTokens` to the completed assistant turn's metadata (next to contextTokens), so the badge needs no client-side model resolution and this survives public shares / per-role models. Server: - ai.types: chatContextWindow on AiProviderSettings + PROVIDER_SETTINGS_KEYS + ResolvedAiConfig + MaskedAiSettings. - workspace.repo: chatContextWindow in AI_PROVIDER_SETTINGS_ALLOWED (parity). - update-ai-settings.dto: @IsInt @Min(0) chatContextWindow. - ai-settings.service: coerce the ::text-stored value to a positive int in resolve()/getMasked(). - ai-chat.service: flushAssistant writes metadata.maxContextTokens (>0); the completed turn passes resolved.chatContextWindow. Client: - ai-chat.types: maxContextTokens on the message-row metadata. - ai-chat-window: read maxContextTokens; render "current [/ max]"; drop the liveTurnTokens state/branch and the onLiveTurnTokens prop; new tooltip. - chat-thread: remove the live-turn-token throttle effect and plumbing. - count-stream-tokens: drop the now-dead liveTurnTokens()/types; keep estimateTokens. - settings: chatContextWindow on IAiSettings(+Update) + a NumberInput in the AI provider settings form. i18n: add the badge/settings keys (en, ru); remove the two now-unused keys. Tests: flushAssistant maxContextTokens, DTO validation, trim token tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
2.0 KiB
TypeScript
96 lines
2.0 KiB
TypeScript
import { IsIn, IsInt, IsOptional, IsString, Min } from 'class-validator';
|
|
import {
|
|
AI_DRIVERS,
|
|
AiDriver,
|
|
CHAT_API_STYLES,
|
|
ChatApiStyle,
|
|
STT_API_STYLES,
|
|
SttApiStyle,
|
|
} from '../ai.types';
|
|
|
|
/**
|
|
* Admin update payload for the workspace AI provider settings.
|
|
*
|
|
* `apiKey` / `embeddingApiKey` / `sttApiKey` are write-only (§8.2): provided →
|
|
* stored encrypted, '' → cleared, absent → left untouched. They are NEVER
|
|
* returned by any endpoint. The global ValidationPipe runs with
|
|
* `whitelist: true`, so unknown fields are stripped.
|
|
*/
|
|
export class UpdateAiSettingsDto {
|
|
@IsOptional()
|
|
@IsIn(AI_DRIVERS)
|
|
driver?: AiDriver;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
chatModel?: string;
|
|
|
|
// Max context window in tokens shown in the chat header badge. 0/empty =
|
|
// clear the limit (no denominator shown).
|
|
@IsOptional()
|
|
@IsInt()
|
|
@Min(0)
|
|
chatContextWindow?: number;
|
|
|
|
@IsOptional()
|
|
@IsIn(CHAT_API_STYLES)
|
|
chatApiStyle?: ChatApiStyle;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
embeddingModel?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
baseUrl?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
embeddingBaseUrl?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
systemPrompt?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
apiKey?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
embeddingApiKey?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
sttModel?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
sttBaseUrl?: string;
|
|
|
|
@IsOptional()
|
|
@IsIn(STT_API_STYLES)
|
|
sttApiStyle?: SttApiStyle;
|
|
|
|
// ISO-639-1 dictation language hint (e.g. 'en', 'ru'). Empty = auto-detect.
|
|
@IsOptional()
|
|
@IsString()
|
|
sttLanguage?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
sttApiKey?: string;
|
|
|
|
// Cheap model id for the anonymous public-share assistant; reuses the chat
|
|
// driver/baseUrl/apiKey. Empty → the assistant falls back to chatModel.
|
|
@IsOptional()
|
|
@IsString()
|
|
publicShareChatModel?: string;
|
|
|
|
// Agent-role id whose persona the anonymous public-share assistant adopts;
|
|
// empty/unset = built-in locked persona.
|
|
@IsOptional()
|
|
@IsString()
|
|
publicShareAssistantRoleId?: string;
|
|
}
|