Chain-Receipt SDK — emit, verify, replay, and chain Receipts (v1.0.0) from LangChain, LlamaIndex, LangGraph, Pydantic AI, DSPy, or any Python callable.
Project description
chain-receipt-sdk
Emit, verify, replay, and chain Chain-Receipts (v1.0.0) from any
Python LLM-agent loop. Drop-in callback for LangChain, plus a
framework-agnostic ReceiptBuilder for raw API loops.
Verifiable record of every LLM call: emit a signed Receipt, replay it against any vendor, and check whether the chain holds.
pip install chain-receipt-sdk
chain-receipt verify sha256:<hash>
chain-receipt replay sha256:<hash> --n 10
60-second demo
from chain_receipt_sdk import ReceiptBuilder, ClientInfo, Interaction
from chain_receipt_core import compute_text_hash, compute_tool_calls_hash, generate_keypair
sk, pub = generate_keypair()
client = ClientInfo(
name="my-agent",
version="0.1.0",
platform="python-3.12",
emitter_pubkey=f"ed25519:{pub}",
)
b = ReceiptBuilder(client=client, private_key=sk, chain_seed=pub.encode())
inter = Interaction(
vendor="anthropic",
model="claude-sonnet-4-5",
temperature=0.0,
system_prompt_hash=compute_text_hash("you are helpful"),
prompt_hash=compute_text_hash("hello"),
response_hash=compute_text_hash("hi"),
tool_calls_hash=compute_tool_calls_hash([]),
n_tool_calls=0,
latency_ms=120,
)
r = b.build(interaction=inter)
print(r.receipt_id, r.chain.sequence_number)
LangChain callback
The ReceiptCallback is a BaseCallbackHandler — pass it via config={"callbacks": [cb]} to any LangChain LLM, chat model, or LCEL chain. One Receipt is emitted per on_llm_end / on_chat_model_end event; sequential calls chain automatically.
Minimal working example (zero-cost; uses LangChain's built-in fake LLM):
from langchain_core.language_models.fake import FakeListLLM
from chain_receipt_sdk.callback import ReceiptCallback
cb = ReceiptCallback(
client_name="my-langchain-agent",
vendor="anthropic",
model="claude-sonnet-4-5",
)
llm = FakeListLLM(responses=["the answer is 42"])
llm.invoke("what is 6 times 7?", config={"callbacks": [cb]})
print(f"emitted {len(cb.receipts)} Receipts")
print(cb.receipts[0].receipt_id, cb.receipts[0].chain.sequence_number)
Production example (real LLM; swap the fake for ChatAnthropic / ChatOpenAI / any chat model):
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from chain_receipt_sdk.callback import ReceiptCallback
cb = ReceiptCallback(
client_name="my-langchain-agent",
vendor="anthropic",
model="claude-sonnet-4-5",
capture_payload=True, # only set this if you control the data sensitivity
)
llm = ChatAnthropic(model="claude-sonnet-4-5", temperature=0)
prompt = ChatPromptTemplate.from_messages(
[("system", "be terse"), ("human", "{q}")]
)
chain = prompt | llm | StrOutputParser()
chain.invoke({"q": "what is 6 times 7?"}, config={"callbacks": [cb]})
Note on RunnableLambda: wrapping a plain Python function via RunnableLambda does NOT fire on_llm_start / on_llm_end — those events only fire when an actual language-model class is invoked. If you need to attribute receipts to non-LLM steps, use ReceiptCallback.emit(...) directly.
LlamaIndex callback
Same shape, different framework. chain_receipt_sdk.llamaindex.ReceiptCallback is a BaseCallbackHandler for LlamaIndex's CallbackManager. One Receipt per CBEventType.LLM event; FUNCTION_CALL events between LLM events accumulate into the next emitted Receipt's tool_calls_hash.
from llama_index.core import Settings
from llama_index.core.callbacks import CallbackManager
from chain_receipt_sdk.llamaindex import ReceiptCallback
cb = ReceiptCallback(
client_name="my-llamaindex-agent",
vendor="anthropic",
model="claude-sonnet-4-5",
capture_payload=True, # only if you control the data sensitivity
)
Settings.callback_manager = CallbackManager([cb])
# ... your normal LlamaIndex query / chat / agent pipeline ...
# Each LLM call emits one Receipt; multi-step pipelines emit one per step.
for r in cb.receipts:
print(r.receipt_id, r.chain.sequence_number, r.interaction.tool_calls_hash)
Install: pip install chain-receipt-sdk[llamaindex] (or pin llama-index-core>=0.13 yourself).
LangGraph adapter
LangGraph is built on LangChain's callback infrastructure, so the LangChain ReceiptCallback works automatically when attached to a compiled graph via compiled.invoke(config={"callbacks": [cb]}). The chain_receipt_sdk.langgraph module adds a thin convenience helper attach_receipts() that pre-attaches the callback so you don't have to thread config={"callbacks": [...]} through every call site:
from langgraph.graph import StateGraph, START, END
from chain_receipt_sdk.langgraph import ReceiptCallback, attach_receipts
# ... build your graph as usual ...
compiled = graph.compile()
cb = ReceiptCallback(
client_name="my-langgraph-agent",
vendor="anthropic",
model="claude-sonnet-4-5",
)
wrapped = attach_receipts(compiled, cb)
result = wrapped.invoke({"question": "hi"})
for r in wrapped.callback.receipts:
print(r.receipt_id, r.chain.sequence_number)
attach_receipts() returns a wrapper that exposes the same invoke / ainvoke / stream / astream surface as the compiled graph and merges your callback into any user-supplied callbacks. Conditional routing emits Receipts only for the chosen branch (verified in tests).
Install: pip install chain-receipt-sdk[langgraph].
Pydantic AI adapter
Pydantic AI uses native OpenTelemetry instrumentation. Once Cruxia CV v0.1.0 ships with always-on gen_ai.* span emission, Pydantic AI's own OTel spans will be visible alongside Cruxia CV's chain-receipt spans in any OTel backend (Datadog / Honeycomb / Grafana / New Relic). That's the recommended production integration.
For direct Receipt emission without an OTel collector, chain_receipt_sdk.pydantic_ai ships a callback + Agent wrapper:
from pydantic_ai import Agent
from chain_receipt_sdk.pydantic_ai import ReceiptCallback, attach_receipts
agent = Agent("anthropic:claude-sonnet-4-5", system_prompt="be terse")
cb = ReceiptCallback(
client_name="my-pydantic-ai-agent",
vendor="anthropic",
model="claude-sonnet-4-5",
)
wrapped = attach_receipts(agent, cb)
result = wrapped.run_sync("what is 6*7?")
for r in wrapped.callback.receipts:
print(r.receipt_id, r.chain.sequence_number)
attach_receipts() wraps the Agent so every .run_sync() / .run() auto-emits a Receipt at completion. Alternatively, call cb.emit_from_result(result) manually after each agent.run_sync(...).
Note on instructions= vs system_prompt=: Pydantic AI's instructions= parameter is passed to the model but NOT included in the message history, so the adapter can't see it. Use system_prompt= if you want the system context captured in the Receipt's system_prompt_hash.
Install: pip install chain-receipt-sdk[pydantic-ai].
DSPy adapter
DSPy (≥3.0) exposes BaseCallback at dspy.utils.callback. Register via dspy.settings.configure(callbacks=[cb]) (or scoped dspy.context(callbacks=[cb])); DSPy fires on_lm_start / on_lm_end / on_tool_start / on_tool_end around every LM call across your Predict / ChainOfThought / ReAct / BootstrapFewShot pipeline.
import dspy
from chain_receipt_sdk.dspy import ReceiptCallback
cb = ReceiptCallback(
client_name="my-dspy-program",
vendor="anthropic",
model="claude-sonnet-4-5",
)
# Scoped via context (preferred — doesn't mutate global settings)
with dspy.context(lm=dspy.LM("anthropic/claude-sonnet-4-5"), callbacks=[cb]):
program = dspy.Predict("question -> answer")
result = program(question="what is 6*7?")
for r in cb.receipts:
print(r.receipt_id, r.chain.sequence_number)
Each LM call emits one Receipt; tool calls (via dspy.ReAct etc.) accumulate between LM events and fold into the next Receipt's tool_calls_hash.
Install: pip install chain-receipt-sdk[dspy].
CLI
chain-receipt status # local chain head + count
chain-receipt verify sha256:<hash> # validate signature + chain link
chain-receipt replay sha256:<hash> --n 10 # re-run captured prompt N times
chain-receipt chain --since 2026-04-25 # list Receipts since timestamp
chain-receipt publish sha256:<hash> # push to chain-determinism.org
Local chain storage
Receipts are written to ~/.chain-receipt/chain.jsonl (one JSON per line)
when emitted via ReceiptCallback or ReceiptBuilder.build(persist=True).
The chain_receipt_sdk.chain.LocalChain reader/writer is the public API
for that file.
Replay determinism
chain_receipt_sdk.replay re-runs the captured prompt against the
same vendor + model + temperature N times and reports
final_answer_consistent, tool_seq_identical, tool_args_identical,
and a Wilson 95% CI for the divergence rate. Integration test gate
in tests/test_replay.py.
License
MIT — see LICENSE.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file chain_receipt_sdk-0.2.0.tar.gz.
File metadata
- Download URL: chain_receipt_sdk-0.2.0.tar.gz
- Upload date:
- Size: 40.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2a05c75149559fde063b0cbc7f7a0c228269d96a2b37babc892a325424ab8c16
|
|
| MD5 |
292a7f32fa0f167d7213971e5a1381d4
|
|
| BLAKE2b-256 |
8ce2eb48f9794a4cb6d7d0f08ed9170b1b9e4e38158505a6fa4916b28973e728
|
File details
Details for the file chain_receipt_sdk-0.2.0-py3-none-any.whl.
File metadata
- Download URL: chain_receipt_sdk-0.2.0-py3-none-any.whl
- Upload date:
- Size: 33.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e39ce24c8772bd572cf09c5125b2bc4d7019b802cf5e44044585d3047f48343e
|
|
| MD5 |
1693a04c223a630208daa8c12de28fa1
|
|
| BLAKE2b-256 |
a10ba508982611e699381d0cb404502bea305b746576af7a3adec7634d337f1f
|