Skip to content

Commit 83409c6

Browse files
committed
Add modelUsage collection from RequestLogs after recording is stopped
1 parent 5b3a74d commit 83409c6

File tree

1 file changed

+96
-0
lines changed

1 file changed

+96
-0
lines changed

DevProxy.Plugins/Inspection/OpenAITelemetryPlugin.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.Extensions.Configuration;
1212
using Microsoft.Extensions.DependencyInjection;
1313
using Microsoft.Extensions.Logging;
14+
using Microsoft.Extensions.Logging.Abstractions;
1415
using OpenTelemetry;
1516
using OpenTelemetry.Exporter;
1617
using OpenTelemetry.Metrics;
@@ -202,6 +203,7 @@ public override Task AfterRecordingStopAsync(RecordingArgs e, CancellationToken
202203
Environment = Configuration.Environment,
203204
Currency = Configuration.Currency,
204205
IncludeCosts = Configuration.IncludeCosts,
206+
ModelUsage = GetOpenAIModelUsage(e.RequestLogs)
205207
};
206208

207209
StoreReport(report, e);
@@ -901,6 +903,100 @@ private void RecordUsageMetrics(Activity activity, OpenAIRequest request, OpenAI
901903
Logger.LogTrace("RecordUsageMetrics() finished");
902904
}
903905

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+
9041000
private static string GetOperationName(OpenAIRequest request)
9051001
{
9061002
if (request == null)

0 commit comments

Comments
 (0)