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