Enforce entitlements on LLM traffic

You are browsing documentation for an older version of . See the latest documentation here.

TL;DR

Meter LLM token usage with the Metering & Billing plugin, grant the customer a token allowance as a metered entitlement, and enable the Entitlement Enforcement plugin on the Route.

The Entitlement Enforcement plugin polls the Metering & Billing Entitlement Access API for the customer’s remaining allowance and blocks requests once the token limit is reached, so enforcement happens at the gateway instead of in your own infrastructure.

Prerequisites

This is a Konnect tutorial and requires a Konnect personal access token.

  1. Create a new personal access token by opening the Konnect PAT page and selecting Generate Token.

  2. Export your token to an environment variable:

    export KONNECT_TOKEN='YOUR_KONNECT_PAT'
  3. Run the quickstart script to automatically provision a Control Plane and Data Plane, and configure your environment:

    curl -Ls https://get.konghq.com/quickstart | bash -s -- -k $KONNECT_TOKEN \
         --deck-output

    This sets up a Konnect Control Plane named quickstart, provisions a local Data Plane, and prints out the following environment variable exports:

    export DECK_KONNECT_TOKEN=$KONNECT_TOKEN
    export DECK_KONNECT_CONTROL_PLANE_NAME=quickstart
    export DECK_KONNECT_ADDR=https://us.api.konghq.com
    export KONNECT_CONTROL_PLANE_URL=https://us.api.konghq.com
    export KONNECT_PROXY_URL='http://localhost:8000'

    Copy and paste these into your terminal to configure your session.

To complete this tutorial, install decK. We recommend keeping decK up to date with the latest version (1.66.1).

decK is a CLI tool for managing Kong Gateway declaratively with state files. This guide uses deck gateway apply, which directly applies entity configuration to your Gateway instance.

You can check your current decK version with deck version.

For this tutorial, you’ll need Kong Gateway entities, like Gateway Services and Routes, pre-configured. These entities are essential for Kong Gateway to function but installing them isn’t the focus of this guide. Follow these steps to pre-configure them:

  1. Run the following command:

    echo '
    _format_version: "3.0"
    services:
      - name: example-service
        url: http://httpbin.konghq.com/anything
    routes:
      - name: example-route
        paths:
        - "/anything"
        service:
          name: example-service
        protocols:
        - http
        - https
    ' | deck gateway apply -

To learn more about entities, you can read our entities documentation.

You need the Metering & Billing Admin role in Konnect to configure Metering & Billing.

This tutorial uses OpenAI:

  1. Create an OpenAI account.
  2. Get an API key.
  3. Create a decK variable with the API key:

    export DECK_OPENAI_API_KEY='YOUR OPENAI API KEY'

You need a Konnect system account token (spat_) with the Ingest role for Metering. This token authenticates the Metering & Billing plugin when it sends events to the Konnect ingest endpoint.

Export your system account token:

export DECK_AUTH_TOKEN='YOUR SPAT TOKEN'

For more information, see system accounts and access tokens.

You need a Konnect system account token (spat_) with the Entitlement Access role for Metering. This role grants the permission to query entitlement access information for the Metering & Billing Entitlement Access API. This token authenticates the Entitlement Enforcement plugin when it checks customer access.

This is a different token from the Ingest token used by the Metering & Billing plugin: ingesting events and querying entitlement access are separate permissions.

Export your system account token:

export DECK_ENTITLEMENT_ACCESS_TOKEN='YOUR SPAT'

For more information, see system accounts and access tokens.

To complete this tutorial, make sure you have the following:

  • A Redis Stack running and accessible from the environment where Kong is deployed.
  • Port 6379, or your custom Redis port is open and reachable from Kong.
  • Redis host set as an environment variable so the plugin can connect:

    export DECK_REDIS_HOST='YOUR-REDIS-HOST'

If you’re testing locally with Docker, use host.docker.internal as the host value.

This guide shows how to enforce an LLM token allowance on AI Gateway traffic. Metering LLM traffic tells you what a customer consumed, but it doesn’t stop them consuming more. The Entitlement Enforcement plugin closes that gap: it polls the Metering & Billing Entitlement Access API for the customer’s remaining token allowance and blocks requests once the allowance is spent.

