|
19 | 19 | using System.Diagnostics;
|
20 | 20 | using System.Diagnostics.Metrics;
|
21 | 21 | using System.Text.Json;
|
| 22 | +using Titanium.Web.Proxy.Http; |
22 | 23 |
|
23 | 24 | namespace DevProxy.Plugins.Inspection;
|
24 | 25 |
|
@@ -321,7 +322,13 @@ private void ProcessSuccessResponse(Activity activity, ProxyResponseArgs e)
|
321 | 322 | return;
|
322 | 323 | }
|
323 | 324 |
|
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); |
325 | 332 |
|
326 | 333 | Logger.LogTrace("ProcessSuccessResponse() finished");
|
327 | 334 | }
|
@@ -997,6 +1004,63 @@ private static string GetOperationName(OpenAIRequest request)
|
997 | 1004 | };
|
998 | 1005 | }
|
999 | 1006 |
|
| 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 | + |
1000 | 1064 | public void Dispose()
|
1001 | 1065 | {
|
1002 | 1066 | _loader?.Dispose();
|
|
0 commit comments