diff --git a/eng/packages/General.props b/eng/packages/General.props index b2978f5b6be..a8de843a353 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -4,7 +4,6 @@ - diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIChatToolJson.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIChatToolJson.cs deleted file mode 100644 index 7be8fee287a..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIChatToolJson.cs +++ /dev/null @@ -1,21 +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; - -/// Used to create the JSON payload for an AzureAI chat tool description. -internal sealed class AzureAIChatToolJson -{ - [JsonPropertyName("type")] - public string Type { get; set; } = "object"; - - [JsonPropertyName("required")] - public List Required { get; set; } = []; - - [JsonPropertyName("properties")] - public Dictionary Properties { get; set; } = []; -} diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs deleted file mode 100644 index 45081c0ab6c..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceChatClient.cs +++ /dev/null @@ -1,552 +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; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.Inference; -using Microsoft.Shared.Diagnostics; - -#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 - -namespace Microsoft.Extensions.AI; - -/// Represents an for an Azure AI Inference . -internal sealed class AzureAIInferenceChatClient : IChatClient -{ - /// Gets the JSON schema transform cache conforming to OpenAI restrictions per https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas. - private static AIJsonSchemaTransformCache SchemaTransformCache { get; } = new(new() - { - RequireAllProperties = true, - DisallowAdditionalProperties = true, - ConvertBooleanSchemas = true, - MoveDefaultKeywordToDescription = true, - }); - - /// Metadata about the client. - private readonly ChatClientMetadata _metadata; - - /// The underlying . - private readonly ChatCompletionsClient _chatCompletionsClient; - - /// 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 , it can be provided per request via . - /// is . - /// is empty or composed entirely of whitespace. - public AzureAIInferenceChatClient(ChatCompletionsClient chatCompletionsClient, string? defaultModelId = null) - { - _ = Throw.IfNull(chatCompletionsClient); - - if (defaultModelId is not null) - { - _ = Throw.IfNullOrWhitespace(defaultModelId); - } - - _chatCompletionsClient = chatCompletionsClient; - - // 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 - // 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. - var providerUrl = typeof(ChatCompletionsClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(chatCompletionsClient) as Uri; - - _metadata = new ChatClientMetadata("azure.ai.inference", providerUrl, defaultModelId); - } - - /// - object? IChatClient.GetService(Type serviceType, object? serviceKey) - { - _ = Throw.IfNull(serviceType); - - return - serviceKey is not null ? null : - serviceType == typeof(ChatCompletionsClient) ? _chatCompletionsClient : - serviceType == typeof(ChatClientMetadata) ? _metadata : - serviceType.IsInstanceOfType(this) ? this : - null; - } - - /// - public async Task GetResponseAsync( - IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(messages); - - // Make the call. - ChatCompletions response = (await _chatCompletionsClient.CompleteAsync( - ToAzureAIOptions(messages, options), - cancellationToken: cancellationToken).ConfigureAwait(false)).Value; - - // Create the return message. - ChatMessage message = new(ToChatRole(response.Role), response.Content) - { - CreatedAt = response.Created, - MessageId = response.Id, // There is no per-message ID, but there's only one message per response, so use the response ID - RawRepresentation = response, - }; - - if (response.ToolCalls is { Count: > 0 } toolCalls) - { - foreach (var toolCall in toolCalls) - { - if (toolCall is ChatCompletionsToolCall ftc && !string.IsNullOrWhiteSpace(ftc.Name)) - { - FunctionCallContent callContent = ParseCallContentFromJsonString(ftc.Arguments, toolCall.Id, ftc.Name); - callContent.RawRepresentation = toolCall; - - message.Contents.Add(callContent); - } - } - } - - UsageDetails? usage = null; - if (response.Usage is CompletionsUsage completionsUsage) - { - usage = new() - { - InputTokenCount = completionsUsage.PromptTokens, - OutputTokenCount = completionsUsage.CompletionTokens, - TotalTokenCount = completionsUsage.TotalTokens, - }; - } - - // Wrap the content in a ChatResponse to return. - return new ChatResponse(message) - { - CreatedAt = response.Created, - ModelId = response.Model, - FinishReason = ToFinishReason(response.FinishReason), - RawRepresentation = response, - ResponseId = response.Id, - Usage = usage, - }; - } - - /// - public async IAsyncEnumerable GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(messages); - - Dictionary? functionCallInfos = null; - ChatRole? streamedRole = default; - ChatFinishReason? finishReason = default; - string? responseId = null; - DateTimeOffset? createdAt = null; - string? modelId = null; - string lastCallId = string.Empty; - - // Process each update as it arrives - var updates = await _chatCompletionsClient.CompleteStreamingAsync(ToAzureAIOptions(messages, options), cancellationToken).ConfigureAwait(false); - await foreach (StreamingChatCompletionsUpdate chatCompletionUpdate in updates.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 ??= chatCompletionUpdate.Role is global::Azure.AI.Inference.ChatRole role ? ToChatRole(role) : null; - finishReason ??= chatCompletionUpdate.FinishReason is CompletionsFinishReason reason ? ToFinishReason(reason) : null; - responseId ??= chatCompletionUpdate.Id; // While it's unclear from the name, this Id is documented to be the response ID, not the chunk ID - createdAt ??= chatCompletionUpdate.Created; - modelId ??= chatCompletionUpdate.Model; - - // Create the response content object. - ChatResponseUpdate responseUpdate = new() - { - CreatedAt = chatCompletionUpdate.Created, - FinishReason = finishReason, - ModelId = modelId, - RawRepresentation = chatCompletionUpdate, - ResponseId = responseId, - MessageId = responseId, // There is no per-message ID, but there's only one message per response, so use the response ID - Role = streamedRole, - }; - - // Transfer over content update items. - if (chatCompletionUpdate.ContentUpdate is string update) - { - responseUpdate.Contents.Add(new TextContent(update)); - } - - // Transfer over tool call updates. - if (chatCompletionUpdate.ToolCallUpdate is { } toolCallUpdate) - { - if (toolCallUpdate.Id is not null) - { - lastCallId = toolCallUpdate.Id; - } - - functionCallInfos ??= []; - if (!functionCallInfos.TryGetValue(lastCallId, out FunctionCallInfo? existing)) - { - functionCallInfos[lastCallId] = existing = new(); - } - - existing.Name ??= toolCallUpdate.Function.Name; - if (toolCallUpdate.Function.Arguments is { } arguments) - { - _ = (existing.Arguments ??= new()).Append(arguments); - } - } - - if (chatCompletionUpdate.Usage is { } usage) - { - responseUpdate.Contents.Add(new UsageContent(new() - { - InputTokenCount = usage.PromptTokens, - OutputTokenCount = usage.CompletionTokens, - TotalTokenCount = usage.TotalTokens, - })); - } - - // 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) - { - var responseUpdate = new ChatResponseUpdate - { - CreatedAt = createdAt, - FinishReason = finishReason, - ModelId = modelId, - ResponseId = responseId, - MessageId = responseId, // There is no per-message ID, but there's only one message per response, so use the response ID - Role = streamedRole, - }; - - foreach (var entry in functionCallInfos) - { - FunctionCallInfo fci = entry.Value; - if (!string.IsNullOrWhiteSpace(fci.Name)) - { - FunctionCallContent callContent = ParseCallContentFromJsonString( - fci.Arguments?.ToString() ?? string.Empty, - entry.Key, - fci.Name!); - responseUpdate.Contents.Add(callContent); - } - } - - yield return responseUpdate; - } - } - - /// - void IDisposable.Dispose() - { - // Nothing to dispose. Implementation required for the IChatClient interface. - } - - /// 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? Name; - public StringBuilder? Arguments; - } - - /// Converts an AzureAI role to an Extensions role. - private static ChatRole ToChatRole(global::Azure.AI.Inference.ChatRole role) => - role.Equals(global::Azure.AI.Inference.ChatRole.System) ? ChatRole.System : - role.Equals(global::Azure.AI.Inference.ChatRole.User) ? ChatRole.User : - role.Equals(global::Azure.AI.Inference.ChatRole.Assistant) ? ChatRole.Assistant : - role.Equals(global::Azure.AI.Inference.ChatRole.Tool) ? ChatRole.Tool : - role.Equals(global::Azure.AI.Inference.ChatRole.Developer) ? ChatRoleDeveloper : - new ChatRole(role.ToString()); - - /// Converts an AzureAI finish reason to an Extensions finish reason. - private static ChatFinishReason? ToFinishReason(CompletionsFinishReason? finishReason) => - finishReason?.ToString() is not string s ? null : - finishReason == CompletionsFinishReason.Stopped ? ChatFinishReason.Stop : - finishReason == CompletionsFinishReason.TokenLimitReached ? ChatFinishReason.Length : - finishReason == CompletionsFinishReason.ContentFiltered ? ChatFinishReason.ContentFilter : - finishReason == CompletionsFinishReason.ToolCalls ? ChatFinishReason.ToolCalls : - new(s); - - private ChatCompletionsOptions CreateAzureAIOptions(IEnumerable chatContents, ChatOptions? options) => - new(ToAzureAIInferenceChatMessages(chatContents, options)) - { - Model = options?.ModelId ?? _metadata.DefaultModelId ?? - throw new InvalidOperationException("No model id was provided when either constructing the client or in the chat options.") - }; - - /// Converts an extensions options instance to an Azure.AI.Inference options instance. - private ChatCompletionsOptions ToAzureAIOptions(IEnumerable chatContents, ChatOptions? options) - { - if (options is null) - { - return CreateAzureAIOptions(chatContents, options); - } - - if (options.RawRepresentationFactory?.Invoke(this) is ChatCompletionsOptions result) - { - result.Messages = ToAzureAIInferenceChatMessages(chatContents, options).ToList(); - result.Model ??= options.ModelId ?? _metadata.DefaultModelId ?? - throw new InvalidOperationException("No model id was provided when either constructing the client or in the chat options."); - } - else - { - result = CreateAzureAIOptions(chatContents, options); - } - - result.FrequencyPenalty ??= options.FrequencyPenalty; - result.MaxTokens ??= options.MaxOutputTokens; - result.NucleusSamplingFactor ??= options.TopP; - result.PresencePenalty ??= options.PresencePenalty; - result.Temperature ??= options.Temperature; - result.Seed ??= options.Seed; - - if (options.StopSequences is { Count: > 0 } stopSequences) - { - foreach (string stopSequence in stopSequences) - { - result.StopSequences.Add(stopSequence); - } - } - - // This property is strongly typed on ChatOptions but not on ChatCompletionsOptions. - if (options.TopK is int topK && !result.AdditionalProperties.ContainsKey("top_k")) - { - result.AdditionalProperties["top_k"] = new BinaryData(JsonSerializer.SerializeToUtf8Bytes(topK, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(int)))); - } - - if (options.AdditionalProperties is { } props) - { - foreach (var prop in props) - { - byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))); - result.AdditionalProperties[prop.Key] = new BinaryData(data); - } - } - - if (options.Tools is { Count: > 0 } tools) - { - foreach (AITool tool in tools) - { - if (tool is AIFunctionDeclaration af) - { - result.Tools.Add(ToAzureAIChatTool(af)); - } - } - - if (result.ToolChoice is null && result.Tools.Count > 0) - { - switch (options.ToolMode) - { - case NoneChatToolMode: - result.ToolChoice = ChatCompletionsToolChoice.None; - break; - - case AutoChatToolMode: - case null: - result.ToolChoice = ChatCompletionsToolChoice.Auto; - break; - - case RequiredChatToolMode required: - result.ToolChoice = required.RequiredFunctionName is null ? - ChatCompletionsToolChoice.Required : - new ChatCompletionsToolChoice(new FunctionDefinition(required.RequiredFunctionName)); - break; - } - } - } - - if (result.ResponseFormat is null) - { - if (options.ResponseFormat is ChatResponseFormatText) - { - result.ResponseFormat = ChatCompletionsResponseFormat.CreateTextFormat(); - } - else if (options.ResponseFormat is ChatResponseFormatJson json) - { - if (SchemaTransformCache.GetOrCreateTransformedSchema(json) is { } schema) - { - var tool = JsonSerializer.Deserialize(schema, JsonContext.Default.AzureAIChatToolJson)!; - result.ResponseFormat = ChatCompletionsResponseFormat.CreateJsonFormat( - json.SchemaName ?? "json_schema", - new Dictionary - { - ["type"] = _objectString, - ["properties"] = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool.Properties, JsonContext.Default.DictionaryStringJsonElement)), - ["required"] = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool.Required, JsonContext.Default.ListString)), - ["additionalProperties"] = _falseString, - }, - json.SchemaDescription); - } - else - { - result.ResponseFormat = ChatCompletionsResponseFormat.CreateJsonFormat(); - } - } - } - - return result; - } - - /// Cached for "object". - private static readonly BinaryData _objectString = BinaryData.FromString("\"object\""); - - /// Cached for "false". - private static readonly BinaryData _falseString = BinaryData.FromString("false"); - - /// Converts an Extensions function to an AzureAI chat tool. - private static ChatCompletionsToolDefinition ToAzureAIChatTool(AIFunctionDeclaration aiFunction) - { - // Map to an intermediate model so that redundant properties are skipped. - var tool = JsonSerializer.Deserialize(SchemaTransformCache.GetOrCreateTransformedSchema(aiFunction), JsonContext.Default.AzureAIChatToolJson)!; - var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, JsonContext.Default.AzureAIChatToolJson)); - return new(new FunctionDefinition(aiFunction.Name) - { - Description = aiFunction.Description, - Parameters = functionParameters, - }); - } - - /// Converts an Extensions chat message enumerable to an AzureAI chat message enumerable. - private static IEnumerable ToAzureAIInferenceChatMessages(IEnumerable inputs, ChatOptions? options) - { - // Maps all of the M.E.AI types to the corresponding AzureAI types. - // Unrecognized or non-processable content is ignored. - - if (options?.Instructions is { } instructions && !string.IsNullOrWhiteSpace(instructions)) - { - yield return new ChatRequestSystemMessage(instructions); - } - - foreach (ChatMessage input in inputs) - { - if (input.Role == ChatRole.System) - { - yield return new ChatRequestSystemMessage(input.Text ?? string.Empty); - } - else if (input.Role == ChatRoleDeveloper) - { - yield return new ChatRequestDeveloperMessage(input.Text ?? string.Empty); - } - 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, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))); - } - catch (NotSupportedException) - { - // If the type can't be serialized, skip it. - } - } - - yield return new ChatRequestToolMessage(result ?? string.Empty, resultContent.CallId); - } - } - } - else if (input.Role == ChatRole.User) - { - if (input.Contents.Count > 0) - { - if (input.Contents.All(c => c is TextContent)) - { - if (string.Concat(input.Contents) is { Length: > 0 } text) - { - yield return new ChatRequestUserMessage(text); - } - } - else if (GetContentParts(input.Contents) is { Count: > 0 } parts) - { - yield return new ChatRequestUserMessage(parts); - } - } - } - else if (input.Role == ChatRole.Assistant) - { - ChatRequestAssistantMessage message = new(string.Concat(input.Contents.Where(c => c is TextContent))); - - foreach (var content in input.Contents) - { - if (content is FunctionCallContent { CallId: not null } callRequest) - { - message.ToolCalls.Add(new ChatCompletionsToolCall( - callRequest.CallId, - new FunctionCall( - callRequest.Name, - JsonSerializer.Serialize(callRequest.Arguments, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary)))))); - } - } - - yield return message; - } - } - } - - /// Converts a list of to a list of . - private static List GetContentParts(IList contents) - { - Debug.Assert(contents is { Count: > 0 }, "Expected non-empty contents"); - - List parts = []; - foreach (var content in contents) - { - switch (content) - { - case TextContent textContent: - parts.Add(new ChatMessageTextContentItem(textContent.Text)); - break; - - case UriContent uriContent when uriContent.HasTopLevelMediaType("image"): - parts.Add(new ChatMessageImageContentItem(uriContent.Uri)); - break; - - case DataContent dataContent when dataContent.HasTopLevelMediaType("image"): - parts.Add(new ChatMessageImageContentItem(BinaryData.FromBytes(dataContent.Data), dataContent.MediaType)); - break; - - case UriContent uriContent when uriContent.HasTopLevelMediaType("audio"): - parts.Add(new ChatMessageAudioContentItem(uriContent.Uri)); - break; - - case DataContent dataContent when dataContent.HasTopLevelMediaType("audio"): - AudioContentFormat format; - if (dataContent.MediaType.Equals("audio/mpeg", StringComparison.OrdinalIgnoreCase)) - { - format = AudioContentFormat.Mp3; - } - else if (dataContent.MediaType.Equals("audio/wav", StringComparison.OrdinalIgnoreCase)) - { - format = AudioContentFormat.Wav; - } - else - { - break; - } - - parts.Add(new ChatMessageAudioContentItem(BinaryData.FromBytes(dataContent.Data), format)); - break; - } - } - - return parts; - } - - private static FunctionCallContent ParseCallContentFromJsonString(string json, string callId, string name) => - FunctionCallContent.CreateFromParsedArguments(json, callId, name, - argumentParser: static json => JsonSerializer.Deserialize(json, - (JsonTypeInfo>)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 deleted file mode 100644 index 04383a85b86..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceEmbeddingGenerator.cs +++ /dev/null @@ -1,197 +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.Buffers; -using System.Buffers.Binary; -using System.Buffers.Text; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.Inference; -using Microsoft.Shared.Diagnostics; - -#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields - -namespace Microsoft.Extensions.AI; - -/// Represents an for an Azure.AI.Inference . -internal sealed class AzureAIInferenceEmbeddingGenerator : - IEmbeddingGenerator> -{ - /// Metadata about the embedding generator. - private readonly EmbeddingGeneratorMetadata _metadata; - - /// The underlying . - private readonly EmbeddingsClient _embeddingsClient; - - /// The number of dimensions produced by the generator. - private readonly int? _dimensions; - - /// 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. - /// is . - /// is empty or composed entirely of whitespace. - /// is not positive. - public AzureAIInferenceEmbeddingGenerator( - EmbeddingsClient embeddingsClient, string? defaultModelId = null, int? defaultModelDimensions = null) - { - _ = Throw.IfNull(embeddingsClient); - - if (defaultModelId is not null) - { - _ = Throw.IfNullOrWhitespace(defaultModelId); - } - - if (defaultModelDimensions is < 1) - { - Throw.ArgumentOutOfRangeException(nameof(defaultModelDimensions), "Value must be greater than 0."); - } - - _embeddingsClient = embeddingsClient; - _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 - // 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. - var providerUrl = typeof(EmbeddingsClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(embeddingsClient) as Uri; - - _metadata = new EmbeddingGeneratorMetadata("azure.ai.inference", providerUrl, defaultModelId, defaultModelDimensions); - } - - /// - object? IEmbeddingGenerator.GetService(Type serviceType, object? serviceKey) - { - _ = Throw.IfNull(serviceType); - - return - serviceKey is not null ? null : - serviceType == typeof(EmbeddingsClient) ? _embeddingsClient : - serviceType == typeof(EmbeddingGeneratorMetadata) ? _metadata : - serviceType.IsInstanceOfType(this) ? this : - null; - } - - /// - public async Task>> GenerateAsync( - IEnumerable values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(values); - - var azureAIOptions = ToAzureAIOptions(values, options); - - var embeddings = (await _embeddingsClient.EmbedAsync(azureAIOptions, cancellationToken).ConfigureAwait(false)).Value; - - GeneratedEmbeddings> result = new(embeddings.Data.Select(e => - new Embedding(ParseBase64Floats(e.Embedding)) - { - CreatedAt = DateTimeOffset.UtcNow, - ModelId = embeddings.Model ?? azureAIOptions.Model, - })); - - if (embeddings.Usage is not null) - { - result.Usage = new() - { - InputTokenCount = embeddings.Usage.PromptTokens, - TotalTokenCount = embeddings.Usage.TotalTokens - }; - } - - return result; - } - - /// - void IDisposable.Dispose() - { - // Nothing to dispose. Implementation required for the IEmbeddingGenerator interface. - } - - internal static float[] ParseBase64Floats(BinaryData binaryData) - { - ReadOnlySpan base64 = binaryData.ToMemory().Span; - - // Remove quotes around base64 string. - if (base64.Length < 2 || base64[0] != (byte)'"' || base64[base64.Length - 1] != (byte)'"') - { - ThrowInvalidData(); - } - - base64 = base64.Slice(1, base64.Length - 2); - - // Decode base64 string to bytes. - byte[] bytes = ArrayPool.Shared.Rent(Base64.GetMaxDecodedFromUtf8Length(base64.Length)); - OperationStatus status = Base64.DecodeFromUtf8(base64, bytes.AsSpan(), out int bytesConsumed, out int bytesWritten); - if (status != OperationStatus.Done || bytesWritten % sizeof(float) != 0) - { - ThrowInvalidData(); - } - - // Interpret bytes as floats - float[] vector = new float[bytesWritten / sizeof(float)]; - bytes.AsSpan(0, bytesWritten).CopyTo(MemoryMarshal.AsBytes(vector.AsSpan())); - if (!BitConverter.IsLittleEndian) - { - Span ints = MemoryMarshal.Cast(vector.AsSpan()); -#if NET - BinaryPrimitives.ReverseEndianness(ints, ints); -#else - for (int i = 0; i < ints.Length; i++) - { - ints[i] = BinaryPrimitives.ReverseEndianness(ints[i]); - } -#endif - } - - ArrayPool.Shared.Return(bytes); - return vector; - - static void ThrowInvalidData() => - throw new FormatException("The input is not a valid Base64 string of encoded floats."); - } - - /// Converts an extensions options instance to an Azure.AI.Inference options instance. - private EmbeddingsOptions ToAzureAIOptions(IEnumerable inputs, EmbeddingGenerationOptions? options) - { - if (options?.RawRepresentationFactory?.Invoke(this) is not EmbeddingsOptions result) - { - result = new EmbeddingsOptions(inputs); - } - else - { - foreach (var input in inputs) - { - result.Input.Add(input); - } - } - - result.Dimensions ??= options?.Dimensions ?? _dimensions; - result.Model ??= options?.ModelId ?? _metadata.DefaultModelId; - result.EncodingFormat = EmbeddingEncodingFormat.Base64; - - if (options?.AdditionalProperties is { } props) - { - foreach (var prop in props) - { - if (prop.Value is not null) - { - byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))); - result.AdditionalProperties[prop.Key] = new BinaryData(data); - } - } - } - - return result; - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceExtensions.cs deleted file mode 100644 index 40d2932dd08..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceExtensions.cs +++ /dev/null @@ -1,36 +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 Azure.AI.Inference; - -namespace Microsoft.Extensions.AI; - -/// Provides extension methods for working with Azure AI Inference. -public static class AzureAIInferenceExtensions -{ - /// Gets an for use with this . - /// The client. - /// 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 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 , 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> AsIEmbeddingGenerator( - this EmbeddingsClient embeddingsClient, string? defaultModelId = null, int? defaultModelDimensions = null) => - new AzureAIInferenceEmbeddingGenerator(embeddingsClient, defaultModelId, defaultModelDimensions); - - /// Gets an for use with this . - /// The client. - /// 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> AsIEmbeddingGenerator( - this ImageEmbeddingsClient imageEmbeddingsClient, string? defaultModelId = null, int? defaultModelDimensions = null) => - new AzureAIInferenceImageEmbeddingGenerator(imageEmbeddingsClient, defaultModelId, defaultModelDimensions); -} diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs deleted file mode 100644 index b04a7c73a39..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/AzureAIInferenceImageEmbeddingGenerator.cs +++ /dev/null @@ -1,151 +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.Reflection; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.Inference; -using Microsoft.Shared.Diagnostics; - -#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields - -namespace Microsoft.Extensions.AI; - -/// Represents an for an Azure.AI.Inference . -internal sealed class AzureAIInferenceImageEmbeddingGenerator : - IEmbeddingGenerator> -{ - /// Metadata about the embedding generator. - private readonly EmbeddingGeneratorMetadata _metadata; - - /// The underlying . - private readonly ImageEmbeddingsClient _imageEmbeddingsClient; - - /// The number of dimensions produced by the generator. - private readonly int? _dimensions; - - /// 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. - /// is . - /// is empty or composed entirely of whitespace. - /// is not positive. - public AzureAIInferenceImageEmbeddingGenerator( - ImageEmbeddingsClient imageEmbeddingsClient, string? defaultModelId = null, int? defaultModelDimensions = null) - { - _ = Throw.IfNull(imageEmbeddingsClient); - - if (defaultModelId is not null) - { - _ = Throw.IfNullOrWhitespace(defaultModelId); - } - - if (defaultModelDimensions is < 1) - { - Throw.ArgumentOutOfRangeException(nameof(defaultModelDimensions), "Value must be greater than 0."); - } - - _imageEmbeddingsClient = imageEmbeddingsClient; - _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 - // 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. - var providerUrl = typeof(ImageEmbeddingsClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - ?.GetValue(imageEmbeddingsClient) as Uri; - - _metadata = new EmbeddingGeneratorMetadata("azure.ai.inference", providerUrl, defaultModelId, defaultModelDimensions); - } - - /// - object? IEmbeddingGenerator.GetService(Type serviceType, object? serviceKey) - { - _ = Throw.IfNull(serviceType); - - return - serviceKey is not null ? null : - serviceType == typeof(ImageEmbeddingsClient) ? _imageEmbeddingsClient : - serviceType == typeof(EmbeddingGeneratorMetadata) ? _metadata : - serviceType.IsInstanceOfType(this) ? this : - null; - } - - /// - public async Task>> GenerateAsync( - IEnumerable values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default) - { - _ = Throw.IfNull(values); - - var azureAIOptions = ToAzureAIOptions(values, options); - - var embeddings = (await _imageEmbeddingsClient.EmbedAsync(azureAIOptions, cancellationToken).ConfigureAwait(false)).Value; - - GeneratedEmbeddings> result = new(embeddings.Data.Select(e => - new Embedding(AzureAIInferenceEmbeddingGenerator.ParseBase64Floats(e.Embedding)) - { - CreatedAt = DateTimeOffset.UtcNow, - ModelId = embeddings.Model ?? azureAIOptions.Model, - })); - - if (embeddings.Usage is not null) - { - result.Usage = new() - { - InputTokenCount = embeddings.Usage.PromptTokens, - TotalTokenCount = embeddings.Usage.TotalTokens - }; - } - - return result; - } - - /// - void IDisposable.Dispose() - { - // Nothing to dispose. Implementation required for the IEmbeddingGenerator interface. - } - - /// Converts an extensions options instance to an Azure.AI.Inference options instance. - private ImageEmbeddingsOptions ToAzureAIOptions(IEnumerable inputs, EmbeddingGenerationOptions? options) - { - IEnumerable imageEmbeddingInputs = inputs.Select(dc => new ImageEmbeddingInput(dc.Uri)); - if (options?.RawRepresentationFactory?.Invoke(this) is not ImageEmbeddingsOptions result) - { - result = new ImageEmbeddingsOptions(imageEmbeddingInputs); - } - else - { - foreach (var input in imageEmbeddingInputs) - { - result.Input.Add(input); - } - } - - result.Dimensions ??= options?.Dimensions ?? _dimensions; - result.Model ??= options?.ModelId ?? _metadata.DefaultModelId; - result.EncodingFormat = EmbeddingEncodingFormat.Base64; - - if (options?.AdditionalProperties is { } props) - { - foreach (var prop in props) - { - if (prop.Value is not null) - { - byte[] data = JsonSerializer.SerializeToUtf8Bytes(prop.Value, AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))); - result.AdditionalProperties[prop.Key] = new BinaryData(data); - } - } - } - - return result; - } -} diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md deleted file mode 100644 index a28e211d427..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md +++ /dev/null @@ -1,94 +0,0 @@ -# Microsoft.Extensions.AI.AzureAIInference Release History - -## NOT YET RELEASED - -- Fixed package references for net10.0 asset. -- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. - -## 10.0.0-preview.1.25560.10 - -- Updated .NET dependencies to 10.0.0 versions. -- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. - -## 9.10.1-preview.1.25521.4 - -- No changes. - -## 9.10.0-preview.1.25513.3 - -- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. - -## 9.9.1-preview.1.25474.6 - -- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. - -## 9.9.0-preview.1.25458.4 - -- Updated tool mapping to recognize any `AIFunctionDeclaration`. -- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. -- Updated `AsIChatClient` for `OpenAIResponseClient` to support reasoning content with `GetStreamingResponseAsync`. - -## 9.8.0-preview.1.25412.6 - -- Updated to depend on Azure.AI.Inference 1.0.0-beta.5. -- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. - -## 9.7.0-preview.1.25356.2 - -- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. - -## 9.6.0-preview.1.25310.2 - -- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`. - -## 9.5.0-preview.1.25265.7 - -- Added `AsIEmbeddingGenerator` for Azure.AI.Inference `ImageEmbeddingsClient`. -- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.4.4-preview.1.25259.16 - -- Added an `AsIEmbeddingGenerator` extension method for `ImageEmbeddingsClient`. -- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.4.3-preview.1.25230.7 - -- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.4.0-preview.1.25207.5 - -- Updated to Azure.AI.Inference 1.0.0-beta.4. -- Renamed `AsChatClient`/`AsEmbeddingGenerator` extension methods to `AsIChatClient`/`AsIEmbeddingGenerator`. -- Removed the public `AzureAIInferenceChatClient`/`AzureAIInferenceEmbeddingGenerator` types. These are only created now via the extension methods. -- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.3.0-preview.1.25161.3 - -- Updated to accommodate the changes in `Microsoft.Extensions.AI.Abstractions`. - -## 9.3.0-preview.1.25114.11 - -- Updated to use Azure.AI.Inference 1.0.0-beta.3, adding support for structured output and audio input. - -## 9.1.0-preview.1.25064.3 - -- Fixed handling of text-only user messages. - -## 9.0.1-preview.1.24570.5 - - - Made the `ToolCallJsonSerializerOptions` property non-nullable. - -## 9.0.0-preview.9.24556.5 - -- Fixed `AzureAIInferenceEmbeddingGenerator` to respect `EmbeddingGenerationOptions.Dimensions`. - -## 9.0.0-preview.9.24525.1 - -- Lowered the required version of System.Text.Json to 8.0.5 when targeting net8.0 or older. -- Updated to use Azure.AI.Inference 1.0.0-beta.2. -- Added `AzureAIInferenceEmbeddingGenerator` and corresponding `AsEmbeddingGenerator` extension method. -- Improved handling of assistant messages that include both text and function call content. - -## 9.0.0-preview.9.24507.7 - -- Initial Preview diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/JsonContext.cs b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/JsonContext.cs deleted file mode 100644 index 89e0946d306..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/JsonContext.cs +++ /dev/null @@ -1,15 +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.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(AzureAIChatToolJson))] -internal sealed partial class JsonContext : JsonSerializerContext; diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj deleted file mode 100644 index ade32d3026c..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.csproj +++ /dev/null @@ -1,43 +0,0 @@ - - - - Microsoft.Extensions.AI - Implementation of generative AI abstractions for Azure.AI.Inference. - AI - true - - - - preview - false - 86 - 0 - - - - $(TargetFrameworks);netstandard2.0 - $(NoWarn);CA1063 - true - true - - - - true - true - true - true - - - - - - - - - - - - - - - diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.json b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/Microsoft.Extensions.AI.AzureAIInference.json deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/README.md b/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/README.md deleted file mode 100644 index 18a8b2c8ea2..00000000000 --- a/src/Libraries/Microsoft.Extensions.AI.AzureAIInference/README.md +++ /dev/null @@ -1,290 +0,0 @@ -# Microsoft.Extensions.AI.AzureAIInference - -Provides an implementation of the `IChatClient` interface for the `Azure.AI.Inference` package. - -## Install the package - -From the command-line: - -```console -dotnet add package Microsoft.Extensions.AI.AzureAIInference -``` - -Or directly in the C# project file: - -```xml - - - -``` - -## Usage Examples - -### Chat - -```csharp -using Azure; -using Microsoft.Extensions.AI; - -IChatClient client = - new Azure.AI.Inference.ChatCompletionsClient( - new("https://models.inference.ai.azure.com"), - new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!)) - .AsIChatClient("gpt-4o-mini"); - -Console.WriteLine(await client.GetResponseAsync("What is AI?")); -``` - -> **Note:** When connecting with Azure Open AI, the URL passed into the `ChatCompletionsClient` needs to include `openai/deployments/{yourDeployment}`. For example: -> ```csharp -> new Azure.AI.Inference.ChatCompletionsClient( -> new("https://{your-resource-name}.openai.azure.com/openai/deployments/{yourDeployment}"), -> new AzureKeyCredential(Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!)) -> ``` - -### Chat + Conversation History - -```csharp -using Azure; -using Microsoft.Extensions.AI; - -IChatClient client = - new Azure.AI.Inference.ChatCompletionsClient( - new("https://models.inference.ai.azure.com"), - new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!)) - .AsIChatClient("gpt-4o-mini"); - -Console.WriteLine(await client.GetResponseAsync( -[ - new ChatMessage(ChatRole.System, "You are a helpful AI assistant"), - new ChatMessage(ChatRole.User, "What is AI?"), -])); -``` - -### Chat streaming - -```csharp -using Azure; -using Microsoft.Extensions.AI; - -IChatClient client = - new Azure.AI.Inference.ChatCompletionsClient( - new("https://models.inference.ai.azure.com"), - new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!)) - .AsIChatClient("gpt-4o-mini"); - -await foreach (var update in client.GetStreamingResponseAsync("What is AI?")) -{ - Console.Write(update); -} -``` - -### Tool calling - -```csharp -using System.ComponentModel; -using Azure; -using Microsoft.Extensions.AI; - -IChatClient azureClient = - new Azure.AI.Inference.ChatCompletionsClient( - new("https://models.inference.ai.azure.com"), - new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!)) - .AsIChatClient("gpt-4o-mini"); - -IChatClient client = new ChatClientBuilder(azureClient) - .UseFunctionInvocation() - .Build(); - -ChatOptions chatOptions = new() -{ - Tools = [AIFunctionFactory.Create(GetWeather)] -}; - -await foreach (var message in client.GetStreamingResponseAsync("Do I need an umbrella?", chatOptions)) -{ - Console.Write(message); -} - -[Description("Gets the weather")] -static string GetWeather() => Random.Shared.NextDouble() > 0.5 ? "It's sunny" : "It's raining"; -``` - -### Caching - -```csharp -using Azure; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; - -IDistributedCache cache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - -IChatClient azureClient = - new Azure.AI.Inference.ChatCompletionsClient( - new("https://models.inference.ai.azure.com"), - new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!)) - .AsIChatClient("gpt-4o-mini"); - -IChatClient client = new ChatClientBuilder(azureClient) - .UseDistributedCache(cache) - .Build(); - -for (int i = 0; i < 3; i++) -{ - await foreach (var message in client.GetStreamingResponseAsync("In less than 100 words, what is AI?")) - { - Console.Write(message); - } - - Console.WriteLine(); - Console.WriteLine(); -} -``` - -### Telemetry - -```csharp -using Azure; -using Microsoft.Extensions.AI; -using OpenTelemetry.Trace; - -// Configure OpenTelemetry exporter -var sourceName = Guid.NewGuid().ToString(); -var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() - .AddSource(sourceName) - .AddConsoleExporter() - .Build(); - -IChatClient azureClient = - new Azure.AI.Inference.ChatCompletionsClient( - new("https://models.inference.ai.azure.com"), - new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!)) - .AsIChatClient("gpt-4o-mini"); - -IChatClient client = new ChatClientBuilder(azureClient) - .UseOpenTelemetry(sourceName: sourceName, configure: c => c.EnableSensitiveData = true) - .Build(); - -Console.WriteLine(await client.GetResponseAsync("What is AI?")); -``` - -### Telemetry, Caching, and Tool Calling - -```csharp -using System.ComponentModel; -using Azure; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using OpenTelemetry.Trace; - -// Configure telemetry -var sourceName = Guid.NewGuid().ToString(); -var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() - .AddSource(sourceName) - .AddConsoleExporter() - .Build(); - -// Configure caching -IDistributedCache cache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); - -// Configure tool calling -var chatOptions = new ChatOptions -{ - Tools = [AIFunctionFactory.Create(GetPersonAge)] -}; - -IChatClient azureClient = - new Azure.AI.Inference.ChatCompletionsClient( - new("https://models.inference.ai.azure.com"), - new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!)) - .AsIChatClient("gpt-4o-mini"); - -IChatClient client = new ChatClientBuilder(azureClient) - .UseDistributedCache(cache) - .UseFunctionInvocation() - .UseOpenTelemetry(sourceName: sourceName, configure: c => c.EnableSensitiveData = true) - .Build(); - -for (int i = 0; i < 3; i++) -{ - Console.WriteLine(await client.GetResponseAsync("How much older is Alice than Bob?", chatOptions)); -} - -[Description("Gets the age of a person specified by name.")] -static int GetPersonAge(string personName) => - personName switch - { - "Alice" => 42, - "Bob" => 35, - _ => 26, - }; -``` - -### Dependency Injection - -```csharp -using Azure; -using Azure.AI.Inference; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -// App Setup -var builder = Host.CreateApplicationBuilder(); -builder.Services.AddSingleton( - new ChatCompletionsClient( - new("https://models.inference.ai.azure.com"), - new AzureKeyCredential(Environment.GetEnvironmentVariable("GH_TOKEN")!))); -builder.Services.AddDistributedMemoryCache(); -builder.Services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Trace)); - -builder.Services.AddChatClient(services => services.GetRequiredService().AsIChatClient("gpt-4o-mini")) - .UseDistributedCache() - .UseLogging(); - -var app = builder.Build(); - -// Elsewhere in the app -var chatClient = app.Services.GetRequiredService(); -Console.WriteLine(await chatClient.GetResponseAsync("What is AI?")); -``` - -### Minimal Web API - -```csharp -using Azure; -using Azure.AI.Inference; -using Microsoft.Extensions.AI; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddSingleton(new ChatCompletionsClient( - new("https://models.inference.ai.azure.com"), - new AzureKeyCredential(builder.Configuration["GH_TOKEN"]!))); - -builder.Services.AddChatClient(services => - services.GetRequiredService().AsIChatClient("gpt-4o-mini")); - -var app = builder.Build(); - -app.MapPost("/chat", async (IChatClient client, string message) => -{ - var response = await client.GetResponseAsync(message); - return response.Message; -}); - -app.Run(); -``` - -## Documentation - -Refer to the [Microsoft.Extensions.AI libraries documentation](https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai) for more information and API usage examples. - -## Feedback & Contributing - -We welcome feedback and contributions in [our GitHub repo](https://github.com/dotnet/extensions). diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientIntegrationTests.cs deleted file mode 100644 index a5f78eef135..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientIntegrationTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI; - -public class AzureAIInferenceChatClientIntegrationTests : ChatClientIntegrationTests -{ - protected override IChatClient? CreateChatClient() => - IntegrationTestHelpers.GetChatCompletionsClient() - ?.AsIChatClient(TestRunnerConfiguration.Instance["AzureAIInference:ChatModel"] ?? "gpt-4o-mini"); -} diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs deleted file mode 100644 index 1431f5096f5..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceChatClientTests.cs +++ /dev/null @@ -1,1370 +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.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Azure; -using Azure.AI.Inference; -using Azure.Core.Pipeline; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Xunit; - -#pragma warning disable S103 // Lines should not be too long -#pragma warning disable S3358 // Ternary operators should not be nested -#pragma warning disable SA1204 // Static elements should appear before instance elements - -namespace Microsoft.Extensions.AI; - -public class AzureAIInferenceChatClientTests -{ - [Fact] - public void AsIChatClient_InvalidArgs_Throws() - { - Assert.Throws("chatCompletionsClient", () => ((ChatCompletionsClient)null!).AsIChatClient("model")); - - ChatCompletionsClient client = new(new("http://somewhere"), new AzureKeyCredential("key")); - Assert.Throws("defaultModelId", () => client.AsIChatClient(" ")); - } - - [Fact] - public async Task NullModel_Throws() - { - ChatCompletionsClient client = new(new("http://localhost/some/endpoint"), new AzureKeyCredential("key")); - IChatClient chatClient = client.AsIChatClient(modelId: null); - - await Assert.ThrowsAsync(() => chatClient.GetResponseAsync("hello")); - await Assert.ThrowsAsync(() => chatClient.GetStreamingResponseAsync("hello").GetAsyncEnumerator().MoveNextAsync().AsTask()); - - await Assert.ThrowsAsync(() => chatClient.GetResponseAsync("hello", new ChatOptions { ModelId = null })); - await Assert.ThrowsAsync(() => chatClient.GetStreamingResponseAsync("hello", new ChatOptions { ModelId = null }).GetAsyncEnumerator().MoveNextAsync().AsTask()); - } - - [Fact] - public void AsIChatClient_ProducesExpectedMetadata() - { - Uri endpoint = new("http://localhost/some/endpoint"); - string model = "amazingModel"; - - ChatCompletionsClient client = new(endpoint, new AzureKeyCredential("key")); - - IChatClient chatClient = client.AsIChatClient(model); - var metadata = chatClient.GetService(); - Assert.Equal("azure.ai.inference", metadata?.ProviderName); - Assert.Equal(endpoint, metadata?.ProviderUri); - Assert.Equal(model, metadata?.DefaultModelId); - } - - [Fact] - public void GetService_SuccessfullyReturnsUnderlyingClient() - { - ChatCompletionsClient client = new(new("http://localhost"), new AzureKeyCredential("key")); - IChatClient chatClient = client.AsIChatClient("model"); - - Assert.Same(chatClient, chatClient.GetService()); - Assert.Same(client, chatClient.GetService()); - - using IChatClient pipeline = chatClient - .AsBuilder() - .UseFunctionInvocation() - .UseOpenTelemetry() - .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) - .Build(); - - Assert.NotNull(pipeline.GetService()); - Assert.NotNull(pipeline.GetService()); - Assert.NotNull(pipeline.GetService()); - Assert.NotNull(pipeline.GetService()); - Assert.NotNull(pipeline.GetService()); - - Assert.Same(client, pipeline.GetService()); - Assert.IsType(pipeline.GetService()); - - Assert.Null(pipeline.GetService("key")); - Assert.Null(pipeline.GetService("key")); - } - - private const string BasicInputNonStreaming = """ - { - "messages": [{"role":"user", "content":"hello"}], - "max_tokens":10, - "temperature":0.5, - "model":"gpt-4o-mini" - } - """; - - private const string BasicOutputNonStreaming = """ - { - "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", - "object": "chat.completion", - "created": 1727888631, - "model": "gpt-4o-mini-2024-07-18", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "Hello! How can I assist you today?", - "refusal": null - }, - "logprobs": null, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 8, - "completion_tokens": 9, - "total_tokens": 17, - "prompt_tokens_details": { - "cached_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0 - } - }, - "system_fingerprint": "fp_f85bea6784" - } - """; - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task BasicRequestResponse_NonStreaming(bool multiContent) - { - using VerbatimHttpHandler handler = new(BasicInputNonStreaming, BasicOutputNonStreaming); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - List messages = multiContent ? - [new ChatMessage(ChatRole.User, "hello".Select(c => (AIContent)new TextContent(c.ToString())).ToList())] : - [new ChatMessage(ChatRole.User, "hello")]; - - var response = await client.GetResponseAsync(messages, new() - { - MaxOutputTokens = 10, - Temperature = 0.5f, - }); - Assert.NotNull(response); - - Assert.Equal("chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", response.ResponseId); - Assert.Equal("Hello! How can I assist you today?", response.Text); - Assert.Single(response.Messages.Single().Contents); - Assert.Equal(ChatRole.Assistant, response.Messages.Single().Role); - Assert.Equal("chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", response.Messages.Single().MessageId); - Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId); - Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_727_888_631), response.CreatedAt); - Assert.Equal(ChatFinishReason.Stop, response.FinishReason); - - Assert.NotNull(response.Usage); - Assert.Equal(8, response.Usage.InputTokenCount); - Assert.Equal(9, response.Usage.OutputTokenCount); - Assert.Equal(17, response.Usage.TotalTokenCount); - } - - private const string BasicInputStreaming = """ - { - "messages": [{"role":"user", "content":"hello"}], - "max_tokens":20, - "temperature":0.5, - "stream":true, - "model":"gpt-4o-mini"} - """; - - private const string BasicOutputStreaming = """ - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":"Hello"},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":"!"},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" How"},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" I"},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" assist"},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":" today"},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null} - - data: {"id":"chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK","object":"chat.completion.chunk","created":1727889370,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[],"usage":{"prompt_tokens":8,"completion_tokens":9,"total_tokens":17,"prompt_tokens_details":{"cached_tokens":0},"completion_tokens_details":{"reasoning_tokens":0}}} - - data: [DONE] - - """; - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task BasicRequestResponse_Streaming(bool multiContent) - { - using VerbatimHttpHandler handler = new(BasicInputStreaming, BasicOutputStreaming); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - List messages = multiContent ? - [new ChatMessage(ChatRole.User, "hello".Select(c => (AIContent)new TextContent(c.ToString())).ToList())] : - [new ChatMessage(ChatRole.User, "hello")]; - - List updates = []; - await foreach (var update in client.GetStreamingResponseAsync(messages, new() - { - MaxOutputTokens = 20, - Temperature = 0.5f, - })) - { - updates.Add(update); - } - - Assert.Equal("Hello! How can I assist you today?", string.Concat(updates.Select(u => u.Text))); - - var createdAt = DateTimeOffset.FromUnixTimeSeconds(1_727_889_370); - Assert.Equal(12, updates.Count); - for (int i = 0; i < updates.Count; i++) - { - Assert.Equal("chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK", updates[i].ResponseId); - Assert.Equal("chatcmpl-ADxFKtX6xIwdWRN42QvBj2u1RZpCK", updates[i].MessageId); - Assert.Equal(createdAt, updates[i].CreatedAt); - Assert.Equal("gpt-4o-mini-2024-07-18", updates[i].ModelId); - Assert.Equal(ChatRole.Assistant, updates[i].Role); - Assert.Equal(i is < 10 or 11 ? 1 : 0, updates[i].Contents.Count); - Assert.Equal(i < 10 ? null : ChatFinishReason.Stop, updates[i].FinishReason); - } - } - - [Fact] - public async Task IChatClient_WithNullModel_ChatOptions_WithNotNullModel_NonStreaming() - { - using VerbatimHttpHandler handler = new(BasicInputNonStreaming, BasicOutputNonStreaming); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, modelId: null!); - - var response = await client.GetResponseAsync("hello", new ChatOptions - { - ModelId = "gpt-4o-mini", - MaxOutputTokens = 10, - Temperature = 0.5f, - }); - Assert.NotNull(response); - Assert.Equal("Hello! How can I assist you today?", response.Text); - } - - [Fact] - public async Task IChatClient_WithNullModel_ChatOptions_WithNotNullModel_Streaming() - { - using VerbatimHttpHandler handler = new(BasicInputStreaming, BasicOutputStreaming); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, modelId: null!); - - string responseText = string.Empty; - await foreach (var update in client.GetStreamingResponseAsync("hello", new ChatOptions - { - ModelId = "gpt-4o-mini", - MaxOutputTokens = 20, - Temperature = 0.5f, - })) - { - responseText += update.Text; - } - - Assert.Equal("Hello! How can I assist you today?", responseText); - } - - [Fact] - public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation_NonStreaming() - { - const string Input = """ - { - "messages":[{"role":"user","content":"hello"}], - "model":"gpt-4o-mini", - "frequency_penalty":0.75, - "max_tokens":10, - "top_p":0.5, - "presence_penalty":0.5, - "temperature":0.5, - "seed":42, - "stop":["hello","world"], - "response_format":{"type":"text"}, - "tools":[ - {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}}, - {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type": "object","required": ["personName"],"properties": {"personName": {"description": "The person whose age is being requested","type": "string"}}}}} - ], - "tool_choice":"auto", - "additional_property_from_raw_representation":42, - "additional_property_from_MEAI_options":42 - } - """; - - const string Output = """ - { - "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", - "object": "chat.completion", - "choices": [ - { - "message": { - "role": "assistant", - "content": "Hello! How can I assist you today?" - } - } - ] - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, modelId: null!); - AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); - - ChatOptions chatOptions = new ChatOptions - { - RawRepresentationFactory = (c) => - { - ChatCompletionsOptions azureAIOptions = new() - { - Model = "gpt-4o-mini", - FrequencyPenalty = 0.75f, - MaxTokens = 10, - NucleusSamplingFactor = 0.5f, - PresencePenalty = 0.5f, - Temperature = 0.5f, - Seed = 42, - ToolChoice = ChatCompletionsToolChoice.Auto, - ResponseFormat = ChatCompletionsResponseFormat.CreateTextFormat() - }; - azureAIOptions.StopSequences.Add("hello"); - azureAIOptions.Tools.Add(ToAzureAIChatTool(tool)); - azureAIOptions.AdditionalProperties["additional_property_from_raw_representation"] = new BinaryData("42"); - return azureAIOptions; - }, - ModelId = null, - FrequencyPenalty = 0.125f, - MaxOutputTokens = 1, - TopP = 0.125f, - PresencePenalty = 0.125f, - Temperature = 0.125f, - Seed = 1, - StopSequences = ["world"], - Tools = [tool], - ToolMode = ChatToolMode.None, - ResponseFormat = ChatResponseFormat.Json, - AdditionalProperties = new AdditionalPropertiesDictionary - { - ["additional_property_from_MEAI_options"] = 42 - } - }; - - var response = await client.GetResponseAsync("hello", chatOptions); - Assert.NotNull(response); - Assert.Equal("Hello! How can I assist you today?", response.Text); - } - - [Fact] - public async Task ChatOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation_Streaming() - { - const string Input = """ - { - "messages":[{"role":"user","content":"hello"}], - "model":"gpt-4o-mini", - "frequency_penalty":0.75, - "max_tokens":10, - "top_p":0.5, - "presence_penalty":0.5, - "temperature":0.5, - "seed":42, - "stop":["hello","world"], - "response_format":{"type":"text"}, - "tools":[ - {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}}, - {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type": "object","required": ["personName"],"properties": {"personName": {"description": "The person whose age is being requested","type": "string"}}}}} - ], - "tool_choice":"auto", - "additional_property_from_raw_representation":42, - "additional_property_from_MEAI_options":42, - "stream":true - } - """; - - const string Output = """ - data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant","content":"Hello! "}}]} - - data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{"content":"How can I assist you today?"}}]} - - data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop"}]} - - data: [DONE] - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, modelId: null!); - AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); - - ChatOptions chatOptions = new ChatOptions - { - RawRepresentationFactory = (c) => - { - ChatCompletionsOptions azureAIOptions = new() - { - Model = "gpt-4o-mini", - FrequencyPenalty = 0.75f, - MaxTokens = 10, - NucleusSamplingFactor = 0.5f, - PresencePenalty = 0.5f, - Temperature = 0.5f, - Seed = 42, - ToolChoice = ChatCompletionsToolChoice.Auto, - ResponseFormat = ChatCompletionsResponseFormat.CreateTextFormat() - }; - azureAIOptions.StopSequences.Add("hello"); - azureAIOptions.Tools.Add(ToAzureAIChatTool(tool)); - azureAIOptions.AdditionalProperties["additional_property_from_raw_representation"] = new BinaryData("42"); - return azureAIOptions; - }, - ModelId = null, - FrequencyPenalty = 0.125f, - MaxOutputTokens = 1, - TopP = 0.125f, - PresencePenalty = 0.125f, - Temperature = 0.125f, - Seed = 1, - StopSequences = ["world"], - Tools = [tool], - ToolMode = ChatToolMode.None, - ResponseFormat = ChatResponseFormat.Json, - AdditionalProperties = new AdditionalPropertiesDictionary - { - ["additional_property_from_MEAI_options"] = 42 - } - }; - - string responseText = string.Empty; - await foreach (var update in client.GetStreamingResponseAsync("hello", chatOptions)) - { - responseText += update.Text; - } - - Assert.Equal("Hello! How can I assist you today?", responseText); - } - - [Fact] - public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_NonStreaming() - { - const string Input = """ - { - "messages":[{"role":"user","content":"hello"}], - "model":"gpt-4o-mini", - "frequency_penalty":0.125, - "max_tokens":1, - "top_p":0.125, - "presence_penalty":0.125, - "temperature":0.125, - "seed":1, - "stop":["world"], - "response_format":{"type":"json_object"}, - "tools":[ - {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}} - ], - "tool_choice":"none" - } - """; - - const string Output = """ - { - "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", - "object": "chat.completion", - "choices": [ - { - "message": { - "role": "assistant", - "content": "Hello! How can I assist you today?" - } - } - ] - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, modelId: null!); - AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); - - ChatOptions chatOptions = new ChatOptions - { - RawRepresentationFactory = (c) => - { - ChatCompletionsOptions azureAIOptions = new(); - Assert.Empty(azureAIOptions.Messages); - Assert.Null(azureAIOptions.Model); - Assert.Null(azureAIOptions.FrequencyPenalty); - Assert.Null(azureAIOptions.MaxTokens); - Assert.Null(azureAIOptions.NucleusSamplingFactor); - Assert.Null(azureAIOptions.PresencePenalty); - Assert.Null(azureAIOptions.Temperature); - Assert.Null(azureAIOptions.Seed); - Assert.Empty(azureAIOptions.StopSequences); - Assert.Empty(azureAIOptions.Tools); - Assert.Null(azureAIOptions.ToolChoice); - Assert.Null(azureAIOptions.ResponseFormat); - return azureAIOptions; - }, - ModelId = "gpt-4o-mini", - FrequencyPenalty = 0.125f, - MaxOutputTokens = 1, - TopP = 0.125f, - PresencePenalty = 0.125f, - Temperature = 0.125f, - Seed = 1, - StopSequences = ["world"], - Tools = [tool], - ToolMode = ChatToolMode.None, - ResponseFormat = ChatResponseFormat.Json - }; - - var response = await client.GetResponseAsync("hello", chatOptions); - Assert.NotNull(response); - Assert.Equal("Hello! How can I assist you today?", response.Text); - } - - [Fact] - public async Task ChatOptions_Overwrite_NullPropertiesInRawRepresentation_Streaming() - { - const string Input = """ - { - "messages":[{"role":"user","content":"hello"}], - "model":"gpt-4o-mini", - "frequency_penalty":0.125, - "max_tokens":1, - "top_p":0.125, - "presence_penalty":0.125, - "temperature":0.125, - "seed":1, - "stop":["world"], - "response_format":{"type":"json_object"}, - "tools":[ - {"type":"function","function":{"name":"GetPersonAge","description":"Gets the age of the specified person.","parameters":{"type":"object","required":["personName"],"properties":{"personName":{"description":"The person whose age is being requested","type":"string"}}}}} - ], - "tool_choice":"none", - "stream":true - } - """; - - const string Output = """ - data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant","content":"Hello! "}}]} - - data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{"content":"How can I assist you today?"}}]} - - data: {"id":"chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop"}]} - - data: [DONE] - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, modelId: null!); - AIFunction tool = AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person."); - - ChatOptions chatOptions = new ChatOptions - { - RawRepresentationFactory = (c) => - { - ChatCompletionsOptions azureAIOptions = new(); - Assert.Empty(azureAIOptions.Messages); - Assert.Null(azureAIOptions.Model); - Assert.Null(azureAIOptions.FrequencyPenalty); - Assert.Null(azureAIOptions.MaxTokens); - Assert.Null(azureAIOptions.NucleusSamplingFactor); - Assert.Null(azureAIOptions.PresencePenalty); - Assert.Null(azureAIOptions.Temperature); - Assert.Null(azureAIOptions.Seed); - Assert.Empty(azureAIOptions.StopSequences); - Assert.Empty(azureAIOptions.Tools); - Assert.Null(azureAIOptions.ToolChoice); - Assert.Null(azureAIOptions.ResponseFormat); - return azureAIOptions; - }, - ModelId = "gpt-4o-mini", - FrequencyPenalty = 0.125f, - MaxOutputTokens = 1, - TopP = 0.125f, - PresencePenalty = 0.125f, - Temperature = 0.125f, - Seed = 1, - StopSequences = ["world"], - Tools = [tool], - ToolMode = ChatToolMode.None, - ResponseFormat = ChatResponseFormat.Json - }; - - string responseText = string.Empty; - await foreach (var update in client.GetStreamingResponseAsync("hello", chatOptions)) - { - responseText += update.Text; - } - - Assert.Equal("Hello! How can I assist you today?", responseText); - } - - /// Converts an Extensions function to an AzureAI chat tool. - private static ChatCompletionsToolDefinition ToAzureAIChatTool(AIFunction aiFunction) - { - // Map to an intermediate model so that redundant properties are skipped. - var tool = JsonSerializer.Deserialize(aiFunction.JsonSchema)!; - var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool)); - return new(new FunctionDefinition(aiFunction.Name) - { - Description = aiFunction.Description, - Parameters = functionParameters, - }); - } - - /// Used to create the JSON payload for an AzureAI chat tool description. - private sealed class AzureAIChatToolJson - { - [JsonPropertyName("type")] - public string Type { get; set; } = "object"; - - [JsonPropertyName("required")] - public List Required { get; set; } = []; - - [JsonPropertyName("properties")] - public Dictionary Properties { get; set; } = []; - } - - [Fact] - public async Task AdditionalOptions_NonStreaming() - { - const string Input = """ - { - "messages":[{"role":"user", "content":"hello"}], - "max_tokens":10, - "temperature":0.5, - "top_p":0.5, - "stop":["yes","no"], - "presence_penalty":0.5, - "frequency_penalty":0.75, - "seed":42, - "model":"gpt-4o-mini", - "top_k":40, - "something_else":"value1", - "and_something_further":123 - } - """; - - const string Output = """ - { - "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", - "object": "chat.completion", - "choices": [ - { - "message": { - "role": "assistant", - "content": "Hello! How can I assist you today?" - } - } - ] - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - Assert.NotNull(await client.GetResponseAsync("hello", new() - { - MaxOutputTokens = 10, - Temperature = 0.5f, - TopP = 0.5f, - TopK = 40, - FrequencyPenalty = 0.75f, - PresencePenalty = 0.5f, - Seed = 42, - StopSequences = ["yes", "no"], - RawRepresentationFactory = (c) => - { - ChatCompletionsOptions azureAIOptions = new(); - azureAIOptions.AdditionalProperties.Add("something_else", new BinaryData(JsonSerializer.SerializeToUtf8Bytes("value1", typeof(object)))); - azureAIOptions.AdditionalProperties.Add("and_something_further", new BinaryData(JsonSerializer.SerializeToUtf8Bytes(123, typeof(object)))); - return azureAIOptions; - }, - })); - } - - [Fact] - public async Task TopK_DoNotOverwrite_NonStreaming() - { - const string Input = """ - { - "messages":[{"role":"user", "content":"hello"}], - "max_tokens":10, - "temperature":0.5, - "top_p":0.5, - "stop":["yes","no"], - "presence_penalty":0.5, - "frequency_penalty":0.75, - "seed":42, - "model":"gpt-4o-mini", - "top_k":40, - "something_else":"value1", - "and_something_further":123 - } - """; - - const string Output = """ - { - "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", - "object": "chat.completion", - "choices": [ - { - "message": { - "role": "assistant", - "content": "Hello! How can I assist you today?" - } - } - ] - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - Assert.NotNull(await client.GetResponseAsync("hello", new() - { - MaxOutputTokens = 10, - Temperature = 0.5f, - TopP = 0.5f, - TopK = 20, // will be ignored because the raw representation already specifies it. - FrequencyPenalty = 0.75f, - PresencePenalty = 0.5f, - Seed = 42, - StopSequences = ["yes", "no"], - RawRepresentationFactory = (c) => - { - ChatCompletionsOptions azureAIOptions = new(); - azureAIOptions.AdditionalProperties.Add("top_k", new BinaryData(JsonSerializer.SerializeToUtf8Bytes(40, typeof(object)))); - azureAIOptions.AdditionalProperties.Add("something_else", new BinaryData(JsonSerializer.SerializeToUtf8Bytes("value1", typeof(object)))); - azureAIOptions.AdditionalProperties.Add("and_something_further", new BinaryData(JsonSerializer.SerializeToUtf8Bytes(123, typeof(object)))); - return azureAIOptions; - }, - })); - } - - [Fact] - public async Task ResponseFormat_Text_NonStreaming() - { - const string Input = """ - { - "messages":[{"role":"user", "content":"hello"}], - "model":"gpt-4o-mini", - "response_format":{"type":"text"} - } - """; - - const string Output = """ - { - "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", - "object": "chat.completion", - "choices": [ - { - "message": { - "role": "assistant", - "content": "Hello! How can I assist you today?" - } - } - ] - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - Assert.NotNull(await client.GetResponseAsync("hello", new() - { - ResponseFormat = ChatResponseFormat.Text, - })); - } - - [Fact] - public async Task ResponseFormat_Json_NonStreaming() - { - const string Input = """ - { - "messages":[{"role":"user", "content":"hello"}], - "model":"gpt-4o-mini", - "response_format":{"type":"json_object"} - } - """; - - const string Output = """ - { - "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", - "object": "chat.completion", - "choices": [ - { - "message": { - "role": "assistant", - "content": "Hello! How can I assist you today?" - } - } - ] - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - Assert.NotNull(await client.GetResponseAsync("hello", new() - { - ResponseFormat = ChatResponseFormat.Json, - })); - } - - [Fact] - public async Task ResponseFormat_JsonSchema_NonStreaming() - { - const string Input = """ - { - "messages":[{"role":"user", "content":"hello"}], - "model":"gpt-4o-mini", - "response_format": - { - "type":"json_schema", - "json_schema": - { - "name": "DescribedObject", - "schema": - { - "type":"object", - "properties": - { - "description": - { - "type":"string" - } - }, - "required":["description"], - "additionalProperties":false - }, - "description":"An object with a description" - } - } - } - """; - - const string Output = """ - { - "id": "chatcmpl-ADx3PvAnCwJg0woha4pYsBTi3ZpOI", - "object": "chat.completion", - "choices": [ - { - "message": { - "role": "assistant", - "content": "Hello! How can I assist you today?" - } - } - ] - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - Assert.NotNull(await client.GetResponseAsync("hello", new() - { - ResponseFormat = ChatResponseFormat.ForJsonSchema(JsonElement.Parse(""" - { - "type": "object", - "properties": { - "description": { - "type": "string" - } - }, - "required": ["description"] - } - """), "DescribedObject", "An object with a description"), - })); - } - - [Fact] - public async Task MultipleMessages_NonStreaming() - { - const string Input = """ - { - "messages": [ - { - "role": "system", - "content": "You are a really nice friend." - }, - { - "role": "user", - "content": "hello!" - }, - { - "role": "assistant", - "content": "hi, how are you?" - }, - { - "role": "user", - "content": "i\u0027m good. how are you?" - }, - { - "role": "assistant", - "content": "", - "tool_calls": [{"id":"abcd123","type":"function","function":{"name":"GetMood","arguments":"null"}}] - }, - { - "role": "tool", - "content": "happy", - "tool_call_id": "abcd123" - } - ], - "temperature": 0.25, - "stop": [ - "great" - ], - "presence_penalty": 0.5, - "frequency_penalty": 0.75, - "seed": 42, - "model": "gpt-4o-mini" - } - """; - - const string Output = """ - { - "id": "chatcmpl-ADyV17bXeSm5rzUx3n46O7m3M0o3P", - "object": "chat.completion", - "created": 1727894187, - "model": "gpt-4o-mini-2024-07-18", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "I’m doing well, thank you! What’s on your mind today?", - "refusal": null - }, - "logprobs": null, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 42, - "completion_tokens": 15, - "total_tokens": 57, - "prompt_tokens_details": { - "cached_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0 - } - }, - "system_fingerprint": "fp_f85bea6784" - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - List messages = - [ - new(ChatRole.System, "You are a really nice friend."), - new(ChatRole.User, "hello!"), - new(ChatRole.Assistant, "hi, how are you?"), - new(ChatRole.User, "i'm good. how are you?"), - new(ChatRole.Assistant, [new FunctionCallContent("abcd123", "GetMood")]), - new(ChatRole.Tool, [new FunctionResultContent("abcd123", "happy")]), - ]; - - var response = await client.GetResponseAsync(messages, new() - { - Temperature = 0.25f, - FrequencyPenalty = 0.75f, - PresencePenalty = 0.5f, - StopSequences = ["great"], - Seed = 42, - }); - Assert.NotNull(response); - - Assert.Equal("chatcmpl-ADyV17bXeSm5rzUx3n46O7m3M0o3P", response.ResponseId); - Assert.Equal("I’m doing well, thank you! What’s on your mind today?", response.Text); - Assert.Single(response.Messages.Single().Contents); - Assert.Equal(ChatRole.Assistant, response.Messages.Single().Role); - Assert.Equal("chatcmpl-ADyV17bXeSm5rzUx3n46O7m3M0o3P", response.Messages.Single().MessageId); - Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId); - Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_727_894_187), response.CreatedAt); - Assert.Equal(ChatFinishReason.Stop, response.FinishReason); - - Assert.NotNull(response.Usage); - Assert.Equal(42, response.Usage.InputTokenCount); - Assert.Equal(15, response.Usage.OutputTokenCount); - Assert.Equal(57, response.Usage.TotalTokenCount); - } - - [Fact] - public async Task MultipleContent_NonStreaming() - { - const string Input = """ - { - "messages": - [ - { - "role": "user", - "content": - [ - { - "type": "text", - "text": "Describe this picture." - }, - { - "type": "image_url", - "image_url": - { - "url": "http://dot.net/someimage.png" - } - } - ] - } - ], - "model": "gpt-4o-mini" - } - """; - - const string Output = """ - { - "id": "chatcmpl-ADyV17bXeSm5rzUx3n46O7m3M0o3P", - "object": "chat.completion", - "choices": [ - { - "message": { - "role": "assistant", - "content": "A picture of a dog." - } - } - ] - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - Assert.NotNull(await client.GetResponseAsync([new(ChatRole.User, - [ - new TextContent("Describe this picture."), - new UriContent("http://dot.net/someimage.png", mediaType: "image/*"), - ])])); - } - - [Fact] - public async Task NullAssistantText_ContentEmpty_NonStreaming() - { - const string Input = """ - { - "messages": [ - { - "role": "assistant", - "content": "" - }, - { - "role": "user", - "content": "hello!" - } - ], - "model": "gpt-4o-mini" - } - """; - - const string Output = """ - { - "id": "chatcmpl-ADyV17bXeSm5rzUx3n46O7m3M0o3P", - "object": "chat.completion", - "created": 1727894187, - "model": "gpt-4o-mini-2024-07-18", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "Hello.", - "refusal": null - }, - "logprobs": null, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 42, - "completion_tokens": 15, - "total_tokens": 57, - "prompt_tokens_details": { - "cached_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0 - } - }, - "system_fingerprint": "fp_f85bea6784" - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - List messages = - [ - new(ChatRole.Assistant, (string?)null), - new(ChatRole.User, "hello!"), - ]; - - var response = await client.GetResponseAsync(messages); - Assert.NotNull(response); - - Assert.Equal("chatcmpl-ADyV17bXeSm5rzUx3n46O7m3M0o3P", response.ResponseId); - Assert.Equal("Hello.", response.Text); - Assert.Single(response.Messages.Single().Contents); - Assert.Equal(ChatRole.Assistant, response.Messages.Single().Role); - Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId); - Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_727_894_187), response.CreatedAt); - Assert.Equal(ChatFinishReason.Stop, response.FinishReason); - - Assert.NotNull(response.Usage); - Assert.Equal(42, response.Usage.InputTokenCount); - Assert.Equal(15, response.Usage.OutputTokenCount); - Assert.Equal(57, response.Usage.TotalTokenCount); - } - - public static IEnumerable FunctionCallContent_NonStreaming_MemberData() - { - yield return [ChatToolMode.Auto]; - yield return [ChatToolMode.None]; - yield return [ChatToolMode.RequireAny]; - yield return [ChatToolMode.RequireSpecific("GetPersonAge")]; - } - - [Theory] - [MemberData(nameof(FunctionCallContent_NonStreaming_MemberData))] - public async Task FunctionCallContent_NonStreaming(ChatToolMode mode) - { - string input = $$""" - { - "messages": [ - { - "role": "user", - "content": "How old is Alice?" - } - ], - "model": "gpt-4o-mini", - "tools": [ - { - "type": "function", - "function": { - "name": "GetPersonAge", - "description": "Gets the age of the specified person.", - "parameters": { - "type": "object", - "required": ["personName"], - "properties": { - "personName": { - "description": "The person whose age is being requested", - "type": "string" - } - } - } - } - } - ], - "tool_choice": {{( - mode is NoneChatToolMode ? "\"none\"" : - mode is AutoChatToolMode ? "\"auto\"" : - mode is RequiredChatToolMode { RequiredFunctionName: not null } f ? "{\"type\":\"function\",\"function\":{\"name\":\"GetPersonAge\"}}" : - "\"required\"" - )}} - } - """; - - const string Output = """ - { - "id": "chatcmpl-ADydKhrSKEBWJ8gy0KCIU74rN3Hmk", - "object": "chat.completion", - "created": 1727894702, - "model": "gpt-4o-mini-2024-07-18", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": null, - "tool_calls": [ - { - "id": "call_8qbINM045wlmKZt9bVJgwAym", - "type": "function", - "function": { - "name": "GetPersonAge", - "arguments": "{\"personName\":\"Alice\"}" - } - } - ], - "refusal": null - }, - "logprobs": null, - "finish_reason": "tool_calls" - } - ], - "usage": { - "prompt_tokens": 61, - "completion_tokens": 16, - "total_tokens": 77, - "prompt_tokens_details": { - "cached_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0 - } - }, - "system_fingerprint": "fp_f85bea6784" - } - """; - - using VerbatimHttpHandler handler = new(input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - var response = await client.GetResponseAsync("How old is Alice?", new() - { - Tools = [AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person.")], - ToolMode = mode, - }); - Assert.NotNull(response); - - Assert.Empty(response.Text); - Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId); - Assert.Equal(ChatRole.Assistant, response.Messages.Single().Role); - Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_727_894_702), response.CreatedAt); - Assert.Equal(ChatFinishReason.ToolCalls, response.FinishReason); - Assert.NotNull(response.Usage); - Assert.Equal(61, response.Usage.InputTokenCount); - Assert.Equal(16, response.Usage.OutputTokenCount); - Assert.Equal(77, response.Usage.TotalTokenCount); - - Assert.Single(response.Messages.Single().Contents); - FunctionCallContent fcc = Assert.IsType(response.Messages.Single().Contents[0]); - Assert.Equal("GetPersonAge", fcc.Name); - AssertExtensions.EqualFunctionCallParameters(new Dictionary { ["personName"] = "Alice" }, fcc.Arguments); - } - - [Fact] - public async Task FunctionCallContent_Streaming() - { - const string Input = """ - { - "messages": [ - { - "role": "user", - "content": "How old is Alice?" - } - ], - "stream": true, - "model": "gpt-4o-mini", - "tools": [ - { - "type": "function", - "function": { - "name": "GetPersonAge", - "description": "Gets the age of the specified person.", - "parameters": { - "type": "object", - "required": ["personName"], - "properties": { - "personName": { - "description": "The person whose age is being requested", - "type": "string" - } - } - } - } - } - ], - "tool_choice": "auto" - } - """; - - const string Output = """ - data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","object":"chat.completion.chunk","created":1727895263,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_F9ZaqPWo69u0urxAhVt8meDW","type":"function","function":{"name":"GetPersonAge","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","object":"chat.completion.chunk","created":1727895263,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","object":"chat.completion.chunk","created":1727895263,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"person"}}]},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","object":"chat.completion.chunk","created":1727895263,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Name"}}]},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","object":"chat.completion.chunk","created":1727895263,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","object":"chat.completion.chunk","created":1727895263,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"Alice"}}]},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","object":"chat.completion.chunk","created":1727895263,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null} - - data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","object":"chat.completion.chunk","created":1727895263,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null} - - data: {"id":"chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl","object":"chat.completion.chunk","created":1727895263,"model":"gpt-4o-mini-2024-07-18","system_fingerprint":"fp_f85bea6784","choices":[],"usage":{"prompt_tokens":61,"completion_tokens":16,"total_tokens":77,"prompt_tokens_details":{"cached_tokens":0},"completion_tokens_details":{"reasoning_tokens":0}}} - - data: [DONE] - - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IChatClient client = CreateChatClient(httpClient, "gpt-4o-mini"); - - List updates = []; - await foreach (var update in client.GetStreamingResponseAsync("How old is Alice?", new() - { - Tools = [AIFunctionFactory.Create(([Description("The person whose age is being requested")] string personName) => 42, "GetPersonAge", "Gets the age of the specified person.")], - })) - { - updates.Add(update); - } - - Assert.Equal("", string.Concat(updates.Select(u => u.Text))); - - var createdAt = DateTimeOffset.FromUnixTimeSeconds(1_727_895_263); - Assert.Equal(10, updates.Count); - for (int i = 0; i < updates.Count; i++) - { - Assert.Equal("chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl", updates[i].ResponseId); - Assert.Equal("chatcmpl-ADymNiWWeqCJqHNFXiI1QtRcLuXcl", updates[i].MessageId); - Assert.Equal(createdAt, updates[i].CreatedAt); - Assert.Equal("gpt-4o-mini-2024-07-18", updates[i].ModelId); - Assert.Equal(ChatRole.Assistant, updates[i].Role); - Assert.Equal(i < 7 ? null : ChatFinishReason.ToolCalls, updates[i].FinishReason); - } - - FunctionCallContent fcc = Assert.IsType(Assert.Single(updates[updates.Count - 1].Contents)); - Assert.Equal("call_F9ZaqPWo69u0urxAhVt8meDW", fcc.CallId); - Assert.Equal("GetPersonAge", fcc.Name); - AssertExtensions.EqualFunctionCallParameters(new Dictionary { ["personName"] = "Alice" }, fcc.Arguments); - } - - private static IChatClient CreateChatClient(HttpClient httpClient, string modelId) => - new ChatCompletionsClient( - new("http://somewhere"), - new AzureKeyCredential("key"), - new AzureAIInferenceClientOptions { Transport = new HttpClientTransport(httpClient) }) - .AsIChatClient(modelId); -} diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorIntegrationTests.cs deleted file mode 100644 index 7895f3bc10a..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorIntegrationTests.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI; - -public class AzureAIInferenceEmbeddingGeneratorIntegrationTests : EmbeddingGeneratorIntegrationTests -{ - protected override IEmbeddingGenerator>? CreateEmbeddingGenerator() => - IntegrationTestHelpers.GetEmbeddingsClient() - ?.AsIEmbeddingGenerator(TestRunnerConfiguration.Instance["AzureAIInference:EmbeddingModel"] ?? "text-embedding-3-small"); -} diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs deleted file mode 100644 index 31e9980a330..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceEmbeddingGeneratorTests.cs +++ /dev/null @@ -1,188 +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.Net.Http; -using System.Threading.Tasks; -using Azure; -using Azure.AI.Inference; -using Azure.Core.Pipeline; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Xunit; - -#pragma warning disable S103 // Lines should not be too long - -namespace Microsoft.Extensions.AI; - -public class AzureAIInferenceEmbeddingGeneratorTests -{ - [Fact] - public void AsIEmbeddingGenerator_InvalidArgs_Throws() - { - Assert.Throws("embeddingsClient", () => ((EmbeddingsClient)null!).AsIEmbeddingGenerator()); - - EmbeddingsClient client = new(new("http://somewhere"), new AzureKeyCredential("key")); - Assert.Throws("defaultModelId", () => client.AsIEmbeddingGenerator(" ")); - - client.AsIEmbeddingGenerator(null); - } - - [Fact] - public void AsIEmbeddingGenerator_AzureAIClient_ProducesExpectedMetadata() - { - Uri endpoint = new("http://localhost/some/endpoint"); - string model = "amazingModel"; - - EmbeddingsClient client = new(endpoint, new AzureKeyCredential("key")); - - IEmbeddingGenerator> embeddingGenerator = client.AsIEmbeddingGenerator(model); - var metadata = embeddingGenerator.GetService(); - Assert.Equal("azure.ai.inference", metadata?.ProviderName); - Assert.Equal(endpoint, metadata?.ProviderUri); - Assert.Equal(model, metadata?.DefaultModelId); - } - - [Fact] - public void GetService_SuccessfullyReturnsUnderlyingClient() - { - var client = new EmbeddingsClient(new("http://somewhere"), new AzureKeyCredential("key")); - var embeddingGenerator = client.AsIEmbeddingGenerator("model"); - - Assert.Same(embeddingGenerator, embeddingGenerator.GetService>>()); - Assert.Same(client, embeddingGenerator.GetService()); - - using IEmbeddingGenerator> pipeline = embeddingGenerator - .AsBuilder() - .UseOpenTelemetry() - .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) - .Build(); - - Assert.NotNull(pipeline.GetService>>()); - Assert.NotNull(pipeline.GetService>>()); - Assert.NotNull(pipeline.GetService>>()); - - Assert.Same(client, pipeline.GetService()); - Assert.IsType>>(pipeline.GetService>>()); - } - - [Fact] - public async Task GenerateAsync_ExpectedRequestResponse() - { - const string Input = """ - {"input":["hello, world!","red, white, blue"],"encoding_format":"base64","model":"text-embedding-3-small"} - """; - - const string Output = """ - { - "object": "list", - "data": [ - { - "object": "embedding", - "index": 0, - "embedding": "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" - }, - { - "object": "embedding", - "index": 1, - "embedding": "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" - } - ], - "model": "text-embedding-3-small", - "usage": { - "prompt_tokens": 9, - "total_tokens": 9 - } - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IEmbeddingGenerator> generator = new EmbeddingsClient(new("http://somewhere"), new AzureKeyCredential("key"), new() - { - Transport = new HttpClientTransport(httpClient), - }).AsIEmbeddingGenerator("text-embedding-3-small"); - - var response = await generator.GenerateAsync([ - "hello, world!", - "red, white, blue", - ]); - Assert.NotNull(response); - Assert.Equal(2, response.Count); - - Assert.NotNull(response.Usage); - Assert.Equal(9, response.Usage.InputTokenCount); - Assert.Equal(9, response.Usage.TotalTokenCount); - - foreach (Embedding e in response) - { - Assert.Equal("text-embedding-3-small", e.ModelId); - Assert.NotNull(e.CreatedAt); - Assert.Equal(1536, e.Vector.Length); - Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); - } - } - - [Fact] - public async Task EmbeddingGenerationOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation() - { - const string Input = """ - { - "input":["hello, world!","red, white, blue"], - "dimensions":1536, - "encoding_format":"base64", - "model":"text-embedding-3-small" - } - """; - - const string Output = """ - { - "object": "list", - "data": [ - { - "object": "embedding", - "index": 0, - "embedding": "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" - }, - { - "object": "embedding", - "index": 1, - "embedding": "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" - } - ], - "model": "text-embedding-3-small" - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IEmbeddingGenerator> generator = new EmbeddingsClient(new("http://somewhere"), new AzureKeyCredential("key"), new() - { - Transport = new HttpClientTransport(httpClient), - }).AsIEmbeddingGenerator("text-embedding-3-large"); - - var response = await generator.GenerateAsync([ - "hello, world!", - "red, white, blue", - ], new EmbeddingGenerationOptions - { - Dimensions = 3072, - RawRepresentationFactory = (e) => new EmbeddingsOptions(input: []) - { - Dimensions = 1536, - Model = "text-embedding-3-small", - EncodingFormat = EmbeddingEncodingFormat.Single, // this will be overwritten, we only support base64. - } - }); - - Assert.NotNull(response); - - foreach (Embedding e in response) - { - Assert.Equal("text-embedding-3-small", e.ModelId); - Assert.NotNull(e.CreatedAt); - Assert.Equal(1536, e.Vector.Length); - Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageEmbeddingGeneratorTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageEmbeddingGeneratorTests.cs deleted file mode 100644 index 7ceefe947f3..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageEmbeddingGeneratorTests.cs +++ /dev/null @@ -1,195 +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.Net.Http; -using System.Threading.Tasks; -using Azure; -using Azure.AI.Inference; -using Azure.Core.Pipeline; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Xunit; - -#pragma warning disable S103 // Lines should not be too long - -namespace Microsoft.Extensions.AI; - -public class AzureAIInferenceImageEmbeddingGeneratorTests -{ - [Fact] - public void AsIEmbeddingGenerator_InvalidArgs_Throws() - { - Assert.Throws("imageEmbeddingsClient", () => ((ImageEmbeddingsClient)null!).AsIEmbeddingGenerator()); - - ImageEmbeddingsClient client = new(new("http://somewhere"), new AzureKeyCredential("key")); - Assert.Throws("defaultModelId", () => client.AsIEmbeddingGenerator(" ")); - - client.AsIEmbeddingGenerator(null); - } - - [Fact] - public void AsIEmbeddingGenerator_OpenAIClient_ProducesExpectedMetadata() - { - Uri endpoint = new("http://localhost/some/endpoint"); - string model = "amazingModel"; - - ImageEmbeddingsClient client = new(endpoint, new AzureKeyCredential("key")); - - IEmbeddingGenerator> embeddingGenerator = client.AsIEmbeddingGenerator(model); - var metadata = embeddingGenerator.GetService(); - Assert.Equal("azure.ai.inference", metadata?.ProviderName); - Assert.Equal(endpoint, metadata?.ProviderUri); - Assert.Equal(model, metadata?.DefaultModelId); - } - - [Fact] - public void GetService_SuccessfullyReturnsUnderlyingClient() - { - var client = new ImageEmbeddingsClient(new("http://somewhere"), new AzureKeyCredential("key")); - var embeddingGenerator = client.AsIEmbeddingGenerator("model"); - - Assert.Same(embeddingGenerator, embeddingGenerator.GetService>>()); - Assert.Same(client, embeddingGenerator.GetService()); - - using IEmbeddingGenerator> pipeline = embeddingGenerator - .AsBuilder() - .UseOpenTelemetry() - .UseDistributedCache(new MemoryDistributedCache(Options.Options.Create(new MemoryDistributedCacheOptions()))) - .Build(); - - Assert.NotNull(pipeline.GetService>>()); - Assert.NotNull(pipeline.GetService>>()); - Assert.NotNull(pipeline.GetService>>()); - - Assert.Same(client, pipeline.GetService()); - Assert.IsType>>(pipeline.GetService>>()); - } - - [Fact] - public async Task GenerateAsync_ExpectedRequestResponse() - { - DataContent dotnetPng = new(ImageDataUri.GetImageDataUri()); - - const string Input = """ - { - "input":[{"image":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcgAAAHICAMAAAD9f4rYAAAAS1BMVEVRK9RRK9T///\u002BolenTyvTp5fpnRdl8YN\u002B\u002Br\u002B708v1cONedh\u002Be\u002Bru5nRtl9YN6HbeKyouzJvfKSeuSzou2\u002Br\u002B9yU9ze1/dcONbe2PcNfWisAAAAAXRSTlP\u002BGuMHfQAAB79JREFUeNrs0QENAAAMw6Ddv\u002Bn7aMACOwomskFkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESKfnTvMTRyGoiisF5K2SYZhKKX7X\u002BpEeuov7Ngxorp\u002BOmcH9KssLnISJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMki/DzkNqUZr7H146M0ynYZnmgof4cn\u002B2BPpQA6rFQMymxDk/GalgMwmBDlcrRSQ2ZQgh79WCMhsUpDTYvsBmU0Kcvhn\u002BwGZTQuydLgCmU0MsjAmgcwmBlkYk0BmU4PcH5NAZlOD3D9cgcwmBzlcLB\u002BQ2fQg98YkkNn0IPfGJJDZBCF3xiSQ2RQhvy3XKyDnsboP\u002B\u002Bk6FpoT/wZjodWeSBEyPyZfATnaKxqHh072yiQhj4xJID1JyCN/XCA9TcgDYxJITxRyXqwyID1RyPoxCaSnClk9JoH0NCDH9jEJpKcBeR\u002BaPzeQngbk5do8JoH0NCA/35vHJJCeBuRqY0Ly0yoC0tOAPNm5dUwC6alA2q1xTALpaUBuYsvUNiaB9DQgP8w9Gq59AOnpQNq1aUwC6QlBnueWMQmkJwRpa8uYBNJTgrSx4doHkJ4UZMuYBNKTgkzeVvyy3YD0tCAbxiSQnhZkw5gE0hODtNvRMQmkpwa5zEOtiwekpwZpl4NjEkhPDvLomATS04M8z4fGJJCeHqSth95uBqQnCGnjkTEJpKcIeT8yJoH0FCEPjUkgPUnI5C91d0v2a08sf1p9QJp34JprM2S5dgcgf/qqHpNAeqKQS/W1DyA9Ucj6MQmkpwpZPSaB9GQhz3PdmATSk4W0U90zBEB6upD2XXW4AukJQ9aNSSA9YUi71YxJID1lyGWqGJNAesqQVYcrkJ40pF3LbzcD0tOGXMpjEkhPG9LW4pgE0hOHLP9S9zTkPNW1Wn1APnSeC28344aApw5pp8KYBNKTh7TCmATS04csjEkgPX1Iu\u002B2OSSC9DiCXae8ZAiC9DiDtsjcmgfR6gNwdk0B6XUDujUkgvS4gbc3/ZAak1wekjdkxCaTXCeQ9OyaB9DqBtFPuVdlAer1AZsckkF4vkPaeGZNAet1A2i09JoH0\u002BoHMXvu4A7nVD6RdMmPyDcitjiDTYxJIryfI85xkWIDc6gnS1vS1DyC3uoK0MTkmZyDN\u002BoJMj8kJSLO\u002BINNjcgTSrDPIZUpIfAFp1hlk8nDlaN3qDTL1KiW\u002BtW51B7nMQKbqDtJWIP\u002BzdwerDcNQEEUZWbIqG9XESev8/5d2EQol7wXcZBSwmLv3Zg54oYXkdTxIREE6HRCyFkHa2JDbfEohlHj5xINehsQgSBsXchtK\u002BC2tcHsdEt\u002BCNFEhx7Tj0XICZBakiQk53gvFCTYCJM5EyOv4nzbs6diQowW6wMaAnBIBsuGVEMeG3Hl9NQMSWZAmFmQO\u002Bx7WpUDiJMhbfEh/2hkmCmQtgkQbyOB2gokCiVmQQAvIHNwSTBxIREE2gVyCH0wkyCrIJpBrMLWFxCDIVr/W90JOSZANIMfgdoWJBYksSD6kx\u002BOft/IgcRZkA0h/owoTD3IqgqRD\u002BqteYCJCYhEkHdJdNVWYmJCIguRD2pXKF2xUyFoESYc0MyXXkQqJWZANILH\u002BNYoVfvNw34KnmwenCQ/Kw4vlvUt4n7aKDwms8aZYPjLU2\u002BJDAlte1jxCvbUbpOohQXaSIDtJkJ0kyE4SZCcJspME2UmC/GGPDmQAAAAABvlb36M9hRBHIo5EHIk4EnEk4kjEkYgjEUcijkQciTgScSTiSMSRiCMRRyKORByJOBJxJOJIxJGIIxFHIo5EHIk4EnEk4kjEkYgjEUciYo8OZAAAAAAG\u002BVvf4yuFRE6InBA5IXJC5ITICZETIidEToicEDkhckLkhMgJkRMiJ0ROiJwQOSFyQuSEyAmREyInRE6InBA5IXJC5ITICZETIidEToicEDkhckLkhMgJkRMiJ0ROiJwQOSFyQuSEyAmREyInRE6InBA5IXJC5ITICZETIidEToicEDkhckLkhMgJkRMiJ0ROiJwQWXt0QAMAAIAwyP6p7cFOBRBFIopEFIkoElEkokhEkYgiEUUiikQUiSgSUSSiSESRiCIRRSKKRBSJKBJRJKJIRJGIIhFFIopEFIkoElEkokhEkYgiEUUiikQUiSgSUSSiSESRiCIRRSKKRBSJKBJRJKJIRJGIIhFFIopEFIkoElEkokjEgjh2WnxgwCuWdQAAAABJRU5ErkJggg=="}], - "encoding_format":"base64", - "model":"embed-v-4-0" - } - """; - - const string Output = """ - { - "id":"9da7c0f0-4b9d-46d9-9323-f10f46977493", - "object":"list", - "data": - [ - { - "index":0, - "object":"embedding", - "embedding":"AADkPAAA6bsAAD68AAAnOwAAYbwAAFa8AABfvQAAqzsAAGy8AABwvAAAyDwAALo9AAC6vAAAID0AAGE8AAA2PAAA4TsAABU9AAC0PAAAqzwAADw8AAAaPQAA2LwAANa8AACoOgAA4DsAAA48AAC9PAAAdz0AAIC8AACGvAAA+7oAAEo7AABMuwAAab0AAFc9AAA2OwAAob0AAPO8AABGOwAAoLwAAKQ6AACHvAAAX70AAJQ8AAA/OwAAtbwAAME7AADMvAAA2DwAABQ9AABZPQAAd7wAAD+9AAAquwAAgTwAABE9AABFPQAALbwAAEk9AAC8ugAABj0AAI27AAAWPQAAMrwAAE88AABnvAAAZbwAAMK8AAAhPQAAPr0AAAg8AABcOgAA/jwAANE8AACvvAAAEbwAALy8AAAIvQAAe7wAAGW8AAAPPQAAFTsAALc5AACrOwAAirwAALa8AACLvAAACLsAABa7AAC+uAAAljwAAMS9AAC0vAAAhz0AAJw7AAAgvQAAxjwAAMK8AACMPAAAdz0AALC8AADIPAAArjsAAGG8AAATvAAAkrsAALs8AAAWPAAAxDwAADK9AAAAvQAAmTwAAIK9AADZPAAAmjwAABG9AAARuwAA/zwAAGO8AAC3PAAAGTwAACC8AAAtPAAAArwAAGG7AAC4PAAA/7sAAKG8AACdOwAA8DwAAJo7AAC8PAAAST0AAAI8AABnvAAAXTwAABc9AACSPAAAMjsAAPc7AABSvAAATLsAAKa8AAB1PAAAA70AAC87AAASvAAA/DwAADC7AABfvAAAYbwAAGW9AADlOwAANzwAAFc8AADEPAAAyrsAAMM8AAATPQAA3DwAABu8AAB+uQAAKj0AADS7AACkOwAAhD0AACK8AABIvQAAaboAALu8AADtOwAAoDwAAI88AACQPAAAmjwAAEy8AAC2OwAAtTwAAE68AACGvQAA0LsAAJM7AAAUvQAA17wAAEg9AABhOwAAVjwAALg8AACHvQAA5DwAACI9AACLPAAA4zsAAOk7AADOPAAA/7wAAPe8AAAGPQAAYTwAAEo9AAA/PAAA47wAAEq8AADgvAAAybsAAPk8AAA7vQAA3zsAAP87AAAUvAAAKjwAAKA8AAATvQAAcrwAAGm7AADlvAAAprwAAJM7AACivAAABr0AAEu7AAAxuwAAjD0AAMu8AAA7vQAATjwAADo8AADfOgAAFboAACA9AAA2OwAAZDwAAOo7AABBvAAAKzsAAJK8AAC7vAAAFL0AAO47AAADvQAARj0AAJS8AADLuwAA5bwAAKa7AAAGPQAA8bwAAKG9AAC/vAAADrwAAKO8AACDOgAAr7wAABM9AAD0uwAAQr0AABs9AAC2PAAAOLwAAM88AADSPAAAqzsAAOm7AABMPAAAGz0AAHU8AAAAvQAAULwAAAa9AADavAAAgzoAAKk8AABVvAAAPboAAHU8AACKvAAAgbwAADI8AAALuwAAIb0AAKi8AABxvAAAUz0AAJk8AAAnvQAA3zwAAMM7AAAVPAAA0bwAAME6AADCuwAAIrwAAMs8AACbPAAArbwAAGG8AAChOwAAEL0AAIQ7AADePAAADr0AADE9AAAbvQAAprwAAK+8AAARvAAAWrwAAL+8AAALPAAAWTwAAJ86AACGOwAAU70AACm8AAAJPQAA+LwAAKC8AABtvAAAtLwAALQ7AACmvAAAAj0AALW8AAA0PQAAhjwAAEa6AAAfPQAAirwAAOa7AABDPAAAqLwAANM8AAAGvQAA4DsAANO7AAAdvAAA7TwAACM7AACfvAAASDsAABs8AACxPAAAVzwAAEy9AAAxPAAAmb0AALw8AAAZvAAAiLwAALY8AAB3vAAA9zwAAJs8AAAkvAAAOz0AAMo7AABLPQAAwbsAAN47AABGuQAAl7oAAG08AACJvAAAZ7oAALw8AACavAAA37wAAKA7AAAgvAAANb0AAGA8AAAhPQAANz0AAMq7AADGvAAAlTwAABI9AABhuwAAkbsAAIY7AADauwAAtDwAABk8AAD7PAAAiDwAAPG8AACwvQAAn70AAFI8AACqugAAn7wAAGA9AAA0vQAAmrwAACo9AACCOwAAoTsAAIE9AABwOwAAAr0AANc8AAAbvAAAjDwAABe9AAAPvQAA07wAACG8AADBOwAAeDwAAAg9AAB0PAAAm7wAAEW6AACaugAADr0AANY8AAD1vAAA5zsAAKK9AAAXOwAAPr0AAAA9AAD3uwAAG7wAABW9AAAOPQAAMrwAAIA7AADdPAAAEb0AAGM8AAAjvQAAUDoAABI9AAD/PAAAHL0AAKM8AACbOQAAlbwAAAO9AACqPAAAAr0AAIy7AACCvAAAZjwAAGO8AAC9PQAA7DsAAJ88AAByOwAAmrsAAD+8AAArvAAA37wAAPo8AAAkvAAAL7sAACO9AAAnvQAA9DwAAJY8AACxPAAAeTwAAFO8AAAFvQAAHzwAADe7AACmPAAAKD0AAHM9AAAgvAAAmrwAALy8AAC/OwAA3LwAAG06AAAfOwAA/7sAALE9AADBPAAAtrsAAKI8AACZuwAAgrwAAES9AADcuwAAsjsAAJE8AABWPAAAK70AAEU8AABEPAAAMbwAAK+7AACcvAAARLsAABK6AAAiPAAAEbwAANG7AAChPAAAzzwAAMs6AAAFPQAA2TsAACG9AAB1PAAAsrwAAC29AABMPAAAzzwAANI8AADfvAAAm7wAAC29AACLuwAAHTwAALq8AAAcuwAA07wAAHm8AACxvAAA7LwAAK06AAA4PQAA7LsAAKC7AAAvuwAAKrwAAC68AABtPAAAtjwAAC+8AAAJvQAATLwAALE7AACCvAAApjwAAKE8AAC4vAAAjDwAACS9AAD3PAAAHz4AACe9AAB7vQAAET0AAII6AAC2OwAAyzwAANY7AAB+PQAAuDwAAME8AADMugAAAjwAANA7AAAgvAAAFT4AAPe7AAAPvQAALrwAAJQ5AAArOwAAFjwAAKe8AAD4uwAAGTwAACQ8AAAJPAAAZTwAAJa8AACgOwAANjsAAJk8AAC7OwAAdzwAAPG7AACfvAAAtjwAAFq8AAAMPQAAMDwAAHu9AAC6vAAAVT0AAKo7AACOPAAAoTsAANc8AAAXPQAAbDwAAKi7AABVOgAA5zwAAHU8AADCvAAAyjwAAAa9AADqOwAAmbwAALq7AAA+vAAAjDwAAB+9AAAqvQAAir0AAFo9AAA+PQAAgrsAANM8AAAhPAAAhbwAAAU8AACavAAAuLwAAKa8AACqPAAAI7wAAHG8AABFPAAAgToAAIy8AAAkuwAAjrwAAA49AACpPAAACz0AABC9AAAbvAAAWjwAAPI7AAAoPAAAJjoAAK26AAAXOwAADzwAAC+9AAC4vAAAIL0AAIk8AABhPAAAPj0AAHI7AAAUvQAALT0AACG8AAByPAAADD0AANk8AAC/vAAA4bwAAGu8AAC1vAAA0jsAALc8AAChvAAAT7wAAMu8AACOvAAA4bwAAHg8AAD1PAAACz0AAB08AAAXPAAAPr0AAIG6AAAFuwAAKTwAAI27AABPPAAAmzsAAOC8AAAbPQAAp7oAAGq8AABdOgAAzDwAANe8AAAdvQAALjoAABU9AAATPQAA0rsAAAc9AAD7PAAATLwAALA6AAAruwAAX7sAABK9AAC7PAAAErwAACG8AAC3OgAAkzwAAMw7AAAEOwAAqjwAAEW7AAAHPQAA6rsAAES8AACCPQAARj0AAGY8AABGPAAAdLwAANE8AAD1vAAAGzsAAEQ6AACuuwAAFb0AAIE8AAA4PAAAlbsAAH68AAACOwAAsjwAAKE8AAAoPAAAhDsAAME8AAD7uwAAkr0AAFq9AAC4OwAAsjwAADA8AACCvAAAbbwAAAs9AACWvAAAEzwAALS8AAAgPQAAd7wAAO42AABWvQAAHLwAAPG6AAAAPAAAFz0AAME7AAAoOwAAULsAANo8AABRuwAAiDwAABw8AADVuwAA+rsAAAo9AAAavAAAMDwAANe8AAD+vAAAibwAAJC8AABfOwAAtTwAAIE8AADmOwAAgLwAAMS8AABwPAAAAb0AALS8AAAqvQAANDwAAOU8AACWvAAAzjwAABG7AACouwAAJr0AAIM7AAAZvQAA0boAAFi8AABPPAAAnzgAAIE8AACbvAAAFb0AABY8AAC+OwAA5DwAAJa6AACkPAAAITwAAGE6AABtPAAAMb0AADg9AAAEPQAAnDwAAJ08AAABPAAAursAAHc8AAAFvAAA5ToAAD28AAAAPAAAazwAADQ7AADqPAAAA70AAFO7AAA6vAAAAj4AAEg6AADhPAAAELwAAFm9AACIvAAAxTwAACQ8AADkOwAAbrwAALq7AACGPAAAIL0AAGE8AADMPAAAOr0AACM9AACMPAAAKrsAAAY8AAAhPAAAKz0AACe9AACOvAAAa7wAACG9AABKuwAASrwAAI+8AAApvQAA+LsAAPe8AADGuwAAgroAADe7AACvuwAATz0AAMQ6AACFvAAAMLwAACg9AAADvQAAtTwAALa8AACuPAAAI70AAJI8AAAauwAAZbwAAA89AADWvAAAqDwAAAm9AAAAPQAAEDwAAOA8AAAxvQAAYzwAAB87AADhOgAAwrsAAOA8AAA3vQAA1jwAAKi8AAB1uwAAGb0AAJo8AABmPAAAPLwAAMI7AAC4PAAAmj0AAFc8AADcOgAAe7wAAH47AABdOwAAlrwAAPO8AAB5PQAAijsAABU8AAAOvQAAkTwAABK8AAC4PAAAZLwAAK68AACRvAAAwzwAAKq8AABWvQAA4DsAAKC8AACUPAAAm7wAAJO8AAAMuQAAwrsAAAk8AABdvQAAkrwAACQ8AAAoNwAApDwAABQ8AAAVPAAAH7wAAFK8AAAGPAAAkrsAAIA8AADGPAAAbrwAALc8AABxPAAApDwAABy8AAAZPQAAk7wAAMW8AABhvAAAPLwAAEI8AAB5PAAAxrwAAFi7AADwvAAAUL0AAAk9AABZOwAAED0AALY8AAB5PAAAmzwAAFM9AAAwPQAAsToAAPA6AADOvAAAMLsAAHO8AADQuAAAqLwAANc7AAA4PAAA3DsAAK48AAAdPAAAH7wAACQ7AAD5OwAAo7sAACY8AACrPAAATzwAAL68AAC9PAAA8DwAABI7AADeOwAAFL0AAAC9AACEOwAAITsAAJI8AADtuwAA8LsAANa8AACvvAAAI70AAAG9AABmOwAAd7wAAIE8AAA6vQAAvzwAAEK9AAD0vAAA/zwAAPU8AACVPAAAET0AAAU7AAAfOwAANroAAKm8AAAUvQAAyLsAAAa9AAAUvAAAErwAAII7AAAFPQAAALsAAC08AAA0uwAAgTwAAIu7AADRvAAADzwAAKA7AABDvAAAirsAALo8AAB3vAAAOLwAACO9AADEPAAA7jwAADg9AAAiPQAAqzcAANA8AAAuPAAAODwAAAW8AACNvAAAIjwAANC8AAAmvQAAoTwAAAc9AACHvAAABjsAAI68AADZPAAAobsAAIi9AADsvAAABrsAAAm8AABkOwAACDwAAIY8AABQvAAAmTwAABE9AAAFvQAABzwAAF08AACoPAAAzjwAAL49AAAfPAAAkbwAALQ6AAByvAAAcD0AAN+6AACTvAAAkDsAAK66AAC0PAAAkzoAAHy8AAAiOwAADDwAAIG9AAAmvAAACrsAADU9AAAjuAAAjbwAAPc8AACNOwAABbwAAMG7AACIvAAAO7wAAL88AAD7vAAAXLwAADw4AAC2PAAAnbsAADs7AAAwvAAA0LwAAPG8AAAmPQAAz7oAAOa8AABhuwAA+jwAAFU8AADLuwAAtzwAAHA8AAA3vAAAdbwAAIG8AAC6PAAAiDkAANi7AADpuQAALrsAAL09AABauwAAMbwAAOG8AAA2OgAAejsAAGY8AAB/uwAACTsAADa7AAAGvAAASrwAAKG8AAC2OgAA3LoAABy8AACiPAAACD0AAPy8AACyvAAAIDsAAIi7AACwvAAA6rwAAMy8AAA0vQAALr0AAKS7AABgPAAASbwAAA69AAAnvQAApLwAAIE8AACUOgAAYbwAABo7AACfPAAADr0AACg9AAAAvAAAFzwAAIM7AAABOwAAujwAABS9AABqvAAAHLwAAHg8AAB3PQAAQ7wAAB08AAAIPQAAhLwAAHq8AAAfPQAAljwAAME7AAChOwAA5jgAAAy7AAALPAAAv7wAAA08AAC+uwAAzDwAAAQ9AACoPAAANTwAANi8AAAPPQAABj0AAM68AAB7uwAAIz0AAB29AAATuwAAjbsAAJ88AACfOwAAAj0AAHi8AAA9vAAAYbwAAMo8AADpPAAAAbwAABU7AAAgPAAA+jsAAAm8AABgPAAAIb0AAIK9AABwPAAAtzwAAFi7AAAmPAAAozwAAFW9AAAwvAAAFT0AAJm8AADjvAAAEjsAAFI8AAACvQAANrwAAEm7AACLuwAAITwAABu8AAD4uwAAyLwAAFw8AAA2PAAAVTwAANW8AADDPAAAMLwAACC7AADMPAAARTsAAA28AABkPAAArjwAADI8AAAEvQAAujsAAFY8AABavAAA9zwAAKI8AABVPAAA+7sAAOC8AACFPQAAjTsAAKg8AACpuwAAsjsAABU7AABRPAAAHL0AAEY8AAAhPAAAerwAAKS7AAAXOwAAkLsAAAA9AAAxPAAA4TwAACi8AADYOwAAu7sAAF68AABLPAAATL0AAEK9AADwuwAAjDsAADW6AACEPAAAv7wAAJa8AABQPQAAfLwAAAe8AAC9PAAAnTsAABM8AADQvAAAcjwAAP86AAA2vQAAKD0AAMQ8AADevAAAobwAAGE8AAB7PAAAjzwAAIY8AACkPAAA2joAAKY8AAAGPQAAc7sAALw8AAABPAAAebwAAAs9AAAoOwAAmjsAAH48AABZPAAAAjwAAIm9AAAGvQAAFTwAACo7AACLvAAArrwAAJS6AADnugAABj0AAAu8AADcvAAAvbwAAKE5AADePAAAqbwAAOw8AAA2vQAA7ToAAIG7AAA2vQAAC70AACk8AACIPAAAFr0AAKe6AAAZvQAArzwAAG48AABsPAAAAbsAAD89AACnPAAAAb0AAOu8AAAQPQAA5TwAALg8AAAbOwAAWbwAAJu7AAAJPAAA4TwAABm9AAD0OgAA07wAAPe7AAB/uwAAED0AADs8AADEOgAAhrsAAJM8AABLvQAAq7wAAL06AACfPAAAlDwAAIY9AABavAAAjDoAAAG9AABlPAAAjLsAALK8AADaPAAA4LsAAPA8AABMvAAAXTsAAOG6AAD6OgAAvjkAANC8AABxuwAAybsAAK+7AABsOwAA5zwAABW8AAC9PAAAiDwAAPg8AAAJOwAAATsAADs8AAAdPQAAeTsAACK8AADrvAAASTsAAKM8AADMPAAAU70AABK8AAD0uwAA0rwAAF08AAChNwAAbbwAAB28AACZPAAAlLwAAME8AABmvAAAhjsAAPA9AADSvAAABTwAAP48AAAVvAAAdzwAADY8AACGPQAA2DwAAC07AAC0ugAAwTsAAJC8AACdPAAAajwAAKE7AAAiPQAAzjsAAA69AACdvAAAIr0AAEi9AADBOgAAgLsAANU8AACpPAAAP7wAAPq8AAAfPAAACTwAAC49AABhPQAAsjwAAMy7AAB0PQAABb0AAAy9AAAhvQAAWL0AAHy8AAAjPQAAjDwAAGC8AACbvAAADT0AAK08AACivAAAF7wAAL+8AACTPAAAz7wAAPw7AABfvAAAt7wAALi8AAAvPQAAtrsAAJY7AAAKPQAAr7wAACS9AAC8PAAAm7wAALa8AADBvAAA3zsAAIk8AABmOwAAw7wAAPm7AAArPAAAvzsAAF+8AABPuwAAXzwAAK+8AAA3PQAAG7wAAIg8AAAXvAAAprwAADA8AADEvAAAorwAANa8AABePQAAJr0AACG8AAAcvAAAQ70AAPC8AACxPAAAOLsAAOc6AABYPAAAsLwAAN68AACXuwAALbwAAJu7AAD7uwAA2jsAALY7AACWPAAAoLwAALa8AACwuwAA/DsAAEy8AAAiPAAA5bwAAGk8AABnPAAADzwAAF27AAAGOwAAtrsAAIS7AAAqPQAAeLwAAAa9" - } - ], - "model":"embed-v4.0", - "usage": - { - "prompt_tokens":1012, - "completion_tokens":0, - "total_tokens":1012 - } - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IEmbeddingGenerator> generator = new ImageEmbeddingsClient( - new("https://somwhere"), new AzureKeyCredential("key"), new() - { - Transport = new HttpClientTransport(httpClient), - }).AsIEmbeddingGenerator("embed-v-4-0"); - - var response = await generator.GenerateAsync([dotnetPng]); - Assert.NotNull(response); - Assert.Single(response); - - Assert.NotNull(response.Usage); - Assert.Equal(1012, response.Usage.InputTokenCount); - Assert.Equal(1012, response.Usage.TotalTokenCount); - - foreach (Embedding e in response) - { - Assert.Equal("embed-v4.0", e.ModelId); - Assert.NotNull(e.CreatedAt); - Assert.Equal(1536, e.Vector.Length); - Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); - } - } - - [Fact] - public async Task EmbeddingGenerationOptions_DoNotOverwrite_NotNullPropertiesInRawRepresentation() - { - DataContent dotnetPng = new(ImageDataUri.GetImageDataUri()); - - const string Input = """ - { - "input":[{"image":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcgAAAHICAMAAAD9f4rYAAAAS1BMVEVRK9RRK9T///\u002BolenTyvTp5fpnRdl8YN\u002B\u002Br\u002B708v1cONedh\u002Be\u002Bru5nRtl9YN6HbeKyouzJvfKSeuSzou2\u002Br\u002B9yU9ze1/dcONbe2PcNfWisAAAAAXRSTlP\u002BGuMHfQAAB79JREFUeNrs0QENAAAMw6Ddv\u002Bn7aMACOwomskFkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESIjREaIjBAZITJCZITICJERIiNERoiMEBkhMkJkhMgIkREiI0RGiIwQGSEyQmSEyAiRESKfnTvMTRyGoiisF5K2SYZhKKX7X\u002BpEeuov7Ngxorp\u002BOmcH9KssLnISJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMkhABgnIIAEZJCCDBGSQgAwSkEECMki/DzkNqUZr7H146M0ynYZnmgof4cn\u002B2BPpQA6rFQMymxDk/GalgMwmBDlcrRSQ2ZQgh79WCMhsUpDTYvsBmU0Kcvhn\u002BwGZTQuydLgCmU0MsjAmgcwmBlkYk0BmU4PcH5NAZlOD3D9cgcwmBzlcLB\u002BQ2fQg98YkkNn0IPfGJJDZBCF3xiSQ2RQhvy3XKyDnsboP\u002B\u002Bk6FpoT/wZjodWeSBEyPyZfATnaKxqHh072yiQhj4xJID1JyCN/XCA9TcgDYxJITxRyXqwyID1RyPoxCaSnClk9JoH0NCDH9jEJpKcBeR\u002BaPzeQngbk5do8JoH0NCA/35vHJJCeBuRqY0Ly0yoC0tOAPNm5dUwC6alA2q1xTALpaUBuYsvUNiaB9DQgP8w9Gq59AOnpQNq1aUwC6QlBnueWMQmkJwRpa8uYBNJTgrSx4doHkJ4UZMuYBNKTgkzeVvyy3YD0tCAbxiSQnhZkw5gE0hODtNvRMQmkpwa5zEOtiwekpwZpl4NjEkhPDvLomATS04M8z4fGJJCeHqSth95uBqQnCGnjkTEJpKcIeT8yJoH0FCEPjUkgPUnI5C91d0v2a08sf1p9QJp34JprM2S5dgcgf/qqHpNAeqKQS/W1DyA9Ucj6MQmkpwpZPSaB9GQhz3PdmATSk4W0U90zBEB6upD2XXW4AukJQ9aNSSA9YUi71YxJID1lyGWqGJNAesqQVYcrkJ40pF3LbzcD0tOGXMpjEkhPG9LW4pgE0hOHLP9S9zTkPNW1Wn1APnSeC28344aApw5pp8KYBNKTh7TCmATS04csjEkgPX1Iu\u002B2OSSC9DiCXae8ZAiC9DiDtsjcmgfR6gNwdk0B6XUDujUkgvS4gbc3/ZAak1wekjdkxCaTXCeQ9OyaB9DqBtFPuVdlAer1AZsckkF4vkPaeGZNAet1A2i09JoH0\u002BoHMXvu4A7nVD6RdMmPyDcitjiDTYxJIryfI85xkWIDc6gnS1vS1DyC3uoK0MTkmZyDN\u002BoJMj8kJSLO\u002BINNjcgTSrDPIZUpIfAFp1hlk8nDlaN3qDTL1KiW\u002BtW51B7nMQKbqDtJWIP\u002BzdwerDcNQEEUZWbIqG9XESev8/5d2EQol7wXcZBSwmLv3Zg54oYXkdTxIREE6HRCyFkHa2JDbfEohlHj5xINehsQgSBsXchtK\u002BC2tcHsdEt\u002BCNFEhx7Tj0XICZBakiQk53gvFCTYCJM5EyOv4nzbs6diQowW6wMaAnBIBsuGVEMeG3Hl9NQMSWZAmFmQO\u002Bx7WpUDiJMhbfEh/2hkmCmQtgkQbyOB2gokCiVmQQAvIHNwSTBxIREE2gVyCH0wkyCrIJpBrMLWFxCDIVr/W90JOSZANIMfgdoWJBYksSD6kx\u002BOft/IgcRZkA0h/owoTD3IqgqRD\u002BqteYCJCYhEkHdJdNVWYmJCIguRD2pXKF2xUyFoESYc0MyXXkQqJWZANILH\u002BNYoVfvNw34KnmwenCQ/Kw4vlvUt4n7aKDwms8aZYPjLU2\u002BJDAlte1jxCvbUbpOohQXaSIDtJkJ0kyE4SZCcJspME2UmC/GGPDmQAAAAABvlb36M9hRBHIo5EHIk4EnEk4kjEkYgjEUcijkQciTgScSTiSMSRiCMRRyKORByJOBJxJOJIxJGIIxFHIo5EHIk4EnEk4kjEkYgjEUciYo8OZAAAAAAG\u002BVvf4yuFRE6InBA5IXJC5ITICZETIidEToicEDkhckLkhMgJkRMiJ0ROiJwQOSFyQuSEyAmREyInRE6InBA5IXJC5ITICZETIidEToicEDkhckLkhMgJkRMiJ0ROiJwQOSFyQuSEyAmREyInRE6InBA5IXJC5ITICZETIidEToicEDkhckLkhMgJkRMiJ0ROiJwQWXt0QAMAAIAwyP6p7cFOBRBFIopEFIkoElEkokhEkYgiEUUiikQUiSgSUSSiSESRiCIRRSKKRBSJKBJRJKJIRJGIIhFFIopEFIkoElEkokhEkYgiEUUiikQUiSgSUSSiSESRiCIRRSKKRBSJKBJRJKJIRJGIIhFFIopEFIkoElEkokjEgjh2WnxgwCuWdQAAAABJRU5ErkJggg=="}], - "dimensions":1536, - "encoding_format":"base64", - "model":"embed-v-4-0" - } - """; - - const string Output = """ - { - "id":"9da7c0f0-4b9d-46d9-9323-f10f46977493", - "object":"list", - "data": - [ - { - "index":0, - "object":"embedding", - "embedding":"AADkPAAA6bsAAD68AAAnOwAAYbwAAFa8AABfvQAAqzsAAGy8AABwvAAAyDwAALo9AAC6vAAAID0AAGE8AAA2PAAA4TsAABU9AAC0PAAAqzwAADw8AAAaPQAA2LwAANa8AACoOgAA4DsAAA48AAC9PAAAdz0AAIC8AACGvAAA+7oAAEo7AABMuwAAab0AAFc9AAA2OwAAob0AAPO8AABGOwAAoLwAAKQ6AACHvAAAX70AAJQ8AAA/OwAAtbwAAME7AADMvAAA2DwAABQ9AABZPQAAd7wAAD+9AAAquwAAgTwAABE9AABFPQAALbwAAEk9AAC8ugAABj0AAI27AAAWPQAAMrwAAE88AABnvAAAZbwAAMK8AAAhPQAAPr0AAAg8AABcOgAA/jwAANE8AACvvAAAEbwAALy8AAAIvQAAe7wAAGW8AAAPPQAAFTsAALc5AACrOwAAirwAALa8AACLvAAACLsAABa7AAC+uAAAljwAAMS9AAC0vAAAhz0AAJw7AAAgvQAAxjwAAMK8AACMPAAAdz0AALC8AADIPAAArjsAAGG8AAATvAAAkrsAALs8AAAWPAAAxDwAADK9AAAAvQAAmTwAAIK9AADZPAAAmjwAABG9AAARuwAA/zwAAGO8AAC3PAAAGTwAACC8AAAtPAAAArwAAGG7AAC4PAAA/7sAAKG8AACdOwAA8DwAAJo7AAC8PAAAST0AAAI8AABnvAAAXTwAABc9AACSPAAAMjsAAPc7AABSvAAATLsAAKa8AAB1PAAAA70AAC87AAASvAAA/DwAADC7AABfvAAAYbwAAGW9AADlOwAANzwAAFc8AADEPAAAyrsAAMM8AAATPQAA3DwAABu8AAB+uQAAKj0AADS7AACkOwAAhD0AACK8AABIvQAAaboAALu8AADtOwAAoDwAAI88AACQPAAAmjwAAEy8AAC2OwAAtTwAAE68AACGvQAA0LsAAJM7AAAUvQAA17wAAEg9AABhOwAAVjwAALg8AACHvQAA5DwAACI9AACLPAAA4zsAAOk7AADOPAAA/7wAAPe8AAAGPQAAYTwAAEo9AAA/PAAA47wAAEq8AADgvAAAybsAAPk8AAA7vQAA3zsAAP87AAAUvAAAKjwAAKA8AAATvQAAcrwAAGm7AADlvAAAprwAAJM7AACivAAABr0AAEu7AAAxuwAAjD0AAMu8AAA7vQAATjwAADo8AADfOgAAFboAACA9AAA2OwAAZDwAAOo7AABBvAAAKzsAAJK8AAC7vAAAFL0AAO47AAADvQAARj0AAJS8AADLuwAA5bwAAKa7AAAGPQAA8bwAAKG9AAC/vAAADrwAAKO8AACDOgAAr7wAABM9AAD0uwAAQr0AABs9AAC2PAAAOLwAAM88AADSPAAAqzsAAOm7AABMPAAAGz0AAHU8AAAAvQAAULwAAAa9AADavAAAgzoAAKk8AABVvAAAPboAAHU8AACKvAAAgbwAADI8AAALuwAAIb0AAKi8AABxvAAAUz0AAJk8AAAnvQAA3zwAAMM7AAAVPAAA0bwAAME6AADCuwAAIrwAAMs8AACbPAAArbwAAGG8AAChOwAAEL0AAIQ7AADePAAADr0AADE9AAAbvQAAprwAAK+8AAARvAAAWrwAAL+8AAALPAAAWTwAAJ86AACGOwAAU70AACm8AAAJPQAA+LwAAKC8AABtvAAAtLwAALQ7AACmvAAAAj0AALW8AAA0PQAAhjwAAEa6AAAfPQAAirwAAOa7AABDPAAAqLwAANM8AAAGvQAA4DsAANO7AAAdvAAA7TwAACM7AACfvAAASDsAABs8AACxPAAAVzwAAEy9AAAxPAAAmb0AALw8AAAZvAAAiLwAALY8AAB3vAAA9zwAAJs8AAAkvAAAOz0AAMo7AABLPQAAwbsAAN47AABGuQAAl7oAAG08AACJvAAAZ7oAALw8AACavAAA37wAAKA7AAAgvAAANb0AAGA8AAAhPQAANz0AAMq7AADGvAAAlTwAABI9AABhuwAAkbsAAIY7AADauwAAtDwAABk8AAD7PAAAiDwAAPG8AACwvQAAn70AAFI8AACqugAAn7wAAGA9AAA0vQAAmrwAACo9AACCOwAAoTsAAIE9AABwOwAAAr0AANc8AAAbvAAAjDwAABe9AAAPvQAA07wAACG8AADBOwAAeDwAAAg9AAB0PAAAm7wAAEW6AACaugAADr0AANY8AAD1vAAA5zsAAKK9AAAXOwAAPr0AAAA9AAD3uwAAG7wAABW9AAAOPQAAMrwAAIA7AADdPAAAEb0AAGM8AAAjvQAAUDoAABI9AAD/PAAAHL0AAKM8AACbOQAAlbwAAAO9AACqPAAAAr0AAIy7AACCvAAAZjwAAGO8AAC9PQAA7DsAAJ88AAByOwAAmrsAAD+8AAArvAAA37wAAPo8AAAkvAAAL7sAACO9AAAnvQAA9DwAAJY8AACxPAAAeTwAAFO8AAAFvQAAHzwAADe7AACmPAAAKD0AAHM9AAAgvAAAmrwAALy8AAC/OwAA3LwAAG06AAAfOwAA/7sAALE9AADBPAAAtrsAAKI8AACZuwAAgrwAAES9AADcuwAAsjsAAJE8AABWPAAAK70AAEU8AABEPAAAMbwAAK+7AACcvAAARLsAABK6AAAiPAAAEbwAANG7AAChPAAAzzwAAMs6AAAFPQAA2TsAACG9AAB1PAAAsrwAAC29AABMPAAAzzwAANI8AADfvAAAm7wAAC29AACLuwAAHTwAALq8AAAcuwAA07wAAHm8AACxvAAA7LwAAK06AAA4PQAA7LsAAKC7AAAvuwAAKrwAAC68AABtPAAAtjwAAC+8AAAJvQAATLwAALE7AACCvAAApjwAAKE8AAC4vAAAjDwAACS9AAD3PAAAHz4AACe9AAB7vQAAET0AAII6AAC2OwAAyzwAANY7AAB+PQAAuDwAAME8AADMugAAAjwAANA7AAAgvAAAFT4AAPe7AAAPvQAALrwAAJQ5AAArOwAAFjwAAKe8AAD4uwAAGTwAACQ8AAAJPAAAZTwAAJa8AACgOwAANjsAAJk8AAC7OwAAdzwAAPG7AACfvAAAtjwAAFq8AAAMPQAAMDwAAHu9AAC6vAAAVT0AAKo7AACOPAAAoTsAANc8AAAXPQAAbDwAAKi7AABVOgAA5zwAAHU8AADCvAAAyjwAAAa9AADqOwAAmbwAALq7AAA+vAAAjDwAAB+9AAAqvQAAir0AAFo9AAA+PQAAgrsAANM8AAAhPAAAhbwAAAU8AACavAAAuLwAAKa8AACqPAAAI7wAAHG8AABFPAAAgToAAIy8AAAkuwAAjrwAAA49AACpPAAACz0AABC9AAAbvAAAWjwAAPI7AAAoPAAAJjoAAK26AAAXOwAADzwAAC+9AAC4vAAAIL0AAIk8AABhPAAAPj0AAHI7AAAUvQAALT0AACG8AAByPAAADD0AANk8AAC/vAAA4bwAAGu8AAC1vAAA0jsAALc8AAChvAAAT7wAAMu8AACOvAAA4bwAAHg8AAD1PAAACz0AAB08AAAXPAAAPr0AAIG6AAAFuwAAKTwAAI27AABPPAAAmzsAAOC8AAAbPQAAp7oAAGq8AABdOgAAzDwAANe8AAAdvQAALjoAABU9AAATPQAA0rsAAAc9AAD7PAAATLwAALA6AAAruwAAX7sAABK9AAC7PAAAErwAACG8AAC3OgAAkzwAAMw7AAAEOwAAqjwAAEW7AAAHPQAA6rsAAES8AACCPQAARj0AAGY8AABGPAAAdLwAANE8AAD1vAAAGzsAAEQ6AACuuwAAFb0AAIE8AAA4PAAAlbsAAH68AAACOwAAsjwAAKE8AAAoPAAAhDsAAME8AAD7uwAAkr0AAFq9AAC4OwAAsjwAADA8AACCvAAAbbwAAAs9AACWvAAAEzwAALS8AAAgPQAAd7wAAO42AABWvQAAHLwAAPG6AAAAPAAAFz0AAME7AAAoOwAAULsAANo8AABRuwAAiDwAABw8AADVuwAA+rsAAAo9AAAavAAAMDwAANe8AAD+vAAAibwAAJC8AABfOwAAtTwAAIE8AADmOwAAgLwAAMS8AABwPAAAAb0AALS8AAAqvQAANDwAAOU8AACWvAAAzjwAABG7AACouwAAJr0AAIM7AAAZvQAA0boAAFi8AABPPAAAnzgAAIE8AACbvAAAFb0AABY8AAC+OwAA5DwAAJa6AACkPAAAITwAAGE6AABtPAAAMb0AADg9AAAEPQAAnDwAAJ08AAABPAAAursAAHc8AAAFvAAA5ToAAD28AAAAPAAAazwAADQ7AADqPAAAA70AAFO7AAA6vAAAAj4AAEg6AADhPAAAELwAAFm9AACIvAAAxTwAACQ8AADkOwAAbrwAALq7AACGPAAAIL0AAGE8AADMPAAAOr0AACM9AACMPAAAKrsAAAY8AAAhPAAAKz0AACe9AACOvAAAa7wAACG9AABKuwAASrwAAI+8AAApvQAA+LsAAPe8AADGuwAAgroAADe7AACvuwAATz0AAMQ6AACFvAAAMLwAACg9AAADvQAAtTwAALa8AACuPAAAI70AAJI8AAAauwAAZbwAAA89AADWvAAAqDwAAAm9AAAAPQAAEDwAAOA8AAAxvQAAYzwAAB87AADhOgAAwrsAAOA8AAA3vQAA1jwAAKi8AAB1uwAAGb0AAJo8AABmPAAAPLwAAMI7AAC4PAAAmj0AAFc8AADcOgAAe7wAAH47AABdOwAAlrwAAPO8AAB5PQAAijsAABU8AAAOvQAAkTwAABK8AAC4PAAAZLwAAK68AACRvAAAwzwAAKq8AABWvQAA4DsAAKC8AACUPAAAm7wAAJO8AAAMuQAAwrsAAAk8AABdvQAAkrwAACQ8AAAoNwAApDwAABQ8AAAVPAAAH7wAAFK8AAAGPAAAkrsAAIA8AADGPAAAbrwAALc8AABxPAAApDwAABy8AAAZPQAAk7wAAMW8AABhvAAAPLwAAEI8AAB5PAAAxrwAAFi7AADwvAAAUL0AAAk9AABZOwAAED0AALY8AAB5PAAAmzwAAFM9AAAwPQAAsToAAPA6AADOvAAAMLsAAHO8AADQuAAAqLwAANc7AAA4PAAA3DsAAK48AAAdPAAAH7wAACQ7AAD5OwAAo7sAACY8AACrPAAATzwAAL68AAC9PAAA8DwAABI7AADeOwAAFL0AAAC9AACEOwAAITsAAJI8AADtuwAA8LsAANa8AACvvAAAI70AAAG9AABmOwAAd7wAAIE8AAA6vQAAvzwAAEK9AAD0vAAA/zwAAPU8AACVPAAAET0AAAU7AAAfOwAANroAAKm8AAAUvQAAyLsAAAa9AAAUvAAAErwAAII7AAAFPQAAALsAAC08AAA0uwAAgTwAAIu7AADRvAAADzwAAKA7AABDvAAAirsAALo8AAB3vAAAOLwAACO9AADEPAAA7jwAADg9AAAiPQAAqzcAANA8AAAuPAAAODwAAAW8AACNvAAAIjwAANC8AAAmvQAAoTwAAAc9AACHvAAABjsAAI68AADZPAAAobsAAIi9AADsvAAABrsAAAm8AABkOwAACDwAAIY8AABQvAAAmTwAABE9AAAFvQAABzwAAF08AACoPAAAzjwAAL49AAAfPAAAkbwAALQ6AAByvAAAcD0AAN+6AACTvAAAkDsAAK66AAC0PAAAkzoAAHy8AAAiOwAADDwAAIG9AAAmvAAACrsAADU9AAAjuAAAjbwAAPc8AACNOwAABbwAAMG7AACIvAAAO7wAAL88AAD7vAAAXLwAADw4AAC2PAAAnbsAADs7AAAwvAAA0LwAAPG8AAAmPQAAz7oAAOa8AABhuwAA+jwAAFU8AADLuwAAtzwAAHA8AAA3vAAAdbwAAIG8AAC6PAAAiDkAANi7AADpuQAALrsAAL09AABauwAAMbwAAOG8AAA2OgAAejsAAGY8AAB/uwAACTsAADa7AAAGvAAASrwAAKG8AAC2OgAA3LoAABy8AACiPAAACD0AAPy8AACyvAAAIDsAAIi7AACwvAAA6rwAAMy8AAA0vQAALr0AAKS7AABgPAAASbwAAA69AAAnvQAApLwAAIE8AACUOgAAYbwAABo7AACfPAAADr0AACg9AAAAvAAAFzwAAIM7AAABOwAAujwAABS9AABqvAAAHLwAAHg8AAB3PQAAQ7wAAB08AAAIPQAAhLwAAHq8AAAfPQAAljwAAME7AAChOwAA5jgAAAy7AAALPAAAv7wAAA08AAC+uwAAzDwAAAQ9AACoPAAANTwAANi8AAAPPQAABj0AAM68AAB7uwAAIz0AAB29AAATuwAAjbsAAJ88AACfOwAAAj0AAHi8AAA9vAAAYbwAAMo8AADpPAAAAbwAABU7AAAgPAAA+jsAAAm8AABgPAAAIb0AAIK9AABwPAAAtzwAAFi7AAAmPAAAozwAAFW9AAAwvAAAFT0AAJm8AADjvAAAEjsAAFI8AAACvQAANrwAAEm7AACLuwAAITwAABu8AAD4uwAAyLwAAFw8AAA2PAAAVTwAANW8AADDPAAAMLwAACC7AADMPAAARTsAAA28AABkPAAArjwAADI8AAAEvQAAujsAAFY8AABavAAA9zwAAKI8AABVPAAA+7sAAOC8AACFPQAAjTsAAKg8AACpuwAAsjsAABU7AABRPAAAHL0AAEY8AAAhPAAAerwAAKS7AAAXOwAAkLsAAAA9AAAxPAAA4TwAACi8AADYOwAAu7sAAF68AABLPAAATL0AAEK9AADwuwAAjDsAADW6AACEPAAAv7wAAJa8AABQPQAAfLwAAAe8AAC9PAAAnTsAABM8AADQvAAAcjwAAP86AAA2vQAAKD0AAMQ8AADevAAAobwAAGE8AAB7PAAAjzwAAIY8AACkPAAA2joAAKY8AAAGPQAAc7sAALw8AAABPAAAebwAAAs9AAAoOwAAmjsAAH48AABZPAAAAjwAAIm9AAAGvQAAFTwAACo7AACLvAAArrwAAJS6AADnugAABj0AAAu8AADcvAAAvbwAAKE5AADePAAAqbwAAOw8AAA2vQAA7ToAAIG7AAA2vQAAC70AACk8AACIPAAAFr0AAKe6AAAZvQAArzwAAG48AABsPAAAAbsAAD89AACnPAAAAb0AAOu8AAAQPQAA5TwAALg8AAAbOwAAWbwAAJu7AAAJPAAA4TwAABm9AAD0OgAA07wAAPe7AAB/uwAAED0AADs8AADEOgAAhrsAAJM8AABLvQAAq7wAAL06AACfPAAAlDwAAIY9AABavAAAjDoAAAG9AABlPAAAjLsAALK8AADaPAAA4LsAAPA8AABMvAAAXTsAAOG6AAD6OgAAvjkAANC8AABxuwAAybsAAK+7AABsOwAA5zwAABW8AAC9PAAAiDwAAPg8AAAJOwAAATsAADs8AAAdPQAAeTsAACK8AADrvAAASTsAAKM8AADMPAAAU70AABK8AAD0uwAA0rwAAF08AAChNwAAbbwAAB28AACZPAAAlLwAAME8AABmvAAAhjsAAPA9AADSvAAABTwAAP48AAAVvAAAdzwAADY8AACGPQAA2DwAAC07AAC0ugAAwTsAAJC8AACdPAAAajwAAKE7AAAiPQAAzjsAAA69AACdvAAAIr0AAEi9AADBOgAAgLsAANU8AACpPAAAP7wAAPq8AAAfPAAACTwAAC49AABhPQAAsjwAAMy7AAB0PQAABb0AAAy9AAAhvQAAWL0AAHy8AAAjPQAAjDwAAGC8AACbvAAADT0AAK08AACivAAAF7wAAL+8AACTPAAAz7wAAPw7AABfvAAAt7wAALi8AAAvPQAAtrsAAJY7AAAKPQAAr7wAACS9AAC8PAAAm7wAALa8AADBvAAA3zsAAIk8AABmOwAAw7wAAPm7AAArPAAAvzsAAF+8AABPuwAAXzwAAK+8AAA3PQAAG7wAAIg8AAAXvAAAprwAADA8AADEvAAAorwAANa8AABePQAAJr0AACG8AAAcvAAAQ70AAPC8AACxPAAAOLsAAOc6AABYPAAAsLwAAN68AACXuwAALbwAAJu7AAD7uwAA2jsAALY7AACWPAAAoLwAALa8AACwuwAA/DsAAEy8AAAiPAAA5bwAAGk8AABnPAAADzwAAF27AAAGOwAAtrsAAIS7AAAqPQAAeLwAAAa9" - } - ], - "model":"embed-v4.0", - "usage": - { - "prompt_tokens":1012, - "completion_tokens":0, - "total_tokens":1012 - } - } - """; - - using VerbatimHttpHandler handler = new(Input, Output); - using HttpClient httpClient = new(handler); - using IEmbeddingGenerator> generator = new ImageEmbeddingsClient( - new("https://somwhere"), new AzureKeyCredential("key"), new() - { - Transport = new HttpClientTransport(httpClient), - }).AsIEmbeddingGenerator("text-embedding-004"); - - var response = await generator.GenerateAsync([dotnetPng], - new EmbeddingGenerationOptions - { - Dimensions = 768, - RawRepresentationFactory = (e) => new ImageEmbeddingsOptions(input: []) - { - Dimensions = 1536, - Model = "embed-v-4-0", - EncodingFormat = EmbeddingEncodingFormat.Single, // this will be overwritten, we only support base64. - } - }); - - Assert.NotNull(response); - - foreach (Embedding e in response) - { - Assert.Equal("embed-v4.0", e.ModelId); - Assert.NotNull(e.CreatedAt); - Assert.Equal(1536, e.Vector.Length); - Assert.Contains(e.Vector.ToArray(), f => !f.Equals(0)); - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageGeneratingChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageGeneratingChatClientIntegrationTests.cs deleted file mode 100644 index 3a05f7fba9c..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/AzureAIInferenceImageGeneratingChatClientIntegrationTests.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.AI; - -/// -/// Azure AI Inference-specific integration tests for ImageGeneratingChatClient. -/// Tests the ImageGeneratingChatClient with Azure AI Inference chat client implementation. -/// -public class AzureAIInferenceImageGeneratingChatClientIntegrationTests : ImageGeneratingChatClientIntegrationTests -{ - protected override IChatClient? CreateChatClient() => - IntegrationTestHelpers.GetChatCompletionsClient() - ?.AsIChatClient(TestRunnerConfiguration.Instance["AzureAIInference:ChatModel"] ?? "gpt-4o-mini"); -} diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/IntegrationTestHelpers.cs b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/IntegrationTestHelpers.cs deleted file mode 100644 index 6f08cf52347..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/IntegrationTestHelpers.cs +++ /dev/null @@ -1,72 +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.Threading.Tasks; -using Azure; -using Azure.AI.Inference; -using Azure.Core; -using Azure.Core.Pipeline; - -namespace Microsoft.Extensions.AI; - -/// Shared utility methods for integration tests. -internal static class IntegrationTestHelpers -{ - private static readonly string? _apiKey = - TestRunnerConfiguration.Instance["AzureAIInference:Key"] ?? - TestRunnerConfiguration.Instance["OpenAI:Key"]; - - private static readonly string _endpoint = - TestRunnerConfiguration.Instance["AzureAIInference:Endpoint"] ?? - "https://api.openai.com/v1"; - - /// Gets a to use for testing, or if the associated tests should be disabled. - public static ChatCompletionsClient? GetChatCompletionsClient() => - _apiKey is string apiKey ? - new ChatCompletionsClient(new Uri(_endpoint), new AzureKeyCredential(apiKey), CreateOptions()) : - null; - - /// Gets an to use for testing, or if the associated tests should be disabled. - public static EmbeddingsClient? GetEmbeddingsClient() => - _apiKey is string apiKey ? - new EmbeddingsClient(new Uri(_endpoint), new AzureKeyCredential(apiKey), CreateOptions()) : - null; - - private static AzureAIInferenceClientOptions CreateOptions() - { - var result = new AzureAIInferenceClientOptions(); - - // The API vesion set here corresponds to the value used by AzureOpenAIClientOptions - // if the AZURE_OPENAI_GA flag is set during its compilation. This API version is the - // minimum required for structured output with JSON schema. - result.AddPolicy(new OverrideApiVersionPolicy("2024-08-01-preview"), HttpPipelinePosition.PerCall); - - return result; - } - - // From https://github.com/Azure/azure-sdk-for-net/issues/48405#issuecomment-2704360548 - private class OverrideApiVersionPolicy : HttpPipelinePolicy - { - private string ApiVersion { get; } - - public OverrideApiVersionPolicy(string apiVersion) - { - ApiVersion = apiVersion; - } - - public override void Process(HttpMessage message, ReadOnlyMemory pipeline) - { - message.Request.Uri.Query = $"?api-version={ApiVersion}"; - ProcessNext(message, pipeline); - } - - public override ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) - { - message.Request.Uri.Query = $"?api-version={ApiVersion}"; - var task = ProcessNextAsync(message, pipeline); - - return task; - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/Microsoft.Extensions.AI.AzureAIInference.Tests.csproj b/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/Microsoft.Extensions.AI.AzureAIInference.Tests.csproj deleted file mode 100644 index 0cf9db6ab60..00000000000 --- a/test/Libraries/Microsoft.Extensions.AI.AzureAIInference.Tests/Microsoft.Extensions.AI.AzureAIInference.Tests.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - Microsoft.Extensions.AI - Unit tests for Microsoft.Extensions.AI.AzureAIInference - - - - true - $(NoWarn);S104 - - - - - - - - - - - - - - - - - - - - - - - - - -