What an adapter does
- Records every model call your agent makes, model, tokens, output hash, endpoint — into a hash-chained, signed Action Provenance record.
- Binds each action to the signed Composition Manifest it ran under, so runtime can always be traced back to what was declared.
- Enforces (optionally) the gate inline: an undeclared model or a disallowed egress is denied before the call leaves your boundary.
How an adapter works
An adapter does one thing: it wraps the method your SDK uses to call the model, leaving your code unchanged. instrument(client, …) replaces that method in place and returns the same client object.
Each adapter wraps the single call surface its SDK exposes for a model request:
| Provider | SDK | Method wrapped |
|---|---|---|
| Anthropic Available | anthropic | client.messages.create |
| OpenAI Available | openai | client.chat.completions.create |
| Mistral Available | mistralai | client.chat.complete |
| LangChain Available | langchain | callbacks: on_tool_start · on_llm_end |
If gate=True, the adapter checks the requested model against the signed manifest first. A model that isn't declared is denied — ActionDenied is raised and the request never leaves your boundary.
The original SDK method runs unchanged. Same arguments, same return value — your agent code doesn't know it's wrapped.
The adapter reads the model, token usage, and output from the response, hashes the output, and appends a record to the run's hash-chained Action Provenance log — bound to the manifest's composition_sha256.
Adapter catalog
One adapter per model provider — the providers ABOM targets for runtime capture. Anthropic, OpenAI, and Mistral are available now; the rest are Planned and the catalog grows as adapters ship.
Framework adapters
Model-SDK adapters capture what an agent says; framework adapters also capture what it does — tool calls, gated deny-by-default before they execute. LangChain is available now via a callback handler.
Guardrail & screening adapters
A third kind of adapter: it doesn't capture calls, it notarizes another product's verdicts. The gateway's detector seam accepts any (tool, args) → findings callable and seals the findings into each signed record's detectors[] — the screen never gates, policy decides. Google Model Armor is available now: abom gateway run --model-armor-template projects/… keeps the guardrail you already bought and gives you the third-party-verifiable evidence its console can't. They decide, you prove.
Using an adapter
The flow is the same for every provider — generate a manifest, install, wrap, verify.
Generate the manifest (abom.json)
The adapter needs a signed Composition Manifest to record against and gate on. You don't write it by hand — abom scan produces it from your code, detecting the models, tools, and frameworks it uses, then signs it.
abom scan . -o abom.json # scan this repo → signed abom.json # abom.json now declares e.g. the model "claude-3-5-sonnet-20241022"
The model you call at runtime must be one this manifest declares — that's what the gate enforces. New to this? See Get started for the full scan walkthrough.
Install
Adapters ship with the CLI. Install abom-cli and the provider extra for the SDK you use.
pip install "abom-cli[anthropic]" # the provider extra for the SDK you use
Wrap your client
Point the adapter at your existing model client and your signed manifest. No changes to your call sites — the adapter records each call and can deny one that falls outside the signed envelope.
from abom.adapters import instrument from anthropic import Anthropic client = instrument(Anthropic(), manifest="abom.json", gate=True) # use the client exactly as before — calls are now recorded client.messages.create(model="claude-3-5-sonnet-20241022", messages=[...])
What instrument returns. It hands back the same client object — your code keeps calling it normally. It doesn't replace the client; it does two things to it:
- swaps the model method (messages.create) for a wrapped version that gates → calls → records;
- attaches a run handle at client.abom that holds the live record list.
client.abom.chain is that record list — one Action Provenance record per model call, appended as your agent runs. After two calls it has two entries; each is hash-chained to the previous one and bound to the manifest. Read it, save it, or hand it to abom verify:
import json # client.abom.chain is a list of provenance records — one per model call for record in client.abom.chain: print(record["seq"], record["data"]["model_calls"]) # persist it next to the manifest for later verification json.dump(client.abom.chain, open("provenance.json", "w"), indent=2)
Each record looks like this — see the Action Provenance reference for every field:
{
"run_id": "loan-doc-agent",
"seq": 0,
"event_type": "ActionProvenance",
"prev_hash": "GENESIS", # chains to the previous record
"hash": "a1b2c3…",
"data": {
"composition_sha256": "…", # the manifest this ran under
"decision": "model_call",
"model_calls": [
{ "model": "claude-3-5-sonnet-20241022", "tokens": 128, "output_sha256": "…" }
]
}
}On LangChain instead? Frameworks act through tools, so the LangChain adapter is a callback handler rather than a client wrapper — it records model and tool calls, and with gate=True an undeclared tool raises ActionDenied before the tool body runs:
from abom.adapters.langchain import AbomLangChainHandler handler = AbomLangChainHandler(manifest="abom.json", gate=True, sink="auto") # the ONE change to your agent: pass the handler as a callback agent.invoke({"input": "..."}, config={"callbacks": [handler]}) # model + tool records, hash-chained and persisted like any run handler.abom.chain
Verify the run
Each call appends to a hash-chained Action Provenance log bound to the manifest. Verify the whole run — signature, chain integrity, and policy — after the fact.
abom verify abom.json --policy finance.json # ✓ VALID — signature OK, chain intact, policy clean
See the Action Provenance reference for the record shape, and the policy hub for a policy to enforce.
Contribute an adapter
Want a provider that isn't available yet? The fastest path is to email hello@abom.ai with the provider name and SDK — we'll implement it. If you'd like to spec or prototype it, here's the whole contract.
An adapter does exactly two provider-specific things: recognize its client and wrap the one method that calls the model. Everything else — the provenance chain, the gate, binding to the manifest — is shared in abom.adapters.base, so an adapter is small.
The contract
An adapter implements three things; parse normalizes a provider response into a ModelCall.
# what every adapter implements class Adapter(Protocol): provider: str # "acme" def matches(self, client) -> bool: ... # is this my client? def wrap(self, client, run) -> Any: ... # instrument it # the normalized capture each call produces class ModelCall: model: str tokens: int | None output_sha256: str | None endpoint: str | None
Write the adapter
Wrap the provider's model method: gate → call through → record. The run object (shared) does the recording and gating; you only supply provider knowledge.
import functools from .base import AbomRun, ModelCall, _sha256, register ACME_ENDPOINT = "api.acme.ai" def parse(model, response) -> ModelCall: # read the provider's response shape → normalized fields text = response.output_text return ModelCall( model=model, tokens=response.usage.total_tokens, output_sha256=_sha256(text) if text else None, endpoint=ACME_ENDPOINT, ) class AcmeAdapter: provider = "acme" def matches(self, client) -> bool: return type(client).__module__.split(".")[0] == "acme" def wrap(self, client, run: AbomRun): original = client.generate # the method that calls the model @functools.wraps(original) def generate(*args, **kwargs): model = kwargs["model"] run.enforce(ModelCall(model=model, endpoint=ACME_ENDPOINT)) # gate response = original(*args, **kwargs) # call through run.record(parse(model, response)) # record return response client.generate = generate return client register(AcmeAdapter()) # makes instrument() auto-detect it
Duck-type on the client (check the module / attributes) rather than importing the SDK, so the provider package stays an optional dependency.
Test it
Prove the three guarantees with a fake client — no live API, no SDK install. Every shipped adapter has this exact test shape.
def test_records_and_gates(): client = instrument(FakeAcme(), manifest=signed_manifest, gate=True) client.generate(model="acme-1", prompt="hi") chain = client.abom.chain assert chain[0]["data"]["model_calls"][0]["model"] == "acme-1" assert verify_chain(chain)["valid"] # hash chain holds # a model not in the manifest is denied before the call runs bad = instrument(FakeAcme(), manifest=other_manifest, gate=True) with pytest.raises(ActionDenied): bad.generate(model="acme-1", prompt="hi")