use everruns_core::message::{ContentPart, Message};
use everruns_core::telemetry::content;
use everruns_provider::tool_types::ToolCall;
use opentelemetry::KeyValue;
pub const SPAN_KIND: &str = "openinference.span.kind";
pub mod span_kind {
pub const AGENT: &str = "AGENT";
pub const LLM: &str = "LLM";
pub const TOOL: &str = "TOOL";
pub const CHAIN: &str = "CHAIN";
}
pub const INPUT_VALUE: &str = "input.value";
pub const INPUT_MIME_TYPE: &str = "input.mime_type";
pub const OUTPUT_VALUE: &str = "output.value";
pub const OUTPUT_MIME_TYPE: &str = "output.mime_type";
pub mod mime {
pub const TEXT: &str = "text/plain";
pub const JSON: &str = "application/json";
}
pub const LLM_MODEL_NAME: &str = "llm.model_name";
pub const LLM_PROVIDER: &str = "llm.provider";
pub const LLM_SYSTEM: &str = "llm.system";
pub const LLM_INVOCATION_PARAMETERS: &str = "llm.invocation_parameters";
pub const LLM_INPUT_MESSAGES: &str = "llm.input_messages";
pub const LLM_OUTPUT_MESSAGES: &str = "llm.output_messages";
pub const LLM_TOOLS: &str = "llm.tools";
pub const LLM_TOKEN_COUNT_PROMPT: &str = "llm.token_count.prompt";
pub const LLM_TOKEN_COUNT_COMPLETION: &str = "llm.token_count.completion";
pub const LLM_TOKEN_COUNT_TOTAL: &str = "llm.token_count.total";
pub const LLM_TOKEN_COUNT_PROMPT_CACHE_READ: &str = "llm.token_count.prompt_details.cache_read";
pub const LLM_TOKEN_COUNT_PROMPT_CACHE_WRITE: &str = "llm.token_count.prompt_details.cache_write";
pub const LLM_COST_TOTAL: &str = "llm.cost.total";
pub const MESSAGE_ROLE: &str = "message.role";
pub const MESSAGE_CONTENT: &str = "message.content";
pub const MESSAGE_TOOL_CALL_ID: &str = "message.tool_call_id";
pub const MESSAGE_TOOL_CALLS: &str = "message.tool_calls";
pub const TOOL_CALL_ID: &str = "tool_call.id";
pub const TOOL_CALL_FUNCTION_NAME: &str = "tool_call.function.name";
pub const TOOL_CALL_FUNCTION_ARGUMENTS: &str = "tool_call.function.arguments";
pub const TOOL_JSON_SCHEMA: &str = "tool.json_schema";
pub const TOOL_NAME: &str = "tool.name";
pub const TOOL_DESCRIPTION: &str = "tool.description";
pub const SESSION_ID: &str = "session.id";
pub const AGENT_NAME: &str = "agent.name";
pub const METADATA: &str = "metadata";
pub const EXCEPTION_TYPE: &str = "exception.type";
pub const EXCEPTION_MESSAGE: &str = "exception.message";
pub fn provider_and_system(driver_id: &str) -> (&str, Option<&'static str>) {
match driver_id {
"openai" | "openai_completions" => ("openai", Some("openai")),
"azure_openai" => ("azure", Some("openai")),
"anthropic" => ("anthropic", Some("anthropic")),
"gemini" => ("google", Some("vertexai")),
"bedrock" => ("aws", None),
"mistral" | "mistral_ai" => ("mistralai", Some("mistralai")),
"cohere" => ("cohere", Some("cohere")),
"xai" | "x_ai" => ("xai", None),
"deepseek" => ("deepseek", None),
other => (other, None),
}
}
pub fn input_message_attributes(index: usize, message: &Message) -> Vec<KeyValue> {
let prefix = format!("{LLM_INPUT_MESSAGES}.{index}");
let mut attrs = vec![KeyValue::new(
format!("{prefix}.{MESSAGE_ROLE}"),
content::role_name(&message.role),
)];
let text = message_text(message);
if !text.is_empty() {
attrs.push(KeyValue::new(format!("{prefix}.{MESSAGE_CONTENT}"), text));
}
if let Some(id) = message.tool_call_id() {
attrs.push(KeyValue::new(
format!("{prefix}.{MESSAGE_TOOL_CALL_ID}"),
id.to_string(),
));
}
let tool_calls: Vec<(&str, &str, &serde_json::Value)> = message
.content
.iter()
.filter_map(|part| match part {
ContentPart::ToolCall(tc) => Some((tc.id.as_str(), tc.name.as_str(), &tc.arguments)),
_ => None,
})
.collect();
for (j, (id, name, arguments)) in tool_calls.into_iter().enumerate() {
attrs.extend(tool_call_attributes(&prefix, j, id, name, arguments));
}
attrs
}
pub fn output_message_attributes(text: Option<&str>, tool_calls: &[ToolCall]) -> Vec<KeyValue> {
let prefix = format!("{LLM_OUTPUT_MESSAGES}.0");
let mut attrs = vec![KeyValue::new(
format!("{prefix}.{MESSAGE_ROLE}"),
"assistant",
)];
if let Some(text) = text.filter(|t| !t.is_empty()) {
attrs.push(KeyValue::new(
format!("{prefix}.{MESSAGE_CONTENT}"),
text.to_string(),
));
}
for (j, call) in tool_calls.iter().enumerate() {
attrs.extend(tool_call_attributes(
&prefix,
j,
&call.id,
&call.name,
&call.arguments,
));
}
attrs
}
fn tool_call_attributes(
message_prefix: &str,
index: usize,
id: &str,
name: &str,
arguments: &serde_json::Value,
) -> Vec<KeyValue> {
let prefix = format!("{message_prefix}.{MESSAGE_TOOL_CALLS}.{index}");
vec![
KeyValue::new(format!("{prefix}.{TOOL_CALL_ID}"), id.to_string()),
KeyValue::new(
format!("{prefix}.{TOOL_CALL_FUNCTION_NAME}"),
name.to_string(),
),
KeyValue::new(
format!("{prefix}.{TOOL_CALL_FUNCTION_ARGUMENTS}"),
arguments.to_string(),
),
]
}
fn message_text(message: &Message) -> String {
let mut chunks: Vec<String> = Vec::new();
for part in &message.content {
match part {
ContentPart::Text(t) => chunks.push(t.text.clone()),
ContentPart::ToolResult(tr) => match (&tr.error, &tr.result) {
(Some(error), _) => chunks.push(format!("Error: {error}")),
(None, Some(serde_json::Value::String(s))) => chunks.push(s.clone()),
(None, Some(value)) => chunks.push(value.to_string()),
(None, None) => {}
},
_ => {}
}
}
chunks.join("\n")
}
pub fn tool_attributes(index: usize, name: &str, description: &str) -> KeyValue {
let schema = serde_json::json!({
"type": "function",
"function": { "name": name, "description": description },
});
KeyValue::new(
format!("{LLM_TOOLS}.{index}.{TOOL_JSON_SCHEMA}"),
schema.to_string(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn attr(attrs: &[KeyValue], key: &str) -> Option<String> {
attrs
.iter()
.find(|kv| kv.key.as_str() == key)
.map(|kv| kv.value.to_string())
}
#[test]
fn input_messages_flatten_role_content_and_tool_calls() {
let call = ToolCall {
id: "call_1".to_string(),
name: "read_file".to_string(),
arguments: json!({ "path": "a.txt" }),
};
let attrs =
input_message_attributes(2, &Message::assistant_with_tools("Reading", vec![call]));
assert_eq!(
attr(&attrs, "llm.input_messages.2.message.role").as_deref(),
Some("assistant")
);
assert_eq!(
attr(&attrs, "llm.input_messages.2.message.content").as_deref(),
Some("Reading")
);
assert_eq!(
attr(
&attrs,
"llm.input_messages.2.message.tool_calls.0.tool_call.function.name"
)
.as_deref(),
Some("read_file")
);
assert_eq!(
attr(
&attrs,
"llm.input_messages.2.message.tool_calls.0.tool_call.function.arguments"
)
.as_deref(),
Some(r#"{"path":"a.txt"}"#)
);
}
#[test]
fn tool_result_messages_carry_their_call_id() {
let attrs = input_message_attributes(
0,
&Message::tool_result("call_9", Some(json!({ "ok": true })), None),
);
assert_eq!(
attr(&attrs, "llm.input_messages.0.message.role").as_deref(),
Some("tool")
);
assert_eq!(
attr(&attrs, "llm.input_messages.0.message.tool_call_id").as_deref(),
Some("call_9")
);
assert_eq!(
attr(&attrs, "llm.input_messages.0.message.content").as_deref(),
Some(r#"{"ok":true}"#)
);
}
#[test]
fn providers_map_to_openinference_vocabulary() {
assert_eq!(provider_and_system("openai"), ("openai", Some("openai")));
assert_eq!(
provider_and_system("azure_openai"),
("azure", Some("openai"))
);
assert_eq!(provider_and_system("gemini"), ("google", Some("vertexai")));
assert_eq!(provider_and_system("openrouter"), ("openrouter", None));
}
}