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
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: warnSend 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:
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
| Field | Type | Default | Description |
|---|---|---|---|
fingerprint_fields | list of strings | user-agent, x-forwarded-for, authorization | Header names, and the values body and model. Case does not matter. |
profile_window_seconds | integer | 60 | How long events are kept. Values below 1 count as 1. |
similarity_threshold | number | 0.9 | Jaccard similarity to one earlier prompt that flags a request. Lint accepts 0.0 to 1.0. |
max_requests_per_window | integer | 5 | Number of earlier requests with the same fingerprint that flags a request. Lint rejects 0. |
action | warn or block | warn | warn keeps a flagged request allowed. block blocks it. |
How it works
- The gateway joins the text of all request messages.
- It builds a SHA-256 fingerprint from
fingerprint_fields:bodyadds the text,modeladds the model, and a header name adds the header and its value if present. - It drops stored events older than
profile_window_seconds. - It counts the stored events with the same fingerprint:
duplicate_count. - It splits the text into lowercase alphanumeric tokens and takes the highest Jaccard similarity to any stored text:
similarity. duplicate_countat or abovemax_requests_per_windowflags the request withbot-detector.duplicate_fingerprint_rate.similarityat or abovesimilarity_thresholdflags it withbot-detector.similarity_threshold_exceeded.- The request is added to the window.
- A flagged request is blocked with
action: block. Otherwise it is allowed.
The details contain fingerprint, duplicate_count, similarity, and flagged.
Phase and verdicts
| Item | Value |
|---|---|
| Phase | input |
| Request families | All |
| Verdicts | allow, block |
| Reason codes | bot-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:
policy:
bot-detector:
fingerprint_fields:
- authorization
- body
- model
action: blockLimits 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
actionand unknown fields. A block with a wrong type that bypasses lint runs with all defaults. - A blocked request never reaches the provider.
Next steps
- Rate Limit Configuration: limit requests per consumer.
- Security and Network Configuration: IP allowlists and CORS.
- Investigate a Blocked Request: find the decision event of a flagged request.