Using function in `tools` in `create_chat_completion` isn't working
Hey @dvcrn thanks for the great lib :).
I'm encountering an issue when using a function call in create_chat_completion in tools.
The ExOpenAI.Components.FunctionParameters struct doesn't expect any keys, so it errors when you try to pass them.
ExOpenAI.Components.FunctionObject expects this struct here which in turn is expected here.
But when I leave them off (as below), I get a JSON encoding error:
** (CaseClauseError) no case clause matching: %Protocol.UndefinedError{protocol: Jason.Encoder, value: {:tools, [%ExOpenAI.Components.ChatCompletionTool{function: %ExOpenAI.Components.FunctionObject{name: "extract_order", strict: true, description: "Extracts order information from a customer message", parameters: %{type: "object", required: ["customer_name", "product", "quantity", "price_per_item", "order_date"], properties: %{product: %{type: "string"}, customer_name: %{type: "string"}, quantity: %{type: "integer"}, price_per_item: %{type: "number"}, order_date: %{type: "string", format: "date"}}}}, type: "function"}]}, description: "Jason.Encoder protocol must always be explicitly implemented"}
(hackney 1.23.0) <path>/deps/hackney/src/hackney_request.erl:322: :hackney_request.handle_body/4
(hackney 1.23.0) <path>/deps/hackney/src/hackney_request.erl:87: :hackney_request.perform/2
(hackney 1.23.0) <path>/deps/hackney/src/hackney.erl:380: :hackney.send_request/2
(httpoison 2.2.3) lib/httpoison/base.ex:883: HTTPoison.Base.request/6
(ex_openai 1.8.0) lib/ex_openai/client.ex:143: ExOpenAI.Client.api_post/4
iex:66: (file)
Here is the ExOpenAI compared to calling directly with Req (which works).
defmodule Test do
alias ExOpenAI.Components.ChatCompletionRequestUserMessage
alias ExOpenAI.Components.ChatCompletionTool
alias ExOpenAI.Components.FunctionObject
alias ExOpenAI.Components.FunctionParameters
@parameters %{
type: "object",
properties: %{
customer_name: %{type: "string"},
product: %{type: "string"},
quantity: %{type: "integer"},
price_per_item: %{type: "number"},
order_date: %{type: "string", format: "date"}
},
required: ["customer_name", "product", "quantity", "price_per_item", "order_date"]
}
def test_with_ex_openai do
msg = "John Doe ordered 3 red T-shirts for $19.99 each on June 5th, 2025."
messages = [%ChatCompletionRequestUserMessage{role: :user, content: msg}]
function_object = %FunctionObject{
name: "extract_order",
description: "Extracts order information from a customer message",
strict: true,
parameters: @parameters
}
function_tool = %ChatCompletionTool{
type: "function",
function: function_object
}
ExOpenAI.Chat.create_chat_completion(messages,
tools: [function_tool]
)
end
def test_with_direct_api do
msg = "John Doe ordered 3 red T-shirts for $19.99 each on June 5th, 2025."
Req.post!(
"https://api.openai.com/v1/chat/completions",
headers: [
{"Authorization", "Bearer #{System.get_env("OPENAI_API_KEY")}"},
{"OpenAI-Organization", System.get_env("OPENAI_ORGANIZATION_KEY")}
],
json: %{
model: "gpt-4o",
messages: [
%{role: "user", content: msg}
],
tools: [
%{
type: "function",
function: %{
name: "extract_order",
description: "Extracts order information from a customer message",
parameters: @parameters
}
}
]
}
)
end
end
Hi, thanks for reporting this, let me look into this.
This is on 1.8, yeah?
Okay I reproduced it. Seems to be an issue with nested maps since OpenAI now likes to nest schema within schema within schema within schema 😅
Will try to get this fixed this weekend!
Hi, thanks for reporting this, let me look into this.
This is on 1.8, yeah?
Yep: {:ex_openai, "~> 1.8.0"}
Hey @hugomorg
While there is definitely an issue with parsing that I found while debugging this, it's not relevant for your issue. On closer look you're simply not specifying the correct parameters:
ExOpenAI.Chat.create_chat_completion(messages,
tools: [function_tool]
)
should be (missing model parameter):
ExOpenAI.Chat.create_chat_completion(messages, :"gpt-4o"
tools: [function_tool]
)
With that your example code is working fine for me. Dialyzer should have also told you that the second argument doesn't match the expected spec
full spec:
iex(9)> h ExOpenAI.Chat.create_chat_completion
def create_chat_completion(messages, model, opts \\ [])
@spec create_chat_completion(
[ExOpenAI.Components.ChatCompletionRequestMessage.t()],
(:"gpt-3.5-turbo-16k-0613"
| :"gpt-3.5-turbo-0125"
| :"gpt-3.5-turbo-1106"
| :"gpt-3.5-turbo-0613"
| :"gpt-3.5-turbo-0301"
| :"gpt-3.5-turbo-16k"
| :"gpt-3.5-turbo"
| :"gpt-4-32k-0613"
| :"gpt-4-32k-0314"
| :"gpt-4-32k"
| :"gpt-4-0613"
| :"gpt-4-0314"
| :"gpt-4"
| :"gpt-4-vision-preview"
| :"gpt-4-1106-preview"
| :"gpt-4-turbo-preview"
| :"gpt-4-0125-preview"
| :"gpt-4-turbo-2024-04-09"
| :"gpt-4-turbo"
| :"gpt-4o-mini-2024-07-18"
| :"gpt-4o-mini"
| :"chatgpt-4o-latest"
| :"gpt-4o-mini-audio-preview-2024-12-17"
| :"gpt-4o-mini-audio-preview"
| :"gpt-4o-audio-preview-2024-12-17"
| :"gpt-4o-audio-preview-2024-10-01"
| :"gpt-4o-audio-preview"
| :"gpt-4o-2024-05-13"
| :"gpt-4o-2024-08-06"
| :"gpt-4o-2024-11-20"
| :"gpt-4o"
| :"gpt-4.5-preview-2025-02-27"
| :"gpt-4.5-preview"
| :"computer-use-preview-2025-03-11"
| :"computer-use-preview-2025-02-04"
| :"computer-use-preview"
| :"o1-mini-2024-09-12"
| :"o1-mini"
| :"o1-preview-2024-09-12"
| :"o1-preview"
| :"o1-2024-12-17"
| :o1
| :"o3-mini-2025-01-31"
| :"o3-mini")
| String.t(),
base_url: String.t(),
openai_organization_key: String.t(),
openai_api_key: String.t(),
user: String.t(),
top_p: float(),
temperature: float(),
metadata: ExOpenAI.Components.Metadata.t(),
web_search_options: %{
search_context_size: ExOpenAI.Components.WebSearchContextSize.t(),
user_location: %{
approximate: ExOpenAI.Components.WebSearchLocation.t(),
type: :approximate
}
},
top_logprobs: integer(),
tools: [ExOpenAI.Components.ChatCompletionTool.t()],
tool_choice: ExOpenAI.Components.ChatCompletionToolChoiceOption.t(),
stream_options: ExOpenAI.Components.ChatCompletionStreamOptions.t(),
stream: boolean(),
store: boolean(),
stop: ExOpenAI.Components.StopConfiguration.t(),
service_tier: :default | :auto,
seed: integer(),
response_format:
ExOpenAI.Components.ResponseFormatJsonObject.t()
| ExOpenAI.Components.ResponseFormatJsonSchema.t()
| ExOpenAI.Components.ResponseFormatText.t(),
reasoning_effort: ExOpenAI.Components.ReasoningEffort.t(),
presence_penalty: float(),
prediction: ExOpenAI.Components.PredictionContent.t(),
parallel_tool_calls: ExOpenAI.Components.ParallelToolCalls.t(),
n: integer(),
modalities: ExOpenAI.Components.ResponseModalities.t(),
max_tokens: integer(),
max_completion_tokens: integer(),
logprobs: boolean(),
logit_bias: map(),
functions: [ExOpenAI.Components.ChatCompletionFunctions.t()],
function_call:
ExOpenAI.Components.ChatCompletionFunctionCallOption.t()
| :auto
| :none,
frequency_penalty: float(),
audio: %{
format: :pcm16 | :opus | :flac | :mp3 | :wav,
voice:
:verse | :shimmer | :sage | :echo | :coral | :ballad | :ash | :alloy
},
stream_to: fun() | pid()
) ::
({:ok, ExOpenAI.Components.CreateChatCompletionStreamResponse.t()}
| {:ok, ExOpenAI.Components.CreateChatCompletionResponse.t()})
| {:error, any()}
Let me know if you still have issues
Thanks for pointing this out @dvcrn!