tokenledger-llm 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ """tokenledger — measure the real cost structure of multi-agent LLM systems."""
2
+ from .parse import Request, load_requests
3
+ from .account import Metrics, account, what_if_remove_step
4
+ from .adapters import (
5
+ from_anthropic_usage,
6
+ from_openai_usage,
7
+ from_records,
8
+ load_provider,
9
+ )
10
+
11
+ __all__ = [
12
+ "Request",
13
+ "load_requests",
14
+ "Metrics",
15
+ "account",
16
+ "what_if_remove_step",
17
+ "from_anthropic_usage",
18
+ "from_openai_usage",
19
+ "from_records",
20
+ "load_provider",
21
+ ]
22
+ __version__ = "0.1.0"
tokenledger/account.py ADDED
@@ -0,0 +1,87 @@
1
+ """The accounting math: where the tokens actually go.
2
+
3
+ Core claims this quantifies (see docs/METHODOLOGY.md):
4
+ 1. input cost tracks (prefix size x number of requests), dominated by cache re-reads;
5
+ 2. cost is superlinear in step count, because each step's priming AND its tool
6
+ result are re-carried into every later step of the turn.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import asdict, dataclass
11
+ from typing import Dict, List
12
+
13
+ from .parse import Request
14
+
15
+
16
+ @dataclass
17
+ class Metrics:
18
+ n_requests: int
19
+ n_turns: int
20
+ steps_per_turn: Dict[int, int]
21
+ total_input: int
22
+ total_output: int
23
+ total_cache_read: int
24
+ cache_read_pct: float
25
+ priming_total: int
26
+ priming_pct: float
27
+ conversation_total: int
28
+ conversation_pct: float
29
+ mean_prefix: float
30
+ max_prefix: int
31
+ prefix_reread_cost: int # tokens spent re-reading already-seen (cached) context
32
+
33
+ def to_dict(self) -> dict:
34
+ return asdict(self)
35
+
36
+
37
+ def _pct(part: int, whole: int) -> float:
38
+ return (part / whole) if whole else 0.0
39
+
40
+
41
+ def account(requests: List[Request]) -> Metrics:
42
+ if not requests:
43
+ raise ValueError("no requests to account")
44
+
45
+ total_input = sum(r.input_tokens for r in requests)
46
+ total_output = sum(r.output_tokens for r in requests)
47
+ total_cache = sum(r.cache_read_tokens for r in requests)
48
+ priming_total = sum(r.priming_tokens for r in requests)
49
+ conversation_total = sum(r.conversation_tokens for r in requests)
50
+
51
+ turns: Dict[int, List[Request]] = {}
52
+ for r in requests:
53
+ turns.setdefault(r.turn, []).append(r)
54
+ steps_per_turn = {t: len(rs) for t, rs in sorted(turns.items())}
55
+
56
+ prefixes = [r.input_tokens for r in requests]
57
+ return Metrics(
58
+ n_requests=len(requests),
59
+ n_turns=len(turns),
60
+ steps_per_turn=steps_per_turn,
61
+ total_input=total_input,
62
+ total_output=total_output,
63
+ total_cache_read=total_cache,
64
+ cache_read_pct=_pct(total_cache, total_input),
65
+ priming_total=priming_total,
66
+ priming_pct=_pct(priming_total, total_input),
67
+ conversation_total=conversation_total,
68
+ conversation_pct=_pct(conversation_total, total_input),
69
+ mean_prefix=sum(prefixes) / len(prefixes),
70
+ max_prefix=max(prefixes),
71
+ prefix_reread_cost=total_cache,
72
+ )
73
+
74
+
75
+ def what_if_remove_step(requests: List[Request], turn: int, step: int) -> int:
76
+ """Estimated input tokens saved by removing (turn, step).
77
+
78
+ Saving = the removed request's own input, PLUS its tool result, which would
79
+ have been re-carried into every later step of the same turn. This is why
80
+ removing an EARLY step saves more than a late one: the superlinear effect.
81
+ """
82
+ same_turn = [r for r in requests if r.turn == turn]
83
+ target = next((r for r in same_turn if r.step == step), None)
84
+ if target is None:
85
+ raise KeyError(f"no step {step} in turn {turn}")
86
+ later_steps = [r for r in same_turn if r.step > step]
87
+ return target.input_tokens + target.tool_result_tokens * len(later_steps)
@@ -0,0 +1,177 @@
1
+ """Adapters: turn real provider usage logs into normalized Request records.
2
+
3
+ The point of tokenledger is that you measure *your own* system, so it has to
4
+ read the logs you already have. Providers report token usage differently, and
5
+ one difference matters for correctness:
6
+
7
+ * Anthropic Messages API: `usage.input_tokens` is the FRESH (uncached) input
8
+ only. Cached context is reported SEPARATELY in `cache_read_input_tokens`
9
+ and `cache_creation_input_tokens`. So the *total* context sent on a request
10
+ is the sum of all three — a naive reader who takes `input_tokens` alone
11
+ undercounts the prefix, which is exactly the cost this tool is about.
12
+
13
+ * OpenAI Chat Completions: `usage.prompt_tokens` is ALREADY the total input
14
+ (it includes cached tokens); the cached portion is broken out under
15
+ `usage.prompt_tokens_details.cached_tokens`.
16
+
17
+ These adapters normalize both into Request.input_tokens = total context sent.
18
+
19
+ Turn/step structure usually is NOT in a raw usage record. The superlinearity
20
+ metric (what_if_remove_step, steps_per_turn) depends on it, so you supply it:
21
+ pass turn_key/step_key (field names present in each record) or turn_fn/step_fn
22
+ (callables over the record). If you supply neither, every record is treated as
23
+ its own step within a single turn (step = file order), which still gives you
24
+ correct totals, cache-read % and priming %, but a flat step structure.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ from pathlib import Path
30
+ from typing import Callable, Iterable, List, Optional, Union
31
+
32
+ from .parse import Request
33
+
34
+
35
+ def _usage(record: dict, usage_key: Optional[str]) -> dict:
36
+ """Return the sub-dict holding token counts, or the record itself."""
37
+ if usage_key and isinstance(record.get(usage_key), dict):
38
+ return record[usage_key]
39
+ if usage_key is None and isinstance(record.get("usage"), dict):
40
+ return record["usage"]
41
+ return record
42
+
43
+
44
+ def _int(d: dict, key: str, default: int = 0) -> int:
45
+ v = d.get(key, default)
46
+ return int(v) if v is not None else default
47
+
48
+
49
+ def from_anthropic_usage(
50
+ record: dict, *, turn: int, step: int, label: str = "", usage_key: Optional[str] = None
51
+ ) -> Request:
52
+ """One Anthropic Messages API record -> Request.
53
+
54
+ total input = input_tokens + cache_creation_input_tokens + cache_read_input_tokens
55
+ (because Anthropic's `input_tokens` excludes cached context).
56
+ """
57
+ u = _usage(record, usage_key)
58
+ fresh = _int(u, "input_tokens")
59
+ cache_creation = _int(u, "cache_creation_input_tokens")
60
+ cache_read = _int(u, "cache_read_input_tokens")
61
+ total_input = fresh + cache_creation + cache_read
62
+ return Request(
63
+ turn=turn,
64
+ step=step,
65
+ input_tokens=total_input,
66
+ cache_read_tokens=cache_read,
67
+ output_tokens=_int(u, "output_tokens"),
68
+ label=label or str(record.get("label", "")),
69
+ )
70
+
71
+
72
+ def from_openai_usage(
73
+ record: dict, *, turn: int, step: int, label: str = "", usage_key: Optional[str] = None
74
+ ) -> Request:
75
+ """One OpenAI Chat Completions record -> Request.
76
+
77
+ `prompt_tokens` is already the total input; the cached portion is in
78
+ `prompt_tokens_details.cached_tokens`.
79
+ """
80
+ u = _usage(record, usage_key)
81
+ total_input = _int(u, "prompt_tokens")
82
+ details = u.get("prompt_tokens_details") or {}
83
+ cache_read = _int(details, "cached_tokens") if isinstance(details, dict) else 0
84
+ return Request(
85
+ turn=turn,
86
+ step=step,
87
+ input_tokens=total_input,
88
+ cache_read_tokens=cache_read,
89
+ output_tokens=_int(u, "completion_tokens"),
90
+ label=label or str(record.get("label", "")),
91
+ )
92
+
93
+
94
+ _CONVERTERS = {
95
+ "anthropic": from_anthropic_usage,
96
+ "openai": from_openai_usage,
97
+ }
98
+
99
+
100
+ def _turn_step_resolver(
101
+ turn_key: Optional[str],
102
+ step_key: Optional[str],
103
+ turn_fn: Optional[Callable[[dict], int]],
104
+ step_fn: Optional[Callable[[dict], int]],
105
+ ):
106
+ def resolve(record: dict, idx: int):
107
+ if turn_fn is not None:
108
+ turn = int(turn_fn(record))
109
+ elif turn_key is not None:
110
+ turn = int(record[turn_key])
111
+ else:
112
+ turn = 1
113
+ if step_fn is not None:
114
+ step = int(step_fn(record))
115
+ elif step_key is not None:
116
+ step = int(record[step_key])
117
+ else:
118
+ step = idx
119
+ return turn, step
120
+
121
+ return resolve
122
+
123
+
124
+ def from_records(
125
+ records: Iterable[dict],
126
+ fmt: str,
127
+ *,
128
+ turn_key: Optional[str] = None,
129
+ step_key: Optional[str] = None,
130
+ turn_fn: Optional[Callable[[dict], int]] = None,
131
+ step_fn: Optional[Callable[[dict], int]] = None,
132
+ usage_key: Optional[str] = None,
133
+ ) -> List[Request]:
134
+ """Convert an iterable of provider usage dicts into Request records."""
135
+ if fmt not in _CONVERTERS:
136
+ raise ValueError(f"unknown format {fmt!r}; expected one of {sorted(_CONVERTERS)}")
137
+ convert = _CONVERTERS[fmt]
138
+ resolve = _turn_step_resolver(turn_key, step_key, turn_fn, step_fn)
139
+ out: List[Request] = []
140
+ for idx, record in enumerate(records):
141
+ turn, step = resolve(record, idx)
142
+ out.append(convert(record, turn=turn, step=step, usage_key=usage_key))
143
+ return out
144
+
145
+
146
+ def load_provider(
147
+ path: Union[str, Path],
148
+ fmt: str,
149
+ *,
150
+ turn_key: Optional[str] = None,
151
+ step_key: Optional[str] = None,
152
+ turn_fn: Optional[Callable[[dict], int]] = None,
153
+ step_fn: Optional[Callable[[dict], int]] = None,
154
+ usage_key: Optional[str] = None,
155
+ ) -> List[Request]:
156
+ """Load a JSONL usage log in a provider format into Request records.
157
+
158
+ Blank lines and lines starting with '#' are skipped. Each remaining line is
159
+ one JSON object carrying a usage record (either at top level or under a
160
+ `usage` key).
161
+ """
162
+ records: List[dict] = []
163
+ with open(path, "r", encoding="utf-8") as f:
164
+ for line in f:
165
+ line = line.strip()
166
+ if not line or line.startswith("#"):
167
+ continue
168
+ records.append(json.loads(line))
169
+ return from_records(
170
+ records,
171
+ fmt,
172
+ turn_key=turn_key,
173
+ step_key=step_key,
174
+ turn_fn=turn_fn,
175
+ step_fn=step_fn,
176
+ usage_key=usage_key,
177
+ )
tokenledger/parse.py ADDED
@@ -0,0 +1,59 @@
1
+ """Ingest request logs into a normalized Request model.
2
+
3
+ A log is JSONL: one JSON object per line, one object per LLM request (one step
4
+ within a turn). Unknown keys are ignored; missing optional fields default to 0.
5
+ Map a provider's own field names with the `mapping` argument.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import List, Optional, Union
13
+
14
+
15
+ @dataclass
16
+ class Request:
17
+ """One LLM request = one step within a conversation turn."""
18
+ turn: int # which turn (main-loop iteration) this belongs to
19
+ step: int # step index within the turn (0-based)
20
+ input_tokens: int # total input tokens sent on this request (the context)
21
+ cache_read_tokens: int = 0 # input tokens served from cache (re-read prefix)
22
+ priming_tokens: int = 0 # fixed re-sent block: system prompt, tool schemas, agreements
23
+ conversation_tokens: int = 0 # re-carried prior messages + tool results
24
+ tool_result_tokens: int = 0 # tokens this step emitted as tool output (carried forward)
25
+ output_tokens: int = 0
26
+ label: str = ""
27
+
28
+ @property
29
+ def fresh_tokens(self) -> int:
30
+ """Input tokens that were NOT served from cache on this request."""
31
+ return max(self.input_tokens - self.cache_read_tokens, 0)
32
+
33
+
34
+ _FIELDS = {
35
+ "turn", "step", "input_tokens", "cache_read_tokens", "priming_tokens",
36
+ "conversation_tokens", "tool_result_tokens", "output_tokens", "label",
37
+ }
38
+
39
+
40
+ def _coerce(obj: dict, mapping: Optional[dict]) -> Request:
41
+ if mapping:
42
+ obj = {ours: obj.get(theirs) for ours, theirs in mapping.items()}
43
+ kwargs = {k: v for k, v in obj.items() if k in _FIELDS and v is not None}
44
+ for required in ("turn", "step", "input_tokens"):
45
+ if required not in kwargs:
46
+ raise ValueError(f"record missing required field '{required}': {obj}")
47
+ return Request(**kwargs)
48
+
49
+
50
+ def load_requests(path: Union[str, Path], mapping: Optional[dict] = None) -> List[Request]:
51
+ """Load a JSONL request log. Blank lines and lines starting with '#' are skipped."""
52
+ out: List[Request] = []
53
+ with open(path, "r", encoding="utf-8") as f:
54
+ for line in f:
55
+ line = line.strip()
56
+ if not line or line.startswith("#"):
57
+ continue
58
+ out.append(_coerce(json.loads(line), mapping))
59
+ return out
tokenledger/report.py ADDED
@@ -0,0 +1,84 @@
1
+ """CLI: `tokenledger report <log.jsonl>` → human or JSON cost-structure report."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from typing import Optional, Sequence
8
+
9
+ from .account import Metrics, account
10
+ from .adapters import load_provider
11
+ from .parse import load_requests
12
+
13
+
14
+ def format_report(m: Metrics) -> str:
15
+ lines = [
16
+ "tokenledger — cost structure report",
17
+ "=" * 42,
18
+ f"requests (steps): {m.n_requests}",
19
+ f"turns: {m.n_turns}",
20
+ f"steps per turn: {m.steps_per_turn}",
21
+ "",
22
+ f"total input tokens: {m.total_input:,}",
23
+ f"total output tokens: {m.total_output:,}",
24
+ "",
25
+ f"cache-read: {m.total_cache_read:,} ({m.cache_read_pct:.1%} of input)",
26
+ f"priming re-sent: {m.priming_total:,} ({m.priming_pct:.1%} of input)",
27
+ f"re-carried conversation: {m.conversation_total:,} ({m.conversation_pct:.1%} of input)",
28
+ "",
29
+ f"mean prefix / request: {m.mean_prefix:,.0f} max prefix: {m.max_prefix:,}",
30
+ f"prefix re-read cost: {m.prefix_reread_cost:,} tokens",
31
+ "",
32
+ "takeaway: cost tracks (prefix size x requests), dominated by cache re-reads —",
33
+ "not prompt size. Cut steps and reset context to move the bill.",
34
+ ]
35
+ return "\n".join(lines)
36
+
37
+
38
+ def main(argv: Optional[Sequence[str]] = None) -> int:
39
+ p = argparse.ArgumentParser(
40
+ prog="tokenledger",
41
+ description="Measure the cost structure of multi-agent LLM systems from request logs.",
42
+ )
43
+ sub = p.add_subparsers(dest="cmd")
44
+ rep = sub.add_parser("report", help="print a cost-structure report for a JSONL log")
45
+ rep.add_argument("logfile", help="path to a JSONL request log")
46
+ rep.add_argument("--json", action="store_true", help="emit JSON instead of text")
47
+ rep.add_argument(
48
+ "--format",
49
+ choices=["native", "anthropic", "openai"],
50
+ default="native",
51
+ help="log format: 'native' tokenledger fields (default), or a provider usage log",
52
+ )
53
+ rep.add_argument("--turn-key", help="record field naming the turn (provider formats)")
54
+ rep.add_argument("--step-key", help="record field naming the step (provider formats)")
55
+ rep.add_argument(
56
+ "--usage-key",
57
+ help="record field holding the usage sub-object (default: 'usage' if present, else top level)",
58
+ )
59
+ args = p.parse_args(argv)
60
+
61
+ if args.cmd != "report":
62
+ p.print_help()
63
+ return 1
64
+
65
+ if args.format == "native":
66
+ requests = load_requests(args.logfile)
67
+ else:
68
+ requests = load_provider(
69
+ args.logfile,
70
+ args.format,
71
+ turn_key=args.turn_key,
72
+ step_key=args.step_key,
73
+ usage_key=args.usage_key,
74
+ )
75
+ metrics = account(requests)
76
+ if args.json:
77
+ print(json.dumps(metrics.to_dict(), indent=2))
78
+ else:
79
+ print(format_report(metrics))
80
+ return 0
81
+
82
+
83
+ if __name__ == "__main__":
84
+ sys.exit(main())
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: tokenledger-llm
3
+ Version: 0.1.0
4
+ Summary: Measure the real cost structure of multi-agent / multi-model LLM systems from request logs.
5
+ Author: Pallav Pokhriyal
6
+ License: MIT
7
+ Keywords: llm,agents,cost,tokens,observability,mlops
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: license-file
12
+
13
+ # tokenledger
14
+
15
+ **Measure where the tokens actually go in a multi-agent / multi-model LLM system.**
16
+
17
+ Most teams reason about LLM cost as if it scales with *prompt size*. In a multi-step agent it
18
+ doesn't — it scales with **(context size × number of requests)**, because almost all input is a
19
+ **cache re-read of a growing prefix**, and it is **superlinear in step count**, because each step's
20
+ priming *and* its tool result get re-carried into every later step. `tokenledger` reads your request
21
+ logs and shows you exactly that, so you optimize the right thing.
22
+
23
+ > Status: **v0.1 — early scaffold, feedback very welcome.** Author: Pallav Pokhriyal.
24
+ > Zero dependencies, zero telemetry. It reads logs you already have.
25
+
26
+ ## Install
27
+ ```bash
28
+ pip install tokenledger-llm # on PyPI; the import name and CLI are both `tokenledger`
29
+ # or from source:
30
+ pip install -e .
31
+ ```
32
+
33
+ ## Quickstart
34
+ ```bash
35
+ tokenledger report examples/sample_logs/sample.jsonl
36
+ # or JSON:
37
+ tokenledger report examples/sample_logs/sample.jsonl --json
38
+ ```
39
+
40
+ Example output:
41
+ ```
42
+ tokenledger — cost structure report
43
+ ==========================================
44
+ requests (steps): 4
45
+ turns: 2
46
+ steps per turn: {1: 3, 2: 1}
47
+
48
+ total input tokens: 89,532
49
+ total output tokens: 505
50
+
51
+ cache-read: 88,432 (98.8% of input)
52
+ priming re-sent: 80,464 (89.9% of input)
53
+ re-carried conversation: 7,968 (8.9% of input)
54
+
55
+ mean prefix / request: 22,383 max prefix: 24,000
56
+ prefix re-read cost: 88,432 tokens
57
+
58
+ takeaway: cost tracks (prefix size x requests), dominated by cache re-reads —
59
+ not prompt size. Cut steps and reset context to move the bill.
60
+ ```
61
+
62
+ ## Log format
63
+ JSONL, one request (one step within a turn) per line. Required: `turn`, `step`, `input_tokens`.
64
+ Everything else is optional and defaults to 0:
65
+
66
+ | field | meaning |
67
+ |---|---|
68
+ | `turn` / `step` | which turn, and the step index within it |
69
+ | `input_tokens` | total input sent on this request (the context) |
70
+ | `cache_read_tokens` | input served from cache (the re-read prefix) |
71
+ | `priming_tokens` | fixed re-sent block: system prompt, tool schemas |
72
+ | `conversation_tokens` | re-carried prior messages + tool results |
73
+ | `tool_result_tokens` | tokens this step emitted as tool output (carried forward) |
74
+ | `output_tokens` | completion tokens |
75
+
76
+ Have a different log shape? Map it:
77
+ ```python
78
+ from tokenledger import load_requests, account
79
+ reqs = load_requests("mylog.jsonl", mapping={"input_tokens": "prompt_tok", "turn": "iteration"})
80
+ print(account(reqs).cache_read_pct)
81
+ ```
82
+
83
+ ## Provider usage logs (no reshaping)
84
+
85
+ Point it straight at Anthropic or OpenAI usage records:
86
+ ```bash
87
+ tokenledger report anthropic_usage.jsonl --format anthropic --turn-key turn --step-key step
88
+ tokenledger report openai_usage.jsonl --format openai --turn-key turn --step-key step
89
+ ```
90
+ Each line is one request whose token counts live at top level or under a `usage` key.
91
+ One correctness detail the adapters handle for you:
92
+
93
+ - **Anthropic** reports `usage.input_tokens` as the *fresh* (uncached) input only; cached
94
+ context is reported separately in `cache_read_input_tokens` / `cache_creation_input_tokens`.
95
+ The adapter sums all three so `input_tokens` is the *true* per-request context — taking
96
+ `input_tokens` alone silently undercounts the prefix, which is the whole thing being measured.
97
+ - **OpenAI** reports `usage.prompt_tokens` as the total input already, with the cached portion
98
+ in `prompt_tokens_details.cached_tokens`.
99
+
100
+ If your records don't carry turn/step, omit the keys — each line becomes its own step in a single
101
+ turn (totals and percentages stay correct; the per-turn step structure just goes flat). In Python:
102
+ ```python
103
+ from tokenledger import load_provider, account
104
+ reqs = load_provider("anthropic_usage.jsonl", "anthropic", turn_key="turn", step_key="step")
105
+ print(account(reqs).cache_read_pct)
106
+ ```
107
+
108
+ ## What-if: which step to cut
109
+ ```python
110
+ from tokenledger import load_requests, what_if_remove_step
111
+ reqs = load_requests("examples/sample_logs/sample.jsonl")
112
+ print(what_if_remove_step(reqs, turn=1, step=0)) # tokens saved by removing an early step
113
+ ```
114
+ Removing an **early** step saves more than a late one — it drops that step's own cost *and* its
115
+ tool result from every later step. That's the superlinearity, made concrete.
116
+
117
+ ## Why it exists
118
+ See [`docs/METHODOLOGY.md`](docs/METHODOLOGY.md) for the measured findings this operationalizes.
119
+
120
+ ## Contributing
121
+ Issues and PRs welcome — especially log-format adapters for different providers/harnesses. Keep
122
+ contributions genuinely useful; no telemetry, no secrets in sample data (synthetic only).
123
+
124
+ ## License
125
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,11 @@
1
+ tokenledger/__init__.py,sha256=LKaBz16YZd7L3pqNL_hOg81QMzNaTtbPEa-6alVEk_g,519
2
+ tokenledger/account.py,sha256=-o386ERFKPShfHRfY2MW3nUWPcTl_EsiQ8XGICXqFPQ,3056
3
+ tokenledger/adapters.py,sha256=npU3mTUghugXKYkG57FBLAAWuyBXs5pbIDfU_LMbzlg,6180
4
+ tokenledger/parse.py,sha256=DZrMGJwWFowVqy9iK7nwUjeag_dPjbRefpemXZRKFRw,2429
5
+ tokenledger/report.py,sha256=4oXlACfIaDcR6GjwEsmuLU1_PZyPT5cvOhU17_Ok_Hk,3017
6
+ tokenledger_llm-0.1.0.dist-info/licenses/LICENSE,sha256=cr7GSixCcUQiaPQc2oX2OMvcx2qVQZ7XvscNZxxRVpA,1073
7
+ tokenledger_llm-0.1.0.dist-info/METADATA,sha256=QoxdFzYD-T_DTjf_y3T9OmZ72cPqNcy1ltx5-mpUwGU,5128
8
+ tokenledger_llm-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
9
+ tokenledger_llm-0.1.0.dist-info/entry_points.txt,sha256=89SvmLxIdpGkB-X27TThW-orGPj_A8CVFlv5hSwdFrc,56
10
+ tokenledger_llm-0.1.0.dist-info/top_level.txt,sha256=xZRn06uJDCJV_gIxBUfJeJGmN0xyWD2UG87zXqzhPIA,12
11
+ tokenledger_llm-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tokenledger = tokenledger.report:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pallav Pokhriyal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ tokenledger