Skip to content

Vercel AI SDK Integration

ai-armor provides a first-class middleware adapter for the Vercel AI SDK. Wrap any language model with ai-armor protection -- rate limiting, caching, cost tracking, and logging are applied automatically.

Installation

bash
npm install ai-armor ai @ai-sdk/openai

Quick Start

ts
import { openai } from '@ai-sdk/openai'
import { generateText, wrapLanguageModel } from 'ai'
import { createArmor } from 'ai-armor'
import { aiArmorMiddleware } from 'ai-armor/ai-sdk'

// 1. Create armor instance
const armor = createArmor({
  rateLimit: {
    strategy: 'sliding-window',
    rules: [{ key: 'user', limit: 30, window: '1m' }],
  },
  budget: {
    daily: 50,
    onExceeded: 'downgrade-model',
    downgradeMap: { 'gpt-4o': 'gpt-4o-mini' },
  },
  cache: {
    enabled: true,
    strategy: 'exact',
    ttl: 3600,

  },
  logging: {
    enabled: true,
    include: ['model', 'tokens', 'cost', 'latency', 'cached', 'userId'],
  },
})

// 2. Wrap any AI SDK model
const protectedModel = wrapLanguageModel({
  model: openai('gpt-4o'),
  middleware: aiArmorMiddleware(armor, { userId: 'user-123' }),
})

// 3. Use as normal -- all protections applied automatically
const { text } = await generateText({
  model: protectedModel,
  prompt: 'Explain TypeScript generics in one sentence.',
})

// eslint-disable-next-line no-console
console.log(text)

How aiArmorMiddleware Works

The middleware hooks into three phases of the AI SDK request lifecycle:

transformParams

Before the request is sent to the AI provider:

  1. Resolve model aliases -- Maps 'fast' to 'gpt-4o-mini', etc.
  2. Check safety -- Blocks prompt injection, PII, and custom patterns
  3. Check rate limit -- Throws if the user has exceeded their limit
  4. Check budget -- Throws if blocked, or suggests a cheaper model
  5. Prepare cache -- Stores cache lookup result for wrapGenerate

wrapGenerate

After the AI provider responds (non-streaming):

  1. Return cached -- If a cache hit was found, returns it without calling the provider
  2. Track cost -- Records token usage and calculated cost
  3. Cache response -- Stores successful responses for future cache hits
  4. Log request -- Records the full ArmorLog entry

wrapStream

For streaming responses (streamText):

  1. Track cost on stream completion -- Captures token usage from step-finish / finish stream events
  2. Log request -- Records the full ArmorLog entry after the stream ends

All rate limiting, safety, and budget checks happen in transformParams, so they apply to both generateText and streamText calls.

Request Flow:

  generateText() --> transformParams --> [cache hit?] --> return cached
                          |
                          | (cache miss)
                          v
                    wrapGenerate --> AI Provider --> trackCost
                          |                          --> cache response
                          v                          --> log request
                    return result

  streamText()  --> transformParams --> wrapStream --> AI Provider
                                            |
                                            v (stream chunks pass through)
                                       [step-finish] --> trackCost + log

Passing Context

The second argument to aiArmorMiddleware is an ArmorContext object. Use it to identify users for rate limiting and per-user budgets:

ts
// Static context (same user for all calls)
const middleware = aiArmorMiddleware(armor, { userId: 'user-123' })

// Dynamic context (per-request in a server)
app.post('/api/chat', async (req, res) => {
  const model = wrapLanguageModel({
    model: openai('gpt-4o'),
    middleware: aiArmorMiddleware(armor, {
      userId: req.headers['x-user-id'] as string,
      ip: req.ip,
      apiKey: req.headers['x-api-key'] as string,
    }),
  })

  const { text } = await generateText({
    model,
    prompt: req.body.prompt,
  })

  res.json({ text })
})

Error Handling

The middleware throws errors for rate limit and budget violations. Catch them in your application code:

ts
import { generateText, wrapLanguageModel } from 'ai'

const protectedModel = wrapLanguageModel({
  model: openai('gpt-4o'),
  middleware: aiArmorMiddleware(armor, { userId: 'user-123' }),
})

try {
  const { text } = await generateText({
    model: protectedModel,
    prompt: 'Hello',
  })
  // eslint-disable-next-line no-console
  console.log(text)
}
catch (error) {
  if (error instanceof Error) {
    if (error.message.includes('[ai-armor] Rate limited')) {
      // Extract reset time from the error message
      // eslint-disable-next-line no-console
      console.log('Rate limited. Try again later.')
    }
    else if (error.message.includes('[ai-armor] Budget exceeded')) {
      // eslint-disable-next-line no-console
      console.log('Budget exceeded. Request blocked.')
    }
    else {
      // AI provider error
      console.error('AI error:', error.message)
    }
  }
}

