Skip to content

Create diff for generator for multipart/form-data#1

Open
annelo-msft wants to merge 31 commits intoopenai-generate-cleanfrom
openai-generate-multipart
Open

Create diff for generator for multipart/form-data#1
annelo-msft wants to merge 31 commits intoopenai-generate-cleanfrom
openai-generate-multipart

Conversation

@annelo-msft
Copy link
Copy Markdown
Owner

No description provided.

using RequestBody content = audio.ToRequestBody();
Result result = await CreateTranscriptionAsync(content, context).ConfigureAwait(false);
return Result.FromValue(CreateTranscriptionResponse.FromResponse(result.GetRawResponse()), result.GetRawResponse());
(BinaryContent content, string contentType, RequestOptions options) = await audio.ToMultipartContentAsync().ConfigureAwait(false);
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is where we obtain the multipart content instead of standard BinaryContent.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This part looks different in #2 and #3.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Comment thread tsp-output/@azure-tools/typespec-csharp/src/Generated/Audio.cs Outdated
Comment thread tsp-output/@azure-tools/typespec-csharp/src/Generated/Audio.cs Outdated
/// <param name="model"> ID of the model to use. Only `whisper-1` is currently available. </param>
/// <exception cref="ArgumentNullException"> <paramref name="file"/> is null. </exception>
public CreateTranscriptionRequest(BinaryData file, CreateTranscriptionRequestModel model)
public CreateTranscriptionRequest(Stream file, string fileName, CreateTranscriptionRequestModel model)
Copy link
Copy Markdown
Owner Author

@annelo-msft annelo-msft Mar 18, 2024

Choose a reason for hiding this comment

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

For MPFD, if there is a file-upload parameter/model property, we need the binary-holding type to be a Stream and an option for the end-user to pass a string with the filename (they can also pass null, but nullable is disabled in this file, so it's only visible here by the fact that the method doesn't throw ArgumentException if null is passed for it). So, we would generate two properties from whatever the TypeSpec is that indicates you can upload a file using the service endpoint.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

/// <param name="speech"> The <see cref="CreateSpeechRequest"/> to use. </param>
/// <param name="cancellationToken"> The cancellation token to use. </param>
/// <exception cref="ArgumentNullException"> <paramref name="speech"/> is null. </exception>
public virtual async Task<ClientResult<BinaryData>> CreateSpeechAsync(CreateSpeechRequest speech, CancellationToken cancellationToken = default)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Unbranded clients don't take cancellation tokens in convenience methods.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

/// <list type="bullet">
/// <item>
/// <description>
/// This <see href="https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/ProtocolMethods.md">protocol method</see> allows explicit creation of the request and processing of the response for advanced scenarios.
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Maybe we want to remove Azure in the docs here?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

/// <exception cref="ClientResultException"> Service returned a non-success status code. </exception>
/// <returns> The response returned from the service. </returns>
public virtual async Task<ClientResult> CreateTranscriptionAsync(BinaryContent content, RequestOptions context = null)
public virtual async Task<ClientResult> CreateTranscriptionAsync(BinaryContent content, string contentType, RequestOptions options = null)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

MPFD protocol methods take contentType -- this is the value of the Content-Type header. This allows the same protocol method to be generated in cases where the service endpoint can accept multiple different content types -- the caller can specify, and we can provide convenience methods for the version that works best for the client.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

request.Headers.Set("Accept", "application/octet-stream");
request.Headers.Set("Content-Type", "application/json");
request.Content = content;
message.Apply(options);
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The call to message.Apply(options) needs to move to the end of the request-creation helper method so that the semantics of SetHeader on RequestOptions are correct. This is because anything the user called SetHeader with should replace what the generated code does.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

