Implement per-workspace LLM provider and personality configuration with proper hierarchy (workspace > user > system fallback). Schema: - Add WorkspaceLlmSettings model with provider/personality FKs - One-to-one relation with Workspace - JSON settings field for extensibility Service: - getSettings: Retrieves/creates workspace settings - updateSettings: Updates with null value support - getEffectiveLlmProvider: Hierarchy-based provider selection - getEffectivePersonality: Hierarchy-based personality selection Endpoints: - GET /workspaces/:id/settings/llm - Get settings - PATCH /workspaces/:id/settings/llm - Update settings - GET /workspaces/:id/settings/llm/effective-provider - GET /workspaces/:id/settings/llm/effective-personality Configuration hierarchy: 1. Workspace-configured provider/personality 2. User-specific provider (for providers) 3. System default fallback Tests: 34 passing with 100% coverage Fixes #133 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
188 lines
4.9 KiB
TypeScript
188 lines
4.9 KiB
TypeScript
import { Injectable } from "@nestjs/common";
|
|
import { Prisma } from "@prisma/client";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
import type { WorkspaceLlmSettings, LlmProviderInstance, Personality } from "@prisma/client";
|
|
import type { UpdateWorkspaceSettingsDto } from "./dto";
|
|
|
|
/**
|
|
* Service for managing workspace LLM settings
|
|
* Handles configuration hierarchy: workspace > user > system
|
|
*/
|
|
@Injectable()
|
|
export class WorkspaceSettingsService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
/**
|
|
* Get settings for a workspace (creates default if not exists)
|
|
*
|
|
* @param workspaceId - Workspace ID
|
|
* @returns Workspace LLM settings
|
|
*/
|
|
async getSettings(workspaceId: string): Promise<WorkspaceLlmSettings> {
|
|
let settings = await this.prisma.workspaceLlmSettings.findUnique({
|
|
where: { workspaceId },
|
|
});
|
|
|
|
// Create default settings if they don't exist
|
|
settings ??= await this.prisma.workspaceLlmSettings.create({
|
|
data: {
|
|
workspaceId,
|
|
settings: {} as unknown as Prisma.InputJsonValue,
|
|
},
|
|
});
|
|
|
|
return settings;
|
|
}
|
|
|
|
/**
|
|
* Update workspace LLM settings
|
|
*
|
|
* @param workspaceId - Workspace ID
|
|
* @param dto - Update data
|
|
* @returns Updated settings
|
|
*/
|
|
async updateSettings(
|
|
workspaceId: string,
|
|
dto: UpdateWorkspaceSettingsDto
|
|
): Promise<WorkspaceLlmSettings> {
|
|
const data: Prisma.WorkspaceLlmSettingsUncheckedUpdateInput = {};
|
|
|
|
if (dto.defaultLlmProviderId !== undefined) {
|
|
data.defaultLlmProviderId = dto.defaultLlmProviderId;
|
|
}
|
|
|
|
if (dto.defaultPersonalityId !== undefined) {
|
|
data.defaultPersonalityId = dto.defaultPersonalityId;
|
|
}
|
|
|
|
if (dto.settings !== undefined) {
|
|
data.settings = dto.settings as unknown as Prisma.InputJsonValue;
|
|
}
|
|
|
|
const settings = await this.prisma.workspaceLlmSettings.update({
|
|
where: { workspaceId },
|
|
data,
|
|
});
|
|
|
|
return settings;
|
|
}
|
|
|
|
/**
|
|
* Get effective LLM provider for a workspace
|
|
* Priority: workspace > user > system default
|
|
*
|
|
* @param workspaceId - Workspace ID
|
|
* @param userId - Optional user ID for user-level provider
|
|
* @returns Effective LLM provider instance
|
|
* @throws {Error} If no provider available
|
|
*/
|
|
async getEffectiveLlmProvider(
|
|
workspaceId: string,
|
|
userId?: string
|
|
): Promise<LlmProviderInstance> {
|
|
// Get workspace settings
|
|
const settings = await this.prisma.workspaceLlmSettings.findUnique({
|
|
where: { workspaceId },
|
|
});
|
|
|
|
// 1. Check workspace-level provider
|
|
if (settings?.defaultLlmProviderId) {
|
|
const provider = await this.prisma.llmProviderInstance.findUnique({
|
|
where: { id: settings.defaultLlmProviderId },
|
|
});
|
|
|
|
if (!provider) {
|
|
throw new Error(`LLM provider ${settings.defaultLlmProviderId} not found`);
|
|
}
|
|
|
|
return provider;
|
|
}
|
|
|
|
// 2. Check user-level provider
|
|
if (userId) {
|
|
const userProvider = await this.prisma.llmProviderInstance.findFirst({
|
|
where: {
|
|
userId,
|
|
isEnabled: true,
|
|
},
|
|
});
|
|
|
|
if (userProvider) {
|
|
return userProvider;
|
|
}
|
|
}
|
|
|
|
// 3. Fall back to system default
|
|
const systemDefault = await this.prisma.llmProviderInstance.findFirst({
|
|
where: {
|
|
userId: null,
|
|
isDefault: true,
|
|
isEnabled: true,
|
|
},
|
|
});
|
|
|
|
if (!systemDefault) {
|
|
throw new Error(`No LLM provider available for workspace ${workspaceId}`);
|
|
}
|
|
|
|
return systemDefault;
|
|
}
|
|
|
|
/**
|
|
* Get effective personality for a workspace
|
|
* Priority: workspace default > workspace enabled > any enabled
|
|
*
|
|
* @param workspaceId - Workspace ID
|
|
* @returns Effective personality
|
|
* @throws {Error} If no personality available
|
|
*/
|
|
async getEffectivePersonality(workspaceId: string): Promise<Personality> {
|
|
// Get workspace settings
|
|
const settings = await this.prisma.workspaceLlmSettings.findUnique({
|
|
where: { workspaceId },
|
|
});
|
|
|
|
// 1. Check workspace-configured personality
|
|
if (settings?.defaultPersonalityId) {
|
|
const personality = await this.prisma.personality.findUnique({
|
|
where: {
|
|
id: settings.defaultPersonalityId,
|
|
},
|
|
});
|
|
|
|
if (!personality) {
|
|
throw new Error(`Personality ${settings.defaultPersonalityId} not found`);
|
|
}
|
|
|
|
return personality;
|
|
}
|
|
|
|
// 2. Fall back to default personality in workspace
|
|
const defaultPersonality = await this.prisma.personality.findFirst({
|
|
where: {
|
|
workspaceId,
|
|
isDefault: true,
|
|
isEnabled: true,
|
|
},
|
|
});
|
|
|
|
if (defaultPersonality) {
|
|
return defaultPersonality;
|
|
}
|
|
|
|
// 3. Fall back to any enabled personality
|
|
const anyPersonality = await this.prisma.personality.findFirst({
|
|
where: {
|
|
workspaceId,
|
|
isEnabled: true,
|
|
},
|
|
});
|
|
|
|
if (!anyPersonality) {
|
|
throw new Error(`No personality available for workspace ${workspaceId}`);
|
|
}
|
|
|
|
return anyPersonality;
|
|
}
|
|
}
|