diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs
index d21d3b20585..406b9768dd7 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs
@@ -9,7 +9,10 @@ namespace Microsoft.Extensions.AI;
public class ChatClientMetadata
{
/// Initializes a new instance of the class.
- /// The name of the chat completion provider, if applicable.
+ ///
+ /// The name of the chat completion provider, if applicable. Where possible, this should map to the
+ /// appropriate name defined in the OpenTelemetry Semantic Conventions for Generative AI systems.
+ ///
/// The URL for accessing the chat completion provider, if applicable.
/// The ID of the chat completion model used, if applicable.
public ChatClientMetadata(string? providerName = null, Uri? providerUri = null, string? modelId = null)
@@ -20,12 +23,19 @@ public ChatClientMetadata(string? providerName = null, Uri? providerUri = null,
}
/// Gets the name of the chat completion provider.
+ ///
+ /// Where possible, this maps to the appropriate name defined in the
+ /// OpenTelemetry Semantic Conventions for Generative AI systems.
+ ///
public string? ProviderName { get; }
/// Gets the URL for accessing the chat completion provider.
public Uri? ProviderUri { get; }
/// Gets the ID of the model used by this chat completion provider.
- /// This value can be null if either the name is unknown or there are multiple possible models associated with this instance.
+ ///
+ /// This value can be null if either the name is unknown or there are multiple possible models associated with this instance.
+ /// An individual request may override this value via .
+ ///
public string? ModelId { get; }
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs
index 0f2f7b23af5..a3f5181648b 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs
@@ -9,7 +9,11 @@ namespace Microsoft.Extensions.AI;
public class EmbeddingGeneratorMetadata
{
/// Initializes a new instance of the class.
- /// The name of the embedding generation provider, if applicable.
+
+ ///
+ /// The name of the embedding generation provider, if applicable. Where possible, this should map to the
+ /// appropriate name defined in the OpenTelemetry Semantic Conventions for Generative AI systems.
+ ///
/// The URL for accessing the embedding generation provider, if applicable.
/// The ID of the embedding generation model used, if applicable.
/// The number of dimensions in vectors produced by this generator, if applicable.
@@ -22,15 +26,26 @@ public EmbeddingGeneratorMetadata(string? providerName = null, Uri? providerUri
}
/// Gets the name of the embedding generation provider.
+ ///
+ /// Where possible, this maps to the appropriate name defined in the
+ /// OpenTelemetry Semantic Conventions for Generative AI systems.
+ ///
public string? ProviderName { get; }
/// Gets the URL for accessing the embedding generation provider.
public Uri? ProviderUri { get; }
/// Gets the ID of the model used by this embedding generation provider.
- /// This value can be null if either the name is unknown or there are multiple possible models associated with this instance.
+ ///
+ /// This value can be null if either the name is unknown or there are multiple possible models associated with this instance.
+ /// An individual request may override this value via .
+ ///
public string? ModelId { get; }
/// Gets the number of dimensions in the embeddings produced by this instance.
+ ///
+ /// This value can be null if either the number of dimensions is unknown or there are multiple possible lengths associated with this instance.
+ /// An individual request may override this value via .
+ ///
public int? Dimensions { get; }
}
diff --git a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs
index 193006780a2..e20a485a02a 100644
--- a/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI/ChatCompletion/OpenTelemetryChatClient.cs
@@ -23,7 +23,7 @@ namespace Microsoft.Extensions.AI;
/// Represents a delegating chat client that implements the OpenTelemetry Semantic Conventions for Generative AI systems.
///
-/// The draft specification this follows is available at .
+/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.29, defined at .
/// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change.
///
public sealed partial class OpenTelemetryChatClient : DelegatingChatClient
@@ -288,6 +288,29 @@ public override async IAsyncEnumerable CompleteSt
{
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.PerProvider(_system, "seed"), seed);
}
+
+ if (options.AdditionalProperties is { } props)
+ {
+ // Log all additional request options as per-provider tags. This is non-normative, but it covers cases where
+ // there's a per-provider specification in a best-effort manner (e.g. gen_ai.openai.request.service_tier),
+ // and more generally cases where there's additional useful information to be logged.
+ foreach (KeyValuePair prop in props)
+ {
+ string name = JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key);
+ switch (name)
+ {
+ // Skip known properties handled above so as to avoid possible conflicts.
+ case "response_format":
+ case "seed":
+ break;
+
+ // Handle all others by adding the snake_lower_case variant of the property name.
+ default:
+ _ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.PerProvider(_system, name), prop.Value);
+ break;
+ }
+ }
+ }
}
}
}
@@ -375,6 +398,22 @@ private void TraceCompletion(
{
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Response.OutputTokens, outputTokens);
}
+
+ if (_system is not null)
+ {
+ // Log all additional response properties as per-provider tags. This is non-normative, but it covers cases where
+ // there's a per-provider specification in a best-effort manner (e.g. gen_ai.openai.response.system_fingerprint),
+ // and more generally cases where there's additional useful information to be logged.
+ if (completion.AdditionalProperties is { } props)
+ {
+ foreach (KeyValuePair prop in props)
+ {
+ _ = activity.AddTag(
+ OpenTelemetryConsts.GenAI.Response.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)),
+ prop.Value);
+ }
+ }
+ }
}
}
diff --git a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs
index 09f762d33d0..8bb38bf2e07 100644
--- a/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs
+++ b/src/Libraries/Microsoft.Extensions.AI/Embeddings/OpenTelemetryEmbeddingGenerator.cs
@@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Linq;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -15,8 +16,8 @@ namespace Microsoft.Extensions.AI;
/// Represents a delegating embedding generator that implements the OpenTelemetry Semantic Conventions for Generative AI systems.
///
-/// The draft specification this follows is available at .
-/// The specification is still experimental and subject to change; as such, the telemetry output by this generator is also subject to change.
+/// This class provides an implementation of the Semantic Conventions for Generative AI systems v1.29, defined at .
+/// The specification is still experimental and subject to change; as such, the telemetry output by this client is also subject to change.
///
/// The type of input used to produce embeddings.
/// The type of embedding generated.
@@ -29,6 +30,7 @@ public sealed class OpenTelemetryEmbeddingGenerator : Delega
private readonly Histogram _tokenUsageHistogram;
private readonly Histogram _operationDurationHistogram;
+ private readonly string? _system;
private readonly string? _modelId;
private readonly string? _modelProvider;
private readonly string? _endpointAddress;
@@ -49,6 +51,7 @@ public OpenTelemetryEmbeddingGenerator(IEmbeddingGenerator i
Debug.Assert(innerGenerator is not null, "Should have been validated by the base ctor.");
EmbeddingGeneratorMetadata metadata = innerGenerator!.Metadata;
+ _system = metadata.ProviderName;
_modelId = metadata.ModelId;
_modelProvider = metadata.ProviderName;
_endpointAddress = metadata.ProviderUri?.GetLeftPart(UriPartial.Path);
@@ -126,11 +129,11 @@ protected override void Dispose(bool disposing)
string? modelId = options?.ModelId ?? _modelId;
activity = _activitySource.StartActivity(
- string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.Embed : $"{OpenTelemetryConsts.GenAI.Embed} {modelId}",
+ string.IsNullOrWhiteSpace(modelId) ? OpenTelemetryConsts.GenAI.Embeddings : $"{OpenTelemetryConsts.GenAI.Embeddings} {modelId}",
ActivityKind.Client,
default(ActivityContext),
[
- new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embed),
+ new(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embeddings),
new(OpenTelemetryConsts.GenAI.Request.Model, modelId),
new(OpenTelemetryConsts.GenAI.SystemName, _modelProvider),
]);
@@ -148,6 +151,23 @@ protected override void Dispose(bool disposing)
{
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Request.EmbeddingDimensions, dimensions);
}
+
+ if (options is not null &&
+ _system is not null)
+ {
+ // Log all additional request options as per-provider tags. This is non-normative, but it covers cases where
+ // there's a per-provider specification in a best-effort manner (e.g. gen_ai.openai.request.service_tier),
+ // and more generally cases where there's additional useful information to be logged.
+ if (options.AdditionalProperties is { } props)
+ {
+ foreach (KeyValuePair prop in props)
+ {
+ _ = activity.AddTag(
+ OpenTelemetryConsts.GenAI.Request.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)),
+ prop.Value);
+ }
+ }
+ }
}
}
@@ -212,12 +232,26 @@ private void TraceCompletion(
{
_ = activity.AddTag(OpenTelemetryConsts.GenAI.Response.Model, responseModelId);
}
+
+ // Log all additional response properties as per-provider tags. This is non-normative, but it covers cases where
+ // there's a per-provider specification in a best-effort manner (e.g. gen_ai.openai.response.system_fingerprint),
+ // and more generally cases where there's additional useful information to be logged.
+ if (_system is not null &&
+ embeddings?.AdditionalProperties is { } props)
+ {
+ foreach (KeyValuePair prop in props)
+ {
+ _ = activity.AddTag(
+ OpenTelemetryConsts.GenAI.Response.PerProvider(_system, JsonNamingPolicy.SnakeCaseLower.ConvertName(prop.Key)),
+ prop.Value);
+ }
+ }
}
}
private void AddMetricTags(ref TagList tags, string? requestModelId, string? responseModelId)
{
- tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embed);
+ tags.Add(OpenTelemetryConsts.GenAI.Operation.Name, OpenTelemetryConsts.GenAI.Embeddings);
if (requestModelId is not null)
{
diff --git a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs
index 27a543705ba..4c40c04c236 100644
--- a/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs
+++ b/src/Libraries/Microsoft.Extensions.AI/OpenTelemetryConsts.cs
@@ -30,7 +30,7 @@ public static class GenAI
public const string SystemName = "gen_ai.system";
public const string Chat = "chat";
- public const string Embed = "embed";
+ public const string Embeddings = "embeddings";
public static class Assistant
{
@@ -81,6 +81,8 @@ public static class Response
public const string InputTokens = "gen_ai.response.input_tokens";
public const string Model = "gen_ai.response.model";
public const string OutputTokens = "gen_ai.response.output_tokens";
+
+ public static string PerProvider(string providerName, string parameterName) => $"gen_ai.{providerName}.response.{parameterName}";
}
public static class System