npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mgvdev/nestjs-ai

v0.2.0

Published

Structured AI toolkit for NestJS — agents, tools, structured output, streaming and embeddings on top of the Vercel AI SDK.

Readme

@mgvdev/nestjs-ai

Structured AI toolkit for NestJS — build agents with tools, structured output, streaming and embeddings on top of the Vercel AI SDK, with first-class dependency injection.

Inspired by Laravel AI: a consistent, framework-native interface over multiple providers (OpenAI, Anthropic, Google).

Docs: full guides live in documentation/. A bundled skill lives at skill/nestjs-ai/SKILL.md. It is discovered automatically by nest-boost via the nestBoost.skills field in package.json. For raw Claude Code, copy it to .claude/skills/nestjs-ai/ (or symlink from node_modules/@mgvdev/nestjs-ai/skill/nestjs-ai).

  • 🧩 @Agent / @Tool decorators — agents and tools are ordinary NestJS providers, so they can inject repositories, services, config, anything.
  • 🔧 Automatic tool discovery — tools are found across your app and wired to the model; execution runs on the DI-managed instance.
  • 🧠 Pluggable conversation memory — a ConversationStore interface with an in-memory default; bring your own DB.
  • 📦 Structured output & streaming — Zod-validated objects and token streams.
  • 🔢 Embeddings — one injectable service for embed / embedMany.
  • 🖼️ Multimodal — image generation, speech synthesis, transcription.
  • 📚 RAG — vector store interface, in-memory impl, retrieval tool for agents.
  • 📝 Prompt registry — named, versioned templates with interpolation.
  • 🛡️ Events & guardrails — lifecycle events (@OnEvent) and blocking hooks.
  • 💾 Durable memory — TypeORM and Prisma conversation-store adapters.
  • 🤝 Multi-agent — agents as tools (supervisor / handoff).
  • 🔌 MCP — adapt Model Context Protocol servers into agent tools.
  • ♻️ Resilience — model fallback chains, retries, response/embedding caching.
  • Human-in-the-loop — tool approval gates.
  • ⏱️ Background jobs — run agents asynchronously on BullMQ.
  • 🌐 HTTP/SSE — stream agents straight to the response.
  • 💰 Cost & budgets — token/cost tracking with per-conversation budget guards.
  • 🚦 Rate limiting — throttle runs per conversation/user.
  • 🧠 Semantic memory — long-term recall via embeddings.
  • 🛡️ Content safety — PII redaction and moderation guardrails.
  • 🥇 Reranking — heuristic or model reranking for RAG.
  • 🧪 Testing & evals — mock helpers and an LLM-as-judge harness.
  • 🔌 More vector stores — Qdrant, Pinecone (plus pgvector).
  • 📡 WebSocket — realtime agent streaming over socket.io.

Installation

npm install @mgvdev/nestjs-ai ai zod
# plus the provider SDK(s) you use:
npm install @ai-sdk/openai        # and/or @ai-sdk/anthropic, @ai-sdk/google

@nestjs/common, @nestjs/core and reflect-metadata are peer dependencies (already present in every Nest app).

Optional peers, only needed for the matching feature:

npm install @nestjs/event-emitter          # events & guardrails
npm install @nestjs/typeorm typeorm         # TypeORM conversation store

Configuration

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AiModule } from '@mgvdev/nestjs-ai';

@Module({
  imports: [
    ConfigModule.forRoot(),
    AiModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        providers: {
          openai: { apiKey: config.getOrThrow('OPENAI_API_KEY') },
        },
        defaultModel: 'openai:gpt-4o',
        defaultEmbeddingModel: 'openai:text-embedding-3-small',
      }),
    }),
  ],
})
export class AppModule {}

AiModule.forRoot(options) is available for static configuration. The module is global, so agents/tools/services are injectable everywhere.

Model ids are "provider:model" (e.g. "openai:gpt-4o"). A bare "gpt-4o" works when a single provider is configured or a defaultModel sets the provider.

Tools

