diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b997b71..f0271ed 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - 3.0.0 + 3.1.0 enable enable 12 diff --git a/src/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionModels.cs b/src/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionModels.cs index 6c0feb6..d587079 100644 --- a/src/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionModels.cs +++ b/src/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionModels.cs @@ -85,6 +85,7 @@ public static class ChatCompletionModels /// This model has a maximum token limit of 4,096. /// The model was trained with data up to September 2021. /// + [Obsolete("Legacy. Snapshot of gpt-3.5-turbo from June 13th 2023. Will be deprecated on June 13, 2024.")] public const string Gpt3_5_Turbo_0613 = "gpt-3.5-turbo-0613"; /// @@ -93,6 +94,7 @@ public static class ChatCompletionModels /// This model has a maximum token limit of 16,384. /// The model was trained with data up to September 2021. /// + [Obsolete("Legacy. Snapshot of gpt-3.5-16k-turbo from June 13th 2023. Will be deprecated on June 13, 2024.")] public const string Gpt3_5_Turbo_16k_0613 = "gpt-3.5-turbo-16k-0613"; /// @@ -101,7 +103,7 @@ public static class ChatCompletionModels /// Unlike gpt-4, this model will not receive updates, /// and will only be supported for a three month period ending on June 14th 2023. /// - [Obsolete("DISCONTINUATION DATE 09/13/2023")] + [Obsolete("Legacy. Snapshot of gpt-4 from March 14th 2023 with function calling support. This model version will be deprecated on June 13th 2024. Use Gpt4 instead.")] public const string Gpt4_0314 = "gpt-4-0314"; /// @@ -109,8 +111,7 @@ public static class ChatCompletionModels /// Unlike gpt-4-32k, this model will not receive updates, /// and will only be supported for a three month period ending on June 14th 2023. /// - [Obsolete("DISCONTINUATION DATE 09/13/2023. This model is available only by request. " + - "Link for joining waitlist: https://openai.com/waitlist/gpt-4-api")] + [Obsolete("Legacy. Snapshot of gpt-4-32k from March 14th 2023 with function calling support. This model version will be deprecated on June 13th 2024. Use Gpt432k instead.")] public const string Gpt4_32k_0314 = "gpt-4-32k-0314"; /// @@ -118,9 +119,13 @@ public static class ChatCompletionModels /// Unlike gpt-3.5-turbo, this model will not receive updates, /// and will only be supported for a three month period ending on June 1st 2023. /// - [Obsolete("DISCONTINUATION DATE 09/13/2023")] + [Obsolete("Snapshot of gpt-3.5-turbo from March 1st 2023. Will be deprecated on June 13th 2024. Use Gpt3_5_Turbo instead.")] public const string Gpt3_5_Turbo_0301 = "gpt-3.5-turbo-0301"; + private static readonly string[] ModelsSupportedJson = { + Gpt4Turbo, Gpt3_5_Turbo_1106 + }; + /// /// The maximum number of tokens that can be processed by the model. /// @@ -132,10 +137,10 @@ public static class ChatCompletionModels { Gpt4_32k, 32_768 }, { Gpt4_32k_0613, 32_768 }, { Gpt3_5_Turbo, 4096 }, - { Gpt3_5_Turbo_1106, 16385 }, - { Gpt3_5_Turbo_16k, 16_384 }, + { Gpt3_5_Turbo_1106, 4096 }, + { Gpt3_5_Turbo_16k, 16_385 }, { Gpt3_5_Turbo_0613, 4096 }, - { Gpt3_5_Turbo_16k_0613, 16_384 }, + { Gpt3_5_Turbo_16k_0613, 16_385 }, { Gpt4_0314, 8192 }, { Gpt4_32k_0314, 32_768 }, { Gpt3_5_Turbo_0301, 4096 }, @@ -222,4 +227,20 @@ public static void EnsureMaxTokensIsSupportedByAnyModel(int maxTokens) nameof(maxTokens), $"Max tokens must be less than or equal to {limit} but was {maxTokens}"); } } + + /// + /// Checks if the model name is supported for JSON mode + /// + /// GPT model name + /// True if the model is supported for JSON mode + public static bool IsJsonModeSupported(string model) + { + ArgumentNullException.ThrowIfNull(model); + return Array.IndexOf(ModelsSupportedJson, model) != -1; + } + + internal static IReadOnlyList GetModelsThatSupportJsonMode() + { + return ModelsSupportedJson; + } } \ No newline at end of file diff --git a/src/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionRequest.cs b/src/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionRequest.cs index 8ee7358..9834361 100644 --- a/src/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionRequest.cs +++ b/src/OpenAI.ChatGpt/Models/ChatCompletion/ChatCompletionRequest.cs @@ -145,7 +145,7 @@ public int MaxTokens /// An object specifying the format that the model must output. /// [JsonPropertyName("response_format")] - public ChatCompletionResponseFormat ResponseFormat { get; set; } = new(); + public ChatCompletionResponseFormat ResponseFormat { get; set; } = new(false); /// /// This feature is in Beta. @@ -158,6 +158,11 @@ public int MaxTokens public class ChatCompletionResponseFormat { + public ChatCompletionResponseFormat(bool jsonMode) + { + Type = jsonMode ? ResponseTypes.JsonObject : ResponseTypes.Text; + } + /// /// Setting to `json_object` enables JSON mode. This guarantees that the message the model generates is valid JSON. /// Note that your system prompt must still instruct the model to produce JSON, and to help ensure you don't forget, @@ -167,6 +172,12 @@ public class ChatCompletionResponseFormat /// Must be one of `text` or `json_object`. /// [JsonPropertyName("type")] - public string Type { get; set; } = "text"; + public string Type { get; set; } + } + + internal static class ResponseTypes + { + public const string Text = "text"; + public const string JsonObject = "json_object"; } } \ No newline at end of file diff --git a/src/OpenAI.ChatGpt/OpenAiClient.cs b/src/OpenAI.ChatGpt/OpenAiClient.cs index 5ee30c5..1ae2ac5 100644 --- a/src/OpenAI.ChatGpt/OpenAiClient.cs +++ b/src/OpenAI.ChatGpt/OpenAiClient.cs @@ -9,11 +9,11 @@ namespace OpenAI.ChatGpt; -/// Thread-safe OpenAI client. +/// Thread-safe OpenAI client. +/// https://github.com/openai/openai-openapi/blob/master/openapi.yaml [Fody.ConfigureAwait(false)] public class OpenAiClient : IOpenAiClient, IDisposable { - internal const string HttpClientName = "OpenAiClient"; private const string DefaultHost = "https://api.openai.com/v1/"; private const string ChatCompletionsEndpoint = "chat/completions"; @@ -142,6 +142,7 @@ public async Task GetChatCompletions( { if (dialog == null) throw new ArgumentNullException(nameof(dialog)); if (model == null) throw new ArgumentNullException(nameof(model)); + EnsureJsonModeIsSupported(model, jsonMode); ThrowIfDisposed(); var request = CreateChatCompletionRequest( dialog.GetMessages(), @@ -174,6 +175,7 @@ public async Task GetChatCompletions( { if (messages == null) throw new ArgumentNullException(nameof(messages)); if (model == null) throw new ArgumentNullException(nameof(model)); + EnsureJsonModeIsSupported(model, jsonMode); ThrowIfDisposed(); var request = CreateChatCompletionRequest( messages, @@ -205,6 +207,7 @@ public async Task GetChatCompletionsRaw( { if (messages == null) throw new ArgumentNullException(nameof(messages)); if (model == null) throw new ArgumentNullException(nameof(model)); + EnsureJsonModeIsSupported(model, jsonMode); ThrowIfDisposed(); var request = CreateChatCompletionRequest( messages, @@ -225,7 +228,7 @@ internal async Task GetChatCompletionsRaw( ChatCompletionRequest request, CancellationToken cancellationToken = default) { - if (request == null) throw new ArgumentNullException(nameof(request)); + ArgumentNullException.ThrowIfNull(request); ThrowIfDisposed(); var response = await _httpClient.PostAsJsonAsync( ChatCompletionsEndpoint, @@ -258,6 +261,7 @@ public IAsyncEnumerable StreamChatCompletions( { if (messages == null) throw new ArgumentNullException(nameof(messages)); if (model == null) throw new ArgumentNullException(nameof(model)); + EnsureJsonModeIsSupported(model, jsonMode); ThrowIfDisposed(); var request = CreateChatCompletionRequest( messages, @@ -292,10 +296,7 @@ private static ChatCompletionRequest CreateChatCompletionRequest( Stream = stream, User = user, Temperature = temperature, - ResponseFormat = new ChatCompletionRequest.ChatCompletionResponseFormat() - { - Type = jsonMode ? "json_object" : "text" - }, + ResponseFormat = new ChatCompletionRequest.ChatCompletionResponseFormat(jsonMode), Seed = seed, }; requestModifier?.Invoke(request); @@ -316,6 +317,7 @@ public IAsyncEnumerable StreamChatCompletions( { if (messages == null) throw new ArgumentNullException(nameof(messages)); if (model == null) throw new ArgumentNullException(nameof(model)); + EnsureJsonModeIsSupported(model, jsonMode); ThrowIfDisposed(); var request = CreateChatCompletionRequest(messages.GetMessages(), maxTokens, @@ -335,7 +337,9 @@ public async IAsyncEnumerable StreamChatCompletions( ChatCompletionRequest request, [EnumeratorCancellation] CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(request); if (request == null) throw new ArgumentNullException(nameof(request)); + EnsureJsonModeIsSupported(request.Model, request.ResponseFormat.Type == ChatCompletionRequest.ResponseTypes.JsonObject); ThrowIfDisposed(); request.Stream = true; await foreach (var response in StreamChatCompletionsRaw(request, cancellationToken)) @@ -351,6 +355,7 @@ public IAsyncEnumerable StreamChatCompletionsRaw( ChatCompletionRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); + EnsureJsonModeIsSupported(request.Model, request.ResponseFormat.Type == ChatCompletionRequest.ResponseTypes.JsonObject); ThrowIfDisposed(); request.Stream = true; return _httpClient.StreamUsingServerSentEvents @@ -361,4 +366,15 @@ public IAsyncEnumerable StreamChatCompletionsRaw( cancellationToken ); } + + private static void EnsureJsonModeIsSupported(string model, bool jsonMode) + { + if(jsonMode && !ChatCompletionModels.IsJsonModeSupported(model)) + { + throw new NotSupportedException( + $"Model {model} does not support JSON mode. " + + $"Supported models are: {string.Join(", ", ChatCompletionModels.GetModelsThatSupportJsonMode())}" + ); + } + } } \ No newline at end of file diff --git a/src/modules/OpenAI.ChatGpt.Modules.StructuredResponse/OpenAiClientExtensions.GetStructuredResponse.cs b/src/modules/OpenAI.ChatGpt.Modules.StructuredResponse/OpenAiClientExtensions.GetStructuredResponse.cs index 6831ff8..81db9cd 100644 --- a/src/modules/OpenAI.ChatGpt.Modules.StructuredResponse/OpenAiClientExtensions.GetStructuredResponse.cs +++ b/src/modules/OpenAI.ChatGpt.Modules.StructuredResponse/OpenAiClientExtensions.GetStructuredResponse.cs @@ -71,7 +71,8 @@ public static Task GetStructuredResponse( ArgumentNullException.ThrowIfNull(dialog); var responseFormat = CreateResponseFormatJson(); - return client.GetStructuredResponse( + return GetStructuredResponse( + client, dialog: dialog, responseFormat: responseFormat, maxTokens: maxTokens, @@ -121,7 +122,7 @@ internal static async Task GetStructuredResponse( model, temperature, user, - true, + ChatCompletionModels.IsJsonModeSupported(model), null, requestModifier, rawResponseGetter, diff --git a/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/OpenAiClient_GetStructuredResponse.cs b/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/OpenAiClient_GetStructuredResponse.cs index 98ffd14..a0fd254 100644 --- a/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/OpenAiClient_GetStructuredResponse.cs +++ b/tests/OpenAI.ChatGpt.IntegrationTests/OpenAiClientTests/OpenAiClient_GetStructuredResponse.cs @@ -6,27 +6,35 @@ public class OpenAiClientGetStructuredResponseTests { private readonly OpenAiClient _client = new(Helpers.GetOpenAiKey()); - [Fact] - public async void Get_simple_structured_response_from_ChatGPT() + [Theory] + [InlineData(ChatCompletionModels.Gpt3_5_Turbo)] + [InlineData(ChatCompletionModels.Gpt4Turbo)] + [InlineData(ChatCompletionModels.Gpt4)] + [InlineData(ChatCompletionModels.Gpt3_5_Turbo_1106)] + public async void Get_simple_structured_response_from_ChatGPT(string model) { var message = Dialog.StartAsSystem("What did user input?") .ThenUser("My name is John, my age is 30, my email is john@gmail.com"); - var response = await _client.GetStructuredResponse(message, model: ChatCompletionModels.Gpt4Turbo); + var examples = new []{ new UserInfo() {Age = 0, Email = "i@rodion-m.ru", Name = "Rodion"} }; + var response = await _client.GetStructuredResponse(message, model: model, examples: examples); response.Should().NotBeNull(); response.Name.Should().Be("John"); response.Age.Should().Be(30); response.Email.Should().Be("john@gmail.com"); } - [Fact] - public async void Get_structured_response_with_ARRAY_from_ChatGPT() + [Theory] + [InlineData(ChatCompletionModels.Gpt4Turbo)] + [InlineData(ChatCompletionModels.Gpt4)] + [InlineData(ChatCompletionModels.Gpt3_5_Turbo_1106)] + public async void Get_structured_response_with_ARRAY_from_ChatGPT(string model) { var message = Dialog .StartAsSystem("What did user input?") .ThenUser("My name is John, my age is 30, my email is john@gmail.com. " + "I want to buy 2 apple and 3 orange."); - var response = await _client.GetStructuredResponse(message, model: ChatCompletionModels.Gpt4Turbo); + var response = await _client.GetStructuredResponse(message, model: model); response.Should().NotBeNull(); response.UserInfo.Should().NotBeNull(); response.UserInfo!.Name.Should().Be("John"); @@ -40,24 +48,30 @@ public async void Get_structured_response_with_ARRAY_from_ChatGPT() response.Items[1].Quantity.Should().Be(3); } - [Fact] - public async void Get_structured_response_with_ENUM_from_ChatGPT() + [Theory] + [InlineData(ChatCompletionModels.Gpt4Turbo)] + [InlineData(ChatCompletionModels.Gpt4)] + [InlineData(ChatCompletionModels.Gpt3_5_Turbo_1106)] + public async void Get_structured_response_with_ENUM_from_ChatGPT(string model) { var message = Dialog .StartAsSystem("What did user input?") .ThenUser("Мой любимый цвет - красный"); - var response = await _client.GetStructuredResponse(message, model: ChatCompletionModels.Gpt4Turbo); + var response = await _client.GetStructuredResponse(message, model: model); response.Should().NotBeNull(); response.Color.Should().Be(Thing.Colors.Red); } - [Fact] - public async void Get_structured_response_with_extra_data_from_ChatGPT() + [Theory] + [InlineData(ChatCompletionModels.Gpt4Turbo)] + [InlineData(ChatCompletionModels.Gpt4)] + [InlineData(ChatCompletionModels.Gpt3_5_Turbo_1106)] + public async void Get_structured_response_with_extra_data_from_ChatGPT(string model) { var message = Dialog .StartAsSystem("Return requested data.") .ThenUser("I need info about Almaty city"); - var response = await _client.GetStructuredResponse(message, model: ChatCompletionModels.Gpt4Turbo); + var response = await _client.GetStructuredResponse(message, model: model); response.Should().NotBeNull(); response.Name.Should().Be("Almaty"); response.Country.Should().Be("Kazakhstan");