Multi-tenant
Copy MarkdownTenant data flows through Dsxir.context/2, never through
Dsxir.configure/1. The framework auto-merges :metadata into every
telemetry event so cost dashboards filter by tenant for free:
def call(conn, _opts) do
tenant = conn.assigns.tenant
Dsxir.context(
[
lm: {Dsxir.LM.Sycophant,
[model: tenant.model_id, api_key: tenant.api_key]},
adapter: tenant.adapter,
cache: false,
metadata: %{tenant_id: tenant.id,
request_id: conn.assigns.request_id},
call_plugs: [&MyApp.Quota.check/1, &MyApp.Audit.before_call/1]
],
fn ->
program = Dsxir.load!(MyApp.QA, "tenants/#{tenant.id}/qa.json")
{_program, pred} = MyApp.QA.forward(program, %{question: conn.params["q"]})
pred
end
)
endNotes:
Dsxir.configure/1is for defaults only. It rejectstenant_*keys (both top-level and nested inside:metadata) and:lmtuples whose config carries a non-nil:api_key. Tenant data flows throughDsxir.context/2.cache: falseis the recommended default inside tenant contexts.call_plugsis the hook point for quota, audit, and rate-limit policies. v0 ships the hook only — consumers write their own plugs as 1-arity functions(%Dsxir.CallContext{} -> :ok | {:halt, reason}).
Telemetry events auto-merge the per-context :metadata, so cost
dashboards filter by tenant for free — see Telemetry.