1111using Microsoft . Extensions . Configuration ;
1212using Microsoft . Extensions . DependencyInjection ;
1313using Microsoft . Extensions . Logging ;
14+ using Microsoft . Extensions . Logging . Abstractions ;
1415using OpenTelemetry ;
1516using OpenTelemetry . Exporter ;
1617using OpenTelemetry . Metrics ;
1718using OpenTelemetry . Resources ;
1819using OpenTelemetry . Trace ;
19- using System . Collections . Concurrent ;
2020using System . Diagnostics ;
2121using System . Diagnostics . Metrics ;
2222using System . Text . Json ;
@@ -67,7 +67,6 @@ public sealed class OpenAITelemetryPlugin(
6767 private LanguageModelPricesLoader ? _loader ;
6868 private MeterProvider ? _meterProvider ;
6969 private TracerProvider ? _tracerProvider ;
70- private readonly ConcurrentDictionary < string , List < OpenAITelemetryPluginReportModelUsageInformation > > _modelUsage = [ ] ;
7170
7271 public override string Name => nameof ( OpenAITelemetryPlugin ) ;
7372
@@ -196,17 +195,18 @@ public override Task AfterRecordingStopAsync(RecordingArgs e, CancellationToken
196195 {
197196 Logger . LogTrace ( "{Method} called" , nameof ( AfterRecordingStopAsync ) ) ;
198197
198+ ArgumentNullException . ThrowIfNull ( e ) ;
199+
199200 var report = new OpenAITelemetryPluginReport
200201 {
201202 Application = Configuration . Application ,
202203 Environment = Configuration . Environment ,
203204 Currency = Configuration . Currency ,
204205 IncludeCosts = Configuration . IncludeCosts ,
205- ModelUsage = _modelUsage . ToDictionary ( )
206+ ModelUsage = GetOpenAIModelUsage ( e . RequestLogs )
206207 } ;
207208
208209 StoreReport ( report , e ) ;
209- _modelUsage . Clear ( ) ;
210210
211211 Logger . LogTrace ( "Left {Name}" , nameof ( AfterRecordingStopAsync ) ) ;
212212 return Task . CompletedTask ;
@@ -849,16 +849,6 @@ private void RecordUsageMetrics(Activity activity, OpenAIRequest request, OpenAI
849849 . SetTag ( SemanticConvention . GEN_AI_USAGE_OUTPUT_TOKENS , usage . CompletionTokens )
850850 . SetTag ( SemanticConvention . GEN_AI_USAGE_TOTAL_TOKENS , usage . TotalTokens ) ;
851851
852- var reportModelUsageInformation = new OpenAITelemetryPluginReportModelUsageInformation
853- {
854- Model = response . Model ,
855- PromptTokens = usage . PromptTokens ,
856- CompletionTokens = usage . CompletionTokens ,
857- CachedTokens = usage . PromptTokensDetails ? . CachedTokens ?? 0L
858- } ;
859- var usagePerModel = _modelUsage . GetOrAdd ( response . Model , model => [ ] ) ;
860- usagePerModel . Add ( reportModelUsageInformation ) ;
861-
862852 if ( ! Configuration . IncludeCosts || Configuration . Prices is null )
863853 {
864854 Logger . LogDebug ( "Cost tracking is disabled or prices data is not available" ) ;
@@ -895,7 +885,6 @@ private void RecordUsageMetrics(Activity activity, OpenAIRequest request, OpenAI
895885 new ( SemanticConvention . GEN_AI_REQUEST_MODEL , request . Model ) ,
896886 new ( SemanticConvention . GEN_AI_RESPONSE_MODEL , response . Model )
897887 ] ) ;
898- reportModelUsageInformation . Cost = totalCost ;
899888 }
900889 else
901890 {
@@ -905,6 +894,100 @@ private void RecordUsageMetrics(Activity activity, OpenAIRequest request, OpenAI
905894 Logger . LogTrace ( "RecordUsageMetrics() finished" ) ;
906895 }
907896
897+ private Dictionary < string , List < OpenAITelemetryPluginReportModelUsageInformation > > GetOpenAIModelUsage ( IEnumerable < RequestLog > requestLogs )
898+ {
899+ var modelUsage = new Dictionary < string , List < OpenAITelemetryPluginReportModelUsageInformation > > ( ) ;
900+ var openAIRequestLogs = requestLogs . Where ( r =>
901+ r is not null &&
902+ r . Context is not null &&
903+ r . Context . Session is not null &&
904+ r . MessageType == MessageType . InterceptedResponse &&
905+ string . Equals ( "POST" , r . Context . Session . HttpClient . Request . Method , StringComparison . OrdinalIgnoreCase ) &&
906+ r . Context . Session . HttpClient . Response . StatusCode >= 200 &&
907+ r . Context . Session . HttpClient . Response . StatusCode < 300 &&
908+ r . Context . Session . HttpClient . Response . HasBody &&
909+ ! string . IsNullOrEmpty ( r . Context . Session . HttpClient . Response . BodyString ) &&
910+ ProxyUtils . MatchesUrlToWatch ( UrlsToWatch , r . Context . Session . HttpClient . Request . RequestUri . AbsoluteUri ) &&
911+ OpenAIRequest . TryGetOpenAIRequest ( r . Context . Session . HttpClient . Request . BodyString , NullLogger . Instance , out var openAiRequest ) &&
912+ openAiRequest is not null
913+ ) ;
914+
915+ foreach ( var requestLog in openAIRequestLogs )
916+ {
917+ try
918+ {
919+ var response = JsonSerializer . Deserialize < OpenAIResponse > ( requestLog . Context ! . Session . HttpClient . Response . BodyString , ProxyUtils . JsonSerializerOptions ) ;
920+ if ( response is null )
921+ {
922+ continue ;
923+ }
924+
925+ var reportModelUsageInfo = GetReportModelUsageInfo ( response ) ;
926+ if ( modelUsage . TryGetValue ( response . Model , out var usagePerModel ) )
927+ {
928+ usagePerModel . AddRange ( reportModelUsageInfo ) ;
929+ }
930+ else
931+ {
932+ modelUsage . Add ( response . Model , reportModelUsageInfo ) ;
933+ }
934+ }
935+ catch ( JsonException ex )
936+ {
937+ Logger . LogError ( ex , "Failed to deserialize OpenAI response" ) ;
938+ }
939+ }
940+
941+ return modelUsage ;
942+ }
943+
944+ private List < OpenAITelemetryPluginReportModelUsageInformation > GetReportModelUsageInfo ( OpenAIResponse response )
945+ {
946+ Logger . LogTrace ( "GetReportModelUsageInfo() called" ) ;
947+ var usagePerModel = new List < OpenAITelemetryPluginReportModelUsageInformation > ( ) ;
948+ var usage = response . Usage ;
949+ if ( usage is null )
950+ {
951+ return usagePerModel ;
952+ }
953+
954+ var reportModelUsageInformation = new OpenAITelemetryPluginReportModelUsageInformation
955+ {
956+ Model = response . Model ,
957+ PromptTokens = usage . PromptTokens ,
958+ CompletionTokens = usage . CompletionTokens ,
959+ CachedTokens = usage . PromptTokensDetails ? . CachedTokens ?? 0L
960+ } ;
961+ usagePerModel . Add ( reportModelUsageInformation ) ;
962+
963+ if ( ! Configuration . IncludeCosts || Configuration . Prices is null )
964+ {
965+ Logger . LogDebug ( "Cost tracking is disabled or prices data is not available" ) ;
966+ return usagePerModel ;
967+ }
968+
969+ if ( string . IsNullOrEmpty ( response . Model ) )
970+ {
971+ Logger . LogDebug ( "Response model is empty or null" ) ;
972+ return usagePerModel ;
973+ }
974+
975+ var ( inputCost , outputCost ) = Configuration . Prices . CalculateCost ( response . Model , usage . PromptTokens , usage . CompletionTokens ) ;
976+
977+ if ( inputCost > 0 )
978+ {
979+ var totalCost = inputCost + outputCost ;
980+ reportModelUsageInformation . Cost = totalCost ;
981+ }
982+ else
983+ {
984+ Logger . LogDebug ( "Input cost is zero, skipping cost metrics recording" ) ;
985+ }
986+
987+ Logger . LogTrace ( "GetReportModelUsageInfo() finished" ) ;
988+ return usagePerModel ;
989+ }
990+
908991 private static string GetOperationName ( OpenAIRequest request )
909992 {
910993 if ( request == null )
0 commit comments