|
11 | 11 | using Microsoft.Extensions.Configuration; |
12 | 12 | using Microsoft.Extensions.DependencyInjection; |
13 | 13 | using Microsoft.Extensions.Logging; |
| 14 | +using Microsoft.Extensions.Logging.Abstractions; |
14 | 15 | using OpenTelemetry; |
15 | 16 | using OpenTelemetry.Exporter; |
16 | 17 | using OpenTelemetry.Metrics; |
@@ -202,6 +203,7 @@ public override Task AfterRecordingStopAsync(RecordingArgs e, CancellationToken |
202 | 203 | Environment = Configuration.Environment, |
203 | 204 | Currency = Configuration.Currency, |
204 | 205 | IncludeCosts = Configuration.IncludeCosts, |
| 206 | + ModelUsage = GetOpenAIModelUsage(e.RequestLogs) |
205 | 207 | }; |
206 | 208 |
|
207 | 209 | StoreReport(report, e); |
@@ -901,6 +903,100 @@ private void RecordUsageMetrics(Activity activity, OpenAIRequest request, OpenAI |
901 | 903 | Logger.LogTrace("RecordUsageMetrics() finished"); |
902 | 904 | } |
903 | 905 |
|
| 906 | + private Dictionary<string, List<OpenAITelemetryPluginReportModelUsageInformation>> GetOpenAIModelUsage(IEnumerable<RequestLog> requestLogs) |
| 907 | + { |
| 908 | + var modelUsage = new Dictionary<string, List<OpenAITelemetryPluginReportModelUsageInformation>>(); |
| 909 | + var openAIRequestLogs = requestLogs.Where(r => |
| 910 | + r is not null && |
| 911 | + r.Context is not null && |
| 912 | + r.Context.Session is not null && |
| 913 | + r.MessageType == MessageType.InterceptedResponse && |
| 914 | + string.Equals("POST", r.Context.Session.HttpClient.Request.Method, StringComparison.OrdinalIgnoreCase) && |
| 915 | + r.Context.Session.HttpClient.Response.StatusCode >= 200 && |
| 916 | + r.Context.Session.HttpClient.Response.StatusCode < 300 && |
| 917 | + r.Context.Session.HttpClient.Response.HasBody && |
| 918 | + !string.IsNullOrEmpty(r.Context.Session.HttpClient.Response.BodyString) && |
| 919 | + ProxyUtils.MatchesUrlToWatch(UrlsToWatch, r.Context.Session.HttpClient.Request.RequestUri.AbsoluteUri) && |
| 920 | + OpenAIRequest.TryGetOpenAIRequest(r.Context.Session.HttpClient.Request.BodyString, NullLogger.Instance, out var openAiRequest) && |
| 921 | + openAiRequest is not null |
| 922 | + ); |
| 923 | + |
| 924 | + foreach (var requestLog in openAIRequestLogs) |
| 925 | + { |
| 926 | + try |
| 927 | + { |
| 928 | + var response = JsonSerializer.Deserialize<OpenAIResponse>(requestLog.Context!.Session.HttpClient.Response.BodyString, ProxyUtils.JsonSerializerOptions); |
| 929 | + if (response is null) |
| 930 | + { |
| 931 | + continue; |
| 932 | + } |
| 933 | + |
| 934 | + var reportModelUsageInfo = GetReportModelUsageInfo(response); |
| 935 | + if (modelUsage.TryGetValue(response.Model, out var usagePerModel)) |
| 936 | + { |
| 937 | + usagePerModel.AddRange(reportModelUsageInfo); |
| 938 | + } |
| 939 | + else |
| 940 | + { |
| 941 | + modelUsage.Add(response.Model, reportModelUsageInfo); |
| 942 | + } |
| 943 | + } |
| 944 | + catch (JsonException ex) |
| 945 | + { |
| 946 | + Logger.LogError(ex, "Failed to deserialize OpenAI response"); |
| 947 | + } |
| 948 | + } |
| 949 | + |
| 950 | + return modelUsage; |
| 951 | + } |
| 952 | + |
| 953 | + private List<OpenAITelemetryPluginReportModelUsageInformation> GetReportModelUsageInfo(OpenAIResponse response) |
| 954 | + { |
| 955 | + Logger.LogTrace("GetReportModelUsageInfo() called"); |
| 956 | + var usagePerModel = new List<OpenAITelemetryPluginReportModelUsageInformation>(); |
| 957 | + var usage = response.Usage; |
| 958 | + if (usage is null) |
| 959 | + { |
| 960 | + return usagePerModel; |
| 961 | + } |
| 962 | + |
| 963 | + var reportModelUsageInformation = new OpenAITelemetryPluginReportModelUsageInformation |
| 964 | + { |
| 965 | + Model = response.Model, |
| 966 | + PromptTokens = usage.PromptTokens, |
| 967 | + CompletionTokens = usage.CompletionTokens, |
| 968 | + CachedTokens = usage.PromptTokensDetails?.CachedTokens ?? 0L |
| 969 | + }; |
| 970 | + usagePerModel.Add(reportModelUsageInformation); |
| 971 | + |
| 972 | + if (!Configuration.IncludeCosts || Configuration.Prices is null) |
| 973 | + { |
| 974 | + Logger.LogDebug("Cost tracking is disabled or prices data is not available"); |
| 975 | + return usagePerModel; |
| 976 | + } |
| 977 | + |
| 978 | + if (string.IsNullOrEmpty(response.Model)) |
| 979 | + { |
| 980 | + Logger.LogDebug("Response model is empty or null"); |
| 981 | + return usagePerModel; |
| 982 | + } |
| 983 | + |
| 984 | + var (inputCost, outputCost) = Configuration.Prices.CalculateCost(response.Model, usage.PromptTokens, usage.CompletionTokens); |
| 985 | + |
| 986 | + if (inputCost > 0) |
| 987 | + { |
| 988 | + var totalCost = inputCost + outputCost; |
| 989 | + reportModelUsageInformation.Cost = totalCost; |
| 990 | + } |
| 991 | + else |
| 992 | + { |
| 993 | + Logger.LogDebug("Input cost is zero, skipping cost metrics recording"); |
| 994 | + } |
| 995 | + |
| 996 | + Logger.LogTrace("GetReportModelUsageInfo() finished"); |
| 997 | + return usagePerModel; |
| 998 | + } |
| 999 | + |
904 | 1000 | private static string GetOperationName(OpenAIRequest request) |
905 | 1001 | { |
906 | 1002 | if (request == null) |
|
0 commit comments