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
46 changes: 35 additions & 11 deletions src/providers/openai/openai_request_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include "ai/openai.h"
#include "utils/message_utils.h"

#include <optional>

namespace ai {
namespace openai {

Expand Down Expand Up @@ -97,18 +99,34 @@ nlohmann::json OpenAIRequestBuilder::build_request_json(
}

// Add optional parameters
if (options.temperature) {
request["temperature"] = *options.temperature;
}
// The GPT-5 family is reasoning-first on Chat Completions: it rejects
// sampling controls ("'temperature' does not support 0.0 with this model.
// Only the default (1) value is supported."). Keep accepting the
// provider-neutral options while omitting them for those model IDs,
// mirroring the Anthropic builder.
const bool is_gpt5_family = options.model.starts_with("gpt-5");
const bool rejects_sampling_parameters = is_gpt5_family;
const auto add_sampling_parameter = [&](const char* key,
const std::optional<double>& value) {
if (!value) {
return;
}
if (rejects_sampling_parameters) {
ai::logger::log_warn(
"Ignoring {} for {} because the model does not support "
"sampling parameters",
key, options.model);
} else {
request[key] = *value;
}
};
add_sampling_parameter("temperature", options.temperature);
add_sampling_parameter("top_p", options.top_p);

if (options.max_tokens) {
request["max_completion_tokens"] = *options.max_tokens;
}

if (options.top_p) {
request["top_p"] = *options.top_p;
}

if (options.frequency_penalty) {
request["frequency_penalty"] = *options.frequency_penalty;
}
Expand All @@ -121,12 +139,18 @@ nlohmann::json OpenAIRequestBuilder::build_request_json(
request["seed"] = *options.seed;
}

// GPT-5.6 defaults to reasoning in Chat Completions. That mode does not
// support function tools and can consume the full completion budget without
// producing user-visible text. The unified SDK currently exposes Chat
// Completions semantics, so explicitly request non-reasoning output.
// The GPT-5 family defaults to reasoning in Chat Completions. That mode
// can consume the full completion budget on hidden reasoning without
// producing user-visible text (acute with a small max_tokens), and on
// GPT-5.6 it does not support function tools. The unified SDK currently
// exposes Chat Completions semantics, so explicitly request non-reasoning
// output for the whole family.
if (options.model.starts_with(models::kGpt56)) {
request["reasoning_effort"] = "none";
} else if (is_gpt5_family) {
// Earlier GPT-5 models reject "none" ("Supported values are: 'minimal',
// 'low', 'medium', and 'high'."); "minimal" is their floor.
request["reasoning_effort"] = "minimal";
}

// Add tools if specified
Expand Down
42 changes: 39 additions & 3 deletions tests/unit/openai_client_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,49 @@ TEST_F(OpenAIClientTest, ValidateOptionsValidation) {
EXPECT_TRUE(valid_options.is_valid());
}

TEST_F(OpenAIClientTest, Gpt56UsesChatCompletionsCompatibleReasoning) {
TEST_F(OpenAIClientTest, Gpt5FamilyUsesChatCompletionsCompatibleReasoning) {
openai::OpenAIRequestBuilder builder;
GenerateOptions options(openai::models::kGpt56Luna, "Use a tool");

GenerateOptions gpt56_options(openai::models::kGpt56Luna, "Use a tool");
EXPECT_EQ(builder.build_request_json(gpt56_options)["reasoning_effort"],
"none");

// Earlier GPT-5 models reject "none"; they get the "minimal" floor.
for (const auto* model :
{openai::models::kGpt5Mini, openai::models::kGpt55}) {
GenerateOptions options(model, "Use a tool");
EXPECT_EQ(builder.build_request_json(options)["reasoning_effort"],
"minimal")
<< model;
}
}

TEST_F(OpenAIClientTest, Gpt5FamilyOmitsUnsupportedSamplingParameters) {
openai::OpenAIRequestBuilder builder;
for (const auto* model :
{openai::models::kGpt56, openai::models::kGpt56Terra,
openai::models::kGpt55, openai::models::kGpt5Mini}) {
GenerateOptions options(model, "Hello");
options.temperature = 0.0;
options.top_p = 0.8;

const auto request = builder.build_request_json(options);

EXPECT_FALSE(request.contains("temperature")) << model;
EXPECT_FALSE(request.contains("top_p")) << model;
}
}

TEST_F(OpenAIClientTest, NonReasoningModelsKeepSamplingParameters) {
openai::OpenAIRequestBuilder builder;
GenerateOptions options(openai::models::kGpt41, "Hello");
options.temperature = 0.2;
options.top_p = 0.9;

const auto request = builder.build_request_json(options);

EXPECT_EQ(request["reasoning_effort"], "none");
EXPECT_EQ(request["temperature"], 0.2);
EXPECT_EQ(request["top_p"], 0.9);
}

// Stream Tests (Basic validation)
Expand Down
Loading