Skip to content

Commit 2f790c6

Browse files
vgrozdanicclaudex-ai/grok-4.5
authored
feat(ai): Attach granular cost data to spans (#6167)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: x-ai/grok-4.5 <noreply@opencode.ai>
1 parent 9788a10 commit 2f790c6

4 files changed

Lines changed: 184 additions & 10 deletions

File tree

relay-event-normalization/src/eap/ai.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,21 @@ fn normalize_ai_costs(attributes: &mut Attributes, model_metadata: Option<&Model
206206

207207
// Overwrite all values, the attributes should reflect the values we used to calculate the total.
208208
attributes.insert(GEN_AI__COST__INPUT_TOKENS, costs.input);
209+
attributes.insert(
210+
GEN_AI__COST__CACHE_READ__INPUT_TOKENS,
211+
costs.cache_read_input,
212+
);
213+
attributes.insert(
214+
GEN_AI__COST__CACHE_CREATION__INPUT_TOKENS,
215+
costs.cache_creation_input,
216+
);
217+
209218
attributes.insert(GEN_AI__COST__OUTPUT_TOKENS, costs.output);
219+
attributes.insert(
220+
GEN_AI__COST__REASONING__OUTPUT_TOKENS,
221+
costs.reasoning_output,
222+
);
223+
210224
attributes.insert(GEN_AI__COST__TOTAL_TOKENS, costs.total());
211225
}
212226

@@ -305,6 +319,14 @@ mod tests {
305319

306320
assert_annotated_snapshot!(attributes, @r#"
307321
{
322+
"gen_ai.cost.cache_creation.input_tokens": {
323+
"type": "double",
324+
"value": 0.0
325+
},
326+
"gen_ai.cost.cache_read.input_tokens": {
327+
"type": "double",
328+
"value": 20.0
329+
},
308330
"gen_ai.cost.input_tokens": {
309331
"type": "double",
310332
"value": 25.0
@@ -313,6 +335,10 @@ mod tests {
313335
"type": "double",
314336
"value": 50.0
315337
},
338+
"gen_ai.cost.reasoning.output_tokens": {
339+
"type": "double",
340+
"value": 30.0
341+
},
316342
"gen_ai.cost.total_tokens": {
317343
"type": "double",
318344
"value": 75.0
@@ -374,6 +400,14 @@ mod tests {
374400

375401
assert_annotated_snapshot!(attributes, @r#"
376402
{
403+
"gen_ai.cost.cache_creation.input_tokens": {
404+
"type": "double",
405+
"value": 0.0
406+
},
407+
"gen_ai.cost.cache_read.input_tokens": {
408+
"type": "double",
409+
"value": 0.0
410+
},
377411
"gen_ai.cost.input_tokens": {
378412
"type": "double",
379413
"value": 90.0
@@ -382,6 +416,10 @@ mod tests {
382416
"type": "double",
383417
"value": 100.0
384418
},
419+
"gen_ai.cost.reasoning.output_tokens": {
420+
"type": "double",
421+
"value": 0.0
422+
},
385423
"gen_ai.cost.total_tokens": {
386424
"type": "double",
387425
"value": 190.0
@@ -483,6 +521,14 @@ mod tests {
483521

484522
assert_annotated_snapshot!(attributes, @r#"
485523
{
524+
"gen_ai.cost.cache_creation.input_tokens": {
525+
"type": "double",
526+
"value": 0.0
527+
},
528+
"gen_ai.cost.cache_read.input_tokens": {
529+
"type": "double",
530+
"value": 0.0
531+
},
486532
"gen_ai.cost.input_tokens": {
487533
"type": "double",
488534
"value": 90.0
@@ -491,6 +537,10 @@ mod tests {
491537
"type": "double",
492538
"value": 100.0
493539
},
540+
"gen_ai.cost.reasoning.output_tokens": {
541+
"type": "double",
542+
"value": 0.0
543+
},
494544
"gen_ai.cost.total_tokens": {
495545
"type": "double",
496546
"value": 190.0
@@ -552,6 +602,14 @@ mod tests {
552602

553603
assert_annotated_snapshot!(attributes, @r#"
554604
{
605+
"gen_ai.cost.cache_creation.input_tokens": {
606+
"type": "double",
607+
"value": 0.0
608+
},
609+
"gen_ai.cost.cache_read.input_tokens": {
610+
"type": "double",
611+
"value": 0.0
612+
},
555613
"gen_ai.cost.input_tokens": {
556614
"type": "double",
557615
"value": 90.0
@@ -560,6 +618,10 @@ mod tests {
560618
"type": "double",
561619
"value": 100.0
562620
},
621+
"gen_ai.cost.reasoning.output_tokens": {
622+
"type": "double",
623+
"value": 0.0
624+
},
563625
"gen_ai.cost.total_tokens": {
564626
"type": "double",
565627
"value": 190.0
@@ -683,6 +745,14 @@ mod tests {
683745
"type": "integer",
684746
"value": 100000
685747
},
748+
"gen_ai.cost.cache_creation.input_tokens": {
749+
"type": "double",
750+
"value": 0.0
751+
},
752+
"gen_ai.cost.cache_read.input_tokens": {
753+
"type": "double",
754+
"value": 0.0
755+
},
686756
"gen_ai.cost.input_tokens": {
687757
"type": "double",
688758
"value": 300.0
@@ -691,6 +761,10 @@ mod tests {
691761
"type": "double",
692762
"value": 240.0
693763
},
764+
"gen_ai.cost.reasoning.output_tokens": {
765+
"type": "double",
766+
"value": 0.0
767+
},
694768
"gen_ai.cost.total_tokens": {
695769
"type": "double",
696770
"value": 540.0

relay-event-normalization/src/event.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2566,8 +2566,11 @@ mod tests {
25662566

25672567
assert_annotated_snapshot!(span1, @r#"
25682568
{
2569+
"gen_ai.cost.cache_creation.input_tokens": 0.0,
2570+
"gen_ai.cost.cache_read.input_tokens": 0.0,
25692571
"gen_ai.cost.input_tokens": 10.0,
25702572
"gen_ai.cost.output_tokens": 40.0,
2573+
"gen_ai.cost.reasoning.output_tokens": 0.0,
25712574
"gen_ai.cost.total_tokens": 50.0,
25722575
"gen_ai.operation.type": "ai_client",
25732576
"gen_ai.request.model": "claude-2.1",
@@ -2580,8 +2583,11 @@ mod tests {
25802583
"#);
25812584
assert_annotated_snapshot!(span2, @r#"
25822585
{
2586+
"gen_ai.cost.cache_creation.input_tokens": 0.0,
2587+
"gen_ai.cost.cache_read.input_tokens": 0.0,
25832588
"gen_ai.cost.input_tokens": 20.0,
25842589
"gen_ai.cost.output_tokens": 60.0,
2590+
"gen_ai.cost.reasoning.output_tokens": 0.0,
25852591
"gen_ai.cost.total_tokens": 80.0,
25862592
"gen_ai.operation.type": "ai_client",
25872593
"gen_ai.request.model": "gpt4-21-04",
@@ -2691,8 +2697,11 @@ mod tests {
26912697

26922698
assert_annotated_snapshot!(span1, @r#"
26932699
{
2700+
"gen_ai.cost.cache_creation.input_tokens": 0.0,
2701+
"gen_ai.cost.cache_read.input_tokens": 20.0,
26942702
"gen_ai.cost.input_tokens": 25.0,
26952703
"gen_ai.cost.output_tokens": 50.0,
2704+
"gen_ai.cost.reasoning.output_tokens": 30.0,
26962705
"gen_ai.cost.total_tokens": 75.0,
26972706
"gen_ai.operation.type": "ai_client",
26982707
"gen_ai.request.model": "claude-2.1",
@@ -2707,8 +2716,11 @@ mod tests {
27072716
"#);
27082717
assert_annotated_snapshot!(span2, @r#"
27092718
{
2719+
"gen_ai.cost.cache_creation.input_tokens": 0.0,
2720+
"gen_ai.cost.cache_read.input_tokens": 0.0,
27102721
"gen_ai.cost.input_tokens": 90.0,
27112722
"gen_ai.cost.output_tokens": 100.0,
2723+
"gen_ai.cost.reasoning.output_tokens": 0.0,
27122724
"gen_ai.cost.total_tokens": 190.0,
27132725
"gen_ai.operation.type": "ai_client",
27142726
"gen_ai.request.model": "gpt4-21-04",
@@ -2721,8 +2733,11 @@ mod tests {
27212733
"#);
27222734
assert_annotated_snapshot!(span3, @r#"
27232735
{
2736+
"gen_ai.cost.cache_creation.input_tokens": 0.0,
2737+
"gen_ai.cost.cache_read.input_tokens": 0.0,
27242738
"gen_ai.cost.input_tokens": 90.0,
27252739
"gen_ai.cost.output_tokens": 100.0,
2740+
"gen_ai.cost.reasoning.output_tokens": 0.0,
27262741
"gen_ai.cost.total_tokens": 190.0,
27272742
"gen_ai.operation.type": "ai_client",
27282743
"gen_ai.response.model": "gpt4-21-04",
@@ -2874,8 +2889,11 @@ mod tests {
28742889

28752890
assert_annotated_snapshot!(span1, @r#"
28762891
{
2892+
"gen_ai.cost.cache_creation.input_tokens": 0.0,
2893+
"gen_ai.cost.cache_read.input_tokens": 20.0,
28772894
"gen_ai.cost.input_tokens": 25.0,
28782895
"gen_ai.cost.output_tokens": 40.0,
2896+
"gen_ai.cost.reasoning.output_tokens": 20.0,
28792897
"gen_ai.cost.total_tokens": 65.0,
28802898
"gen_ai.operation.type": "ai_client",
28812899
"gen_ai.request.model": "claude-2.1",
@@ -2890,8 +2908,11 @@ mod tests {
28902908
"#);
28912909
assert_annotated_snapshot!(span2, @r#"
28922910
{
2911+
"gen_ai.cost.cache_creation.input_tokens": 0.0,
2912+
"gen_ai.cost.cache_read.input_tokens": 0.0,
28932913
"gen_ai.cost.input_tokens": 90.0,
28942914
"gen_ai.cost.output_tokens": 100.0,
2915+
"gen_ai.cost.reasoning.output_tokens": 0.0,
28952916
"gen_ai.cost.total_tokens": 190.0,
28962917
"gen_ai.operation.type": "ai_client",
28972918
"gen_ai.request.model": "gpt4-21-04",

relay-event-normalization/src/normalize/span/ai.rs

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,16 @@ impl UsedTokens {
7373
/// Calculated model call costs.
7474
#[derive(Debug, Copy, Clone)]
7575
pub struct CalculatedCost {
76-
/// The cost of input tokens used.
76+
/// The total cost of all input tokens (raw + cached + cache_write).
7777
pub input: f64,
78-
/// The cost of output tokens used.
78+
/// The total cost of all output tokens (raw + reasoning).
7979
pub output: f64,
80+
/// The cost of cached input tokens only (subset of `input`).
81+
pub cache_read_input: f64,
82+
/// The cost of cache-write input tokens only (subset of `input`).
83+
pub cache_creation_input: f64,
84+
/// The cost of reasoning output tokens only (subset of `output`).
85+
pub reasoning_output: f64,
8086
}
8187

8288
impl CalculatedCost {
@@ -105,19 +111,21 @@ pub fn calculate_costs(
105111
return None;
106112
}
107113

114+
let cache_read_input = tokens.input_cached_tokens * model_cost.input_cached_per_token;
115+
let cache_creation_input =
116+
tokens.input_cache_write_tokens * model_cost.input_cache_write_per_token;
108117
let input = (tokens.raw_input_tokens() * model_cost.input_per_token)
109-
+ (tokens.input_cached_tokens * model_cost.input_cached_per_token)
110-
+ (tokens.input_cache_write_tokens * model_cost.input_cache_write_per_token);
118+
+ cache_read_input
119+
+ cache_creation_input;
111120

112121
// For now most of the models do not differentiate between reasoning and output token cost,
113122
// it costs the same.
114-
let reasoning_cost = match model_cost.output_reasoning_per_token {
115-
reasoning_cost if reasoning_cost > 0.0 => reasoning_cost,
123+
let reasoning_per_token = match model_cost.output_reasoning_per_token {
124+
r if r > 0.0 => r,
116125
_ => model_cost.output_per_token,
117126
};
118-
119-
let output = (tokens.raw_output_tokens() * model_cost.output_per_token)
120-
+ (tokens.output_reasoning_tokens * reasoning_cost);
127+
let reasoning_output = tokens.output_reasoning_tokens * reasoning_per_token;
128+
let output = (tokens.raw_output_tokens() * model_cost.output_per_token) + reasoning_output;
121129

122130
let metric_label = match (input, output) {
123131
(x, y) if x < 0.0 || y < 0.0 => "calculation_negative",
@@ -132,7 +140,13 @@ pub fn calculate_costs(
132140
platform = platform,
133141
);
134142

135-
Some(CalculatedCost { input, output })
143+
Some(CalculatedCost {
144+
input,
145+
output,
146+
cache_read_input,
147+
cache_creation_input,
148+
reasoning_output,
149+
})
136150
}
137151

138152
/// Default AI operation stored in [`GEN_AI__OPERATION__TYPE`]
@@ -221,10 +235,24 @@ fn extract_ai_model_cost_data(
221235
.entry(GEN_AI__COST__INPUT_TOKENS.to_owned())
222236
.or_default()
223237
.set_value(Value::F64(costs.input).into());
238+
data.other
239+
.entry(GEN_AI__COST__CACHE_READ__INPUT_TOKENS.to_owned())
240+
.or_default()
241+
.set_value(Value::F64(costs.cache_read_input).into());
242+
data.other
243+
.entry(GEN_AI__COST__CACHE_CREATION__INPUT_TOKENS.to_owned())
244+
.or_default()
245+
.set_value(Value::F64(costs.cache_creation_input).into());
246+
224247
data.other
225248
.entry(GEN_AI__COST__OUTPUT_TOKENS.to_owned())
226249
.or_default()
227250
.set_value(Value::F64(costs.output).into());
251+
252+
data.other
253+
.entry(GEN_AI__COST__REASONING__OUTPUT_TOKENS.to_owned())
254+
.or_default()
255+
.set_value(Value::F64(costs.reasoning_output).into());
228256
}
229257

230258
/// Maps AI-related measurements (legacy) to span data.
@@ -556,6 +584,9 @@ mod tests {
556584
CalculatedCost {
557585
input: 5.5,
558586
output: 39.0,
587+
cache_read_input: 2.5,
588+
cache_creation_input: 0.0,
589+
reasoning_output: 27.0,
559590
}
560591
");
561592
}
@@ -587,6 +618,9 @@ mod tests {
587618
CalculatedCost {
588619
input: 5.5,
589620
output: 30.0,
621+
cache_read_input: 2.5,
622+
cache_creation_input: 0.0,
623+
reasoning_output: 18.0,
590624
}
591625
");
592626
}
@@ -620,6 +654,9 @@ mod tests {
620654
CalculatedCost {
621655
input: -9.0,
622656
output: -7.0,
657+
cache_read_input: 11.0,
658+
cache_creation_input: 0.0,
659+
reasoning_output: 9.0,
623660
}
624661
");
625662
}
@@ -653,6 +690,9 @@ mod tests {
653690
CalculatedCost {
654691
input: 82.5,
655692
output: 110.0,
693+
cache_read_input: 10.0,
694+
cache_creation_input: 22.5,
695+
reasoning_output: 30.0,
656696
}
657697
");
658698
}
@@ -701,6 +741,9 @@ mod tests {
701741
CalculatedCost {
702742
input: 90.0,
703743
output: 100.0,
744+
cache_read_input: 10.0,
745+
cache_creation_input: 0.0,
746+
reasoning_output: 0.0,
704747
}
705748
");
706749
}

0 commit comments

Comments
 (0)