Nitro hooks intercept and modify requests at runtime. Use them for query rewriting, result filtering, chat persistence, or custom RAG logic.
'ai-search:search:query'Type: async (ctx: SearchQueryContext, result: SearchQueryResult) => SearchQueryResult | void
Fires before vector search. Transform queries, add synonyms, or implement custom preprocessing.
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('ai-search:search:query', async (ctx, result) => {
let query = ctx.query
if (query.includes('SSR'))
query = query.replace('SSR', 'server-side rendering SSR')
return { query: `${query} Nuxt Vue` }
})
})
Context:
| Property | Type | Description |
|---|---|---|
query | string | Original query string |
event | H3Event | Request event |
limit | number | Max results requested |
model | LanguageModel | undefined | LLM model instance |
config | RuntimeConfig | Runtime config |
Return: { query: string } to override, or void to keep original.
'ai-search:search:results'Type: async (ctx: SearchResultsContext, result: SearchResultsResult) => SearchResultsResult | void
Fires after vector search. Filter results, add metadata, or re-rank.
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('ai-search:search:results', async (ctx, result) => {
const filtered = ctx.results.filter(r => !r.route.startsWith('/drafts/'))
return { results: filtered }
})
})
Context:
| Property | Type | Description |
|---|---|---|
query | string | Query (possibly transformed) |
results | SearchResult[] | Vector search results |
event | H3Event | Request event |
config | RuntimeConfig | Runtime config |
Return: { results: SearchResult[] } to override, or void to keep original.
'ai-search:chat:load'Type: async (ctx: ChatLoadContext, result: ChatLoadResult) => void
Fires before generation. Load conversation history from storage.
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('ai-search:chat:load', async (ctx, result) => {
if (!ctx.chatId) return
const history = await loadMessagesFromDb(ctx.chatId)
result.history = history
})
})
Context:
| Property | Type | Description |
|---|---|---|
chatId | string | undefined | Chat ID from request |
message | ChatMessage | Current user message |
event | H3Event | Request event |
model | LanguageModel | LLM model instance |
config | RuntimeConfig | Runtime config |
Result: Set result.history to an array of previous ChatMessage objects.
'ai-search:chat:context'Type: async (ctx: ChatContextContext, result: ChatContextResult) => ChatContextResult | void
Fires before RAG generation. Modify the retrieved context or query.
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('ai-search:chat:context', async (ctx, result) => {
const confident = ctx.results.filter(r => r.score > 0.7)
return { results: confident }
})
})
Context:
| Property | Type | Description |
|---|---|---|
query | string | User query text |
results | SearchResult[] | Vector search results for RAG |
messages | ChatMessage[] | Full conversation (history + current) |
event | H3Event | Request event |
config | RuntimeConfig | Runtime config |
Return: { query?: string, results?: SearchResult[] } to override.
'ai-search:chat:finish'Type: async (ctx: ChatFinishContext) => void
Fires after generation completes. Save messages to storage.
consumeStream() internally, so it fires reliably even if the client disconnects before the response completes.import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('ai-search:chat:finish', async (ctx) => {
if (!ctx.chatId) return
await saveMessagesToDb(ctx.chatId, ctx.messages)
})
})
Context:
| Property | Type | Description |
|---|---|---|
chatId | string | undefined | Chat ID from request |
messages | ChatMessage[] | All messages including assistant response (AI SDK UIMessage format) |
responseText | string | The assistant's response text |
event | H3Event | Request event |
config | RuntimeConfig | Runtime config |
Expand queries with synonyms. Enable built-in rewriting with queryRewriting.enabled: true, or implement custom logic:
import { generateText } from 'ai'
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('ai-search:search:query', async (ctx) => {
if (!ctx.model) return
const { text } = await generateText({
model: ctx.model,
prompt: `Add synonyms to this search query: ${ctx.query}`,
})
return { query: text }
})
})
Restrict search to authenticated users:
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('ai-search:search:query', async (ctx) => {
const session = await getSession(ctx.event)
if (!session?.user) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
})
})
Filter results by tenant in multi-tenant apps:
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('ai-search:search:results', async (ctx) => {
const tenantId = ctx.event.context.tenant
return {
results: ctx.results.filter(r =>
r.route.startsWith(`/${tenantId}/`) || r.route.startsWith('/shared/')
),
}
})
})
Summarize old messages to save tokens:
import { generateText } from 'ai'
import { defineNitroPlugin } from 'nitropack/runtime'
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('ai-search:chat:load', async (ctx, result) => {
const history = await loadMessagesFromDb(ctx.chatId)
if (history.length > 20) {
const old = history.slice(0, -10)
const recent = history.slice(-10)
const { text: summary } = await generateText({
model: ctx.model,
prompt: `Summarize this conversation:\n${old.map(m => `${m.role}: ${m.content}`).join('\n')}`,
})
result.history = [
{ role: 'system', content: `Previous context: ${summary}` },
...recent,
]
}
else {
result.history = history
}
})
})