diff --git a/+llms/+internal/callOpenAIChatAPI.m b/+llms/+internal/callOpenAIChatAPI.m index 3cd485c..6226653 100644 --- a/+llms/+internal/callOpenAIChatAPI.m +++ b/+llms/+internal/callOpenAIChatAPI.m @@ -119,18 +119,16 @@ parameters.stream = ~isempty(nvp.StreamFun); -if ~isempty(functions) && ~strcmp(nvp.ModelName,'gpt-4-vision-preview') +if ~isempty(functions) parameters.tools = functions; end -if ~isempty(nvp.ToolChoice) && ~strcmp(nvp.ModelName,'gpt-4-vision-preview') +if ~isempty(nvp.ToolChoice) parameters.tool_choice = nvp.ToolChoice; end -if ismember(nvp.ModelName,["gpt-3.5-turbo-1106","gpt-4-1106-preview"]) - if strcmp(nvp.ResponseFormat,"json") - parameters.response_format = struct('type','json_object'); - end +if strcmp(nvp.ResponseFormat,"json") + parameters.response_format = struct('type','json_object'); end if ~isempty(nvp.Seed) @@ -142,15 +140,21 @@ dict = mapNVPToParameters; nvpOptions = keys(dict); -if strcmp(nvp.ModelName,'gpt-4-vision-preview') - nvpOptions(ismember(nvpOptions,"StopSequences")) = []; -end for opt = nvpOptions.' if isfield(nvp, opt) parameters.(dict(opt)) = nvp.(opt); end end + +if isempty(nvp.StopSequences) + parameters = rmfield(parameters,"stop"); +end + +if nvp.MaxNumTokens == Inf + parameters = rmfield(parameters,"max_tokens"); +end + end function dict = mapNVPToParameters() diff --git a/+llms/+utils/errorMessageCatalog.m b/+llms/+utils/errorMessageCatalog.m index 3791319..caf4c5e 100644 --- a/+llms/+utils/errorMessageCatalog.m +++ b/+llms/+utils/errorMessageCatalog.m @@ -49,8 +49,10 @@ catalog("llms:mustBeMessagesOrTxt") = "Messages must be text with one or more characters or an openAIMessages objects."; catalog("llms:invalidOptionAndValueForModel") = "'{1}' with value '{2}' is not supported for ModelName '{3}'"; catalog("llms:invalidOptionForModel") = "{1} is not supported for ModelName '{2}'"; +catalog("llms:invalidContentTypeForModel") = "{1} is not supported for ModelName '{2}'"; catalog("llms:functionNotAvailableForModel") = "This function is not supported for ModelName '{1}'"; catalog("llms:promptLimitCharacter") = "Prompt must have a maximum length of {1} characters for ModelName '{2}'"; catalog("llms:pngExpected") = "Argument must be a PNG image."; catalog("llms:warningJsonInstruction") = "When using JSON mode, you must also prompt the model to produce JSON yourself via a system or user message."; +catalog("llms:apiReturnedError") = "OpenAI API Error: {1}"; end \ No newline at end of file diff --git a/README.md b/README.md index 03b934f..a003170 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ This repository contains example code to demonstrate how to connect MATLAB to th The functionality shown here serves as an interface to the ChatGPT and DALLĀ·E APIs. To start using the OpenAI APIs, you first need to obtain OpenAI API keys. You are responsible for any fees OpenAI may charge for the use of their APIs. You should be familiar with the limitations and risks associated with using this technology, and you agree that you shall be solely responsible for full compliance with any terms that may apply to your use of the OpenAI APIs. Some of the current LLMs supported are: -- gpt-3.5-turbo, gpt-3.5-turbo-1106 -- gpt-4, gpt-4-1106-preview -- gpt-4-vision-preview (a.k.a. GPT-4 Turbo with Vision) +- gpt-3.5-turbo, gpt-3.5-turbo-1106, gpt-3.5-turbo-0125 +- gpt-4-turbo, gpt-4-turbo-2024-04-09 (GPT-4 Turbo with Vision) +- gpt-4, gpt-4-0613 - dall-e-2, dall-e-3 - + For details on the specification of each model, check the official [OpenAI documentation](https://platform.openai.com/docs/models). ## Requirements @@ -52,15 +52,15 @@ To use this repository with a local installation of MATLAB, first clone the repo Set up your OpenAI API key. Create a `.env` file in the project root directory with the following content. - ``` - OPENAI_API_KEY= - ``` +``` +OPENAI_API_KEY= +``` - Then load your `.env` file as follows: +Then load your `.env` file as follows: - ```matlab - loadenv(".env") - ``` +```matlab +loadenv(".env") +``` ## Getting Started with Chat Completion API @@ -287,13 +287,13 @@ You can extract the arguments and write the data to a table, for example. ### Understand the content of an image -You can use gpt-4-vision-preview to experiment with image understanding. +You can use gpt-4-turbo to experiment with image understanding. ```matlab -chat = openAIChat("You are an AI assistant.", ModelName="gpt-4-vision-preview"); +chat = openAIChat("You are an AI assistant.", ModelName="gpt-4-turbo"); image_path = "peppers.png"; messages = openAIMessages; messages = addUserMessageWithImages(messages,"What is in the image?",image_path); -[txt,response] = generate(chat,messages); +[txt,response] = generate(chat,messages,MaxNumTokens=4096); % Should output the description of the image ``` diff --git a/examples/AnalyzeSentimentinTextUsingChatGPTinJSONMode.mlx b/examples/AnalyzeSentimentinTextUsingChatGPTinJSONMode.mlx index 17d6f47..8fc5a63 100644 Binary files a/examples/AnalyzeSentimentinTextUsingChatGPTinJSONMode.mlx and b/examples/AnalyzeSentimentinTextUsingChatGPTinJSONMode.mlx differ diff --git a/examples/DescribeImagesUsingChatGPT.mlx b/examples/DescribeImagesUsingChatGPT.mlx index 8aaf47c..b55f4a2 100644 Binary files a/examples/DescribeImagesUsingChatGPT.mlx and b/examples/DescribeImagesUsingChatGPT.mlx differ diff --git a/examples/ExampleParallelFunctionCalls.mlx b/examples/ExampleParallelFunctionCalls.mlx index 11ab88c..94f45ba 100644 Binary files a/examples/ExampleParallelFunctionCalls.mlx and b/examples/ExampleParallelFunctionCalls.mlx differ diff --git a/examples/UsingDALLEToEditImages.mlx b/examples/UsingDALLEToEditImages.mlx index b355818..b01e891 100644 Binary files a/examples/UsingDALLEToEditImages.mlx and b/examples/UsingDALLEToEditImages.mlx differ diff --git a/examples/UsingDALLEToGenerateImages.mlx b/examples/UsingDALLEToGenerateImages.mlx index 28a0e75..f0ce158 100644 Binary files a/examples/UsingDALLEToGenerateImages.mlx and b/examples/UsingDALLEToGenerateImages.mlx differ diff --git a/openAIChat.m b/openAIChat.m index 5ba1215..395d166 100644 --- a/openAIChat.m +++ b/openAIChat.m @@ -114,10 +114,10 @@ arguments systemPrompt {llms.utils.mustBeTextOrEmpty} = [] nvp.Tools (1,:) {mustBeA(nvp.Tools, "openAIFunction")} = openAIFunction.empty - nvp.ModelName (1,1) {mustBeMember(nvp.ModelName,["gpt-4", "gpt-4-0613", "gpt-4-32k", ... - "gpt-3.5-turbo", "gpt-4-1106-preview", ... - "gpt-3.5-turbo-1106", "gpt-4-vision-preview", ... - "gpt-4-turbo-preview"])} = "gpt-3.5-turbo" + nvp.ModelName (1,1) {mustBeMember(nvp.ModelName,["gpt-4-turbo", ... + "gpt-4-turbo-2024-04-09","gpt-4","gpt-4-0613", ... + "gpt-3.5-turbo","gpt-3.5-turbo-0125", ... + "gpt-3.5-turbo-1106"])} = "gpt-3.5-turbo" nvp.Temperature {mustBeValidTemperature} = 1 nvp.TopProbabilityMass {mustBeValidTopP} = 1 nvp.StopSequences {mustBeValidStop} = {} @@ -131,10 +131,6 @@ if isfield(nvp,"StreamFun") this.StreamFun = nvp.StreamFun; - if strcmp(nvp.ModelName,'gpt-4-vision-preview') - error("llms:invalidOptionForModel", ... - llms.utils.errorMessageCatalog.getMessage("llms:invalidOptionForModel", "StreamFun", nvp.ModelName)); - end else this.StreamFun = []; end @@ -146,10 +142,6 @@ else this.Tools = nvp.Tools; [this.FunctionsStruct, this.FunctionNames] = functionAsStruct(nvp.Tools); - if strcmp(nvp.ModelName,'gpt-4-vision-preview') - error("llms:invalidOptionForModel", ... - llms.utils.errorMessageCatalog.getMessage("llms:invalidOptionForModel", "Tools", nvp.ModelName)); - end end if ~isempty(systemPrompt) @@ -163,20 +155,15 @@ this.Temperature = nvp.Temperature; this.TopProbabilityMass = nvp.TopProbabilityMass; this.StopSequences = nvp.StopSequences; - if ~isempty(nvp.StopSequences) && strcmp(nvp.ModelName,'gpt-4-vision-preview') - error("llms:invalidOptionForModel", ... - llms.utils.errorMessageCatalog.getMessage("llms:invalidOptionForModel", "StopSequences", nvp.ModelName)); - end - % ResponseFormat is only supported in the latest models only if (nvp.ResponseFormat == "json") - if ismember(this.ModelName,["gpt-3.5-turbo-1106","gpt-4-1106-preview"]) - warning("llms:warningJsonInstruction", ... - llms.utils.errorMessageCatalog.getMessage("llms:warningJsonInstruction")) - else + if ismember(this.ModelName,["gpt-4","gpt-4-0613"]) error("llms:invalidOptionAndValueForModel", ... llms.utils.errorMessageCatalog.getMessage("llms:invalidOptionAndValueForModel", "ResponseFormat", "json", this.ModelName)); + else + warning("llms:warningJsonInstruction", ... + llms.utils.errorMessageCatalog.getMessage("llms:warningJsonInstruction")) end end @@ -222,10 +209,6 @@ end toolChoice = convertToolChoice(this, nvp.ToolChoice); - if ~isempty(nvp.ToolChoice) && strcmp(this.ModelName,'gpt-4-vision-preview') - error("llms:invalidOptionForModel", ... - llms.utils.errorMessageCatalog.getMessage("llms:invalidOptionForModel", "ToolChoice", this.ModelName)); - end if isstring(messages) && isscalar(messages) messagesStruct = {struct("role", "user", "content", messages)}; @@ -233,6 +216,13 @@ messagesStruct = messages.Messages; end + if iscell(messagesStruct{end}.content) && any(cellfun(@(x) isfield(x,"image_url"), messagesStruct{end}.content)) + if ~ismember(this.ModelName,["gpt-4-turbo","gpt-4-turbo-2024-04-09"]) + error("llms:invalidContentTypeForModel", ... + llms.utils.errorMessageCatalog.getMessage("llms:invalidContentTypeForModel", "Image content", this.ModelName)); + end + end + if ~isempty(this.SystemPrompt) messagesStruct = horzcat(this.SystemPrompt, messagesStruct); end @@ -244,6 +234,13 @@ PresencePenalty=this.PresencePenalty, FrequencyPenalty=this.FrequencyPenalty, ... ResponseFormat=this.ResponseFormat,Seed=nvp.Seed, ... ApiKey=this.ApiKey,TimeOut=this.TimeOut, StreamFun=this.StreamFun); + + if isfield(response.Body.Data,"error") + err = response.Body.Data.error.message; + text = llms.utils.errorMessageCatalog.getMessage("llms:apiReturnedError",err); + message = struct("role","assistant","content",text); + end + end function this = set.Temperature(this, temperature) diff --git a/tests/topenAIChat.m b/tests/topenAIChat.m index 390ec9e..c001c3b 100644 --- a/tests/topenAIChat.m +++ b/tests/topenAIChat.m @@ -38,7 +38,7 @@ function generateAcceptsMessagesAsInput(testCase) end function constructMdlWithInvalidParameters(testCase) - testCase.verifyError(@()openAIChat(ApiKey="this-is-not-a-real-key", ResponseFormat="json"), "llms:invalidOptionAndValueForModel"); + testCase.verifyError(@()openAIChat(ApiKey="this-is-not-a-real-key", ModelName="gpt-4", ResponseFormat="json"), "llms:invalidOptionAndValueForModel"); end function keyNotFound(testCase) @@ -100,7 +100,21 @@ function assignValueToProperty(property, value) end testCase.verifyError(@()assignValueToProperty(InvalidValuesSetters.Property,InvalidValuesSetters.Value), InvalidValuesSetters.Error); - end + end + + function invalidGenerateInputforModel(testCase) + chat = openAIChat(ApiKey="this-is-not-a-real-key"); + image_path = "peppers.png"; + emptyMessages = openAIMessages; + inValidMessages = addUserMessageWithImages(emptyMessages,"What is in the image?",image_path); + testCase.verifyError(@()generate(chat,inValidMessages), "llms:invalidContentTypeForModel") + end + + function noStopSequencesNoMaxNumTokens(testCase) + chat = openAIChat(ApiKey="this-is-not-a-real-key"); + testCase.verifyWarningFree(@()generate(chat,"This is okay")); + end + end end