In this guide, you’ll:

  • Create a Kong Gateway Consumer that you’ll map to a customer
  • Route LLM traffic through the AI Gateway AI Proxy plugin
  • Set up a meter for LLM tokens and a feature that counts only prompt tokens
  • Create a plan that grants a token allowance as a metered entitlement, and publish it
  • Create a customer and start a subscription so the entitlement is active
  • Enable the Metering & Billing plugin to report token usage, and the Entitlement Enforcement plugin to enforce the allowance
  • Verify that LLM requests are allowed until the token allowance runs out, then blocked

Enforcement needs configuration on both sides, and the pieces reference each other. The following table lists what you’ll create, why each piece is needed, and what it connects to:

What you configure

Where

Why it’s needed

example-service and example-route Prerequisite The entities the plugins attach to. The Entitlement Enforcement plugin is enabled on the Route, so it runs for traffic matching that Route only.
Two Konnect system account tokens Prerequisite One with the Ingest role for reporting usage, one with the Entitlement Access role for reading entitlements. The roles are separate, so a single token can’t do both unless it has the Admin role.
OpenAI API key Prerequisite AI Proxy authenticates to OpenAI with it. Without a working model call there are no tokens to meter or enforce.
Redis Prerequisite Where the plugin caches enforcement state: a background timer writes the state it fetches from Metering & Billing, and each worker syncs its local cache from Redis. The plugin can’t enforce without a reachable Redis.
Consumer with a pinned id Kong Gateway Identifies the client. The pinned id fixes the consumer:<id> subject key that both plugins use, so the customer you create later can be matched to it.
Key Auth plugin Globally Authenticates the request so Kong Gateway can resolve a Consumer. Without an authenticated Consumer, there’s no customer to enforce against.
AI Proxy plugin, with statistics enabled example-service Proxies chat requests to the model and reports the token counts. Token metering depends on log_statistics.
Meter that sums tokens Metering & Billing Counts the tokens reported for each LLM request. This is the usage the allowance is measured against.
Metered feature, filtered to prompt tokens Metering & Billing Makes token usage enforceable, and restricts what counts to OpenAI prompt tokens. Its key is what you set as the plugin’s feature.key.
Plan with a token entitlement, published Metering & Billing Defines the allowance in tokens, on a rate card that references the feature.
Customer with a matching subject key Metering & Billing The entity whose access is enforced. Its usage_attribution.subject_keys must contain the Consumer’s subject key, or usage and enforcement won’t resolve to this customer.
Subscription to the plan Metering & Billing Materializes the entitlement onto the customer. Until the subscription starts, the customer has no entitlement to enforce.
Metering & Billing plugin, with the Ingest token example-service Reports token usage events. Nothing counts against the allowance unless usage is reported, and allow_status_codes keeps failed responses out of that usage.
Entitlement Enforcement plugin, with the Entitlement Access token example-route Reads the customer’s remaining token allowance and blocks the request once it’s spent.