Declare tools as methods on an injectable provider. The Zod schema types the arguments; the return value is fed back to the model.

import { Injectable } from '@nestjs/common';
import { Tool } from '@mgvdev/nestjs-ai';
import { z } from 'zod';

@Injectable()
export class WeatherTools {
  constructor(private readonly weather: WeatherApi) {} // regular DI

  @Tool({
    description: 'Get the current weather for a city',
    schema: z.object({ city: z.string() }),
  })
  getWeather({ city }: { city: string }) {
    return this.weather.lookup(city); // uses the injected dependency
  }
}

Agents

Extend AiAgent and annotate with @Agent. Reference tool provider classes in tools.

import { Agent, AiAgent } from '@mgvdev/nestjs-ai';

@Agent({
  model: 'openai:gpt-4o',
  system: 'You are a helpful weather assistant.',
  tools: [WeatherTools],
  maxSteps: 5,
})
export class WeatherAgent extends AiAgent {}

Register the classes (or use AiModule.forFeature):

@Module({
  imports: [AiModule.forFeature({ agents: [WeatherAgent], tools: [WeatherTools] })],
  providers: [WeatherApi],
})
export class WeatherModule {}

Run it:

@Injectable()
export class ChatService {
  constructor(private readonly agent: WeatherAgent) {}

  async ask(question: string) {
    const { text } = await this.agent.run(question);
    return text;
  }
}

Conversation memory

Pass a conversationId to load prior turns and persist the new exchange:

await this.agent.run('What about tomorrow?', { conversationId: user.id });

The default store is in-memory. Provide your own by implementing ConversationStore and passing it to AiModule.forRoot({ conversationStore }).

Structured output

Declare an output schema on the agent (or pass schema per call) to get a validated object instead of text:

@Agent({
  model: 'openai:gpt-4o',
  system: 'Extract the order details.',
  output: z.object({ product: z.string(), quantity: z.number() }),
})
export class OrderAgent extends AiAgent {}

const { object } = await orderAgent.run<{ product: string; quantity: number }>(text);

Streaming

.stream() resolves to the raw Vercel AI SDK stream result, so you can pipe it to an HTTP response or iterate textStream:

@Controller('chat')
export class ChatController {
  constructor(private readonly agent: WeatherAgent) {}

  @Post()
  async stream(@Body('prompt') prompt: string, @Res() res: Response) {
    const result = await this.agent.stream(prompt);
    result.pipeUIMessageStreamToResponse(res);
  }
}

Embeddings

import { EmbeddingsService } from '@mgvdev/nestjs-ai';

@Injectable()
export class SearchService {
  constructor(private readonly embeddings: EmbeddingsService) {}

  async index(docs: string[]) {
    const { embeddings } = await this.embeddings.embedMany(docs);
    return embeddings;
  }
}

Multimodal

ImageService, SpeechService, and TranscriptionService wrap the SDK's image/speech/transcription APIs. Set defaultImageModel / defaultSpeechModel / defaultTranscriptionModel in options, or pass model per call.

const { image } = await this.images.generate('a fox in the snow', { size: '1024x1024' });
const { audio } = await this.speech.generate('Hello there', { model: 'openai:tts-1', voice: 'alloy' });
const { text } = await this.transcription.transcribe(buffer, { model: 'openai:whisper-1' });

RAG (retrieval-augmented generation)

Ingest documents (chunked + embedded) into a VectorStore, then retrieve the most relevant chunks. The default store is InMemoryVectorStore (cosine similarity); override vectorStore in options for pgvector etc.

import { RagService, createRetrievalTool } from '@mgvdev/nestjs-ai';

await this.rag.ingest([
  { id: 'doc1', content: longText, metadata: { source: 'handbook' } },
]);
const hits = await this.rag.retrieve('vacation policy', { topK: 4 });

Expose retrieval to an agent as a tool — either a @Tool method that calls RagService, or the createRetrievalTool factory:

@Injectable()
class KnowledgeTools {
  constructor(private readonly rag: RagService) {}

