Skip to content

Conversation

@christothes
Copy link
Collaborator

No description provided.

Copy link
Collaborator

@KrzysztofCwalina KrzysztofCwalina left a comment

Choose a reason for hiding this comment

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

This is very nice and close. I added 3 comments, but supper happy to see how this does not complicate the namespace too much (or at all)

protected override BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options);
}
[Experimental("OPENAI001")]
public class CreateResponseOptions : IJsonModel<CreateResponseOptions>, IPersistableModel<CreateResponseOptions> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

There are only minor differences between this class and the existing ResponseCreationOptions:

  1. Renames.
  2. The Input property, which is a parameter of the CreateResponse client method.
  3. The Model property, which is missing here but it is also a parameter of the CreateResponse client method.
  4. The Stream property, which is hidden because we set on behalf of the user when they call CreateResponseStreaming.

I think the renames are desirable, and I would be in favor of adding the missing properties to ResponseCreationOptions and have that one be the protocol model.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I implemented the renames.

protected override ResponseTool PersistableModelCreateCore(BinaryData data, ModelReaderWriterOptions options);
protected override BinaryData PersistableModelWriteCore(ModelReaderWriterOptions options);
}
public partial struct GetResponseInputItemsOptions {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is identical to ResponseItemCollectionOptions except for one single difference: This class includes the ResponseId.

Furthermore, this is an interesting case because technically this model does not actually exist. What I mean is that the properties here actually correspond to path and query parameters instead of a body parameter. Personally, I do see merits in having a property bag that holds the optional parameters (which is what we do today with ResponseItemCollectionOptions), but I have a feeling that a "protocol model" is not applicable in this case.

public string Order { get; set; }
public string ResponseId { get; set; }
}
public class GetResponseOptions {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is similar to my comment about GetResposneInputItemsOptions: The properties here are path and query parameters (not a body parameter), so I wonder if having a "protocol model" here is actually applicable.

public override readonly string ToString();
}
[Experimental("OPENAI001")]
public enum Includable {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have a PR out that adds this enum:
🔗 #820

Once it's merged, that's one less thing that you will need to keep in this PR. 🙂

Copy link
Collaborator

Choose a reason for hiding this comment

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

This PR has been merged.

public override readonly string ToString();
}
[Experimental("OPENAI001")]
public class ResponseResult : IJsonModel<ResponseResult>, IPersistableModel<ResponseResult> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

There are only minor differences between this class and the existing OpenAIResponse:

  1. Renames.
  2. The Object property, which is hidden in OpenAIResponse because it's not very useful in .NET.
  3. The OutputText property, which is not a real service property, but a convenience the Stainless added for their own libraries. As such, it shouldn't be included in the "protocol model". In OpenAIResponse, we exposed it via the GetOutputText method.

I think the renames are desirable. I would be in favor of adding the Object property to OpenAIResponse but hiding it with EBN. And I think the GetOutputText is desirable over an OutputText property.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I implemented the renames. I also marked OutputText as internal. We already had GetOutputText

public virtual Uri Endpoint { get; }
[Experimental("OPENAI001")]
public string Model { get; }
public virtual string Model { get; }
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm curious: Why do Endpoint and Model need to be virtual?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I found that sometimes non-virtual properties are nulled when referncing them in an instrumented client (during tests)

public virtual Task<ClientResult<ResponseResult>> GetResponseAsync(GetResponseOptions options, CancellationToken cancellationToken = default);
public virtual Task<ClientResult> GetResponseAsync(string responseId, bool? stream, int? startingAfter, RequestOptions options);
public virtual Task<ClientResult<OpenAIResponse>> GetResponseAsync(string responseId, CancellationToken cancellationToken = default);
public virtual ClientResult<ResponseItemList> GetResponseInputItems(GetResponseInputItemsOptions options = default, CancellationToken cancellationToken = default);
Copy link
Collaborator

Choose a reason for hiding this comment

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

