Skip to main content

Attribute conventions

The observability ecosystem hasn't agreed on attribute names for LLM telemetry yet. Langfuse and SigNoz use the (pre-standard) gen_ai.* convention from OTel's GenAI SIG; Phoenix uses Arize's OpenInference with llm.token_count.* and input.value / output.value.

hermes-otel emits both conventions on the same span so whichever backend reads from it picks up the data it's expecting. No vendor-specific adapter code per backend.

Token counts (on api.* spans)

MetricLangfuse / gen_aiPhoenix / OpenInference
Prompt tokensgen_ai.usage.input_tokensllm.token_count.prompt
Completion tokensgen_ai.usage.output_tokensllm.token_count.completion
Total tokensllm.token_count.total
Cache readgen_ai.usage.cache_read_input_tokensllm.token_count.cache_read
Cache writegen_ai.usage.cache_creation_input_tokensllm.token_count.cache_write

Phoenix adds a total variant that's the sum; gen_ai doesn't. Cache read/write are only populated when the provider reports them (Anthropic's prompt caching, OpenAI's — both surface them in their API responses).

Message content (on llm.* spans)

Langfuse / gen_aiPhoenix / OpenInference
User messagegen_ai.content.promptinput.value
Assistant responsegen_ai.content.completionoutput.value
Content type(not set)input.mime_type, output.mime_type

When conversation capture is on, input.value becomes JSON of the full message list, input.mime_type becomes application/json, and hermes.conversation.message_count records how many messages were passed.

Model / request metadata

Langfuse / gen_aiPhoenix / OpenInference
Model namegen_ai.request.modelllm.model_name
Providergen_ai.systemllm.provider
Invocation paramsllm.invocation_parameters (JSON)
Finish reasongen_ai.response.finish_reason(same)

llm.invocation_parameters is a JSON blob with the request params (temperature, max_tokens, tool schemas, etc.) that Phoenix pretty-prints in the UI.

Tool spans

Tool-span attributes are largely OpenInference-native (the gen_ai.* convention doesn't have tool-specific names yet). Both backends index on them:

AttributeMeaning
tool.nameTool name
input.valueTool args (JSON string)
output.valueTool result (string)
hermes.tool.targetInferred file / URL (plugin-specific)
hermes.tool.commandInferred shell command (plugin-specific)
hermes.tool.outcomecompleted / error / timeout / blocked (plugin-specific)
hermes.skill.nameInferred skill name (plugin-specific, optional)

See Tool identity.

Session / turn metadata (on session.*)

All plugin-specific:

AttributeMeaning
openinference.project.nameProject name from OTEL_PROJECT_NAME
hermes.session.kindcli / telegram / discord / cron / ...
hermes.session.idHermes session ID
session.idStandard OTel alias of the above
user.idHermes user ID
hermes.turn.*Turn summary (see Turn summary)

Resource-level attributes

Set on the OTel Resource and therefore stamped on every span:

AttributeSource
service.nameOTEL_PROJECT_NAME (falls back to "hermes-agent")
service.versionhermes-otel plugin version
otel.scope.namehermes-otel
openinference.project.nameSame as service.name
plus any resource_attributes: / global_tags: from config.yaml

Why dual-convention rather than pick one?

Every backend supports a different set. Emitting both is cheap (same span, a few extra key-value pairs) and saves every user from writing their own mapping adapter. When the GenAI spec stabilises and Phoenix/Langfuse converge, this will simplify.

Roadmap

  • OpenInference Tool span kind is now stable — already emitted.
  • gen_ai.tool.* convention is evolving; we'll add it once the spec is stable.
  • session.id is the standard OTel name; the plugin emits both hermes.session.id (for compatibility with older backends that key on it) and session.id. The former may be dropped in a future major version — watch the changelog.