  @Tool({ description: 'Search the handbook', schema: z.object({ query: z.string() }) })
  async search({ query }: { query: string }) {
    const hits = await this.rag.retrieve(query);
    return hits.map((h) => h.content).join('\n\n');
  }
}

Prompt registry

Register named, versioned templates and render them with {{var}} interpolation.

AiModule.forRoot({
  prompts: [
    { name: 'support', version: 'v1', template: 'Help {{user}} with {{topic}}.' },
  ],
});

// render directly…
const system = this.prompts.render('support', { user: 'Ada', topic: 'billing' });
// …or resolve an agent's system prompt per call:
await agent.run(question, { systemPrompt: { name: 'support', vars: { user, topic } } });

Events & guardrails

With @nestjs/event-emitter installed and EventEmitterModule.forRoot() imported, the library emits lifecycle events (no-op otherwise):

import { AI_EVENTS } from '@mgvdev/nestjs-ai';

@Injectable()
class AiListener {
  @OnEvent(AI_EVENTS.agentRunFinish)
  onFinish(payload: { agent: string; result: AgentResult }) {
    // log usage, persist traces, …
  }
}

Events: ai.agent.run.start|finish|error, ai.tool.call|result, ai.stream.finish.

Guardrails inspect, mutate, or block runs and tool calls. Throw to abort:

import { Guardrail, type GuardrailContext } from '@mgvdev/nestjs-ai';

@Guardrail()
export class ProfanityGuard {
  beforeRun(ctx: GuardrailContext) {
    if (isProfane(ctx.messages)) throw new ForbiddenException('Blocked');
  }
  onToolCall(tool: string, args: unknown) {
    // veto specific tool calls
  }
}

Register guardrails as providers (auto-discovered) or via AiModule.forRoot({ guardrails: [ProfanityGuard] }).

Telemetry

Forward OpenTelemetry spans to the AI SDK by enabling telemetry (requires an OTel setup in your app):

AiModule.forRoot({ telemetry: { isEnabled: true, functionId: 'support-agent' } });

Persistent conversation stores

The in-memory store is the default. For durable history:

TypeORM (subpath @mgvdev/nestjs-ai/typeorm, needs typeorm + @nestjs/typeorm):

import { getRepositoryToken, TypeOrmModule } from '@nestjs/typeorm';
import {
  ConversationMessageEntity,
  TypeOrmConversationStore,
} from '@mgvdev/nestjs-ai/typeorm';

@Module({
  imports: [
    TypeOrmModule.forFeature([ConversationMessageEntity]),
    AiModule.forRoot({
      providers: { openai: { apiKey: process.env.OPENAI_API_KEY } },
      conversationStore: {
        useFactory: (repo) => new TypeOrmConversationStore(repo),
        inject: [getRepositoryToken(ConversationMessageEntity)],
      },
    }),
  ],
})
export class AppModule {}

Prisma (no build-time dependency — pass a model delegate):

import { PrismaConversationStore } from '@mgvdev/nestjs-ai';

AiModule.forRoot({
  conversationStore: {
    useFactory: (prisma: PrismaService) => new PrismaConversationStore(prisma.aiMessage),
    inject: [PrismaService],
  },
});
model AiMessage {
  id             String   @id @default(cuid())
  conversationId String
  role           String
  content        Json
  position       Int
  createdAt      DateTime @default(now())
  @@index([conversationId])
}

Multi-agent orchestration

Reference an @Agent class in another agent's tools to delegate to it (supervisor / handoff). The sub-agent's output is returned to the caller.

@Agent({ model: 'openai:gpt-4o', system: 'Research facts.' })
export class ResearchAgent extends AiAgent {}

@Agent({
  model: 'openai:gpt-4o',
  system: 'Coordinate specialists to answer the user.',
  tools: [ResearchAgent, WriterAgent],
})
export class SupervisorAgent extends AiAgent {}

AgentRegistry indexes all agents by class name (used by orchestration and background jobs); createAgentTool(agent, opts) wraps any agent as a tool manually.

MCP (Model Context Protocol)

