1#[cfg(feature = "otel")]
4use opentelemetry_semantic_conventions::attribute as semconv;
5
6#[cfg(feature = "otel")]
8pub const OTEL_SEMCONV_SCHEMA_URL: &str = opentelemetry_semantic_conventions::SCHEMA_URL;
9
10#[cfg(not(feature = "otel"))]
12pub const OTEL_SEMCONV_SCHEMA_URL: &str = "https://opentelemetry.io/schemas/1.31.0";
13
14pub const GEN_AI_TOOL_CALL_SPAN_NAME: &str = "gen_ai.tool.call";
16
17pub const GEN_AI_TOOL_CALL_OPERATION_NAME: &str = "tool.call";
19
20#[cfg(feature = "otel")]
21pub const ATTR_GEN_AI_SYSTEM: &str = semconv::GEN_AI_SYSTEM;
22#[cfg(not(feature = "otel"))]
23pub const ATTR_GEN_AI_SYSTEM: &str = "gen_ai.system";
24
25#[cfg(feature = "otel")]
26pub const ATTR_GEN_AI_OPERATION_NAME: &str = semconv::GEN_AI_OPERATION_NAME;
27#[cfg(not(feature = "otel"))]
28pub const ATTR_GEN_AI_OPERATION_NAME: &str = "gen_ai.operation.name";
29
30#[cfg(feature = "otel")]
31pub const ATTR_GEN_AI_REQUEST_MODEL: &str = semconv::GEN_AI_REQUEST_MODEL;
32#[cfg(not(feature = "otel"))]
33pub const ATTR_GEN_AI_REQUEST_MODEL: &str = "gen_ai.request.model";
34
35#[cfg(feature = "otel")]
36pub const ATTR_GEN_AI_TOOL_CALL_ID: &str = semconv::GEN_AI_TOOL_CALL_ID;
37#[cfg(not(feature = "otel"))]
38pub const ATTR_GEN_AI_TOOL_CALL_ID: &str = "gen_ai.tool.call.id";
39
40#[cfg(feature = "otel")]
41pub const ATTR_GEN_AI_TOOL_NAME: &str = semconv::GEN_AI_TOOL_NAME;
42#[cfg(not(feature = "otel"))]
43pub const ATTR_GEN_AI_TOOL_NAME: &str = "gen_ai.tool.name";
44
45#[cfg(feature = "otel")]
46pub const ATTR_GEN_AI_RESPONSE_FINISH_REASONS: &str = semconv::GEN_AI_RESPONSE_FINISH_REASONS;
47#[cfg(not(feature = "otel"))]
48pub const ATTR_GEN_AI_RESPONSE_FINISH_REASONS: &str = "gen_ai.response.finish_reasons";
49
50#[cfg(feature = "otel")]
51pub const ATTR_GEN_AI_USAGE_INPUT_TOKENS: &str = semconv::GEN_AI_USAGE_INPUT_TOKENS;
52#[cfg(not(feature = "otel"))]
53pub const ATTR_GEN_AI_USAGE_INPUT_TOKENS: &str = "gen_ai.usage.input_tokens";
54
55#[cfg(feature = "otel")]
56pub const ATTR_GEN_AI_USAGE_OUTPUT_TOKENS: &str = semconv::GEN_AI_USAGE_OUTPUT_TOKENS;
57#[cfg(not(feature = "otel"))]
58pub const ATTR_GEN_AI_USAGE_OUTPUT_TOKENS: &str = "gen_ai.usage.output_tokens";
59
60pub const ATTR_CHIO_RECEIPT_ID: &str = "chio.receipt.id";
61pub const ATTR_CHIO_KERNEL_ID: &str = "chio.kernel.id";
62pub const ATTR_CHIO_SERVER_ID: &str = "chio.server.id";
63pub const ATTR_CHIO_AGENT_ID: &str = "chio.agent.id";
64
65pub const GEN_AI_TOOL_CALL_LOCKED_ATTRIBUTES: [&str; 12] = [
67 ATTR_GEN_AI_SYSTEM,
68 ATTR_GEN_AI_OPERATION_NAME,
69 ATTR_GEN_AI_REQUEST_MODEL,
70 ATTR_GEN_AI_TOOL_CALL_ID,
71 ATTR_GEN_AI_TOOL_NAME,
72 ATTR_GEN_AI_RESPONSE_FINISH_REASONS,
73 ATTR_GEN_AI_USAGE_INPUT_TOKENS,
74 ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
75 ATTR_CHIO_RECEIPT_ID,
76 ATTR_CHIO_KERNEL_ID,
77 ATTR_CHIO_SERVER_ID,
78 ATTR_CHIO_AGENT_ID,
79];
80
81pub const ATTRIBUTE_VALUE_MAX_CHARS: usize = 128;
82pub const FINISH_REASONS_MAX_CHARS: usize = 192;
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum AttributeCardinality {
87 Low,
89 Bounded,
91 High,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct OtelAttribute {
99 pub key: &'static str,
100 pub value: serde_json::Value,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct GenAiToolCallSpan {
106 pub name: &'static str,
107 pub schema_url: &'static str,
108 pub attributes: Vec<OtelAttribute>,
109}
110
111#[derive(Debug, Clone, Copy)]
113pub struct GenAiToolCallSpanInput<'a> {
114 pub system: &'a str,
115 pub request_model: Option<&'a str>,
116 pub tool_call_id: &'a str,
117 pub tool_name: &'a str,
118 pub finish_reasons: &'a [&'a str],
119 pub usage_input_tokens: Option<u64>,
120 pub usage_output_tokens: Option<u64>,
121 pub chio_receipt_id: Option<&'a str>,
122 pub chio_kernel_id: Option<&'a str>,
123 pub chio_server_id: Option<&'a str>,
124 pub chio_agent_id: Option<&'a str>,
125}
126
127impl GenAiToolCallSpan {
128 pub fn attribute_keys(&self) -> impl Iterator<Item = &'static str> + '_ {
129 self.attributes.iter().map(|attribute| attribute.key)
130 }
131}
132
133pub fn build_gen_ai_tool_call_span(input: GenAiToolCallSpanInput<'_>) -> GenAiToolCallSpan {
135 let mut attributes = Vec::with_capacity(GEN_AI_TOOL_CALL_LOCKED_ATTRIBUTES.len());
136 push_attr(&mut attributes, ATTR_GEN_AI_SYSTEM, input.system);
137 push_attr(
138 &mut attributes,
139 ATTR_GEN_AI_OPERATION_NAME,
140 GEN_AI_TOOL_CALL_OPERATION_NAME,
141 );
142 if let Some(request_model) = input.request_model {
143 push_attr(&mut attributes, ATTR_GEN_AI_REQUEST_MODEL, request_model);
144 }
145 push_attr(
146 &mut attributes,
147 ATTR_GEN_AI_TOOL_CALL_ID,
148 input.tool_call_id,
149 );
150 push_attr(&mut attributes, ATTR_GEN_AI_TOOL_NAME, input.tool_name);
151 if !input.finish_reasons.is_empty() {
152 push_string_array_attr(
153 &mut attributes,
154 ATTR_GEN_AI_RESPONSE_FINISH_REASONS,
155 input.finish_reasons,
156 );
157 }
158 if let Some(tokens) = input.usage_input_tokens {
159 push_u64_attr(&mut attributes, ATTR_GEN_AI_USAGE_INPUT_TOKENS, tokens);
160 }
161 if let Some(tokens) = input.usage_output_tokens {
162 push_u64_attr(&mut attributes, ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, tokens);
163 }
164 if let Some(receipt_id) = input.chio_receipt_id {
165 push_attr(&mut attributes, ATTR_CHIO_RECEIPT_ID, receipt_id);
166 }
167 if let Some(kernel_id) = input.chio_kernel_id {
168 push_attr(&mut attributes, ATTR_CHIO_KERNEL_ID, kernel_id);
169 }
170 if let Some(server_id) = input.chio_server_id {
171 push_attr(&mut attributes, ATTR_CHIO_SERVER_ID, server_id);
172 }
173 if let Some(agent_id) = input.chio_agent_id {
174 push_attr(&mut attributes, ATTR_CHIO_AGENT_ID, agent_id);
175 }
176
177 GenAiToolCallSpan {
178 name: GEN_AI_TOOL_CALL_SPAN_NAME,
179 schema_url: OTEL_SEMCONV_SCHEMA_URL,
180 attributes,
181 }
182}
183
184pub fn attribute_cardinality(key: &str) -> Option<AttributeCardinality> {
185 match key {
186 ATTR_GEN_AI_SYSTEM | ATTR_GEN_AI_OPERATION_NAME | ATTR_GEN_AI_RESPONSE_FINISH_REASONS => {
187 Some(AttributeCardinality::Low)
188 }
189 ATTR_GEN_AI_REQUEST_MODEL
190 | ATTR_GEN_AI_TOOL_NAME
191 | ATTR_GEN_AI_USAGE_INPUT_TOKENS
192 | ATTR_GEN_AI_USAGE_OUTPUT_TOKENS
193 | ATTR_CHIO_KERNEL_ID
194 | ATTR_CHIO_SERVER_ID
195 | ATTR_CHIO_AGENT_ID => Some(AttributeCardinality::Bounded),
196 ATTR_GEN_AI_TOOL_CALL_ID | ATTR_CHIO_RECEIPT_ID => Some(AttributeCardinality::High),
197 _ => None,
198 }
199}
200
201pub fn is_locked_attribute(key: &str) -> bool {
202 GEN_AI_TOOL_CALL_LOCKED_ATTRIBUTES.contains(&key)
203}
204
205fn push_attr(attributes: &mut Vec<OtelAttribute>, key: &'static str, value: &str) {
206 push_attr_with_limit(attributes, key, value, ATTRIBUTE_VALUE_MAX_CHARS);
207}
208
209fn push_attr_with_limit(
210 attributes: &mut Vec<OtelAttribute>,
211 key: &'static str,
212 value: &str,
213 max_chars: usize,
214) {
215 attributes.push(OtelAttribute {
216 key,
217 value: serde_json::Value::String(truncate_chars(value, max_chars)),
218 });
219}
220
221fn push_string_array_attr(attributes: &mut Vec<OtelAttribute>, key: &'static str, values: &[&str]) {
222 attributes.push(OtelAttribute {
223 key,
224 value: serde_json::Value::Array(
225 values
226 .iter()
227 .map(|value| {
228 serde_json::Value::String(truncate_chars(value, FINISH_REASONS_MAX_CHARS))
229 })
230 .collect(),
231 ),
232 });
233}
234
235fn push_u64_attr(attributes: &mut Vec<OtelAttribute>, key: &'static str, value: u64) {
236 attributes.push(OtelAttribute {
237 key,
238 value: serde_json::Value::Number(serde_json::Number::from(value)),
239 });
240}
241
242fn truncate_chars(value: &str, max_chars: usize) -> String {
243 value.chars().take(max_chars).collect()
244}