Skip to content

Commit dd64272

Browse files
Adds support for logging LM token usage for streamed responses. Closes #1345 (#1346)
* Adds support for logging LM token usage for streamed responses. Closes #1345 * Update DevProxy.Plugins/Inspection/OpenAITelemetryPlugin.cs Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 3d31553 commit dd64272

File tree

1 file changed

+65
-1
lines changed

1 file changed

+65
-1
lines changed

DevProxy.Plugins/Inspection/OpenAITelemetryPlugin.cs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using System.Diagnostics;
2020
using System.Diagnostics.Metrics;
2121
using System.Text.Json;
22+
using Titanium.Web.Proxy.Http;
2223

2324
namespace DevProxy.Plugins.Inspection;
2425

@@ -321,7 +322,13 @@ private void ProcessSuccessResponse(Activity activity, ProxyResponseArgs e)
321322
return;
322323
}
323324

324-
AddResponseTypeSpecificTags(activity, openAiRequest, response.BodyString);
325+
var bodyString = response.BodyString;
326+
if (IsStreamingResponse(response))
327+
{
328+
bodyString = GetBodyFromStreamingResponse(response);
329+
}
330+
331+
AddResponseTypeSpecificTags(activity, openAiRequest, bodyString);
325332

326333
Logger.LogTrace("ProcessSuccessResponse() finished");
327334
}
@@ -997,6 +1004,63 @@ private static string GetOperationName(OpenAIRequest request)
9971004
};
9981005
}
9991006

1007+
private bool IsStreamingResponse(Response response)
1008+
{
1009+
Logger.LogTrace("{Method} called", nameof(IsStreamingResponse));
1010+
var contentType = response.Headers.FirstOrDefault(h => h.Name.Equals("content-type", StringComparison.OrdinalIgnoreCase))?.Value;
1011+
if (string.IsNullOrEmpty(contentType))
1012+
{
1013+
Logger.LogDebug("No content-type header found");
1014+
return false;
1015+
}
1016+
1017+
var isStreamingResponse = contentType.Contains("text/event-stream", StringComparison.OrdinalIgnoreCase);
1018+
Logger.LogDebug("IsStreamingResponse: {IsStreamingResponse}", isStreamingResponse);
1019+
1020+
Logger.LogTrace("{Method} finished", nameof(IsStreamingResponse));
1021+
return isStreamingResponse;
1022+
}
1023+
1024+
private string GetBodyFromStreamingResponse(Response response)
1025+
{
1026+
Logger.LogTrace("{Method} called", nameof(GetBodyFromStreamingResponse));
1027+
1028+
// default to the whole body
1029+
var bodyString = response.BodyString;
1030+
1031+
var chunks = bodyString.Split("\n\n", StringSplitOptions.RemoveEmptyEntries);
1032+
if (chunks.Length == 0)
1033+
{
1034+
Logger.LogDebug("No chunks found in the response body");
1035+
return bodyString;
1036+
}
1037+
1038+
// check if the last chunk is `data: [DONE]`
1039+
var lastChunk = chunks.Last().Trim();
1040+
if (lastChunk.Equals("data: [DONE]", StringComparison.OrdinalIgnoreCase))
1041+
{
1042+
// get next to last chunk
1043+
var chunk = chunks.Length > 1 ? chunks[^2].Trim() : string.Empty;
1044+
if (chunk.StartsWith("data: ", StringComparison.OrdinalIgnoreCase))
1045+
{
1046+
// remove the "data: " prefix
1047+
bodyString = chunk["data: ".Length..].Trim();
1048+
Logger.LogDebug("Last chunk starts with 'data: ', using the last chunk as the body: {BodyString}", bodyString);
1049+
}
1050+
else
1051+
{
1052+
Logger.LogDebug("Last chunk does not start with 'data: ', using the whole body");
1053+
}
1054+
}
1055+
else
1056+
{
1057+
Logger.LogDebug("Last chunk is not `data: [DONE]`, using the whole body");
1058+
}
1059+
1060+
Logger.LogTrace("{Method} finished", nameof(GetBodyFromStreamingResponse));
1061+
return bodyString;
1062+
}
1063+
10001064
public void Dispose()
10011065
{
10021066
_loader?.Dispose();

0 commit comments

Comments
 (0)