because I got bored of customising my CV for every job
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(server): add user AI settings module with BYOK provider management

+725
+39
apps/server/prisma/migrations/20260207151745_add_user_ai_settings/migration.sql
··· 1 + -- CreateEnum 2 + CREATE TYPE "AiPreference" AS ENUM ('NO_AI', 'PLATFORM', 'BYOK'); 3 + 4 + -- CreateTable 5 + CREATE TABLE "user_ai_settings" ( 6 + "id" TEXT NOT NULL, 7 + "userId" TEXT NOT NULL, 8 + "aiPreference" "AiPreference" NOT NULL DEFAULT 'NO_AI', 9 + "activeProviderId" TEXT, 10 + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 + "updatedAt" TIMESTAMP(3) NOT NULL, 12 + 13 + CONSTRAINT "user_ai_settings_pkey" PRIMARY KEY ("id") 14 + ); 15 + 16 + -- CreateTable 17 + CREATE TABLE "user_ai_providers" ( 18 + "id" TEXT NOT NULL, 19 + "userId" TEXT NOT NULL, 20 + "label" TEXT NOT NULL, 21 + "providerType" TEXT NOT NULL, 22 + "encryptedApiKey" TEXT NOT NULL, 23 + "keyLastFour" TEXT NOT NULL, 24 + "model" TEXT, 25 + "baseUrl" TEXT, 26 + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 27 + "updatedAt" TIMESTAMP(3) NOT NULL, 28 + 29 + CONSTRAINT "user_ai_providers_pkey" PRIMARY KEY ("id") 30 + ); 31 + 32 + -- CreateIndex 33 + CREATE UNIQUE INDEX "user_ai_settings_userId_key" ON "user_ai_settings"("userId"); 34 + 35 + -- AddForeignKey 36 + ALTER TABLE "user_ai_settings" ADD CONSTRAINT "user_ai_settings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; 37 + 38 + -- AddForeignKey 39 + ALTER TABLE "user_ai_providers" ADD CONSTRAINT "user_ai_providers_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+35
apps/server/prisma/models/user-ai-settings.prisma
··· 1 + enum AiPreference { 2 + NO_AI 3 + PLATFORM 4 + BYOK 5 + } 6 + 7 + model UserAiSettings { 8 + id String @id @default(cuid()) 9 + userId String @unique 10 + aiPreference AiPreference @default(NO_AI) 11 + activeProviderId String? 12 + createdAt DateTime @default(now()) 13 + updatedAt DateTime @updatedAt 14 + 15 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 16 + 17 + @@map("user_ai_settings") 18 + } 19 + 20 + model UserAiProvider { 21 + id String @id @default(cuid()) 22 + userId String 23 + label String 24 + providerType String 25 + encryptedApiKey String 26 + keyLastFour String 27 + model String? 28 + baseUrl String? 29 + createdAt DateTime @default(now()) 30 + updatedAt DateTime @updatedAt 31 + 32 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 33 + 34 + @@map("user_ai_providers") 35 + }
+116
apps/server/src/modules/cv-parser/ai-provider-resolver.service.ts
··· 1 + import { randomUUID } from "node:crypto"; 2 + import { 3 + AI_PROVIDER, 4 + type AIProvider, 5 + AnthropicProvider, 6 + OpenAIProvider, 7 + } from "@cv/ai-provider"; 8 + import type { User } from "@cv/auth"; 9 + import { 10 + BadRequestException, 11 + ForbiddenException, 12 + Inject, 13 + Injectable, 14 + Optional, 15 + } from "@nestjs/common"; 16 + import { AiCallLogService } from "@/modules/admin/ai-call-log.service"; 17 + import { LoggingAIProvider } from "@/modules/admin/logging-ai-provider"; 18 + import { UserAiSettingsService } from "@/modules/user-settings/user-ai-settings.service"; 19 + 20 + const DEFAULT_BASE_URLS: Record<string, string> = { 21 + anthropic: "https://api.anthropic.com", 22 + openai: "https://api.openai.com", 23 + }; 24 + 25 + /** 26 + * Resolves the correct AIProvider for a given user based on their AI preference. 27 + */ 28 + @Injectable() 29 + export class AIProviderResolverService { 30 + constructor( 31 + @Inject(AI_PROVIDER) 32 + @Optional() 33 + private readonly globalProvider: AIProvider | undefined, 34 + private readonly userAiSettingsService: UserAiSettingsService, 35 + private readonly aiCallLogService: AiCallLogService, 36 + ) {} 37 + 38 + async resolveForUser(user: User): Promise<AIProvider> { 39 + try { 40 + return await this.resolveProvider(user); 41 + } catch (err) { 42 + this.aiCallLogService.record({ 43 + id: randomUUID(), 44 + timestamp: new Date().toISOString(), 45 + providerName: "resolution", 46 + durationMs: 0, 47 + status: "error", 48 + error: err instanceof Error ? err.message : String(err), 49 + userId: user.id, 50 + source: "resolution", 51 + }); 52 + throw err; 53 + } 54 + } 55 + 56 + private async resolveProvider(user: User): Promise<AIProvider> { 57 + const settings = await this.userAiSettingsService.getOrCreateSettings(user); 58 + 59 + if (settings.aiPreference === "NO_AI") { 60 + throw new ForbiddenException( 61 + "AI is disabled. Enable it in profile settings.", 62 + ); 63 + } 64 + 65 + if (settings.aiPreference === "PLATFORM") { 66 + if (!this.globalProvider) { 67 + throw new BadRequestException( 68 + "Platform AI is not available. Configure your own provider in profile settings.", 69 + ); 70 + } 71 + return new LoggingAIProvider(this.globalProvider, this.aiCallLogService, { 72 + userId: user.id, 73 + source: "platform", 74 + }); 75 + } 76 + 77 + const resolved = 78 + await this.userAiSettingsService.resolveActiveProvider(user); 79 + 80 + return this.instantiateProvider(resolved, user.id); 81 + } 82 + 83 + private instantiateProvider( 84 + config: { 85 + providerType: string; 86 + decryptedApiKey: string; 87 + model: string | null; 88 + baseUrl: string | null; 89 + }, 90 + userId: string, 91 + ): AIProvider { 92 + const baseUrl = 93 + config.baseUrl || DEFAULT_BASE_URLS[config.providerType] || ""; 94 + 95 + const providerConfig = { 96 + baseUrl, 97 + apiKey: config.decryptedApiKey, 98 + ...(config.model != null && { model: config.model }), 99 + }; 100 + 101 + const provider = (() => { 102 + if (config.providerType === "anthropic") 103 + return new AnthropicProvider(providerConfig); 104 + if (config.providerType === "openai") 105 + return new OpenAIProvider(providerConfig); 106 + throw new BadRequestException( 107 + `Unsupported provider type: ${config.providerType}`, 108 + ); 109 + })(); 110 + 111 + return new LoggingAIProvider(provider, this.aiCallLogService, { 112 + userId, 113 + source: "byok", 114 + }); 115 + } 116 + }
+22
apps/server/src/modules/user-settings/user-ai-settings.module.ts
··· 1 + import { AIModule, type AIProviderType } from "@cv/ai-provider"; 2 + import { DatabaseModule } from "@cv/system"; 3 + import { Module } from "@nestjs/common"; 4 + import { AiPreferenceOnboardingStep } from "./onboarding/ai-preference.step"; 5 + import { UserAiSettingsResolver } from "./user-ai-settings.resolver"; 6 + import { UserAiSettingsService } from "./user-ai-settings.service"; 7 + 8 + @Module({ 9 + imports: [ 10 + DatabaseModule, 11 + AIModule.forRoot({ 12 + type: (process.env["AI_PROVIDER"] as AIProviderType) || "llama-cpp", 13 + }), 14 + ], 15 + providers: [ 16 + UserAiSettingsService, 17 + UserAiSettingsResolver, 18 + AiPreferenceOnboardingStep, 19 + ], 20 + exports: [UserAiSettingsService], 21 + }) 22 + export class UserAiSettingsModule {}
+209
apps/server/src/modules/user-settings/user-ai-settings.resolver.ts
··· 1 + import type { AIProvider } from "@cv/ai-provider"; 2 + import { AI_PROVIDER, registeredProviderTypes } from "@cv/ai-provider"; 3 + import type { User as DomainUser } from "@cv/auth"; 4 + import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; 5 + import { Inject, Optional, UseGuards } from "@nestjs/common"; 6 + import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; 7 + import { AiPreference } from "@prisma/client"; 8 + import { CurrentUser } from "@/modules/current-user/current-user.decorator"; 9 + import { UserAiSettingsService } from "./user-ai-settings.service"; 10 + import { 11 + AiSettings, 12 + PlatformAiStatus, 13 + PlatformCapabilities, 14 + RemoveAiProviderResult, 15 + UserAiProviderType, 16 + } from "./user-ai-settings.type"; 17 + 18 + @Resolver() 19 + @UseGuards(JwtAuthGuard, VerifiedScopeGuard) 20 + export class UserAiSettingsResolver { 21 + constructor( 22 + private readonly settingsService: UserAiSettingsService, 23 + @Inject(AI_PROVIDER) 24 + @Optional() 25 + private readonly globalProvider?: AIProvider, 26 + ) {} 27 + 28 + @Query(() => AiSettings) 29 + async myAiSettings(@CurrentUser() user: DomainUser): Promise<AiSettings> { 30 + const settings = await this.settingsService.getOrCreateSettings(user); 31 + return { 32 + id: settings.id, 33 + aiPreference: settings.aiPreference, 34 + activeProviderId: settings.activeProviderId, 35 + }; 36 + } 37 + 38 + @Query(() => [UserAiProviderType]) 39 + async myAiProviders( 40 + @CurrentUser() user: DomainUser, 41 + ): Promise<UserAiProviderType[]> { 42 + const providers = await this.settingsService.listProviders(user); 43 + return providers.map((p) => ({ 44 + id: p.id, 45 + label: p.label, 46 + providerType: p.providerType, 47 + model: p.model, 48 + baseUrl: p.baseUrl, 49 + maskedApiKey: `...${p.keyLastFour}`, 50 + createdAt: p.createdAt, 51 + })); 52 + } 53 + 54 + @Query(() => PlatformAiStatus) 55 + async platformAiAvailable(): Promise<PlatformAiStatus> { 56 + if (!this.globalProvider) { 57 + return { available: false }; 58 + } 59 + 60 + try { 61 + const healthy = await this.globalProvider.isHealthy(); 62 + return { available: healthy }; 63 + } catch { 64 + return { available: false }; 65 + } 66 + } 67 + 68 + @Query(() => PlatformCapabilities) 69 + async platformCapabilities(): Promise<PlatformCapabilities> { 70 + const aiParsing = this.globalProvider 71 + ? await this.globalProvider.isHealthy().catch(() => false) 72 + : false; 73 + 74 + return { 75 + availableProviderTypes: registeredProviderTypes(), 76 + aiParsing, 77 + storyMode: false, 78 + }; 79 + } 80 + 81 + @Mutation(() => AiSettings) 82 + async updateAiPreference( 83 + @CurrentUser() user: DomainUser, 84 + @Args("preference", { type: () => AiPreference }) preference: AiPreference, 85 + ): Promise<AiSettings> { 86 + const settings = await this.settingsService.updatePreference( 87 + user, 88 + preference, 89 + ); 90 + return { 91 + id: settings.id, 92 + aiPreference: settings.aiPreference, 93 + activeProviderId: settings.activeProviderId, 94 + }; 95 + } 96 + 97 + @Mutation(() => UserAiProviderType) 98 + async addAiProvider( 99 + @CurrentUser() user: DomainUser, 100 + @Args("label") label: string, 101 + @Args("providerType") providerType: string, 102 + @Args("apiKey") apiKey: string, 103 + @Args("model", { nullable: true }) model?: string, 104 + @Args("baseUrl", { nullable: true }) baseUrl?: string, 105 + @Args("setActive", { nullable: true, defaultValue: false }) 106 + setActive?: boolean, 107 + ): Promise<UserAiProviderType> { 108 + const provider = await this.settingsService.addProvider(user, { 109 + label, 110 + providerType, 111 + apiKey, 112 + setActive: setActive ?? false, 113 + ...(model !== undefined && { model }), 114 + ...(baseUrl !== undefined && { baseUrl }), 115 + }); 116 + return { 117 + id: provider.id, 118 + label: provider.label, 119 + providerType: provider.providerType, 120 + model: provider.model, 121 + baseUrl: provider.baseUrl, 122 + maskedApiKey: `...${provider.keyLastFour}`, 123 + createdAt: provider.createdAt, 124 + }; 125 + } 126 + 127 + @Mutation(() => RemoveAiProviderResult) 128 + async removeAiProvider( 129 + @CurrentUser() user: DomainUser, 130 + @Args("providerId") providerId: string, 131 + ): Promise<RemoveAiProviderResult> { 132 + const { wasActive } = await this.settingsService.removeProvider( 133 + user, 134 + providerId, 135 + ); 136 + const settings = await this.settingsService.getOrCreateSettings(user); 137 + return { 138 + success: true, 139 + wasActive, 140 + newPreference: settings.aiPreference, 141 + }; 142 + } 143 + 144 + @Mutation(() => UserAiProviderType) 145 + async replaceAiProviderKey( 146 + @CurrentUser() user: DomainUser, 147 + @Args("providerId") providerId: string, 148 + @Args("newApiKey") newApiKey: string, 149 + ): Promise<UserAiProviderType> { 150 + const provider = await this.settingsService.replaceProviderKey( 151 + user, 152 + providerId, 153 + newApiKey, 154 + ); 155 + return { 156 + id: provider.id, 157 + label: provider.label, 158 + providerType: provider.providerType, 159 + model: provider.model, 160 + baseUrl: provider.baseUrl, 161 + maskedApiKey: `...${provider.keyLastFour}`, 162 + createdAt: provider.createdAt, 163 + }; 164 + } 165 + 166 + @Mutation(() => UserAiProviderType) 167 + async updateAiProvider( 168 + @CurrentUser() user: DomainUser, 169 + @Args("providerId") providerId: string, 170 + @Args("label", { nullable: true }) label?: string, 171 + @Args("model", { nullable: true }) model?: string, 172 + @Args("baseUrl", { nullable: true }) baseUrl?: string, 173 + ): Promise<UserAiProviderType> { 174 + const provider = await this.settingsService.updateProviderMeta( 175 + user, 176 + providerId, 177 + { 178 + ...(label !== undefined && { label }), 179 + ...(model !== undefined && { model }), 180 + ...(baseUrl !== undefined && { baseUrl }), 181 + }, 182 + ); 183 + return { 184 + id: provider.id, 185 + label: provider.label, 186 + providerType: provider.providerType, 187 + model: provider.model, 188 + baseUrl: provider.baseUrl, 189 + maskedApiKey: `...${provider.keyLastFour}`, 190 + createdAt: provider.createdAt, 191 + }; 192 + } 193 + 194 + @Mutation(() => AiSettings) 195 + async setActiveAiProvider( 196 + @CurrentUser() user: DomainUser, 197 + @Args("providerId") providerId: string, 198 + ): Promise<AiSettings> { 199 + const settings = await this.settingsService.setActiveProvider( 200 + user, 201 + providerId, 202 + ); 203 + return { 204 + id: settings.id, 205 + aiPreference: settings.aiPreference, 206 + activeProviderId: settings.activeProviderId, 207 + }; 208 + } 209 + }
+231
apps/server/src/modules/user-settings/user-ai-settings.service.ts
··· 1 + import type { User } from "@cv/auth"; 2 + import { TokenEncryptionService } from "@cv/auth"; 3 + import { PrismaService } from "@cv/system"; 4 + import { 5 + BadRequestException, 6 + Injectable, 7 + NotFoundException, 8 + } from "@nestjs/common"; 9 + import type { AiPreference } from "@prisma/client"; 10 + 11 + @Injectable() 12 + export class UserAiSettingsService { 13 + constructor( 14 + private readonly prisma: PrismaService, 15 + private readonly encryption: TokenEncryptionService, 16 + ) {} 17 + 18 + async getSettings(user: User) { 19 + return this.prisma.userAiSettings.findUnique({ 20 + where: { userId: user.id }, 21 + }); 22 + } 23 + 24 + async getOrCreateSettings(user: User) { 25 + return this.prisma.userAiSettings.upsert({ 26 + where: { userId: user.id }, 27 + create: { userId: user.id, aiPreference: "NO_AI" }, 28 + update: {}, 29 + }); 30 + } 31 + 32 + async updatePreference(user: User, preference: AiPreference) { 33 + const settings = await this.getOrCreateSettings(user); 34 + 35 + const update: { aiPreference: AiPreference; activeProviderId?: null } = { 36 + aiPreference: preference, 37 + }; 38 + 39 + if (preference !== "BYOK" && settings.activeProviderId) { 40 + update.activeProviderId = null; 41 + } 42 + 43 + return this.prisma.userAiSettings.update({ 44 + where: { userId: user.id }, 45 + data: update, 46 + }); 47 + } 48 + 49 + async setActiveProvider(user: User, providerId: string) { 50 + const provider = await this.prisma.userAiProvider.findFirst({ 51 + where: { id: providerId, userId: user.id }, 52 + }); 53 + 54 + if (!provider) { 55 + throw new NotFoundException( 56 + "Provider not found or does not belong to you", 57 + ); 58 + } 59 + 60 + return this.prisma.userAiSettings.upsert({ 61 + where: { userId: user.id }, 62 + create: { 63 + userId: user.id, 64 + aiPreference: "BYOK", 65 + activeProviderId: providerId, 66 + }, 67 + update: { 68 + aiPreference: "BYOK", 69 + activeProviderId: providerId, 70 + }, 71 + }); 72 + } 73 + 74 + async listProviders(user: User) { 75 + return this.prisma.userAiProvider.findMany({ 76 + where: { userId: user.id }, 77 + orderBy: { createdAt: "desc" }, 78 + }); 79 + } 80 + 81 + async addProvider( 82 + user: User, 83 + input: { 84 + label: string; 85 + providerType: string; 86 + apiKey: string; 87 + model?: string; 88 + baseUrl?: string; 89 + setActive?: boolean; 90 + }, 91 + ) { 92 + const keyLastFour = input.apiKey.slice(-4); 93 + const encryptedApiKey = this.encryption.encrypt(input.apiKey); 94 + 95 + const provider = await this.prisma.userAiProvider.create({ 96 + data: { 97 + userId: user.id, 98 + label: input.label, 99 + providerType: input.providerType, 100 + encryptedApiKey, 101 + keyLastFour, 102 + model: input.model ?? null, 103 + baseUrl: input.baseUrl ?? null, 104 + }, 105 + }); 106 + 107 + const settings = await this.getOrCreateSettings(user); 108 + const shouldActivate = input.setActive || !settings.activeProviderId; 109 + 110 + if (shouldActivate) { 111 + await this.prisma.userAiSettings.update({ 112 + where: { userId: user.id }, 113 + data: { activeProviderId: provider.id, aiPreference: "BYOK" }, 114 + }); 115 + } 116 + 117 + return provider; 118 + } 119 + 120 + async removeProvider(user: User, providerId: string) { 121 + const provider = await this.prisma.userAiProvider.findFirst({ 122 + where: { id: providerId, userId: user.id }, 123 + }); 124 + 125 + if (!provider) { 126 + throw new NotFoundException( 127 + "Provider not found or does not belong to you", 128 + ); 129 + } 130 + 131 + await this.prisma.userAiProvider.delete({ where: { id: providerId } }); 132 + 133 + const settings = await this.prisma.userAiSettings.findUnique({ 134 + where: { userId: user.id }, 135 + }); 136 + 137 + const wasActive = settings?.activeProviderId === providerId; 138 + 139 + if (wasActive) { 140 + const remaining = await this.prisma.userAiProvider.findFirst({ 141 + where: { userId: user.id }, 142 + orderBy: { createdAt: "desc" }, 143 + }); 144 + 145 + await this.prisma.userAiSettings.update({ 146 + where: { userId: user.id }, 147 + data: remaining 148 + ? { activeProviderId: remaining.id } 149 + : { activeProviderId: null, aiPreference: "PLATFORM" }, 150 + }); 151 + } 152 + 153 + return { wasActive }; 154 + } 155 + 156 + async replaceProviderKey(user: User, providerId: string, newApiKey: string) { 157 + const provider = await this.prisma.userAiProvider.findFirst({ 158 + where: { id: providerId, userId: user.id }, 159 + }); 160 + 161 + if (!provider) { 162 + throw new NotFoundException( 163 + "Provider not found or does not belong to you", 164 + ); 165 + } 166 + 167 + return this.prisma.userAiProvider.update({ 168 + where: { id: providerId }, 169 + data: { 170 + encryptedApiKey: this.encryption.encrypt(newApiKey), 171 + keyLastFour: newApiKey.slice(-4), 172 + }, 173 + }); 174 + } 175 + 176 + async updateProviderMeta( 177 + user: User, 178 + providerId: string, 179 + input: { label?: string; model?: string; baseUrl?: string }, 180 + ) { 181 + const provider = await this.prisma.userAiProvider.findFirst({ 182 + where: { id: providerId, userId: user.id }, 183 + }); 184 + 185 + if (!provider) { 186 + throw new NotFoundException( 187 + "Provider not found or does not belong to you", 188 + ); 189 + } 190 + 191 + return this.prisma.userAiProvider.update({ 192 + where: { id: providerId }, 193 + data: { 194 + ...(input.label !== undefined && { label: input.label }), 195 + ...(input.model !== undefined && { model: input.model }), 196 + ...(input.baseUrl !== undefined && { baseUrl: input.baseUrl }), 197 + }, 198 + }); 199 + } 200 + 201 + /** 202 + * Resolve the active provider's decrypted config for internal use. 203 + * Not exposed via GraphQL. 204 + */ 205 + async resolveActiveProvider(user: User) { 206 + const settings = await this.getOrCreateSettings(user); 207 + 208 + if (!settings.activeProviderId) { 209 + throw new BadRequestException( 210 + "No active AI provider. Configure one in profile settings.", 211 + ); 212 + } 213 + 214 + const provider = await this.prisma.userAiProvider.findFirst({ 215 + where: { id: settings.activeProviderId, userId: user.id }, 216 + }); 217 + 218 + if (!provider) { 219 + throw new BadRequestException( 220 + "Active AI provider no longer exists. Configure a new one in profile settings.", 221 + ); 222 + } 223 + 224 + return { 225 + providerType: provider.providerType, 226 + decryptedApiKey: this.encryption.decrypt(provider.encryptedApiKey), 227 + model: provider.model, 228 + baseUrl: provider.baseUrl, 229 + }; 230 + } 231 + }
+73
apps/server/src/modules/user-settings/user-ai-settings.type.ts
··· 1 + import { Field, ObjectType, registerEnumType } from "@nestjs/graphql"; 2 + import { AiPreference } from "@prisma/client"; 3 + 4 + registerEnumType(AiPreference, { 5 + name: "AiPreference", 6 + description: "User AI preference mode", 7 + }); 8 + 9 + @ObjectType() 10 + export class AiSettings { 11 + @Field() 12 + id!: string; 13 + 14 + @Field(() => AiPreference) 15 + aiPreference!: AiPreference; 16 + 17 + @Field(() => String, { nullable: true }) 18 + activeProviderId!: string | null; 19 + } 20 + 21 + @ObjectType() 22 + export class UserAiProviderType { 23 + @Field() 24 + id!: string; 25 + 26 + @Field() 27 + label!: string; 28 + 29 + @Field() 30 + providerType!: string; 31 + 32 + @Field(() => String, { nullable: true }) 33 + model!: string | null; 34 + 35 + @Field(() => String, { nullable: true }) 36 + baseUrl!: string | null; 37 + 38 + @Field() 39 + maskedApiKey!: string; 40 + 41 + @Field() 42 + createdAt!: Date; 43 + } 44 + 45 + @ObjectType() 46 + export class RemoveAiProviderResult { 47 + @Field() 48 + success!: boolean; 49 + 50 + @Field() 51 + wasActive!: boolean; 52 + 53 + @Field(() => AiPreference) 54 + newPreference!: AiPreference; 55 + } 56 + 57 + @ObjectType() 58 + export class PlatformAiStatus { 59 + @Field() 60 + available!: boolean; 61 + } 62 + 63 + @ObjectType() 64 + export class PlatformCapabilities { 65 + @Field(() => [String]) 66 + availableProviderTypes!: string[]; 67 + 68 + @Field() 69 + aiParsing!: boolean; 70 + 71 + @Field() 72 + storyMode!: boolean; 73 + }