recovea
Install Recovea beside the model client you already use, and it 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, and flushed every 2,000 ms and whenever 100 are waiting. Nothing about delivery can reach your call path.
A 429 or a 503 means not now, and posting the same batch again is safe after either. The tap keeps the batch and the queue behind it, waits the answer's Retry-After (whole seconds or an HTTP date, never less than 200 ms and never more than 60 seconds a wait), and posts the same batch again. After a 503 each wait is at least double the one before, so a Retry-After of 5 gives waits of 5, 10, 20, 40 and then 60 seconds. With no readable Retry-After the waits are 200 ms, 800 ms, and then double. A batch gets at most eight attempts. Any other failure worth retrying (a 408, another 5xx, or no answer at all) is retried twice, after 200 ms and 800 ms.
A batch that is still not accepted is dropped, and the tap writes one line to stderr naming the last answer, the attempts and the events lost:
recovea tap: a batch was not accepted after 8 attempts (last answer 503); 100 events were not reported.
A 401 or a 410 writes its own line instead, below. The queue holds 1,000 events and drops the oldest first, counted through on_drop; the ones it drops while a batch waits out a 429 or a 503 are counted in a line of the same kind. On the way out of the process there is one more flush, a single attempt for each batch, a waiting one included, with a 1.5 second deadline, so the tap is never the reason a process is slow to exit.
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 or 503 can see up to eight requests where the pacing counts one, each sent no sooner than its Retry-After asked. Pacing the retries as well would only make the tap slower to obey 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.4
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.4.tar.gz | 88.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| recovea-0.1.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 134.6 kB
Release files / recovea-0.1.4.tar.gz
| Download URL | recovea-0.1.4.tar.gz |
|---|---|
| Size | 88.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e5facc39089ed22f0566c06954b09394ffb8a3929f4b08562a662a8026183359
|
|
BLAKE2b-256 checksum How to use checksums |
d75dd7cfd4788aa35352c4cc04ebdcb0d4ab74b988adf5f30e1ccd7e11e63bf4
|
| 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 26, 2026.
Transparency logRelease files / recovea-0.1.4-py3-none-any.whl
| Download URL | recovea-0.1.4-py3-none-any.whl |
|---|---|
| Size | 45.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
69d0616fe074b3b6e41fbfcbecceb674153975d388a51dd887372afcab810c9a
|
|
BLAKE2b-256 checksum How to use checksums |
360b7d74fc64505f78c57fe145bebed875c6f9df896674e4b8a8624e928d76d9
|
| 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 26, 2026.
Transparency log