ex_openai icon indicating copy to clipboard operation
ex_openai copied to clipboard

Using function in `tools` in `create_chat_completion` isn't working

Open hugomorg opened this issue 6 months ago • 5 comments

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

hugomorg avatar Jun 20 '25 07:06 hugomorg

Hi, thanks for reporting this, let me look into this.

This is on 1.8, yeah?

dvcrn avatar Jun 21 '25 01:06 dvcrn

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!

dvcrn avatar Jun 21 '25 05:06 dvcrn

Hi, thanks for reporting this, let me look into this.

This is on 1.8, yeah?

Yep: {:ex_openai, "~> 1.8.0"}

hugomorg avatar Jun 21 '25 10:06 hugomorg

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

dvcrn avatar Jun 23 '25 03:06 dvcrn

Thanks for pointing this out @dvcrn!

hugomorg avatar Jul 08 '25 06:07 hugomorg