@@ -246,6 +246,48 @@ class QuotaAwareValidator(OperationValidatorExtension):
246246` validate_recall ` or ` validate_reflect ` in synchronous HTTP request
247247paths — there is no queue to defer to and it will surface as a 500.
248248
249+ #### Pre-body-parse precheck
250+
251+ ` OperationValidatorExtension ` also exposes an optional ` precheck ` hook that runs
252+ ** before the request body is parsed** . FastAPI resolves ` Depends ` callables
253+ ahead of body deserialisation, so a ` precheck ` wired onto the billable POST
254+ routes (retain, recall, reflect, files retain, mental-model create + refresh)
255+ can short-circuit a request without ever materialising the JSON payload in
256+ memory. Use it for "should this caller be allowed to spend resources on this
257+ request at all" decisions — an exhausted balance, a revoked key, or a
258+ rate-limited tenant.
259+
260+ The hook receives a ` PrecheckContext ` , which carries only the cheap,
261+ already-resolved request state (the body is not yet available):
262+
263+ - ` operation ` : a short route identifier, e.g. ` "retain" ` , ` "recall" ` ,
264+ ` "reflect" ` , ` "files_retain" ` , ` "mental_model_create" ` ,
265+ ` "mental_model_refresh" ` .
266+ - ` bank_id ` : parsed from the URL path.
267+ - ` request_context ` : the authenticated ` RequestContext ` (tenant already
268+ resolved by the tenant extension).
269+
270+ ``` python
271+ from hindsight_api.extensions import (
272+ OperationValidatorExtension,
273+ PrecheckContext,
274+ ValidationResult,
275+ )
276+
277+
278+ class QuotaAwareValidator (OperationValidatorExtension ):
279+ async def precheck (self , ctx : PrecheckContext) -> ValidationResult:
280+ if await self ._balance_exhausted(ctx.bank_id):
281+ return ValidationResult.reject(" balance exhausted" )
282+ return ValidationResult.accept()
283+ ```
284+
285+ Keep ` precheck ` cheap (prefer cached lookups) and conservative on errors —
286+ prefer ` ValidationResult.accept() ` so a transient lookup failure does not turn
287+ into a request rejection. The default implementation accepts everything, so the
288+ hook is opt-in; the post-body ` validate_* ` hooks still run and remain the source
289+ of truth for the precise per-call cost / quota arithmetic.
290+
249291### Example: Custom MCPExtension
250292
251293``` python
0 commit comments