Adapt tools from an MCP server into an agent tool set. Bring your own client from @modelcontextprotocol/sdk (a listTools/callTool/close object):

const set = await this.mcp.connect('filesystem', mcpClient);
// tools are now available; reference them via AiService or merge into an agent
await this.ai.generateText({ model: 'openai:gpt-4o', tools: set, prompt });

Resilience: fallback models & retries

Pass a model array to configure a fallback chain (each tried in order), and maxRetries for SDK-native retries:

@Agent({ model: ['openai:gpt-4o', 'anthropic:claude-sonnet-5'] })
export class ResilientAgent extends AiAgent {}

await agent.run(prompt, { maxRetries: 3 });

Caching

Configure a cache to memoize model responses (via middleware) and single embeddings. Implement AiCache (e.g. Redis) or use the in-memory default:

AiModule.forRoot({
  providers: { openai: { apiKey } },
  cache: InMemoryAiCache,
  cacheTtlMs: 60_000,
});

Human-in-the-loop (tool approval)

Flag a tool with requiresApproval and register an ApprovalGate. The gate is consulted before the tool executes; returning false blocks it.

@Tool({ description: 'Delete a record', schema: z.object({ id: z.string() }), requiresApproval: true })
deleteRecord({ id }: { id: string }) { /* … */ }

@Injectable()
class QueueApprovalGate implements ApprovalGate {
  async requestApproval({ tool, args }: ApprovalContext) {
    return askAHuman(tool, args); // resolve true/false
  }
}

AiModule.forRoot({ providers: { openai: { apiKey } }, approvalGate: QueueApprovalGate });

Background jobs (BullMQ)

Run agents asynchronously. Import AgentJobsModule (needs the optional peer bullmq + Redis); a worker resolves agents from AgentRegistry.

@Module({ imports: [AgentJobsModule.forRoot({ connection: { host: 'localhost', port: 6379 } })] })
export class JobsModule {}

// enqueue
await this.queue.enqueue({ agent: 'SupportAgent', input: 'summarize ticket 42' });

HTTP / SSE streaming

Pipe an agent stream straight to the HTTP response:

@Post('chat')
async chat(@Body('prompt') prompt: string, @Res() res: Response) {
  pipeAgentStream(await this.agent.stream(prompt), res, { protocol: 'ui' });
}

Or use the interceptor and return the stream promise; Nest resolves it before AgentStreamInterceptor receives the result:

@UseInterceptors(new AgentStreamInterceptor({ protocol: 'ui' }))
@Post('chat')
chat(@Body('prompt') prompt: string) {
  return this.agent.stream(prompt);
}

pgvector

A Postgres/pgvector VectorStore. Pass your own pg Pool:

import { PgVectorStore } from '@mgvdev/nestjs-ai';

AiModule.forRoot({
  providers: { openai: { apiKey } },
  vectorStore: { useFactory: () => new PgVectorStore(pool, { table: 'ai_documents' }) },
});
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE ai_documents (
  id text PRIMARY KEY, content text NOT NULL,
  embedding vector(1536) NOT NULL, metadata jsonb
);
CREATE INDEX ON ai_documents USING hnsw (embedding vector_cosine_ops);

Cost tracking & budgets

Token usage and USD cost are tracked per conversation. Set a budget to block runaway spend:

AiModule.forRoot({
  providers: { openai: { apiKey } },
  maxCostPerConversation: 0.5,   // blocks the next run once $0.50 is reached
  pricing: { 'gpt-4o': { input: 2.5, output: 10 } }, // override defaults
});

// inspect anytime
const { cost, inputTokens } = this.usage.totals(conversationId);
// or listen: @OnEvent('ai.usage') onUsage(record) { ... }

Cap a single run with budget (globally, or per agent to override it):

AiModule.forRoot({
  providers: { openai: { apiKey } },
  budget: { maxCostPerRun: 0.05, maxTotalTokensPerRun: 20_000 },
});

