Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ public async Task<ChatResponse> GetResponseAsync(
{
_ = Throw.IfNull(messages);

OpenAIClientExtensions.AddOpenAIApiType(OpenAIClientExtensions.OpenAIApiTypeChatCompletions);

var openAIChatMessages = ToOpenAIChatMessages(messages, options);
var openAIOptions = ToOpenAIOptions(options);

Expand All @@ -106,6 +108,8 @@ public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
{
_ = Throw.IfNull(messages);

OpenAIClientExtensions.AddOpenAIApiType(OpenAIClientExtensions.OpenAIApiTypeChatCompletions);

var openAIChatMessages = ToOpenAIChatMessages(messages, options);
var openAIOptions = ToOpenAIOptions(options);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Mime;
using System.Text;
Expand Down Expand Up @@ -279,4 +280,34 @@ internal sealed class ToolJson
[JsonExtensionData]
public Dictionary<string, JsonElement>? ExtensionData { get; set; }
}

/// <summary>The "openai.api.type" tag name per the OpenTelemetry semantic conventions for OpenAI.</summary>
internal const string OpenAIApiTypeTag = "openai.api.type";

/// <summary>The "chat_completions" value for the "openai.api.type" tag.</summary>
internal const string OpenAIApiTypeChatCompletions = "chat_completions";

/// <summary>The "responses" value for the "openai.api.type" tag.</summary>
internal const string OpenAIApiTypeResponses = "responses";

/// <summary>The "chat" operation name used by the OpenTelemetry chat client.</summary>
private const string ChatOperationName = "chat";

/// <summary>
/// If the current <see cref="Activity"/> represents a "chat" operation span,
/// adds the "openai.api.type" tag with the specified value.
/// </summary>
internal static void AddOpenAIApiType(string apiType)
{
Activity? activity = Activity.Current;
if (activity is { IsAllDataRequested: true })
{
string name = activity.DisplayName;
if (name.StartsWith(ChatOperationName, StringComparison.Ordinal) &&
(name.Length == ChatOperationName.Length || name[ChatOperationName.Length] == ' '))
{
_ = activity.AddTag(OpenAIApiTypeTag, apiType);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ public async Task<ChatResponse> GetResponseAsync(
{
_ = Throw.IfNull(messages);

OpenAIClientExtensions.AddOpenAIApiType(OpenAIClientExtensions.OpenAIApiTypeResponses);

// Convert the inputs into what ResponsesClient expects.
var openAIOptions = AsCreateResponseOptions(options, out string? openAIConversationId);

Expand Down Expand Up @@ -260,6 +262,8 @@ public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
{
_ = Throw.IfNull(messages);

OpenAIClientExtensions.AddOpenAIApiType(OpenAIClientExtensions.OpenAIApiTypeResponses);

var openAIOptions = AsCreateResponseOptions(options, out string? openAIConversationId);
openAIOptions.StreamingEnabled = true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
Expand Down Expand Up @@ -2010,4 +2011,68 @@ public async Task ReasoningContent_Streaming_SurfacedAsTextReasoningContent()
// Verify regular content was also captured from the content deltas
Assert.Equal("9.8 is larger.", string.Concat(updates.Select(u => u.Text)));
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task OpenAIApiTypeTag_SetToChatCompletions(bool streaming)
{
const string Output = """
{
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 1727888631,
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello!"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 8,
"completion_tokens": 2,
"total_tokens": 10
}
}
""";

var sourceName = Guid.NewGuid().ToString();
var activities = new List<Activity>();
using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == sourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStopped = activity => activities.Add(activity),
};
ActivitySource.AddActivityListener(listener);

using VerbatimHttpHandler handler = new(new HttpHandlerExpectedInput(), Output);
using HttpClient httpClient = new(handler);
using IChatClient client = new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) })
.GetChatClient("gpt-4o-mini")
.AsIChatClient()
.AsBuilder()
.UseOpenTelemetry(sourceName: sourceName)
.Build();

if (streaming)
{
await foreach (var update in client.GetStreamingResponseAsync("hello"))
{
// Drain the stream.
}
}
else
{
await client.GetResponseAsync("hello");
}

var activity = Assert.Single(activities);
Assert.Equal("chat_completions", activity.GetTagItem("openai.api.type"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
Expand Down Expand Up @@ -6010,6 +6011,75 @@ public async Task ReasoningOptions_NoneValues_ProducesNoReasoningInJson()
}));
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task OpenAIApiTypeTag_SetToResponses(bool streaming)
{
const string Output = """
{
"id": "resp_test",
"object": "response",
"created_at": 1741891428,
"status": "completed",
"model": "gpt-4o-mini",
"output": [
{
"id": "msg_test",
"type": "message",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "Hello!"
}
]
}
],
"usage": {
"input_tokens": 8,
"output_tokens": 2,
"total_tokens": 10
}
}
""";

var sourceName = Guid.NewGuid().ToString();
var activities = new List<Activity>();
using var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == sourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
ActivityStopped = activity => activities.Add(activity),
};
ActivitySource.AddActivityListener(listener);

using VerbatimHttpHandler handler = new(new HttpHandlerExpectedInput(), Output);
using HttpClient httpClient = new(handler);
using IChatClient client = new OpenAIClient(new ApiKeyCredential("apikey"), new OpenAIClientOptions { Transport = new HttpClientPipelineTransport(httpClient) })
.GetResponsesClient("gpt-4o-mini")
.AsIChatClient()
.AsBuilder()
.UseOpenTelemetry(sourceName: sourceName)
.Build();

if (streaming)
{
await foreach (var update in client.GetStreamingResponseAsync("hello"))
{
// Drain the stream.
}
}
else
{
await client.GetResponseAsync("hello");
}

var activity = Assert.Single(activities);
Assert.Equal("responses", activity.GetTagItem("openai.api.type"));
}

private static IChatClient CreateResponseClient(HttpClient httpClient, string modelId) =>
new OpenAIClient(
new ApiKeyCredential("apikey"),
Expand Down
Loading