Skip to content

Conversation

jozkee
Copy link
Owner

@jozkee jozkee commented Apr 16, 2025

if (openAICompletion.SystemFingerprint is string systemFingerprint)
{
(response.AdditionalProperties ??= [])[nameof(openAICompletion.SystemFingerprint)] = systemFingerprint;
}

Choose a reason for hiding this comment

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

ChatCompletion.SerializedAdditionalRawData stores all of the properties, even ones that aren't surfaced in a strong-typed manner. We should try to transfer those over to AdditionalProperties, so that all of the data is available. Same goes for ChatResponseUpdate.

if (additionalProperties.TryGetValue(nameof(result.TopLogProbabilityCount), out int topLogProbabilityCountInt))
{
result.TopLogProbabilityCount = topLogProbabilityCountInt;
}

Choose a reason for hiding this comment

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

Outside of the OpenAI assembly, we didn't have a way to transfer arbitrary AdditionalProperties to the options object. It's possible we do now. That would enable being able to use non-standard properties with OpenAI-compatible endpoints.

functionCall.FunctionName,
static json => JsonSerializer.Deserialize(json.Span, ResponseClientJsonContext.Default.IDictionaryStringObject)!));
break;
}

Choose a reason for hiding this comment

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

I opened dotnet/extensions#6311 for it, but we're going to want to support ReasoningResponseItem here, translating it to TextReasoningContent, and the same for streaming, and the same in the inverse direction when translating TextReasoningContent in history back into the OpenAI object model.

callContent.Arguments,
AIJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IDictionary<string, object?>)))));
break;
}

Choose a reason for hiding this comment

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

It occurs to me that although we're creating ErrorContent when processing responses output, we're not here translating that ErrorContent back into something in the ResponseItems. I don't know if there's a way to do so, but if there is, we probably should.


<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />
</ItemGroup>

Choose a reason for hiding this comment

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

Presumably we'll be adding tests to the OpenAI repo as part of this as well.

<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="System.ClientModel" Version="1.2.1" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="6.0.1" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />

Choose a reason for hiding this comment

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

I'm surprised this is necessary. Worst case this should be an 8.x version, right?