The following diagram shows how those pieces relate:

 
flowchart TB
  client(["Client (API key)"])
  subgraph gateway["Kong Gateway"]
        route["example-route"]
        service["example-service"]
        consumer1["Consumer (Kong Air)"]
        enforcement["Entitlement Enforcement plugin"]
        proxy["AI Proxy plugin"]
        metering["Metering & Billing plugin"]
  end
  redis[("Redis
enforcement cache")] openai(["OpenAI (gpt-4o)"]) subgraph mb["Konnect Metering & Billing"] events["Events API"] access["Entitlement Access API"] meter["Meter (LLM tokens)"] subgraph plan["Token Plan"] feature2["Metered feature + entitlement (token limit)"] end subgraph subscription["Token Subscription"] customer1["Customer (Kong Air)"] end end client -->|chat request| route route -.->|enforced by| enforcement route -->|allowed| service service -.->|proxied by| proxy proxy -->|prompt| openai service -.->|metered by| metering client -.->|authenticates as| consumer1 consumer1 -->|subject key| customer1 metering -->|token usage events, Ingest token| events events -->|aggregated by| meter meter -->|referenced by| feature2 subscription -->|activates| plan enforcement -->|queries, Entitlement Access token| access access -->|reads entitlement| customer1 enforcement <-->|cached state| redis feature2 -.->|feature.key| enforcement

Create a Consumer

Before you configure Metering & Billing, set up a Consumer (kong-air in this example). Consumers let you identify the client that’s interacting with Kong Gateway. Later in this guide, you’ll map this Consumer to a customer in Metering & Billing.

The Entitlement Enforcement plugin identifies the customer from the request’s Consumer and sends the subject key consumer:<consumer-id> to the Entitlement Access API. To keep that subject key predictable, this guide sets an explicit id on the Consumer so you can reference it directly when you create the customer.

You’re going to use key authentication in this tutorial, so the Consumer needs an API key to access any Kong Gateway Services.

echo '
_format_version: "3.0"
consumers:
  - id: a3d1f5e2-1b2c-4d3e-9f80-000000000001
    username: kong-air
    keyauth_credentials:
    - key: air-key
' | deck gateway apply -

Enable authentication

Authentication lets you identify a Consumer so you can enforce their entitlements as a customer. This example uses the Key Authentication plugin, but you can use any authentication plugin that you prefer.

Enable the plugin globally, which means it applies to all Kong Gateway Services and Routes:

echo '
_format_version: "3.0"
plugins:
  - name: key-auth
    config:
      key_names:
      - apikey
' | deck gateway apply -

Configure the AI Proxy plugin

To set up AI Proxy with OpenAI, specify the model and set the appropriate authentication header. You must also enable log_payloads and log_statistics, because the token counts that Metering & Billing meters come from the AI Proxy statistics:

echo '
_format_version: "3.0"
plugins:
  - name: ai-proxy
    service: example-service
    config:
      route_type: llm/v1/chat
      auth:
        header_name: Authorization
        header_value: Bearer ${{ env "DECK_OPENAI_API_KEY" }}
      model:
        provider: openai
        name: gpt-4o
      logging:
        log_payloads: true
        log_statistics: true
' | deck gateway apply -

Create a meter

In Metering & Billing, meters track and record the consumption of a resource over time. Create a meter that sums the tokens reported for each LLM request. The command captures the new meter’s ID so you can reference it when you create the feature:

METER_ID=$(curl -X POST "https://us.api.konghq.com/v3/openmeter/meters" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN" \
     --json '{
       "name": "LLM Tokens",
       "key": "llm_tokens_total",
       "description": "Number of input and output tokens across models",
       "event_type": "kong.llm_request",
       "aggregation": "sum",
       "value_property": "$.tokens",
       "dimensions": {
         "model": "$.model",
         "provider": "$.provider",
         "type": "$.type"
       }
     }' | jq -r ".id"
)

Create a feature

Meters collect raw usage, but features make that usage enforceable. Create a metered feature that references the meter, and filter it so that only OpenAI prompt tokens count against the allowance.

The type dimension on a kong.llm_request event is either request or response. Filtering on request means the allowance is spent by what the customer sends, which is predictable, rather than by how long the model’s answer happens to be:

FEATURE_ID=$(curl -X POST "https://us.api.konghq.com/v3/openmeter/features" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN" \
     --json '{
       "name": "LLM token access",
       "key": "llm_token_access",
       "meter": {
         "id": "'$METER_ID'",
         "filters": {
           "provider": {
             "eq": "openai"
           },
           "type": {
             "eq": "request"
           }
         }
       }
     }' | jq -r ".id"
)

Enable the Metering & Billing plugin

The Entitlement Enforcement plugin enforces the token allowance, but something has to report the tokens that count against it. Configure the Metering & Billing plugin on example-service to emit an LLM token event for every request. Because both plugins use consumer lookup, the usage the Metering & Billing plugin reports is attributed to the same consumer:<consumer-id> subject that the Entitlement Enforcement plugin checks.

