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.
Quick Start
Section titled “Quick Start”pip install candela-sdkfrom openai import OpenAIfrom 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!"}],)npm install @candelahq/sdkimport { CandelaSession } from "@candelahq/sdk";import OpenAI from "openai";
const session = new CandelaSession({ tenantId: "acme-corp", jobId: "eval-42",});
// With OpenAI SDKconst client = new OpenAI({ baseURL: "http://localhost:1234/v1", defaultHeaders: session.headers(),});
// Or wrap fetch directlyconst cfetch = session.wrapFetch(fetch);import ( "net/http" candela "github.com/candelahq/candela/sdks/go")
// Wrap any http.RoundTripperclient := &http.Client{ Transport: candela.NewTransport(nil, candela.WithTenantID("acme-corp"), candela.WithJobID("eval-42"), ),}
// All requests through this client carry enrichment headers.resp, err := client.Do(req)import com.candelahq.sdk.CandelaSession
val session = CandelaSession( tenantId = "acme-corp", jobId = "eval-42",)
// Use with any HTTP clientval headers = session.headers()// headers["X-Candela-Tenant-Id"] == "acme-corp"// headers["Baggage"] == "candela.tenant_id=acme-corp,candela.job_id=eval-42"[dependencies]candela-sdk = "0.1"use candela_sdk::CandelaSession;
let session = CandelaSession::builder() .tenant_id("acme-corp") .job_id("eval-42") .build()?;
let headers = session.headers();// Use with reqwest, hyper, or any HTTP clientWhat Gets Injected
Section titled “What Gets Injected”Every SDK call produces the same set of headers, regardless of language:
| Header | Example Value | Purpose |
|---|---|---|
X-Candela-Tenant-Id | acme-corp | Explicit tenant for cost attribution |
X-Candela-Job-Id | eval-42 | Job/experiment grouping |
Baggage | candela.tenant_id=acme-corp,candela.job_id=eval-42 | W3C standard propagation |
The proxy reads both — Baggage has priority, explicit headers are the fallback. The SDKs set both for maximum compatibility.
Architecture
Section titled “Architecture”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 spanThe proxy strips all X-Candela-* headers before forwarding to the upstream LLM — your metadata never leaks to providers.
API Reference
Section titled “API Reference”All SDKs expose the same three primitives:
| Primitive | Description |
|---|---|
CandelaSession | Reusable session with validated tenant/job IDs. Returns headers for any HTTP client. |
injectHeaders | One-shot function that mutates a headers dict/map with enrichment metadata. |
validateId | Validates an ID against the allowed pattern: [a-zA-Z0-9._-]{1,128}. |
ID Validation
Section titled “ID Validation”All IDs are validated on construction — invalid IDs throw immediately rather than failing silently at the proxy:
Valid: acme-corp, trial_NCT01750580, customer.42Invalid: has spaces!, <script>, "" (empty)Pattern: ^[a-zA-Z0-9._-]{1,128}$Using with Google ADK
Section titled “Using with Google ADK”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 httpxfrom candela import CandelaSessionfrom google.adk.agents import Agentfrom 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 callagent = 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.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
ValueError: invalid tenant_id | ID contains spaces or special chars | Use only [a-zA-Z0-9._-], max 128 chars |
| Tenant shows as empty in dashboard | SDK not installed / headers not injected | Verify with curl -v that X-Candela-Tenant-Id appears |
| Baggage not propagating | Existing Baggage header clobbered | SDKs append, never overwrite — check for middleware that resets headers |
| Cost attribution missing | Proxy not configured to parse enrichment | Ensure candela ≥ v0.2.0 or candela-sidecar ≥ v0.4.0 |