@@ -42,9 +42,12 @@ import {
4242 ATTR_GEN_AI_RESPONSE_MODEL ,
4343 ATTR_GEN_AI_USAGE_OUTPUT_TOKENS ,
4444 ATTR_GEN_AI_USAGE_INPUT_TOKENS ,
45+ ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS ,
46+ ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS ,
4547 ATTR_GEN_AI_PROVIDER_NAME ,
4648 ATTR_GEN_AI_OPERATION_NAME ,
4749 ATTR_GEN_AI_RESPONSE_FINISH_REASONS ,
50+ GEN_AI_OPERATION_NAME_VALUE_CHAT ,
4851} from "@opentelemetry/semantic-conventions/incubating" ;
4952
5053const memoryExporter = new InMemorySpanExporter ( ) ;
@@ -343,3 +346,108 @@ describe("Test Anthropic instrumentation", async function () {
343346 assert . equal ( + promptTokens + + completionTokens , totalTokens ) ;
344347 } ) . timeout ( 30000 ) ;
345348} ) ;
349+
350+ describe ( "Anthropic cache token fold-in semantics" , ( ) => {
351+ // Per OTel GenAI semconv, cache_read.input_tokens and cache_creation.input_tokens
352+ // SHOULD be included in gen_ai.usage.input_tokens (subset semantics).
353+ // These tests exercise _endSpan directly with synthetic Message objects.
354+
355+ const exporter = new InMemorySpanExporter ( ) ;
356+ const provider = new NodeTracerProvider ( {
357+ spanProcessors : [ new SimpleSpanProcessor ( exporter ) ] ,
358+ } ) ;
359+ const instrumentation = new AnthropicInstrumentation ( ) ;
360+ instrumentation . setTracerProvider ( provider ) ;
361+
362+ afterEach ( ( ) => exporter . reset ( ) ) ;
363+
364+ const endSpanWithUsage = ( usage : Record < string , unknown > ) => {
365+ const span = ( instrumentation as any ) . tracer . startSpan ( "chat test-model" ) ;
366+ ( instrumentation as any ) . _endSpan ( {
367+ span,
368+ type : GEN_AI_OPERATION_NAME_VALUE_CHAT ,
369+ result : {
370+ id : "msg_test" ,
371+ type : "message" ,
372+ model : "test-model" ,
373+ role : "assistant" ,
374+ stop_reason : "end_turn" ,
375+ stop_sequence : null ,
376+ content : [ ] ,
377+ usage,
378+ } ,
379+ } ) ;
380+ const spans = exporter . getFinishedSpans ( ) ;
381+ return spans [ spans . length - 1 ] ;
382+ } ;
383+
384+ it ( "folds cache_read + cache_creation into input_tokens and total_tokens" , ( ) => {
385+ const span = endSpanWithUsage ( {
386+ input_tokens : 100 ,
387+ output_tokens : 50 ,
388+ cache_read_input_tokens : 900 ,
389+ cache_creation_input_tokens : 200 ,
390+ } ) ;
391+ assert . strictEqual (
392+ span . attributes [ ATTR_GEN_AI_USAGE_INPUT_TOKENS ] ,
393+ 1200 ,
394+ "input_tokens should equal 100 + 900 + 200" ,
395+ ) ;
396+ assert . strictEqual (
397+ span . attributes [ SpanAttributes . GEN_AI_USAGE_TOTAL_TOKENS ] ,
398+ 1250 ,
399+ "total_tokens should equal summed input (1200) + output (50)" ,
400+ ) ;
401+ assert . strictEqual (
402+ span . attributes [ ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS ] ,
403+ 900 ,
404+ "cache_read should still be emitted separately" ,
405+ ) ;
406+ assert . strictEqual (
407+ span . attributes [ ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS ] ,
408+ 200 ,
409+ "cache_creation should still be emitted separately" ,
410+ ) ;
411+ } ) ;
412+
413+ it ( "folds only cache_read when cache_creation is absent" , ( ) => {
414+ const span = endSpanWithUsage ( {
415+ input_tokens : 100 ,
416+ output_tokens : 50 ,
417+ cache_read_input_tokens : 900 ,
418+ } ) ;
419+ assert . strictEqual ( span . attributes [ ATTR_GEN_AI_USAGE_INPUT_TOKENS ] , 1000 ) ;
420+ assert . strictEqual (
421+ span . attributes [ SpanAttributes . GEN_AI_USAGE_TOTAL_TOKENS ] ,
422+ 1050 ,
423+ ) ;
424+ assert . strictEqual (
425+ span . attributes [ ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS ] ,
426+ 900 ,
427+ ) ;
428+ assert . strictEqual (
429+ span . attributes [ ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS ] ,
430+ undefined ,
431+ ) ;
432+ } ) ;
433+
434+ it ( "leaves input_tokens unchanged when no cache fields present" , ( ) => {
435+ const span = endSpanWithUsage ( {
436+ input_tokens : 100 ,
437+ output_tokens : 50 ,
438+ } ) ;
439+ assert . strictEqual ( span . attributes [ ATTR_GEN_AI_USAGE_INPUT_TOKENS ] , 100 ) ;
440+ assert . strictEqual (
441+ span . attributes [ SpanAttributes . GEN_AI_USAGE_TOTAL_TOKENS ] ,
442+ 150 ,
443+ ) ;
444+ assert . strictEqual (
445+ span . attributes [ ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS ] ,
446+ undefined ,
447+ ) ;
448+ assert . strictEqual (
449+ span . attributes [ ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS ] ,
450+ undefined ,
451+ ) ;
452+ } ) ;
453+ } ) ;
0 commit comments