feat: Add OpenAI Responses API (#282)

* feat: Add OpenAI Responses API

* fix: Make temperature optional

* chore: Increase default value of max tokens
This commit is contained in:
Petr Mironychev
2025-12-01 12:14:55 +01:00
committed by GitHub
parent e1fa01d123
commit a466332822
26 changed files with 3261 additions and 4 deletions

View File

@ -0,0 +1,54 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
namespace QodeAssist::OpenAIResponses {
struct CancelResponseRequest
{
QString responseId;
QString buildUrl(const QString &baseUrl) const
{
return QString("%1/v1/responses/%2/cancel").arg(baseUrl, responseId);
}
bool isValid() const { return !responseId.isEmpty(); }
};
class CancelResponseRequestBuilder
{
public:
CancelResponseRequestBuilder &setResponseId(const QString &id)
{
m_request.responseId = id;
return *this;
}
CancelResponseRequest build() const { return m_request; }
private:
CancelResponseRequest m_request;
};
} // namespace QodeAssist::OpenAIResponses

View File

@ -0,0 +1,69 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonObject>
#include <QString>
namespace QodeAssist::OpenAIResponses {
struct DeleteResponseRequest
{
QString responseId;
QString buildUrl(const QString &baseUrl) const
{
return QString("%1/v1/responses/%2").arg(baseUrl, responseId);
}
bool isValid() const { return !responseId.isEmpty(); }
};
class DeleteResponseRequestBuilder
{
public:
DeleteResponseRequestBuilder &setResponseId(const QString &id)
{
m_request.responseId = id;
return *this;
}
DeleteResponseRequest build() const { return m_request; }
private:
DeleteResponseRequest m_request;
};
struct DeleteResponseResult
{
bool success = false;
QString message;
static DeleteResponseResult fromJson(const QJsonObject &obj)
{
DeleteResponseResult result;
result.success = obj["success"].toBool();
result.message = obj["message"].toString();
return result;
}
};
} // namespace QodeAssist::OpenAIResponses

View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonObject>
#include <QString>
#include <QStringList>
#include <optional>
namespace QodeAssist::OpenAIResponses {
struct GetResponseRequest
{
QString responseId;
std::optional<QStringList> include;
std::optional<bool> includeObfuscation;
std::optional<int> startingAfter;
std::optional<bool> stream;
QString buildUrl(const QString &baseUrl) const
{
QString url = QString("%1/v1/responses/%2").arg(baseUrl, responseId);
QStringList queryParams;
if (include && !include->isEmpty()) {
for (const auto &item : *include) {
queryParams.append(QString("include=%1").arg(item));
}
}
if (includeObfuscation) {
queryParams.append(
QString("include_obfuscation=%1").arg(*includeObfuscation ? "true" : "false"));
}
if (startingAfter) {
queryParams.append(QString("starting_after=%1").arg(*startingAfter));
}
if (stream) {
queryParams.append(QString("stream=%1").arg(*stream ? "true" : "false"));
}
if (!queryParams.isEmpty()) {
url += "?" + queryParams.join("&");
}
return url;
}
bool isValid() const { return !responseId.isEmpty(); }
};
class GetResponseRequestBuilder
{
public:
GetResponseRequestBuilder &setResponseId(const QString &id)
{
m_request.responseId = id;
return *this;
}
GetResponseRequestBuilder &setInclude(const QStringList &include)
{
m_request.include = include;
return *this;
}
GetResponseRequestBuilder &addInclude(const QString &item)
{
if (!m_request.include) {
m_request.include = QStringList();
}
m_request.include->append(item);
return *this;
}
GetResponseRequestBuilder &setIncludeObfuscation(bool enabled)
{
m_request.includeObfuscation = enabled;
return *this;
}
GetResponseRequestBuilder &setStartingAfter(int sequence)
{
m_request.startingAfter = sequence;
return *this;
}
GetResponseRequestBuilder &setStream(bool enabled)
{
m_request.stream = enabled;
return *this;
}
GetResponseRequest build() const { return m_request; }
private:
GetResponseRequest m_request;
};
} // namespace QodeAssist::OpenAIResponses

View File

@ -0,0 +1,219 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "ModelRequest.hpp"
#include <QJsonObject>
#include <QString>
namespace QodeAssist::OpenAIResponses {
struct InputTokensRequest
{
std::optional<QString> conversation;
std::optional<QJsonArray> input;
std::optional<QString> instructions;
std::optional<QString> model;
std::optional<bool> parallelToolCalls;
std::optional<QString> previousResponseId;
std::optional<QJsonObject> reasoning;
std::optional<QJsonObject> text;
std::optional<QJsonValue> toolChoice;
std::optional<QJsonArray> tools;
std::optional<QString> truncation;
QString buildUrl(const QString &baseUrl) const
{
return QString("%1/v1/responses/input_tokens").arg(baseUrl);
}
QJsonObject toJson() const
{
QJsonObject obj;
if (conversation)
obj["conversation"] = *conversation;
if (input)
obj["input"] = *input;
if (instructions)
obj["instructions"] = *instructions;
if (model)
obj["model"] = *model;
if (parallelToolCalls)
obj["parallel_tool_calls"] = *parallelToolCalls;
if (previousResponseId)
obj["previous_response_id"] = *previousResponseId;
if (reasoning)
obj["reasoning"] = *reasoning;
if (text)
obj["text"] = *text;
if (toolChoice)
obj["tool_choice"] = *toolChoice;
if (tools)
obj["tools"] = *tools;
if (truncation)
obj["truncation"] = *truncation;
return obj;
}
bool isValid() const { return input.has_value() || previousResponseId.has_value(); }
};
class InputTokensRequestBuilder
{
public:
InputTokensRequestBuilder &setConversation(const QString &conversationId)
{
m_request.conversation = conversationId;
return *this;
}
InputTokensRequestBuilder &setInput(const QJsonArray &input)
{
m_request.input = input;
return *this;
}
InputTokensRequestBuilder &addInputMessage(const Message &message)
{
if (!m_request.input) {
m_request.input = QJsonArray();
}
m_request.input->append(message.toJson());
return *this;
}
InputTokensRequestBuilder &setInstructions(const QString &instructions)
{
m_request.instructions = instructions;
return *this;
}
InputTokensRequestBuilder &setModel(const QString &model)
{
m_request.model = model;
return *this;
}
InputTokensRequestBuilder &setParallelToolCalls(bool enabled)
{
m_request.parallelToolCalls = enabled;
return *this;
}
InputTokensRequestBuilder &setPreviousResponseId(const QString &responseId)
{
m_request.previousResponseId = responseId;
return *this;
}
InputTokensRequestBuilder &setReasoning(const QJsonObject &reasoning)
{
m_request.reasoning = reasoning;
return *this;
}
InputTokensRequestBuilder &setReasoningEffort(ReasoningEffort effort)
{
QString effortStr;
switch (effort) {
case ReasoningEffort::None:
effortStr = "none";
break;
case ReasoningEffort::Minimal:
effortStr = "minimal";
break;
case ReasoningEffort::Low:
effortStr = "low";
break;
case ReasoningEffort::Medium:
effortStr = "medium";
break;
case ReasoningEffort::High:
effortStr = "high";
break;
}
m_request.reasoning = QJsonObject{{"effort", effortStr}};
return *this;
}
InputTokensRequestBuilder &setText(const QJsonObject &text)
{
m_request.text = text;
return *this;
}
InputTokensRequestBuilder &setTextFormat(const TextFormatOptions &format)
{
m_request.text = format.toJson();
return *this;
}
InputTokensRequestBuilder &setToolChoice(const QJsonValue &toolChoice)
{
m_request.toolChoice = toolChoice;
return *this;
}
InputTokensRequestBuilder &setTools(const QJsonArray &tools)
{
m_request.tools = tools;
return *this;
}
InputTokensRequestBuilder &addTool(const Tool &tool)
{
if (!m_request.tools) {
m_request.tools = QJsonArray();
}
m_request.tools->append(tool.toJson());
return *this;
}
InputTokensRequestBuilder &setTruncation(const QString &truncation)
{
m_request.truncation = truncation;
return *this;
}
InputTokensRequest build() const { return m_request; }
private:
InputTokensRequest m_request;
};
struct InputTokensResponse
{
QString object;
int inputTokens = 0;
static InputTokensResponse fromJson(const QJsonObject &obj)
{
InputTokensResponse result;
result.object = obj["object"].toString();
result.inputTokens = obj["input_tokens"].toInt();
return result;
}
};
} // namespace QodeAssist::OpenAIResponses

View File

@ -0,0 +1,143 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
namespace QodeAssist::OpenAIResponses {
/*
* REFERENCE: Item Types in List Input Items Response
* ===================================================
*
* The `data` array in ListInputItemsResponse can contain various item types.
* This file serves as a reference for all possible item types.
*
* EXISTING TYPES (already implemented):
* -------------------------------------
* - MessageOutput (in ResponseObject.hpp)
* - FunctionCall (in ResponseObject.hpp)
* - ReasoningOutput (in ResponseObject.hpp)
* - FileSearchCall (in ResponseObject.hpp)
* - CodeInterpreterCall (in ResponseObject.hpp)
* - Message (in ModelRequest.hpp) - for input messages
*
* ADDITIONAL TYPES (to be implemented if needed):
* -----------------------------------------------
*
* 1. Computer Tool Call (computer_call)
* - Computer use tool for UI automation
* - Properties: action, call_id, id, pending_safety_checks, status, type
* - Actions: click, double_click, drag, keypress, move, screenshot, scroll, type, wait
*
* 2. Computer Tool Call Output (computer_call_output)
* - Output from computer tool
* - Properties: call_id, id, output, type, acknowledged_safety_checks, status
*
* 3. Web Search Tool Call (web_search_call)
* - Web search results
* - Properties: action, id, status, type
* - Actions: search, open_page, find
*
* 4. Image Generation Call (image_generation_call)
* - AI image generation request
* - Properties: id, result (base64), status, type
*
* 5. Local Shell Call (local_shell_call)
* - Execute shell commands locally
* - Properties: action (exec), call_id, id, status, type
* - Action properties: command, env, timeout_ms, user, working_directory
*
* 6. Local Shell Call Output (local_shell_call_output)
* - Output from local shell execution
* - Properties: id, output (JSON string), type, status
*
* 7. Shell Tool Call (shell_call)
* - Managed shell environment execution
* - Properties: action, call_id, id, status, type, created_by
*
* 8. Shell Call Output (shell_call_output)
* - Output from shell tool
* - Properties: call_id, id, max_output_length, output (array), type, created_by
* - Output chunks: outcome (exit/timeout), stderr, stdout
*
* 9. Apply Patch Tool Call (apply_patch_call)
* - File diff operations
* - Properties: call_id, id, operation, status, type, created_by
* - Operations: create_file, delete_file, update_file
*
* 10. Apply Patch Tool Call Output (apply_patch_call_output)
* - Output from patch operations
* - Properties: call_id, id, status, type, created_by, output
*
* 11. MCP List Tools (mcp_list_tools)
* - List of tools from MCP server
* - Properties: id, server_label, tools (array), type, error
*
* 12. MCP Approval Request (mcp_approval_request)
* - Request for human approval
* - Properties: arguments, id, name, server_label, type
*
* 13. MCP Approval Response (mcp_approval_response)
* - Response to approval request
* - Properties: approval_request_id, approve (bool), id, type, reason
*
* 14. MCP Tool Call (mcp_call)
* - Tool invocation on MCP server
* - Properties: arguments, id, name, server_label, type
* - Optional: approval_request_id, error, output, status
*
* 15. Custom Tool Call (custom_tool_call)
* - User-defined tool call
* - Properties: call_id, input, name, type, id
*
* 16. Custom Tool Call Output (custom_tool_call_output)
* - Output from custom tool
* - Properties: call_id, output (string or array), type, id
*
* 17. Item Reference (item_reference)
* - Internal reference to another item
* - Properties: id, type
*
* USAGE:
* ------
* When parsing ListInputItemsResponse.data array:
* 1. Check item["type"] field
* 2. Use appropriate parser based on type
* 3. For existing types, use ResponseObject.hpp or ModelRequest.hpp
* 4. For additional types, implement parsers as needed
*
* EXAMPLE:
* --------
* for (const auto &itemValue : response.data) {
* const QJsonObject itemObj = itemValue.toObject();
* const QString type = itemObj["type"].toString();
*
* if (type == "message") {
* // Use MessageOutput or Message
* } else if (type == "function_call") {
* // Use FunctionCall
* } else if (type == "computer_call") {
* // Implement ComputerCall parser
* }
* // ... handle other types
* }
*/
} // namespace QodeAssist::OpenAIResponses

View File

@ -0,0 +1,166 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QStringList>
#include <optional>
namespace QodeAssist::OpenAIResponses {
enum class SortOrder { Ascending, Descending };
struct ListInputItemsRequest
{
QString responseId;
std::optional<QString> after;
std::optional<QStringList> include;
std::optional<int> limit;
std::optional<SortOrder> order;
QString buildUrl(const QString &baseUrl) const
{
QString url = QString("%1/v1/responses/%2/input_items").arg(baseUrl, responseId);
QStringList queryParams;
if (after) {
queryParams.append(QString("after=%1").arg(*after));
}
if (include && !include->isEmpty()) {
for (const auto &item : *include) {
queryParams.append(QString("include=%1").arg(item));
}
}
if (limit) {
queryParams.append(QString("limit=%1").arg(*limit));
}
if (order) {
QString orderStr = (*order == SortOrder::Ascending) ? "asc" : "desc";
queryParams.append(QString("order=%1").arg(orderStr));
}
if (!queryParams.isEmpty()) {
url += "?" + queryParams.join("&");
}
return url;
}
bool isValid() const
{
if (responseId.isEmpty()) {
return false;
}
if (limit && (*limit < 1 || *limit > 100)) {
return false;
}
return true;
}
};
class ListInputItemsRequestBuilder
{
public:
ListInputItemsRequestBuilder &setResponseId(const QString &id)
{
m_request.responseId = id;
return *this;
}
ListInputItemsRequestBuilder &setAfter(const QString &itemId)
{
m_request.after = itemId;
return *this;
}
ListInputItemsRequestBuilder &setInclude(const QStringList &include)
{
m_request.include = include;
return *this;
}
ListInputItemsRequestBuilder &addInclude(const QString &item)
{
if (!m_request.include) {
m_request.include = QStringList();
}
m_request.include->append(item);
return *this;
}
ListInputItemsRequestBuilder &setLimit(int limit)
{
m_request.limit = limit;
return *this;
}
ListInputItemsRequestBuilder &setOrder(SortOrder order)
{
m_request.order = order;
return *this;
}
ListInputItemsRequestBuilder &setAscendingOrder()
{
m_request.order = SortOrder::Ascending;
return *this;
}
ListInputItemsRequestBuilder &setDescendingOrder()
{
m_request.order = SortOrder::Descending;
return *this;
}
ListInputItemsRequest build() const { return m_request; }
private:
ListInputItemsRequest m_request;
};
struct ListInputItemsResponse
{
QJsonArray data;
QString firstId;
QString lastId;
bool hasMore = false;
QString object;
static ListInputItemsResponse fromJson(const QJsonObject &obj)
{
ListInputItemsResponse result;
result.data = obj["data"].toArray();
result.firstId = obj["first_id"].toString();
result.lastId = obj["last_id"].toString();
result.hasMore = obj["has_more"].toBool();
result.object = obj["object"].toString();
return result;
}
};
} // namespace QodeAssist::OpenAIResponses

View File

@ -0,0 +1,354 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVariant>
#include <optional>
#include <variant>
namespace QodeAssist::OpenAIResponses {
enum class Role { User, Assistant, System, Developer };
enum class MessageStatus { InProgress, Completed, Incomplete };
enum class ReasoningEffort { None, Minimal, Low, Medium, High };
enum class TextFormat { Text, JsonSchema, JsonObject };
struct InputText
{
QString text;
QJsonObject toJson() const
{
return QJsonObject{{"type", "input_text"}, {"text", text}};
}
bool isValid() const noexcept { return !text.isEmpty(); }
};
struct InputImage
{
std::optional<QString> fileId;
std::optional<QString> imageUrl;
QString detail = "auto";
QJsonObject toJson() const
{
QJsonObject obj{{"type", "input_image"}, {"detail", detail}};
if (fileId)
obj["file_id"] = *fileId;
if (imageUrl)
obj["image_url"] = *imageUrl;
return obj;
}
bool isValid() const noexcept { return fileId.has_value() || imageUrl.has_value(); }
};
struct InputFile
{
std::optional<QString> fileId;
std::optional<QString> fileUrl;
std::optional<QString> fileData;
std::optional<QString> filename;
QJsonObject toJson() const
{
QJsonObject obj{{"type", "input_file"}};
if (fileId)
obj["file_id"] = *fileId;
if (fileUrl)
obj["file_url"] = *fileUrl;
if (fileData)
obj["file_data"] = *fileData;
if (filename)
obj["filename"] = *filename;
return obj;
}
bool isValid() const noexcept
{
return fileId.has_value() || fileUrl.has_value() || fileData.has_value();
}
};
class MessageContent
{
public:
MessageContent(QString text) : m_variant(std::move(text)) {}
MessageContent(InputText text) : m_variant(std::move(text)) {}
MessageContent(InputImage image) : m_variant(std::move(image)) {}
MessageContent(InputFile file) : m_variant(std::move(file)) {}
QJsonValue toJson() const
{
return std::visit([](const auto &content) -> QJsonValue {
using T = std::decay_t<decltype(content)>;
if constexpr (std::is_same_v<T, QString>) {
return content;
} else {
return content.toJson();
}
}, m_variant);
}
bool isValid() const noexcept
{
return std::visit([](const auto &content) -> bool {
using T = std::decay_t<decltype(content)>;
if constexpr (std::is_same_v<T, QString>) {
return !content.isEmpty();
} else {
return content.isValid();
}
}, m_variant);
}
private:
std::variant<QString, InputText, InputImage, InputFile> m_variant;
};
struct Message
{
Role role;
QList<MessageContent> content;
std::optional<MessageStatus> status;
QJsonObject toJson() const
{
QJsonObject obj;
obj["role"] = roleToString(role);
if (content.size() == 1) {
obj["content"] = content[0].toJson();
} else {
QJsonArray arr;
for (const auto &c : content) {
arr.append(c.toJson());
}
obj["content"] = arr;
}
if (status) {
obj["status"] = statusToString(*status);
}
return obj;
}
bool isValid() const noexcept
{
if (content.isEmpty()) {
return false;
}
for (const auto &c : content) {
if (!c.isValid()) {
return false;
}
}
return true;
}
static QString roleToString(Role r) noexcept
{
switch (r) {
case Role::User:
return "user";
case Role::Assistant:
return "assistant";
case Role::System:
return "system";
case Role::Developer:
return "developer";
}
return "user";
}
static QString statusToString(MessageStatus s) noexcept
{
switch (s) {
case MessageStatus::InProgress:
return "in_progress";
case MessageStatus::Completed:
return "completed";
case MessageStatus::Incomplete:
return "incomplete";
}
return "in_progress";
}
};
struct FunctionTool
{
QString name;
QJsonObject parameters;
std::optional<QString> description;
bool strict = true;
QJsonObject toJson() const
{
QJsonObject obj{{"type", "function"},
{"name", name},
{"parameters", parameters},
{"strict", strict}};
if (description)
obj["description"] = *description;
return obj;
}
bool isValid() const noexcept
{
return !name.isEmpty() && !parameters.isEmpty();
}
};
struct FileSearchTool
{
QStringList vectorStoreIds;
std::optional<int> maxNumResults;
std::optional<double> scoreThreshold;
QJsonObject toJson() const
{
QJsonObject obj{{"type", "file_search"}};
QJsonArray ids;
for (const auto &id : vectorStoreIds) {
ids.append(id);
}
obj["vector_store_ids"] = ids;
if (maxNumResults)
obj["max_num_results"] = *maxNumResults;
if (scoreThreshold)
obj["score_threshold"] = *scoreThreshold;
return obj;
}
bool isValid() const noexcept
{
return !vectorStoreIds.isEmpty();
}
};
struct WebSearchTool
{
QString searchContextSize = "medium";
QJsonObject toJson() const
{
return QJsonObject{{"type", "web_search"}, {"search_context_size", searchContextSize}};
}
bool isValid() const noexcept
{
return !searchContextSize.isEmpty();
}
};
struct CodeInterpreterTool
{
QString container;
QJsonObject toJson() const
{
return QJsonObject{{"type", "code_interpreter"}, {"container", container}};
}
bool isValid() const noexcept
{
return !container.isEmpty();
}
};
class Tool
{
public:
Tool(FunctionTool tool) : m_variant(std::move(tool)) {}
Tool(FileSearchTool tool) : m_variant(std::move(tool)) {}
Tool(WebSearchTool tool) : m_variant(std::move(tool)) {}
Tool(CodeInterpreterTool tool) : m_variant(std::move(tool)) {}
QJsonObject toJson() const
{
return std::visit([](const auto &t) { return t.toJson(); }, m_variant);
}
bool isValid() const noexcept
{
return std::visit([](const auto &t) { return t.isValid(); }, m_variant);
}
private:
std::variant<FunctionTool, FileSearchTool, WebSearchTool, CodeInterpreterTool> m_variant;
};
struct TextFormatOptions
{
TextFormat type = TextFormat::Text;
std::optional<QString> name;
std::optional<QJsonObject> schema;
std::optional<QString> description;
std::optional<bool> strict;
QJsonObject toJson() const
{
QJsonObject obj;
switch (type) {
case TextFormat::Text:
obj["type"] = "text";
break;
case TextFormat::JsonSchema:
obj["type"] = "json_schema";
if (name)
obj["name"] = *name;
if (schema)
obj["schema"] = *schema;
if (description)
obj["description"] = *description;
if (strict)
obj["strict"] = *strict;
break;
case TextFormat::JsonObject:
obj["type"] = "json_object";
break;
}
return obj;
}
bool isValid() const noexcept
{
if (type == TextFormat::JsonSchema) {
return name.has_value() && schema.has_value();
}
return true;
}
};
} // namespace QodeAssist::OpenAIResponses

View File

@ -0,0 +1,562 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <variant>
#include <QJsonArray>
#include <QJsonObject>
#include <QList>
#include <QString>
namespace QodeAssist::OpenAIResponses {
enum class ResponseStatus { Completed, Failed, InProgress, Cancelled, Queued, Incomplete };
enum class ItemStatus { InProgress, Completed, Incomplete };
struct FileCitation
{
QString fileId;
QString filename;
int index = 0;
static FileCitation fromJson(const QJsonObject &obj)
{
return {obj["file_id"].toString(), obj["filename"].toString(), obj["index"].toInt()};
}
bool isValid() const noexcept { return !fileId.isEmpty(); }
};
struct UrlCitation
{
QString url;
QString title;
int startIndex = 0;
int endIndex = 0;
static UrlCitation fromJson(const QJsonObject &obj)
{
return {
obj["url"].toString(),
obj["title"].toString(),
obj["start_index"].toInt(),
obj["end_index"].toInt()};
}
bool isValid() const noexcept { return !url.isEmpty(); }
};
struct OutputText
{
QString text;
QList<FileCitation> fileCitations;
QList<UrlCitation> urlCitations;
static OutputText fromJson(const QJsonObject &obj)
{
OutputText result;
result.text = obj["text"].toString();
if (obj.contains("annotations")) {
const QJsonArray annotations = obj["annotations"].toArray();
result.fileCitations.reserve(annotations.size());
result.urlCitations.reserve(annotations.size());
for (const auto &annValue : annotations) {
const QJsonObject ann = annValue.toObject();
const QString type = ann["type"].toString();
if (type == "file_citation") {
result.fileCitations.append(FileCitation::fromJson(ann));
} else if (type == "url_citation") {
result.urlCitations.append(UrlCitation::fromJson(ann));
}
}
}
return result;
}
bool isValid() const noexcept { return !text.isEmpty(); }
};
struct Refusal
{
QString refusal;
static Refusal fromJson(const QJsonObject &obj)
{
return {obj["refusal"].toString()};
}
bool isValid() const noexcept { return !refusal.isEmpty(); }
};
struct MessageOutput
{
QString id;
QString role;
ItemStatus status = ItemStatus::InProgress;
QList<OutputText> outputTexts;
QList<Refusal> refusals;
static MessageOutput fromJson(const QJsonObject &obj)
{
MessageOutput result;
result.id = obj["id"].toString();
result.role = obj["role"].toString();
const QString statusStr = obj["status"].toString();
if (statusStr == "in_progress")
result.status = ItemStatus::InProgress;
else if (statusStr == "completed")
result.status = ItemStatus::Completed;
else
result.status = ItemStatus::Incomplete;
if (obj.contains("content")) {
const QJsonArray content = obj["content"].toArray();
result.outputTexts.reserve(content.size());
result.refusals.reserve(content.size());
for (const auto &item : content) {
const QJsonObject itemObj = item.toObject();
const QString type = itemObj["type"].toString();
if (type == "output_text") {
result.outputTexts.append(OutputText::fromJson(itemObj));
} else if (type == "refusal") {
result.refusals.append(Refusal::fromJson(itemObj));
}
}
}
return result;
}
bool isValid() const noexcept { return !id.isEmpty(); }
bool hasContent() const noexcept { return !outputTexts.isEmpty() || !refusals.isEmpty(); }
};
struct FunctionCall
{
QString id;
QString callId;
QString name;
QString arguments;
ItemStatus status = ItemStatus::InProgress;
static FunctionCall fromJson(const QJsonObject &obj)
{
FunctionCall result;
result.id = obj["id"].toString();
result.callId = obj["call_id"].toString();
result.name = obj["name"].toString();
result.arguments = obj["arguments"].toString();
const QString statusStr = obj["status"].toString();
if (statusStr == "in_progress")
result.status = ItemStatus::InProgress;
else if (statusStr == "completed")
result.status = ItemStatus::Completed;
else
result.status = ItemStatus::Incomplete;
return result;
}
bool isValid() const noexcept { return !id.isEmpty() && !callId.isEmpty() && !name.isEmpty(); }
};
struct ReasoningOutput
{
QString id;
ItemStatus status = ItemStatus::InProgress;
QString summaryText;
QString encryptedContent;
QList<QString> contentTexts;
static ReasoningOutput fromJson(const QJsonObject &obj)
{
ReasoningOutput result;
result.id = obj["id"].toString();
const QString statusStr = obj["status"].toString();
if (statusStr == "in_progress")
result.status = ItemStatus::InProgress;
else if (statusStr == "completed")
result.status = ItemStatus::Completed;
else
result.status = ItemStatus::Incomplete;
if (obj.contains("summary")) {
const QJsonArray summary = obj["summary"].toArray();
for (const auto &item : summary) {
const QJsonObject itemObj = item.toObject();
if (itemObj["type"].toString() == "summary_text") {
result.summaryText = itemObj["text"].toString();
break;
}
}
}
if (obj.contains("content")) {
const QJsonArray content = obj["content"].toArray();
result.contentTexts.reserve(content.size());
for (const auto &item : content) {
const QJsonObject itemObj = item.toObject();
if (itemObj["type"].toString() == "reasoning_text") {
result.contentTexts.append(itemObj["text"].toString());
}
}
}
if (obj.contains("encrypted_content")) {
result.encryptedContent = obj["encrypted_content"].toString();
}
return result;
}
bool isValid() const noexcept { return !id.isEmpty(); }
bool hasContent() const noexcept
{
return !summaryText.isEmpty() || !contentTexts.isEmpty() || !encryptedContent.isEmpty();
}
};
struct FileSearchResult
{
QString fileId;
QString filename;
QString text;
double score = 0.0;
static FileSearchResult fromJson(const QJsonObject &obj)
{
return {
obj["file_id"].toString(),
obj["filename"].toString(),
obj["text"].toString(),
obj["score"].toDouble()};
}
bool isValid() const noexcept { return !fileId.isEmpty(); }
};
struct FileSearchCall
{
QString id;
QString status;
QStringList queries;
QList<FileSearchResult> results;
static FileSearchCall fromJson(const QJsonObject &obj)
{
FileSearchCall result;
result.id = obj["id"].toString();
result.status = obj["status"].toString();
if (obj.contains("queries")) {
const QJsonArray queries = obj["queries"].toArray();
result.queries.reserve(queries.size());
for (const auto &q : queries) {
result.queries.append(q.toString());
}
}
if (obj.contains("results")) {
const QJsonArray results = obj["results"].toArray();
result.results.reserve(results.size());
for (const auto &r : results) {
result.results.append(FileSearchResult::fromJson(r.toObject()));
}
}
return result;
}
bool isValid() const noexcept { return !id.isEmpty(); }
};
struct CodeInterpreterOutput
{
QString type;
QString logs;
QString imageUrl;
static CodeInterpreterOutput fromJson(const QJsonObject &obj)
{
CodeInterpreterOutput result;
result.type = obj["type"].toString();
if (result.type == "logs") {
result.logs = obj["logs"].toString();
} else if (result.type == "image") {
result.imageUrl = obj["url"].toString();
}
return result;
}
bool isValid() const noexcept
{
return !type.isEmpty() && (!logs.isEmpty() || !imageUrl.isEmpty());
}
};
struct CodeInterpreterCall
{
QString id;
QString containerId;
std::optional<QString> code;
QString status;
QList<CodeInterpreterOutput> outputs;
static CodeInterpreterCall fromJson(const QJsonObject &obj)
{
CodeInterpreterCall result;
result.id = obj["id"].toString();
result.containerId = obj["container_id"].toString();
result.status = obj["status"].toString();
if (obj.contains("code") && !obj["code"].isNull()) {
result.code = obj["code"].toString();
}
if (obj.contains("outputs")) {
const QJsonArray outputs = obj["outputs"].toArray();
result.outputs.reserve(outputs.size());
for (const auto &o : outputs) {
result.outputs.append(CodeInterpreterOutput::fromJson(o.toObject()));
}
}
return result;
}
bool isValid() const noexcept { return !id.isEmpty() && !containerId.isEmpty(); }
};
class OutputItem
{
public:
enum class Type { Message, FunctionCall, Reasoning, FileSearch, CodeInterpreter, Unknown };
explicit OutputItem(const MessageOutput &msg)
: m_type(Type::Message)
, m_data(msg)
{}
explicit OutputItem(const FunctionCall &call)
: m_type(Type::FunctionCall)
, m_data(call)
{}
explicit OutputItem(const ReasoningOutput &reasoning)
: m_type(Type::Reasoning)
, m_data(reasoning)
{}
explicit OutputItem(const FileSearchCall &search)
: m_type(Type::FileSearch)
, m_data(search)
{}
explicit OutputItem(const CodeInterpreterCall &interpreter)
: m_type(Type::CodeInterpreter)
, m_data(interpreter)
{}
Type type() const { return m_type; }
const MessageOutput *asMessage() const
{
return std::holds_alternative<MessageOutput>(m_data) ? &std::get<MessageOutput>(m_data)
: nullptr;
}
const FunctionCall *asFunctionCall() const
{
return std::holds_alternative<FunctionCall>(m_data) ? &std::get<FunctionCall>(m_data)
: nullptr;
}
const ReasoningOutput *asReasoning() const
{
return std::holds_alternative<ReasoningOutput>(m_data) ? &std::get<ReasoningOutput>(m_data)
: nullptr;
}
const FileSearchCall *asFileSearch() const
{
return std::holds_alternative<FileSearchCall>(m_data) ? &std::get<FileSearchCall>(m_data)
: nullptr;
}
const CodeInterpreterCall *asCodeInterpreter() const
{
return std::holds_alternative<CodeInterpreterCall>(m_data)
? &std::get<CodeInterpreterCall>(m_data)
: nullptr;
}
static OutputItem fromJson(const QJsonObject &obj)
{
const QString type = obj["type"].toString();
if (type == "message") {
return OutputItem(MessageOutput::fromJson(obj));
} else if (type == "function_call") {
return OutputItem(FunctionCall::fromJson(obj));
} else if (type == "reasoning") {
return OutputItem(ReasoningOutput::fromJson(obj));
} else if (type == "file_search_call") {
return OutputItem(FileSearchCall::fromJson(obj));
} else if (type == "code_interpreter_call") {
return OutputItem(CodeInterpreterCall::fromJson(obj));
}
return OutputItem(MessageOutput{});
}
private:
Type m_type;
std::variant<MessageOutput, FunctionCall, ReasoningOutput, FileSearchCall, CodeInterpreterCall>
m_data;
};
struct Usage
{
int inputTokens = 0;
int outputTokens = 0;
int totalTokens = 0;
static Usage fromJson(const QJsonObject &obj)
{
return {
obj["input_tokens"].toInt(),
obj["output_tokens"].toInt(),
obj["total_tokens"].toInt()
};
}
bool isValid() const noexcept { return totalTokens > 0; }
};
struct ResponseError
{
QString code;
QString message;
static ResponseError fromJson(const QJsonObject &obj)
{
return {obj["code"].toString(), obj["message"].toString()};
}
bool isValid() const noexcept { return !code.isEmpty() && !message.isEmpty(); }
};
struct Response
{
QString id;
qint64 createdAt = 0;
QString model;
ResponseStatus status = ResponseStatus::InProgress;
QList<OutputItem> output;
QString outputText;
std::optional<Usage> usage;
std::optional<ResponseError> error;
std::optional<QString> conversationId;
static Response fromJson(const QJsonObject &obj)
{
Response result;
result.id = obj["id"].toString();
result.createdAt = obj["created_at"].toInteger();
result.model = obj["model"].toString();
const QString statusStr = obj["status"].toString();
if (statusStr == "completed")
result.status = ResponseStatus::Completed;
else if (statusStr == "failed")
result.status = ResponseStatus::Failed;
else if (statusStr == "in_progress")
result.status = ResponseStatus::InProgress;
else if (statusStr == "cancelled")
result.status = ResponseStatus::Cancelled;
else if (statusStr == "queued")
result.status = ResponseStatus::Queued;
else
result.status = ResponseStatus::Incomplete;
if (obj.contains("output")) {
const QJsonArray output = obj["output"].toArray();
result.output.reserve(output.size());
for (const auto &item : output) {
result.output.append(OutputItem::fromJson(item.toObject()));
}
}
if (obj.contains("output_text")) {
result.outputText = obj["output_text"].toString();
}
if (obj.contains("usage")) {
result.usage = Usage::fromJson(obj["usage"].toObject());
}
if (obj.contains("error")) {
result.error = ResponseError::fromJson(obj["error"].toObject());
}
if (obj.contains("conversation")) {
const QJsonObject conv = obj["conversation"].toObject();
result.conversationId = conv["id"].toString();
}
return result;
}
QString getAggregatedText() const
{
if (!outputText.isEmpty()) {
return outputText;
}
QString aggregated;
for (const auto &item : output) {
if (const auto *msg = item.asMessage()) {
for (const auto &text : msg->outputTexts) {
aggregated += text.text;
}
}
}
return aggregated;
}
bool isValid() const noexcept { return !id.isEmpty(); }
bool hasError() const noexcept { return error.has_value(); }
bool isCompleted() const noexcept { return status == ResponseStatus::Completed; }
bool isFailed() const noexcept { return status == ResponseStatus::Failed; }
};
} // namespace QodeAssist::OpenAIResponses

View File

@ -0,0 +1,246 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "OpenAIResponsesMessage.hpp"
#include "OpenAIResponses/ResponseObject.hpp"
#include "logger/Logger.hpp"
#include <QJsonArray>
namespace QodeAssist::Providers {
OpenAIResponsesMessage::OpenAIResponsesMessage(QObject *parent)
: QObject(parent)
{}
void OpenAIResponsesMessage::handleItemDelta(const QJsonObject &item)
{
using namespace QodeAssist::OpenAIResponses;
const QString itemType = item["type"].toString();
if (itemType == "message" || (itemType.isEmpty() && item.contains("content"))) {
OutputItem outputItem = OutputItem::fromJson(item);
if (const auto *msg = outputItem.asMessage()) {
for (const auto &outputText : msg->outputTexts) {
if (!outputText.text.isEmpty()) {
auto textItem = getOrCreateTextItem();
textItem->appendText(outputText.text);
}
}
}
}
}
void OpenAIResponsesMessage::handleToolCallStart(const QString &callId, const QString &name)
{
auto toolContent = new LLMCore::ToolUseContent(callId, name);
toolContent->setParent(this);
m_items.append(toolContent);
m_toolCalls[callId] = toolContent;
m_pendingToolArguments[callId] = "";
}
void OpenAIResponsesMessage::handleToolCallDelta(const QString &callId, const QString &argumentsDelta)
{
if (m_pendingToolArguments.contains(callId)) {
m_pendingToolArguments[callId] += argumentsDelta;
}
}
void OpenAIResponsesMessage::handleToolCallComplete(const QString &callId)
{
if (m_pendingToolArguments.contains(callId) && m_toolCalls.contains(callId)) {
QString jsonArgs = m_pendingToolArguments[callId];
QJsonObject argsObject;
if (!jsonArgs.isEmpty()) {
QJsonDocument doc = QJsonDocument::fromJson(jsonArgs.toUtf8());
if (doc.isObject()) {
argsObject = doc.object();
}
}
m_toolCalls[callId]->setInput(argsObject);
m_pendingToolArguments.remove(callId);
}
}
void OpenAIResponsesMessage::handleReasoningStart(const QString &itemId)
{
auto thinkingContent = new LLMCore::ThinkingContent();
thinkingContent->setParent(this);
m_items.append(thinkingContent);
m_thinkingBlocks[itemId] = thinkingContent;
}
void OpenAIResponsesMessage::handleReasoningDelta(const QString &itemId, const QString &text)
{
if (m_thinkingBlocks.contains(itemId)) {
m_thinkingBlocks[itemId]->appendThinking(text);
}
}
void OpenAIResponsesMessage::handleReasoningComplete(const QString &itemId)
{
Q_UNUSED(itemId);
}
void OpenAIResponsesMessage::handleStatus(const QString &status)
{
m_status = status;
updateStateFromStatus();
}
QList<QJsonObject> OpenAIResponsesMessage::toItemsFormat() const
{
QList<QJsonObject> items;
QString textContent;
QList<LLMCore::ToolUseContent *> toolCalls;
for (const auto *block : m_items) {
if (const auto *text = qobject_cast<const LLMCore::TextContent *>(block)) {
textContent += text->text();
} else if (auto *tool = qobject_cast<LLMCore::ToolUseContent *>(
const_cast<LLMCore::ContentBlock *>(block))) {
toolCalls.append(tool);
}
}
if (!textContent.isEmpty()) {
QJsonObject message;
message["role"] = "assistant";
message["content"] = textContent;
items.append(message);
}
for (const auto *tool : toolCalls) {
QJsonObject functionCallItem;
functionCallItem["type"] = "function_call";
functionCallItem["call_id"] = tool->id();
functionCallItem["name"] = tool->name();
functionCallItem["arguments"] = QString::fromUtf8(
QJsonDocument(tool->input()).toJson(QJsonDocument::Compact));
items.append(functionCallItem);
}
return items;
}
QList<LLMCore::ToolUseContent *> OpenAIResponsesMessage::getCurrentToolUseContent() const
{
QList<LLMCore::ToolUseContent *> toolBlocks;
for (auto *block : m_items) {
if (auto *toolContent = qobject_cast<LLMCore::ToolUseContent *>(block)) {
toolBlocks.append(toolContent);
}
}
return toolBlocks;
}
QList<LLMCore::ThinkingContent *> OpenAIResponsesMessage::getCurrentThinkingContent() const
{
QList<LLMCore::ThinkingContent *> thinkingBlocks;
for (auto *block : m_items) {
if (auto *thinkingContent = qobject_cast<LLMCore::ThinkingContent *>(block)) {
thinkingBlocks.append(thinkingContent);
}
}
return thinkingBlocks;
}
QJsonArray OpenAIResponsesMessage::createToolResultItems(const QHash<QString, QString> &toolResults) const
{
QJsonArray items;
for (const auto *toolContent : getCurrentToolUseContent()) {
if (toolResults.contains(toolContent->id())) {
QJsonObject toolResultItem;
toolResultItem["type"] = "function_call_output";
toolResultItem["call_id"] = toolContent->id();
toolResultItem["output"] = toolResults[toolContent->id()];
items.append(toolResultItem);
}
}
return items;
}
QString OpenAIResponsesMessage::accumulatedText() const
{
QString text;
for (const auto *block : m_items) {
if (const auto *textContent = qobject_cast<const LLMCore::TextContent *>(block)) {
text += textContent->text();
}
}
return text;
}
void OpenAIResponsesMessage::updateStateFromStatus()
{
using namespace QodeAssist::OpenAIResponses;
if (m_status == "completed") {
if (!getCurrentToolUseContent().isEmpty()) {
m_state = LLMCore::MessageState::RequiresToolExecution;
} else {
m_state = LLMCore::MessageState::Complete;
}
} else if (m_status == "in_progress") {
m_state = LLMCore::MessageState::Building;
} else if (m_status == "failed" || m_status == "cancelled" || m_status == "incomplete") {
m_state = LLMCore::MessageState::Final;
} else {
m_state = LLMCore::MessageState::Building;
}
}
LLMCore::TextContent *OpenAIResponsesMessage::getOrCreateTextItem()
{
for (auto *block : m_items) {
if (auto *textContent = qobject_cast<LLMCore::TextContent *>(block)) {
return textContent;
}
}
auto *textContent = new LLMCore::TextContent();
textContent->setParent(this);
m_items.append(textContent);
return textContent;
}
void OpenAIResponsesMessage::startNewContinuation()
{
m_toolCalls.clear();
m_thinkingBlocks.clear();
qDeleteAll(m_items);
m_items.clear();
m_pendingToolArguments.clear();
m_status.clear();
m_state = LLMCore::MessageState::Building;
}
} // namespace QodeAssist::Providers

View File

@ -0,0 +1,67 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <llmcore/ContentBlocks.hpp>
namespace QodeAssist::Providers {
class OpenAIResponsesMessage : public QObject
{
Q_OBJECT
public:
explicit OpenAIResponsesMessage(QObject *parent = nullptr);
void handleItemDelta(const QJsonObject &item);
void handleToolCallStart(const QString &callId, const QString &name);
void handleToolCallDelta(const QString &callId, const QString &argumentsDelta);
void handleToolCallComplete(const QString &callId);
void handleReasoningStart(const QString &itemId);
void handleReasoningDelta(const QString &itemId, const QString &text);
void handleReasoningComplete(const QString &itemId);
void handleStatus(const QString &status);
QList<QJsonObject> toItemsFormat() const;
QJsonArray createToolResultItems(const QHash<QString, QString> &toolResults) const;
LLMCore::MessageState state() const noexcept { return m_state; }
QString accumulatedText() const;
QList<LLMCore::ToolUseContent *> getCurrentToolUseContent() const;
QList<LLMCore::ThinkingContent *> getCurrentThinkingContent() const;
bool hasToolCalls() const noexcept { return !m_toolCalls.isEmpty(); }
bool hasThinkingContent() const noexcept { return !m_thinkingBlocks.isEmpty(); }
void startNewContinuation();
private:
QString m_status;
LLMCore::MessageState m_state = LLMCore::MessageState::Building;
QList<LLMCore::ContentBlock *> m_items;
QHash<QString, QString> m_pendingToolArguments;
QHash<QString, LLMCore::ToolUseContent *> m_toolCalls;
QHash<QString, LLMCore::ThinkingContent *> m_thinkingBlocks;
void updateStateFromStatus();
LLMCore::TextContent *getOrCreateTextItem();
};
} // namespace QodeAssist::Providers

View File

@ -0,0 +1,666 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "OpenAIResponsesProvider.hpp"
#include "OpenAIResponses/ResponseObject.hpp"
#include "llmcore/ValidationUtils.hpp"
#include "logger/Logger.hpp"
#include "settings/ChatAssistantSettings.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include <QEventLoop>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkReply>
namespace QodeAssist::Providers {
OpenAIResponsesProvider::OpenAIResponsesProvider(QObject *parent)
: LLMCore::Provider(parent)
, m_toolsManager(new Tools::ToolsManager(this))
{
connect(
m_toolsManager,
&Tools::ToolsManager::toolExecutionComplete,
this,
&OpenAIResponsesProvider::onToolExecutionComplete);
}
QString OpenAIResponsesProvider::name() const
{
return "OpenAI Responses";
}
QString OpenAIResponsesProvider::url() const
{
return "https://api.openai.com";
}
QString OpenAIResponsesProvider::completionEndpoint() const
{
return "/v1/responses";
}
QString OpenAIResponsesProvider::chatEndpoint() const
{
return "/v1/responses";
}
bool OpenAIResponsesProvider::supportsModelListing() const
{
return true;
}
void OpenAIResponsesProvider::prepareRequest(
QJsonObject &request,
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled)
{
if (!prompt->isSupportProvider(providerID())) {
LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name()));
}
prompt->prepareRequest(request, context);
auto applyModelParams = [&request](const auto &settings) {
request["max_output_tokens"] = settings.maxTokens();
if (settings.useTopP()) {
request["top_p"] = settings.topP();
}
};
auto applyThinkingMode = [&request](const auto &settings) {
QString effortStr = settings.openAIResponsesReasoningEffort.stringValue().toLower();
if (effortStr.isEmpty()) {
effortStr = "medium";
}
QJsonObject reasoning;
reasoning["effort"] = effortStr;
request["reasoning"] = reasoning;
request["max_output_tokens"] = settings.thinkingMaxTokens();
request["store"] = true;
QJsonArray include;
include.append("reasoning.encrypted_content");
request["include"] = include;
};
if (type == LLMCore::RequestType::CodeCompletion) {
applyModelParams(Settings::codeCompletionSettings());
} else if (type == LLMCore::RequestType::QuickRefactoring) {
const auto &qrSettings = Settings::quickRefactorSettings();
applyModelParams(qrSettings);
if (isThinkingEnabled) {
applyThinkingMode(qrSettings);
}
} else {
const auto &chatSettings = Settings::chatAssistantSettings();
applyModelParams(chatSettings);
if (isThinkingEnabled) {
applyThinkingMode(chatSettings);
}
}
if (isToolsEnabled) {
const LLMCore::RunToolsFilter filter = (type == LLMCore::RequestType::QuickRefactoring)
? LLMCore::RunToolsFilter::OnlyRead
: LLMCore::RunToolsFilter::ALL;
const auto toolsDefinitions
= m_toolsManager->getToolsDefinitions(LLMCore::ToolSchemaFormat::OpenAI, filter);
if (!toolsDefinitions.isEmpty()) {
QJsonArray responsesTools;
for (const QJsonValue &toolValue : toolsDefinitions) {
const QJsonObject tool = toolValue.toObject();
if (tool.contains("function")) {
const QJsonObject functionObj = tool["function"].toObject();
QJsonObject responsesTool;
responsesTool["type"] = "function";
responsesTool["name"] = functionObj["name"];
responsesTool["description"] = functionObj["description"];
responsesTool["parameters"] = functionObj["parameters"];
responsesTools.append(responsesTool);
}
}
request["tools"] = responsesTools;
}
}
request["stream"] = true;
}
QList<QString> OpenAIResponsesProvider::getInstalledModels(const QString &url)
{
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1/v1/models").arg(url));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) {
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
}
QNetworkReply *reply = manager.get(request);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
if (reply->error() == QNetworkReply::NoError) {
const QByteArray responseData = reply->readAll();
const QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
const QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("data")) {
const QJsonArray modelArray = jsonObject["data"].toArray();
models.reserve(modelArray.size());
static const QStringList modelPrefixes = {"gpt-5", "o1", "o2", "o3", "o4"};
for (const QJsonValue &value : modelArray) {
const QJsonObject modelObject = value.toObject();
if (!modelObject.contains("id")) {
continue;
}
const QString modelId = modelObject["id"].toString();
for (const QString &prefix : modelPrefixes) {
if (modelId.contains(prefix)) {
models.append(modelId);
break;
}
}
}
}
} else {
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(reply->errorString()));
}
reply->deleteLater();
return models;
}
QList<QString> OpenAIResponsesProvider::validateRequest(
const QJsonObject &request, LLMCore::TemplateType type)
{
Q_UNUSED(type);
QList<QString> errors;
if (!request.contains("input")) {
errors.append("Missing required field: input");
return errors;
}
const QJsonValue inputValue = request["input"];
if (!inputValue.isString() && !inputValue.isArray()) {
errors.append("Field 'input' must be either a string or an array");
}
if (request.contains("max_output_tokens") && !request["max_output_tokens"].isDouble()) {
errors.append("Field 'max_output_tokens' must be a number");
}
if (request.contains("top_p") && !request["top_p"].isDouble()) {
errors.append("Field 'top_p' must be a number");
}
if (request.contains("reasoning") && !request["reasoning"].isObject()) {
errors.append("Field 'reasoning' must be an object");
}
if (request.contains("stream") && !request["stream"].isBool()) {
errors.append("Field 'stream' must be a boolean");
}
if (request.contains("tools") && !request["tools"].isArray()) {
errors.append("Field 'tools' must be an array");
}
return errors;
}
QString OpenAIResponsesProvider::apiKey() const
{
return Settings::providerSettings().openAiApiKey();
}
void OpenAIResponsesProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const
{
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) {
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
}
}
LLMCore::ProviderID OpenAIResponsesProvider::providerID() const
{
return LLMCore::ProviderID::OpenAIResponses;
}
void OpenAIResponsesProvider::sendRequest(
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload)
{
if (!m_messages.contains(requestId)) {
m_dataBuffers[requestId].clear();
}
m_requestUrls[requestId] = url;
m_originalRequests[requestId] = payload;
QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest);
LLMCore::HttpRequest
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
emit httpClient()->sendRequest(request);
}
bool OpenAIResponsesProvider::supportsTools() const
{
return true;
}
bool OpenAIResponsesProvider::supportImage() const
{
return true;
}
bool OpenAIResponsesProvider::supportThinking() const
{
return true;
}
void OpenAIResponsesProvider::cancelRequest(const LLMCore::RequestID &requestId)
{
LLMCore::Provider::cancelRequest(requestId);
cleanupRequest(requestId);
}
void OpenAIResponsesProvider::onDataReceived(
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
{
LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
const QStringList lines = buffers.rawStreamBuffer.processData(data);
QString currentEventType;
for (const QString &line : lines) {
const QString trimmedLine = line.trimmed();
if (trimmedLine.isEmpty()) {
continue;
}
if (line == "data: [DONE]") {
continue;
}
if (line.startsWith("event: ")) {
currentEventType = line.mid(7).trimmed();
continue;
}
QString dataLine = line;
if (line.startsWith("data: ")) {
dataLine = line.mid(6);
}
const QJsonDocument doc = QJsonDocument::fromJson(dataLine.toUtf8());
if (doc.isObject()) {
const QJsonObject obj = doc.object();
processStreamEvent(requestId, currentEventType, obj);
}
}
}
void OpenAIResponsesProvider::onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
{
if (!success) {
LOG_MESSAGE(QString("OpenAIResponses request %1 failed: %2").arg(requestId, error));
emit requestFailed(requestId, error);
cleanupRequest(requestId);
return;
}
if (m_messages.contains(requestId)) {
OpenAIResponsesMessage *message = m_messages[requestId];
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
return;
}
}
if (m_dataBuffers.contains(requestId)) {
const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
if (!buffers.responseContent.isEmpty()) {
emit fullResponseReceived(requestId, buffers.responseContent);
} else {
LOG_MESSAGE(QString("WARNING: OpenAIResponses - Response content is empty for %1, "
"emitting empty response")
.arg(requestId));
emit fullResponseReceived(requestId, "");
}
} else {
LOG_MESSAGE(
QString("WARNING: OpenAIResponses - No data buffer found for %1").arg(requestId));
}
cleanupRequest(requestId);
}
void OpenAIResponsesProvider::processStreamEvent(
const QString &requestId, const QString &eventType, const QJsonObject &data)
{
OpenAIResponsesMessage *message = m_messages.value(requestId);
if (!message) {
message = new OpenAIResponsesMessage(this);
m_messages[requestId] = message;
if (m_dataBuffers.contains(requestId)) {
emit continuationStarted(requestId);
}
} else if (
m_dataBuffers.contains(requestId)
&& message->state() == LLMCore::MessageState::RequiresToolExecution) {
message->startNewContinuation();
emit continuationStarted(requestId);
}
if (eventType == "response.content_part.added") {
} else if (eventType == "response.output_text.delta") {
const QString delta = data["delta"].toString();
if (!delta.isEmpty()) {
m_dataBuffers[requestId].responseContent += delta;
emit partialResponseReceived(requestId, delta);
}
} else if (eventType == "response.output_text.done") {
const QString fullText = data["text"].toString();
if (!fullText.isEmpty()) {
m_dataBuffers[requestId].responseContent = fullText;
}
} else if (eventType == "response.content_part.done") {
} else if (eventType == "response.output_item.added") {
using namespace QodeAssist::OpenAIResponses;
const QJsonObject item = data["item"].toObject();
OutputItem outputItem = OutputItem::fromJson(item);
if (const auto *functionCall = outputItem.asFunctionCall()) {
if (!functionCall->callId.isEmpty() && !functionCall->name.isEmpty()) {
if (!m_itemIdToCallId.contains(requestId)) {
m_itemIdToCallId[requestId] = QHash<QString, QString>();
}
m_itemIdToCallId[requestId][functionCall->id] = functionCall->callId;
message->handleToolCallStart(functionCall->callId, functionCall->name);
}
} else if (const auto *reasoning = outputItem.asReasoning()) {
if (!reasoning->id.isEmpty()) {
message->handleReasoningStart(reasoning->id);
}
}
} else if (eventType == "response.reasoning_content.delta") {
const QString itemId = data["item_id"].toString();
const QString delta = data["delta"].toString();
if (!itemId.isEmpty() && !delta.isEmpty()) {
message->handleReasoningDelta(itemId, delta);
}
} else if (eventType == "response.reasoning_content.done") {
const QString itemId = data["item_id"].toString();
if (!itemId.isEmpty()) {
message->handleReasoningComplete(itemId);
emitPendingThinkingBlocks(requestId);
}
} else if (eventType == "response.function_call_arguments.delta") {
const QString itemId = data["item_id"].toString();
const QString delta = data["delta"].toString();
if (!itemId.isEmpty() && !delta.isEmpty()) {
const QString callId = m_itemIdToCallId.value(requestId).value(itemId);
if (!callId.isEmpty()) {
message->handleToolCallDelta(callId, delta);
} else {
LOG_MESSAGE(QString("ERROR: No call_id mapping found for item_id: %1").arg(itemId));
}
}
} else if (
eventType == "response.function_call_arguments.done"
|| eventType == "response.output_item.done") {
const QString itemId = data["item_id"].toString();
const QJsonObject item = data["item"].toObject();
if (!item.isEmpty() && item["type"].toString() == "reasoning") {
using namespace QodeAssist::OpenAIResponses;
const QString finalItemId = itemId.isEmpty() ? item["id"].toString() : itemId;
ReasoningOutput reasoningOutput = ReasoningOutput::fromJson(item);
QString reasoningText;
if (!reasoningOutput.summaryText.isEmpty()) {
reasoningText = reasoningOutput.summaryText;
} else if (!reasoningOutput.contentTexts.isEmpty()) {
reasoningText = reasoningOutput.contentTexts.join("\n");
}
if (reasoningText.isEmpty()) {
reasoningText = QString(
"[Reasoning process completed, but detailed thinking is not available in "
"streaming mode. The model has processed your request with extended reasoning.]");
}
if (!finalItemId.isEmpty()) {
message->handleReasoningDelta(finalItemId, reasoningText);
message->handleReasoningComplete(finalItemId);
emitPendingThinkingBlocks(requestId);
}
} else if (item.isEmpty() && !itemId.isEmpty()) {
const QString callId = m_itemIdToCallId.value(requestId).value(itemId);
if (!callId.isEmpty()) {
message->handleToolCallComplete(callId);
} else {
LOG_MESSAGE(
QString("ERROR: OpenAIResponses - No call_id mapping found for item_id: %1")
.arg(itemId));
}
} else if (!item.isEmpty() && item["type"].toString() == "function_call") {
const QString callId = item["call_id"].toString();
if (!callId.isEmpty()) {
message->handleToolCallComplete(callId);
} else {
LOG_MESSAGE(
QString("ERROR: OpenAIResponses - Function call done but call_id is empty"));
}
}
} else if (eventType == "response.created") {
} else if (eventType == "response.in_progress") {
} else if (eventType == "response.completed") {
using namespace QodeAssist::OpenAIResponses;
const QJsonObject responseObj = data["response"].toObject();
Response response = Response::fromJson(responseObj);
const QString statusStr = responseObj["status"].toString();
if (m_dataBuffers[requestId].responseContent.isEmpty()) {
const QString aggregatedText = response.getAggregatedText();
if (!aggregatedText.isEmpty()) {
m_dataBuffers[requestId].responseContent = aggregatedText;
}
}
message->handleStatus(statusStr);
handleMessageComplete(requestId);
} else if (eventType == "response.incomplete") {
using namespace QodeAssist::OpenAIResponses;
const QJsonObject responseObj = data["response"].toObject();
if (!responseObj.isEmpty()) {
Response response = Response::fromJson(responseObj);
const QString statusStr = responseObj["status"].toString();
if (m_dataBuffers[requestId].responseContent.isEmpty()) {
const QString aggregatedText = response.getAggregatedText();
if (!aggregatedText.isEmpty()) {
m_dataBuffers[requestId].responseContent = aggregatedText;
}
}
message->handleStatus(statusStr);
} else {
message->handleStatus("incomplete");
}
handleMessageComplete(requestId);
} else if (!eventType.isEmpty()) {
LOG_MESSAGE(QString("WARNING: OpenAIResponses - Unhandled event type '%1' for request %2\nData: %3")
.arg(eventType)
.arg(requestId)
.arg(QString::fromUtf8(QJsonDocument(data).toJson(QJsonDocument::Compact))));
}
}
void OpenAIResponsesProvider::emitPendingThinkingBlocks(const QString &requestId)
{
if (!m_messages.contains(requestId)) {
return;
}
OpenAIResponsesMessage *message = m_messages[requestId];
const auto thinkingBlocks = message->getCurrentThinkingContent();
if (thinkingBlocks.isEmpty()) {
return;
}
const int alreadyEmitted = m_emittedThinkingBlocksCount.value(requestId, 0);
const int totalBlocks = thinkingBlocks.size();
for (int i = alreadyEmitted; i < totalBlocks; ++i) {
const auto *thinkingContent = thinkingBlocks[i];
if (thinkingContent->thinking().trimmed().isEmpty()) {
continue;
}
emit thinkingBlockReceived(
requestId, thinkingContent->thinking(), thinkingContent->signature());
}
m_emittedThinkingBlocksCount[requestId] = totalBlocks;
}
void OpenAIResponsesProvider::handleMessageComplete(const QString &requestId)
{
if (!m_messages.contains(requestId)) {
return;
}
OpenAIResponsesMessage *message = m_messages[requestId];
emitPendingThinkingBlocks(requestId);
if (message->state() == LLMCore::MessageState::RequiresToolExecution) {
const auto toolUseContent = message->getCurrentToolUseContent();
if (toolUseContent.isEmpty()) {
return;
}
for (const auto *toolContent : toolUseContent) {
const auto toolStringName = m_toolsManager->toolsFactory()->getStringName(
toolContent->name());
emit toolExecutionStarted(requestId, toolContent->id(), toolStringName);
m_toolsManager->executeToolCall(
requestId, toolContent->id(), toolContent->name(), toolContent->input());
}
}
}
void OpenAIResponsesProvider::onToolExecutionComplete(
const QString &requestId, const QHash<QString, QString> &toolResults)
{
if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) {
LOG_MESSAGE(QString("ERROR: OpenAIResponses - Missing data for continuation request %1")
.arg(requestId));
cleanupRequest(requestId);
return;
}
OpenAIResponsesMessage *message = m_messages[requestId];
const auto toolContent = message->getCurrentToolUseContent();
for (auto it = toolResults.constBegin(); it != toolResults.constEnd(); ++it) {
for (const auto *tool : toolContent) {
if (tool->id() == it.key()) {
const auto toolStringName = m_toolsManager->toolsFactory()->getStringName(
tool->name());
emit toolExecutionCompleted(
requestId, tool->id(), toolStringName, toolResults[tool->id()]);
break;
}
}
}
QJsonObject continuationRequest = m_originalRequests[requestId];
QJsonArray input = continuationRequest["input"].toArray();
const QList<QJsonObject> assistantItems = message->toItemsFormat();
for (const QJsonObject &item : assistantItems) {
input.append(item);
}
const QJsonArray toolResultItems = message->createToolResultItems(toolResults);
for (const QJsonValue &item : toolResultItems) {
input.append(item);
}
continuationRequest["input"] = input;
m_dataBuffers[requestId].responseContent.clear();
sendRequest(requestId, m_requestUrls[requestId], continuationRequest);
}
void OpenAIResponsesProvider::cleanupRequest(const LLMCore::RequestID &requestId)
{
if (m_messages.contains(requestId)) {
OpenAIResponsesMessage *message = m_messages.take(requestId);
message->deleteLater();
}
m_dataBuffers.remove(requestId);
m_requestUrls.remove(requestId);
m_originalRequests.remove(requestId);
m_itemIdToCallId.remove(requestId);
m_emittedThinkingBlocksCount.remove(requestId);
m_toolsManager->cleanupRequest(requestId);
}
} // namespace QodeAssist::Providers

