Investigation: Azure and unbranded clients#5
Investigation: Azure and unbranded clients#5annelo-msft wants to merge 26 commits intoazoai-generate-cleanfrom
Conversation
| using OpenAI.Models; | ||
| using System.ClientModel; | ||
|
|
||
| using AzureOpenAI.Models; |
There was a problem hiding this comment.
Once you add the namespace with the extension methods/properties, you can use an unbranded model like an Azure model. How do we make sure people instantiate the client as an Azure client and only send Azure properties to the Azure service?
|
|
||
| namespace AzureOpenAI; | ||
|
|
||
| internal class PersistableModelList<TModel> : List<IPersistableModel<TModel>>, IPersistableModel<PersistableModelList<TModel>> |
There was a problem hiding this comment.
This is a proposal for a new emitted type.
| [PersistableModelProxy(typeof(UnknownAzureChatExtensionConfiguration))] | ||
| public partial class AzureChatExtensionConfiguration : IJsonModel<AzureChatExtensionConfiguration> | ||
| { | ||
| void IJsonModel<AzureChatExtensionConfiguration>.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) |
There was a problem hiding this comment.
Per @KrzysztofCwalina, look at different options for "Wire" and "AzureWire"
There was a problem hiding this comment.
We decided to move away from this approach in favor of having separate models for each type.
|
|
||
| namespace AzureOpenAI.Models; | ||
|
|
||
| internal class AzureCreateChatCompletionRequest : CreateChatCompletionRequest |
There was a problem hiding this comment.
I think this goes away entirely.
There was a problem hiding this comment.
Actually, maybe this is better than keeping a Dictionary<string, object> ...
There was a problem hiding this comment.
Discussed offline and for now we will continue to pursue the extension methods approach
There was a problem hiding this comment.
New approach is to keep these model subtypes types internal and use extension properties to retrieve values via internal properties on subtypes.
|
|
||
| namespace AzureOpenAI.Models; | ||
|
|
||
| public partial class AzureSearchChatExtensionConfiguration : IJsonModel<AzureSearchChatExtensionConfiguration> |
There was a problem hiding this comment.
Note, some models like these are only in the Azure package. But we assign instances of them at runtime to objects in the unbranded package and reference them as IJsonModel. Think a bit more about this and the implications of it.
There was a problem hiding this comment.
One angle: what does the generator do?
Side question on that - how does the Tsp represent the differences so the generator can do a different thing?
| /// <param name="keyCredential"> The key credential to copy. </param> | ||
| /// <param name="endpoint"> OpenAI Endpoint. </param> | ||
| internal Chat(ClientPipeline pipeline, ApiKeyCredential keyCredential, Uri endpoint) | ||
| protected internal Chat(ClientPipeline pipeline, ApiKeyCredential keyCredential, Uri endpoint) |
There was a problem hiding this comment.
The subclient constructor becomes protected internal.
| private readonly ClientPipeline _pipeline; | ||
| private readonly Uri _endpoint; | ||
|
|
||
| protected Uri Endpoint => _endpoint; |
There was a problem hiding this comment.
Some client internal fields become protected properties so the derived type can use them.
| } | ||
|
|
||
| internal PipelineMessage CreateCreateChatCompletionRequest(BinaryContent content, RequestOptions context) | ||
| protected virtual PipelineMessage CreateCreateChatCompletionRequest(BinaryContent content, RequestOptions context) |
There was a problem hiding this comment.
Request creation helpers become protected virtual.
|
|
||
| // Note: we were holding strongly-typed values for added Azure properties | ||
| // We have to serialize them separately here. | ||
| // TODO: Does the format matter here? It seems like we want to write them |
There was a problem hiding this comment.
Look at this in the context of different options for Wire-3rd party and Wire-Azure.
|
|
||
| writer.WritePropertyName(property.Key); | ||
|
|
||
| // Note: what if it's just a primitive, i.e. a string or int, what do we do? |
| /// </summary> | ||
| private IDictionary<string, BinaryData> _serializedAdditionalRawData; | ||
|
|
||
| private IDictionary<string, object> _additionalTypedProperties; |
| public partial class OpenAIClient | ||
| { | ||
| protected ApiKeyCredential KeyCredential => _keyCredential; | ||
| protected Uri Endpoint => _endpoint; |
There was a problem hiding this comment.
Some client fields become protected properties so that they're accessible to derived clients. Consider this approach.
| /// <summary> The OpenAI service client. </summary> | ||
| public partial class OpenAIClient | ||
| { | ||
| protected ApiKeyCredential KeyCredential => _keyCredential; |
There was a problem hiding this comment.
One problem with this is that if the client doesn't use api key auth, what does this do?
| { | ||
| } | ||
|
|
||
| // TODO: Show how this would differ for this case. Do we still need OperationName and the |
There was a problem hiding this comment.
Confirmed we don't need OperationName for this approach.
| // and stick it in AdditionalTypedProperties (are these "deserialized | ||
| // additional properties, e.g.?). | ||
|
|
||
| // TODO: what is our concurrency story for models in unbranded clients? |
There was a problem hiding this comment.
| { | ||
| public static IList<AzureChatExtensionConfiguration> GetDataSources(this CreateChatCompletionRequest request) | ||
| { | ||
| // TODO: How can we validate that this is being called in the right context, |
There was a problem hiding this comment.
How can we throw an exception when using the wrong client? Seems like MRW.Options is useful here.
| // 2. The extension properties are actually quite simple -- they just get | ||
| // or set stuff in either the input or output properties dictionaries. | ||
| // | ||
| // TODO: What is the interplay of SerializedAdditionalRawData and |
…col method to Azure client
| Argument.AssertNotNull(createChatCompletionRequest, nameof(createChatCompletionRequest)); | ||
|
|
||
| // Initialize Azure input model | ||
| AzureCreateChatCompletionRequest azureChatCompletionOptions = new(createChatCompletionRequest); |
There was a problem hiding this comment.
Could you just wrap and then call the base implementation? It would be good to not have to reimplement the convenience methods totally from scratch.
There was a problem hiding this comment.
I think when the protocol method signature is the same that will work. But since here we needed to add an overload for the protocol method because an input value has moved from the request body into a path parameter, we need to reimplement it to call the new protocol method overload.
There was a problem hiding this comment.
Ah, makes sense. But, where is the new protocol method? I have a hard time finding it. I was interested in seeing how you think we will make the new protocol method available to the callers.
There was a problem hiding this comment.
OK, I found the protocol method. But, the method needs to be an extension method, not just a public method on the internal type. People won't be able to call it, if it's an instance method
There was a problem hiding this comment.
Good point! That is my mistake.
| string IPersistableModel<AzureCreateChatCompletionRequest>.GetFormatFromOptions(ModelReaderWriterOptions options) | ||
| => "J"; | ||
|
|
||
| void IJsonModel<AzureCreateChatCompletionRequest>.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options) |
There was a problem hiding this comment.
likewise, I wonder if we could reuse the serialization implementation for the things that are the same and only add/modify the parts that are different.
There was a problem hiding this comment.
That seems likely. My question on that would be -- what is the value of doing it? I'm still wondering how we represent the added properties in the TypeSpec to indicate to the generator to generate just the diff from the base type. Once we solve that piece , it'll be easier to say which approach would be less work for the generator. If there's strong value to not duplicating generated serialization code, then we can prioritize figuring that part out.
There was a problem hiding this comment.
yeah, it's a good point that it will be easier on the generator to not have to understand such tricks. As far as the generator will be concerned is that there are two model types and two typespecs. It will generate serialization code as if there was no hierarchy,
|
|
||
| public override Task<ClientResult> CreateChatCompletionAsync(BinaryContent content, RequestOptions context = null) | ||
| { | ||
| // Note, that we can later remap the values from the 3rd party client format to the |
There was a problem hiding this comment.
it would be good to hide this one, but I don't think we can do it given we don't expose the subtype
There was a problem hiding this comment.
Yes, that makes sense. If we did expose the subtype, though, I think is a clear violation of the Liskov substitution principle -- I think you told me a few weeks ago that if we hide base type methods on the subtype, it makes the abstraction confusing ... why could I call this method on the base client, and now I don't see that method in the intellisense when I'm looking at a subtype? I dunno.
…ses 1-1 with the type they extend
This PR looks at the model-inheritance approach.