fal_client,
replicate, or openai. keeps never sits in your request path. You instrument a
provider call by calling capture() around it yourself —
once when you submit, once when it finishes.
If you came here looking for
keeps.instrument(), an auto-patching hook, or a
with keeps.generation(...) span — none of those exist. The entire surface is
init, capture, context, session, flush, shutdown, stats. Manual
capture() is the integration. See the API reference.The pattern
Every generation is the same three-beat shape, no matter the provider:Capture the submission
Call
capture(model=..., inputs=..., status="submitted") with no
asset_id. keeps mints a uuid7 and returns it. Hold onto that string.Run your real provider call — untouched
Call fal / Replicate / OpenAI exactly as you would without keeps. Same
arguments, same return value, same exceptions. keeps is side-band: it never
sees or alters this call.
Side-band, always
Write the wrapping so that if you deleted the twocapture() lines, your
provider code would be byte-for-byte identical. keeps captures references to your
inputs and outputs — it never changes the arguments you pass the provider, the
value the provider returns, or the exception it raises. outputs are URLs and
metadata only, never raw bytes (see Privacy & safety).
Example A — a synchronous call
A blocking fal/OpenAI-style call where submission and completion live in the same function. Mint on the way in, finish on the way out.Example B — an async / polling job
When submission and completion happen in different places — an HTTP handler submits, a worker or webhook finishes — the only thing you carry between them is theasset_id. Store it next to the provider’s request id (in your
DB, your queue payload, wherever you already track the job) and pass it back.
Out-of-order is fine. If the completion event lands before the submission
event, ingestion still merges them: the fold orders events by their
ts,
not by arrival time, and a late-arriving submitted can never flip a
finished asset back to in-flight. You don’t have to guarantee ordering —
only that both events share the asset_id.Example C — the failure path
Wrap the real call so its exception propagates unchanged, and record the provider’s error verbatim before re-raising. keeps ships errors as-is — normalization into a taxonomy happens server-side, so don’t pre-massage them.code/message/status fields. error accepts a plain dict:
Attaching your own dimensions
properties are GROUP BY tags (stored server-side as a string map) — film_id,
shot_id, step, anything you want to break generations down by later. Two
things to remember: they’re tags, not schema columns, and the server-side fold
is per key — each capture() updates only the keys it carries (latest value
per key wins), so you can append a partial bag on a later event and the keys you
set earlier stay intact.
keeps.context() instead of repeating the literal — they’re
merged into each capture() automatically (per-call values still win per key):
context() only tags — it does not flush. See
Reliability.
Serverless & short-lived workers: flush
capture() is a non-blocking append to an in-memory buffer; a daemon thread
delivers it about once a second. If your process can be frozen or killed before
that tick — a serverless function, a queue worker that exits, a forked job —
call keeps.flush() before the unit of work returns, or the buffered (and
in-flight) events are lost. Delivery is best-effort, not at-least-once.
Reliability model
Buffering, the flush daemon, overflow drops, retry/backoff, and exactly when
you need an explicit
flush() — all in one place.Replicate, in one block
Same three beats. The model ref goes inmodel; the input map goes in
inputs; outputs come back as URLs.
For a split create/poll flow with
replicate.predictions.create(...) then
.get(id) in another process, follow Example B:
persist the asset_id next to the prediction id and reuse it when you finish.Checklist
One asset_id per generation
One asset_id per generation
Let the first
capture() mint it; pass it back on every later call. Never
mint a second id for the same generation.The provider call is untouched
The provider call is untouched
No keeps argument changes what you send the provider or what it returns.
Removing the
capture() lines must leave provider behavior identical.Outputs are references
Outputs are references
URLs and metadata only — never raw image/audio/video bytes.
Errors are verbatim
Errors are verbatim
Capture the provider’s error as-is and re-raise the original exception.
Normalization is server-side.
Properties merge per key
Properties merge per key
Each
capture() updates only the keys it carries (latest per key wins), so
append partial bags freely — earlier keys persist.Flush before short-lived processes exit
Flush before short-lived processes exit
Serverless / workers / forks:
keeps.flush() before returning.