feat(extensions): add OperationValidator.precheck pre-body-parse hook#1548
Merged
Conversation
Add an optional ``precheck`` method to ``OperationValidatorExtension`` that extensions can override to gate a request *before* its body is read off the wire. Wire it as a FastAPI ``Depends`` ahead of the body parameter on the billable POST routes (retain, recall, reflect, file retain, mental-model create, mental-model refresh) so a rejecting precheck short-circuits the request without ever materialising the JSON payload in memory. The post-body-parse ``validate_retain`` / ``validate_recall`` / ``validate_reflect`` hooks are unchanged and remain the source of truth for precise per-call cost and quota arithmetic. ``precheck`` is intentionally a cheap, side-effect-free check — its sole purpose is to let an extension short-circuit work that would otherwise allocate the request body unnecessarily (e.g. a quota-exhausted caller submitting many large bodies). Why before body parse: FastAPI resolves dependencies before deserialising the route's body parameter. A validator that runs only after parse — i.e. inside the route handler's body — sees the already-materialised request, which is the wrong layer for "this caller should not be allowed to spend resources on this request at all" decisions. Wiring as ``Depends`` puts the gate at the right layer with a one-line change per route. Verified: - FastAPI 0.125.0 resolves ``Depends`` raising ``HTTPException`` before Pydantic deserialises the body, regardless of declaration order. A reproducer using a ``model_validator(mode='before')`` recorder confirms zero body-parse calls on the rejection path. - The new ``PrecheckContext`` carries only operation name + bank_id + request_context (already-resolved tenant). No body access — by design. - Default ``precheck`` returns ``ValidationResult.accept()``; existing validators are unaffected. Tests: +7 unit tests covering the default no-op, the FastAPI Depends wiring, accept/reject paths, status-code/reason propagation, and explicit "body never parsed on rejection" assertions for retain / recall / reflect plus a "GET routes are unaffected" guard. All passing.
nicoloboschi
approved these changes
May 25, 2026
r266-tech
added a commit
to r266-tech/hindsight
that referenced
this pull request
May 25, 2026
r0gig0r
added a commit
to r0gig0r/hindsight
that referenced
this pull request
May 26, 2026
Notable upstream additions pulled in: - feat(api): clear endpoint for mental model content (vectorize-io#1706) - feat(api): per-operation LLM concurrency caps (vectorize-io#1738) - feat(typescript-client): concrete generated types (replace Promise<any>) - feat(reranker): Alibaba Qwen3-Rerank support (vectorize-io#1501) - feat: opencode-go LLM provider (vectorize-io#1652) - feat(extensions): OperationValidator.precheck pre-body-parse hook (vectorize-io#1548) - feat(right-agent): new Right Agent integration (vectorize-io#1599) - fix(ollama): ollama-cloud provider + native API auth (vectorize-io#1734) - fix(reflect): hide disabled tools from agent system prompt (vectorize-io#1740) - fix(retain): split oversized single items in batch retain (vectorize-io#1736) - fix: escape literal braces in user-supplied prompt fields (vectorize-io#1728) - fix(mental-models): full refresh pending delta baselines (vectorize-io#1684) - fix(api): lazy load reflect tiktoken encoding (vectorize-io#1654) - fix(api): reject blank retain content (vectorize-io#1685) - fix(api): auto-refresh openai-codex OAuth access_token (vectorize-io#1637) - fix(api): gzip middleware for graph payloads (vectorize-io#1731) - fix(reranker): detect pre-normalized scores; rank-based fallback (vectorize-io#1512) Conflicts: only package-lock.json files (took upstream, npm install verified) Fork customizations verified intact (all 14 checks): - duplicate_checker_fn streaming Phase 1.5 in orchestrator - FallbackLLMProvider + CircuitBreaker (fallback_llm.py) - Single-fact consolidation mode (is_fallback_active routing) - recallExp + Jaccard dedup + compact memory formatter (plugin) - Codex 5.1-codex-mini reasoning guard - Infinity reranker /models fallback in cross_encoder.py - diversity.py + deduplication.py fork-only modules retained Tests: - openclaw vitest: 267/267 pass - ruff: clean - tsc --noEmit: clean - pytest: pre-existing env-config flakes (need HINDSIGHT_API_LLM_API_KEY); upstream commit 90cb145 acknowledged as pre-existing CI flakes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
precheck()method toOperationValidatorthat lets an extension gate a request before its body is read.Dependsahead of the body parameter on the billable POST routes (retain, recall, reflect, files retain, mental-model create + refresh).Test plan
tests/test_extensions.pycovering: default no-op accept, rejection short-circuits before body parse (asserted via Pydanticmodel_validator(mode='before')recorder — body never deserialized on reject), rejection returns the validator's status code + reason, GET routes are unaffected, accept lets request through to body parse, body-parse-skipped behaviour holds for retain/recall/reflect.uv run pytest tests/test_extensions.py→ 15 passed). The 21 errors visible in the same run are pre-existing environment issues (missing LLM API key, alembic state) onmainitself — verified independent of this PR.