ViteHub is still experimental. Expect bugs and breaking changes.

Rate limit

Consume a Rate Limiter before an Agent Invocation starts.

rateLimit() consumes one budget unit before the main Agent Invocation starts. The Capability owns trusted Agent identity and rejection behavior, while the Rate Limit primitive owns atomic enforcement.

Configure a limiter

Create a direct limiter beside the Agent and pass it to the Capability. The Capability needs the portable decision to attach Agent identity and rejection context, so the handler-only requireRateLimit() guard is not its input.

The built-in memory driver is an owner-package API. Install that package when using it.

Terminal
pnpm add @vite-hub/rate-limit
server/agents/support.ts
import { defineAgent } from 'vite-hub/agent'
import { rateLimit } from 'vite-hub/agent/capabilities'
import { createRateLimiter } from '@vite-hub/rate-limit'
import { memoryRateLimitDriver } from '@vite-hub/rate-limit/drivers/memory'

const invocations = createRateLimiter({
  driver: memoryRateLimitDriver(),
  limit: 20,
  window: '1m',
})

export default defineAgent({
  driver: { model },
  capabilities: [
    rateLimit({
      limiter: invocations,
    }),
  ],
})

How rate limits work

The Capability runs during the input phase. It derives a stable key from the Capability id, scope, and trusted identity, then calls the RateLimiter exactly once.

A rejected decision throws ViteHubError with code RATE_LIMIT_REJECTED; the HTTP handler maps that code to 429 and emits retry-after headers only when the selected driver reports retryAfter. Cloudflare native enforcement does not return portable quota metadata.

The decision is stored under the Capability id in Agent Invocation Context and exposed as a finish extension. It contains the primitive decision plus capabilityId, identity, identitySource, key, and scope.

Choose identity

The Agent Definition chooses the identity, not the Rate Limit Driver. The default identity: 'auto' prefers the Agent Invoker, then Agent Run metadata, then trusted IP headers, and finally an anonymous identity.

Use identity: 'invoker' when authentication provides a stable Agent Invoker. Use identity: 'ip' only after naming headers that the deployed host sets and sanitizes.

server/agents/public-support.ts
rateLimit({
  identity: 'ip',
  limiter: invocations,
  trustedIpHeaders: ['cf-connecting-ip'],
})

Do not trust a client-controlled forwarding header. The Capability reads only the headers listed in trustedIpHeaders, but the application remains responsible for ensuring the host overwrites them.

Use a custom driver

Pass any RateLimiter when the application owns state or enforcement outside managed ViteHub Rate Limits.

server/rate-limiter.ts
import { createRateLimiter } from 'vite-hub/rate-limit'
import type { RateLimitDriver } from 'vite-hub/rate-limit'

declare const driver: RateLimitDriver

export const invocationLimiter = createRateLimiter({
  driver,
  enforcement: 'strict',
  limit: 20,
  window: '1m',
})
server/agents/support.ts
import { rateLimit } from 'vite-hub/agent/capabilities'
import { invocationLimiter } from '../rate-limiter'

rateLimit({ limiter: invocationLimiter })

The custom driver must implement atomic consume(). ViteHub does not provide a generic KV adapter because a portable get() followed by set() cannot guarantee an atomic decision under concurrency.

Options

OptionTypeDefaultDescription
limiterRateLimiter | functionrequiredDirect limiter or runtime resolver.
idstring"rate-limit"Capability id and Agent Invocation Context key.
identity"auto" | "invoker" | "ip" | "run" | function"auto"Identity used to derive the private rate-limit key.
scopestring | functionCapability idAdditional key partition.
trustedIpHeadersstring[]noneHost-controlled headers allowed for IP identity.
messagestring | functiondefault rejection messageError message for a rejected decision.
onDecisionfunctionnoneCallback after every decision.
onAllowedfunctionnoneCallback after an allowed decision.
onRejectedfunctionnoneCallback after a rejected decision.

Migrate from an inline store

The Capability no longer owns limit, window, action, or store. Move policy into a direct RateLimiter, then replace store with limiter.

Before
rateLimit({
  limit: 20,
  store: 'memory',
  window: '1m',
})
After
const invocations = createRateLimiter({
  driver: memoryRateLimitDriver(),
  limit: 20,
  window: '1m',
})

rateLimit({
  limiter: invocations,
})

Verify it

Run repeated Agent Invocations with the same identity. Confirm that the first limit invocations reach the Agent Driver and the next fails with code RATE_LIMIT_REJECTED before model, provider, or custom-run execution.

For local tests, use a dedicated memory driver instance. For Cloudflare, resolve the request binding in the limiter resolver and test the deployed binding because the native decision depends on request-scoped Worker environment.