Skip to content

Testing

ai-sdk-rate-limiter/testing provides a drop-in test limiter that records every completed call. Use it to assert on model usage, token counts, and costs without mocking internals.

Basic usage

typescript
import { createTestLimiter } from 'ai-sdk-rate-limiter/testing'
import { openai } from '@ai-sdk/openai'
import { generateText } from 'ai'

const limiter = createTestLimiter()
const model = limiter.wrap(openai('gpt-4o'))

// Run your code under test
await generateText({ model, prompt: 'Hello!' })
await generateText({ model, prompt: 'Another request' })

// Assert on recorded calls
const calls = limiter.getCalls()
expect(calls).toHaveLength(2)
expect(calls[0].modelId).toBe('gpt-4o')
expect(calls[0].inputTokens).toBeGreaterThan(0)
expect(calls[0].costUsd).toBeGreaterThan(0)

// Reset between tests
limiter.reset()

Testing with real config

createTestLimiter() accepts the same config as createRateLimiter(), so you can test budget enforcement, retry behavior, and circuit breaker logic:

typescript
describe('budget enforcement', () => {
  it('throws when daily budget is exceeded', async () => {
    const limiter = createTestLimiter({
      cost: { budget: { daily: 0.001 }, onExceeded: 'throw' },
    })
    const model = limiter.wrap(openai('gpt-4o'))

    // Exhaust the budget with a big call
    await generateText({ model, prompt: 'big request' })

    // Next call should throw BudgetExceededError
    await expect(generateText({ model, prompt: 'another' }))
      .rejects.toThrow(BudgetExceededError)
  })
})

Testing events

All RateLimiter methods work identically, including on()/off():

typescript
it('emits rateLimited event when queue fills', async () => {
  const limiter = createTestLimiter({
    limits: { 'gpt-4o': { rpm: 1 } },
  })

  const events: RateLimitedEvent[] = []
  limiter.on('rateLimited', e => events.push(e))

  // ... trigger rate limiting ...

  expect(events[0].source).toBe('local')
  expect(events[0].model).toBe('gpt-4o')
})

CallRecord fields

FieldTypeDescription
modelIdstringModel that was called
providerstringProvider name
inputTokensnumberInput tokens from API response
outputTokensnumberOutput tokens from API response
costUsdnumberCost in USD
latencyMsnumberTotal latency in ms
streamingbooleanWhether this was a streaming call
timestampnumberUnix timestamp (ms) when completed

Vitest example (full test file)

typescript
import { describe, it, expect, beforeEach } from 'vitest'
import { createTestLimiter } from 'ai-sdk-rate-limiter/testing'
import { BudgetExceededError } from 'ai-sdk-rate-limiter'
import { openai } from '@ai-sdk/openai'
import { generateText } from 'ai'

describe('my AI service', () => {
  let limiter: ReturnType<typeof createTestLimiter>
  let model: ReturnType<typeof limiter.wrap>

  beforeEach(() => {
    limiter = createTestLimiter()
    model = limiter.wrap(openai('gpt-4o'))
  })

  it('records each call', async () => {
    await generateText({ model, prompt: 'Hello' })
    expect(limiter.getCalls()).toHaveLength(1)
    expect(limiter.getCalls()[0]?.modelId).toBe('gpt-4o')
  })

  it('reset() clears history', async () => {
    await generateText({ model, prompt: 'Hello' })
    limiter.reset()
    expect(limiter.getCalls()).toHaveLength(0)
  })
})

Released under the MIT License.