/// <summary>Converts an Extensions function to an OpenAI chat tool.</summary>
private static ChatTool ToOpenAIChatTool(AIFunction aiFunction)
{
// Default strict to true, but allow to be overridden by an additional Strict property.

Choose a reason for hiding this comment

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

All uses of strict need to be re-evaluated.
dotnet/extensions#6285

Choose a reason for hiding this comment

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

Agreed. This is a subjective area since it seems like there are cases where we do know we could turn on strict mode automatically (the cases where we know MEAI generated the schema), but it's unclear how manageable that will be in the long run. We don't really want MEAI internals to have to do a lot of OpenAI-specific stuff when generating schema.

So at this point I'd be happy with going for a simpler rule of just defaulting strict to false in all cases. That aligns with OpenAI's decision for strict not to be the default.

// Handle loosely-typed properties from AdditionalProperties.
if (options.AdditionalProperties is { Count: > 0 } additionalProperties)
{
if (additionalProperties.TryGetValue(nameof(result.ParallelToolCallsEnabled), out bool allowParallelToolCalls))

Choose a reason for hiding this comment

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

@stephentoub
Copy link

cc: @SteveSandersonMS

/// <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>An <see cref="IEmbeddingGenerator{String, Embedding}"/> for an OpenAI <see cref="EmbeddingClient"/>.</summary>
internal sealed class OpenAIEmbeddingGenerator : IEmbeddingGenerator<string, Embedding<float>>
Copy link

@SteveSandersonMS SteveSandersonMS Apr 17, 2025

Choose a reason for hiding this comment

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

What's the reason for putting all these classes in one file? Is this to match some style preference from openai-dotnet?

Looking at https://github.com/jozkee/openai-dotnet/tree/main/src/Custom, it seems like they have a lot of subdirectories representing different functional areas, so would seem to follow to have a Microsoft.Extensions.AI subdirectory and then class-per-file as usual.

Copy link
Owner Author

Choose a reason for hiding this comment

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

It was our initial understanding that it was their preference on how this should be contributed. I will revert it to +/- the original split we had.


if (audio.Id is string id)
{
dc.AdditionalProperties[nameof(audio.Id)] = id;

Choose a reason for hiding this comment

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

I thought the plan was to avoid setting AdditionalProperties for data that could also be reached in a strongly-typed way through RawRepresentation (except if there's some reason to believe it needs to show up in telemetry, which I don't think is the case for the data here, and especially not for Transcript which might be huge).

if (openAICompletion.SystemFingerprint is string systemFingerprint)
{
(response.AdditionalProperties ??= [])[nameof(openAICompletion.SystemFingerprint)] = systemFingerprint;
}

Choose a reason for hiding this comment

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

As above, I think all these AdditionalProperties can be removed.

/// <summary>Converts an extensions options instance to an OpenAI options instance.</summary>
private static ChatCompletionOptions ToOpenAIOptions(ChatOptions? options)
{
ChatCompletionOptions result = new();
Copy link

@SteveSandersonMS SteveSandersonMS Apr 22, 2025

Choose a reason for hiding this comment

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

Not sure if you're planning to handle it within this PR or later, but from our previous discussion, I think the intent was to try obtaining the initial ChatCompletionOptions from options.AdditionalProperties, e.g. something like:

ChatCompletionOptions result = options.AdditionalProperties?.TryGetValue(typeof(ChatCompletionOptions).FullName, out var initialOptions)
    ? initialOptions
    : new();

... and then we can eliminate all the code from L496-558 that reads individual values from AdditionalProperties. The benefit for app developers is they would have a 100% strongly-typed way to set OpenAI options and it would automatically work with all future OpenAI options.

We might also wish to define an extension method on ChatOptions inside this assembly so that you can do:

var options = new ChatOptions();
options.SetOpenAIOptions(new ChatCompletionOptions { ... }); // Extension method that writes to AdditionalProperties[typeof(ChatCompletionOptions).FullName]

... but that's optional and would just be for convenience.

Choose a reason for hiding this comment

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

We should make all of these changes in dotnet/extensions for remaining releases of the M.E.AI.OpenAI lib. Ideally the behavior of what's PR'd to openai/openai-dotnet is then identical.

bool strict =
!aiFunction.AdditionalProperties.TryGetValue("Strict", out object? strictObj) ||
strictObj is not bool strictValue ||
strictValue;

Choose a reason for hiding this comment

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

Can you coordinate with @eiriktsarpalis to make sure his changes in dotnet/extensions#6285 don't get lost here?

Choose a reason for hiding this comment

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

The changes have now been merged, so now would be a good time update the implementation.

if (contentPart.Refusal is string refusal)
{
(aiContent.AdditionalProperties ??= [])[nameof(contentPart.Refusal)] = refusal;
}

Choose a reason for hiding this comment

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

I think the two uses of AdditionalProperties in this method can be removed since they can be read from RawRepresentation instead.


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.

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

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?

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.

Comment on lines +252 to +255
{
ErrorCode = errorUpdate.Code,
Details = errorUpdate.Param,
}

Choose a reason for hiding this comment

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

Suggested change
{
ErrorCode = errorUpdate.Code,
Details = errorUpdate.Param,
}
{
ErrorCode = errorUpdate.Code,
Details = errorUpdate.Param,
}

/// <summary>Converts a <see cref="ChatOptions"/> to a <see cref="ResponseCreationOptions"/>.</summary>
private static ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? options)
{
ResponseCreationOptions result = new();

Choose a reason for hiding this comment

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

Same thing about ResponseCreationOptions as in the OpenAIChatClient equivalent.

Comment on lines +155 to +159
response.AdditionalProperties = new AdditionalPropertiesDictionary
{
[nameof(audioTranscription.Language)] = audioTranscription.Language,
[nameof(audioTranscription.Duration)] = audioTranscription.Duration
};

Choose a reason for hiding this comment

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

Can be removed

/// <summary>Converts an extensions options instance to an OpenAI options instance.</summary>
private static AudioTranscriptionOptions ToOpenAITranscriptionOptions(SpeechToTextOptions? options)
{
AudioTranscriptionOptions result = new();

Choose a reason for hiding this comment

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

Same suggestion as with ChatCompletionOptions

Comment on lines +224 to +228
response.AdditionalProperties = new AdditionalPropertiesDictionary
{
[nameof(audioTranslation.Language)] = audioTranslation.Language,
[nameof(audioTranslation.Duration)] = audioTranslation.Duration
};

Choose a reason for hiding this comment

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

Can be removed

/// <summary>Converts an extensions options instance to an OpenAI options instance.</summary>
private static AudioTranslationOptions ToOpenAITranslationOptions(SpeechToTextOptions? options)
{
AudioTranslationOptions result = new();

Choose a reason for hiding this comment

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

Same suggestion as with ChatCompletionOptions

@SteveSandersonMS
Copy link

Thanks for the updates, @jozkee! It's much nicer now it's split across multiple files.

@jozkee jozkee closed this Jun 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants