docs(api): declare the 429 headers instead of only describing them

The shared 429 named `Retry-After` and the `X-RateLimit` triple in prose, so a
generated client got no typed accessor for the field the same sentence tells
it to back off on. They are a `headers` block now: `Retry-After` and the cap
and remainder as integers, the reset instant as a date-time. The text also
says what a 429 from a ceiling that is not one of these buckets carries —
nothing, because there is no bucket to describe.

Two route descriptions still listed the pre-change header set: the ingest 429
named only `X-RateLimit-Remaining` / `X-RateLimit-Reset`, and the document
suggest 429 named only the reset instant beside `meta.retryAt`. Both are short
of the wire in exactly the direction this work exists to close.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 05:07:08 +02:00
1 parent 42100d94a7
commit cf9e8b8790
4 files changed
+57 -12

No files matched your search

+32 -9
View File
@@ -112,10 +112,32 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"429": &a5
description: Rate limit exceeded. The response carries `Retry-After` (whole seconds, rounded up — wait at least that
long before retrying), `X-RateLimit-Limit` (the bucket's cap), `X-RateLimit-Remaining` and
description: Rate limit exceeded. The response carries `Retry-After` (whole seconds, rounded up and never below 1 — wait
at least that long before retrying), `X-RateLimit-Limit` (the bucket's cap), `X-RateLimit-Remaining` and
`X-RateLimit-Reset` (the reset instant, ISO-8601, not epoch seconds). Back off on `Retry-After` rather than
guessing.
guessing. A 429 raised by a ceiling that is not one of these buckets — a daily AI budget, an upstream
provider's own refusal relayed onward — carries none of them, because there is no bucket to describe.
headers:
Retry-After:
description: Whole seconds to wait before retrying. Rounded up, never below 1.
schema:
type: integer
minimum: 1
X-RateLimit-Limit:
description: Requests the bucket allows per window.
schema:
type: integer
minimum: 1
X-RateLimit-Remaining:
description: Requests left in the current window; 0 on a refusal.
schema:
type: integer
minimum: 0
X-RateLimit-Reset:
description: Instant the current window rolls over, ISO-8601 — not epoch seconds.
schema:
type: string
format: date-time
content:
application/json:
schema:
@@ -15432,9 +15454,10 @@ paths:
returns a `{ title, kind, documentDate }` DRAFT for the edit form. AI-consent / rate / budget gated (shares the
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."
`Retry-After`, the `X-RateLimit-Limit` / `-Remaining` / `-Reset` triple, 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
@@ -19768,9 +19791,9 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"429":
description: More than 60 requests in a minute for this token — or, when no valid token was presented, from this client
IP. `meta.errorCode` = `ingest.rate_limited`, plus the standard `X-RateLimit-Remaining` /
`X-RateLimit-Reset` headers.
description: "More than 60 requests in a minute for this token — or, when no valid token was presented, from this client
IP. `meta.errorCode` = `ingest.rate_limited`, plus the standard rate-limit headers: `Retry-After` (whole
seconds, at least 1), `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`."
content:
application/json:
schema:
+1 -1
View File
@@ -793,7 +793,7 @@ export const inboundDocumentPaths: NonNullable<ZodOpenApiObject["paths"]> = {
tags: ["Documents"],
summary: "Suggest filing metadata (drafts only)",
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 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.',
'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 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 `Retry-After`, the `X-RateLimit-Limit` / `-Remaining` / `-Reset` triple, 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", required: true, schema: { type: "string" } },
],
+1 -1
View File
@@ -92,7 +92,7 @@ export const ingestPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
"429": {
description:
"More than 60 requests in a minute for this token — or, when no valid token was presented, from this client IP. `meta.errorCode` = `ingest.rate_limited`, plus the standard `X-RateLimit-Remaining` / `X-RateLimit-Reset` headers.",
"More than 60 requests in a minute for this token — or, when no valid token was presented, from this client IP. `meta.errorCode` = `ingest.rate_limited`, plus the standard rate-limit headers: `Retry-After` (whole seconds, at least 1), `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.",
content: { "application/json": { schema: errorEnvelope } },
},
},
+23 -1
View File
@@ -239,8 +239,30 @@ export const stdResponses = {
},
"429": {
description:
"Rate limit exceeded. The response carries `Retry-After` (whole seconds, rounded up — wait at least that long before retrying), `X-RateLimit-Limit` (the bucket's cap), `X-RateLimit-Remaining` and `X-RateLimit-Reset` (the reset instant, ISO-8601, not epoch seconds). Back off on `Retry-After` rather than guessing.",
"Rate limit exceeded. The response carries `Retry-After` (whole seconds, rounded up and never below 1 — wait at least that long before retrying), `X-RateLimit-Limit` (the bucket's cap), `X-RateLimit-Remaining` and `X-RateLimit-Reset` (the reset instant, ISO-8601, not epoch seconds). Back off on `Retry-After` rather than guessing. A 429 raised by a ceiling that is not one of these buckets — a daily AI budget, an upstream provider's own refusal relayed onward — carries none of them, because there is no bucket to describe.",
content: { "application/json": { schema: errorEnvelope } },
// Declared rather than only described: a generated client gets a typed
// accessor for the field the description tells it to back off on.
headers: {
"Retry-After": {
description:
"Whole seconds to wait before retrying. Rounded up, never below 1.",
schema: { type: "integer" as const, minimum: 1 },
},
"X-RateLimit-Limit": {
description: "Requests the bucket allows per window.",
schema: { type: "integer" as const, minimum: 1 },
},
"X-RateLimit-Remaining": {
description: "Requests left in the current window; 0 on a refusal.",
schema: { type: "integer" as const, minimum: 0 },
},
"X-RateLimit-Reset": {
description:
"Instant the current window rolls over, ISO-8601 — not epoch seconds.",
schema: { type: "string" as const, format: "date-time" },
},
},
},
};