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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Custom/Audio/AudioClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public partial class AudioClient
{
private readonly string _model;

internal Uri Endpoint => _endpoint;
internal string Model => _model;

// CUSTOM: Added as a convenience.
/// <summary> Initializes a new instance of <see cref="AudioClient"/>. </summary>
/// <param name="model"> The name of the model to use in requests sent to the service. To learn more about the available models, see <see href="https://platform.openai.com/docs/models"/>. </param>
Expand Down
3 changes: 3 additions & 0 deletions src/Custom/Chat/ChatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public partial class ChatClient
private readonly string _model;
private readonly OpenTelemetrySource _telemetry;

internal Uri Endpoint => _endpoint;
internal string Model => _model;

// CUSTOM: Added as a convenience.
/// <summary> Initializes a new instance of <see cref="ChatClient"/>. </summary>
/// <param name="model"> The name of the model to use in requests sent to the service. To learn more about the available models, see <see href="https://platform.openai.com/docs/models"/>. </param>
Expand Down
3 changes: 3 additions & 0 deletions src/Custom/Embeddings/EmbeddingClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public partial class EmbeddingClient
{
private readonly string _model;

internal Uri Endpoint => _endpoint;
internal string Model => _model;

// CUSTOM: Added as a convenience.
/// <summary> Initializes a new instance of <see cref="EmbeddingClient"/>. </summary>
/// <param name="model"> The name of the model to use in requests sent to the service. To learn more about the available models, see <see href="https://platform.openai.com/docs/models"/>. </param>
Expand Down
760 changes: 760 additions & 0 deletions src/Custom/Microsoft.Extensions.AI/OpenAIChatClient.cs

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions src/Custom/Microsoft.Extensions.AI/OpenAIClientExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#nullable enable

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.AI;
using OpenAI.Audio;
using OpenAI.Chat;
using OpenAI.Embeddings;
using OpenAI.Responses;

namespace OpenAI.Custom.Microsoft.Extensions.AI;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This namespace looks wrong. Should be Microsoft.Extensions.AI?



Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

/// <summary>Provides extension methods for working with <see cref="OpenAIClient"/>s.</summary>
public static class OpenAIClientExtensions
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

/// <summary>Gets an <see cref="IChatClient"/> for use with this <see cref="ChatClient"/>.</summary>
/// <param name="chatClient">The client.</param>
/// <returns>An <see cref="IChatClient"/> that can be used to converse via the <see cref="ChatClient"/>.</returns>
[Experimental("OPENAI003")]
public static IChatClient AsIChatClient(this ChatClient chatClient) =>
new OpenAIChatClient(chatClient);

/// <summary>Gets an <see cref="IChatClient"/> for use with this <see cref="OpenAIResponseClient"/>.</summary>
/// <param name="responseClient">The client.</param>
/// <returns>An <see cref="IChatClient"/> that can be used to converse via the <see cref="OpenAIResponseClient"/>.</returns>
[Experimental("OPENAI003")]
public static IChatClient AsIChatClient(this OpenAIResponseClient responseClient) =>
new OpenAIResponseChatClient(responseClient);

/// <summary>Gets an <see cref="ISpeechToTextClient"/> for use with this <see cref="AudioClient"/>.</summary>
/// <param name="audioClient">The client.</param>
/// <returns>An <see cref="ISpeechToTextClient"/> that can be used to transcribe audio via the <see cref="AudioClient"/>.</returns>
[Experimental("OPENAI003")]
public static ISpeechToTextClient AsISpeechToTextClient(this AudioClient audioClient) =>
new OpenAISpeechToTextClient(audioClient);

/// <summary>Gets an <see cref="IEmbeddingGenerator{String, Single}"/> for use with this <see cref="EmbeddingClient"/>.</summary>
/// <param name="embeddingClient">The client.</param>
/// <param name="defaultModelDimensions">The number of dimensions to generate in each embedding.</param>
/// <returns>An <see cref="IEmbeddingGenerator{String, Embedding}"/> that can be used to generate embeddings via the <see cref="EmbeddingClient"/>.</returns>
[Experimental("OPENAI003")]
public static IEmbeddingGenerator<string, Embedding<float>> AsIEmbeddingGenerator(this EmbeddingClient embeddingClient, int? defaultModelDimensions = null) =>
new OpenAIEmbeddingGenerator(embeddingClient, defaultModelDimensions);
}
109 changes: 109 additions & 0 deletions src/Custom/Microsoft.Extensions.AI/OpenAIEmbeddingGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OpenAI;
using OpenAI.Embeddings;

namespace Microsoft.Extensions.AI;

/// <summary>An <see cref="IEmbeddingGenerator{String, Embedding}"/> for an OpenAI <see cref="EmbeddingClient"/>.</summary>
internal sealed class OpenAIEmbeddingGenerator : IEmbeddingGenerator<string, Embedding<float>>
{
/// <summary>Metadata about the embedding generator.</summary>
private readonly EmbeddingGeneratorMetadata _metadata;

/// <summary>The underlying <see cref="OpenAI.Chat.ChatClient" />.</summary>
private readonly EmbeddingClient _embeddingClient;

/// <summary>The number of dimensions produced by the generator.</summary>
private readonly int? _dimensions;

/// <summary>Initializes a new instance of the <see cref="OpenAIEmbeddingGenerator"/> class.</summary>
/// <param name="embeddingClient">The underlying client.</param>
/// <param name="defaultModelDimensions">The number of dimensions to generate in each embedding.</param>
/// <exception cref="ArgumentNullException"><paramref name="embeddingClient"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="defaultModelDimensions"/> is not positive.</exception>
public OpenAIEmbeddingGenerator(EmbeddingClient embeddingClient, int? defaultModelDimensions = null)
{
Argument.AssertNotNull(embeddingClient, nameof(embeddingClient));
if (defaultModelDimensions < 1)
{
throw new ArgumentOutOfRangeException(nameof(defaultModelDimensions), "Value must be greater than 0.");
}

_embeddingClient = embeddingClient;
_dimensions = defaultModelDimensions;
_metadata = CreateMetadata("openai",
embeddingClient.Endpoint ?? OpenAIChatClient.DefaultOpenAIEndpoint,
embeddingClient.Model,
defaultModelDimensions);
}

/// <inheritdoc />
public async Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(IEnumerable<string> values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default)
{
OpenAI.Embeddings.EmbeddingGenerationOptions? openAIOptions = ToOpenAIOptions(options);

var embeddings = (await _embeddingClient.GenerateEmbeddingsAsync(values, openAIOptions, cancellationToken).ConfigureAwait(false)).Value;

return new(embeddings.Select(e =>
new Embedding<float>(e.ToFloats())
{
CreatedAt = DateTimeOffset.UtcNow,
ModelId = embeddings.Model,
}))
{
Usage = new()
{
InputTokenCount = embeddings.Usage.InputTokenCount,
TotalTokenCount = embeddings.Usage.TotalTokenCount
},
};
}

/// <inheritdoc />
void IDisposable.Dispose()
{
// Nothing to dispose. Implementation required for the IEmbeddingGenerator interface.
}

/// <inheritdoc />
object? IEmbeddingGenerator.GetService(Type serviceType, object? serviceKey)
{
Argument.AssertNotNull(serviceType, nameof(serviceType));

return
serviceKey is not null ? null :
serviceType == typeof(EmbeddingGeneratorMetadata) ? _metadata :
serviceType == typeof(EmbeddingClient) ? _embeddingClient :
serviceType.IsInstanceOfType(this) ? this :
null;
}

/// <summary>Creates the <see cref="EmbeddingGeneratorMetadata"/> for this instance.</summary>
private static EmbeddingGeneratorMetadata CreateMetadata(string providerName, Uri providerUri, string? defaultModelId, int? defaultModelDimensions) =>
new(providerName, providerUri, defaultModelId, defaultModelDimensions);

/// <summary>Converts an extensions options instance to an OpenAI options instance.</summary>
private OpenAI.Embeddings.EmbeddingGenerationOptions? ToOpenAIOptions(EmbeddingGenerationOptions? options)
{
OpenAI.Embeddings.EmbeddingGenerationOptions openAIOptions = new()
{
Dimensions = options?.Dimensions ?? _dimensions,
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we'd follow the same pattern of getting an initial EmbeddingGenerationOptions from AdditionalProperties here as we will do for OpenAIChatClient, and eliminate L99-105 below.


if (options?.AdditionalProperties is { Count: > 0 } additionalProperties)
{
if (additionalProperties.TryGetValue(nameof(openAIOptions.EndUserId), out string? endUserId))
{
openAIOptions.EndUserId = endUserId;
}
}

return openAIOptions;
}
}
Loading