public virtual ClientResult<ResponseItemList> GetResponseInputItems(GetResponseInputItemsOptions options = default, CancellationToken cancellationToken = default);
public virtual CollectionResult<ResponseItem> GetResponseInputItems(string responseId, ResponseItemCollectionOptions options = null, CancellationToken cancellationToken = default);
public virtual CollectionResult GetResponseInputItems(string responseId, int? limit, string order, string after, string before, RequestOptions options);
public virtual Task<ClientResult<ResponseItemList>> GetResponseInputItemsAsync(GetResponseInputItemsOptions options = default, CancellationToken cancellationToken = default);
public virtual AsyncCollectionResult<ResponseItem> GetResponseInputItemsAsync(string responseId, ResponseItemCollectionOptions options = null, CancellationToken cancellationToken = default);
public virtual AsyncCollectionResult GetResponseInputItemsAsync(string responseId, int? limit, string order, string after, string before, RequestOptions options);        

Looking at the above, I'm a little conflicted about having six methods for each list operation. Additionally, I think it's a little odd that the protocol method has pagination conveniences, while the convenience method that takes protocol models does not have pagination conveniences (it's like neither variant is fully protocol nor fully convenience). I wonder if we can simplify this...

What would you think of the following: What if we do not add list methods that return a protocol model? 99% of users won't benefit from these, and they would instead favor the methods that can paginate. If necessary, I would be in favor of shipping the protocol model by itself to support MRW scenarios and server scenarios.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think the protocol model return types give better control over advanced pagination scenarios, but I agree they are probably needed rarely. I do think there is some value in having access to the raw protocol model without the pagination abstractions.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@KrzysztofCwalina What is the story for users growing from paginating protocol methods to convenience methods?

public void Example01_SimpleResponse()
{
OpenAIResponseClient client = new(model: "gpt-5", apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY"));
ResponseClient client = new(model: "gpt-5", apiKey: Environment.GetEnvironmentVariable("OPENAI_API_KEY"));
Copy link
Collaborator

Choose a reason for hiding this comment

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

we should call it ResponsesClient, not ResponseClient

public virtual AsyncCollectionResult GetResponseInputItemsAsync(string responseId, int? limit, string order, string after, string before, RequestOptions options);
public virtual CollectionResult<StreamingResponseUpdate> GetResponseStreaming(GetResponseOptions options, CancellationToken cancellationToken = default);
public virtual AsyncCollectionResult<StreamingResponseUpdate> GetResponseStreamingAsync(GetResponseOptions options, CancellationToken cancellationToken = default);
public readonly partial struct ModelIdsResponses : IEquatable<ModelIdsResponses> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

@joseharriaga, do we really want this type? It seems like it will be continuously out of date.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't. The model should be represented as a simple string.

public string FirstId { get; }
public bool HasMore { get; }
public string LastId { get; }
public string Object { get; }
Copy link
Collaborator

Choose a reason for hiding this comment

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

we should consider renaming this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

About the Object property, I think we should make it internal, although in the spirit of protocol models, I suppose we need to keep it? If so, we should at least EBN it.

[Experimental("OPENAI001")]
public class ResponsesClient {
protected ResponsesClient();
protected internal ResponsesClient(ClientPipeline pipeline, string model, OpenAIClientOptions options);
Copy link
Collaborator

Choose a reason for hiding this comment

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

@m-nash, it would be good to change this into your new ctor pattern before we GA, i.e. not necessarily in this PR.

@christothes christothes marked this pull request as ready for review November 13, 2025 18:55
public IList<Includable> Include { get; set; }
public IList<ResponseItem> Input { get; }
public string Instructions { get; set; }
public bool? IsBackgroundModeEnabled { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

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

The pattern that we have used so far does not use the "Is" prefix, so I'm thinking that maybe we should stick to it for consistency.

public bool? IsStreamingEnabled { get; set; }
public int? MaxOutputTokenCount { get; set; }
public IDictionary<string, string> Metadata { get; }
public ModelIdsResponses? Model { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Historically, we don't use an enum for model names, and instead we keep it just as a string.

public class CreateResponseOptions : IJsonModel<CreateResponseOptions>, IPersistableModel<CreateResponseOptions> {
public CreateResponseOptions(List<ResponseItem> input);
public string EndUserId { get; set; }
public IList<Includable> Include { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the change where I introduced this property (which just got merged), I called it "IncludedProperties".

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's also not supposed to have a setter because it's a collection property.

public CreateResponseOptions(List<ResponseItem> input);
public string EndUserId { get; set; }
public IList<Includable> Include { get; set; }
public IList<ResponseItem> Input { get; }
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Rename to "InputItems" to make it clearer that this is a collection of items.

}
[Experimental("OPENAI001")]
public class CreateResponseOptions : IJsonModel<CreateResponseOptions>, IPersistableModel<CreateResponseOptions> {
public CreateResponseOptions(List<ResponseItem> input);
Copy link
Collaborator

Choose a reason for hiding this comment

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

For this constructor, we should consider:

  1. Adding a string model parameter
  2. Renaming the input parameter to "inputItems"
  3. Changing the type of the input parameter from List<ResponseItem> to IEnumerable<ResponseItem>.

}
[Experimental("OPENAI001")]
public class ResponseResult : IJsonModel<ResponseResult>, IPersistableModel<ResponseResult> {
public DateTimeOffset CreatedAt { get; }
Copy link
Collaborator

Choose a reason for hiding this comment

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

ResponseResult is an output model, but it is also a protocol model. Do the properties need both: getters and setters?

public override readonly string ToString();
}
[Experimental("OPENAI001")]
public static class OpenAIResponsesModelFactory {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Assuming that the properties of the new ResponseResult don't have setters, we need a new method in this model factory to instantiate a mock ResponseResult.

/// <returns> A new <see cref="ResponsesClient"/>. </returns>
[Experimental("OPENAI001")]
public virtual OpenAIResponseClient GetOpenAIResponseClient(string model) => new(Pipeline, model, _options);
public virtual ResponsesClient GetResponsesClient(string model) => new(Pipeline, model, _options);
Copy link
Collaborator

Choose a reason for hiding this comment

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

There are a few generator attributes at the top of this file that need to be updated with the new name.

public virtual Task<ClientResult> CancelResponseAsync(string responseId, RequestOptions options);
public virtual Task<ClientResult<OpenAIResponse>> CancelResponseAsync(string responseId, CancellationToken cancellationToken = default);
public virtual ClientResult CreateResponse(BinaryContent content, RequestOptions options = null);
public virtual ClientResult<OpenAIResponse> CreateResponse(IEnumerable<ResponseItem> inputItems, ResponseCreationOptions options = null, CancellationToken cancellationToken = default);
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the OpenAI library, we started using this pattern of separating the required properties from the optional properties for a few reasons:

  1. Users found it awkward to have use two different ways to set the properties of the options class: They had to use the constructor for required properties, and initialization syntax for the optional properties. The options classes in the OpenAI API usually have tons of properties, and it is very common to at least want to set one or two. So this issue was very front-and-center. By making the options class all optional properties, this pain point was mitigated.
  2. Users wanted to re-use an instance of the options class across requests. Consider the following scenario: When using stateful responses, if the input items are in the options class, users need to clear the collection and then re-populate it with the new input items every time. By moving the input items out of the options class, users can forget about the options instance entirely.

Would we want to preserve this functionality?

What if we kept the convenience methods as they are right now?

Here's an example of what I'm thinking...

We keep the following method:

public virtual ClientResult<OpenAIResponse> CreateResponse(IEnumerable<ResponseItem> inputItems, ResponseCreationOptions options = null, CancellationToken cancellationToken = default);

And then, we add a parameterless constructor to the options class. If a user specifies inputItems in both, the method parameter and the options property, we throw an exception (similar to what we would do if they set the stream property but call the non-streaming method).

public virtual Task<ClientResult<ResponseResult>> GetResponseAsync(GetResponseOptions options, CancellationToken cancellationToken = default);
public virtual Task<ClientResult> GetResponseAsync(string responseId, bool? stream, int? startingAfter, RequestOptions options);
public virtual Task<ClientResult<OpenAIResponse>> GetResponseAsync(string responseId, CancellationToken cancellationToken = default);
public virtual ClientResult<ResponseItemList> GetResponseInputItems(GetResponseInputItemsOptions options = default, CancellationToken cancellationToken = default);
Copy link
Collaborator

Choose a reason for hiding this comment

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

@KrzysztofCwalina What is the story for users growing from paginating protocol methods to convenience methods?

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.

3 participants