echo '
_format_version: "3.0"
plugins:
  - name: metering-and-billing
    service: example-service
    config:
      ingest_endpoint: https://us.api.konghq.com/v3/openmeter/events
      api_token: "${{ env "DECK_AUTH_TOKEN" }}"
      ssl_verify: true
      meter_api_requests: false
      meter_ai_token_usage: true
      subject:
        look_up_value_in: consumer
      allow_status_codes:
      - 200-299
' | deck gateway apply -

allow_status_codes limits metering to the response codes you list, which is 200-299 in this example. The two plugins work independently, so without allow_status_codes, the Metering & Billing plugin also reports token usage for failed requests, and a customer who already reached their usage limit keeps spending tokens on requests the Entitlement Enforcement plugin rejects.

For alternatives, see Excluding blocked requests from usage.

Create a plan with a token entitlement

Plans are the core building blocks of your product catalog. A plan is a collection of rate cards, where each rate card ties a feature to a price and an optional entitlement. The entitlement is what the Entitlement Enforcement plugin reads to decide access.

Create a token plan with one rate card that grants 100 prompt tokens per hour. The rate card uses a free price, because enforcement depends only on the entitlement, not on price:

PLAN_ID=$(curl -X POST "https://us.api.konghq.com/v3/openmeter/plans" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN" \
     --json '{
       "name": "Token",
       "key": "token",
       "currency": "USD",
       "billing_cadence": "P1M",
       "phases": [
         {
           "name": "Main",
           "key": "main",
           "rate_cards": [
             {
               "name": "LLM token access",
               "key": "llm_token_access",
               "feature": {
                 "id": "'$FEATURE_ID'"
               },
               "billing_cadence": "P1M",
               "price": {
                 "type": "free"
               },
               "entitlement": {
                 "type": "metered",
                 "is_soft_limit": false,
                 "usage_period": "PT1H",
                 "limit": 100
               }
             }
           ]
         }
       ]
     }' | jq -r ".id"
)

The limit is expressed in the meter’s own unit, which is tokens here. A short chat request like the one in the validation step below costs roughly 20 prompt tokens, so 100 tokens is spent after about five requests: long enough to see traffic pass, short enough to see it blocked. usage_period is set to PT1H because one hour is the shortest usage period the API accepts.

Publish the plan so you can subscribe customers to it:

curl -X POST "https://us.api.konghq.com/v3/openmeter/plans/$PLAN_ID/publish" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"

Create a customer and subscription

A customer is the entity whose access is enforced. The customer’s usage-attribution subject key must match the subject the Entitlement Enforcement plugin sends, which is consumer: followed by the Consumer ID you set earlier.

This guide creates the customer through the API so that the subject key can be set to the exact consumer:<consumer-id> value. In the Metering & Billing UI, the Include usage from dropdown only lists subjects that have already sent events, so a freshly created subject key isn’t selectable yet.

Create the customer with the matching subject key:

curl -X POST "https://us.api.konghq.com/v3/openmeter/customers" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN" \
     --json '{
       "name": "Kong Air",
       "key": "kong-air",
       "currency": "USD",
       "usage_attribution": {
         "subject_keys": [
           "consumer:a3d1f5e2-1b2c-4d3e-9f80-000000000001"
         ]
       }
     }'

Now subscribe the customer to the token plan. The subscription becomes active as soon as you create it, which materializes the token entitlement onto the customer:

curl -X POST "https://us.api.konghq.com/v3/openmeter/subscriptions" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN" \
     --json '{
       "customer": {
         "key": "kong-air"
       },
       "plan": {
         "key": "token"
       }
     }'

Enable the Entitlement Enforcement plugin

Enable the Entitlement Enforcement plugin on example-route. It points at the Entitlement Access API, enforces the llm_token_access feature, reads the customer from the request’s Consumer, and uses your Redis instance as its enforcement cache at config.redis. Note that api_token uses the Entitlement Access token, not the Ingest token the Metering & Billing plugin uses:

