Aviary
Packages

@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-core
npm install @dudousxd/nestjs-agent-ai-sdk ai @dudousxd/nestjs-agent-core

aiSdkModel(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

ConcernBehavior
StreamingCalls streamText, forwards every text-delta to args.sink as it arrives, and assembles the full text for the turn result.
Tool callsBuilds 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.
UsageMaps SDK LanguageModelUsage to the core MessageUsage shape, including cache (cacheWriteTokens / cacheReadTokens) and reasoning (reasoningTokens) breakdowns when the provider reports them.
CostReads 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 idSurfaces 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 its input() 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.

  • 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

On this page