Skip to content

Commit 2fe289b

Browse files
Merge pull request #40 from lucaromagnoli/feat/gpt5
Feat/gpt5
2 parents 10e53f6 + c9f972b commit 2fe289b

File tree

7 files changed

+184
-27
lines changed

7 files changed

+184
-27
lines changed

include/openai/OpenAITypes.h

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ namespace OpenAI {
1717
* Provides type safety and IDE auto-completion for model selection
1818
*/
1919
enum class Model {
20+
// GPT-5 series (Latest - 2025)
21+
GPT_5, // gpt-5 - Next-generation model
22+
2023
// O3 series (Latest - 2025)
2124
O3, // o3 - Latest reasoning model
2225
O3_Mini, // o3-mini - Cost-effective reasoning model
@@ -56,6 +59,8 @@ enum class Model {
5659
*/
5760
inline std::string toString(Model model) {
5861
switch (model) {
62+
case Model::GPT_5:
63+
return "gpt-5";
5964
case Model::O3:
6065
return "o3";
6166
case Model::O3_Mini:
@@ -96,6 +101,7 @@ inline std::string toString(Model model) {
96101
* Convert API string to OpenAI Model enum
97102
*/
98103
inline Model modelFromString(const std::string& modelStr) {
104+
if (modelStr == "gpt-5") return Model::GPT_5;
99105
if (modelStr == "o3") return Model::O3;
100106
if (modelStr == "o3-mini") return Model::O3_Mini;
101107
if (modelStr == "o1") return Model::O1;
@@ -119,6 +125,7 @@ inline Model modelFromString(const std::string& modelStr) {
119125
*/
120126
inline bool supportsStructuredOutputs(Model model) {
121127
switch (model) {
128+
case Model::GPT_5:
122129
case Model::O3:
123130
case Model::O3_Mini:
124131
case Model::O1:
@@ -592,7 +599,7 @@ struct McpApprovalResponse {
592599
};
593600

594601
// Response status enumeration
595-
enum class ResponseStatus { Queued, InProgress, Completed, Failed, Cancelled };
602+
enum class ResponseStatus { Queued, InProgress, Completed, Failed, Cancelled, Incomplete };
596603

597604
inline std::string toString(ResponseStatus status) {
598605
switch (status) {
@@ -606,6 +613,8 @@ inline std::string toString(ResponseStatus status) {
606613
return "failed";
607614
case ResponseStatus::Cancelled:
608615
return "cancelled";
616+
case ResponseStatus::Incomplete:
617+
return "incomplete";
609618
}
610619
return "";
611620
}
@@ -616,6 +625,7 @@ inline ResponseStatus responseStatusFromString(const std::string& str) {
616625
if (str == "completed") return ResponseStatus::Completed;
617626
if (str == "failed") return ResponseStatus::Failed;
618627
if (str == "cancelled") return ResponseStatus::Cancelled;
628+
if (str == "incomplete") return ResponseStatus::Incomplete;
619629
throw std::invalid_argument("Invalid response status: " + str);
620630
}
621631

@@ -656,11 +666,11 @@ struct ResponsesRequest {
656666
// Convert model string to enum for easier checking
657667
auto modelEnum = modelFromString(model);
658668

659-
// Reasoning models (O-series) have different parameter support
660-
if (modelEnum == Model::O3 || modelEnum == Model::O3_Mini || modelEnum == Model::O1 ||
661-
modelEnum == Model::O1_Mini || modelEnum == Model::O1_Preview ||
662-
modelEnum == Model::O1_Pro || modelEnum == Model::O4_Mini ||
663-
modelEnum == Model::O4_Mini_Deep_Research) {
669+
// Reasoning models (O-series + GPT-5) have different parameter support
670+
if (modelEnum == Model::GPT_5 || modelEnum == Model::O3 || modelEnum == Model::O3_Mini ||
671+
modelEnum == Model::O1 || modelEnum == Model::O1_Mini ||
672+
modelEnum == Model::O1_Preview || modelEnum == Model::O1_Pro ||
673+
modelEnum == Model::O4_Mini || modelEnum == Model::O4_Mini_Deep_Research) {
664674
// Parameters NOT supported by reasoning models
665675
if (paramName == "temperature" || paramName == "top_p" || paramName == "top_logprobs" ||
666676
paramName == "truncation") {
@@ -1004,8 +1014,8 @@ std::string getRecommendedApiForModel(const std::string& model);
10041014

10051015
// Model lists for different APIs
10061016
const std::vector<std::string> RESPONSES_MODELS = {
1007-
"gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-nano", "gpt-4.1-mini", "gpt-image-1",
1008-
"o1", "o3-mini", "o3", "o4-mini", "computer-use-preview"};
1017+
"gpt-5", "gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-nano", "gpt-4.1-mini",
1018+
"gpt-image-1", "o1", "o3-mini", "o3", "o4-mini", "computer-use-preview"};
10091019

10101020
const std::vector<std::string> CHAT_COMPLETION_MODELS = {"gpt-4", "gpt-4-turbo", "gpt-4o",
10111021
"gpt-4o-mini", "gpt-3.5-turbo"};
@@ -1029,21 +1039,38 @@ inline ResponsesRequest ResponsesRequest::fromLLMRequest(const LLMRequest& reque
10291039
if (!request.context.empty()) {
10301040
// Convert context (vector of json) to InputMessages
10311041
std::vector<InputMessage> messages;
1042+
10321043
for (const auto& contextItem : request.context) {
1044+
// Case 1: Single JSON object with role/content
10331045
if (contextItem.is_object() && contextItem.contains("role") &&
10341046
contextItem.contains("content")) {
10351047
InputMessage msg;
10361048
msg.role = InputMessage::stringToRole(contextItem["role"].get<std::string>());
10371049
msg.content = contextItem["content"].get<std::string>();
10381050
messages.push_back(msg);
1039-
} else {
1040-
// If it's not a proper message format, treat as user message
1041-
InputMessage msg;
1042-
msg.role = InputMessage::Role::User;
1043-
msg.content = contextItem.dump();
1044-
messages.push_back(msg);
1051+
continue;
10451052
}
1053+
1054+
// Case 2: Array of message-like objects [{role, content}, ...]
1055+
if (contextItem.is_array()) {
1056+
for (const auto& item : contextItem) {
1057+
if (item.is_object() && item.contains("role") && item.contains("content")) {
1058+
InputMessage msg;
1059+
msg.role = InputMessage::stringToRole(item["role"].get<std::string>());
1060+
msg.content = item["content"].get<std::string>();
1061+
messages.push_back(msg);
1062+
}
1063+
}
1064+
continue;
1065+
}
1066+
1067+
// Fallback: stringify unknown item as a user message
1068+
InputMessage msg;
1069+
msg.role = InputMessage::Role::User;
1070+
msg.content = contextItem.dump();
1071+
messages.push_back(msg);
10461072
}
1073+
10471074
responsesReq.input = ResponsesInput::fromContentList(messages);
10481075
} else if (!request.prompt.empty()) {
10491076
// If context is empty but prompt is present, use prompt as input

src/openai/OpenAIClient.cpp

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,22 @@ OpenAI::ResponsesResponse OpenAIClient::sendResponsesRequest(
166166
throw std::invalid_argument("Invalid request: " + errorMessage);
167167
}
168168

169-
return responsesApi_->create(request);
169+
// Create initial response
170+
auto response = responsesApi_->create(request);
171+
172+
// If the model returns a non-completed status (e.g., queued/in_progress/incomplete),
173+
// poll until completion or failure. This particularly affects reasoning models like GPT-5.
174+
if (!response.isCompleted() && !response.id.empty()) {
175+
try {
176+
// Reasonable defaults: wait up to 90s, polling every 2s
177+
response = responsesApi_->waitForCompletion(response.id, /*timeoutSeconds=*/90,
178+
/*pollIntervalSeconds=*/2);
179+
} catch (const std::exception& /*e*/) {
180+
// Fall through and return the last known response (likely non-completed)
181+
}
182+
}
183+
184+
return response;
170185
}
171186

172187
std::future<OpenAI::ResponsesResponse> OpenAIClient::sendResponsesRequestAsync(
@@ -337,7 +352,7 @@ OpenAI::Model OpenAIClient::stringToModel(const std::string& modelStr) {
337352
}
338353

339354
std::vector<OpenAI::Model> OpenAIClient::getAvailableModelEnums() {
340-
return {OpenAI::Model::GPT_4_1, OpenAI::Model::GPT_4_1_Mini, OpenAI::Model::GPT_4_1_Nano,
341-
OpenAI::Model::GPT_4o, OpenAI::Model::GPT_4o_Mini, OpenAI::Model::GPT_4_5,
342-
OpenAI::Model::GPT_3_5_Turbo, OpenAI::Model::Custom};
355+
return {OpenAI::Model::GPT_5, OpenAI::Model::GPT_4_1, OpenAI::Model::GPT_4_1_Mini,
356+
OpenAI::Model::GPT_4_1_Nano, OpenAI::Model::GPT_4o, OpenAI::Model::GPT_4o_Mini,
357+
OpenAI::Model::GPT_4_5, OpenAI::Model::GPT_3_5_Turbo, OpenAI::Model::Custom};
343358
}

src/openai/OpenAIHttpClient.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ class OpenAIHttpClient::HttpClientImpl {
108108
httplib::Headers buildHeaders() const {
109109
httplib::Headers headers;
110110
headers.emplace("Authorization", "Bearer " + config_.apiKey);
111-
headers.emplace("Content-Type", "application/json");
112111
headers.emplace("User-Agent", "llmcpp/1.0.0");
113112

114113
if (!config_.organization.empty()) {
@@ -254,7 +253,6 @@ std::unordered_map<std::string, std::string> OpenAIHttpClient::buildHeaders(
254253
const json& requestBody [[maybe_unused]]) const {
255254
std::unordered_map<std::string, std::string> headers;
256255
headers["Authorization"] = "Bearer " + config_.apiKey;
257-
headers["Content-Type"] = "application/json";
258256
headers["User-Agent"] = userAgent_;
259257

260258
if (!config_.organization.empty()) {

src/openai/OpenAIResponsesApi.cpp

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ OpenAI::ResponsesResponse OpenAIResponsesApi::create(const OpenAI::ResponsesRequ
3434
// Parse the JSON response
3535
json responseJson = json::parse(httpResponse.body);
3636

37+
// Extra debug for GPT-5 incomplete
38+
try {
39+
auto model = OpenAI::safeGetRequiredJson<std::string>(responseJson, "model");
40+
auto status = OpenAI::safeGetJson(responseJson, "status", std::string(""));
41+
if (model == "gpt-5" && status != "completed") {
42+
std::cerr << "⚠️ GPT-5 non-completed status: " << status << std::endl;
43+
if (responseJson.contains("incomplete_details")) {
44+
std::cerr << "⚠️ Incomplete details: "
45+
<< responseJson["incomplete_details"].dump(2) << std::endl;
46+
}
47+
}
48+
} catch (...) {
49+
}
50+
3751
// Check for API errors using safe JSON function
3852
auto error = OpenAI::safeGetOptionalJson<json>(responseJson, "error");
3953
if (error.has_value()) {
@@ -142,7 +156,8 @@ OpenAI::ResponsesResponse OpenAIResponsesApi::waitForCompletion(const std::strin
142156
int timeoutSeconds [[maybe_unused]],
143157
int pollIntervalSeconds
144158
[[maybe_unused]]) {
145-
throw std::runtime_error("OpenAIResponsesApi::waitForCompletion not yet implemented");
159+
const int maxAttempts = std::max(1, timeoutSeconds / std::max(1, pollIntervalSeconds));
160+
return pollForCompletion(responseId, maxAttempts, pollIntervalSeconds);
146161
}
147162

148163
std::future<OpenAI::ResponsesResponse> OpenAIResponsesApi::resumeStreaming(
@@ -399,6 +414,15 @@ OpenAI::ResponsesResponse OpenAIResponsesApi::pollForCompletion(const std::strin
399414
int maxAttempts [[maybe_unused]],
400415
int intervalSeconds
401416
[[maybe_unused]]) {
402-
// TODO: Implement polling
403-
throw std::runtime_error("OpenAIResponsesApi::pollForCompletion not yet implemented");
417+
for (int attempt = 0; attempt < maxAttempts; ++attempt) {
418+
auto resp = retrieve(responseId);
419+
if (resp.status == OpenAI::ResponseStatus::Completed ||
420+
resp.status == OpenAI::ResponseStatus::Failed ||
421+
resp.status == OpenAI::ResponseStatus::Cancelled) {
422+
return resp;
423+
}
424+
std::this_thread::sleep_for(std::chrono::seconds(std::max(1, intervalSeconds)));
425+
}
426+
// Final retrieve before giving up
427+
return retrieve(responseId);
404428
}

tests/integration/test_openai_integration.cpp

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include <fstream>
44
#include <iostream>
55
#include <nlohmann/json.hpp>
6+
#include <chrono>
7+
#include <thread>
68

79
#include "core/ClientManager.h"
810
#include "core/LLMTypes.h"
@@ -84,7 +86,6 @@ TEST_CASE("OpenAI Integration - Simple text completion", "[openai][integration][
8486
LLMRequestConfig config;
8587
config.client = "openai";
8688
config.model = "gpt-4o-mini"; // Cheaper model for testing
87-
config.maxTokens = 50;
8889
config.temperature = 0.1f; // Low temperature for predictable results
8990

9091
json context = json::array({json{{"role", "user"}, {"content", "What is 2+2?"}}});
@@ -94,7 +95,6 @@ TEST_CASE("OpenAI Integration - Simple text completion", "[openai][integration][
9495

9596
std::cout << "Making API call to OpenAI..." << std::endl;
9697
auto response = client.sendRequest(request);
97-
9898
REQUIRE(response.success == true);
9999
REQUIRE(response.errorMessage.empty());
100100
REQUIRE(!response.responseId.empty());
@@ -112,6 +112,95 @@ TEST_CASE("OpenAI Integration - Simple text completion", "[openai][integration][
112112
std::cout << "Output: " << response.result["text"].get<std::string>() << std::endl;
113113
}
114114
}
115+
116+
SECTION("Basic text completion with gpt-5 (no temperature, reasoning)") {
117+
OpenAIClient client(apiKey);
118+
119+
LLMRequestConfig config;
120+
config.client = "openai";
121+
config.model = "gpt-5"; // Try GPT-5
122+
// Do not set temperature; GPT-5 treated as reasoning model
123+
124+
json context = json::array({json{{"role", "user"}, {"content", "What is 5+7?"}}});
125+
126+
LLMRequest request(config, "You are a math assistant. Answer with just the number.",
127+
context);
128+
129+
std::cout << "Making API call to OpenAI (gpt-5)..." << std::endl;
130+
auto response = client.sendRequest(request);
131+
if (!response.success) {
132+
std::cout << "❌ GPT-5 request failed. Error: " << response.errorMessage << std::endl;
133+
std::cout << "🔎 Full result: " << response.result.dump(2) << std::endl;
134+
}
135+
REQUIRE(response.success == true);
136+
REQUIRE(response.errorMessage.empty());
137+
REQUIRE(!response.responseId.empty());
138+
REQUIRE(response.usage.inputTokens > 0);
139+
REQUIRE(response.usage.outputTokens > 0);
140+
141+
REQUIRE((response.result.contains("text") || response.result.contains("choices")));
142+
}
143+
144+
SECTION("GPT-5 structured output via Responses API") {
145+
OpenAIClient client(apiKey);
146+
147+
LLMRequestConfig config;
148+
config.client = "openai";
149+
config.model = "gpt-5"; // Reasoning family, omit temperature
150+
config.functionName = "sum_two";
151+
152+
auto schema = OpenAIResponsesSchemaBuilder("sum_two")
153+
.property("result", JsonSchemaBuilder::integer())
154+
.required({"result"})
155+
.additionalProperties(false)
156+
.buildSchema();
157+
config.schemaObject = schema;
158+
159+
json context = json::array({json{{"role", "user"}, {"content", "Return only JSON."}}});
160+
161+
LLMRequest request(config,
162+
"Sum 5 and 7. Respond only with the sum_two JSON: {\\\"result\\\": 12}.",
163+
context);
164+
165+
std::cout << "Making structured API call to OpenAI (gpt-5)..." << std::endl;
166+
auto response = client.sendRequest(request);
167+
if (!response.success) {
168+
std::cout << "❌ GPT-5 structured request failed. Error: " << response.errorMessage
169+
<< std::endl;
170+
std::cout << "🔎 Full result: " << response.result.dump(2) << std::endl;
171+
}
172+
REQUIRE(response.success == true);
173+
REQUIRE(!response.responseId.empty());
174+
REQUIRE(response.result.contains("text"));
175+
176+
auto text = response.result["text"].get<std::string>();
177+
auto parsed = json::parse(text);
178+
REQUIRE(parsed.contains("result"));
179+
REQUIRE(parsed["result"].is_number());
180+
REQUIRE(parsed["result"].get<int>() == 12);
181+
}
182+
183+
SECTION("GPT-5 should not fail authentication with valid key") {
184+
OpenAIClient client(apiKey);
185+
186+
LLMRequestConfig config;
187+
config.client = "openai";
188+
config.model = "gpt-5"; // Reasoning family
189+
190+
LLMRequest request(config, "Say OK");
191+
192+
std::cout << "Auth check call to GPT-5..." << std::endl;
193+
auto response = client.sendRequest(request);
194+
if (!response.success) {
195+
// If we ever get invalid_api_key here, make it explicit
196+
bool invalidKey = response.errorMessage.find("invalid_api_key") != std::string::npos ||
197+
response.errorMessage.find("Incorrect API key") != std::string::npos;
198+
if (invalidKey) {
199+
FAIL("Received invalid_api_key from OpenAI for GPT-5 despite OPENAI_API_KEY being set.");
200+
}
201+
}
202+
REQUIRE(response.success == true);
203+
}
115204
}
116205

117206
TEST_CASE("OpenAI Integration - Async request", "[openai][integration][manual]") {

tests/integration/test_simple_integration.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ TEST_CASE("OpenAI Integration - Simple request", "[openai][integration]") {
101101
LLMRequestConfig config;
102102
config.client = "openai";
103103
config.model = "gpt-4o-mini"; // Use cheaper model for testing
104-
config.maxTokens = 20; // Limit tokens to minimize cost
104+
// Do not set maxTokens explicitly; let server default decide
105105
config.temperature = 0.1f; // Low temperature for consistency
106106

107107
LLMRequest request(config, "Say 'Hello, World!' and nothing else.");

tests/unit/test_model_enum.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
TEST_CASE("OpenAI Model enum functionality", "[openai][model][enum]") {
77
SECTION("Model enum to string conversion") {
8+
REQUIRE(OpenAIClient::modelToString(OpenAI::Model::GPT_5) == "gpt-5");
89
REQUIRE(OpenAIClient::modelToString(OpenAI::Model::GPT_4_1) == "gpt-4.1");
910
REQUIRE(OpenAIClient::modelToString(OpenAI::Model::GPT_4_1_Mini) == "gpt-4.1-mini");
1011
REQUIRE(OpenAIClient::modelToString(OpenAI::Model::GPT_4_1_Nano) == "gpt-4.1-nano");
@@ -16,6 +17,7 @@ TEST_CASE("OpenAI Model enum functionality", "[openai][model][enum]") {
1617
}
1718

1819
SECTION("String to Model enum conversion") {
20+
REQUIRE(OpenAIClient::stringToModel("gpt-5") == OpenAI::Model::GPT_5);
1921
REQUIRE(OpenAIClient::stringToModel("gpt-4.1") == OpenAI::Model::GPT_4_1);
2022
REQUIRE(OpenAIClient::stringToModel("gpt-4.1-mini") == OpenAI::Model::GPT_4_1_Mini);
2123
REQUIRE(OpenAIClient::stringToModel("gpt-4.1-nano") == OpenAI::Model::GPT_4_1_Nano);
@@ -29,6 +31,7 @@ TEST_CASE("OpenAI Model enum functionality", "[openai][model][enum]") {
2931
SECTION("Model support checking") {
3032
OpenAIClient client("test-key");
3133

34+
REQUIRE(client.isModelSupported(OpenAI::Model::GPT_5) == true);
3235
REQUIRE(client.isModelSupported(OpenAI::Model::GPT_4_1) == true);
3336
REQUIRE(client.isModelSupported(OpenAI::Model::GPT_4o_Mini) == true);
3437
REQUIRE(client.isModelSupported(OpenAI::Model::GPT_3_5_Turbo) == true);
@@ -37,9 +40,10 @@ TEST_CASE("OpenAI Model enum functionality", "[openai][model][enum]") {
3740

3841
SECTION("Available model enums") {
3942
auto models = OpenAIClient::getAvailableModelEnums();
40-
REQUIRE(models.size() >= 8); // Should include all defined models
43+
REQUIRE(models.size() >= 9); // Should include all defined models
4144

4245
// Check that all expected models are present
46+
REQUIRE(std::find(models.begin(), models.end(), OpenAI::Model::GPT_5) != models.end());
4347
REQUIRE(std::find(models.begin(), models.end(), OpenAI::Model::GPT_4_1) != models.end());
4448
REQUIRE(std::find(models.begin(), models.end(), OpenAI::Model::GPT_4_1_Mini) !=
4549
models.end());

0 commit comments

Comments
 (0)