diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs
index 027817ddff8..12b43543c03 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatClientMetadata.cs
@@ -34,7 +34,7 @@ public ChatClientMetadata(string? providerName = null, Uri? providerUri = null,
/// Gets the ID of the default model used by this chat client.
///
- /// This value can be null if no default model is set on the corresponding .
+ /// This value can be if no default model is set on the corresponding .
/// An individual request may override this value via .
///
public string? DefaultModelId { get; }
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatFinishReason.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatFinishReason.cs
index 970a833477c..18b3ec658e3 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatFinishReason.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatFinishReason.cs
@@ -14,12 +14,12 @@ namespace Microsoft.Extensions.AI;
[JsonConverter(typeof(Converter))]
public readonly struct ChatFinishReason : IEquatable
{
- /// The finish reason value. If null because `default(ChatFinishReason)` was used, the instance will behave like .
+ /// The finish reason value. If because `default(ChatFinishReason)` was used, the instance will behave like .
private readonly string? _value;
/// Initializes a new instance of the struct with a string that describes the reason.
/// The reason value.
- /// is null.
+ /// is .
/// is empty or composed entirely of whitespace.
[JsonConstructor]
public ChatFinishReason(string value)
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs
index d457c934f97..071254182c2 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatOptions.cs
@@ -62,7 +62,7 @@ public class ChatOptions
/// Gets or sets the response format for the chat request.
///
///
- /// If null, no response format is specified and the client will use its default.
+ /// If , no response format is specified and the client will use its default.
/// This property can be set to to specify that the response should be unstructured text,
/// to to specify that the response should be structured JSON data, or
/// an instance of constructed with a specific JSON schema to request that the
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormatJson.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormatJson.cs
index 673c2c51474..51f99f4d2c4 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormatJson.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseFormatJson.cs
@@ -32,7 +32,7 @@ public ChatResponseFormatJson(
SchemaDescription = schemaDescription;
}
- /// Gets the JSON schema associated with the response, or null if there is none.
+ /// Gets the JSON schema associated with the response, or if there is none.
public JsonElement? Schema { get; }
/// Gets a name for the schema.
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatRole.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatRole.cs
index c4f324de9a8..88e1bedb6e9 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatRole.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatRole.cs
@@ -54,7 +54,7 @@ public ChatRole(string value)
///
/// The first instance to compare.
/// The second instance to compare.
- /// if left and right are both null or have equivalent values; otherwise, .
+ /// if left and right are both or have equivalent values; otherwise, .
public static bool operator ==(ChatRole left, ChatRole right)
{
return left.Equals(right);
@@ -66,7 +66,7 @@ public ChatRole(string value)
///
/// The first instance to compare.
/// The second instance to compare.
- /// if left and right have different values; if they have equivalent values or are both null.
+ /// if left and right have different values; if they have equivalent values or are both .
public static bool operator !=(ChatRole left, ChatRole right)
{
return !(left == right);
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs
index dc0c5db9289..6353586208f 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/DataContent.cs
@@ -110,7 +110,7 @@ public DataContent([StringSyntax(StringSyntaxAttribute.Uri)] string uri, string?
///
/// The byte contents.
/// The media type (also known as MIME type) represented by the content.
- /// is null.
+ /// is .
/// is empty or composed entirely of whitespace.
public DataContent(ReadOnlyMemory data, string mediaType)
{
diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs
index 5f5f9d8c5c2..6c3503b4227 100644
--- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Embeddings/EmbeddingGeneratorMetadata.cs
@@ -36,14 +36,14 @@ public EmbeddingGeneratorMetadata(string? providerName = null, Uri? providerUri
/// Gets the ID of the default model used by this embedding generator.
///
- /// This value can be null if no default model is set on the corresponding embedding generator.
+ /// This value can be if no default model is set on the corresponding embedding generator.
/// An individual request may override this value via .
///
public string? DefaultModelId { get; }
/// Gets the number of dimensions in the embeddings produced by the default model.
///
- /// This value can be null if either the number of dimensions is unknown or there are multiple possible lengths associated with this model.
+ /// This value can be if either the number of dimensions is unknown or there are multiple possible lengths associated with this model.
/// An individual request may override this value via .
///
public int? DefaultModelDimensions { get; }
diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs
index c4ccafe098e..c78b910378c 100644
--- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs
@@ -22,7 +22,7 @@
namespace Microsoft.Extensions.AI;
/// Represents an for an Azure AI Inference .
-public sealed class AzureAIInferenceChatClient : IChatClient
+internal sealed class AzureAIInferenceChatClient : IChatClient
{
/// Metadata about the client.
private readonly ChatClientMetadata _metadata;
@@ -30,24 +30,21 @@ public sealed class AzureAIInferenceChatClient : IChatClient
/// The underlying .
private readonly ChatCompletionsClient _chatCompletionsClient;
- /// The use for any serialization activities related to tool call arguments and results.
- private JsonSerializerOptions _toolCallJsonSerializerOptions = AIJsonUtilities.DefaultOptions;
-
/// Gets a ChatRole.Developer value.
private static ChatRole ChatRoleDeveloper { get; } = new("developer");
/// Initializes a new instance of the class for the specified .
/// The underlying client.
- /// The ID of the model to use. If null, it can be provided per request via .
+ /// The ID of the model to use. If , it can be provided per request via .
/// is .
- /// is empty or composed entirely of whitespace.
- public AzureAIInferenceChatClient(ChatCompletionsClient chatCompletionsClient, string? modelId = null)
+ /// is empty or composed entirely of whitespace.
+ public AzureAIInferenceChatClient(ChatCompletionsClient chatCompletionsClient, string? defaultModelId = null)
{
_ = Throw.IfNull(chatCompletionsClient);
- if (modelId is not null)
+ if (defaultModelId is not null)
{
- _ = Throw.IfNullOrWhitespace(modelId);
+ _ = Throw.IfNullOrWhitespace(defaultModelId);
}
_chatCompletionsClient = chatCompletionsClient;
@@ -59,14 +56,7 @@ public AzureAIInferenceChatClient(ChatCompletionsClient chatCompletionsClient, s
var providerUrl = typeof(ChatCompletionsClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
?.GetValue(chatCompletionsClient) as Uri;
- _metadata = new ChatClientMetadata("az.ai.inference", providerUrl, modelId);
- }
-
- /// Gets or sets to use for any serialization activities related to tool call arguments and results.
- public JsonSerializerOptions ToolCallJsonSerializerOptions
- {
- get => _toolCallJsonSerializerOptions;
- set => _toolCallJsonSerializerOptions = Throw.IfNull(value);
+ _metadata = new ChatClientMetadata("az.ai.inference", providerUrl, defaultModelId);
}
///
@@ -324,7 +314,7 @@ private ChatCompletionsOptions ToAzureAIOptions(IEnumerable chatCon
default:
if (prop.Value is not null)
{
- byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(object)));
+ byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
result.AdditionalProperties[prop.Key] = new BinaryData(data);
}
@@ -413,7 +403,7 @@ private static ChatCompletionsToolDefinition ToAzureAIChatTool(AIFunction aiFunc
}
/// Converts an Extensions chat message enumerable to an AzureAI chat message enumerable.
- private IEnumerable ToAzureAIInferenceChatMessages(IEnumerable inputs)
+ private static IEnumerable ToAzureAIInferenceChatMessages(IEnumerable inputs)
{
// Maps all of the M.E.AI types to the corresponding AzureAI types.
// Unrecognized or non-processable content is ignored.
@@ -439,7 +429,7 @@ private IEnumerable ToAzureAIInferenceChatMessages(IEnumerab
{
try
{
- result = JsonSerializer.Serialize(resultContent.Result, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(object)));
+ result = JsonSerializer.Serialize(resultContent.Result, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object)));
}
catch (NotSupportedException)
{
@@ -482,7 +472,7 @@ private IEnumerable ToAzureAIInferenceChatMessages(IEnumerab
callRequest.CallId,
new FunctionCall(
callRequest.Name,
- JsonSerializer.Serialize(callRequest.Arguments, ToolCallJsonSerializerOptions.GetTypeInfo(typeof(IDictionary))))));
+ JsonSerializer.Serialize(callRequest.Arguments, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary))))));
}
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs
index b3fadc7bf54..9721befca0b 100644
--- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs
@@ -22,7 +22,7 @@
namespace Microsoft.Extensions.AI;
/// Represents an for an Azure.AI.Inference .
-public sealed class AzureAIInferenceEmbeddingGenerator :
+internal sealed class AzureAIInferenceEmbeddingGenerator :
IEmbeddingGenerator>
{
/// Metadata about the embedding generator.
@@ -36,31 +36,31 @@ public sealed class AzureAIInferenceEmbeddingGenerator :
/// Initializes a new instance of the class.
/// The underlying client.
- ///
+ ///
/// The ID of the model to use. This can also be overridden per request via .
/// Either this parameter or must provide a valid model ID.
///
- /// The number of dimensions to generate in each embedding.
+ /// The number of dimensions to generate in each embedding.
/// is .
- /// is empty or composed entirely of whitespace.
- /// is not positive.
+ /// is empty or composed entirely of whitespace.
+ /// is not positive.
public AzureAIInferenceEmbeddingGenerator(
- EmbeddingsClient embeddingsClient, string? modelId = null, int? dimensions = null)
+ EmbeddingsClient embeddingsClient, string? defaultModelId = null, int? defaultModelDimensions = null)
{
_ = Throw.IfNull(embeddingsClient);
- if (modelId is not null)
+ if (defaultModelId is not null)
{
- _ = Throw.IfNullOrWhitespace(modelId);
+ _ = Throw.IfNullOrWhitespace(defaultModelId);
}
- if (dimensions is < 1)
+ if (defaultModelDimensions is < 1)
{
- Throw.ArgumentOutOfRangeException(nameof(dimensions), "Value must be greater than 0.");
+ Throw.ArgumentOutOfRangeException(nameof(defaultModelDimensions), "Value must be greater than 0.");
}
_embeddingsClient = embeddingsClient;
- _dimensions = dimensions;
+ _dimensions = defaultModelDimensions;
// https://github.com/Azure/azure-sdk-for-net/issues/46278
// The endpoint isn't currently exposed, so use reflection to get at it, temporarily. Once packages
@@ -69,7 +69,7 @@ public AzureAIInferenceEmbeddingGenerator(
var providerUrl = typeof(EmbeddingsClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
?.GetValue(embeddingsClient) as Uri;
- _metadata = new EmbeddingGeneratorMetadata("az.ai.inference", providerUrl, modelId, dimensions);
+ _metadata = new EmbeddingGeneratorMetadata("az.ai.inference", providerUrl, defaultModelId, defaultModelDimensions);
}
///
diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceExtensions.cs
index 117a416b30a..58739b00b0d 100644
--- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceExtensions.cs
@@ -10,18 +10,18 @@ public static class AzureAIInferenceExtensions
{
/// Gets an for use with this .
/// The client.
- /// The ID of the model to use. If null, it can be provided per request via .
+ /// The ID of the model to use. If , it can be provided per request via .
/// An that can be used to converse via the .
- public static IChatClient AsChatClient(
+ public static IChatClient AsIChatClient(
this ChatCompletionsClient chatCompletionsClient, string? modelId = null) =>
new AzureAIInferenceChatClient(chatCompletionsClient, modelId);
/// Gets an for use with this .
/// The client.
- /// The ID of the model to use. If null, it can be provided per request via .
- /// The number of dimensions to generate in each embedding.
+ /// The ID of the model to use. If , it can be provided per request via .
+ /// The number of dimensions generated in each embedding.
/// An that can be used to generate embeddings via the .
- public static IEmbeddingGenerator> AsEmbeddingGenerator(
- this EmbeddingsClient embeddingsClient, string? modelId = null, int? dimensions = null) =>
- new AzureAIInferenceEmbeddingGenerator(embeddingsClient, modelId, dimensions);
+ public static IEmbeddingGenerator> AsIEmbeddingGenerator(
+ this EmbeddingsClient embeddingsClient, string? defaultModelId = null, int? defaultModelDimensions = null) =>
+ new AzureAIInferenceEmbeddingGenerator(embeddingsClient, defaultModelId, defaultModelDimensions);
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/README.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/README.md
index d8cd5a424ea..2c2ad628c53 100644
--- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/README.md
+++ b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/README.md
@@ -30,7 +30,7 @@ IChatClient client =
new Azure.AI.Inference.ChatCompletionsClient(
new("https://models.inference.ai.azure.com"),
new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!))
- .AsChatClient("gpt-4o-mini");
+ .AsIChatClient("gpt-4o-mini");
Console.WriteLine(await client.GetResponseAsync("What is AI?"));
```
@@ -52,7 +52,7 @@ IChatClient client =
new Azure.AI.Inference.ChatCompletionsClient(
new("https://models.inference.ai.azure.com"),
new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!))
- .AsChatClient("gpt-4o-mini");
+ .AsIChatClient("gpt-4o-mini");
Console.WriteLine(await client.GetResponseAsync(
[
@@ -71,7 +71,7 @@ IChatClient client =
new Azure.AI.Inference.ChatCompletionsClient(
new("https://models.inference.ai.azure.com"),
new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!))
- .AsChatClient("gpt-4o-mini");
+ .AsIChatClient("gpt-4o-mini");
await foreach (var update in client.GetStreamingResponseAsync("What is AI?"))
{
@@ -90,7 +90,7 @@ IChatClient azureClient =
new Azure.AI.Inference.ChatCompletionsClient(
new("https://models.inference.ai.azure.com"),
new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!))
- .AsChatClient("gpt-4o-mini");
+ .AsIChatClient("gpt-4o-mini");
IChatClient client = new ChatClientBuilder(azureClient)
.UseFunctionInvocation()
@@ -125,7 +125,7 @@ IChatClient azureClient =
new Azure.AI.Inference.ChatCompletionsClient(
new("https://models.inference.ai.azure.com"),
new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!))
- .AsChatClient("gpt-4o-mini");
+ .AsIChatClient("gpt-4o-mini");
IChatClient client = new ChatClientBuilder(azureClient)
.UseDistributedCache(cache)
@@ -161,7 +161,7 @@ IChatClient azureClient =
new Azure.AI.Inference.ChatCompletionsClient(
new("https://models.inference.ai.azure.com"),
new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!))
- .AsChatClient("gpt-4o-mini");
+ .AsIChatClient("gpt-4o-mini");
IChatClient client = new ChatClientBuilder(azureClient)
.UseOpenTelemetry(sourceName: sourceName, configure: c => c.EnableSensitiveData = true)
@@ -201,7 +201,7 @@ IChatClient azureClient =
new Azure.AI.Inference.ChatCompletionsClient(
new("https://models.inference.ai.azure.com"),
new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!))
- .AsChatClient("gpt-4o-mini");
+ .AsIChatClient("gpt-4o-mini");
IChatClient client = new ChatClientBuilder(azureClient)
.UseDistributedCache(cache)
@@ -243,7 +243,7 @@ builder.Services.AddSingleton(
builder.Services.AddDistributedMemoryCache();
builder.Services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Trace));
-builder.Services.AddChatClient(services => services.GetRequiredService().AsChatClient("gpt-4o-mini"))
+builder.Services.AddChatClient(services => services.GetRequiredService().AsIChatClient("gpt-4o-mini"))
.UseDistributedCache()
.UseLogging();
@@ -268,7 +268,7 @@ builder.Services.AddSingleton(new ChatCompletionsClient(
new AzureKeyCredential(builder.Configuration["GH_TOKEN"]!)));
builder.Services.AddChatClient(services =>
- services.GetRequiredService().AsChatClient("gpt-4o-mini"));
+ services.GetRequiredService().AsIChatClient("gpt-4o-mini"));
var app = builder.Build();
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/JsonModelHelpers.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/JsonModelHelpers.cs
deleted file mode 100644
index 53188b333ee..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/JsonModelHelpers.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.ClientModel.Primitives;
-using System.Text.Json;
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Defines a set of helper methods for working with types.
-///
-internal static class JsonModelHelpers
-{
- public static BinaryData Serialize(TModel value)
- where TModel : IJsonModel
- {
- return value.Write(ModelReaderWriterOptions.Json);
- }
-
- public static TModel Deserialize(BinaryData data)
- where TModel : IJsonModel, new()
- {
- return JsonModelDeserializationWitness.Value.Create(data, ModelReaderWriterOptions.Json);
- }
-
- public static TModel Deserialize(ref Utf8JsonReader reader)
- where TModel : IJsonModel, new()
- {
- return JsonModelDeserializationWitness.Value.Create(ref reader, ModelReaderWriterOptions.Json);
- }
-
- private sealed class JsonModelDeserializationWitness
- where TModel : IJsonModel, new()
- {
- public static readonly IJsonModel Value = new TModel();
- }
-}
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantClient.cs
deleted file mode 100644
index d838a98ccbb..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantClient.cs
+++ /dev/null
@@ -1,369 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Text;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Shared.Diagnostics;
-using OpenAI;
-using OpenAI.Assistants;
-using OpenAI.Chat;
-
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
-#pragma warning disable CA1031 // Do not catch general exception types
-#pragma warning disable S1067 // Expressions should not be too complex
-#pragma warning disable S1751 // Loops with at most one iteration should be refactored
-#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
-#pragma warning disable SA1204 // Static elements should appear before instance elements
-#pragma warning disable SA1108 // Block statements should not contain embedded comments
-
-namespace Microsoft.Extensions.AI;
-
-/// Represents an for an OpenAI or .
-internal sealed class OpenAIAssistantClient : IChatClient
-{
- /// Metadata for the client.
- private readonly ChatClientMetadata _metadata;
-
- /// The underlying .
- private readonly AssistantClient _assistantClient;
-
- /// The ID of the assistant to use.
- private readonly string _assistantId;
-
- /// The thread ID to use if none is supplied in .
- private readonly string? _threadId;
-
- /// Initializes a new instance of the class for the specified .
- /// The underlying client.
- /// The ID of the assistant to use.
- ///
- /// The ID of the thread to use. If not supplied here, it should be supplied per request in .
- /// If none is supplied, a new thread will be created for a request.
- ///
- public OpenAIAssistantClient(AssistantClient assistantClient, string assistantId, string? threadId)
- {
- _assistantClient = Throw.IfNull(assistantClient);
- _assistantId = Throw.IfNull(assistantId);
- _threadId = threadId;
-
- _metadata = new("openai");
- }
-
- ///
- public object? GetService(Type serviceType, object? serviceKey = null)
- {
- _ = Throw.IfNull(serviceType);
-
- return
- serviceKey is not null ? null :
- serviceType == typeof(ChatClientMetadata) ? _metadata :
- serviceType == typeof(AssistantClient) ? _assistantClient :
- serviceType.IsInstanceOfType(this) ? this :
- null;
- }
-
- ///
- public Task GetResponseAsync(
- IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
- GetStreamingResponseAsync(messages, options, cancellationToken).ToChatResponseAsync(cancellationToken);
-
- ///
- public async IAsyncEnumerable GetStreamingResponseAsync(
- IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
- {
- // Extract necessary state from messages and options.
- (RunCreationOptions runOptions, List? toolResults) = CreateRunOptions(messages, options);
-
- // Get the thread ID.
- string? threadId = options?.ChatThreadId ?? _threadId;
- if (threadId is null && toolResults is not null)
- {
- Throw.ArgumentException(nameof(messages), "No thread ID was provided, but chat messages includes tool results.");
- }
-
- // Get the updates to process from the assistant. If we have any tool results, this means submitting those and ignoring
- // our runOptions. Otherwise, create a run, and a thread if we don't have one.
- IAsyncEnumerable updates;
- if (GetRunId(toolResults, out List? toolOutputs) is string existingRunId)
- {
- updates = _assistantClient.SubmitToolOutputsToRunStreamingAsync(threadId, existingRunId, toolOutputs, cancellationToken);
- }
- else if (threadId is null)
- {
- ThreadCreationOptions creationOptions = new();
- foreach (var message in runOptions.AdditionalMessages)
- {
- creationOptions.InitialMessages.Add(message);
- }
-
- runOptions.AdditionalMessages.Clear();
-
- updates = _assistantClient.CreateThreadAndRunStreamingAsync(_assistantId, creationOptions, runOptions, cancellationToken: cancellationToken);
- }
- else
- {
- updates = _assistantClient.CreateRunStreamingAsync(threadId, _assistantId, runOptions, cancellationToken);
- }
-
- // Process each update.
- string? responseId = null;
- await foreach (var update in updates.ConfigureAwait(false))
- {
- switch (update)
- {
- case MessageContentUpdate mcu:
- yield return new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text)
- {
- ChatThreadId = threadId,
- RawRepresentation = mcu,
- ResponseId = responseId,
- MessageId = responseId, // There's no per-message ID, but there's only one mesage per run, so use the run ID
- };
- break;
-
- case ThreadUpdate tu when options is not null:
- threadId ??= tu.Value.Id;
- break;
-
- case RunUpdate ru:
- threadId ??= ru.Value.ThreadId;
- responseId ??= ru.Value.Id;
-
- ChatResponseUpdate ruUpdate = new()
- {
- AuthorName = ru.Value.AssistantId,
- ChatThreadId = threadId,
- CreatedAt = ru.Value.CreatedAt,
- ModelId = ru.Value.Model,
- RawRepresentation = ru,
- ResponseId = responseId,
- MessageId = responseId, // There's no per-message ID, but there's only one mesage per run, so use the run ID
- Role = ChatRole.Assistant,
- };
-
- if (ru.Value.Usage is { } usage)
- {
- ruUpdate.Contents.Add(new UsageContent(new()
- {
- InputTokenCount = usage.InputTokenCount,
- OutputTokenCount = usage.OutputTokenCount,
- TotalTokenCount = usage.TotalTokenCount,
- }));
- }
-
- if (ru is RequiredActionUpdate rau && rau.ToolCallId is string toolCallId && rau.FunctionName is string functionName)
- {
- ruUpdate.Contents.Add(
- new FunctionCallContent(
- JsonSerializer.Serialize(new[] { ru.Value.Id, toolCallId }, OpenAIJsonContext.Default.StringArray!),
- functionName,
- JsonSerializer.Deserialize(rau.FunctionArguments, OpenAIJsonContext.Default.IDictionaryStringObject)!));
- }
-
- yield return ruUpdate;
- break;
- }
- }
- }
-
- ///
- void IDisposable.Dispose()
- {
- // Nothing to dispose. Implementation required for the IChatClient interface.
- }
-
- /// Adds the provided messages to the thread and returns the options to use for the request.
- private static (RunCreationOptions RunOptions, List? ToolResults) CreateRunOptions(
- IEnumerable messages, ChatOptions? options)
- {
- _ = Throw.IfNull(messages);
-
- RunCreationOptions runOptions = new();
-
- // Handle ChatOptions.
- if (options is not null)
- {
- // Propagate the simple properties that have a 1:1 correspondence.
- runOptions.MaxOutputTokenCount = options.MaxOutputTokens;
- runOptions.ModelOverride = options.ModelId;
- runOptions.NucleusSamplingFactor = options.TopP;
- runOptions.Temperature = options.Temperature;
-
- // Propagate additional properties from AdditionalProperties.
- if (options.AdditionalProperties?.TryGetValue(nameof(RunCreationOptions.AllowParallelToolCalls), out bool allowParallelToolCalls) is true)
- {
- runOptions.AllowParallelToolCalls = allowParallelToolCalls;
- }
-
- if (options.AdditionalProperties?.TryGetValue(nameof(RunCreationOptions.MaxInputTokenCount), out int maxInputTokenCount) is true)
- {
- runOptions.MaxInputTokenCount = maxInputTokenCount;
- }
-
- if (options.AdditionalProperties?.TryGetValue(nameof(RunCreationOptions.TruncationStrategy), out RunTruncationStrategy? truncationStrategy) is true)
- {
- runOptions.TruncationStrategy = truncationStrategy;
- }
-
- // Store all the tools to use.
- if (options.Tools is { Count: > 0 } tools)
- {
- foreach (AITool tool in tools)
- {
- switch (tool)
- {
- case AIFunction aiFunction:
- // Default strict to true, but allow to be overridden by an additional Strict property.
- bool strict =
- !aiFunction.AdditionalProperties.TryGetValue("Strict", out object? strictObj) ||
- strictObj is not bool strictValue ||
- strictValue;
-
- var functionParameters = BinaryData.FromBytes(
- JsonSerializer.SerializeToUtf8Bytes(
- JsonSerializer.Deserialize(aiFunction.JsonSchema, OpenAIJsonContext.Default.OpenAIChatToolJson)!,
- OpenAIJsonContext.Default.OpenAIChatToolJson));
-
- runOptions.ToolsOverride.Add(ToolDefinition.CreateFunction(aiFunction.Name, aiFunction.Description, functionParameters, strict));
- break;
-
- case HostedCodeInterpreterTool:
- runOptions.ToolsOverride.Add(ToolDefinition.CreateCodeInterpreter());
- break;
- }
- }
- }
-
- // Store the tool mode.
- switch (options.ToolMode)
- {
- case NoneChatToolMode:
- runOptions.ToolConstraint = ToolConstraint.None;
- break;
-
- case null:
- case AutoChatToolMode:
- runOptions.ToolConstraint = ToolConstraint.Auto;
- break;
-
- case RequiredChatToolMode required:
- runOptions.ToolConstraint = required.RequiredFunctionName is null ?
- new ToolConstraint(ToolDefinition.CreateFunction(required.RequiredFunctionName)) :
- ToolConstraint.Required;
- break;
- }
-
- // Store the response format.
- if (options.ResponseFormat is ChatResponseFormatText)
- {
- runOptions.ResponseFormat = AssistantResponseFormat.Text;
- }
- else if (options.ResponseFormat is ChatResponseFormatJson jsonFormat)
- {
- runOptions.ResponseFormat = jsonFormat.Schema is { } jsonSchema ?
- AssistantResponseFormat.CreateJsonSchemaFormat(
- jsonFormat.SchemaName ?? "json_schema",
- BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
- jsonFormat.SchemaDescription,
- strictSchemaEnabled: true) :
- AssistantResponseFormat.JsonObject;
- }
- }
-
- // Handle ChatMessages. System messages are turned into additional instructions.
- StringBuilder? instructions = null;
- List? functionResults = null;
- foreach (var chatMessage in messages)
- {
- List messageContents = [];
-
- if (chatMessage.Role == ChatRole.System ||
- chatMessage.Role == OpenAIModelMappers.ChatRoleDeveloper)
- {
- instructions ??= new();
- foreach (var textContent in chatMessage.Contents.OfType())
- {
- _ = instructions.Append(textContent);
- }
-
- continue;
- }
-
- foreach (AIContent content in chatMessage.Contents)
- {
- switch (content)
- {
- case TextContent tc:
- messageContents.Add(MessageContent.FromText(tc.Text));
- break;
-
- case DataContent dc when dc.HasTopLevelMediaType("image"):
- messageContents.Add(MessageContent.FromImageUri(new(dc.Uri)));
- break;
-
- case FunctionResultContent frc:
- (functionResults ??= []).Add(frc);
- break;
- }
- }
-
- if (messageContents.Count > 0)
- {
- runOptions.AdditionalMessages.Add(new(
- chatMessage.Role == ChatRole.Assistant ? MessageRole.Assistant : MessageRole.User,
- messageContents));
- }
- }
-
- if (instructions is not null)
- {
- runOptions.AdditionalInstructions = instructions.ToString();
- }
-
- return (runOptions, functionResults);
- }
-
- private static string? GetRunId(List? toolResults, out List? toolOutputs)
- {
- string? runId = null;
- toolOutputs = null;
- if (toolResults?.Count > 0)
- {
- foreach (var frc in toolResults)
- {
- // When creating the FunctionCallContext, we created it with a CallId == [runId, callId].
- // We need to extract the run ID and ensure that the ToolOutput we send back to OpenAI
- // is only the call ID.
- string[]? runAndCallIDs;
- try
- {
- runAndCallIDs = JsonSerializer.Deserialize(frc.CallId, OpenAIJsonContext.Default.StringArray);
- }
- catch
- {
- continue;
- }
-
- if (runAndCallIDs is null ||
- runAndCallIDs.Length != 2 ||
- string.IsNullOrWhiteSpace(runAndCallIDs[0]) || // run ID
- string.IsNullOrWhiteSpace(runAndCallIDs[1]) || // call ID
- (runId is not null && runId != runAndCallIDs[0]))
- {
- continue;
- }
-
- runId = runAndCallIDs[0];
- (toolOutputs ??= []).Add(new(runAndCallIDs[1], frc.Result?.ToString() ?? string.Empty));
- }
- }
-
- return runId;
- }
-}
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
index c85b202f7e8..43a2e21c9e0 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatClient.cs
@@ -4,61 +4,35 @@
using System;
using System.Collections.Generic;
using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Text;
using System.Text.Json;
+using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Shared.Diagnostics;
using OpenAI;
using OpenAI.Chat;
+#pragma warning disable CA1308 // Normalize strings to uppercase
+#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?)
#pragma warning disable S1067 // Expressions should not be too complex
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
-#pragma warning disable SA1204 // Static elements should appear before instance elements
-#pragma warning disable SA1108 // Block statements should not contain embedded comments
namespace Microsoft.Extensions.AI;
/// Represents an for an OpenAI or .
-public sealed class OpenAIChatClient : IChatClient
+internal sealed partial class OpenAIChatClient : IChatClient
{
/// Gets the default OpenAI endpoint.
- internal static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1");
+ private static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1");
/// Metadata about the client.
private readonly ChatClientMetadata _metadata;
- /// The underlying .
- private readonly OpenAIClient? _openAIClient;
-
/// The underlying .
private readonly ChatClient _chatClient;
- /// The use for any serialization activities related to tool call arguments and results.
- private JsonSerializerOptions _toolCallJsonSerializerOptions = AIJsonUtilities.DefaultOptions;
-
- /// Initializes a new instance of the class for the specified .
- /// The underlying client.
- /// The model to use.
- /// is .
- /// is empty or composed entirely of whitespace.
- public OpenAIChatClient(OpenAIClient openAIClient, string modelId)
- {
- _ = Throw.IfNull(openAIClient);
- _ = Throw.IfNullOrWhitespace(modelId);
-
- _openAIClient = openAIClient;
- _chatClient = openAIClient.GetChatClient(modelId);
-
- // https://github.com/openai/openai-dotnet/issues/215
- // The endpoint isn't currently exposed, so use reflection to get at it, temporarily. Once packages
- // implement the abstractions directly rather than providing adapters on top of the public APIs,
- // the package can provide such implementations separate from what's exposed in the public API.
- Uri providerUrl = typeof(OpenAIClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
- ?.GetValue(openAIClient) as Uri ?? DefaultOpenAIEndpoint;
-
- _metadata = new ChatClientMetadata("openai", providerUrl, modelId);
- }
-
/// Initializes a new instance of the class for the specified .
/// The underlying client.
/// is .
@@ -80,13 +54,6 @@ public OpenAIChatClient(ChatClient chatClient)
_metadata = new("openai", providerUrl, model);
}
- /// Gets or sets to use for any serialization activities related to tool call arguments and results.
- public JsonSerializerOptions ToolCallJsonSerializerOptions
- {
- get => _toolCallJsonSerializerOptions;
- set => _toolCallJsonSerializerOptions = Throw.IfNull(value);
- }
-
///
object? IChatClient.GetService(Type serviceType, object? serviceKey)
{
@@ -95,7 +62,6 @@ public JsonSerializerOptions ToolCallJsonSerializerOptions
return
serviceKey is not null ? null :
serviceType == typeof(ChatClientMetadata) ? _metadata :
- serviceType == typeof(OpenAIClient) ? _openAIClient :
serviceType == typeof(ChatClient) ? _chatClient :
serviceType.IsInstanceOfType(this) ? this :
null;
@@ -107,13 +73,13 @@ public async Task GetResponseAsync(
{
_ = Throw.IfNull(messages);
- var openAIChatMessages = OpenAIModelMappers.ToOpenAIChatMessages(messages, ToolCallJsonSerializerOptions);
- var openAIOptions = OpenAIModelMappers.ToOpenAIOptions(options);
+ var openAIChatMessages = ToOpenAIChatMessages(messages, AIJsonUtilities.DefaultOptions);
+ var openAIOptions = ToOpenAIOptions(options);
// Make the call to OpenAI.
var response = await _chatClient.CompleteChatAsync(openAIChatMessages, openAIOptions, cancellationToken).ConfigureAwait(false);
- return OpenAIModelMappers.FromOpenAIChatCompletion(response.Value, options, openAIOptions);
+ return FromOpenAIChatCompletion(response.Value, options, openAIOptions);
}
///
@@ -122,13 +88,13 @@ public IAsyncEnumerable GetStreamingResponseAsync(
{
_ = Throw.IfNull(messages);
- var openAIChatMessages = OpenAIModelMappers.ToOpenAIChatMessages(messages, ToolCallJsonSerializerOptions);
- var openAIOptions = OpenAIModelMappers.ToOpenAIOptions(options);
+ var openAIChatMessages = ToOpenAIChatMessages(messages, AIJsonUtilities.DefaultOptions);
+ var openAIOptions = ToOpenAIOptions(options);
// Make the call to OpenAI.
var chatCompletionUpdates = _chatClient.CompleteChatStreamingAsync(openAIChatMessages, openAIOptions, cancellationToken);
- return OpenAIModelMappers.FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, cancellationToken);
+ return FromOpenAIStreamingChatCompletionAsync(chatCompletionUpdates, cancellationToken);
}
///
@@ -136,4 +102,656 @@ void IDisposable.Dispose()
{
// Nothing to dispose. Implementation required for the IChatClient interface.
}
+
+ private static ChatRole ChatRoleDeveloper { get; } = new ChatRole("developer");
+
+ /// Converts an Extensions chat message enumerable to an OpenAI chat message enumerable.
+ private static IEnumerable ToOpenAIChatMessages(IEnumerable inputs, JsonSerializerOptions options)
+ {
+ // Maps all of the M.E.AI types to the corresponding OpenAI types.
+ // Unrecognized or non-processable content is ignored.
+
+ foreach (ChatMessage input in inputs)
+ {
+ if (input.Role == ChatRole.System ||
+ input.Role == ChatRole.User ||
+ input.Role == ChatRoleDeveloper)
+ {
+ var parts = ToOpenAIChatContent(input.Contents);
+ yield return
+ input.Role == ChatRole.System ? new SystemChatMessage(parts) { ParticipantName = input.AuthorName } :
+ input.Role == ChatRoleDeveloper ? new DeveloperChatMessage(parts) { ParticipantName = input.AuthorName } :
+ new UserChatMessage(parts) { ParticipantName = input.AuthorName };
+ }
+ else if (input.Role == ChatRole.Tool)
+ {
+ foreach (AIContent item in input.Contents)
+ {
+ if (item is FunctionResultContent resultContent)
+ {
+ string? result = resultContent.Result as string;
+ if (result is null && resultContent.Result is not null)
+ {
+ try
+ {
+ result = JsonSerializer.Serialize(resultContent.Result, options.GetTypeInfo(typeof(object)));
+ }
+ catch (NotSupportedException)
+ {
+ // If the type can't be serialized, skip it.
+ }
+ }
+
+ yield return new ToolChatMessage(resultContent.CallId, result ?? string.Empty);
+ }
+ }
+ }
+ else if (input.Role == ChatRole.Assistant)
+ {
+ AssistantChatMessage message = new(ToOpenAIChatContent(input.Contents))
+ {
+ ParticipantName = input.AuthorName
+ };
+
+ foreach (var content in input.Contents)
+ {
+ if (content is FunctionCallContent callRequest)
+ {
+ message.ToolCalls.Add(
+ ChatToolCall.CreateFunctionToolCall(
+ callRequest.CallId,
+ callRequest.Name,
+ new(JsonSerializer.SerializeToUtf8Bytes(
+ callRequest.Arguments,
+ options.GetTypeInfo(typeof(IDictionary))))));
+ }
+ }
+
+ if (input.AdditionalProperties?.TryGetValue(nameof(message.Refusal), out string? refusal) is true)
+ {
+ message.Refusal = refusal;
+ }
+
+ yield return message;
+ }
+ }
+ }
+
+ /// Converts a list of to a list of .
+ private static List ToOpenAIChatContent(IList contents)
+ {
+ List parts = [];
+ foreach (var content in contents)
+ {
+ switch (content)
+ {
+ case TextContent textContent:
+ parts.Add(ChatMessageContentPart.CreateTextPart(textContent.Text));
+ break;
+
+ case UriContent uriContent when uriContent.HasTopLevelMediaType("image"):
+ parts.Add(ChatMessageContentPart.CreateImagePart(uriContent.Uri));
+ break;
+
+ case DataContent dataContent when dataContent.HasTopLevelMediaType("image"):
+ parts.Add(ChatMessageContentPart.CreateImagePart(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType));
+ break;
+
+ case DataContent dataContent when dataContent.HasTopLevelMediaType("audio"):
+ var audioData = BinaryData.FromBytes(dataContent.Data);
+ if (dataContent.MediaType.Equals("audio/mpeg", StringComparison.OrdinalIgnoreCase))
+ {
+ parts.Add(ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Mp3));
+ }
+ else if (dataContent.MediaType.Equals("audio/wav", StringComparison.OrdinalIgnoreCase))
+ {
+ parts.Add(ChatMessageContentPart.CreateInputAudioPart(audioData, ChatInputAudioFormat.Wav));
+ }
+
+ break;
+ }
+ }
+
+ if (parts.Count == 0)
+ {
+ parts.Add(ChatMessageContentPart.CreateTextPart(string.Empty));
+ }
+
+ return parts;
+ }
+
+ private static async IAsyncEnumerable FromOpenAIStreamingChatCompletionAsync(
+ IAsyncEnumerable updates,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ Dictionary? functionCallInfos = null;
+ ChatRole? streamedRole = null;
+ ChatFinishReason? finishReason = null;
+ StringBuilder? refusal = null;
+ string? responseId = null;
+ DateTimeOffset? createdAt = null;
+ string? modelId = null;
+ string? fingerprint = null;
+
+ // Process each update as it arrives
+ await foreach (StreamingChatCompletionUpdate update in updates.WithCancellation(cancellationToken).ConfigureAwait(false))
+ {
+ // The role and finish reason may arrive during any update, but once they've arrived, the same value should be the same for all subsequent updates.
+ streamedRole ??= update.Role is ChatMessageRole role ? FromOpenAIChatRole(role) : null;
+ finishReason ??= update.FinishReason is OpenAI.Chat.ChatFinishReason reason ? FromOpenAIFinishReason(reason) : null;
+ responseId ??= update.CompletionId;
+ createdAt ??= update.CreatedAt;
+ modelId ??= update.Model;
+ fingerprint ??= update.SystemFingerprint;
+
+ // Create the response content object.
+ ChatResponseUpdate responseUpdate = new()
+ {
+ ResponseId = update.CompletionId,
+ MessageId = update.CompletionId, // There is no per-message ID, but there's only one message per response, so use the response ID
+ CreatedAt = update.CreatedAt,
+ FinishReason = finishReason,
+ ModelId = modelId,
+ RawRepresentation = update,
+ Role = streamedRole,
+ };
+
+ // Populate it with any additional metadata from the OpenAI object.
+ if (update.ContentTokenLogProbabilities is { Count: > 0 } contentTokenLogProbs)
+ {
+ (responseUpdate.AdditionalProperties ??= [])[nameof(update.ContentTokenLogProbabilities)] = contentTokenLogProbs;
+ }
+
+ if (update.RefusalTokenLogProbabilities is { Count: > 0 } refusalTokenLogProbs)
+ {
+ (responseUpdate.AdditionalProperties ??= [])[nameof(update.RefusalTokenLogProbabilities)] = refusalTokenLogProbs;
+ }
+
+ if (fingerprint is not null)
+ {
+ (responseUpdate.AdditionalProperties ??= [])[nameof(update.SystemFingerprint)] = fingerprint;
+ }
+
+ // Transfer over content update items.
+ if (update.ContentUpdate is { Count: > 0 })
+ {
+ foreach (ChatMessageContentPart contentPart in update.ContentUpdate)
+ {
+ if (ToAIContent(contentPart) is AIContent aiContent)
+ {
+ responseUpdate.Contents.Add(aiContent);
+ }
+ }
+ }
+
+ // Transfer over refusal updates.
+ if (update.RefusalUpdate is not null)
+ {
+ _ = (refusal ??= new()).Append(update.RefusalUpdate);
+ }
+
+ // Transfer over tool call updates.
+ if (update.ToolCallUpdates is { Count: > 0 } toolCallUpdates)
+ {
+ foreach (StreamingChatToolCallUpdate toolCallUpdate in toolCallUpdates)
+ {
+ functionCallInfos ??= [];
+ if (!functionCallInfos.TryGetValue(toolCallUpdate.Index, out FunctionCallInfo? existing))
+ {
+ functionCallInfos[toolCallUpdate.Index] = existing = new();
+ }
+
+ existing.CallId ??= toolCallUpdate.ToolCallId;
+ existing.Name ??= toolCallUpdate.FunctionName;
+ if (toolCallUpdate.FunctionArgumentsUpdate is { } argUpdate && !argUpdate.ToMemory().IsEmpty)
+ {
+ _ = (existing.Arguments ??= new()).Append(argUpdate.ToString());
+ }
+ }
+ }
+
+ // Transfer over usage updates.
+ if (update.Usage is ChatTokenUsage tokenUsage)
+ {
+ var usageDetails = FromOpenAIUsage(tokenUsage);
+ responseUpdate.Contents.Add(new UsageContent(usageDetails));
+ }
+
+ // Now yield the item.
+ yield return responseUpdate;
+ }
+
+ // Now that we've received all updates, combine any for function calls into a single item to yield.
+ if (functionCallInfos is not null)
+ {
+ ChatResponseUpdate responseUpdate = new()
+ {
+ ResponseId = responseId,
+ MessageId = responseId, // There is no per-message ID, but there's only one message per response, so use the response ID
+ CreatedAt = createdAt,
+ FinishReason = finishReason,
+ ModelId = modelId,
+ Role = streamedRole,
+ };
+
+ foreach (var entry in functionCallInfos)
+ {
+ FunctionCallInfo fci = entry.Value;
+ if (!string.IsNullOrWhiteSpace(fci.Name))
+ {
+ var callContent = ParseCallContentFromJsonString(
+ fci.Arguments?.ToString() ?? string.Empty,
+ fci.CallId!,
+ fci.Name!);
+ responseUpdate.Contents.Add(callContent);
+ }
+ }
+
+ // Refusals are about the model not following the schema for tool calls. As such, if we have any refusal,
+ // add it to this function calling item.
+ if (refusal is not null)
+ {
+ (responseUpdate.AdditionalProperties ??= [])[nameof(ChatMessageContentPart.Refusal)] = refusal.ToString();
+ }
+
+ // Propagate additional relevant metadata.
+ if (fingerprint is not null)
+ {
+ (responseUpdate.AdditionalProperties ??= [])[nameof(ChatCompletion.SystemFingerprint)] = fingerprint;
+ }
+
+ yield return responseUpdate;
+ }
+ }
+
+ private static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompletion, ChatOptions? options, ChatCompletionOptions chatCompletionOptions)
+ {
+ _ = Throw.IfNull(openAICompletion);
+
+ // Create the return message.
+ ChatMessage returnMessage = new()
+ {
+ MessageId = openAICompletion.Id, // There's no per-message ID, so we use the same value as the response ID
+ RawRepresentation = openAICompletion,
+ Role = FromOpenAIChatRole(openAICompletion.Role),
+ };
+
+ // Populate its content from those in the OpenAI response content.
+ foreach (ChatMessageContentPart contentPart in openAICompletion.Content)
+ {
+ if (ToAIContent(contentPart) is AIContent aiContent)
+ {
+ returnMessage.Contents.Add(aiContent);
+ }
+ }
+
+ // Output audio is handled separately from message content parts.
+ if (openAICompletion.OutputAudio is ChatOutputAudio audio)
+ {
+ string mimeType = chatCompletionOptions?.AudioOptions?.OutputAudioFormat.ToString()?.ToLowerInvariant() switch
+ {
+ "opus" => "audio/opus",
+ "aac" => "audio/aac",
+ "flac" => "audio/flac",
+ "wav" => "audio/wav",
+ "pcm" => "audio/pcm",
+ "mp3" or _ => "audio/mpeg",
+ };
+
+ var dc = new DataContent(audio.AudioBytes.ToMemory(), mimeType)
+ {
+ AdditionalProperties = new() { [nameof(audio.ExpiresAt)] = audio.ExpiresAt },
+ };
+
+ if (audio.Id is string id)
+ {
+ dc.AdditionalProperties[nameof(audio.Id)] = id;
+ }
+
+ if (audio.Transcript is string transcript)
+ {
+ dc.AdditionalProperties[nameof(audio.Transcript)] = transcript;
+ }
+
+ returnMessage.Contents.Add(dc);
+ }
+
+ // Also manufacture function calling content items from any tool calls in the response.
+ if (options?.Tools is { Count: > 0 })
+ {
+ foreach (ChatToolCall toolCall in openAICompletion.ToolCalls)
+ {
+ if (!string.IsNullOrWhiteSpace(toolCall.FunctionName))
+ {
+ var callContent = ParseCallContentFromBinaryData(toolCall.FunctionArguments, toolCall.Id, toolCall.FunctionName);
+ callContent.RawRepresentation = toolCall;
+
+ returnMessage.Contents.Add(callContent);
+ }
+ }
+ }
+
+ // Wrap the content in a ChatResponse to return.
+ var response = new ChatResponse(returnMessage)
+ {
+ CreatedAt = openAICompletion.CreatedAt,
+ FinishReason = FromOpenAIFinishReason(openAICompletion.FinishReason),
+ ModelId = openAICompletion.Model,
+ RawRepresentation = openAICompletion,
+ ResponseId = openAICompletion.Id,
+ };
+
+ if (openAICompletion.Usage is ChatTokenUsage tokenUsage)
+ {
+ response.Usage = FromOpenAIUsage(tokenUsage);
+ }
+
+ if (openAICompletion.ContentTokenLogProbabilities is { Count: > 0 } contentTokenLogProbs)
+ {
+ (response.AdditionalProperties ??= [])[nameof(openAICompletion.ContentTokenLogProbabilities)] = contentTokenLogProbs;
+ }
+
+ if (openAICompletion.Refusal is string refusal)
+ {
+ (response.AdditionalProperties ??= [])[nameof(openAICompletion.Refusal)] = refusal;
+ }
+
+ if (openAICompletion.RefusalTokenLogProbabilities is { Count: > 0 } refusalTokenLogProbs)
+ {
+ (response.AdditionalProperties ??= [])[nameof(openAICompletion.RefusalTokenLogProbabilities)] = refusalTokenLogProbs;
+ }
+
+ if (openAICompletion.SystemFingerprint is string systemFingerprint)
+ {
+ (response.AdditionalProperties ??= [])[nameof(openAICompletion.SystemFingerprint)] = systemFingerprint;
+ }
+
+ return response;
+ }
+
+ /// Converts an extensions options instance to an OpenAI options instance.
+ private static ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
+ {
+ ChatCompletionOptions result = new();
+
+ if (options is not null)
+ {
+ result.FrequencyPenalty = options.FrequencyPenalty;
+ result.MaxOutputTokenCount = options.MaxOutputTokens;
+ result.TopP = options.TopP;
+ result.PresencePenalty = options.PresencePenalty;
+ result.Temperature = options.Temperature;
+#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
+ result.Seed = options.Seed;
+#pragma warning restore OPENAI001
+
+ if (options.StopSequences is { Count: > 0 } stopSequences)
+ {
+ foreach (string stopSequence in stopSequences)
+ {
+ result.StopSequences.Add(stopSequence);
+ }
+ }
+
+ if (options.AdditionalProperties is { Count: > 0 } additionalProperties)
+ {
+ if (additionalProperties.TryGetValue(nameof(result.AllowParallelToolCalls), out bool allowParallelToolCalls))
+ {
+ result.AllowParallelToolCalls = allowParallelToolCalls;
+ }
+
+ if (additionalProperties.TryGetValue(nameof(result.AudioOptions), out ChatAudioOptions? audioOptions))
+ {
+ result.AudioOptions = audioOptions;
+ }
+
+ if (additionalProperties.TryGetValue(nameof(result.EndUserId), out string? endUserId))
+ {
+ result.EndUserId = endUserId;
+ }
+
+ if (additionalProperties.TryGetValue(nameof(result.IncludeLogProbabilities), out bool includeLogProbabilities))
+ {
+ result.IncludeLogProbabilities = includeLogProbabilities;
+ }
+
+ if (additionalProperties.TryGetValue(nameof(result.LogitBiases), out IDictionary? logitBiases))
+ {
+ foreach (KeyValuePair kvp in logitBiases!)
+ {
+ result.LogitBiases[kvp.Key] = kvp.Value;
+ }
+ }
+
+ if (additionalProperties.TryGetValue(nameof(result.Metadata), out IDictionary? metadata))
+ {
+ foreach (KeyValuePair kvp in metadata)
+ {
+ result.Metadata[kvp.Key] = kvp.Value;
+ }
+ }
+
+ if (additionalProperties.TryGetValue(nameof(result.OutputPrediction), out ChatOutputPrediction? outputPrediction))
+ {
+ result.OutputPrediction = outputPrediction;
+ }
+
+ if (additionalProperties.TryGetValue(nameof(result.ReasoningEffortLevel), out ChatReasoningEffortLevel reasoningEffortLevel))
+ {
+ result.ReasoningEffortLevel = reasoningEffortLevel;
+ }
+
+ if (additionalProperties.TryGetValue(nameof(result.ResponseModalities), out ChatResponseModalities responseModalities))
+ {
+ result.ResponseModalities = responseModalities;
+ }
+
+ if (additionalProperties.TryGetValue(nameof(result.StoredOutputEnabled), out bool storeOutputEnabled))
+ {
+ result.StoredOutputEnabled = storeOutputEnabled;
+ }
+
+ if (additionalProperties.TryGetValue(nameof(result.TopLogProbabilityCount), out int topLogProbabilityCountInt))
+ {
+ result.TopLogProbabilityCount = topLogProbabilityCountInt;
+ }
+ }
+
+ if (options.Tools is { Count: > 0 } tools)
+ {
+ foreach (AITool tool in tools)
+ {
+ if (tool is AIFunction af)
+ {
+ result.Tools.Add(ToOpenAIChatTool(af));
+ }
+ }
+
+ switch (options.ToolMode)
+ {
+ case NoneChatToolMode:
+ result.ToolChoice = ChatToolChoice.CreateNoneChoice();
+ break;
+
+ case AutoChatToolMode:
+ case null:
+ result.ToolChoice = ChatToolChoice.CreateAutoChoice();
+ break;
+
+ case RequiredChatToolMode required:
+ result.ToolChoice = required.RequiredFunctionName is null ?
+ ChatToolChoice.CreateRequiredChoice() :
+ ChatToolChoice.CreateFunctionChoice(required.RequiredFunctionName);
+ break;
+ }
+ }
+
+ if (options.ResponseFormat is ChatResponseFormatText)
+ {
+ result.ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat();
+ }
+ else if (options.ResponseFormat is ChatResponseFormatJson jsonFormat)
+ {
+ result.ResponseFormat = jsonFormat.Schema is { } jsonSchema ?
+ OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat(
+ jsonFormat.SchemaName ?? "json_schema",
+ BinaryData.FromBytes(
+ JsonSerializer.SerializeToUtf8Bytes(jsonSchema, ChatClientJsonContext.Default.JsonElement)),
+ jsonFormat.SchemaDescription,
+ jsonSchemaIsStrict: true) :
+ OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat();
+ }
+ }
+
+ return result;
+ }
+
+ /// Converts an Extensions function to an OpenAI chat tool.
+ private static ChatTool ToOpenAIChatTool(AIFunction aiFunction)
+ {
+ // Default strict to true, but allow to be overridden by an additional Strict property.
+ bool strict =
+ !aiFunction.AdditionalProperties.TryGetValue("Strict", out object? strictObj) ||
+ strictObj is not bool strictValue ||
+ strictValue;
+
+ // Map to an intermediate model so that redundant properties are skipped.
+ var tool = JsonSerializer.Deserialize(aiFunction.JsonSchema, ChatClientJsonContext.Default.ChatToolJson)!;
+ var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, ChatClientJsonContext.Default.ChatToolJson));
+ return ChatTool.CreateFunctionTool(aiFunction.Name, aiFunction.Description, functionParameters, strict);
+ }
+
+ private static UsageDetails FromOpenAIUsage(ChatTokenUsage tokenUsage)
+ {
+ var destination = new UsageDetails
+ {
+ InputTokenCount = tokenUsage.InputTokenCount,
+ OutputTokenCount = tokenUsage.OutputTokenCount,
+ TotalTokenCount = tokenUsage.TotalTokenCount,
+ AdditionalCounts = [],
+ };
+
+ var counts = destination.AdditionalCounts;
+
+ if (tokenUsage.InputTokenDetails is ChatInputTokenUsageDetails inputDetails)
+ {
+ const string InputDetails = nameof(ChatTokenUsage.InputTokenDetails);
+ counts.Add($"{InputDetails}.{nameof(ChatInputTokenUsageDetails.AudioTokenCount)}", inputDetails.AudioTokenCount);
+ counts.Add($"{InputDetails}.{nameof(ChatInputTokenUsageDetails.CachedTokenCount)}", inputDetails.CachedTokenCount);
+ }
+
+ if (tokenUsage.OutputTokenDetails is ChatOutputTokenUsageDetails outputDetails)
+ {
+ const string OutputDetails = nameof(ChatTokenUsage.OutputTokenDetails);
+ counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.ReasoningTokenCount)}", outputDetails.ReasoningTokenCount);
+ counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.AudioTokenCount)}", outputDetails.AudioTokenCount);
+ counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.AcceptedPredictionTokenCount)}", outputDetails.AcceptedPredictionTokenCount);
+ counts.Add($"{OutputDetails}.{nameof(ChatOutputTokenUsageDetails.RejectedPredictionTokenCount)}", outputDetails.RejectedPredictionTokenCount);
+ }
+
+ return destination;
+ }
+
+ /// Converts an OpenAI role to an Extensions role.
+ private static ChatRole FromOpenAIChatRole(ChatMessageRole role) =>
+ role switch
+ {
+ ChatMessageRole.System => ChatRole.System,
+ ChatMessageRole.User => ChatRole.User,
+ ChatMessageRole.Assistant => ChatRole.Assistant,
+ ChatMessageRole.Tool => ChatRole.Tool,
+ ChatMessageRole.Developer => ChatRoleDeveloper,
+ _ => new ChatRole(role.ToString()),
+ };
+
+ /// Creates an from a .
+ /// The content part to convert into a content.
+ /// The constructed , or if the content part could not be converted.
+ private static AIContent? ToAIContent(ChatMessageContentPart contentPart)
+ {
+ AIContent? aiContent = null;
+
+ if (contentPart.Kind == ChatMessageContentPartKind.Text)
+ {
+ aiContent = new TextContent(contentPart.Text);
+ }
+ else if (contentPart.Kind == ChatMessageContentPartKind.Image)
+ {
+ aiContent =
+ contentPart.ImageUri is not null ? new UriContent(contentPart.ImageUri, "image/*") :
+ contentPart.ImageBytes is not null ? new DataContent(contentPart.ImageBytes.ToMemory(), contentPart.ImageBytesMediaType) :
+ null;
+
+ if (aiContent is not null && contentPart.ImageDetailLevel?.ToString() is string detail)
+ {
+ (aiContent.AdditionalProperties ??= [])[nameof(contentPart.ImageDetailLevel)] = detail;
+ }
+ }
+
+ if (aiContent is not null)
+ {
+ if (contentPart.Refusal is string refusal)
+ {
+ (aiContent.AdditionalProperties ??= [])[nameof(contentPart.Refusal)] = refusal;
+ }
+
+ aiContent.RawRepresentation = contentPart;
+ }
+
+ return aiContent;
+ }
+
+ /// Converts an OpenAI finish reason to an Extensions finish reason.
+ private static ChatFinishReason? FromOpenAIFinishReason(OpenAI.Chat.ChatFinishReason? finishReason) =>
+ finishReason?.ToString() is not string s ? null :
+ finishReason switch
+ {
+ OpenAI.Chat.ChatFinishReason.Stop => ChatFinishReason.Stop,
+ OpenAI.Chat.ChatFinishReason.Length => ChatFinishReason.Length,
+ OpenAI.Chat.ChatFinishReason.ContentFilter => ChatFinishReason.ContentFilter,
+ OpenAI.Chat.ChatFinishReason.ToolCalls or OpenAI.Chat.ChatFinishReason.FunctionCall => ChatFinishReason.ToolCalls,
+ _ => new ChatFinishReason(s),
+ };
+
+ private static FunctionCallContent ParseCallContentFromJsonString(string json, string callId, string name) =>
+ FunctionCallContent.CreateFromParsedArguments(json, callId, name,
+ argumentParser: static json => JsonSerializer.Deserialize(json, ChatClientJsonContext.Default.IDictionaryStringObject)!);
+
+ private static FunctionCallContent ParseCallContentFromBinaryData(BinaryData ut8Json, string callId, string name) =>
+ FunctionCallContent.CreateFromParsedArguments(ut8Json, callId, name,
+ argumentParser: static json => JsonSerializer.Deserialize(json, ChatClientJsonContext.Default.IDictionaryStringObject)!);
+
+ /// Used to create the JSON payload for an OpenAI chat tool description.
+ private sealed class ChatToolJson
+ {
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = "object";
+
+ [JsonPropertyName("required")]
+ public HashSet Required { get; set; } = [];
+
+ [JsonPropertyName("properties")]
+ public Dictionary Properties { get; set; } = [];
+
+ [JsonPropertyName("additionalProperties")]
+ public bool AdditionalProperties { get; set; }
+ }
+
+ /// POCO representing function calling info. Used to concatenation information for a single function call from across multiple streaming updates.
+ private sealed class FunctionCallInfo
+ {
+ public string? CallId;
+ public string? Name;
+ public StringBuilder? Arguments;
+ }
+
+ /// Source-generated JSON type information.
+ [JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
+ UseStringEnumConverter = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ WriteIndented = true)]
+ [JsonSerializable(typeof(ChatToolJson))]
+ [JsonSerializable(typeof(IDictionary))]
+ [JsonSerializable(typeof(string[]))]
+ private sealed partial class ChatClientJsonContext : JsonSerializerContext;
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatCompletionRequest.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatCompletionRequest.cs
deleted file mode 100644
index 6a28c9f5490..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIChatCompletionRequest.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using OpenAI.Chat;
-
-#pragma warning disable CA1034 // Nested types should not be visible
-
-namespace Microsoft.Extensions.AI;
-
-///
-/// Represents an OpenAI chat completion request deserialized as Microsoft.Extension.AI models.
-///
-[JsonConverter(typeof(Converter))]
-public sealed class OpenAIChatCompletionRequest
-{
- ///
- /// Gets the chat messages specified in the request.
- ///
- public required IList Messages { get; init; }
-
- ///
- /// Gets the chat options governing the request.
- ///
- public required ChatOptions Options { get; init; }
-
- ///
- /// Gets a value indicating whether the response should be streamed.
- ///
- public bool Stream { get; init; }
-
- ///
- /// Gets the model id requested by the chat completion.
- ///
- public string? ModelId { get; init; }
-
- ///
- /// Converts an OpenAIChatCompletionRequest object to and from JSON.
- ///
- [EditorBrowsable(EditorBrowsableState.Never)]
- public sealed class Converter : JsonConverter
- {
- ///
- /// Reads and converts the JSON to type OpenAIChatCompletionRequest.
- ///
- /// The reader.
- /// The type to convert.
- /// The serializer options.
- /// The converted OpenAIChatCompletionRequest object.
- public override OpenAIChatCompletionRequest? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- ChatCompletionOptions chatCompletionOptions = JsonModelHelpers.Deserialize(ref reader);
- return OpenAIModelMappers.FromOpenAIChatCompletionRequest(chatCompletionOptions);
- }
-
- ///
- /// Writes the specified value as JSON.
- ///
- /// The writer.
- /// The value to write.
- /// The serializer options.
- public override void Write(Utf8JsonWriter writer, OpenAIChatCompletionRequest value, JsonSerializerOptions options) =>
- throw new NotSupportedException("Request body serialization is not supported.");
- }
-}
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
index 483786a3174..6b330e4da00 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIClientExtensions.cs
@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System;
+using System.ComponentModel;
+using Microsoft.Shared.Diagnostics;
using OpenAI;
-using OpenAI.Assistants;
using OpenAI.Chat;
using OpenAI.Embeddings;
using OpenAI.Responses;
@@ -16,46 +18,37 @@ public static class OpenAIClientExtensions
/// The client.
/// The model.
/// An that can be used to converse via the .
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("This method will be removed in an upcoming release.")]
public static IChatClient AsChatClient(this OpenAIClient openAIClient, string modelId) =>
- new OpenAIChatClient(openAIClient, modelId);
+ new OpenAIChatClient(Throw.IfNull(openAIClient).GetChatClient(modelId));
/// Gets an for use with this .
/// The client.
/// An that can be used to converse via the .
- public static IChatClient AsChatClient(this ChatClient chatClient) =>
+ public static IChatClient AsIChatClient(this ChatClient chatClient) =>
new OpenAIChatClient(chatClient);
/// Gets an for use with this .
/// The client.
/// An that can be used to converse via the .
- public static IChatClient AsChatClient(this OpenAIResponseClient responseClient) =>
+ public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient) =>
new OpenAIResponseChatClient(responseClient);
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only
- /// Gets an for use with this .
- /// The client.
- /// The ID of the assistant to use.
- ///
- /// The ID of the thread to use. If not supplied here, it should be supplied per request in .
- /// If none is supplied, a new thread will be created for a request.
- ///
- /// An that can be used to converse via the .
- public static IChatClient AsChatClient(this AssistantClient assistantClient, string assistantId, string? threadId = null) =>
- new OpenAIAssistantClient(assistantClient, assistantId, threadId);
-#pragma warning restore OPENAI001
-
/// Gets an for use with this .
/// The client.
/// The model to use.
/// The number of dimensions to generate in each embedding.
/// An that can be used to generate embeddings via the .
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("This method will be removed in an upcoming release.")]
public static IEmbeddingGenerator> AsEmbeddingGenerator(this OpenAIClient openAIClient, string modelId, int? dimensions = null) =>
- new OpenAIEmbeddingGenerator(openAIClient, modelId, dimensions);
+ new OpenAIEmbeddingGenerator(Throw.IfNull(openAIClient).GetEmbeddingClient(modelId), dimensions);
/// Gets an for use with this .
/// The client.
- /// The number of dimensions to generate in each embedding.
+ /// The number of dimensions to generate in each embedding.
/// An that can be used to generate embeddings via the .
- public static IEmbeddingGenerator> AsEmbeddingGenerator(this EmbeddingClient embeddingClient, int? dimensions = null) =>
- new OpenAIEmbeddingGenerator(embeddingClient, dimensions);
+ public static IEmbeddingGenerator> AsIEmbeddingGenerator(this EmbeddingClient embeddingClient, int? defaultModelDimensions = null) =>
+ new OpenAIEmbeddingGenerator(embeddingClient, defaultModelDimensions);
}
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs
index 8ae8a32b898..cf54b7906f0 100644
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs
+++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIEmbeddingGenerator.cs
@@ -17,7 +17,7 @@
namespace Microsoft.Extensions.AI;
/// An for an OpenAI .
-public sealed class OpenAIEmbeddingGenerator : IEmbeddingGenerator>
+internal sealed class OpenAIEmbeddingGenerator : IEmbeddingGenerator>
{
/// Default OpenAI endpoint.
private const string DefaultOpenAIEndpoint = "https://api.openai.com/v1";
@@ -25,62 +25,27 @@ public sealed class OpenAIEmbeddingGenerator : IEmbeddingGeneratorMetadata about the embedding generator.
private readonly EmbeddingGeneratorMetadata _metadata;
- /// The underlying .
- private readonly OpenAIClient? _openAIClient;
-
/// The underlying .
private readonly EmbeddingClient _embeddingClient;
/// The number of dimensions produced by the generator.
private readonly int? _dimensions;
- /// Initializes a new instance of the class.
- /// The underlying client.
- /// The model to use.
- /// The number of dimensions to generate in each embedding.
- /// is .
- /// is empty or composed entirely of whitespace.
- /// is not positive.
- public OpenAIEmbeddingGenerator(
- OpenAIClient openAIClient, string modelId, int? dimensions = null)
- {
- _ = Throw.IfNull(openAIClient);
- _ = Throw.IfNullOrWhitespace(modelId);
- if (dimensions is < 1)
- {
- Throw.ArgumentOutOfRangeException(nameof(dimensions), "Value must be greater than 0.");
- }
-
- _openAIClient = openAIClient;
- _embeddingClient = openAIClient.GetEmbeddingClient(modelId);
- _dimensions = dimensions;
-
- // https://github.com/openai/openai-dotnet/issues/215
- // The endpoint isn't currently exposed, so use reflection to get at it, temporarily. Once packages
- // implement the abstractions directly rather than providing adapters on top of the public APIs,
- // the package can provide such implementations separate from what's exposed in the public API.
- string providerUrl = (typeof(OpenAIClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
- ?.GetValue(openAIClient) as Uri)?.ToString() ??
- DefaultOpenAIEndpoint;
-
- _metadata = CreateMetadata("openai", providerUrl, modelId, dimensions);
- }
-
/// Initializes a new instance of the class.
/// The underlying client.
- /// The number of dimensions to generate in each embedding.
+ /// The number of dimensions to generate in each embedding.
/// is .
- /// is not positive.
- public OpenAIEmbeddingGenerator(EmbeddingClient embeddingClient, int? dimensions = null)
+ /// is not positive.
+ public OpenAIEmbeddingGenerator(EmbeddingClient embeddingClient, int? defaultModelDimensions = null)
{
_ = Throw.IfNull(embeddingClient);
- if (dimensions < 1)
+ if (defaultModelDimensions < 1)
{
- Throw.ArgumentOutOfRangeException(nameof(dimensions), "Value must be greater than 0.");
+ Throw.ArgumentOutOfRangeException(nameof(defaultModelDimensions), "Value must be greater than 0.");
}
_embeddingClient = embeddingClient;
- _dimensions = dimensions;
+ _dimensions = defaultModelDimensions;
// https://github.com/openai/openai-dotnet/issues/215
// The endpoint and model aren't currently exposed, so use reflection to get at them, temporarily. Once packages
@@ -91,9 +56,9 @@ public OpenAIEmbeddingGenerator(EmbeddingClient embeddingClient, int? dimensions
DefaultOpenAIEndpoint;
FieldInfo? modelField = typeof(EmbeddingClient).GetField("_model", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
- string? model = modelField?.GetValue(embeddingClient) as string;
+ string? modelId = modelField?.GetValue(embeddingClient) as string;
- _metadata = CreateMetadata("openai", providerUrl, model, dimensions);
+ _metadata = CreateMetadata("openai", providerUrl, modelId, defaultModelDimensions);
}
///
@@ -132,15 +97,14 @@ void IDisposable.Dispose()
return
serviceKey is not null ? null :
serviceType == typeof(EmbeddingGeneratorMetadata) ? _metadata :
- serviceType == typeof(OpenAIClient) ? _openAIClient :
serviceType == typeof(EmbeddingClient) ? _embeddingClient :
serviceType.IsInstanceOfType(this) ? this :
null;
}
/// Creates the for this instance.
- private static EmbeddingGeneratorMetadata CreateMetadata(string providerName, string providerUrl, string? model, int? dimensions) =>
- new(providerName, Uri.TryCreate(providerUrl, UriKind.Absolute, out Uri? providerUri) ? providerUri : null, model, dimensions);
+ private static EmbeddingGeneratorMetadata CreateMetadata(string providerName, string providerUrl, string? defaultModelId, int? defaultModelDimensions) =>
+ new(providerName, Uri.TryCreate(providerUrl, UriKind.Absolute, out Uri? providerUri) ? providerUri : null, defaultModelId, defaultModelDimensions);
/// Converts an extensions options instance to an OpenAI options instance.
private OpenAI.Embeddings.EmbeddingGenerationOptions? ToOpenAIOptions(EmbeddingGenerationOptions? options)
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs
deleted file mode 100644
index c75b2a4c644..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Generic;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Microsoft.Extensions.AI;
-
-/// Source-generated JSON type information.
-[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
- UseStringEnumConverter = true,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- WriteIndented = true)]
-[JsonSerializable(typeof(OpenAIRealtimeExtensions.ConversationFunctionToolParametersSchema))]
-[JsonSerializable(typeof(OpenAIModelMappers.OpenAIChatToolJson))]
-[JsonSerializable(typeof(IDictionary))]
-[JsonSerializable(typeof(string[]))]
-internal sealed partial class OpenAIJsonContext : JsonSerializerContext;
diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs
deleted file mode 100644
index ea28823e8fb..00000000000
--- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIModelMapper.ChatCompletion.cs
+++ /dev/null
@@ -1,650 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Shared.Diagnostics;
-using OpenAI.Chat;
-
-#pragma warning disable CA1308 // Normalize strings to uppercase
-#pragma warning disable CA1859 // Use concrete types when possible for improved performance
-#pragma warning disable SA1204 // Static elements should appear before instance elements
-#pragma warning disable S103 // Lines should not be too long
-#pragma warning disable S1067 // Expressions should not be too complex
-#pragma warning disable S2178 // Short-circuit logic should be used in boolean contexts
-#pragma warning disable S3440 // Variables should not be checked against the values they're about to be assigned
-#pragma warning disable EA0011 // Consider removing unnecessary conditional access operator (?)
-
-namespace Microsoft.Extensions.AI;
-
-internal static partial class OpenAIModelMappers
-{
- public static ChatCompletion ToOpenAIChatCompletion(ChatResponse response, JsonSerializerOptions options)
- {
- _ = Throw.IfNull(response);
-
- List? toolCalls = null;
- ChatRole? role = null;
- List allContents = [];
- foreach (ChatMessage message in response.Messages)
- {
- role = message.Role;
- foreach (AIContent content in message.Contents)
- {
- allContents.Add(content);
- if (content is FunctionCallContent callRequest)
- {
- toolCalls ??= [];
- toolCalls.Add(ChatToolCall.CreateFunctionToolCall(
- callRequest.CallId,
- callRequest.Name,
- new(JsonSerializer.SerializeToUtf8Bytes(
- callRequest.Arguments,
- options.GetTypeInfo(typeof(IDictionary))))));
- }
- }
- }
-
- ChatTokenUsage? chatTokenUsage = null;
- if (response.Usage is UsageDetails usageDetails)
- {
- chatTokenUsage = ToOpenAIUsage(usageDetails);
- }
-
- return OpenAIChatModelFactory.ChatCompletion(
- id: response.ResponseId ?? CreateCompletionId(),
- model: response.ModelId,
- createdAt: response.CreatedAt ?? DateTimeOffset.UtcNow,
- role: ToOpenAIChatRole(role) ?? ChatMessageRole.Assistant,
- finishReason: ToOpenAIFinishReason(response.FinishReason),
- content: new(ToOpenAIChatContent(allContents)),
- toolCalls: toolCalls,
- refusal: response.AdditionalProperties.GetValueOrDefault(nameof(ChatCompletion.Refusal)),
- contentTokenLogProbabilities: response.AdditionalProperties.GetValueOrDefault>(nameof(ChatCompletion.ContentTokenLogProbabilities)),
- refusalTokenLogProbabilities: response.AdditionalProperties.GetValueOrDefault>(nameof(ChatCompletion.RefusalTokenLogProbabilities)),
- systemFingerprint: response.AdditionalProperties.GetValueOrDefault(nameof(ChatCompletion.SystemFingerprint)),
- usage: chatTokenUsage);
- }
-
- public static ChatResponse FromOpenAIChatCompletion(ChatCompletion openAICompletion, ChatOptions? options, ChatCompletionOptions chatCompletionOptions)
- {
- _ = Throw.IfNull(openAICompletion);
-
- // Create the return message.
- ChatMessage returnMessage = new()
- {
- MessageId = openAICompletion.Id, // There's no per-message ID, so we use the same value as the response ID
- RawRepresentation = openAICompletion,
- Role = FromOpenAIChatRole(openAICompletion.Role),
- };
-
- // Populate its content from those in the OpenAI response content.
- foreach (ChatMessageContentPart contentPart in openAICompletion.Content)
- {
- if (ToAIContent(contentPart) is AIContent aiContent)
- {
- returnMessage.Contents.Add(aiContent);
- }
- }
-
- // Output audio is handled separately from message content parts.
- if (openAICompletion.OutputAudio is ChatOutputAudio audio)
- {
- string mimeType = chatCompletionOptions?.AudioOptions?.OutputAudioFormat.ToString()?.ToLowerInvariant() switch
- {
- "opus" => "audio/opus",
- "aac" => "audio/aac",
- "flac" => "audio/flac",
- "wav" => "audio/wav",
- "pcm" => "audio/pcm",
- "mp3" or _ => "audio/mpeg",
- };
-
- var dc = new DataContent(audio.AudioBytes.ToMemory(), mimeType)
- {
- AdditionalProperties = new() { [nameof(audio.ExpiresAt)] = audio.ExpiresAt },
- };
-
- if (audio.Id is string id)
- {
- dc.AdditionalProperties[nameof(audio.Id)] = id;
- }
-
- if (audio.Transcript is string transcript)
- {
- dc.AdditionalProperties[nameof(audio.Transcript)] = transcript;
- }
-
- returnMessage.Contents.Add(dc);
- }
-
- // Also manufacture function calling content items from any tool calls in the response.
- if (options?.Tools is { Count: > 0 })
- {
- foreach (ChatToolCall toolCall in openAICompletion.ToolCalls)
- {
- if (!string.IsNullOrWhiteSpace(toolCall.FunctionName))
- {
- var callContent = ParseCallContentFromBinaryData(toolCall.FunctionArguments, toolCall.Id, toolCall.FunctionName);
- callContent.RawRepresentation = toolCall;
-
- returnMessage.Contents.Add(callContent);
- }
- }
- }
-
- // Wrap the content in a ChatResponse to return.
- var response = new ChatResponse(returnMessage)
- {
- CreatedAt = openAICompletion.CreatedAt,
- FinishReason = FromOpenAIFinishReason(openAICompletion.FinishReason),
- ModelId = openAICompletion.Model,
- RawRepresentation = openAICompletion,
- ResponseId = openAICompletion.Id,
- };
-
- if (openAICompletion.Usage is ChatTokenUsage tokenUsage)
- {
- response.Usage = FromOpenAIUsage(tokenUsage);
- }
-
- if (openAICompletion.ContentTokenLogProbabilities is { Count: > 0 } contentTokenLogProbs)
- {
- (response.AdditionalProperties ??= [])[nameof(openAICompletion.ContentTokenLogProbabilities)] = contentTokenLogProbs;
- }
-
- if (openAICompletion.Refusal is string refusal)
- {
- (response.AdditionalProperties ??= [])[nameof(openAICompletion.Refusal)] = refusal;
- }
-
- if (openAICompletion.RefusalTokenLogProbabilities is { Count: > 0 } refusalTokenLogProbs)
- {
- (response.AdditionalProperties ??= [])[nameof(openAICompletion.RefusalTokenLogProbabilities)] = refusalTokenLogProbs;
- }
-
- if (openAICompletion.SystemFingerprint is string systemFingerprint)
- {
- (response.AdditionalProperties ??= [])[nameof(openAICompletion.SystemFingerprint)] = systemFingerprint;
- }
-
- return response;
- }
-
- public static ChatOptions FromOpenAIOptions(ChatCompletionOptions? options)
- {
- ChatOptions result = new();
-
- if (options is not null)
- {
- result.ModelId = _getModelIdAccessor.Invoke(options, null)?.ToString() switch
- {
- null or "" => null,
- var modelId => modelId,
- };
-
- result.FrequencyPenalty = options.FrequencyPenalty;
- result.MaxOutputTokens = options.MaxOutputTokenCount;
- result.TopP = options.TopP;
- result.PresencePenalty = options.PresencePenalty;
- result.Temperature = options.Temperature;
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
- result.Seed = options.Seed;
-#pragma warning restore OPENAI001
-
- if (options.StopSequences is { Count: > 0 } stopSequences)
- {
- result.StopSequences = [.. stopSequences];
- }
-
- if (options.EndUserId is string endUserId)
- {
- (result.AdditionalProperties ??= [])[nameof(options.EndUserId)] = endUserId;
- }
-
- if (options.IncludeLogProbabilities is bool includeLogProbabilities)
- {
- (result.AdditionalProperties ??= [])[nameof(options.IncludeLogProbabilities)] = includeLogProbabilities;
- }
-
- if (options.LogitBiases is { Count: > 0 } logitBiases)
- {
- (result.AdditionalProperties ??= [])[nameof(options.LogitBiases)] = new Dictionary(logitBiases);
- }
-
- if (options.AllowParallelToolCalls is bool allowParallelToolCalls)
- {
- (result.AdditionalProperties ??= [])[nameof(options.AllowParallelToolCalls)] = allowParallelToolCalls;
- }
-
- if (options.TopLogProbabilityCount is int topLogProbabilityCount)
- {
- (result.AdditionalProperties ??= [])[nameof(options.TopLogProbabilityCount)] = topLogProbabilityCount;
- }
-
- if (options.Metadata is IDictionary { Count: > 0 } metadata)
- {
- (result.AdditionalProperties ??= [])[nameof(options.Metadata)] = new Dictionary(metadata);
- }
-
- if (options.StoredOutputEnabled is bool storedOutputEnabled)
- {
- (result.AdditionalProperties ??= [])[nameof(options.StoredOutputEnabled)] = storedOutputEnabled;
- }
-
- if (options.Tools is { Count: > 0 } tools)
- {
- foreach (ChatTool tool in tools)
- {
- if (FromOpenAIChatTool(tool) is { } convertedTool)
- {
- (result.Tools ??= []).Add(convertedTool);
- }
- }
-
- using var toolChoiceJson = JsonDocument.Parse(JsonModelHelpers.Serialize(options.ToolChoice).ToMemory());
- JsonElement jsonElement = toolChoiceJson.RootElement;
- switch (jsonElement.ValueKind)
- {
- case JsonValueKind.String:
- result.ToolMode = jsonElement.GetString() switch
- {
- "required" => ChatToolMode.RequireAny,
- "none" => ChatToolMode.None,
- _ => ChatToolMode.Auto,
- };
-
- break;
- case JsonValueKind.Object:
- if (jsonElement.TryGetProperty("function", out JsonElement functionElement))
- {
- result.ToolMode = ChatToolMode.RequireSpecific(functionElement.GetString()!);
- }
-
- break;
- }
- }
- }
-
- return result;
- }
-
- /// Converts an extensions options instance to an OpenAI options instance.
- public static ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
- {
- ChatCompletionOptions result = new();
-
- if (options is not null)
- {
- result.FrequencyPenalty = options.FrequencyPenalty;
- result.MaxOutputTokenCount = options.MaxOutputTokens;
- result.TopP = options.TopP;
- result.PresencePenalty = options.PresencePenalty;
- result.Temperature = options.Temperature;
-#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
- result.Seed = options.Seed;
-#pragma warning restore OPENAI001
-
- if (options.StopSequences is { Count: > 0 } stopSequences)
- {
- foreach (string stopSequence in stopSequences)
- {
- result.StopSequences.Add(stopSequence);
- }
- }
-
- if (options.AdditionalProperties is { Count: > 0 } additionalProperties)
- {
- if (additionalProperties.TryGetValue(nameof(result.AllowParallelToolCalls), out bool allowParallelToolCalls))
- {
- result.AllowParallelToolCalls = allowParallelToolCalls;
- }
-
- if (additionalProperties.TryGetValue(nameof(result.AudioOptions), out ChatAudioOptions? audioOptions))
- {
- result.AudioOptions = audioOptions;
- }
-
- if (additionalProperties.TryGetValue(nameof(result.EndUserId), out string? endUserId))
- {
- result.EndUserId = endUserId;
- }
-
- if (additionalProperties.TryGetValue(nameof(result.IncludeLogProbabilities), out bool includeLogProbabilities))
- {
- result.IncludeLogProbabilities = includeLogProbabilities;
- }
-
- if (additionalProperties.TryGetValue(nameof(result.LogitBiases), out IDictionary? logitBiases))
- {
- foreach (KeyValuePair kvp in logitBiases!)
- {
- result.LogitBiases[kvp.Key] = kvp.Value;
- }
- }
-
- if (additionalProperties.TryGetValue(nameof(result.Metadata), out IDictionary? metadata))
- {
- foreach (KeyValuePair kvp in metadata)
- {
- result.Metadata[kvp.Key] = kvp.Value;
- }
- }
-
- if (additionalProperties.TryGetValue(nameof(result.OutputPrediction), out ChatOutputPrediction? outputPrediction))
- {
- result.OutputPrediction = outputPrediction;
- }
-
- if (additionalProperties.TryGetValue(nameof(result.ReasoningEffortLevel), out ChatReasoningEffortLevel reasoningEffortLevel))
- {
- result.ReasoningEffortLevel = reasoningEffortLevel;
- }
-
- if (additionalProperties.TryGetValue(nameof(result.ResponseModalities), out ChatResponseModalities responseModalities))
- {
- result.ResponseModalities = responseModalities;
- }
-
- if (additionalProperties.TryGetValue(nameof(result.StoredOutputEnabled), out bool storeOutputEnabled))
- {
- result.StoredOutputEnabled = storeOutputEnabled;
- }
-
- if (additionalProperties.TryGetValue(nameof(result.TopLogProbabilityCount), out int topLogProbabilityCountInt))
- {
- result.TopLogProbabilityCount = topLogProbabilityCountInt;
- }
- }
-
- if (options.Tools is { Count: > 0 } tools)
- {
- foreach (AITool tool in tools)
- {
- if (tool is AIFunction af)
- {
- result.Tools.Add(ToOpenAIChatTool(af));
- }
- }
-
- switch (options.ToolMode)
- {
- case NoneChatToolMode:
- result.ToolChoice = ChatToolChoice.CreateNoneChoice();
- break;
-
- case AutoChatToolMode:
- case null:
- result.ToolChoice = ChatToolChoice.CreateAutoChoice();
- break;
-
- case RequiredChatToolMode required:
- result.ToolChoice = required.RequiredFunctionName is null ?
- ChatToolChoice.CreateRequiredChoice() :
- ChatToolChoice.CreateFunctionChoice(required.RequiredFunctionName);
- break;
- }
- }
-
- if (options.ResponseFormat is ChatResponseFormatText)
- {
- result.ResponseFormat = OpenAI.Chat.ChatResponseFormat.CreateTextFormat();
- }
- else if (options.ResponseFormat is ChatResponseFormatJson jsonFormat)
- {
- result.ResponseFormat = jsonFormat.Schema is { } jsonSchema ?
- OpenAI.Chat.ChatResponseFormat.CreateJsonSchemaFormat(
- jsonFormat.SchemaName ?? "json_schema",
- BinaryData.FromBytes(
- JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
- jsonFormat.SchemaDescription,
- jsonSchemaIsStrict: true) :
- OpenAI.Chat.ChatResponseFormat.CreateJsonObjectFormat();
- }
- }
-
- return result;
- }
-
- private static AITool? FromOpenAIChatTool(ChatTool chatTool)
- {
- switch (chatTool.Kind)
- {
- case ChatToolKind.Function:
- AdditionalPropertiesDictionary additionalProperties = [];
- if (chatTool.FunctionSchemaIsStrict is bool strictValue)
- {
- additionalProperties["Strict"] = strictValue;
- }
-
- OpenAIChatToolJson openAiChatTool = JsonSerializer.Deserialize(chatTool.FunctionParameters.ToMemory().Span, OpenAIJsonContext.Default.OpenAIChatToolJson)!;
- JsonElement schema = JsonSerializer.SerializeToElement(openAiChatTool, OpenAIJsonContext.Default.OpenAIChatToolJson);
- return new MetadataOnlyAIFunction(chatTool.FunctionName, chatTool.FunctionDescription, schema, additionalProperties);
-
- default:
- return null;
- }
- }
-
- private sealed class MetadataOnlyAIFunction(string name, string description, JsonElement schema, IReadOnlyDictionary additionalProps) : AIFunction
- {
- public override string Name => name;
- public override string Description => description;
- public override JsonElement JsonSchema => schema;
- public override IReadOnlyDictionary AdditionalProperties => additionalProps;
- protected override ValueTask