View File

@ -0,0 +1,87 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "OpenAIResponsesMessage.hpp"
#include "tools/ToolsManager.hpp"
#include <llmcore/Provider.hpp>
namespace QodeAssist::Providers {
class OpenAIResponsesProvider : public LLMCore::Provider
{
Q_OBJECT
public:
explicit OpenAIResponsesProvider(QObject *parent = nullptr);
QString name() const override;
QString url() const override;
QString completionEndpoint() const override;
QString chatEndpoint() const override;
bool supportsModelListing() const override;
void prepareRequest(
QJsonObject &request,
LLMCore::PromptTemplate *prompt,
LLMCore::ContextData context,
LLMCore::RequestType type,
bool isToolsEnabled,
bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
LLMCore::ProviderID providerID() const override;
void sendRequest(
const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override;
bool supportsTools() const override;
bool supportImage() const override;
bool supportThinking() const override;
void cancelRequest(const LLMCore::RequestID &requestId) override;
public slots:
void onDataReceived(
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
void onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId,
bool success,
const QString &error) override;
private slots:
void onToolExecutionComplete(
const QString &requestId, const QHash<QString, QString> &toolResults);
private:
void processStreamEvent(const QString &requestId, const QString &eventType, const QJsonObject &data);
void emitPendingThinkingBlocks(const QString &requestId);
void handleMessageComplete(const QString &requestId);
void cleanupRequest(const LLMCore::RequestID &requestId);
QHash<LLMCore::RequestID, OpenAIResponsesMessage *> m_messages;
QHash<LLMCore::RequestID, QUrl> m_requestUrls;
QHash<LLMCore::RequestID, QJsonObject> m_originalRequests;
QHash<LLMCore::RequestID, QHash<QString, QString>> m_itemIdToCallId;
QHash<LLMCore::RequestID, int> m_emittedThinkingBlocksCount;
Tools::ToolsManager *m_toolsManager;
};
} // namespace QodeAssist::Providers

View File

@ -0,0 +1,255 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "OpenAIResponses/ModelRequest.hpp"
#include <QJsonObject>
#include <QList>
#include <QMap>
#include <QString>
namespace QodeAssist::OpenAIResponses {
class RequestBuilder
{
public:
RequestBuilder() = default;
RequestBuilder &setModel(QString model)
{
m_model = std::move(model);
return *this;
}
RequestBuilder &addMessage(Role role, QString content)
{
Message msg;
msg.role = role;
msg.content.append(MessageContent(std::move(content)));
m_messages.append(std::move(msg));
return *this;
}
RequestBuilder &addMessage(Message msg)
{
m_messages.append(std::move(msg));
return *this;
}
RequestBuilder &setInstructions(QString instructions)
{
m_instructions = std::move(instructions);
return *this;
}
RequestBuilder &addTool(Tool tool)
{
m_tools.append(std::move(tool));
return *this;
}
RequestBuilder &setTemperature(double temp) noexcept
{
m_temperature = temp;
return *this;
}
RequestBuilder &setTopP(double topP) noexcept
{
m_topP = topP;
return *this;
}
RequestBuilder &setMaxOutputTokens(int tokens) noexcept
{
m_maxOutputTokens = tokens;
return *this;
}
RequestBuilder &setStream(bool stream) noexcept
{
m_stream = stream;
return *this;
}
RequestBuilder &setStore(bool store) noexcept
{
m_store = store;
return *this;
}
RequestBuilder &setTextFormat(TextFormatOptions format)
{
m_textFormat = std::move(format);
return *this;
}
RequestBuilder &setReasoningEffort(ReasoningEffort effort) noexcept
{
m_reasoningEffort = effort;
return *this;
}
RequestBuilder &setMetadata(QMap<QString, QVariant> metadata)
{
m_metadata = std::move(metadata);
return *this;
}
RequestBuilder &setIncludeReasoningContent(bool include) noexcept
{
m_includeReasoningContent = include;
return *this;
}
RequestBuilder &clear() noexcept
{
m_model.clear();
m_messages.clear();
m_instructions.reset();
m_tools.clear();
m_temperature.reset();
m_topP.reset();
m_maxOutputTokens.reset();
m_stream = false;
m_store.reset();
m_textFormat.reset();
m_reasoningEffort.reset();
m_includeReasoningContent = false;
m_metadata.clear();
return *this;
}
QJsonObject toJson() const
{
QJsonObject obj;
if (!m_model.isEmpty()) {
obj["model"] = m_model;
}
if (!m_messages.isEmpty()) {
if (m_messages.size() == 1 && m_messages[0].role == Role::User
&& m_messages[0].content.size() == 1) {
obj["input"] = m_messages[0].content[0].toJson();
} else {
QJsonArray input;
for (const auto &msg : m_messages) {
input.append(msg.toJson());
}
obj["input"] = input;
}
}
if (m_instructions) {
obj["instructions"] = *m_instructions;
}
if (!m_tools.isEmpty()) {
QJsonArray tools;
for (const auto &tool : m_tools) {
tools.append(tool.toJson());
}
obj["tools"] = tools;
}
if (m_temperature) {
obj["temperature"] = *m_temperature;
}
if (m_topP) {
obj["top_p"] = *m_topP;
}
if (m_maxOutputTokens) {
obj["max_output_tokens"] = *m_maxOutputTokens;
}
obj["stream"] = m_stream;
if (m_store) {
obj["store"] = *m_store;
}
if (m_textFormat) {
QJsonObject textObj;
textObj["format"] = m_textFormat->toJson();
obj["text"] = textObj;
}
if (m_reasoningEffort) {
QJsonObject reasoning;
reasoning["effort"] = effortToString(*m_reasoningEffort);
obj["reasoning"] = reasoning;
}
if (m_includeReasoningContent) {
QJsonArray include;
include.append("reasoning.encrypted_content");
obj["include"] = include;
}
if (!m_metadata.isEmpty()) {
QJsonObject metadata;
for (auto it = m_metadata.constBegin(); it != m_metadata.constEnd(); ++it) {
metadata[it.key()] = QJsonValue::fromVariant(it.value());
}
obj["metadata"] = metadata;
}
return obj;
}
private:
QString m_model;
QList<Message> m_messages;
std::optional<QString> m_instructions;
QList<Tool> m_tools;
std::optional<double> m_temperature;
std::optional<double> m_topP;
std::optional<int> m_maxOutputTokens;
bool m_stream = false;
std::optional<bool> m_store;
std::optional<TextFormatOptions> m_textFormat;
std::optional<ReasoningEffort> m_reasoningEffort;
bool m_includeReasoningContent = false;
QMap<QString, QVariant> m_metadata;
static QString effortToString(ReasoningEffort e)
{
switch (e) {
case ReasoningEffort::None:
return "none";
case ReasoningEffort::Minimal:
return "minimal";
case ReasoningEffort::Low:
return "low";
case ReasoningEffort::Medium:
return "medium";
case ReasoningEffort::High:
return "high";
}
return "medium";
}
};
} // namespace QodeAssist::OpenAIResponses

View File

@ -29,6 +29,7 @@
#include "providers/OllamaProvider.hpp"
#include "providers/OpenAICompatProvider.hpp"
#include "providers/OpenAIProvider.hpp"
#include "providers/OpenAIResponsesProvider.hpp"
#include "providers/OpenRouterAIProvider.hpp"
namespace QodeAssist::Providers {
@ -39,6 +40,7 @@ inline void registerProviders()
providerManager.registerProvider<OllamaProvider>();
providerManager.registerProvider<ClaudeProvider>();
providerManager.registerProvider<OpenAIProvider>();
providerManager.registerProvider<OpenAIResponsesProvider>();
providerManager.registerProvider<OpenAICompatProvider>();
providerManager.registerProvider<LMStudioProvider>();
providerManager.registerProvider<OpenRouterProvider>();