Error Messages

  • Rate limit: [ai-armor] Rate limited. Resets at 2026-03-22T10:30:00.000Z
  • Budget block: [ai-armor] Budget exceeded. Action: block

Automatic Features

When using the middleware, these features are handled without any manual code:

Rate Limiting

Checked before every request. If the user exceeds the limit, the middleware throws an error.

Model Resolution

Aliases in the routing.aliases config are resolved automatically. If you pass openai('fast') and fast is aliased to gpt-4o-mini, the request goes to gpt-4o-mini.

Budget Checking

If the budget is exceeded:

  • onExceeded: 'block' -- throws an error
  • onExceeded: 'warn' -- proceeds, fires onWarned callback
  • onExceeded: 'downgrade-model' -- uses the suggestedModel from downgradeMap

Caching

Identical requests (same model, messages, temperature, tools) return cached responses. Cache hits are logged with cached: true and cost: 0.

Cost Tracking

After every successful response, armor.trackCost() is called with the actual token counts from the response usage metadata.

Logging

Every request is logged with a full ArmorLog entry, including failed requests (with the error stored in the blocked field).

Full Example

ts
import { openai } from '@ai-sdk/openai'
import { generateText, wrapLanguageModel } from 'ai'
import { createArmor } from 'ai-armor'
import { aiArmorMiddleware } from 'ai-armor/ai-sdk'

const armor = createArmor({
  rateLimit: {
    strategy: 'sliding-window',
    rules: [{ key: 'user', limit: 30, window: '1m' }],
  },
  budget: {
    daily: 50,
    perUser: 10,
    onExceeded: 'downgrade-model',
    downgradeMap: { 'gpt-4o': 'gpt-4o-mini' },
  },
  cache: {
    enabled: true,
    strategy: 'exact',
    ttl: 3600,

  },
  logging: {
    enabled: true,
    include: ['model', 'tokens', 'cost', 'latency', 'cached', 'userId'],
  },
})

async function main() {
  const protectedModel = wrapLanguageModel({
    model: openai('gpt-4o'),
    middleware: aiArmorMiddleware(armor, { userId: 'user-789' }),
  })

  // First call: hits the API
  const { text } = await generateText({
    model: protectedModel,
    prompt: 'Explain REST vs GraphQL in 2 sentences.',
  })
  // eslint-disable-next-line no-console
  console.log(text)

  // Second identical call: returns from cache
  const { text: cached } = await generateText({
    model: protectedModel,
    prompt: 'Explain REST vs GraphQL in 2 sentences.',
  })
  // eslint-disable-next-line no-console
  console.log('(cached)', cached)

  // View logs
  const logs = armor.getLogs()
  // eslint-disable-next-line no-console
  console.log(`Requests: ${logs.length}`)
  // eslint-disable-next-line no-console
  console.log(`Total cost: $${logs.reduce((s, l) => s + l.cost, 0).toFixed(4)}`)
  // eslint-disable-next-line no-console
  console.log(`Cache hits: ${logs.filter(l => l.cached).length}`)
}

main()

Streaming

Streaming responses are fully supported via wrapStream. Token usage and cost are tracked automatically when the stream completes:

ts
import { openai } from '@ai-sdk/openai'
import { streamText, wrapLanguageModel } from 'ai'
import { createArmor } from 'ai-armor'
import { aiArmorMiddleware } from 'ai-armor/ai-sdk'

const armor = createArmor({
  budget: { daily: 50, onExceeded: 'warn' },
  logging: { enabled: true, include: ['model', 'tokens', 'cost', 'latency'] },
})

const protectedModel = wrapLanguageModel({
  model: openai('gpt-4o'),
  middleware: aiArmorMiddleware(armor, { userId: 'user-123' }),
})

const { textStream } = streamText({
  model: protectedModel,
  prompt: 'Write a haiku about TypeScript.',
})

for await (const chunk of textStream) {
  process.stdout.write(chunk)
}
// Cost and tokens are tracked automatically when the stream ends

Using with Other Providers

The middleware works with any AI SDK provider:

ts
import { anthropic } from '@ai-sdk/anthropic'
import { google } from '@ai-sdk/google'
import { mistral } from '@ai-sdk/mistral'

// Anthropic
const protectedClaude = wrapLanguageModel({
  model: anthropic('claude-sonnet-4-20250514'),
  middleware: aiArmorMiddleware(armor, { userId: 'user-1' }),
})

// Google
const protectedGemini = wrapLanguageModel({
  model: google('gemini-2.5-pro'),
  middleware: aiArmorMiddleware(armor, { userId: 'user-1' }),
})

// Mistral
const protectedMistral = wrapLanguageModel({
  model: mistral('mistral-large-latest'),
  middleware: aiArmorMiddleware(armor, { userId: 'user-1' }),
})

Released under the MIT License.