Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion lib/chat_models/chat_open_ai_responses.ex
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,29 @@ defmodule LangChain.ChatModels.ChatOpenAIResponses do
|> Utils.conditionally_add_to_map(:tools, get_tools_for_api(openai, tools))
|> Utils.conditionally_add_to_map(:user, openai.user)
|> Utils.conditionally_add_to_map(:temperature, openai.temperature)
|> Utils.conditionally_add_to_map(:top_p, openai.top_p)
|> maybe_add_top_p(openai)
end

# gpt-5.2 and newer do not support the top_p parameter.
# Earlier models (gpt-4.x, gpt-5.0, gpt-5.1) accept top_p.
defp maybe_add_top_p(map, %ChatOpenAIResponses{model: model, top_p: top_p}) do
if supports_top_p?(model) do
Utils.conditionally_add_to_map(map, :top_p, top_p)
else
map
end
end

@doc false
@spec supports_top_p?(String.t()) :: boolean()
def supports_top_p?(model) when is_binary(model) do
# Match models known to support top_p. This set is fixed and won't grow.
cond do
String.starts_with?(model, "gpt-4") -> true
String.starts_with?(model, "gpt-5.0") -> true
String.starts_with?(model, "gpt-5.1") -> true
true -> false
end
end

defp get_tools_for_api(%ChatOpenAIResponses{} = _model, nil), do: []
Expand Down
60 changes: 60 additions & 0 deletions test/chat_models/chat_open_ai_responses_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,66 @@ defmodule LangChain.ChatModels.ChatOpenAIResponsesTest do
assert {"must be greater than or equal to %{number}", _} = changeset.errors[:temperature]
end

test "top_p defaults to 1.0 and is included for gpt-4 models" do
{:ok, openai} = ChatOpenAIResponses.new(%{"model" => @test_model})
assert openai.top_p == 1.0

data = ChatOpenAIResponses.for_api(openai, [], [])
assert data.top_p == 1.0
end

test "top_p is excluded for gpt-5.2 and newer models" do
{:ok, openai} = ChatOpenAIResponses.new(%{"model" => "gpt-5.2", "top_p" => 0.9})
assert openai.top_p == 0.9

data = ChatOpenAIResponses.for_api(openai, [], [])
refute Map.has_key?(data, :top_p)
end

test "top_p is included for gpt-5.1 and earlier models" do
{:ok, openai} = ChatOpenAIResponses.new(%{"model" => "gpt-5.1", "top_p" => 0.8})
data = ChatOpenAIResponses.for_api(openai, [], [])
assert data.top_p == 0.8

{:ok, openai} = ChatOpenAIResponses.new(%{"model" => "gpt-5.0", "top_p" => 0.7})
data = ChatOpenAIResponses.for_api(openai, [], [])
assert data.top_p == 0.7
end

test "supports_top_p?/1 returns correct values for various models" do
assert ChatOpenAIResponses.supports_top_p?("gpt-4")
assert ChatOpenAIResponses.supports_top_p?("gpt-4o")
assert ChatOpenAIResponses.supports_top_p?("gpt-4o-mini")
assert ChatOpenAIResponses.supports_top_p?("gpt-4o-mini-2024-07-18")
assert ChatOpenAIResponses.supports_top_p?("gpt-5.0")
assert ChatOpenAIResponses.supports_top_p?("gpt-5.1")
refute ChatOpenAIResponses.supports_top_p?("gpt-5.2")
refute ChatOpenAIResponses.supports_top_p?("gpt-5.3")
refute ChatOpenAIResponses.supports_top_p?("gpt-6")
end

test "supports overriding top_p" do
{:ok, openai} = ChatOpenAIResponses.new(%{"model" => @test_model, "top_p" => 0.9})
assert openai.top_p == 0.9

data = ChatOpenAIResponses.for_api(openai, [], [])
assert data.top_p == 0.9
end

test "returns error for out-of-bounds top_p" do
assert {:error, changeset} =
ChatOpenAIResponses.new(%{"model" => @test_model, "top_p" => 1.5})

refute changeset.valid?
assert {"must be less than or equal to %{number}", _} = changeset.errors[:top_p]

assert {:error, changeset} =
ChatOpenAIResponses.new(%{"model" => @test_model, "top_p" => -0.1})

refute changeset.valid?
assert {"must be greater than or equal to %{number}", _} = changeset.errors[:top_p]
end

test "supports setting reasoning options" do
{:ok, openai} =
ChatOpenAIResponses.new(%{
Expand Down