Skip to main content

recovea

One line wraps the model client you already use, and reports the envelope of each call. Never a prompt, never a completion.

pip install recovea
from openai import OpenAI
from recovea import tap

client = tap(OpenAI(), tag="matter-4821")
# use client exactly as before
from anthropic import Anthropic
from recovea import tap

client = tap(Anthropic(), tag="matter-4821")

Your calls keep going straight to your provider. Nothing routes through Recovea, and no provider key is ever asked for. The wrapper reads the response your provider already returned, builds one small object from it, and posts that object on the side, after your call has returned. It never delays your call, never changes what your call returns, and never raises into your code.

Supported call sites: OpenAI chat.completions.create and responses.create, Anthropic messages.create, sync and async clients, streaming and non streaming.

What the tap sends

One event per call, recovea-tap-v1. This is the whole object. The published schema is at https://recovea.ai/docs/passive-tap

field type where it comes from
schema string always recovea-tap-v1
provider openai anthropic google xai mistral other your client's base URL, then its class name
model string, 1 to 128 chars the model your provider says it served
request_id string, 1 to 128 chars the provider's response id or request id header, otherwise a generated UUID
input_tokens integer the provider's usage field
output_tokens integer the provider's usage field
cached_tokens integer the provider's usage field (cache reads)
stop_reason stop length tool_calls content_filter error other the provider's stop or finish reason, mapped to this list
tool_calls integer how many tool calls the response contained. The count only
status integer 100 to 599 the HTTP status of the call. 599 when the call failed with no HTTP response, such as a transport failure or a timeout; 200 is assumed for a successful response the SDK returns without a status
retries integer always 0 in v1 (no SDK exposes a count)
latency_ms integer measured by the wrapper, start of call to end of response
rate_limit object or null the provider's rate limit response headers. Null unless you pass rate_limit_headers=True
timestamp RFC 3339 with timezone when the event was built
tag string, 1 to 64 chars of [A-Za-z0-9._:-] the tag you passed to tap(). The only free text field

What the tap never sends

Prompts. Messages. System prompts. Completions. Text, image, audio or file content of any kind. Tool definitions. Tool arguments. Tool results. Embeddings. Your provider key. Your Recovea key is a header, never a field.

The allowlist above is enforced twice: the SDK builds the object field by field and validates it before it leaves your process, and the server re-checks every field on arrival and rejects a batch that carries anything else, without logging it. A field that is not on the published page is not in the object.

See exactly what is sent

RECOVEA_TAP_INSPECT=1 (or inspect=True) prints every batch to stderr as the exact JSON body the endpoint would receive, and transmits nothing.

It needs no tap key, so you can read the fifteen fields on your own machine before any of them leave it.

One line on stderr says the mode is on; unset the variable and delivery works exactly as it did before.

If a tap key is configured as well, inspect mode still wins: nothing is transmitted, so a Baseline you are watching stays on "waiting for your first event" until you turn inspect mode off. That stderr line says so in as many words when both are set.

Options

client = tap(
    OpenAI(),
    tag="matter-4821",  # required, 1 to 64 chars of [A-Za-z0-9._:-]
    key=None,  # default: RECOVEA_TAP_KEY
    endpoint=None,  # default: RECOVEA_TAP_ENDPOINT
    flush_interval_ms=2000,
    max_queue=1000,  # oldest dropped first
    on_drop=None,  # called with a count only, never the event
    rate_limit_headers=False,  # see below
    inspect=None,  # default: RECOVEA_TAP_INSPECT. A bool overrides it both ways
)

One tag per wrapped client. Wrap a second client if you need a second tag. An invalid tag or provider raises at tap() time, never at request time.

Delivery is in memory and best effort: events are queued, posted in batches of up to 100 by a background thread, flushed every 2,000 ms and whenever 100 are waiting, retried twice on failure and then dropped. On the way out of the process there is one more flush, with a single attempt and a 1.5 second deadline, so the tap is never the reason a process is slow to exit. Nothing about delivery can reach your call path.

