Skip to main content

Input Controls

Bot Detector

bot-detector flags automated traffic: many requests with the same client fingerprint in a short window, or a prompt that is almost identical to a recent one. It can warn or block.

The history lives in the gateway's memory. It starts empty when the gateway starts, and gateways do not share it. The similarity check compares a prompt with every recent prompt on the gateway, from any caller. Start with action: warn and look at what it flags before you block.

Configuration​

policy-config.yaml
pack:
  name: bot-detector-example
  version: 1.0.0
  enabled: true
policies:
  chain:
    - bot-detector
policy:
  bot-detector:
    fingerprint_fields:
      - user-agent
      - x-forwarded-for
      - authorization
    profile_window_seconds: 60
    similarity_threshold: 0.9
    max_requests_per_window: 5
    action: warn

Send the same prompt again​

On a test gateway, send the same prompt twice within 60 seconds with the same application token. The second prompt has a similarity of 1.0. With action: warn, it still reaches the provider, and its nested bot-detector result shows flagged: true and bot-detector.similarity_threshold_exceeded. The final verdict stays allow.

You can also see the behavior offline. With action: block, the second of two identical cases is blocked, because the cases of one test run share the gateway memory:

Add to policy-config.yaml, with action: block
testing:
  suites:
    - name: s
      cases:
        - name: first
          input: {messages: [{role: user, content: "What are your opening hours today?"}]}
          expected: {verdict: allow, reason_code: ok}
        - name: second
          input: {messages: [{role: user, content: "What are your opening hours today?"}]}
          expected: {verdict: block, reason_code: bot-detector.similarity_threshold_exceeded}

Before you switch to block in production, check how often normal retries and repeated user questions are flagged.

Fields​

FieldTypeDefaultDescription
fingerprint_fieldslist of stringsuser-agent, x-forwarded-for, authorizationHeader names, and the values body and model. Case does not matter.
profile_window_secondsinteger60How long events are kept. Values below 1 count as 1.
similarity_thresholdnumber0.9Jaccard similarity to one earlier prompt that flags a request. Lint accepts 0.0 to 1.0.
max_requests_per_windowinteger5Number of earlier requests with the same fingerprint that flags a request. Lint rejects 0.
actionwarn or blockwarnwarn keeps a flagged request allowed. block blocks it.

How it works​

  1. The gateway joins the text of all request messages.
  2. It builds a SHA-256 fingerprint from fingerprint_fields: body adds the text, model adds the model, and a header name adds the header and its value if present.
  3. It drops stored events older than profile_window_seconds.
  4. It counts the stored events with the same fingerprint: duplicate_count.
  5. It splits the text into lowercase alphanumeric tokens and takes the highest Jaccard similarity to any stored text: similarity.
  6. duplicate_count at or above max_requests_per_window flags the request with bot-detector.duplicate_fingerprint_rate.
  7. similarity at or above similarity_threshold flags it with bot-detector.similarity_threshold_exceeded.
  8. The request is added to the window.
  9. A flagged request is blocked with action: block. Otherwise it is allowed.

The details contain fingerprint, duplicate_count, similarity, and flagged.

Phase and verdicts​

ItemValue
Phaseinput
Request familiesAll
Verdictsallow, block
Reason codesbot-detector.ok, bot-detector.duplicate_fingerprint_rate, bot-detector.similarity_threshold_exceeded

With action: warn, the nested result has verdict allow and the flagged reason code. The decision reason can stay ok.

Examples​

Fingerprint by client token, text, and model, and block flagged requests:

Inside policy-config.yaml
policy:
  bot-detector:
    fingerprint_fields:
      - authorization
      - body
      - model
    action: block

Limits and failure behavior​

  • The window is one in-memory list for the whole gateway, not one per agent, token, or tenant.
  • It starts empty on each start, and gateways do not share it.
  • It keeps at most 10,000 events and drops the oldest ones at the limit.
  • With max_requests_per_window: 5, the sixth request with the same fingerprint is the first one flagged.
  • A header that the request does not have adds nothing to the fingerprint. Two requests without any configured header have the same fingerprint.
  • Lint rejects an unknown action and unknown fields. A block with a wrong type that bypasses lint runs with all defaults.
  • A blocked request never reaches the provider.

Next steps​