openai/openai-dotnet

[BUG] Bad status code deserialization in OpenAIError DeserializeOpenAIError

Closed this issue · 5 comments

Describe the bug

If the OpenAI api returns an error (witnessed via Azure OpenAI Services), with a response like

{
    "requestid": "1b7cfa98-de62-440f-b230-72919bfa42a0",
    "code": 400,
    "message": "Validation error at #/max_completion_tokens: Extra inputs are not permitted"
}

The code flow will try to deserialize the json like this:

internal static OpenAIError DeserializeOpenAIError(JsonElement element, ModelReaderWriterOptions options)
{
    if (element.ValueKind == JsonValueKind.Null)
    {
        return null;
    }
    string code = default;
    string message = default;
    string @param = default;
    string kind = default;
    IDictionary<string, BinaryData> additionalBinaryDataProperties = new ChangeTrackingDictionary<string, BinaryData>();
    foreach (var prop in element.EnumerateObject())
    {
        if (prop.NameEquals("code"u8))
        {
            if (prop.Value.ValueKind == JsonValueKind.Null)
            {
                code = null;
                continue;
            }
            code = prop.Value.GetString();
            continue;
        }
        if (prop.NameEquals("message"u8))
        {
            message = prop.Value.GetString();
            continue;
        }
        if (prop.NameEquals("param"u8))
        {
            if (prop.Value.ValueKind == JsonValueKind.Null)
            {
                @param = null;
                continue;
            }
            @param = prop.Value.GetString();
            continue;
        }
        if (prop.NameEquals("type"u8))
        {
            kind = prop.Value.GetString();
            continue;
        }
        // Plugin customization: remove options.Format != "W" check
        additionalBinaryDataProperties.Add(prop.Name, BinaryData.FromString(prop.Value.GetRawText()));
    }
    return new OpenAIError(code, message, @param, kind, additionalBinaryDataProperties);
}

It tries to read the "code" property and calls GetString() on it, but the response is a number.
throwing the following exception

System.InvalidOperationException
  HResult=0x80131509
  Message=The requested operation requires an element of type 'String', but the target element has type 'Number'.
  Source=System.Text.Json
  StackTrace:
   at System.Text.Json.ThrowHelper.ThrowJsonElementWrongTypeException(JsonTokenType expectedType, JsonTokenType actualType) in System.Text.Json\ThrowHelper.cs:line 288

The behavior is documented here: https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonelement.getstring?view=net-9.0
This method does not create a string representation of values other than JSON strings.

I don't know if your api previously returned strings instead of numbers for the code, depending on that you might want to check if the value is a number first and then use GetInt32() on it and call ToString on the number yourselves or simply call ToString() directly.

looking through your repo and searching for "code"u8: this, then you will find a lot of instances of that or very similar code

Steps to reproduce

This does not use OpenAiError directly, but RunError as the type is public, same logic as above applies, see code snippet below for repro.

Code snippets

using System.ClientModel.Primitives;
using System.Text.Json;
using OpenAI;
using OpenAI.Assistants;

namespace ConsoleApp3
{
    internal class Program
    {
        static void Main(string[] args)
        {
            byte[] json = """
                                {
                                    "requestid": "1b7cfa98-de62-440f-b230-72919bfa42a0",
                                    "code": 400,
                                    "message": "Validation error at #/max_completion_tokens: Extra inputs are not permitted"
                                }
                                """u8.ToArray();

#pragma warning disable OPENAI001
            var converter = new JsonModelConverter(new ModelReaderWriterOptions("W"), OpenAIContext.Default);
            var utf8JsonReader = new Utf8JsonReader(json);
            var output = converter.Read(ref utf8JsonReader, typeof(RunError), JsonSerializerOptions.Default);
#pragma warning restore OPENAI001
        }
    }
}

OS

Windows

.NET version

.NET 9

Library version

OpenAI 2.4