How fast it catches up. One wrapped client posts at most four batches a second on a clean path: the drain leaves at least 250 ms between the starts of two posts, so a burst is delivered steadily rather than as fast as the network allows. A single batch waits for nothing unless it follows another within that 250 ms. Two things are outside that number and are meant to be. A post that is retried makes its extra attempts inside the same gap, so an endpoint answering 429 can see up to three requests where the pacing counts one — that is how Retry-After is honoured, and slowing it down would make the tap worse at obeying it. And the pacing is per wrapped client, so if you wrap a second client for a second tag, the two pace themselves independently. The ceiling that follows is about four hundred events a second per wrapped client, at the 100-event batch. Above that the queue fills to max_queue (1,000) and the oldest events are dropped and counted through on_drop — the tap sheds load rather than growing without limit, and it never slows your own calls down.

If the endpoint answers 410, or answers 401 three times in a row without a success in between, the tap stops sending — the queue is dropped, nothing more is posted, and one line to stderr says why — one line per reason, for the whole process, however many clients you have wrapped, so two clients that stop for the same reason say it once and a later, different ending still gets said — while your client goes on working exactly as it did. A 410 says why: when your Baseline has closed, the line is the server's own sentence; for any other ending the tap prints a line of its own.

A Baseline that has closed is the one that comes back. Leave the tap line exactly where it is and start a plan: reporting starts again by itself within about a minute of your application's next call, with no restart, no second line, and nothing to re-integrate. Only events from then on are sent — whatever was queued when the Baseline closed was dropped and counted through on_drop, and it is not replayed. While it is waiting, the tap tries the endpoint again at most once a minute, and only when your own traffic gives it something to send. A plan that ends returns the same closed answer, and starting again resumes reporting the same way.

An organization whose data was erased does not come back. Its key is not accepted any more, so the tap sees 401 three times and stops for the rest of the process. A 410 whose reason cannot be read is treated as an ending too: one neutral line that tells nobody to remove anything, and a pointer to the page in Recovea.

If no key is configured the client still works and the tap reports nothing, with one line to stderr when you construct it — unless inspect mode is on, which needs no key and prints every batch instead of reporting nothing (see "See exactly what is sent" above).

rate_limit_headers=True reads the provider's rate limit headers through the SDK's .with_raw_response path, which means the wrapper calls that method and hands you back its .parse() result. That is the same object the plain call returns, but it is a different code path inside the provider SDK, so it is off by default and rate_limit is null until you turn it on. The JavaScript tap defaults it off too, so the same call produces the same event shape in both SDKs.

Environment

variable meaning
RECOVEA_TAP_KEY your tap key, shown once in the product. Sent as Authorization: Bearer <key>
RECOVEA_TAP_ENDPOINT where events are posted. Default https://platform-api.recovea.ai/tap/v1/events
RECOVEA_TAP_INSPECT set to 1, true, yes or on (case and surrounding spaces ignored) to print every batch to stderr and send nothing. Any other value, and unset, leaves it off. No key needed

Requirements

Python 3.9 or newer. No dependencies.

Learn more: https://recovea.ai


(c) 2026 Recovea, Inc. MIT License.

Release files for recovea 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for recovea 0.1.3
File Size Uploaded
recovea-0.1.3.tar.gz 73.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for recovea 0.1.3
File Interpreter ABI Platform
recovea-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 111.9 kB

Release files / recovea-0.1.3.tar.gz

Download URL recovea-0.1.3.tar.gz
Size 73.7 kB
Tags Source
SHA-256 checksum
How to use checksums
639ea57ebdc6aefba5d7950ca66e81e734c61c0e2b8e6da945cebc1588deb969
BLAKE2b-256 checksum
How to use checksums
7e71196401153048dfd164d8d5deaa90f74b3c84b58b0610727af2badfb53776
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release files / recovea-0.1.3-py3-none-any.whl

Download URL recovea-0.1.3-py3-none-any.whl
Size 38.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
350ef7769ba0adeed744000a10088f1d0e3e8f404aa63879562f207885088d6e
BLAKE2b-256 checksum
How to use checksums
479cb3f0dcdb7fa74c7c018a864994939d82f41cc7d28fb3e3a8a53d6d78df30
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 release files

0.1.2

2 release files

0.0.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page