Skip to content

Enrichment SDKs

Candela ships zero-dependency enrichment SDKs for five languages. Each SDK injects X-Candela-* headers and W3C Baggage entries into your LLM requests — giving you per-tenant, per-job cost attribution automatically.

Terminal window
pip install candela-sdk
from openai import OpenAI
from candela import CandelaSession
session = CandelaSession(tenant_id="acme-corp", job_id="eval-42")
client = OpenAI(
base_url="http://localhost:1234/v1",
http_client=session.httpx_client(),
)
response = client.chat.completions.create(
model="gemini-2.5-pro",
messages=[{"role": "user", "content": "Hello!"}],
)

Every SDK call produces the same set of headers, regardless of language:

HeaderExample ValuePurpose
X-Candela-Tenant-Idacme-corpExplicit tenant for cost attribution
X-Candela-Job-Ideval-42Job/experiment grouping
Baggagecandela.tenant_id=acme-corp,candela.job_id=eval-42W3C standard propagation

The proxy reads both — Baggage has priority, explicit headers are the fallback. The SDKs set both for maximum compatibility.


Your App → SDK injects headers → Candela Proxy → LLM Provider
X-Candela-Tenant-Id: acme-corp
X-Candela-Job-Id: eval-42
Baggage: candela.tenant_id=acme-corp,candela.job_id=eval-42
Proxy strips X-Candela-* before forwarding to LLM
Proxy records tenant_id + job_id in the span

The proxy strips all X-Candela-* headers before forwarding to the upstream LLM — your metadata never leaks to providers.


All SDKs expose the same three primitives:

PrimitiveDescription
CandelaSessionReusable session with validated tenant/job IDs. Returns headers for any HTTP client.
injectHeadersOne-shot function that mutates a headers dict/map with enrichment metadata.
validateIdValidates an ID against the allowed pattern: [a-zA-Z0-9._-]{1,128}.

All IDs are validated on construction — invalid IDs throw immediately rather than failing silently at the proxy:

Valid: acme-corp, trial_NCT01750580, customer.42
Invalid: has spaces!, <script>, "" (empty)
Pattern: ^[a-zA-Z0-9._-]{1,128}$

ADK agents use httpx under the hood. Create a CandelaSession and pass its pre-configured httpx_client() to ADK’s model — the SDK injects Baggage headers on every outgoing request:

import httpx
from candela import CandelaSession
from google.adk.agents import Agent
from google.adk.models import Gemini
session = CandelaSession(tenant_id="pharma-co", job_id="ctm-trial-abc")
# Pass the SDK's httpx client so enrichment headers are injected on every call
agent = Agent(
model=Gemini(
model="gemini-2.5-flash",
base_url="http://localhost:8080/proxy/google",
http_client=session.httpx_client(),
),
name="clinical_agent",
instruction="You are a clinical trial analyst.",
)

See ADK Integration for full OTel trace correlation setup.


SymptomCauseFix
ValueError: invalid tenant_idID contains spaces or special charsUse only [a-zA-Z0-9._-], max 128 chars
Tenant shows as empty in dashboardSDK not installed / headers not injectedVerify with curl -v that X-Candela-Tenant-Id appears
Baggage not propagatingExisting Baggage header clobberedSDKs append, never overwrite — check for middleware that resets headers
Cost attribution missingProxy not configured to parse enrichmentEnsure candela ≥ v0.2.0 or candela-sidecar ≥ v0.4.0