{
await pipeline.SendAsync(message).ConfigureAwait(false);

if (message.Response == null)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

We don't need to validate this in unbranded clients.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

await pipeline.SendAsync(message).ConfigureAwait(false);

if (message.Response == null)
if (message.Response!.IsError && options.ErrorOptions == ClientErrorBehaviors.Default)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

We should clarify the positive case where we throw, instead of the negative case. The method should end returning a successful result to clarify what's happening in it.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

{
internal static class ClientPipelineExtensions
{
public static async ValueTask<PipelineResponse> ProcessMessageAsync(this ClientPipeline pipeline, PipelineMessage message, RequestOptions requestContext, CancellationToken cancellationToken = default)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

We don't use the cancellationToken and shouldn't give the option to pass it.

if (message.Response!.IsError && options.ErrorOptions == ClientErrorBehaviors.Default)
{
throw new InvalidOperationException("Failed to receive Result.");
throw await ClientResultException.CreateAsync(message.Response).ConfigureAwait(false);
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

In an async context, we need to call the factory method to create the exception asynchronously.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

return message.Response;
}

public static async ValueTask<ClientResult<bool>> ProcessHeadAsBoolMessageAsync(this ClientPipeline pipeline, PipelineMessage message, RequestOptions requestContext)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I don't think this is the correct logic for unbranded clients. We don't have to revisit or block on this today for OpenAI, but we should address it soon so we don't ship an unbranded client with the wrong behavior.

One specific problem here is that we don't honor any response classifiers an end-user might pass. There may be others as well, e.g. I don't think we want to return exploding responses from unbranded client -- it is not idiomatic to .NET to throw from properties on types.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

/// <exception cref="ArgumentNullException"> <paramref name="content"/> is null. </exception>
/// <exception cref="ClientResultException"> Service returned a non-success status code. </exception>
/// <returns> The response returned from the service. </returns>
public virtual async Task<ClientResult> CreateSpeechAsync(BinaryContent content, RequestOptions context = null)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Since the variable name is public, we need to change context to options.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

}

internal PipelineMessage CreateCreateSpeechRequest(BinaryContent content, RequestOptions context)
private PipelineMessage CreateCreateSpeechRequest(BinaryContent content, RequestOptions options)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

It is better for these to be private if there isn't a clear need for them to be internal.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I do not think we need this to be "internal", because other parts of our generated libraries are not using them.
But I prefer we change this later after all the changes that affect functionalities are resolved.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@ArcturusZhang, that's fine, I agree this is lower priority than addressing public API or functional issues. Thanks!

// <auto-generated/>

#nullable disable
#nullable enable
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is important for a "ClientModel" type so we can do proper validation on nulls.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

well the generated code will always have "nullable disable", therefore we cannot do this unless we change all of them in generated code (including models clients) and all other dependencies (such as resourcemanager)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@ArcturusZhang, why is generated code not able to change #nullable disable to #nullable enable for select emitted files?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We can change. But I believe we really do not have to change it now.
The generator does not support generating nullable context yet.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@ArcturusZhang: I think the question that needs to be answered here is why are we looking at this as "all or nothing?" I would think that it should be possible to emit nullable in the scope of the unbranded writer without impacting DPG/MGP. Is that not the case, and if not - why?

Copy link
Copy Markdown

@ArcturusZhang ArcturusZhang Apr 3, 2024

Choose a reason for hiding this comment

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

@jsquire Yes it is possible to support that but not now, we could put this as a future plan.
I was trying to explain why we cannot do that now, and as a result, my PRs will not change the nullable context
cc @m-nash for further clarification if needed

// <auto-generated/>

#nullable disable
#nullable enable
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I don't have strong opinions about this implementation -- I just copied over the one from the customized OpenAI.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Tracking bug fixes for this with this issue: Azure/autorest.csharp#4480


/// <summary> Deserializes the model from a raw response. </summary>
/// <param name="response"> The result to deserialize the model from. </param>
/// <param name="response"> The response to deserialize the model from. </param>
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

These changes are low-priority since the APIs are internal, but if they are easy, they would make the code more readable and correct.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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


namespace OpenAI.Models
{
public partial class CreateTranscriptionRequest : IJsonModel<CreateTranscriptionRequest>
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I don't know offhand if we want MPFD input models to support IPersistableModel, but they are not JSON, so probably not IJsonModel. Let me know if you want to discuss what IPersistableModel would look like and on what timeline we would want to solve that.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

/// </para>
/// </summary>
public BinaryData File { get; }
public Stream File { get; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Split the information of a file to upload in Model as separate Properties works on the scenario which only have one file to upload, but it does not work on multiple files to upload. (Azure/autorest.csharp#4488 (comment) it add additional Properties to the model which may break the model. Considering the model will be used in both multipart and application/json, it may not correct to serialize to json format.
How about we define an new type for File in Multipart, as Azure/autorest.csharp#4488 (comment)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Responded to this comment here: #3 (comment)

Copy link
Copy Markdown

@ArcturusZhang ArcturusZhang Apr 2, 2024

Choose a reason for hiding this comment

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

Changes in this file is processing in this PR: Azure/autorest.csharp#4516

{
DateTimeKind.Utc => ToString((DateTimeOffset)value, format),
_ => throw new NotSupportedException($"DateTime {value} has a Kind of {value.Kind}. Azure SDK requires it to be UTC. You can call DateTime.SpecifyKind to change Kind property value to DateTimeKind.Utc.")
_ => throw new NotSupportedException($"DateTime {value} has a Kind of {value.Kind}. Generated clients require it to be UTC. You can call DateTime.SpecifyKind to change Kind property value to DateTimeKind.Utc.")
Copy link
Copy Markdown

@ArcturusZhang ArcturusZhang Apr 2, 2024

Choose a reason for hiding this comment

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

This change is resolved in this PR: Azure/autorest.csharp#4516

Comment on lines +40 to +41
PathBuilder.Append(value);
UriBuilder.Path = PathBuilder.ToString();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

comparing with the old code, we removed the pathSeparator part in this method. This is included in this PR: Azure/autorest.csharp#4519

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I did not include other removals in this PR yet. Will those break the current generated code? Those are various of overloads of the two remaining methods.
Would those removals require generator code changes in the clients? (I assume so)
If so, I suggest we delay those parts of changes, and prioritize others.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I'm not sure I fully understand your question or the concern here -- but please clarify, I am happy to answer any questions on this. I had filed Azure/autorest.csharp#4480 to track updates to ClientUriBuilder -- we have some problems with the current implementation around escaping that will need to be resolved, but if the OpenAI generated client is functional, we can de-prioritize those for the time being.

Copy link
Copy Markdown

@ArcturusZhang ArcturusZhang Apr 8, 2024

Choose a reason for hiding this comment

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

Let me rephrase myself: I compared the new code and old code of this file, because here github is not properly comparing it.
Your change consists of two parts:

  1. The first part included in this change: Azure/autorest.csharp@e5e175f (#4519)
  2. The second part is that we are removing those variant of AppendPath and AppendQuery (for instance the overload that takes DateTimeOffset). I am wondering that would this causes compile errors in the generated clients? Are you removing those because your specific case is not using those methods, or removing them is part of the issue?

In the meantime, could you have a review on this PR? Now this PR is very simple, I only included the first part of the change, which resolves an issue reported by openai

/// <exception cref="ClientResultException"> Service returned a non-success status code. </exception>
/// <returns> The response returned from the service. </returns>
public virtual async Task<ClientResult> CreateTranslationAsync(BinaryContent content, RequestOptions context = null)
public virtual async Task<ClientResult> CreateTranslationAsync(BinaryContent content, string contentType, RequestOptions options = null)

This comment was marked as outdated.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

OK I saw the other comments that this change belongs to MPFD changes

/// <param name="cancellationToken"> The cancellation token to use. </param>
/// <exception cref="ArgumentNullException"> <paramref name="audio"/> is null. </exception>
public virtual ClientResult<CreateTranslationResponse> CreateTranslation(CreateTranslationRequest audio, CancellationToken cancellationToken = default)
public virtual ClientResult<CreateTranslationResponse> CreateTranslation(CreateTranslationRequest audio)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

this change has been included in this PR: Azure/autorest.csharp#4524

Comment on lines +231 to +232
(BinaryContent content, string contentType, RequestOptions options) = audio.ToMultipartContentAsync().GetAwaiter().GetResult();
ClientResult result = CreateTranslation(content, contentType, options);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I believe this is part of the multipart change, this PR does not contain this change.

/// <item>
/// <description>
/// This <see href="https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/ProtocolMethods.md">protocol method</see> allows explicit creation of the request and processing of the response for advanced scenarios.
/// This <see href="https://azsdk/net/protocol/quickstart">protocol method</see> allows explicit creation of the request and processing of the response for advanced scenarios.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

is this link correct? I cannot open it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sure I will change the link to this new one

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