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)
| File | Size | Uploaded | |
|---|---|---|---|
| recovea-0.1.3.tar.gz | 73.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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