Skip to main content
keeps is observability for apps that generate media with models. If your product calls fal, Replicate, OpenAI, or anything like them to make images, video, or audio, keeps records what happened to every generation and turns that stream into analytics you can act on. Think PostHog, but the event is a generation, not a pageview.

The mental model

There’s exactly one idea to hold onto:
You capture an asset — one generation — and append events to it as its life unfolds. The backend folds all those events into a single row per asset and powers your insights from there.
A generation has a story: it gets submitted, it queues, it finishes or fails, and then a human accepts it, regenerates, or downloads it. Each of those moments is an event you append to the same asset. You don’t manage state or stitch things together — you just keep calling capture() with the same asset_id, and correlation happens server-side.
import keeps

keeps.init(api_key="kp_live_…")   # once, where your app starts

# Capture the asset — the first call mints and returns an id.
asset_id = keeps.capture(model="fal-ai/flux/dev", status="submitted",
                        inputs={"prompt": "studio product shot"})

# … your real provider call runs here …

# Append more events to the SAME asset as its story unfolds.
keeps.capture(asset_id, status="completed",
             outputs={"url": "https://cdn.example.com/out.png"})
keeps.capture(asset_id, properties={"accepted": "true"})   # what the user did with it

keeps.flush()   # best-effort delivery; see /reliability
That’s the whole shape of it. Everything else in these docs is detail on top of capture an asset, append events.

What keeps is

Event-sourced

You emit small, immutable events. The backend merges them — latest value per field wins, and a finished status sticks — into one materialized row per asset. Events can even arrive out of order.

Fail-open

No keeps call ever raises into your code. capture() is a non-blocking in-memory append. If our ingest is slow or down, your app does not notice.

References, not bytes

Outputs are URLs and metadata — never raw media. keeps never touches, copies, or stores your generated pixels.

Your dimensions

Attach properties like film_id or prompt_version and break your insights down by them. They’re your tags, not our schema.
properties are stored as a single map per asset and folded per key (latest value per key wins, by ts) — so you can append partial property bags freely; a later event only touches the keys it sets and leaves the rest intact. Details in Core concepts.

What keeps is NOT

keeps is a neutral third party that sits beside your pipeline, never inside it. This is a hard line, not a default.
  • Never in the request path. keeps does not wrap, intercept, or proxy your provider calls. There is no auto-patching and there are no decorators — you instrument by calling capture() yourself, on your terms.
  • Never a proxy. Your generation traffic goes straight to fal / Replicate / OpenAI exactly as it does today. keeps only sees the events you choose to emit on the side.
  • Never ships media bytes. You send references (URLs) and metadata. Raw images, video, and audio stay where they are.
Because keeps is side-band and fail-open, the worst case for your app is that some analytics events are missed — never a slowed-down or broken generation.

How it works

1

Init once

Call keeps.init() at startup with your API key (or set KEEPS_API_KEY / KEEPS_INGEST_URL). Your project_id is derived server-side from that key — you never send it.
2

Capture per generation, append events

Call keeps.capture() when a generation starts; reuse the returned asset_id to append every later event (queued, completed, failed, cancelled, plus your own annotations like “accepted”) to that same asset.
3

Query insights

The backend folds your events into one row per asset and serves the dashboard — failure rates, latency, spend, retries, broken down by any properties you attached.
capture() buffers in memory and a background thread delivers on a timer. In short-lived, serverless, or worker processes that may exit before the next tick, call keeps.flush() before your work returns so nothing is lost. See Reliability.

Where to go next

Quickstart

From pip install to your first live asset in a few minutes.

Core concepts

Assets, events, correlation by asset_id, and how properties fold.