Files
gitmost/apps/server/src/integrations/ai/dto/update-ai-settings.dto.ts
claude code agent 227 59190148db feat(ai-chat): explicit chatApiStyle selector to surface reasoning (#175)
Rebuilt on develop (after #176) and reworked per review: instead of inferring the
provider from baseUrl (`if (baseUrl)`), the admin picks the chat provider
EXPLICITLY via a new `chatApiStyle` ('openai-compatible' | 'openai'), mirroring
the existing sttApiStyle. A custom baseURL can front real OpenAI too, so the
heuristic was fragile.

Why reasoning was missing: glm-5.2 (and DeepSeek etc.) stream their thinking as
`reasoning_content`, but the official @ai-sdk/openai provider does not map that
field. 'openai-compatible' uses @ai-sdk/openai-compatible, which does — so
reasoning parts now stream (verified live: reasoning-start/delta/end appear, and
disappear when set to 'openai').

- Default (unset) = 'openai-compatible', so existing openai+baseUrl workspaces
  surface reasoning with no admin action. No DB migration (field lives in the
  settings.ai.provider JSON blob).
- includeUsage: true on the openai-compatible model — without it the provider
  omits streamed usage, zeroing the live token counter / reasoning-token
  metadata. The official provider always sent it; this keeps parity. (Confirmed
  live: usage.totalTokens present.)
- openai-compatible has no default endpoint, so with no baseURL (real OpenAI, or
  a role's cross-driver override that cleared it) it falls back to the official
  provider.

Plumbing: ai.types (ChatApiStyle / CHAT_API_STYLES + AiProviderSettings /
MaskedAiSettings), update DTO (@IsIn), ai-settings.service (resolve / getMasked /
update allowlist), workspace.repo updateAiProviderSettings ALLOWED (the second,
SQL-level allowlist the review missed — without it the field never persisted),
ai.service selector. Client: ai-settings-service types + a Protocol <Select> in
the chat section + i18n (en/ru). Scope is chat-only (embeddings don't stream
reasoning; STT already has sttApiStyle).

Tests: ai.service.spec — 4 cases (openai-compatible+baseURL, openai+baseURL,
default-unset, openai-compatible-without-baseURL fallback). Verified on the stand:
default streams reasoning + usage; 'openai' drops reasoning; the setting
round-trips. server + client tsc clean; 36 ai/settings specs green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:58:15 +03:00

89 lines
1.8 KiB
TypeScript

import { IsIn, IsOptional, IsString } 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;
@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;
}