lc_callbacks/semconv.rs
1//! OTel GenAI / MCP semantic-convention names shared by every OTel emission
2//! surface (`OtelHandler` and `OtelTracingBackend`) so the two cannot drift.
3//! Other crates (e.g. `lc-agents`) may also reference the `pub` constants
4//! here instead of hardcoding attribute strings.
5//!
6//! # Stability (T10, v0.23 — 2026 alignment)
7//!
8//! The GenAI semconv is still published at **Development** stability: in 2026
9//! the working group moved the docs to a dedicated repository
10//! (`open-telemetry/semantic-conventions-genai`) and renames still happen
11//! (notably `gen_ai.usage.cache_creation.input_tokens` →
12//! [`GEN_AI_USAGE_CACHE_WRITE`]). Centralizing every name in this module is
13//! deliberate: when the registry moves a name, one constant changes and both
14//! export surfaces follow. The provider-extension and agent/tool attributes
15//! below follow the 2026 draft (`gen-ai-agent-spans.md`, `gen-ai-events.md`,
16//! `mcp.md`); the core chat attributes (`provider.name`, `operation.name`,
17//! request/response model, usage, finish_reasons) follow the March 2025 stable
18//! release.
19//!
20//! MCP client spans live in `lc-mcp`, which cannot depend on this crate;
21//! their `mcp.*` constants are mirrored in `lc_mcp::instrument` with a
22//! cross-reference comment.
23
24#![allow(dead_code)] // Not every surface uses every constant.
25
26/// `gen_ai.provider.name` (stabilized rename of experimental `gen_ai.system`).
27pub const GEN_AI_PROVIDER_NAME: &str = "gen_ai.provider.name";
28/// `gen_ai.operation.name` (chat / execute_tool / invoke_agent / plan / retrieve …).
29pub const GEN_AI_OPERATION_NAME: &str = "gen_ai.operation.name";
30/// `gen_ai.request.model`.
31pub const GEN_AI_REQUEST_MODEL: &str = "gen_ai.request.model";
32/// `gen_ai.response.model`.
33pub const GEN_AI_RESPONSE_MODEL: &str = "gen_ai.response.model";
34/// `gen_ai.usage.input_tokens`.
35pub const GEN_AI_USAGE_INPUT_TOKENS: &str = "gen_ai.usage.input_tokens";
36/// `gen_ai.usage.output_tokens`.
37pub const GEN_AI_USAGE_OUTPUT_TOKENS: &str = "gen_ai.usage.output_tokens";
38/// `gen_ai.response.finish_reasons` (string array, plural).
39pub const GEN_AI_RESPONSE_FINISH_REASONS: &str = "gen_ai.response.finish_reasons";
40/// `gen_ai.request.max_tokens`.
41pub const GEN_AI_REQUEST_MAX_TOKENS: &str = "gen_ai.request.max_tokens";
42/// `gen_ai.request.temperature`.
43pub const GEN_AI_REQUEST_TEMPERATURE: &str = "gen_ai.request.temperature";
44/// `gen_ai.tool.name` (tool spans; conditionally required on MCP tool calls).
45pub const GEN_AI_TOOL_NAME: &str = "gen_ai.tool.name";
46/// `gen_ai.tool.call.id` — the provider tool-call id (Recommended, 2026 draft).
47pub const GEN_AI_TOOL_CALL_ID: &str = "gen_ai.tool.call.id";
48/// `gen_ai.tool.description` (Recommended, 2026 draft).
49pub const GEN_AI_TOOL_DESCRIPTION: &str = "gen_ai.tool.description";
50/// `gen_ai.tool.call.arguments` — Opt-In: recorded only when explicitly enabled.
51pub const GEN_AI_TOOL_CALL_ARGUMENTS: &str = "gen_ai.tool.call.arguments";
52/// `gen_ai.tool.call.result` — Opt-In: recorded only when explicitly enabled.
53pub const GEN_AI_TOOL_CALL_RESULT: &str = "gen_ai.tool.call.result";
54/// `gen_ai.agent.name` — agent identity for `invoke_agent` / `plan` spans.
55pub const GEN_AI_AGENT_NAME: &str = "gen_ai.agent.name";
56
57/// `gen_ai.message.content` — payload key of the legacy per-message span
58/// events (see [`EVENT_INFERENCE_DETAILS`] doc for why those are retained).
59pub const GEN_AI_MESSAGE_CONTENT: &str = "gen_ai.message.content";
60/// `gen_ai.choice.index`.
61pub const GEN_AI_CHOICE_INDEX: &str = "gen_ai.choice.index";
62
63/// `gen_ai.client.inference.operation.details` (2026 events model, Opt-In).
64///
65/// The only GenAI LLM event the current registry defines: a single opt-in
66/// snapshot event on the chat span carrying the operation attributes plus
67/// structured `gen_ai.input.messages` / `gen_ai.output.messages`. It is
68/// emitted solely when the consumer opts in
69/// (`OtelHandler::with_operation_details_event`); the legacy
70/// `gen_ai.{system,user,assistant,tool}.message` / `gen_ai.choice.message`
71/// span events (removed from the registry in the 2025 events redesign) stay
72/// on by default because shipping backends (Langfuse, Datadog, New Relic)
73/// still key off them.
74pub const EVENT_INFERENCE_DETAILS: &str = "gen_ai.client.inference.operation.details";
75/// Structured input messages JSON on [`EVENT_INFERENCE_DETAILS`].
76pub const GEN_AI_INPUT_MESSAGES: &str = "gen_ai.input.messages";
77/// Structured output messages JSON on [`EVENT_INFERENCE_DETAILS`].
78pub const GEN_AI_OUTPUT_MESSAGES: &str = "gen_ai.output.messages";
79
80/// Standard error attribute (OTel general semconv).
81pub const ERROR_TYPE: &str = "error.type";
82
83/// Framework join key: the run id (also carried on evaluation reports).
84pub const RUN_ID_ATTR: &str = "langchainrust.run_id";
85/// Framework join key: the trace id a run belongs to.
86pub const TRACE_ID_ATTR: &str = "langchainrust.trace_id";
87
88// Provider-extension usage attributes (Development stability). Values SHOULD
89// be included in `gen_ai.usage.input_tokens` totals by backend convention.
90/// `gen_ai.usage.cache_read.input_tokens` (Anthropic prompt-cache hits).
91pub const GEN_AI_USAGE_CACHE_READ: &str = "gen_ai.usage.cache_read.input_tokens";
92/// `gen_ai.usage.cache_write.input_tokens` — the 2026-draft rename of
93/// `gen_ai.usage.cache_creation.input_tokens` (still the published name in
94/// registry v1.40). Provider payloads use *both* spellings
95/// (`cache_write_input_tokens` and the Anthropic legacy
96/// `cache_creation_input_tokens`); the handler accepts either key.
97pub const GEN_AI_USAGE_CACHE_WRITE: &str = "gen_ai.usage.cache_write.input_tokens";
98/// `gen_ai.usage.reasoning.output_tokens` (reasoning/thinking tokens).
99pub const GEN_AI_USAGE_REASONING: &str = "gen_ai.usage.reasoning.output_tokens";
100
101/// Cap for `error.type` values (it is a type, not a message).
102const MAX_ERROR_TYPE_CHARS: usize = 64;
103
104/// Derives an `error.type`-style short type from an error message: the first
105/// line, cut at the first colon. Falls back to `"error"`.
106pub(crate) fn error_type(message: &str) -> String {
107 let first = message.lines().next().unwrap_or("").trim();
108 let head = first.split(':').next().unwrap_or(first).trim();
109 if head.is_empty() {
110 "error".to_string()
111 } else {
112 truncate(head, MAX_ERROR_TYPE_CHARS)
113 }
114}
115
116/// Unicode-safe truncation (chars, not bytes), appends an ellipsis when cut.
117pub(crate) fn truncate(s: &str, max_chars: usize) -> String {
118 match s.char_indices().nth(max_chars) {
119 None => s.to_string(),
120 Some((idx, _)) => format!("{}…", &s[..idx]),
121 }
122}