aegida-console / lib / chat / validation.ts
validation.ts
Raw
import {
  isModelId,
  type ChatRequest,
  type MessageRole,
  type ModelId,
} from "@/lib/chat/types";
import {
  MAX_CHAT_MESSAGE_CHARACTERS,
  MAX_CHAT_REQUEST_CHARACTERS,
  MAX_CHAT_REQUEST_MESSAGES,
} from "@/lib/chat/request-limits";

const roles = new Set<MessageRole>(["user", "assistant"]);

export class ChatValidationError extends Error {
  readonly status = 400;

  constructor() {
    super("Некорректный запрос");
    this.name = "ChatValidationError";
  }
}

export function validateChatRequest(value: unknown): ChatRequest {
  if (!isRecord(value) || !isModelId(value.model) || !Array.isArray(value.messages)) {
    throw new ChatValidationError();
  }

  if (
    value.messages.length === 0 ||
    value.messages.length > MAX_CHAT_REQUEST_MESSAGES
  ) {
    throw new ChatValidationError();
  }

  let totalCharacters = 0;
  const messages = Array.from(value.messages, (message) => {
    if (
      !isRecord(message) ||
      !isMessageRole(message.role) ||
      typeof message.content !== "string" ||
      message.content.trim().length === 0 ||
      message.content.length > MAX_CHAT_MESSAGE_CHARACTERS
    ) {
      throw new ChatValidationError();
    }

    totalCharacters += message.content.length;
    if (totalCharacters > MAX_CHAT_REQUEST_CHARACTERS) {
      throw new ChatValidationError();
    }

    return { role: message.role, content: message.content };
  });

  return { model: value.model, messages };
}

export type PersistentChatRequest = {
  conversationId?: string;
  model: ModelId;
  content?: string;
  retry?: boolean;
  attachmentIds?: string[];
};

export function validatePersistentChatRequest(value: unknown): PersistentChatRequest {
  if (!isRecord(value) || !isModelId(value.model)) {
    throw new ChatValidationError();
  }
  const conversationId = value.conversationId;
  const content = value.content;
  const retry = value.retry;
  const attachmentIds = value.attachmentIds;

  if (
    (conversationId !== undefined && (typeof conversationId !== "string" || !conversationId)) ||
    (content !== undefined &&
      (typeof content !== "string" ||
        !content.trim() ||
        content.length > MAX_CHAT_MESSAGE_CHARACTERS)) ||
    (retry !== undefined && typeof retry !== "boolean") ||
    (attachmentIds !== undefined && (!Array.isArray(attachmentIds) || attachmentIds.length > 10 || attachmentIds.some((id) => typeof id !== "string" || !id) || new Set(attachmentIds).size !== attachmentIds.length)) ||
    (retry === true && content !== undefined) ||
    (retry !== true && content === undefined && (!attachmentIds || attachmentIds.length === 0))
  ) {
    throw new ChatValidationError();
  }

  return { conversationId, model: value.model, content, retry, attachmentIds };
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function isMessageRole(value: unknown): value is MessageRole {
  return typeof value === "string" && roles.has(value as MessageRole);
}