@Agent({ model: 'gpt-4o', budget: { maxOutputTokensPerRun: 2_000 } })
export class SummaryAgent extends AiAgent {}

Exceeding a limit throws RunBudgetExceededError. Implement OnBudgetExceeded on the agent (beforeRunBudget to block up front, afterRunBudget to deduct credits, onBudgetExceeded to allow/block) or register a module-wide budgetExceededHandler. See Reliability.

Rate limiting

AiModule.forRoot({
  providers: { openai: { apiKey } },
  rateLimiter: {
    useValue: new InMemoryRateLimiter({ capacity: 10, refillTokens: 10, intervalMs: 60_000 }),
  },
});

Runs are throttled per conversationId. Implement RateLimiter for a distributed (Redis) limiter.

Semantic memory

Store and recall context across turns via embeddings:

await this.memory.remember(conversationId, 'The user prefers dark mode.');
// recall automatically on the next run:
await agent.run('what are my settings?', { conversationId, recall: { topK: 3 } });

Content safety

Register guardrails to redact PII and moderate content:

AiModule.forFeature({
  guardrails: [
    PiiRedactionGuardrail,
    createModerationGuardrail({ blocked: ['secret-project'] }),
  ],
});

Reranking

Improve RAG precision by reranking candidates:

// heuristic (no dependency), or configure a model reranker:
const hits = await this.rag.retrieve('query', { topK: 4, rerank: true });

More vector stores

QdrantVectorStore and PineconeVectorStore join PgVectorStore and InMemoryVectorStore. Pass your own client:

AiModule.forRoot({
  providers: { openai: { apiKey } },
  vectorStore: { useFactory: () => new QdrantVectorStore(qdrant, { collection: 'docs' }) },
});

Evals (LLM-as-judge)

const report = await this.evals.run(myAgent, [
  { input: 'capital of France?', expected: 'Paris' },
], { judge: createLlmJudge(this.ai, { scale: 5 }) });
// report.averageScore, report.passRate, report.results

Testing utilities

From @mgvdev/nestjs-ai/testing (needs msw):

import { createTestingAiModule, createMockModel } from '@mgvdev/nestjs-ai/testing';

const app = await createTestingAiModule({
  model: createMockModel('mocked answer'),
  providers: [MyAgent],
});
expect((await app.get(MyAgent).run('hi')).text).toBe('mocked answer');

Realtime (WebSocket)

From @mgvdev/nestjs-ai/websocket (needs @nestjs/websockets, socket.io):

@Module({ providers: [AgentGateway] })
export class RealtimeModule {}
// client: socket.emit('agent:run', { agent: 'ChatAgent', input });
//         socket.on('agent:chunk', ({ delta }) => …); socket.on('agent:done', ({ text }) => …);

Raw generation (no agent class)

AiService is a thin, DI-friendly facade over the SDK when you don't want an agent class. It resolves model strings and tools references:

import { AiService } from '@mgvdev/nestjs-ai';

const { text } = await this.ai.generateText({
  model: 'openai:gpt-4o',
  tools: [WeatherTools],
  prompt: 'Weather in Paris?',
});

API surface

| Export | Purpose | | --- | --- | | AiModule | forRoot / forRootAsync / forFeature | | @Agent, AiAgent | Declare and run agents (.run / .stream) | | @Tool | Declare a tool method on a provider | | AiService | Raw generateText / streamText / generateObject / streamObject | | EmbeddingsService | embed / embedMany | | ImageService, SpeechService, TranscriptionService | Multimodal generation | | RagService, VectorStore, InMemoryVectorStore, createRetrievalTool | RAG | | PromptRegistry | Named, versioned prompt templates | | @Guardrail, GuardrailRegistry, AiEventEmitter, AI_EVENTS | Events & guardrails | | ProviderRegistry | Resolve "provider:model" ids to models | | ConversationStore, InMemoryConversationStore | Conversation memory | | TypeOrmConversationStore (/typeorm), PrismaConversationStore | Durable memory | | ToolRegistry | Introspect discovered tools |

License

MIT © Maxence Guyonvarho