Metadata-Version: 2.4
Name: kadari
Version: 0.1.0
Summary: Local capture client for Kadari -- record your LLM calls to a local log for cost analysis.
Author-email: Kadari <support.kadari@gmail.com>
Maintainer-email: Kadari <support.kadari@gmail.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://kadari.ai/
Keywords: llm,openai,anthropic,observability,cost,logging,capture,inference,classification
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Logging
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# kadari — the local capture client

`kadari` is the only thing Kadari ships to you. It's a **thin, zero-dependency Python
library** that records the LLM calls you *already make* to a **local** log. Kadari's
engine (which stays on Kadari's side) then analyses that log and shows you, with proof,
where you could safely route to a cheaper in-family model — without ever re-running a
call or crossing a quality bar you set.

This package carries **none of Kadari's analysis engine** — no scoring, no classifier
model, no benchmarks. It is capture glue, by design. Read every line.

## Install

Zero runtime dependencies (stdlib only):

```
pip install kadari
```

Or install from a checkout / a wheel your Kadari contact hands you:

```
pip install ./client
# or:  pip install kadari-0.1.0-py3-none-any.whl
```

## Use

```python
from kadari import LiveRecorder, wrap

rec = LiveRecorder("kadari_capture.jsonl")   # a local path you control

# Option A — record explicitly, right after a call you already make:
resp = client.messages.create(model="claude-opus-4-8", messages=[{"role": "user", "content": text}])
rec.record_anthropic(id=req_id, input=text, response=resp.to_dict())
# OpenAI: rec.record_openai(id=req_id, input=text, response=resp.to_dict())

# Option B — wrap the call once and capture every call automatically:
create = wrap(client.chat.completions.create, provider="openai", recorder=rec)
resp = create(model="gpt-5.4-nano", messages=[{"role": "user", "content": text}])
```

Then hand the resulting `kadari_capture.jsonl` to Kadari (or, later, point the hosted
client at it) to get your savings report.

## Safety contract

- **Fail open on the host, fail closed on the data.** Recording is a side-channel: a bug
  here will **never** raise into your production call path — `record()` warns and returns
  `False` instead of throwing (unless you pass `strict=True`). What *does* get written is
  always valid and loadable.
- **Local-first.** This library never opens a network connection. Your prompts and outputs
  stay in the local file you named; nothing is uploaded by `kadari`.
- **Privacy controls.** `redact=` scrubs each input before it is written; `sample=` records
  only a fraction of calls to bound log size.

```python
rec = LiveRecorder("kadari_capture.jsonl", sample=0.1, redact=my_scrubber)
```

## Wire format — the stable SDK contract

The on-disk JSONL log is the **contract** between this client and Kadari's engine. It is
**near-immutable**: backward-compatible within a major version; a breaking change requires a
new major version and a documented migration path (Kadari rule 6). One JSON object per line:

```json
{"id": "req-123", "model": "gpt-5.4-nano", "input": "...", "output": "Electronics",
 "usage": {"input_tokens": 64, "output_tokens": 2}}
```

| field | type | required | meaning |
|-------|------|----------|---------|
| `id` | string | yes | unique per log — a captured log is **rejected wholesale** on a duplicate id. If you don't pass one, a uuid is generated, and a reused id is refused at write time so one retry can't make the whole log unloadable. |
| `model` | string | yes | the premium provider model string you actually called (e.g. `gpt-5.5`, `claude-opus-4-8`) — priced verbatim. |
| `input` | string | yes (may be empty) | the request text that gets scored/re-classified cheaply. |
| `output` | string | yes | the label the premium model already returned. The provider adapters (`record_openai`/`record_anthropic`/`wrap`) unwrap a single-field structured object `{"category": "X"}` to `X`; the raw `record(output=...)` path writes the string **verbatim**, so pass a bare label there. |
| `usage` | object | optional | `{"input_tokens", "output_tokens"}`, both non-negative integers. Attached **only when both are present** — a half block is dropped, since the engine honours usage only if both are set. When omitted, Kadari estimates spend from text length. |

Comment lines (`//…`) and blank lines are ignored by the reader.

**Compatibility guarantees within a major version:**

- the five fields above keep their names and meaning;
- new **optional** fields may be added; readers ignore unknown fields, so a newer client log
  still loads in an older engine and vice-versa;
- no required field is removed or repurposed, and no field type changes.

This is enforced, not promised: the client never imports engine code, but a contract test in
the engine repo (`tests/test_kadari_client.py`) writes a log with **this** client and asserts
it loads unchanged through the engine's reader — so the two halves can never silently drift.
The engine accepts one additional **input** shape (a nested Anthropic Messages
`{"id","request","response"}` transcript); this client always emits the flattened shape above.