Hi @extedosse. Thank you for reaching out and we regret that you're experiencing difficulties. Apologies, but I'm not clear on the end-to-end scenario that you're describing. Can you please help us understand:

  • What operation you're invoking when you see the exception, and what configuration or options you're passing to trigger it.
  • Whether or not you're running against the official OpenAI service or the Azure OpenAI service.
  • Any other details that would be helpful in reproducing the behavior deterministically.

Hi, I apologize for the lack information, below is a full repro:

  • I was calling ChatCompleteAsync
  • It's done on Azure OpenAI Service
  • The important part is AzureSearchChatDataSource and one of the reasoning models, in this case gpt-5-mini.

AzureSearchChatDataSource is apparently not valid anymore for reasoning models and throws a validation error from the server and that error has the integer code in it.

I opened the issue for the parsing here, it is a spin-off bug of Azure/azure-sdk-for-net#52539 which is unrelated to the parsing issue.

using System.ClientModel;
using System.ClientModel.Primitives;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.AI.OpenAI.Chat;
using OpenAI;
using OpenAI.Assistants;
using OpenAI.Chat;

namespace ConsoleApp3
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            const string openAiEndpoint = "https://your_ai_endpoint.openai.azure.com/";
            const string openAiKey = "Key";
            const string searchEndpoint = "https://does_not_matter.search.windows.net";
            const string indexName = "vector";
            const string searchKey = "Key";
            const string semanticConfiguration = "vector-semantic-configuration";
            const string embeddingId = "text-embedding-3-small";
            const string modelDeploymentId = "gpt-5-mini";

            using var cts = new CancellationTokenSource();

            var aiClientOptions = new AzureOpenAIClientOptions
            {
                RetryPolicy = new ClientRetryPolicy()
            };
            var client = new AzureOpenAIClient(new Uri(openAiEndpoint), new ApiKeyCredential(openAiKey), aiClientOptions);

            var chatCompletionsOptions = new ChatCompletionOptions
            {
                MaxOutputTokenCount = 500,
            };
#pragma warning disable AOAI001
            // the actual values do not matter here, it's only populated for 
            var chatDataSource = new AzureSearchChatDataSource
            {
                Endpoint = new Uri(searchEndpoint),
                IndexName = indexName,
                InScope = true,
                QueryType = DataSourceQueryType.VectorSemanticHybrid,
                Authentication = DataSourceAuthentication.FromApiKey(searchKey),
                SemanticConfiguration = semanticConfiguration,
                VectorizationSource = DataSourceVectorizer.FromDeploymentName(embeddingId)
            };
            chatCompletionsOptions.AddDataSource(chatDataSource); // this triggers the exception
            chatCompletionsOptions.SetNewMaxCompletionTokensPropertyEnabled();
#pragma warning restore AOAI001
            var chatClient = client.GetChatClient(modelDeploymentId);
            var systemChatMessage = new SystemChatMessage("You're a nice AI agent.");
            var userMessage = new UserChatMessage("Hello World");
            // this will now throw, enable break on all Exceptions,so it breaks already on the System.InvalidOperationException
            // 'The requested operation requires an element of type 'String', but the target element has type 'Number'.'
            var completion = await chatClient.CompleteChatAsync([systemChatMessage, userMessage], chatCompletionsOptions, cts.Token).ConfigureAwait(false);
        }
    }
}

Hi @extedosse. Thanks for the additional information. The code in question is generated from the REST API spec for the OpenAI service. Looking over the spec, Error defines the code as a string. To rule out a spec issue, I double-checked the YAML form of the spec here, the API docs and the Python SDK source. These are also string for the code.

In your scenario, the Azure OpenAI service is not following the service contract properly, which is a service bug that Azure will need to fix. Unfortunately, the maintainers of the OpenAI library cannot assist.

Because Azure does not provide service support from a GitHub repository, we cannot transfer this on your behalf. To ensure that the right team has visibility and can help, your best path forward for would be to open an Azure support request or inquire on the Microsoft Q&A site.

//cc: @trrwilson, for awareness.

Thank you for your answer, I'll try my luck with your suggestions.