import * as z from 'zod'; import { renamePrompt } from '~~/prompts'; import { generateText } from 'ai'; import { GatewayFetchError, getProviderDetails, type ModelGateway } from '~~/server/utils/ai-provider'; import { addPendingRename } from '~~/server/utils/renames'; import { userEvents } from '~~/server/utils/events'; import { db } from '~~/server/lib/db'; import { type Model } from '~/composables/useModels'; import { topics } from '~~/drizzle/schema'; import { eq } from 'drizzle-orm'; export default defineEventHandler(async (event) => { await protectRoute(event); const userId = event.context.user!.id; const topicId = getRouterParam(event, 'topicId')!; const result = await readValidatedBody(event, (body) => z .object({ modelId: z.string(), providerApiKey: z.string().optional(), }) .safeParse(body), ); if (!result.success) { throw createError({ statusCode: 400, message: result.error.issues[0]!.message, data: { code: 'INVALID_BODY', ok: false, } }); } const firstMessage = await db.query.messages.findFirst({ where: { topicId, userId, }, orderBy: { createdAt: 'asc', }, with: { parts: true, }, }); if (firstMessage === undefined) { throw createError({ statusCode: 400, statusMessage: 'Invalid topic', data: { code: 'INVALID_TOPIC', ok: false, } }); } if (firstMessage.role !== 'user') { throw createError({ statusCode: 400, statusMessage: 'Unimplemented', data: { code: 'INVALID_TOPIC', ok: false, } }); } const { modelId, providerApiKey } = result.data; const topic = await db.query.topics.findFirst({ where: { id: topicId, userId, }, }); if (topic === undefined || topic.userId !== userId) { throw createError({ statusCode: 400, statusMessage: 'Invalid topic', data: { code: 'INVALID_TOPIC', ok: false, } }); } if (topic.renaming) { throw createError({ statusCode: 400, statusMessage: 'Already renaming', data: { code: 'ALREADY_RENAMING', ok: false, } }); } const model = await db.query.models.findFirst({ where: { id: modelId, userId, }, with: { provider: true, }, }); if (model === undefined || model.userId !== userId) { throw createError({ statusCode: 400, statusMessage: 'Invalid model', data: { code: 'INVALID_MODEL', ok: false, } }); } const providerDetails = await getProviderDetails(model.provider, providerApiKey, model); if (!providerDetails.ok) { switch (providerDetails.error) { case GatewayFetchError.NoProviderApiKey: { setResponseStatus(event, 400, "No provider API key"); throw createError({ statusCode: 400, statusMessage: `${model.provider.type} provider requires an API key`, data: { code: 'NO_PROVIDER_API_KEY', ok: false, } }); } case GatewayFetchError.NoProviderBaseUrl: { throw createError({ statusCode: 400, statusMessage: 'Invalid provider URL', data: { code: 'BAD_PROVIDER_URL', ok: false, } }); } } } const { gateway } = providerDetails.data; if (gateway === null) { throw createError({ statusCode: 500, statusMessage: 'Invalid gateway', data: { code: 'INVALID_GATEWAY', ok: false, } }); } const abortController = addPendingRename(topicId); event.waitUntil(autoRename(topicId, abortController, { gateway: gateway.gateway, model }, gateway.textTransformer, firstMessage.content!, userId)); return { ok: true, }; }); const autoRename = async ( topicId: string, abortController: AbortController, model: { gateway: ModelGateway, model: Model, }, textTransformer: ((text: string) => string) | ((text: string) => string)[] | undefined, prompt: string, userId: string, ) => { try { await db.update(topics).set({ renaming: true, }).where(eq(topics.id, topicId)); userEvents.emit(model.model.userId, 'topics', { op: 'update', payload: { topicId, renaming: true, }, }); const response = await generateText({ model: model.gateway(model.model.externalId), providerOptions: { openrouter: { user: userId, } }, system: renamePrompt, prompt, timeout: 90 * 1000, abortSignal: abortController.signal, }) let text = response.text; if (textTransformer !== undefined) { if (Array.isArray(textTransformer)) { for (const transformer of textTransformer) { text = transformer(text); } } else { text = textTransformer(text); } } await db.update(topics).set({ renaming: false, name: text, }).where(eq(topics.id, topicId)); userEvents.emit(model.model.userId, 'topics', { op: 'update', payload: { topicId, name: text, renaming: false, }, }); topicEvents.emit(topicId, { type: 'topic_updated', payload: { topicId, name: text, renaming: false, }, }); } catch (error) { console.error('Failed to auto-rename:', error); await db.update(topics).set({ renaming: false, }).where(eq(topics.id, topicId)); userEvents.emit(model.model.userId, 'topics', { op: 'update', payload: { topicId, renaming: false, }, }); topicEvents.emit(topicId, { type: 'topic_updated', payload: { topicId, renaming: false, }, }); } finally { completeRename(topicId); } }