dotnet / extensions

This repository contains a suite of libraries that provide facilities commonly needed when creating production-ready applications.
MIT License
2.63k stars 752 forks source link

Invalid response_format.json_schema.name when structured output type is generic #5501

Closed kzu closed 2 weeks ago

kzu commented 2 weeks ago

Description

Given a record like public record Movie(string Title, int Year, ...);, when invoking the client with var movies = await client.CompleteAsync<Movie[]>(...) we get an exception:

Unhandled exception. System.ClientModel.ClientResultException: HTTP 400 (invalid_request_error: invalid_value)
Parameter: response_format.json_schema.name

Invalid 'response_format.json_schema.name': string does not match pattern. Expected a string that matches the pattern '^[a-zA-Z0-9_-]+$'.
   at OpenAI.ClientPipelineExtensions.ProcessMessageAsync(ClientPipeline pipeline, PipelineMessage message, RequestOptions options)
   at OpenAI.Chat.ChatClient.CompleteChatAsync(BinaryContent content, RequestOptions options)
   at OpenAI.Chat.ChatClient.CompleteChatAsync(IEnumerable`1 messages, ChatCompletionOptions options, CancellationToken cancellationToken)
   at Microsoft.Extensions.AI.OpenAIChatClient.CompleteAsync(IList`1 chatMessages, ChatOptions options, CancellationToken cancellationToken)
   at Microsoft.Extensions.AI.OpenTelemetryChatClient.CompleteAsync(IList`1 chatMessages, ChatOptions options, CancellationToken cancellationToken)
   at Microsoft.Extensions.AI.LoggingChatClient.CompleteAsync(IList`1 chatMessages, ChatOptions options, CancellationToken cancellationToken)
   at Microsoft.Extensions.AI.ChatClientStructuredOutputExtensions.CompleteAsync[T](IChatClient chatClient, IList`1 chatMessages, JsonSerializerOptions serializerOptions, ChatOptions options, Nullable`1 useNativeJsonSchema, CancellationToken cancellationToken)
   at Program.<Main>$(String[] args) in C:\Code\script\TypedChat.cs:line 39
   at Program.<Main>(String[] args)

Reproduction Steps

var services = new ServiceCollection()
    .AddChatClient(builder => builder
        .Use(new OpenAI.OpenAIClient(configuration["OpenAI:Key"]!).AsChatClient("gpt-4o-mini")))
    .BuildServiceProvider();

var movies = await client.CompleteAsync<Movie[]>("Top 5 movies from Christopher Nolan", useNativeJsonSchema: true);

Console.WriteLine(movies.Length);

public record Movie(string Title, int Year);

If you switch the type to be a wrapper type, things work, such as: public record MoviesResult(Movie[] Movies);.

Note that switching to a non-generic type such as public class Movies : List<Movie> { } also fails, but this time with:

Unhandled exception. System.ClientModel.ClientResultException: HTTP 400 (invalid_request_error: )
Parameter: response_format

Invalid schema for response_format 'Movies': schema must be a JSON Schema of 'type: "object"', got 'type: "array"'.
   at OpenAI.ClientPipelineExtensions.ProcessMessageAsync(ClientPipeline pipeline, PipelineMessage message, RequestOptions options)
   at OpenAI.Chat.ChatClient.CompleteChatAsync(BinaryContent content, RequestOptions options)
   at OpenAI.Chat.ChatClient.CompleteChatAsync(IEnumerable`1 messages, ChatCompletionOptions options, CancellationToken cancellationToken)

In this case, it's due to the fact that OpenAI expects the root schema element to be an object, and the JSON exporter infers it to be an array instead. This can be solved by detecting this situation automatically and using a wrapper type automatically and unwrapping before returning, such as

    public class Values<T>
    {
        public required T Data { get; set; }
    }

Expected behavior

Typed chats work regardless of which T you are using: arrays, lists, dictionaries,etc. should all be supported and work as-is.

Actual behavior

Multiple failure modes depending on what types you are specifying for the result, with many "gotchas" to learn as a consequence. User doesn't fall in the proverbial pit of success :)

Regression?

No response

Known Workarounds

Use "Response" wrapper types always. Very annoying.

Configuration

<PackageReference Include="Microsoft.Extensions.AI" Version="9.0.0-preview.9.24507.7" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.0.0-preview.9.24507.7" />

SDK: 9.0.100-rc.1.24452.12

Other information

No response

stephentoub commented 2 weeks ago

cc: @SteveSandersonMS

kzu commented 2 weeks ago

The naming issue should be fixed with something like https://github.com/dotnet/extensions/pull/5504