Max Token Plugin
max-token is a guardrail plugin that rejects a request whose declared completion ceiling exceeds max_tokens, whose message count exceeds max_messages, or whose total input length exceeds max_input_length. It runs at before_request, so a rejected request never reaches a provider.
Factsโ
| Stage(s) | before_request only |
| Reported type | guardrail (plugin.TypeGuardrail) |
| Failure policy | Fails closed โ a plugin error (not a rejection) aborts the request as 500 |
| Denial status | 400 invalid_request_error (request_rejected) โ not 429 and not 402 |
| Multi-stage | No โ a single before_request entry is a complete, valid configuration |
| Agentic loops | Re-checked on every MCP tool-loop turn (RunBeforeLoopTurn), since guardrails are not exempted the way transform/logging/metrics plugins are โ each turn's completion ceiling and message count are enforced again |
| Content-reading | ContentAgnostic โ reads request content only when max_input_length is configured above 0; the other two checks are counts, not content |
| External dependencies | None โ pure in-process arithmetic over the already-decoded request, no state carried between requests |
Configurationโ
| Key | Type | Default | Required | Description |
|---|---|---|---|---|
max_tokens | int (or float64) | 4096 | No | Rejects when the request's effective completion ceiling โ EffectiveMaxTokens(), where max_completion_tokens supersedes max_tokens when both are set โ exceeds this. 0 disables the check. A request that sets neither field declares no ceiling and passes uncapped; see Gotchas. |
max_messages | int (or float64) | 100 | No | Rejects when len(Request.Messages) exceeds this. 0 disables the check. Skipped on projected surfaces (embeddings/images set Metadata["surface"]), so a 150-document embedding batch is not refused as 150 "messages". |
max_input_length | int (or float64) | 0 (off) | No | Rejects when the total character length of every message โ Content, or for a multipart message the sum of ContentParts[].Text and each ImageURL.URL โ exceeds this. 0 means no limit. Setting this above 0 makes the plugin read request content; see Gotchas. |
Config values are accepted only as int or float64 (YAML/JSON both decode plain numbers to one of these); a value of any other type โ a quoted string, for instance โ is silently ignored and the key keeps its default. There is no ferrogw validate check for this: max-token does not implement ConfigValidator.
Minimal configโ
plugins:
- name: max-token
type: guardrail
stage: before_request
enabled: true
config:
max_tokens: 4096
max_messages: 100
max_input_length: 0
Gotchasโ
- It's reject-only โ it never imposes a ceiling. A request that sets neither
max_tokensnormax_completion_tokensdeclares no ceiling, is not rejected, and runs to the provider's own default uncapped. This is deliberate: writing a ceiling the caller never asked for is transform behavior in a guardrail โ the injected value would change what is sent upstream, show up in the provider's bill, and truncate completions nobody configured. An operator who needs a hard bound sets it on the client or picks a model whose default is the bound they want. max_completion_tokenssupersedesmax_tokens. The plugin readsEffectiveMaxTokens(), neverRequest.MaxTokensdirectly, so a request cannot pair a smallmax_tokenswith a hugemax_completion_tokensand have the huge value smuggled past the cap โ a request settingmax_tokens: 5andmax_completion_tokens: 500000is rejected against the 500000 figure, not the 5.0disables each check โ opposite polarity torate-limit. Formax_tokens,max_messages, andmax_input_length,0means "this check is off." That's the reverse of therate-limitplugin, whererequests_per_second: 0is rejected as a load error rather than treated as "no limiting." Use0here freely; disablemax-tokenentirely withenabled: falseinstead if you want no checks at all.max_messagesis skipped on projected surfaces. Embeddings and image-generation requests reach the plugin through a projection that turns each input element into one user message, so a 150-document embedding batch arrives here as 150 "messages." The gateway setsMetadata["surface"]on those requests, andmax-tokenskips the message-count check when that key is present โ a conversation-turn ceiling isn't a statement about how many documents may be embedded at once.max_input_length, a size measure, still applies on projected surfaces.- Content-agnostic by default;
max_input_length > 0flips that. Withmax_input_lengthunset (or0),max-tokennever reads message content โmax_tokensis the caller's own declared number andmax_messagesis a list length, neither of which changes when the text inside a message can't be read. That matters for the/v1/*pass-through proxy: a surface that hands a guardrail an unreadable body (a multipart upload, an audio payload, token-ID input) has to treat a guardrail's vacuous approval as consent unless the guardrail says otherwise, so it refuses uninspectable pass-through bodies by default โ except when the only configuredbefore_requestguardrail declares it reads no content, whichmax-tokendoes (itsIgnoresRequestContent()returnstrue) as long asmax_input_lengthis0. Setmax_input_lengthabove0and the plugin becomes content-reading: it now measures the projected text, so an unreadable pass-through body would satisfy the cap at length zero โ a vacuous pass, not a real one โ and the gateway refuses those bodies instead of serving them once this key is set. - Multipart messages are measured from
ContentPartsalone.Contentalready holds the concatenated text parts for a decoded multipart message, somax_input_lengthsumsContentParts[].Textplus each part'sImageURL.URLlength rather than addingContenton top โ adding both would double-count the text while still ignoring the image payload, which for a base64 data URI is nearly the entire request. Executedoes not branch on stage. Nothing stops a config from registeringmax-tokenatafter_requestoron_errortoo, and the manager will call it there โExecutejust re-runs the same three checks againstpctx.Request, which is still populated in every stage. On a request that already clearedbefore_request, that second run is redundant (it re-approves what it already approved) rather than harmful, but it is not a documented pairing the waybudget's two stages are โ there's nothing for a later stage to add.