| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using System.Diagnostics; |
| | | 3 | | using System.Runtime.CompilerServices; |
| | | 4 | | |
| | | 5 | | using Microsoft.Extensions.AI; |
| | | 6 | | |
| | | 7 | | using NexusLabs.Needlr.AgentFramework.Progress; |
| | | 8 | | |
| | | 9 | | namespace NexusLabs.Needlr.AgentFramework.Diagnostics; |
| | | 10 | | |
| | | 11 | | /// <summary> |
| | | 12 | | /// Single writer for chat completion diagnostics. Wraps each |
| | | 13 | | /// <c>IChatClient.GetResponseAsync()</c> call to capture per-completion timing, |
| | | 14 | | /// token usage, and full request/response payloads. Records to the AsyncLocal |
| | | 15 | | /// <see cref="AgentRunDiagnosticsBuilder"/> and a thread-safe collection (for |
| | | 16 | | /// workflow runs where AsyncLocal doesn't propagate). Optionally emits |
| | | 17 | | /// <see cref="LlmCallStartedEvent"/>/<see cref="LlmCallCompletedEvent"/> to the |
| | | 18 | | /// progress reporter and OTel metrics via <see cref="IAgentMetrics"/>. |
| | | 19 | | /// </summary> |
| | | 20 | | /// <remarks> |
| | | 21 | | /// <para> |
| | | 22 | | /// <c>IterativeAgentLoop</c> wraps its chat client with this middleware |
| | | 23 | | /// internally, making it the sole writer for <see cref="ChatCompletionDiagnostics"/>. |
| | | 24 | | /// No other code should call <see cref="AgentRunDiagnosticsBuilder.AddChatCompletion"/> |
| | | 25 | | /// for calls that pass through this middleware. |
| | | 26 | | /// </para> |
| | | 27 | | /// <para> |
| | | 28 | | /// <see cref="IAgentMetrics"/> and <see cref="IProgressReporterAccessor"/> are optional. |
| | | 29 | | /// When null, recording still occurs but OTel metrics and progress events are skipped. |
| | | 30 | | /// </para> |
| | | 31 | | /// </remarks> |
| | | 32 | | [DoNotAutoRegister] |
| | | 33 | | internal sealed class DiagnosticsChatClientMiddleware : IChatCompletionCollector |
| | | 34 | | { |
| | | 35 | | private readonly IAgentMetrics? _metrics; |
| | | 36 | | private readonly IProgressReporterAccessor? _progressAccessor; |
| | | 37 | | private readonly ChatCompletionActivityMode _activityMode; |
| | 176 | 38 | | private readonly ConcurrentQueue<ChatCompletionDiagnostics> _allCompletions = new(); |
| | | 39 | | private int _sequenceCounter; |
| | | 40 | | |
| | 176 | 41 | | internal DiagnosticsChatClientMiddleware( |
| | 176 | 42 | | IAgentMetrics? metrics = null, |
| | 176 | 43 | | IProgressReporterAccessor? progressAccessor = null, |
| | 176 | 44 | | ChatCompletionActivityMode activityMode = ChatCompletionActivityMode.Always) |
| | | 45 | | { |
| | 176 | 46 | | _metrics = metrics; |
| | 176 | 47 | | _progressAccessor = progressAccessor; |
| | 176 | 48 | | _activityMode = activityMode; |
| | 176 | 49 | | } |
| | | 50 | | |
| | | 51 | | /// <summary> |
| | | 52 | | /// Drains all captured completions since the last drain. Thread-safe. |
| | | 53 | | /// </summary> |
| | | 54 | | public IReadOnlyList<ChatCompletionDiagnostics> DrainCompletions() |
| | | 55 | | { |
| | 23 | 56 | | var results = new List<ChatCompletionDiagnostics>(); |
| | 44 | 57 | | while (_allCompletions.TryDequeue(out var completion)) |
| | | 58 | | { |
| | 21 | 59 | | results.Add(completion); |
| | 21 | 60 | | } |
| | 23 | 61 | | return results; |
| | | 62 | | } |
| | | 63 | | |
| | | 64 | | internal async Task<ChatResponse> HandleAsync( |
| | | 65 | | IEnumerable<ChatMessage> messages, |
| | | 66 | | ChatOptions? options, |
| | | 67 | | IChatClient innerChatClient, |
| | | 68 | | CancellationToken cancellationToken) |
| | | 69 | | { |
| | 281 | 70 | | var builder = AgentRunDiagnosticsBuilder.GetCurrent(); |
| | 281 | 71 | | var sequence = builder?.NextChatCompletionSequence() |
| | 281 | 72 | | ?? Interlocked.Increment(ref _sequenceCounter) - 1; |
| | 281 | 73 | | var startedAt = DateTimeOffset.UtcNow; |
| | 281 | 74 | | var stopwatch = Stopwatch.StartNew(); |
| | | 75 | | |
| | 281 | 76 | | var (ownedActivity, targetActivity) = StartChatActivity("agent.chat"); |
| | 281 | 77 | | using var _ = ownedActivity; |
| | | 78 | | |
| | 281 | 79 | | if (_progressAccessor is not null) |
| | | 80 | | { |
| | 35 | 81 | | _progressAccessor.Current.Report(new LlmCallStartedEvent( |
| | 35 | 82 | | Timestamp: startedAt, |
| | 35 | 83 | | WorkflowId: _progressAccessor.Current.WorkflowId, |
| | 35 | 84 | | AgentId: _progressAccessor.Current.AgentId, |
| | 35 | 85 | | ParentAgentId: builder?.ParentAgentName, |
| | 35 | 86 | | Depth: _progressAccessor.Current.Depth, |
| | 35 | 87 | | SequenceNumber: _progressAccessor.Current.NextSequence(), |
| | 35 | 88 | | CallSequence: sequence)); |
| | | 89 | | } |
| | | 90 | | |
| | | 91 | | try |
| | | 92 | | { |
| | 281 | 93 | | var response = await innerChatClient.GetResponseAsync(messages, options, cancellationToken) |
| | 281 | 94 | | .ConfigureAwait(false); |
| | | 95 | | |
| | 272 | 96 | | stopwatch.Stop(); |
| | | 97 | | |
| | 272 | 98 | | var model = response.ModelId ?? "unknown"; |
| | | 99 | | |
| | 272 | 100 | | targetActivity?.SetTag("gen_ai.response.model", model); |
| | 272 | 101 | | targetActivity?.SetTag("agent.chat.sequence", sequence); |
| | 272 | 102 | | targetActivity?.SetTag("status", "success"); |
| | | 103 | | |
| | 272 | 104 | | _metrics?.RecordChatCompletion(model, stopwatch.Elapsed, succeeded: true, agentName: builder?.AgentName); |
| | | 105 | | |
| | 272 | 106 | | var usage = response.Usage; |
| | 272 | 107 | | var tokens = new TokenUsage( |
| | 272 | 108 | | InputTokens: usage?.InputTokenCount ?? 0, |
| | 272 | 109 | | OutputTokens: usage?.OutputTokenCount ?? 0, |
| | 272 | 110 | | TotalTokens: usage?.TotalTokenCount ?? 0, |
| | 272 | 111 | | CachedInputTokens: usage?.AdditionalCounts?.GetValueOrDefault("CachedInputTokens") ?? 0, |
| | 272 | 112 | | ReasoningTokens: usage?.AdditionalCounts?.GetValueOrDefault("ReasoningTokens") ?? 0); |
| | | 113 | | |
| | 272 | 114 | | targetActivity?.SetTag("gen_ai.usage.input_tokens", tokens.InputTokens); |
| | 272 | 115 | | targetActivity?.SetTag("gen_ai.usage.output_tokens", tokens.OutputTokens); |
| | | 116 | | |
| | 272 | 117 | | var messageList = messages as ICollection<ChatMessage> ?? messages.ToList(); |
| | | 118 | | |
| | 272 | 119 | | var diagnostics = new ChatCompletionDiagnostics( |
| | 272 | 120 | | Sequence: sequence, |
| | 272 | 121 | | Model: model, |
| | 272 | 122 | | Tokens: tokens, |
| | 272 | 123 | | InputMessageCount: messageList.Count, |
| | 272 | 124 | | Duration: stopwatch.Elapsed, |
| | 272 | 125 | | Succeeded: true, |
| | 272 | 126 | | ErrorMessage: null, |
| | 272 | 127 | | StartedAt: startedAt, |
| | 272 | 128 | | CompletedAt: DateTimeOffset.UtcNow) |
| | 272 | 129 | | { |
| | 272 | 130 | | AgentName = builder?.AgentName, |
| | 272 | 131 | | RequestMessages = messageList as IReadOnlyList<ChatMessage> ?? messageList.ToList(), |
| | 272 | 132 | | Response = response, |
| | 272 | 133 | | RequestCharCount = DiagnosticsCharCounter.ChatMessagesLength(messageList as IReadOnlyList<ChatMessage> ? |
| | 272 | 134 | | ResponseCharCount = DiagnosticsCharCounter.ChatResponseLength(response), |
| | 272 | 135 | | }; |
| | | 136 | | |
| | 272 | 137 | | builder?.AddChatCompletion(diagnostics); |
| | 272 | 138 | | _allCompletions.Enqueue(diagnostics); |
| | | 139 | | |
| | 272 | 140 | | if (_progressAccessor is not null) |
| | | 141 | | { |
| | 34 | 142 | | _progressAccessor.Current.Report(new LlmCallCompletedEvent( |
| | 34 | 143 | | Timestamp: DateTimeOffset.UtcNow, |
| | 34 | 144 | | WorkflowId: _progressAccessor.Current.WorkflowId, |
| | 34 | 145 | | AgentId: _progressAccessor.Current.AgentId, |
| | 34 | 146 | | ParentAgentId: builder?.ParentAgentName, |
| | 34 | 147 | | Depth: _progressAccessor.Current.Depth, |
| | 34 | 148 | | SequenceNumber: _progressAccessor.Current.NextSequence(), |
| | 34 | 149 | | CallSequence: sequence, |
| | 34 | 150 | | Model: model, |
| | 34 | 151 | | Duration: stopwatch.Elapsed, |
| | 34 | 152 | | InputTokens: tokens.InputTokens, |
| | 34 | 153 | | OutputTokens: tokens.OutputTokens, |
| | 34 | 154 | | TotalTokens: tokens.TotalTokens)); |
| | | 155 | | } |
| | | 156 | | |
| | 272 | 157 | | return response; |
| | | 158 | | } |
| | 9 | 159 | | catch (Exception ex) |
| | | 160 | | { |
| | 9 | 161 | | stopwatch.Stop(); |
| | | 162 | | |
| | 9 | 163 | | targetActivity?.SetStatus(ActivityStatusCode.Error, ex.Message); |
| | 9 | 164 | | targetActivity?.SetTag("status", "failed"); |
| | | 165 | | |
| | 9 | 166 | | _metrics?.RecordChatCompletion("unknown", stopwatch.Elapsed, succeeded: false, agentName: builder?.AgentName |
| | | 167 | | |
| | 9 | 168 | | var failedMessageList = messages as IReadOnlyList<ChatMessage> ?? messages.ToList(); |
| | | 169 | | |
| | 9 | 170 | | var diagnostics = new ChatCompletionDiagnostics( |
| | 9 | 171 | | Sequence: sequence, |
| | 9 | 172 | | Model: "unknown", |
| | 9 | 173 | | Tokens: new TokenUsage(0, 0, 0, 0, 0), |
| | 9 | 174 | | InputMessageCount: 0, |
| | 9 | 175 | | Duration: stopwatch.Elapsed, |
| | 9 | 176 | | Succeeded: false, |
| | 9 | 177 | | ErrorMessage: ex.Message, |
| | 9 | 178 | | StartedAt: startedAt, |
| | 9 | 179 | | CompletedAt: DateTimeOffset.UtcNow) |
| | 9 | 180 | | { |
| | 9 | 181 | | AgentName = builder?.AgentName, |
| | 9 | 182 | | RequestMessages = failedMessageList, |
| | 9 | 183 | | RequestCharCount = DiagnosticsCharCounter.ChatMessagesLength(failedMessageList), |
| | 9 | 184 | | }; |
| | | 185 | | |
| | 9 | 186 | | builder?.AddChatCompletion(diagnostics); |
| | 9 | 187 | | _allCompletions.Enqueue(diagnostics); |
| | | 188 | | |
| | 9 | 189 | | if (_progressAccessor is not null) |
| | | 190 | | { |
| | 1 | 191 | | _progressAccessor.Current.Report(new LlmCallFailedEvent( |
| | 1 | 192 | | Timestamp: DateTimeOffset.UtcNow, |
| | 1 | 193 | | WorkflowId: _progressAccessor.Current.WorkflowId, |
| | 1 | 194 | | AgentId: _progressAccessor.Current.AgentId, |
| | 1 | 195 | | ParentAgentId: builder?.ParentAgentName, |
| | 1 | 196 | | Depth: _progressAccessor.Current.Depth, |
| | 1 | 197 | | SequenceNumber: _progressAccessor.Current.NextSequence(), |
| | 1 | 198 | | CallSequence: sequence, |
| | 1 | 199 | | ErrorMessage: ex.Message, |
| | 1 | 200 | | Duration: stopwatch.Elapsed)); |
| | | 201 | | } |
| | | 202 | | |
| | 9 | 203 | | throw; |
| | | 204 | | } |
| | 272 | 205 | | } |
| | | 206 | | |
| | | 207 | | internal async IAsyncEnumerable<ChatResponseUpdate> HandleStreamingAsync( |
| | | 208 | | IEnumerable<ChatMessage> messages, |
| | | 209 | | ChatOptions? options, |
| | | 210 | | IChatClient innerChatClient, |
| | | 211 | | [EnumeratorCancellation] CancellationToken cancellationToken) |
| | | 212 | | { |
| | 25 | 213 | | var builder = AgentRunDiagnosticsBuilder.GetCurrent(); |
| | 25 | 214 | | var sequence = builder?.NextChatCompletionSequence() |
| | 25 | 215 | | ?? Interlocked.Increment(ref _sequenceCounter) - 1; |
| | 25 | 216 | | var startedAt = DateTimeOffset.UtcNow; |
| | 25 | 217 | | var stopwatch = Stopwatch.StartNew(); |
| | | 218 | | |
| | 25 | 219 | | var (ownedStreamActivity, targetActivity) = StartChatActivity("agent.chat.stream"); |
| | 25 | 220 | | using var _s = ownedStreamActivity; |
| | | 221 | | |
| | 25 | 222 | | if (_progressAccessor is not null) |
| | | 223 | | { |
| | 23 | 224 | | _progressAccessor.Current.Report(new LlmCallStartedEvent( |
| | 23 | 225 | | Timestamp: startedAt, |
| | 23 | 226 | | WorkflowId: _progressAccessor.Current.WorkflowId, |
| | 23 | 227 | | AgentId: _progressAccessor.Current.AgentId, |
| | 23 | 228 | | ParentAgentId: builder?.ParentAgentName, |
| | 23 | 229 | | Depth: _progressAccessor.Current.Depth, |
| | 23 | 230 | | SequenceNumber: _progressAccessor.Current.NextSequence(), |
| | 23 | 231 | | CallSequence: sequence)); |
| | | 232 | | } |
| | | 233 | | |
| | 25 | 234 | | var messageList = messages as IReadOnlyList<ChatMessage> ?? messages.ToList(); |
| | 25 | 235 | | var buffered = new List<ChatResponseUpdate>(); |
| | 25 | 236 | | Exception? failure = null; |
| | | 237 | | |
| | 25 | 238 | | var enumerable = innerChatClient.GetStreamingResponseAsync(messages, options, cancellationToken); |
| | 24 | 239 | | var enumerator = enumerable.GetAsyncEnumerator(cancellationToken); |
| | | 240 | | |
| | | 241 | | try |
| | | 242 | | { |
| | | 243 | | while (true) |
| | | 244 | | { |
| | | 245 | | ChatResponseUpdate update; |
| | | 246 | | try |
| | | 247 | | { |
| | 54 | 248 | | if (!await enumerator.MoveNextAsync().ConfigureAwait(false)) |
| | | 249 | | { |
| | 22 | 250 | | break; |
| | | 251 | | } |
| | 30 | 252 | | update = enumerator.Current; |
| | 30 | 253 | | } |
| | 2 | 254 | | catch (Exception ex) |
| | | 255 | | { |
| | 2 | 256 | | failure = ex; |
| | 2 | 257 | | break; |
| | | 258 | | } |
| | | 259 | | |
| | 30 | 260 | | buffered.Add(update); |
| | 30 | 261 | | yield return update; |
| | | 262 | | } |
| | | 263 | | } |
| | | 264 | | finally |
| | | 265 | | { |
| | 24 | 266 | | await enumerator.DisposeAsync().ConfigureAwait(false); |
| | | 267 | | } |
| | | 268 | | |
| | 24 | 269 | | stopwatch.Stop(); |
| | | 270 | | |
| | 24 | 271 | | var aggregated = buffered.ToChatResponse(); |
| | | 272 | | |
| | 24 | 273 | | if (failure is null) |
| | | 274 | | { |
| | 22 | 275 | | var model = aggregated.ModelId ?? "unknown"; |
| | | 276 | | |
| | 22 | 277 | | targetActivity?.SetTag("gen_ai.response.model", model); |
| | 22 | 278 | | targetActivity?.SetTag("agent.chat.sequence", sequence); |
| | 22 | 279 | | targetActivity?.SetTag("status", "success"); |
| | | 280 | | |
| | 22 | 281 | | _metrics?.RecordChatCompletion(model, stopwatch.Elapsed, succeeded: true, agentName: builder?.AgentName); |
| | | 282 | | |
| | 22 | 283 | | var usage = aggregated.Usage; |
| | 22 | 284 | | var tokens = new TokenUsage( |
| | 22 | 285 | | InputTokens: usage?.InputTokenCount ?? 0, |
| | 22 | 286 | | OutputTokens: usage?.OutputTokenCount ?? 0, |
| | 22 | 287 | | TotalTokens: usage?.TotalTokenCount ?? 0, |
| | 22 | 288 | | CachedInputTokens: usage?.AdditionalCounts?.GetValueOrDefault("CachedInputTokens") ?? 0, |
| | 22 | 289 | | ReasoningTokens: usage?.AdditionalCounts?.GetValueOrDefault("ReasoningTokens") ?? 0); |
| | | 290 | | |
| | 22 | 291 | | targetActivity?.SetTag("gen_ai.usage.input_tokens", tokens.InputTokens); |
| | 22 | 292 | | targetActivity?.SetTag("gen_ai.usage.output_tokens", tokens.OutputTokens); |
| | | 293 | | |
| | 22 | 294 | | var diagnostics = new ChatCompletionDiagnostics( |
| | 22 | 295 | | Sequence: sequence, |
| | 22 | 296 | | Model: model, |
| | 22 | 297 | | Tokens: tokens, |
| | 22 | 298 | | InputMessageCount: messageList.Count, |
| | 22 | 299 | | Duration: stopwatch.Elapsed, |
| | 22 | 300 | | Succeeded: true, |
| | 22 | 301 | | ErrorMessage: null, |
| | 22 | 302 | | StartedAt: startedAt, |
| | 22 | 303 | | CompletedAt: DateTimeOffset.UtcNow) |
| | 22 | 304 | | { |
| | 22 | 305 | | AgentName = builder?.AgentName, |
| | 22 | 306 | | RequestMessages = messageList, |
| | 22 | 307 | | Response = aggregated, |
| | 22 | 308 | | RequestCharCount = DiagnosticsCharCounter.ChatMessagesLength(messageList), |
| | 22 | 309 | | ResponseCharCount = DiagnosticsCharCounter.ChatResponseLength(aggregated), |
| | 22 | 310 | | }; |
| | | 311 | | |
| | 22 | 312 | | builder?.AddChatCompletion(diagnostics); |
| | 22 | 313 | | _allCompletions.Enqueue(diagnostics); |
| | | 314 | | |
| | 22 | 315 | | if (_progressAccessor is not null) |
| | | 316 | | { |
| | 20 | 317 | | _progressAccessor.Current.Report(new LlmCallCompletedEvent( |
| | 20 | 318 | | Timestamp: DateTimeOffset.UtcNow, |
| | 20 | 319 | | WorkflowId: _progressAccessor.Current.WorkflowId, |
| | 20 | 320 | | AgentId: _progressAccessor.Current.AgentId, |
| | 20 | 321 | | ParentAgentId: builder?.ParentAgentName, |
| | 20 | 322 | | Depth: _progressAccessor.Current.Depth, |
| | 20 | 323 | | SequenceNumber: _progressAccessor.Current.NextSequence(), |
| | 20 | 324 | | CallSequence: sequence, |
| | 20 | 325 | | Model: model, |
| | 20 | 326 | | Duration: stopwatch.Elapsed, |
| | 20 | 327 | | InputTokens: tokens.InputTokens, |
| | 20 | 328 | | OutputTokens: tokens.OutputTokens, |
| | 20 | 329 | | TotalTokens: tokens.TotalTokens)); |
| | | 330 | | } |
| | | 331 | | } |
| | | 332 | | else |
| | | 333 | | { |
| | 2 | 334 | | targetActivity?.SetStatus(ActivityStatusCode.Error, failure.Message); |
| | 2 | 335 | | targetActivity?.SetTag("status", "failed"); |
| | | 336 | | |
| | 2 | 337 | | _metrics?.RecordChatCompletion("unknown", stopwatch.Elapsed, succeeded: false, agentName: builder?.AgentName |
| | | 338 | | |
| | 2 | 339 | | var diagnostics = new ChatCompletionDiagnostics( |
| | 2 | 340 | | Sequence: sequence, |
| | 2 | 341 | | Model: aggregated.ModelId ?? "unknown", |
| | 2 | 342 | | Tokens: new TokenUsage(0, 0, 0, 0, 0), |
| | 2 | 343 | | InputMessageCount: 0, |
| | 2 | 344 | | Duration: stopwatch.Elapsed, |
| | 2 | 345 | | Succeeded: false, |
| | 2 | 346 | | ErrorMessage: failure.Message, |
| | 2 | 347 | | StartedAt: startedAt, |
| | 2 | 348 | | CompletedAt: DateTimeOffset.UtcNow) |
| | 2 | 349 | | { |
| | 2 | 350 | | AgentName = builder?.AgentName, |
| | 2 | 351 | | RequestMessages = messageList, |
| | 2 | 352 | | Response = aggregated, |
| | 2 | 353 | | RequestCharCount = DiagnosticsCharCounter.ChatMessagesLength(messageList), |
| | 2 | 354 | | ResponseCharCount = DiagnosticsCharCounter.ChatResponseLength(aggregated), |
| | 2 | 355 | | }; |
| | | 356 | | |
| | 2 | 357 | | builder?.AddChatCompletion(diagnostics); |
| | 2 | 358 | | _allCompletions.Enqueue(diagnostics); |
| | | 359 | | |
| | 2 | 360 | | if (_progressAccessor is not null) |
| | | 361 | | { |
| | 2 | 362 | | _progressAccessor.Current.Report(new LlmCallFailedEvent( |
| | 2 | 363 | | Timestamp: DateTimeOffset.UtcNow, |
| | 2 | 364 | | WorkflowId: _progressAccessor.Current.WorkflowId, |
| | 2 | 365 | | AgentId: _progressAccessor.Current.AgentId, |
| | 2 | 366 | | ParentAgentId: builder?.ParentAgentName, |
| | 2 | 367 | | Depth: _progressAccessor.Current.Depth, |
| | 2 | 368 | | SequenceNumber: _progressAccessor.Current.NextSequence(), |
| | 2 | 369 | | CallSequence: sequence, |
| | 2 | 370 | | ErrorMessage: failure.Message, |
| | 2 | 371 | | Duration: stopwatch.Elapsed)); |
| | | 372 | | } |
| | | 373 | | |
| | 2 | 374 | | throw failure; |
| | | 375 | | } |
| | 22 | 376 | | } |
| | | 377 | | |
| | | 378 | | /// <summary> |
| | | 379 | | /// Creates a chat completion activity respecting <see cref="_activityMode"/>. |
| | | 380 | | /// When <see cref="ChatCompletionActivityMode.EnrichParent"/> is active and a |
| | | 381 | | /// parent <c>gen_ai.*</c> activity exists, returns <c>created = null</c> and |
| | | 382 | | /// <c>target = parent</c> so callers enrich the parent span without creating a |
| | | 383 | | /// duplicate child. The caller must only dispose <c>created</c>, never <c>target</c>. |
| | | 384 | | /// </summary> |
| | | 385 | | private (Activity? Created, Activity? Target) StartChatActivity(string operationName) |
| | | 386 | | { |
| | 306 | 387 | | if (_metrics is null) |
| | | 388 | | { |
| | 231 | 389 | | return (null, null); |
| | | 390 | | } |
| | | 391 | | |
| | 75 | 392 | | if (_activityMode == ChatCompletionActivityMode.EnrichParent) |
| | | 393 | | { |
| | 7 | 394 | | var parent = Activity.Current; |
| | 7 | 395 | | if (parent?.OperationName.StartsWith("gen_ai.", StringComparison.Ordinal) == true) |
| | | 396 | | { |
| | 5 | 397 | | return (Created: null, Target: parent); |
| | | 398 | | } |
| | | 399 | | } |
| | | 400 | | |
| | 70 | 401 | | var created = _metrics.ActivitySource.StartActivity(operationName, ActivityKind.Client); |
| | 70 | 402 | | return (Created: created, Target: created); |
| | | 403 | | } |
| | | 404 | | } |