feat(documents): make the document-AI rate limit an operator setting with honest accounting

The document-AI actions (read/index, extract, suggest, summarise) shared a
hard-coded 6/hour bucket that lied twice: the 429 said "try again in a few
minutes" while the window is an hour, and several failure paths burned a
slot before any provider was dispatched.

- DOCUMENT_AI_LIMIT_PER_HOUR configures the shared per-user ceiling
  (default 6, clamped 1-1000), wired end to end: compose environment
  whitelist, .env.production.example, env manifest, operator docs.
- INSIGHTS_RATE_LIMIT_PER_HOUR joins the compose whitelist and manifest;
  it was documented but never listed, so an operator value could not reach
  the container.
- The 429 mirrors the bucket's reset instant into meta.retryAt (plus the
  existing X-RateLimit-Reset header); the client derives "try again in
  about N minutes" from it in all six locales, with a plural-aware key.
- A slot is consumed only when a request actually reaches the provider (or
  the local index/echo work): malformed bodies, unreadable originals,
  refused consent, and exhausted daily budgets refund the charged slot via
  a window-scoped refundRateLimit primitive. Consistent across all four
  routes sharing the bucket.
This commit is contained in:
Marc-André Bombeck committed 2026-08-13 00:33:14 +02:00
1 parent 592d1e9aa8
commit 8e16df6894
26 files changed
+560 -160

No files matched your search

+5 -3
View File
@@ -12495,9 +12495,11 @@ paths:
description: "Optional AI assist on an already-stored document. Runs ONE provider call over the stored original (VISION,
empty body) or browser-OCR'd text (TEXT, `application/json` `{ mode: \"text\", text }`, opt-in local OCR) and
returns a `{ title, kind, documentDate }` DRAFT for the edit form. AI-consent / rate / budget gated (shares the
6/hour extract bucket). WRITES NOTHING — never stages facts, never flips status; the user reviews and saves. 422
(`documents.inbound.providerUnsupported`) with no provider configured. Never interprets or diagnoses; the title
is a neutral filing label."
per-user document-AI bucket with extract/summary/index — default 6/hour, operator-tunable via
`DOCUMENT_AI_LIMIT_PER_HOUR`; a slot is only consumed when the request reaches the provider, and the 429 carries
the reset instant in `X-RateLimit-Reset` and `meta.retryAt`). WRITES NOTHING — never stages facts, never flips
status; the user reviews and saves. 422 (`documents.inbound.providerUnsupported`) with no provider configured.
Never interprets or diagnoses; the title is a neutral filing label."
parameters:
- name: id
in: path