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

+8
View File
@@ -129,6 +129,14 @@
{
"name": "INSIGHTS_MAX_TOKENS",
"purpose": "Daily-briefing output-token ceiling. Default 2500, clamped to 500-8000."
},
{
"name": "INSIGHTS_RATE_LIMIT_PER_HOUR",
"purpose": "Per-user hourly ceiling for POST /api/insights/generate. Default 10; values below 1 fall back to the default."
},
{
"name": "DOCUMENT_AI_LIMIT_PER_HOUR",
"purpose": "Per-user hourly ceiling for the document-AI actions (read/index, extract, suggest, summarise share one bucket). Default 6, clamped to 1-1000."
}
]
},