@dudousxd/nestjs-agent-ai-sdk
aiSdkModel adapts any Vercel AI SDK v7 LanguageModel to the ModelProvider SPI — streaming, tool-call translation, cache-aware usage, and gateway cost, with zero provider code.
pnpm add @dudousxd/nestjs-agent-ai-sdk ai @dudousxd/nestjs-agent-corenpm install @dudousxd/nestjs-agent-ai-sdk ai @dudousxd/nestjs-agent-coreaiSdkModel(model, opts?) is the whole package: it wraps any Vercel AI SDK v7 LanguageModel so
AgentModule.forRoot({ model }) needs no hand-written ModelProvider. Pick a model from any AI SDK
provider package and pass it straight in.
import { aiSdkModel } from '@dudousxd/nestjs-agent-ai-sdk';
import { anthropic } from '@ai-sdk/anthropic';
import { AgentModule } from '@dudousxd/nestjs-agent';
AgentModule.forRoot({
model: aiSdkModel(anthropic('claude-sonnet-4-6')),
// …
});opts is CallSettings — anything you'd pass to the SDK's streamText (temperature,
maxOutputTokens, providerOptions, headers, …). model, system, messages, tools, and
abortSignal are owned by the adapter and always win over opts.
What the adapter does
| Concern | Behavior |
|---|---|
| Streaming | Calls streamText, forwards every text-delta to args.sink as it arrives, and assembles the full text for the turn result. |
| Tool calls | Builds an SDK ToolSet without an execute function, so the SDK returns tool-calls for the agent loop to run as its own steps — the adapter never executes a tool itself. |
| Usage | Maps SDK LanguageModelUsage to the core MessageUsage shape, including cache (cacheWriteTokens / cacheReadTokens) and reasoning (reasoningTokens) breakdowns when the provider reports them. |
| Cost | Reads a gateway's real reported cost — Vercel AI Gateway's providerMetadata.gateway.cost, then OpenRouter's total_cost — into costUsd. Direct providers (Anthropic/OpenAI/Bedrock direct) report only tokens, so costUsd is left undefined and the governance read-model estimates instead. See Cost & Governance. |
| Model id | Surfaces response.modelId on the result when the SDK reports one, so accounting reflects the model actually used rather than only the configured id. |
Tool schemas reach the model as real shapes
A tool's input is a Standard Schema — Zod, Valibot, or ArkType all
work (see Tools). The adapter recognizes two forms and hands them straight
to the SDK's own schema conversion rather than degrading them:
- Zod — detected by its
~standard.vendor === 'zod'tag; the SDK converts it with its native zod-to-json-schema path. - Valibot / ArkType / Zod 4 — detected by the Standard JSON Schema extension
(
~standard.jsonSchema); the SDK calls itsinput()converter.
Either way the model sees the tool's real parameter names and required fields, not a generic object.
A hand-rolled Standard Schema that implements neither still validates correctly (the loop calls
~standard.validate before running the handler), but the adapter degrades its model-facing schema
to a permissive { type: 'object', additionalProperties: true } — the model has to guess the
arguments. Implement ~standard.jsonSchema on a custom schema to avoid that.
Peer dependency: ai ^7.0.0
ai is a peer, not a bundled dependency — bring the AI SDK version your app already uses. @ai-sdk/*
provider packages (@ai-sdk/anthropic, @ai-sdk/openai, @ai-sdk/amazon-bedrock, …) are separate
installs; pick whichever the model in aiSdkModel(...) needs.
Downloading staged attachments
AiSdkModelOptions (the adapter's second argument) accepts experimental_download, forwarded
straight to the SDK's streamText call — its hook for fetching a message part's bytes before the
model sees it. It matters for a chat turn's image/PDF attachments (core's MessageAttachment,
rendered by this adapter as native image/file content parts): the AI SDK's default downloader
refuses localhost/private hostnames as an SSRF guard, which kills the model call with
AI_DownloadError: URL with hostname localhost is not allowed the moment an attachment's URL points
at a non-public store — local MinIO in dev, a VPC-only S3 endpoint.
attachmentFetchDownloader(fetchImpl?) is the ready-made fix: a plain fetch with no hostname
policy, for hosts whose attachment staging presigns exactly that kind of non-public URL. URLs the
model already supports natively are left to the provider unchanged; everything else is fetched and
inlined as bytes. Errors carry the response status and hostname only — never the full (possibly
presigned) URL, so a failure log can't leak a credentialed link.
import { aiSdkModel, attachmentFetchDownloader } from '@dudousxd/nestjs-agent-ai-sdk';
import { anthropic } from '@ai-sdk/anthropic';
AgentModule.forRoot({
model: aiSdkModel(anthropic('claude-sonnet-4-6'), {
experimental_download: attachmentFetchDownloader(),
}),
// …
});Only safe for URLs the host itself generated
This bypasses the SDK's SSRF guard entirely, so it's safe only because agent attachment URLs come
exclusively from the host's own AGENT_ATTACHMENT_STAGING presigner — never from user input. Don't
point attachmentFetchDownloader at a downloader hook that might see a user-supplied URL.
Related
- Getting Started — the zero-boilerplate model path in full
- Cost & Governance — reported-cost-wins, the cache-aware fallback estimate, and the pricing table
- Tools — why Standard Schema shape matters for tool calling
@dudousxd/nestjs-agent
The umbrella NestJS module — AgentModule, @AiTool discovery, provideAgentTool, the inline runner, HeaderActorResolver, and the /durable subpath.
@dudousxd/nestjs-agent-react
useAgentChat, the AgentChatTransport and AgentClient, styling-agnostic chat components, dictation, and the markdown subpath.