echo '
_format_version: "3.0"
plugins:
  - name: entitlement-enforcement
    route: example-route
    config:
      entitlement_access_endpoint: https://us.api.konghq.com/v3/openmeter/entitlement-access/query
      api_token: "${{ env "DECK_ENTITLEMENT_ACCESS_TOKEN" }}"
      ssl_verify: true
      feature:
        key: llm_token_access
      customer:
        look_up_value_in: consumer
      refresh_interval: 3
      redis:
        host: "${{ env "DECK_REDIS_HOST" }}"
        port: 6379
        ssl_verify: true
' | deck gateway apply -

This configuration relies on the plugin’s defaults for the rest of its behavior. For every available setting, see the Entitlement Enforcement plugin reference:

  • deny_unknown_customers defaults to true, so a request whose customer can’t be resolved is blocked.
  • fail_policy defaults to allow, so if the enforcement state can’t be retrieved, requests are allowed through.
  • response_codes defaults return 429 when a usage limit is reached, and 403 for feature or customer errors.

refresh_interval is set to 3 seconds here so the tutorial responds quickly. In production, use a higher interval to reduce load on the Entitlement Access API.

Validate

Send a chat request to your Route with the Kong Air API key:

curl -X POST "$KONNECT_PROXY_URL/anything" \
     --no-progress-meter --fail-with-body  \
     -H "Accept: application/json"\
     -H "Content-Type: application/json"\
     -H "apikey: air-key" \
     --json '{
       "messages": [
         {
           "role": "system",
           "content": "You are a mathematician"
         },
         {
           "role": "user",
           "content": "What is 1+1?"
         }
       ]
     }'

Now repeat the same request until the token allowance runs out:

for _ in {1..10}; do
  curl -i $KONNECT_PROXY_URL/anything \
       -H "apikey:air-key" \
       -H "Content-Type: application/json" \
       -d '{"messages":[{"role":"system","content":"You are a mathematician"},{"role":"user","content":"What is 1+1?"}]}'
  echo
  sleep 10
done

Expect the following progression:

  • Cold start: The first requests may return 403 with "Customer is not found by subject." The Entitlement Enforcement plugin hasn’t polled the customer’s state yet. It records the subject and fetches its entitlements on the next poll (every refresh_interval seconds), so retry for up to a minute.
  • Within the allowance: Once the state is loaded, requests return 200 and reach the model. Each one spends prompt tokens against the 100-token entitlement.
  • Allowance spent: Once the reported prompt tokens cross 100 in the usage period, the Entitlement Enforcement plugin blocks further requests with 429 and "Customer has reached usage limit for feature."

Blocking is not instant, which is why the verification loop sleeps between requests. Two delays stack up: Metering & Billing aggregates entitlement usage at one-minute granularity, so tokens you just spent take up to a minute to count, and the plugin then needs another refresh_interval seconds to poll the updated state. If you don’t see 429 right after the allowance should have run out, keep sending requests for another minute.

Query the Entitlement Access API directly

To confirm the customer’s remaining allowance independent of the plugin’s cache, you can call the same endpoint the plugin uses. This reports has_access per feature for the subject:

The Entitlement Access query endpoint is an internal, unstable API. It may change without notice. Use it for verification only, and do not use it for production integrations.

curl -X POST "https://us.api.konghq.com/v3/openmeter/entitlement-access/query" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN" \
     --json '{
       "customer": {
         "keys": [
           "consumer:a3d1f5e2-1b2c-4d3e-9f80-000000000001"
         ]
       },
       "feature": {
         "keys": [
           "llm_token_access"
         ]
       }
     }'

In the response, data[0].features.llm_token_access.has_access is true while the customer still has tokens left and false once the allowance is spent, with a reason.code of usage_limit_reached.

Enforce at the gateway, not downstream: Without the Entitlement Enforcement plugin, an exhausted entitlement only shows up in reporting and notifications, and the LLM request still reaches the model and still costs you. With the plugin on the Route, the request is rejected before it’s proxied. Customize the response_codes in the plugin configuration to control the status code and message returned for each denial reason.

Cleanup

If you created a new control plane and want to conserve your free trial credits or avoid unnecessary charges, delete the new control plane used in this tutorial.

Help us make these docs great!

Kong Developer docs are open source. If you find these useful and want to make them better, contribute today!