Skip to content

Commit a42f913

Browse files
author
mkulakow
committed
Convert flat tools to nested format
1 parent 36f2b66 commit a42f913

1 file changed

Lines changed: 83 additions & 6 deletions

File tree

src/llm/apis/openai_responses.cpp

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ absl::Status OpenAIResponsesHandler::parseInput(std::optional<std::string> allow
8888
return absl::InvalidArgumentError("Messages array cannot be empty");
8989
}
9090

91+
std::string pendingReasoning;
92+
9193
for (size_t i = 0; i < inputIt->value.GetArray().Size(); ++i) {
9294
auto& item = inputIt->value.GetArray()[i];
9395
if (!item.IsObject()) {
@@ -101,8 +103,23 @@ absl::Status OpenAIResponsesHandler::parseInput(std::optional<std::string> allow
101103
const std::string itemType = (itemTypeIt != itemObj.MemberEnd() && itemTypeIt->value.IsString())
102104
? itemTypeIt->value.GetString() : "";
103105

104-
// Skip reasoning items — they are internal chain-of-thought and not passed to the model
106+
// Parse reasoning items — extract summary text and buffer for the next assistant message
105107
if (itemType == "reasoning") {
108+
auto summaryIt = itemObj.FindMember("summary");
109+
if (summaryIt != itemObj.MemberEnd() && summaryIt->value.IsArray()) {
110+
for (const auto& summaryItem : summaryIt->value.GetArray()) {
111+
if (!summaryItem.IsObject()) continue;
112+
auto stTypeIt = summaryItem.GetObject().FindMember("type");
113+
if (stTypeIt == summaryItem.GetObject().MemberEnd() || !stTypeIt->value.IsString()) continue;
114+
if (std::string(stTypeIt->value.GetString()) == "summary_text") {
115+
auto textIt = summaryItem.GetObject().FindMember("text");
116+
if (textIt != summaryItem.GetObject().MemberEnd() && textIt->value.IsString()) {
117+
if (!pendingReasoning.empty()) pendingReasoning += "\n";
118+
pendingReasoning += textIt->value.GetString();
119+
}
120+
}
121+
}
122+
}
106123
continue;
107124
}
108125

@@ -113,6 +130,10 @@ absl::Status OpenAIResponsesHandler::parseInput(std::optional<std::string> allow
113130
request.chatHistory.push_back({});
114131
request.chatHistory.last()["role"] = "assistant";
115132
request.chatHistory.last()["content"] = "";
133+
if (!pendingReasoning.empty()) {
134+
request.chatHistory.last()["reasoning_content"] = pendingReasoning;
135+
pendingReasoning.clear();
136+
}
116137
continue;
117138
}
118139

@@ -139,6 +160,10 @@ absl::Status OpenAIResponsesHandler::parseInput(std::optional<std::string> allow
139160

140161
request.chatHistory.push_back({});
141162
request.chatHistory.last()["role"] = roleIt->value.GetString();
163+
if (!pendingReasoning.empty()) {
164+
request.chatHistory.last()["reasoning_content"] = pendingReasoning;
165+
pendingReasoning.clear();
166+
}
142167

143168
auto contentIt = itemObj.FindMember("content");
144169
if (contentIt == itemObj.MemberEnd()) {
@@ -285,6 +310,7 @@ absl::Status OpenAIResponsesHandler::parseResponsesPart(std::optional<uint32_t>
285310
if (inputArrIt != doc.MemberEnd() && inputArrIt->value.IsArray()) {
286311
// Pending function_call items to be merged into the next assistant message
287312
std::vector<const rapidjson::Value*> pendingFunctionCalls;
313+
std::string pendingReasoningJson;
288314

289315
// Helper: flush pending function_calls as an assistant message with the given text content
290316
auto flushPendingFunctionCalls = [&](const std::string& textContent) {
@@ -294,6 +320,10 @@ absl::Status OpenAIResponsesHandler::parseResponsesPart(std::optional<uint32_t>
294320
Value msgObj(kObjectType);
295321
msgObj.AddMember("role", Value("assistant", alloc), alloc);
296322
msgObj.AddMember("content", Value(textContent.c_str(), alloc), alloc);
323+
if (!pendingReasoningJson.empty()) {
324+
msgObj.AddMember("reasoning_content", Value(pendingReasoningJson.c_str(), alloc), alloc);
325+
pendingReasoningJson.clear();
326+
}
297327
Value toolCallsArray(kArrayType);
298328
for (const auto* fc : pendingFunctionCalls) {
299329
auto fcObj = fc->GetObject();
@@ -351,8 +381,23 @@ absl::Status OpenAIResponsesHandler::parseResponsesPart(std::optional<uint32_t>
351381
const std::string itemType = (itemTypeIt != itemObj.MemberEnd() && itemTypeIt->value.IsString())
352382
? itemTypeIt->value.GetString() : "";
353383

354-
// Skip reasoning items
384+
// Parse reasoning items — extract summary text and buffer for the next assistant message
355385
if (itemType == "reasoning") {
386+
auto summaryIt = itemObj.FindMember("summary");
387+
if (summaryIt != itemObj.MemberEnd() && summaryIt->value.IsArray()) {
388+
for (const auto& summaryItem : summaryIt->value.GetArray()) {
389+
if (!summaryItem.IsObject()) continue;
390+
auto stTypeIt = summaryItem.GetObject().FindMember("type");
391+
if (stTypeIt == summaryItem.GetObject().MemberEnd() || !stTypeIt->value.IsString()) continue;
392+
if (std::string(stTypeIt->value.GetString()) == "summary_text") {
393+
auto textIt = summaryItem.GetObject().FindMember("text");
394+
if (textIt != summaryItem.GetObject().MemberEnd() && textIt->value.IsString()) {
395+
if (!pendingReasoningJson.empty()) pendingReasoningJson += "\n";
396+
pendingReasoningJson += textIt->value.GetString();
397+
}
398+
}
399+
}
400+
}
356401
continue;
357402
}
358403

@@ -401,6 +446,10 @@ absl::Status OpenAIResponsesHandler::parseResponsesPart(std::optional<uint32_t>
401446
Value msgObj(kObjectType);
402447
msgObj.AddMember("role", Value("assistant", alloc), alloc);
403448
msgObj.AddMember("content", Value(contentText.c_str(), alloc), alloc);
449+
if (!pendingReasoningJson.empty()) {
450+
msgObj.AddMember("reasoning_content", Value(pendingReasoningJson.c_str(), alloc), alloc);
451+
pendingReasoningJson.clear();
452+
}
404453
messagesArray.PushBack(msgObj, alloc);
405454
}
406455
} else {
@@ -419,11 +468,39 @@ absl::Status OpenAIResponsesHandler::parseResponsesPart(std::optional<uint32_t>
419468

420469
processedDoc.AddMember("messages", messagesArray, alloc);
421470

422-
// Copy tools from original doc if present
471+
// Convert tools from Responses API flat format to chat/completions nested format.
472+
// Responses API: {"type": "function", "name": "foo", "description": "...", "parameters": {...}}
473+
// Chat/completions: {"type": "function", "function": {"name": "foo", "description": "...", "parameters": {...}}}
423474
auto toolsIt = doc.FindMember("tools");
424-
if (toolsIt != doc.MemberEnd() && !toolsIt->value.IsNull()) {
425-
Value toolsCopy(toolsIt->value, alloc);
426-
processedDoc.AddMember("tools", toolsCopy, alloc);
475+
if (toolsIt != doc.MemberEnd() && !toolsIt->value.IsNull() && toolsIt->value.IsArray()) {
476+
Value toolsArray(kArrayType);
477+
for (const auto& tool : toolsIt->value.GetArray()) {
478+
if (!tool.IsObject()) continue;
479+
auto toolObj = tool.GetObject();
480+
// Check if this tool already has a nested "function" key (chat/completions format)
481+
if (toolObj.FindMember("function") != toolObj.MemberEnd()) {
482+
// Already in chat/completions format — copy as-is
483+
Value toolCopy(tool, alloc);
484+
toolsArray.PushBack(toolCopy, alloc);
485+
} else {
486+
// Responses API flat format — wrap under "function" key
487+
Value convertedTool(kObjectType);
488+
convertedTool.AddMember("type", Value("function", alloc), alloc);
489+
Value funcObj(kObjectType);
490+
// Copy all fields except "type" and "response" into the nested function object
491+
for (auto it2 = toolObj.MemberBegin(); it2 != toolObj.MemberEnd(); ++it2) {
492+
if (!it2->name.IsString()) continue;
493+
const std::string fieldName = it2->name.GetString();
494+
if (fieldName == "type" || fieldName == "response") continue;
495+
Value keyCopy(it2->name, alloc);
496+
Value valCopy(it2->value, alloc);
497+
funcObj.AddMember(keyCopy, valCopy, alloc);
498+
}
499+
convertedTool.AddMember("function", funcObj, alloc);
500+
toolsArray.PushBack(convertedTool, alloc);
501+
}
502+
}
503+
processedDoc.AddMember("tools", toolsArray, alloc);
427504
}
428505

429506
// Copy chat_template_kwargs from original doc if present

0 commit comments

Comments
 (0)