keld
Drop-in OTLP telemetry for applications that call the openai or anthropic Python
clients directly, boto3's Bedrock runtime client, or any OpenAI-compatible endpoint
(OpenRouter, vLLM, Together, Groq, Ollama, ...) via a custom base_url.
Call keld.init() once at startup and every inference call your app already
makes is reported to a Keld Atlas backend as usage telemetry — zero
call-site changes. openai is observed via the official OpenTelemetry instrumentation
(class-level, covering every client instance); anthropic and bedrock clients are patched
in place. Requests still go straight to the real provider, untouched. Keld is never in
the request path: no added latency of consequence, and an Atlas outage can never break
your app.
Install
pip install "keld[openai]"
The [openai] extra installs the OpenTelemetry instrumentation that openai reporting
requires. Without it, openai calls are not reported (the SDK logs one warning and
your app runs normally). Apps that only use anthropic/bedrock can install the bare
package:
pip install keld
Python 3.10 or newer.
The only runtime dependency of the bare package is httpx. Installing this SDK never
pulls in openai, anthropic, or boto3: it patches whichever of them your app
already has.
To install an unreleased commit straight from the repo:
pip install "keld[openai] @ git+https://github.com/ncx-ai/atlas-telemetry-python@main"
Usage
import keld
keld.init(
ingest_token="kagt_...", # or ATLAS_INGEST_TOKEN env var
endpoint="https://atlas.example.com", # or ATLAS_ENDPOINT env var
environment="production", # optional
repo="acme/checkout-service", # optional
)
Call this before (or after — see "Clients constructed before init()" below) you
construct your openai.OpenAI() / anthropic.Anthropic() / boto3.client("bedrock- runtime") client. From then on, every chat.completions.create, messages.create,
invoke_model, and converse call — streaming or not — is reported.
Clients constructed before init()
If an anthropic or bedrock client already exists by the time init() runs (common in
scripts and notebooks, or when a client is built inside a third-party library you don't
control), pass it explicitly:
keld.init(
ingest_token="kagt_...",
endpoint="https://atlas.example.com",
clients=[my_anthropic_client, my_bedrock_client],
)
openai clients never need this: the OpenTelemetry instrumentation wraps the openai
resource classes, so clients constructed before or after init() are both covered
(passing one in clients is harmless and does nothing).
Custom / self-hosted OpenAI-compatible endpoints
OpenRouter, vLLM, Together, Groq, Ollama and similar gateways are attributed
automatically from the endpoint's host (via the span's server.address; Ollama is also
recognized by its registered port 11434). An unrecognized host is reported as
hosted_vllm with a one-time warning — a confidently-wrong provider label would price
at $0 while looking like real data. For such endpoints pass an explicit override:
keld.init(..., provider="ollama")
Configuration
All of the following can be set as init() keyword arguments or as environment
variables (the kwarg wins if both are set):
| Env var | init() kwarg |
Required | What it is |
|---|---|---|---|
ATLAS_INGEST_TOKEN |
ingest_token |
Yes | A Keld Atlas agent key (kagt_...), obtained from the Atlas Integrations page. This is not an inference API key — see below. |
ATLAS_ENDPOINT |
endpoint |
Yes | Your Keld Atlas deployment's base URL, e.g. https://atlas.example.com. The SDK appends /v1/logs itself. |
ATLAS_ENVIRONMENT |
environment |
No | Freeform deploy environment label (production, staging, ...); feeds Atlas's CapEx/OpEx classification. |
ATLAS_REPO |
repo |
No | Freeform string identifying the calling application/repo, e.g. acme/checkout-service. |
| — | gzip |
No | Default True. Gzips the OTLP export body and sends Content-Encoding: gzip; pass gzip=False to send plain JSON. |
There is no env var for clients= or provider= — those only make sense as explicit
init() arguments.
ATLAS_INGEST_TOKEN is not your provider API key
This is the single most common point of confusion, so to be unmissable:
ATLAS_INGEST_TOKEN(kagt_...) is a Keld Atlas credential. It authenticates telemetry reports to Atlas. Get it from your Atlas admin's Integrations page.- Your OpenAI API key, Anthropic API key, and AWS credentials (for Bedrock)
are configured completely separately, the normal way each provider's own client/SDK
already expects (
OPENAI_API_KEY,ANTHROPIC_API_KEY, AWS credential chain, etc). - This SDK never sees, reads, or transmits your inference provider credentials. It only reads the response of calls your own client makes with your own credentials — token counts, model id, request id, timing — never request/response content, and never your keys.
What gets sent (and what doesn't)
Every call is reported as one OTLP log record: provider, model, input_tokens,
output_tokens, cache_read_tokens, cache_creation_tokens, duration_ms,
request_id, and stream (anthropic/bedrock streaming calls only — openai records on
this branch carry no stream marker) — plus a session.id /
event.sequence / prompt.id the SDK mints itself. Token counts always come from the
provider's own response; the SDK never estimates them.
Never sent: prompt text, response/completion text, message bodies, or cost_usd.
Atlas recomputes cost server-side from a price table — sending a cost figure here would
be redundant at best and wrong (stale/incomplete) at worst.
Failure modes — the SDK is designed to disappear
init()never raises. A client it can't find or patch is skipped with one warning log; the rest of your app runs exactly as if unpatched.init()is idempotent — call it twice and the second call is a no-op.- A bug in our extraction/export code is caught and logged; it can never surface to
or break your call. A real error from the provider always propagates to you
unchanged, exactly as if the SDK weren't installed — it's also recorded as a
zero-token event with an
errorattribute, so error rates stay visible in Atlas. - Export runs on a background thread over a bounded queue that drops the oldest entries under backpressure. It never blocks your request thread and never blocks on network I/O.
- A slow or unreachable Atlas endpoint costs you nothing beyond a few dropped telemetry rows — never added request latency, never a failed provider call.
Streaming
OpenAI only includes token usage on a streamed response when the request carries
stream_options={"include_usage": True}. The SDK injects that by default — the one
place it modifies an outgoing request (telemetry capture itself never touches requests;
it's OpenTelemetry-based). Every chunk, including the resulting usage-only chunk with
empty choices, still passes through to your code unchanged. A caller's own
stream_options is merged with, never replaced. Opt out with:
keld.init(..., inject_stream_usage=False)
The consequence of opting out: streamed openai rows report zero tokens. Opt out if your
OpenAI-compatible gateway rejects stream_options.
Anthropic and Bedrock streaming carry usage in their own event/metadata shapes and need no request changes — usage is accumulated from the stream as it passes through.
Bedrock specifics
request_id(the AWS request id) is always sent for Bedrock calls — Atlas uses it to de-duplicate against a separately-deployed AWS-side CloudWatch collector, if your org runs one.modelis sent exactly as passed toinvoke_model/converse(including inference profile ids/ARNs) — Atlas normalizes these server-side, this SDK never transforms them.invoke_model's response body is a one-shot stream; the SDK reads it once to extract usage and replaces it with an equivalent fresh stream over the same bytes, so your ownresponse["body"].read()still works exactly as if unpatched.
Development
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[test]"
pytest -v
The contract test (tests/test_contract.py) asserts the SDK's OTLP export payload is
structurally identical to tests/fixtures/atlas-sdk-otlp-export.golden.json, a copy of
the normative fixture published by the keld-atlas backend
(services/api/tests/fixtures/atlas-sdk-otlp-export.golden.json). If you touch the wire
format, that fixture — and its counterpart in keld-atlas — is the thing to keep in
sync; a diff there is the signal the contract moved, not something to "fix" locally.
Releasing to PyPI
Releases are published by .github/workflows/release.yml using
PyPI Trusted Publishing, so no API token is
stored in this repo. One-time setup registers the publisher at
https://pypi.org/manage/project/keld/settings/publishing/ (Your projects → keld →
Manage → Publishing): owner ncx-ai, repository atlas-telemetry-python, workflow
release.yml, environment pypi. Add a second entry on TestPyPI with environment
testpypi for dry runs.
Use that project page, not the account-level pending publisher form. Pending
publishers only accept names that do not exist on PyPI yet; keld does, so that form
fails with "project already exists". PyPI returns the same message whether you own the
project or not, so a failure there is about which form you used, or about your role.
That step needs the PyPI Owner role on the project. A Maintainer can upload releases and nothing else, so a Maintainer can neither add a trusted publisher nor yank a release.
keld on PyPI previously held the Keld CLI at 0.1.0. This SDK starts at 1.0.0: PyPI
versions are immutable, so the CLI's release can be yanked but never replaced.
To cut a release:
-
Bump
__version__insrc/keld/_version.py. It is the single source of truth;pyproject.tomlreads it. -
Move the
Unreleasednotes into a new version section inCHANGELOG.md. -
Merge to
main, then tag and push:git tag v0.1.0 && git push origin v0.1.0
The workflow verifies the tag matches __version__, builds an sdist and a wheel, runs
the suite against the built wheel on Python 3.10 through 3.13, and only then publishes.
Running the workflow manually (Actions → Release → Run workflow) does the same build and
publishes to TestPyPI instead.
License
MIT
Metadata
Release files for keld 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| keld-1.0.0.tar.gz | 39.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| keld-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.3 kB
Release files / keld-1.0.0.tar.gz
| Download URL | keld-1.0.0.tar.gz |
|---|---|
| Size | 39.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1409205cc590a2fd7f431bd021df06e26e20a762a468843f8445b4bb8bf2323d
|
|
BLAKE2b-256 checksum How to use checksums |
bfa3aaef4fa11e8df12f170d266f92f74239dd042cfb8c8e56443f5d84021537
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency logRelease files / keld-1.0.0-py3-none-any.whl
| Download URL | keld-1.0.0-py3-none-any.whl |
|---|---|
| Size | 26.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3d5e3c0dfe37eedb56841b55e7b047173988f1e83096e2917279700b5c3d8f6c
|
|
BLAKE2b-256 checksum How to use checksums |
7c40d5912660a916c389fc0c705efebf5e7e31b65722ba54cccc67b4a8170c3d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency log