1212
1313from alphatrion .storage import runtime
1414from alphatrion .storage .sql_models import Status
15- from alphatrion .tracing .clickhouse_exporter import determine_semantic_kind
15+ from alphatrion .tracing .span_processor import (
16+ SEMANTIC_KIND_CHAT ,
17+ SEMANTIC_KIND_REASONING ,
18+ SEMANTIC_KIND_TOOL ,
19+ SEMANTIC_KIND_UNKNOWN ,
20+ )
21+ from alphatrion .utils .pricing import calculate_cost
1622
1723
1824def handle_hook (hook_type : str ):
@@ -548,15 +554,6 @@ def process_transcript_incremental(
548554 # Turn is complete, create run and multiple LLM spans
549555 user_message = current_user_msg .get ("message" , {})
550556
551- # Calculate total tokens and duration from all messages
552- total_input_tokens = 0
553- total_output_tokens = 0
554- for msg in current_assistant_messages :
555- msg_usage = msg .get ("message" , {}).get ("usage" , {})
556- total_input_tokens += msg_usage .get ("input_tokens" , 0 )
557- total_output_tokens += msg_usage .get ("output_tokens" , 0 )
558- total_tokens = total_input_tokens + total_output_tokens
559-
560557 # Calculate duration from timestamps
561558 # (first user message to last assistant message)
562559 duration = calculate_duration (
@@ -598,11 +595,6 @@ def process_transcript_incremental(
598595 user_id = session .user_id ,
599596 status = run_status ,
600597 duration = duration ,
601- usage = {
602- "input_tokens" : total_input_tokens ,
603- "output_tokens" : total_output_tokens ,
604- "total_tokens" : total_tokens ,
605- },
606598 )
607599
608600 # Prepare user content (original user message only)
@@ -1009,8 +1001,12 @@ def create_clickhouse_spans_for_turn(
10091001 msg_content = []
10101002
10111003 msg_usage = message_data .get ("usage" , {})
1012- msg_input_tokens = msg_usage .get ("input_tokens" , 0 )
1013- msg_output_tokens = msg_usage .get ("output_tokens" , 0 )
1004+ input_tokens = msg_usage .get ("input_tokens" , 0 )
1005+ output_tokens = msg_usage .get ("output_tokens" , 0 )
1006+ cache_creation_input_tokens = msg_usage .get (
1007+ "cache_creation_input_tokens" , 0
1008+ )
1009+ cache_read_input_tokens = msg_usage .get ("cache_read_input_tokens" , 0 )
10141010
10151011 # Determine content type
10161012 tool_use_blocks = [
@@ -1083,13 +1079,17 @@ def create_clickhouse_spans_for_turn(
10831079 span_id = str (uuid .uuid4 ()).replace ("-" , "" )[:16 ]
10841080
10851081 # Determine semantic kind
1086- semantic_kind = determine_semantic_kind ({}, msg_content )
1082+ semantic_kind = determine_semantic_kind (msg_content )
10871083
1088- # Token assignment:
1089- # Each message has actual token usage from Claude API
1090- # Assign actual tokens to each span for accurate aggregation
1091- input_tokens = msg_input_tokens
1092- output_tokens = msg_output_tokens
1084+ # Calculate cost for this span
1085+ span_costs = calculate_cost (
1086+ provider = "anthropic" ,
1087+ model = model ,
1088+ input_tokens = input_tokens ,
1089+ output_tokens = output_tokens ,
1090+ cache_creation_input_tokens = cache_creation_input_tokens ,
1091+ cache_read_input_tokens = cache_read_input_tokens ,
1092+ )
10931093
10941094 # Build span attributes
10951095 span_attributes = {
@@ -1099,7 +1099,25 @@ def create_clickhouse_spans_for_turn(
10991099 "gen_ai.response.model" : model ,
11001100 "gen_ai.usage.input_tokens" : str (input_tokens ),
11011101 "gen_ai.usage.output_tokens" : str (output_tokens ),
1102- "llm.usage.total_tokens" : str (input_tokens + output_tokens ),
1102+ "gen_ai.usage.cache_creation_input_tokens" : str (
1103+ cache_creation_input_tokens
1104+ ),
1105+ "gen_ai.usage.cache_read_input_tokens" : str (cache_read_input_tokens ),
1106+ "llm.usage.total_tokens" : str (
1107+ input_tokens
1108+ + output_tokens
1109+ + cache_creation_input_tokens
1110+ + cache_read_input_tokens
1111+ ),
1112+ "alphatrion.cost.total_tokens" : str (span_costs ["total_cost" ]),
1113+ "alphatrion.cost.input_tokens" : str (span_costs ["input_cost" ]),
1114+ "alphatrion.cost.output_tokens" : str (span_costs ["output_cost" ]),
1115+ "alphatrion.cost.cache_creation_input_tokens" : str (
1116+ span_costs ["cache_creation_input_cost" ]
1117+ ),
1118+ "alphatrion.cost.cache_read_input_tokens" : str (
1119+ span_costs ["cache_read_input_cost" ]
1120+ ),
11031121 }
11041122
11051123 # Add prompt to first span only (where the user input is sent)
@@ -1222,3 +1240,37 @@ def create_clickhouse_spans_for_turn(
12221240 import logging
12231241
12241242 logging .error (f"Failed to create ClickHouse spans: { e } " , exc_info = True )
1243+
1244+
1245+ def determine_semantic_kind (content_blocks : list ) -> str :
1246+ """Determine semantic kind of a message based on content blocks.
1247+
1248+ - If contains tool_use block → "tool"
1249+ - Else if contains thinking block → "thinking"
1250+ - Else → "text"
1251+
1252+ Args:
1253+ content_blocks: List of content blocks in the message
1254+
1255+ Returns:
1256+ Semantic kind string
1257+ """
1258+ has_tool_use = any (
1259+ isinstance (b , dict ) and b .get ("type" ) == "tool_use" for b in content_blocks
1260+ )
1261+ if has_tool_use :
1262+ return SEMANTIC_KIND_TOOL
1263+
1264+ has_thinking = any (
1265+ isinstance (b , dict ) and b .get ("type" ) == "thinking" for b in content_blocks
1266+ )
1267+ if has_thinking :
1268+ return SEMANTIC_KIND_REASONING
1269+
1270+ has_text = any (
1271+ isinstance (b , dict ) and b .get ("type" ) == "text" for b in content_blocks
1272+ )
1273+ if has_text :
1274+ return SEMANTIC_KIND_CHAT
1275+
1276+ return SEMANTIC_KIND_UNKNOWN
0 commit comments