Skip to main content

Local capture client for Kadari -- record your LLM calls to a local log for cost analysis.

Project description

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.2.0-py3-none-any.whl

Use

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 — and each tool-call argument value — before it is written; sample= records only a fraction of calls to bound log size.
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:

{"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 or prose 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. When a response carries a tool call and no prose, the adapters write tool:<name> (see below).
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.
tool_calls array optional (since 0.2.0) the structured decision(s) the model already emitted, in provider order. Never written empty — a log with no tool call is byte-identical to one written before this field existed.
tool_calls[].name string yes (per entry) the tool/function name — which decision was made.
tool_calls[].arguments object optional the arguments, verbatim, with their original JSON types. Present unless arguments_omitted is.
tool_calls[].arguments_omitted string optional why the payload is absent: oversize, unparsed, not_an_object, unserializable. Mutually exclusive with arguments. Readers must tolerate a reason they don't recognise.

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.

Tool calls — the structured decision

If your premium call already emits a tool call (an agentic support agent deciding to issue a refund, look up an order, escalate), that tool call is the decision — and it is the part worth measuring. The adapters record it automatically; nothing to configure:

{"id": "req-9", "model": "gpt-5.5", "input": "where is my order 118?",
 "output": "I've started your refund — 3–5 business days.",
 "usage": {"input_tokens": 812, "output_tokens": 96},
 "tool_calls": [{"name": "refund_order", "arguments": {"order_id": "A-118", "amount_cents": 4000}}]}
  • A reply that explains and acts keeps both halves — prose in output, decision in tool_calls. Nothing about output changed for calls that already captured.
  • A reply that only acts has no prose, so output becomes tool:<first tool name>output is required and non-empty, and the tool: prefix keeps an agentic call from being mistaken for a classification label.
  • Every tool call is recorded, in the order the provider returned them — not just the first.
  • We only record a decision the model already emitted. Kadari never infers one from prose: an inferred decision is a guess wearing a proof's clothing.
  • Arguments are recorded verbatim or not at all — never truncated. If they can't be represented faithfully (malformed JSON from the model, a value that isn't JSON, a payload too large for the line ceiling), the entry keeps the tool name and states the reason in arguments_omitted. Losing the payload never loses the call.
  • redact= applies to argument values as well as input (keys and tool names are structure, not content, and are left alone). One consequence worth knowing: if you redact a value that also appears in the reply text, Kadari's checks can no longer match the two — which is the honest outcome, not a silent one.

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kadari-0.2.0.tar.gz (22.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kadari-0.2.0-py3-none-any.whl (20.7 kB view details)

Uploaded Python 3

File details

Details for the file kadari-0.2.0.tar.gz.

File metadata

  • Download URL: kadari-0.2.0.tar.gz
  • Upload date:
  • Size: 22.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.2

File hashes

Hashes for kadari-0.2.0.tar.gz
Algorithm Hash digest
SHA256 4f5225ba4d5b268ad44d5ec1d2dac2917e45b6e98f5217341f3c1bc44cd2d627
MD5 c88ac6e82e69dc2c9f39772e50b22575
BLAKE2b-256 3a55f48cdceb68e605f4b9db39bdb530ebf224bb1f20832919fb1df55c81562c

See more details on using hashes here.

File details

Details for the file kadari-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: kadari-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 20.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.2

File hashes

Hashes for kadari-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7589bc073c866def7ccacdec8c91d3cfa457dd0f6121a47fc577be232bd63b8d
MD5 bdc222c948e5e4002de2a9c9458486bf
BLAKE2b-256 68f2c5173b2b83384721fd7c0867e740c18adcf02465baf6b612c85ccd800be4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page