Metadata-Version: 2.4
Name: saive-client
Version: 0.1.0
Summary: Zero-friction LLM footprint reporting for saive (saive.tech) — patches OpenAI, Anthropic and Google GenAI clients to report usage asynchronously. Never sends prompt or completion content.
Project-URL: Homepage, https://saive.tech
Project-URL: Repository, https://github.com/katerberg-justus/saive
License: MIT
Requires-Python: >=3.9
Requires-Dist: httpx>=0.24
Provides-Extra: dev
Requires-Dist: anthropic>=0.30; extra == 'dev'
Requires-Dist: google-genai>=0.1; extra == 'dev'
Requires-Dist: openai>=1.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# saive

Zero-friction LLM footprint reporting. Patches the OpenAI, Anthropic and Google GenAI Python
clients to report token usage to [saive](https://saive.tech) asynchronously, in the
background, without ever seeing your prompts or completions.

```
pip install saive-client
```

```python
import saive
saive.init()  # reads SAIVE_API_KEY from the environment

from openai import OpenAI
client = OpenAI()  # unchanged from here on — sync, async, and streaming all work
```

That's the entire integration. If `SAIVE_API_KEY` isn't set, `saive.init()` is a complete
no-op — safe to call unconditionally, including from library code that doesn't know whether
its caller uses saive.

## What this does and does not do

- **Not a proxy.** Your calls to OpenAI/Anthropic/Google go straight to them, exactly as
  before. saive never sits in the request path, never sees your API keys for those
  providers, and a saive outage is invisible to your application — every failure path here
  degrades to a silent no-op plus one warning-level log line, never an exception.
- **Never sends prompt or completion content.** Only the model string, input/output/
  reasoning token counts, a timestamp, and latency are extracted from each response's
  `usage` object. Nothing else is read from your requests or responses.
- **Never blocks.** Usage is buffered in memory and flushed from a background thread on a
  size/time trigger, plus once more at interpreter exit. Buffered events are dropped (not
  retried indefinitely, never written to disk) if saive can't be reached — an acceptable
  loss for a component whose numbers are estimates with wide uncertainty bands already.

## Configuration

All via environment variables — there is no config object to construct.

| Variable | Default | |
|---|---|---|
| `SAIVE_API_KEY` | *(none)* | Required. Without it, `init()` patches nothing. |
| `SAIVE_DISABLED` | *(unset)* | Set to `1` to force saive off regardless of `SAIVE_API_KEY`. |
| `SAIVE_BASE_URL` | production saive endpoint | Override for self-hosted saive deployments. |
| `SAIVE_FLUSH_INTERVAL_SECONDS` | `5` | How often the background thread flushes. |
| `SAIVE_MAX_BUFFER_SIZE` | `500` | Max events buffered before new ones are dropped. |
| `SAIVE_MAX_RETRIES` | `2` | Retries (with backoff) per flush before discarding the batch. |

## Tagging

```python
with saive.tags(feature="summarise", customer_id="acme"):
    client.chat.completions.create(...)
```

Tags apply to every tracked call made inside the `with` block, on whichever thread or
`asyncio` task is inside it (tags use `contextvars`, so they follow async control flow
correctly — a plain thread-local would not). Nested `tags()` blocks merge, innermost wins on
key collision. These merge server-side with whatever `default_tags` your saive API key
carries; the event's own tags win on collision there too.

## Serverless

Call `saive.flush()` at the end of your handler, before the process might be frozen or
killed:

```python
def handler(event, context):
    ...
    saive.flush()
```

## Streaming

Streaming is supported for all three providers. Usage typically only arrives in (or after)
the final chunk, so if you stop consuming a stream early, saive will not have data to
report for that call.

**OpenAI specifically:** the API only includes usage in a streamed response if you pass
`stream_options={"include_usage": True}` to `create()`. saive does not add this for you
(saive does not modify your outgoing requests, ever) — without it, saive has no token counts
to report for that streamed call and will silently skip it.

## Supported call surfaces (v1)

- OpenAI: `client.chat.completions.create()` (sync, async, streaming). The newer
  `client.responses.create()` API is not yet instrumented.
- Anthropic: `client.messages.create()` (sync, async, streaming). The `client.messages.stream()`
  context-manager helper is a separate code path and is not yet instrumented — use
  `create(stream=True)` if you want saive to see it.
- Google GenAI: `client.models.generate_content()` and `generate_content_stream()`, sync and
  async (`client.aio.models...`).

## What gets sent

For every tracked call, exactly this shape is POSTed (batched) to saive:

```json
{
  "request_id": "generated-uuid4",
  "provider": "openai",
  "model": "gpt-4o-mini",
  "input_tokens": 123,
  "output_tokens": 45,
  "reasoning_tokens": 12,
  "max_tokens": 500,
  "timestamp": "2026-01-01T00:00:00+00:00",
  "latency_ms": 842.3,
  "tags": {"env": "prod"}
}
```

`max_tokens` is the ceiling your code passed to the SDK call (`max_tokens` for
OpenAI/Anthropic, `max_completion_tokens` taking priority over `max_tokens` for OpenAI,
`config.max_output_tokens` for Google GenAI) — not a usage figure. It powers one of the
reduction insights (calls that request far more headroom than they use).

`reasoning_tokens`, `max_tokens`, `latency_ms` and `tags` are omitted entirely when not applicable rather
than sent as `null`/`{}`.
