Skip to main content
Attributes are how you slice everything in the dashboard: filter by user, break down spend by model, segment by your own experiment ID. Some are built into the SDK (user_id, model, convo_id); the rest you define yourself with properties={...}. Skip a kwarg and the dashboard column behind it stays empty for that call.

The built-in fields

Each built-in kwarg maps to one OTel attribute and one dashboard column. The mapping is the same for track_ai, begin, interaction.update, interaction.finish, and tool_span. Unpassed kwargs are omitted from the span, not set to null. A few of these fields earn extra attention: provider has no auto-inference. A model name like claude-3-5-sonnet isn’t auto-tagged as Anthropic, and Bedrock model IDs (anthropic.claude-3-...) are ambiguous on purpose. Pass provider= explicitly: "openai", "anthropic", "google", "aws_bedrock", etc. Bedrock routes through AWS, so a Bedrock call wants provider="aws_bedrock", not "anthropic". convo_id is the only way to group turns. Without it, multi-turn conversations look like N independent requests. See Sessions and users. user_id drives user filtering. It’s required to filter the trace list by user, and it’s a pass-through string only. Bento doesn’t store profile data: no email, no name, no traits. model feeds the cost view. The cost view and per-model breakdowns key off gen_ai.request.model. Without it, spend rolls up under “Unknown”.

Custom attributes via properties

properties covers anything you want to filter or aggregate on that the built-in fields don’t.
Each key in properties becomes a top-level OTel attribute with the same name. No namespace is added.
Don’t put gen_ai.*, input.value, output.value, or openinference.span.kind in properties. SDK kwargs are written after properties and will silently overwrite them. Use the dedicated kwargs.

Type fidelity

Property values keep their type when possible, so downstream filters and aggregates work. The JSON-string fallback is lossless but won’t be filterable as a number or bool. Flatten dicts to multiple keys (user.tier, user.region) if you need numeric filters.

Span kind

The kind of a span lives in openinference.span.kind. The SDK sets it for you: OpenInference defines other kinds (retriever, embedding, agent, chain). Set them through properties:

What an emitted span looks like

The OTel JSON for a track_ai call with properties={"experiment_id": 17}:
The same attributes show up in any OTel backend you wire this into. Nothing about your code is Bento-specific. The exporter batches spans, so a short-lived process can exit before they ship. Call bento.flush() (or bento.shutdown()) before the process ends.