diff --git a/CMakeLists.txt b/CMakeLists.txt index 3fab07f..bb879fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,8 +81,6 @@ add_qtc_plugin(QodeAssist templates/OpenAI.hpp templates/MistralAI.hpp templates/StarCoder2Fim.hpp - # templates/DeepSeekCoderFim.hpp - # templates/CustomFimTemplate.hpp templates/Qwen25CoderFIM.hpp templates/OpenAICompatible.hpp templates/Llama3.hpp @@ -107,15 +105,6 @@ add_qtc_plugin(QodeAssist providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp providers/CodestralProvider.hpp providers/CodestralProvider.cpp - providers/OpenAIResponses/ModelRequest.hpp - providers/OpenAIResponses/ResponseObject.hpp - providers/OpenAIResponses/GetResponseRequest.hpp - providers/OpenAIResponses/DeleteResponseRequest.hpp - providers/OpenAIResponses/CancelResponseRequest.hpp - providers/OpenAIResponses/ListInputItemsRequest.hpp - providers/OpenAIResponses/InputTokensRequest.hpp - providers/OpenAIResponses/ItemTypesReference.hpp - providers/OpenAIResponsesRequestBuilder.hpp providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp QodeAssist.qrc LSPCompletion.hpp diff --git a/logger/CMakeLists.txt b/logger/CMakeLists.txt index e23e273..615468f 100644 --- a/logger/CMakeLists.txt +++ b/logger/CMakeLists.txt @@ -1,5 +1,4 @@ add_library(QodeAssistLogger STATIC - EmptyRequestPerformanceLogger.hpp IRequestPerformanceLogger.hpp Logger.cpp Logger.hpp diff --git a/logger/EmptyRequestPerformanceLogger.hpp b/logger/EmptyRequestPerformanceLogger.hpp deleted file mode 100644 index 6237b65..0000000 --- a/logger/EmptyRequestPerformanceLogger.hpp +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (C) 2025 Povilas Kanapickas -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "IRequestPerformanceLogger.hpp" - -namespace QodeAssist { - -class EmptyRequestPerformanceLogger : public IRequestPerformanceLogger -{ -public: - void startTimeMeasurement(const QString &requestId) override {} - void endTimeMeasurement(const QString &requestId) override {} - void logPerformance(const QString &requestId, qint64 elapsedMs) override {} -}; - -} // namespace QodeAssist diff --git a/pluginllmcore/CMakeLists.txt b/pluginllmcore/CMakeLists.txt index 58e0c35..55a85d8 100644 --- a/pluginllmcore/CMakeLists.txt +++ b/pluginllmcore/CMakeLists.txt @@ -10,10 +10,6 @@ add_library(PluginLLMCore STATIC PromptTemplate.hpp PromptTemplateManager.hpp PromptTemplateManager.cpp ProviderID.hpp - HttpClient.hpp HttpClient.cpp - DataBuffers.hpp - SSEBuffer.hpp SSEBuffer.cpp - ContentBlocks.hpp RulesLoader.hpp RulesLoader.cpp ResponseCleaner.hpp ) diff --git a/pluginllmcore/ContentBlocks.hpp b/pluginllmcore/ContentBlocks.hpp deleted file mode 100644 index 1345862..0000000 --- a/pluginllmcore/ContentBlocks.hpp +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::PluginLLMCore { - -enum class MessageState { Building, Complete, RequiresToolExecution, Final }; - -enum class ProviderFormat { Claude, OpenAI }; - -class ContentBlock : public QObject -{ - Q_OBJECT -public: - explicit ContentBlock(QObject *parent = nullptr) - : QObject(parent) - {} - virtual ~ContentBlock() = default; - virtual QString type() const = 0; - virtual QJsonValue toJson(ProviderFormat format) const = 0; -}; - -class TextContent : public ContentBlock -{ - Q_OBJECT -public: - explicit TextContent(const QString &text = QString()) - : ContentBlock() - , m_text(text) - {} - - QString type() const override { return "text"; } - QString text() const { return m_text; } - void appendText(const QString &text) { m_text += text; } - void setText(const QString &text) { m_text = text; } - - QJsonValue toJson(ProviderFormat format) const override - { - Q_UNUSED(format); - return QJsonObject{{"type", "text"}, {"text", m_text}}; - } - -private: - QString m_text; -}; - -class ImageContent : public ContentBlock -{ - Q_OBJECT -public: - enum class ImageSourceType { Base64, Url }; - - ImageContent(const QString &data, const QString &mediaType, ImageSourceType sourceType = ImageSourceType::Base64) - : ContentBlock() - , m_data(data) - , m_mediaType(mediaType) - , m_sourceType(sourceType) - {} - - QString type() const override { return "image"; } - QString data() const { return m_data; } - QString mediaType() const { return m_mediaType; } - ImageSourceType sourceType() const { return m_sourceType; } - - QJsonValue toJson(ProviderFormat format) const override - { - if (format == ProviderFormat::Claude) { - QJsonObject source; - if (m_sourceType == ImageSourceType::Base64) { - source["type"] = "base64"; - source["media_type"] = m_mediaType; - source["data"] = m_data; - } else { - source["type"] = "url"; - source["url"] = m_data; - } - return QJsonObject{{"type", "image"}, {"source", source}}; - } else { // OpenAI format - QJsonObject imageUrl; - if (m_sourceType == ImageSourceType::Base64) { - imageUrl["url"] = QString("data:%1;base64,%2").arg(m_mediaType, m_data); - } else { - imageUrl["url"] = m_data; - } - return QJsonObject{{"type", "image_url"}, {"image_url", imageUrl}}; - } - } - -private: - QString m_data; - QString m_mediaType; - ImageSourceType m_sourceType; -}; - -class ToolUseContent : public ContentBlock -{ - Q_OBJECT -public: - ToolUseContent(const QString &id, const QString &name, const QJsonObject &input = QJsonObject()) - : ContentBlock() - , m_id(id) - , m_name(name) - , m_input(input) - {} - - QString type() const override { return "tool_use"; } - QString id() const { return m_id; } - QString name() const { return m_name; } - QJsonObject input() const { return m_input; } - void setInput(const QJsonObject &input) { m_input = input; } - - QJsonValue toJson(ProviderFormat format) const override - { - if (format == ProviderFormat::Claude) { - return QJsonObject{ - {"type", "tool_use"}, {"id", m_id}, {"name", m_name}, {"input", m_input}}; - } else { // OpenAI - QJsonDocument doc(m_input); - return QJsonObject{ - {"id", m_id}, - {"type", "function"}, - {"function", - QJsonObject{ - {"name", m_name}, - {"arguments", QString::fromUtf8(doc.toJson(QJsonDocument::Compact))}}}}; - } - } - -private: - QString m_id; - QString m_name; - QJsonObject m_input; -}; - -class ToolResultContent : public ContentBlock -{ - Q_OBJECT -public: - ToolResultContent(const QString &toolUseId, const QString &result) - : ContentBlock() - , m_toolUseId(toolUseId) - , m_result(result) - {} - - QString type() const override { return "tool_result"; } - QString toolUseId() const { return m_toolUseId; } - QString result() const { return m_result; } - - QJsonValue toJson(ProviderFormat format) const override - { - if (format == ProviderFormat::Claude) { - return QJsonObject{ - {"type", "tool_result"}, {"tool_use_id", m_toolUseId}, {"content", m_result}}; - } else { // OpenAI - return QJsonObject{{"role", "tool"}, {"tool_call_id", m_toolUseId}, {"content", m_result}}; - } - } - -private: - QString m_toolUseId; - QString m_result; -}; - -class ThinkingContent : public ContentBlock -{ - Q_OBJECT -public: - explicit ThinkingContent(const QString &thinking = QString(), const QString &signature = QString()) - : ContentBlock() - , m_thinking(thinking) - , m_signature(signature) - {} - - QString type() const override { return "thinking"; } - QString thinking() const { return m_thinking; } - QString signature() const { return m_signature; } - void appendThinking(const QString &text) { m_thinking += text; } - void setThinking(const QString &text) { m_thinking = text; } - void setSignature(const QString &signature) { m_signature = signature; } - - QJsonValue toJson(ProviderFormat format) const override - { - Q_UNUSED(format); - // Only include signature field if it's not empty - // Empty signature is rejected by API with "Invalid signature" error - // In streaming mode, signature is not provided, so we omit the field entirely - QJsonObject obj{{"type", "thinking"}, {"thinking", m_thinking}}; - if (!m_signature.isEmpty()) { - obj["signature"] = m_signature; - } - return obj; - } - -private: - QString m_thinking; - QString m_signature; -}; - -class RedactedThinkingContent : public ContentBlock -{ - Q_OBJECT -public: - explicit RedactedThinkingContent(const QString &signature = QString()) - : ContentBlock() - , m_signature(signature) - {} - - QString type() const override { return "redacted_thinking"; } - QString signature() const { return m_signature; } - void setSignature(const QString &signature) { m_signature = signature; } - - QJsonValue toJson(ProviderFormat format) const override - { - Q_UNUSED(format); - // Only include signature field if it's not empty - // Empty signature is rejected by API with "Invalid signature" error - QJsonObject obj{{"type", "redacted_thinking"}}; - if (!m_signature.isEmpty()) { - obj["signature"] = m_signature; - } - return obj; - } - -private: - QString m_signature; -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/DataBuffers.hpp b/pluginllmcore/DataBuffers.hpp deleted file mode 100644 index f8be9fb..0000000 --- a/pluginllmcore/DataBuffers.hpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include "SSEBuffer.hpp" -#include - -namespace QodeAssist::PluginLLMCore { - -struct DataBuffers -{ - SSEBuffer rawStreamBuffer; - QString responseContent; - - void clear() - { - rawStreamBuffer.clear(); - responseContent.clear(); - } -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/HttpClient.cpp b/pluginllmcore/HttpClient.cpp deleted file mode 100644 index 02d86d6..0000000 --- a/pluginllmcore/HttpClient.cpp +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "HttpClient.hpp" - -#include -#include - -#include - -namespace QodeAssist::PluginLLMCore { - -HttpClient::HttpClient(QObject *parent) - : QObject(parent) - , m_manager(new QNetworkAccessManager(this)) -{} - -HttpClient::~HttpClient() -{ - QMutexLocker locker(&m_mutex); - for (auto *reply : std::as_const(m_activeRequests)) { - reply->abort(); - reply->deleteLater(); - } - m_activeRequests.clear(); -} - -QFuture HttpClient::get(const QNetworkRequest &request) -{ - LOG_MESSAGE(QString("HttpClient: GET %1").arg(request.url().toString())); - - auto promise = std::make_shared>(); - promise->start(); - - QNetworkReply *reply = m_manager->get(request); - setupNonStreamingReply(reply, promise); - - return promise->future(); -} - -QFuture HttpClient::post(const QNetworkRequest &request, const QJsonObject &payload) -{ - QJsonDocument doc(payload); - LOG_MESSAGE(QString("HttpClient: POST %1, data: %2") - .arg(request.url().toString(), doc.toJson(QJsonDocument::Indented))); - - auto promise = std::make_shared>(); - promise->start(); - - QNetworkReply *reply = m_manager->post(request, doc.toJson(QJsonDocument::Compact)); - setupNonStreamingReply(reply, promise); - - return promise->future(); -} - -QFuture HttpClient::del(const QNetworkRequest &request, - std::optional payload) -{ - auto promise = std::make_shared>(); - promise->start(); - - QNetworkReply *reply; - if (payload) { - QJsonDocument doc(*payload); - LOG_MESSAGE(QString("HttpClient: DELETE %1, data: %2") - .arg(request.url().toString(), doc.toJson(QJsonDocument::Indented))); - reply = m_manager->sendCustomRequest(request, "DELETE", doc.toJson(QJsonDocument::Compact)); - } else { - LOG_MESSAGE(QString("HttpClient: DELETE %1").arg(request.url().toString())); - reply = m_manager->deleteResource(request); - } - - setupNonStreamingReply(reply, promise); - - return promise->future(); -} - -void HttpClient::setupNonStreamingReply(QNetworkReply *reply, - std::shared_ptr> promise) -{ - connect(reply, &QNetworkReply::finished, this, [this, reply, promise]() { - int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - QByteArray responseBody = reply->readAll(); - QNetworkReply::NetworkError networkError = reply->error(); - QString networkErrorString = reply->errorString(); - - reply->disconnect(); - reply->deleteLater(); - - LOG_MESSAGE( - QString("HttpClient: Non-streaming request - HTTP Status: %1").arg(statusCode)); - - bool hasError = (networkError != QNetworkReply::NoError) || (statusCode >= 400); - if (hasError) { - QString errorMsg = parseErrorFromResponse(statusCode, responseBody, networkErrorString); - LOG_MESSAGE(QString("HttpClient: Non-streaming request - Error: %1").arg(errorMsg)); - promise->setException( - std::make_exception_ptr(std::runtime_error(errorMsg.toStdString()))); - } else { - promise->addResult(responseBody); - } - promise->finish(); - }); -} - -void HttpClient::postStreaming(const QString &requestId, const QNetworkRequest &request, - const QJsonObject &payload) -{ - QJsonDocument doc(payload); - LOG_MESSAGE(QString("HttpClient: POST streaming %1, data: %2") - .arg(request.url().toString(), doc.toJson(QJsonDocument::Indented))); - - QNetworkReply *reply = m_manager->post(request, doc.toJson(QJsonDocument::Compact)); - addActiveRequest(reply, requestId); - - connect(reply, &QNetworkReply::readyRead, this, &HttpClient::onReadyRead); - connect(reply, &QNetworkReply::finished, this, &HttpClient::onStreamingFinished); -} - -void HttpClient::cancelRequest(const QString &requestId) -{ - QMutexLocker locker(&m_mutex); - auto it = m_activeRequests.find(requestId); - if (it != m_activeRequests.end()) { - QNetworkReply *reply = it.value(); - if (reply) { - reply->disconnect(); - reply->abort(); - reply->deleteLater(); - } - m_activeRequests.erase(it); - LOG_MESSAGE(QString("HttpClient: Cancelled request: %1").arg(requestId)); - } -} - -void HttpClient::onReadyRead() -{ - QNetworkReply *reply = qobject_cast(sender()); - if (!reply || reply->isFinished()) - return; - - int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (statusCode >= 400) - return; - - QString requestId = findRequestId(reply); - if (requestId.isEmpty()) - return; - - QByteArray data = reply->readAll(); - if (!data.isEmpty()) { - emit dataReceived(requestId, data); - } -} - -void HttpClient::onStreamingFinished() -{ - QNetworkReply *reply = qobject_cast(sender()); - if (!reply) - return; - - int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - QByteArray responseBody = reply->readAll(); - QNetworkReply::NetworkError networkError = reply->error(); - QString networkErrorString = reply->errorString(); - - reply->disconnect(); - - QString requestId; - std::optional error; - - { - QMutexLocker locker(&m_mutex); - for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) { - if (it.value() == reply) { - requestId = it.key(); - m_activeRequests.erase(it); - break; - } - } - - if (requestId.isEmpty()) { - reply->deleteLater(); - return; - } - - bool hasError = (networkError != QNetworkReply::NoError) || (statusCode >= 400); - if (hasError) { - error = parseErrorFromResponse(statusCode, responseBody, networkErrorString); - } - - LOG_MESSAGE( - QString("HttpClient: Request %1 - HTTP Status: %2").arg(requestId).arg(statusCode)); - - if (!responseBody.isEmpty()) { - LOG_MESSAGE(QString("HttpClient: Request %1 - Response body (%2 bytes): %3") - .arg(requestId) - .arg(responseBody.size()) - .arg(QString::fromUtf8(responseBody))); - } - - if (error) { - LOG_MESSAGE(QString("HttpClient: Request %1 - Error: %2").arg(requestId, *error)); - } - } - - reply->deleteLater(); - - if (!requestId.isEmpty()) { - emit requestFinished(requestId, error); - } -} - -QString HttpClient::findRequestId(QNetworkReply *reply) -{ - QMutexLocker locker(&m_mutex); - for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) { - if (it.value() == reply) - return it.key(); - } - return {}; -} - -void HttpClient::addActiveRequest(QNetworkReply *reply, const QString &requestId) -{ - QMutexLocker locker(&m_mutex); - m_activeRequests[requestId] = reply; - LOG_MESSAGE(QString("HttpClient: Added active request: %1").arg(requestId)); -} - -QString HttpClient::parseErrorFromResponse( - int statusCode, const QByteArray &responseBody, const QString &networkErrorString) -{ - if (!responseBody.isEmpty()) { - QJsonDocument errorDoc = QJsonDocument::fromJson(responseBody); - if (!errorDoc.isNull() && errorDoc.isObject()) { - QJsonObject errorObj = errorDoc.object(); - if (errorObj.contains("error")) { - QJsonObject error = errorObj["error"].toObject(); - QString message = error["message"].toString(); - QString type = error["type"].toString(); - QString code = error["code"].toString(); - - QString errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(message); - if (!type.isEmpty()) - errorMsg += QString(" (type: %1)").arg(type); - if (!code.isEmpty()) - errorMsg += QString(" (code: %1)").arg(code); - return errorMsg; - } - return QString("HTTP %1: %2") - .arg(statusCode) - .arg(QString::fromUtf8(responseBody)); - } - return QString("HTTP %1: %2").arg(statusCode).arg(QString::fromUtf8(responseBody)); - } - return QString("HTTP %1: %2").arg(statusCode).arg(networkErrorString); -} - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/HttpClient.hpp b/pluginllmcore/HttpClient.hpp deleted file mode 100644 index 068c61e..0000000 --- a/pluginllmcore/HttpClient.hpp +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace QodeAssist::PluginLLMCore { - -class HttpClient : public QObject -{ - Q_OBJECT - -public: - HttpClient(QObject *parent = nullptr); - ~HttpClient(); - - // Non-streaming — return QFuture with full response - QFuture get(const QNetworkRequest &request); - QFuture post(const QNetworkRequest &request, const QJsonObject &payload); - QFuture del(const QNetworkRequest &request, - std::optional payload = std::nullopt); - - // Streaming — signal-based with requestId - void postStreaming(const QString &requestId, const QNetworkRequest &request, - const QJsonObject &payload); - - void cancelRequest(const QString &requestId); - -signals: - void dataReceived(const QString &requestId, const QByteArray &data); - void requestFinished(const QString &requestId, std::optional error); - -private slots: - void onReadyRead(); - void onStreamingFinished(); - -private: - void setupNonStreamingReply(QNetworkReply *reply, std::shared_ptr> promise); - - QString findRequestId(QNetworkReply *reply); - void addActiveRequest(QNetworkReply *reply, const QString &requestId); - QString parseErrorFromResponse(int statusCode, const QByteArray &responseBody, - const QString &networkErrorString); - - QNetworkAccessManager *m_manager; - QHash m_activeRequests; - mutable QMutex m_mutex; -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/SSEBuffer.cpp b/pluginllmcore/SSEBuffer.cpp deleted file mode 100644 index 94e764a..0000000 --- a/pluginllmcore/SSEBuffer.cpp +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "SSEBuffer.hpp" - -namespace QodeAssist::PluginLLMCore { - -QStringList SSEBuffer::processData(const QByteArray &data) -{ - m_buffer += QString::fromUtf8(data); - - QStringList lines = m_buffer.split('\n'); - m_buffer = lines.takeLast(); - - lines.removeAll(QString()); - - return lines; -} - -void SSEBuffer::clear() -{ - m_buffer.clear(); -} - -QString SSEBuffer::currentBuffer() const -{ - return m_buffer; -} - -bool SSEBuffer::hasIncompleteData() const -{ - return !m_buffer.isEmpty(); -} - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/SSEBuffer.hpp b/pluginllmcore/SSEBuffer.hpp deleted file mode 100644 index 0dc6a12..0000000 --- a/pluginllmcore/SSEBuffer.hpp +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include - -namespace QodeAssist::PluginLLMCore { - -class SSEBuffer -{ -public: - SSEBuffer() = default; - - QStringList processData(const QByteArray &data); - - void clear(); - QString currentBuffer() const; - bool hasIncompleteData() const; - -private: - QString m_buffer; -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/providers/ClaudeMessage.cpp b/providers/ClaudeMessage.cpp deleted file mode 100644 index 1326f0d..0000000 --- a/providers/ClaudeMessage.cpp +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "ClaudeMessage.hpp" -#include "logger/Logger.hpp" - -#include -#include - -namespace QodeAssist { - -ClaudeMessage::ClaudeMessage(QObject *parent) - : QObject(parent) -{} - -void ClaudeMessage::handleContentBlockStart( - int index, const QString &blockType, const QJsonObject &data) -{ - LOG_MESSAGE(QString("ClaudeMessage: handleContentBlockStart index=%1, blockType=%2") - .arg(index) - .arg(blockType)); - - if (blockType == "text") { - addCurrentContent(); - - } else if (blockType == "image") { - QJsonObject source = data["source"].toObject(); - QString sourceType = source["type"].toString(); - QString imageData; - QString mediaType; - PluginLLMCore::ImageContent::ImageSourceType imgSourceType = PluginLLMCore::ImageContent::ImageSourceType::Base64; - - if (sourceType == "base64") { - imageData = source["data"].toString(); - mediaType = source["media_type"].toString(); - imgSourceType = PluginLLMCore::ImageContent::ImageSourceType::Base64; - } else if (sourceType == "url") { - imageData = source["url"].toString(); - imgSourceType = PluginLLMCore::ImageContent::ImageSourceType::Url; - } - - addCurrentContent(imageData, mediaType, imgSourceType); - - } else if (blockType == "tool_use") { - QString toolId = data["id"].toString(); - QString toolName = data["name"].toString(); - QJsonObject toolInput = data["input"].toObject(); - - addCurrentContent(toolId, toolName, toolInput); - m_pendingToolInputs[index] = ""; - - } else if (blockType == "thinking") { - QString thinking = data["thinking"].toString(); - QString signature = data["signature"].toString(); - LOG_MESSAGE(QString("ClaudeMessage: Creating thinking block with signature length=%1") - .arg(signature.length())); - addCurrentContent(thinking, signature); - - } else if (blockType == "redacted_thinking") { - QString signature = data["signature"].toString(); - LOG_MESSAGE(QString("ClaudeMessage: Creating redacted_thinking block with signature length=%1") - .arg(signature.length())); - addCurrentContent(signature); - } -} - -void ClaudeMessage::handleContentBlockDelta( - int index, const QString &deltaType, const QJsonObject &delta) -{ - if (index >= m_currentBlocks.size()) { - return; - } - - if (deltaType == "text_delta") { - if (auto textContent = qobject_cast(m_currentBlocks[index])) { - textContent->appendText(delta["text"].toString()); - } - - } else if (deltaType == "input_json_delta") { - QString partialJson = delta["partial_json"].toString(); - if (m_pendingToolInputs.contains(index)) { - m_pendingToolInputs[index] += partialJson; - } - - } else if (deltaType == "thinking_delta") { - if (auto thinkingContent = qobject_cast(m_currentBlocks[index])) { - thinkingContent->appendThinking(delta["thinking"].toString()); - } - - } else if (deltaType == "signature_delta") { - if (auto thinkingContent = qobject_cast(m_currentBlocks[index])) { - QString signature = delta["signature"].toString(); - thinkingContent->setSignature(signature); - LOG_MESSAGE(QString("Set signature for thinking block %1: length=%2") - .arg(index).arg(signature.length())); - } else if (auto redactedContent = qobject_cast(m_currentBlocks[index])) { - QString signature = delta["signature"].toString(); - redactedContent->setSignature(signature); - LOG_MESSAGE(QString("Set signature for redacted_thinking block %1: length=%2") - .arg(index).arg(signature.length())); - } - } -} - -void ClaudeMessage::handleContentBlockStop(int index) -{ - if (m_pendingToolInputs.contains(index)) { - QString jsonInput = m_pendingToolInputs[index]; - QJsonObject inputObject; - - if (!jsonInput.isEmpty()) { - QJsonDocument doc = QJsonDocument::fromJson(jsonInput.toUtf8()); - if (doc.isObject()) { - inputObject = doc.object(); - } - } - - if (index < m_currentBlocks.size()) { - if (auto toolContent = qobject_cast(m_currentBlocks[index])) { - toolContent->setInput(inputObject); - } - } - - m_pendingToolInputs.remove(index); - } -} - -void ClaudeMessage::handleStopReason(const QString &stopReason) -{ - m_stopReason = stopReason; - updateStateFromStopReason(); -} - -QJsonObject ClaudeMessage::toProviderFormat() const -{ - QJsonObject message; - message["role"] = "assistant"; - - QJsonArray content; - - for (auto block : m_currentBlocks) { - QJsonValue blockJson = block->toJson(PluginLLMCore::ProviderFormat::Claude); - content.append(blockJson); - } - - message["content"] = content; - - LOG_MESSAGE(QString("ClaudeMessage::toProviderFormat - message with %1 content block(s)") - .arg(m_currentBlocks.size())); - - return message; -} - -QJsonArray ClaudeMessage::createToolResultsContent(const QHash &toolResults) const -{ - QJsonArray results; - - for (auto toolContent : getCurrentToolUseContent()) { - if (toolResults.contains(toolContent->id())) { - auto toolResult = std::make_unique( - toolContent->id(), toolResults[toolContent->id()]); - results.append(toolResult->toJson(PluginLLMCore::ProviderFormat::Claude)); - } - } - - return results; -} - -QList ClaudeMessage::getCurrentToolUseContent() const -{ - QList toolBlocks; - for (auto block : m_currentBlocks) { - if (auto toolContent = qobject_cast(block)) { - toolBlocks.append(toolContent); - } - } - return toolBlocks; -} - -QList ClaudeMessage::getCurrentThinkingContent() const -{ - QList thinkingBlocks; - for (auto block : m_currentBlocks) { - if (auto thinkingContent = qobject_cast(block)) { - thinkingBlocks.append(thinkingContent); - } - } - return thinkingBlocks; -} - -QList ClaudeMessage::getCurrentRedactedThinkingContent() const -{ - QList redactedBlocks; - for (auto block : m_currentBlocks) { - if (auto redactedContent = qobject_cast(block)) { - redactedBlocks.append(redactedContent); - } - } - return redactedBlocks; -} - -void ClaudeMessage::startNewContinuation() -{ - LOG_MESSAGE(QString("ClaudeMessage: Starting new continuation")); - - m_currentBlocks.clear(); - m_pendingToolInputs.clear(); - m_stopReason.clear(); - m_state = PluginLLMCore::MessageState::Building; -} - -void ClaudeMessage::updateStateFromStopReason() -{ - if (m_stopReason == "tool_use" && !getCurrentToolUseContent().empty()) { - m_state = PluginLLMCore::MessageState::RequiresToolExecution; - } else if (m_stopReason == "end_turn") { - m_state = PluginLLMCore::MessageState::Final; - } else { - m_state = PluginLLMCore::MessageState::Complete; - } -} - -} // namespace QodeAssist diff --git a/providers/ClaudeMessage.hpp b/providers/ClaudeMessage.hpp deleted file mode 100644 index 0c04da4..0000000 --- a/providers/ClaudeMessage.hpp +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -namespace QodeAssist { - -class ClaudeMessage : public QObject -{ - Q_OBJECT -public: - explicit ClaudeMessage(QObject *parent = nullptr); - - void handleContentBlockStart(int index, const QString &blockType, const QJsonObject &data); - void handleContentBlockDelta(int index, const QString &deltaType, const QJsonObject &delta); - void handleContentBlockStop(int index); - void handleStopReason(const QString &stopReason); - - QJsonObject toProviderFormat() const; - QJsonArray createToolResultsContent(const QHash &toolResults) const; - - PluginLLMCore::MessageState state() const { return m_state; } - QList getCurrentToolUseContent() const; - QList getCurrentThinkingContent() const; - QList getCurrentRedactedThinkingContent() const; - const QList &getCurrentBlocks() const { return m_currentBlocks; } - - void startNewContinuation(); - -private: - QString m_stopReason; - PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building; - QList m_currentBlocks; - QHash m_pendingToolInputs; - - void updateStateFromStopReason(); - - template - T *addCurrentContent(Args &&...args) - { - T *content = new T(std::forward(args)...); - content->setParent(this); - m_currentBlocks.append(content); - return content; - } -}; - -} // namespace QodeAssist diff --git a/providers/GoogleMessage.cpp b/providers/GoogleMessage.cpp deleted file mode 100644 index c149daf..0000000 --- a/providers/GoogleMessage.cpp +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "GoogleMessage.hpp" - -#include -#include - -#include "logger/Logger.hpp" - -namespace QodeAssist::Providers { - -GoogleMessage::GoogleMessage(QObject *parent) - : QObject(parent) -{} - -void GoogleMessage::handleContentDelta(const QString &text) -{ - if (m_currentBlocks.isEmpty() || !qobject_cast(m_currentBlocks.last())) { - auto textContent = new PluginLLMCore::TextContent(); - textContent->setParent(this); - m_currentBlocks.append(textContent); - } - - if (auto textContent = qobject_cast(m_currentBlocks.last())) { - textContent->appendText(text); - } -} - -void GoogleMessage::handleThoughtDelta(const QString &text) -{ - if (m_currentBlocks.isEmpty() || !qobject_cast(m_currentBlocks.last())) { - auto thinkingContent = new PluginLLMCore::ThinkingContent(); - thinkingContent->setParent(this); - m_currentBlocks.append(thinkingContent); - } - - if (auto thinkingContent = qobject_cast(m_currentBlocks.last())) { - thinkingContent->appendThinking(text); - } -} - -void GoogleMessage::handleThoughtSignature(const QString &signature) -{ - for (int i = m_currentBlocks.size() - 1; i >= 0; --i) { - if (auto thinkingContent = qobject_cast(m_currentBlocks[i])) { - thinkingContent->setSignature(signature); - return; - } - } - - auto thinkingContent = new PluginLLMCore::ThinkingContent(); - thinkingContent->setParent(this); - thinkingContent->setSignature(signature); - m_currentBlocks.append(thinkingContent); -} - -void GoogleMessage::handleFunctionCallStart(const QString &name) -{ - m_currentFunctionName = name; - m_pendingFunctionArgs.clear(); -} - -void GoogleMessage::handleFunctionCallArgsDelta(const QString &argsJson) -{ - m_pendingFunctionArgs += argsJson; -} - -void GoogleMessage::handleFunctionCallComplete() -{ - if (m_currentFunctionName.isEmpty()) { - return; - } - - QJsonObject args; - if (!m_pendingFunctionArgs.isEmpty()) { - QJsonDocument doc = QJsonDocument::fromJson(m_pendingFunctionArgs.toUtf8()); - if (doc.isObject()) { - args = doc.object(); - } - } - - QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); - auto toolContent = new PluginLLMCore::ToolUseContent(id, m_currentFunctionName, args); - toolContent->setParent(this); - m_currentBlocks.append(toolContent); - - m_currentFunctionName.clear(); - m_pendingFunctionArgs.clear(); -} - -void GoogleMessage::handleFinishReason(const QString &reason) -{ - m_finishReason = reason; - updateStateFromFinishReason(); -} - -QJsonObject GoogleMessage::toProviderFormat() const -{ - QJsonObject content; - content["role"] = "model"; - - QJsonArray parts; - - for (auto block : m_currentBlocks) { - if (!block) - continue; - - if (auto text = qobject_cast(block)) { - parts.append(QJsonObject{{"text", text->text()}}); - } else if (auto tool = qobject_cast(block)) { - QJsonObject functionCall; - functionCall["name"] = tool->name(); - functionCall["args"] = tool->input(); - parts.append(QJsonObject{{"functionCall", functionCall}}); - } else if (auto thinking = qobject_cast(block)) { - // Include thinking blocks with their text - QJsonObject thinkingPart; - thinkingPart["text"] = thinking->thinking(); - thinkingPart["thought"] = true; - parts.append(thinkingPart); - - // If there's a signature, add it as a separate part - if (!thinking->signature().isEmpty()) { - QJsonObject signaturePart; - signaturePart["thoughtSignature"] = thinking->signature(); - parts.append(signaturePart); - } - } - } - - content["parts"] = parts; - return content; -} - -QJsonArray GoogleMessage::createToolResultParts(const QHash &toolResults) const -{ - QJsonArray parts; - - for (auto toolContent : getCurrentToolUseContent()) { - if (toolResults.contains(toolContent->id())) { - QJsonObject functionResponse; - functionResponse["name"] = toolContent->name(); - - QJsonObject response; - response["result"] = toolResults[toolContent->id()]; - functionResponse["response"] = response; - - parts.append(QJsonObject{{"functionResponse", functionResponse}}); - } - } - - return parts; -} - -QList GoogleMessage::getCurrentToolUseContent() const -{ - QList toolBlocks; - for (auto block : m_currentBlocks) { - if (auto toolContent = qobject_cast(block)) { - toolBlocks.append(toolContent); - } - } - return toolBlocks; -} - -QList GoogleMessage::getCurrentThinkingContent() const -{ - QList thinkingBlocks; - for (auto block : m_currentBlocks) { - if (auto thinkingContent = qobject_cast(block)) { - thinkingBlocks.append(thinkingContent); - } - } - return thinkingBlocks; -} - -void GoogleMessage::startNewContinuation() -{ - LOG_MESSAGE(QString("GoogleMessage: Starting new continuation")); - - m_currentBlocks.clear(); - m_pendingFunctionArgs.clear(); - m_currentFunctionName.clear(); - m_finishReason.clear(); - m_state = PluginLLMCore::MessageState::Building; -} - -bool GoogleMessage::isErrorFinishReason() const -{ - return m_finishReason == "SAFETY" - || m_finishReason == "RECITATION" - || m_finishReason == "MALFORMED_FUNCTION_CALL" - || m_finishReason == "PROHIBITED_CONTENT" - || m_finishReason == "SPII" - || m_finishReason == "OTHER"; -} - -QString GoogleMessage::getErrorMessage() const -{ - if (m_finishReason == "SAFETY") { - return "Response blocked by safety filters"; - } else if (m_finishReason == "RECITATION") { - return "Response blocked due to recitation of copyrighted content"; - } else if (m_finishReason == "MALFORMED_FUNCTION_CALL") { - return "Model attempted to call a function with malformed arguments. Please try rephrasing your request or disabling tools."; - } else if (m_finishReason == "PROHIBITED_CONTENT") { - return "Response blocked due to prohibited content"; - } else if (m_finishReason == "SPII") { - return "Response blocked due to sensitive personally identifiable information"; - } else if (m_finishReason == "OTHER") { - return "Request failed due to an unknown reason"; - } - return QString(); -} - -void GoogleMessage::updateStateFromFinishReason() -{ - if (m_finishReason == "STOP" || m_finishReason == "MAX_TOKENS") { - m_state = getCurrentToolUseContent().isEmpty() - ? PluginLLMCore::MessageState::Complete - : PluginLLMCore::MessageState::RequiresToolExecution; - } else { - m_state = PluginLLMCore::MessageState::Complete; - } -} - -} // namespace QodeAssist::Providers diff --git a/providers/GoogleMessage.hpp b/providers/GoogleMessage.hpp deleted file mode 100644 index 7802de0..0000000 --- a/providers/GoogleMessage.hpp +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include -#include - -#include - -namespace QodeAssist::Providers { - -class GoogleMessage : public QObject -{ - Q_OBJECT -public: - explicit GoogleMessage(QObject *parent = nullptr); - - void handleContentDelta(const QString &text); - void handleThoughtDelta(const QString &text); - void handleThoughtSignature(const QString &signature); - void handleFunctionCallStart(const QString &name); - void handleFunctionCallArgsDelta(const QString &argsJson); - void handleFunctionCallComplete(); - void handleFinishReason(const QString &reason); - - QJsonObject toProviderFormat() const; - QJsonArray createToolResultParts(const QHash &toolResults) const; - - QList getCurrentToolUseContent() const; - QList getCurrentThinkingContent() const; - QList currentBlocks() const { return m_currentBlocks; } - - PluginLLMCore::MessageState state() const { return m_state; } - QString finishReason() const { return m_finishReason; } - bool isErrorFinishReason() const; - QString getErrorMessage() const; - void startNewContinuation(); - -private: - void updateStateFromFinishReason(); - - QList m_currentBlocks; - QString m_pendingFunctionArgs; - QString m_currentFunctionName; - QString m_finishReason; - PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/OllamaMessage.cpp b/providers/OllamaMessage.cpp deleted file mode 100644 index 2edb85c..0000000 --- a/providers/OllamaMessage.cpp +++ /dev/null @@ -1,349 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "OllamaMessage.hpp" -#include "logger/Logger.hpp" - -#include -#include - -namespace QodeAssist::Providers { - -OllamaMessage::OllamaMessage(QObject *parent) - : QObject(parent) -{} - -void OllamaMessage::handleContentDelta(const QString &content) -{ - m_accumulatedContent += content; - QString trimmed = m_accumulatedContent.trimmed(); - - if (trimmed.startsWith('{')) { - return; - } - - if (!m_contentAddedToTextBlock) { - PluginLLMCore::TextContent *textContent = getOrCreateTextContent(); - textContent->setText(m_accumulatedContent); - m_contentAddedToTextBlock = true; - LOG_MESSAGE(QString("OllamaMessage: Added accumulated content to TextContent, length=%1") - .arg(m_accumulatedContent.length())); - } else { - PluginLLMCore::TextContent *textContent = getOrCreateTextContent(); - textContent->appendText(content); - } -} - -void OllamaMessage::handleToolCall(const QJsonObject &toolCall) -{ - QJsonObject function = toolCall["function"].toObject(); - QString name = function["name"].toString(); - QJsonObject arguments = function["arguments"].toObject(); - - QString toolId = QString("call_%1_%2").arg(name).arg(QDateTime::currentMSecsSinceEpoch()); - - if (!m_contentAddedToTextBlock && !m_accumulatedContent.trimmed().isEmpty()) { - LOG_MESSAGE( - QString("OllamaMessage: Clearing accumulated content (tool call detected), length=%1") - .arg(m_accumulatedContent.length())); - m_accumulatedContent.clear(); - } - - addCurrentContent(toolId, name, arguments); - - LOG_MESSAGE( - QString("OllamaMessage: Structured tool call detected - name=%1, id=%2").arg(name, toolId)); -} - -void OllamaMessage::handleThinkingDelta(const QString &thinking) -{ - PluginLLMCore::ThinkingContent *thinkingContent = getOrCreateThinkingContent(); - thinkingContent->appendThinking(thinking); -} - -void OllamaMessage::handleThinkingComplete(const QString &signature) -{ - if (m_currentThinkingContent) { - m_currentThinkingContent->setSignature(signature); - LOG_MESSAGE(QString("OllamaMessage: Set thinking signature, length=%1") - .arg(signature.length())); - } -} - -void OllamaMessage::handleDone(bool done) -{ - m_done = done; - if (done) { - bool isToolCall = tryParseToolCall(); - - if (!isToolCall && !m_contentAddedToTextBlock && !m_accumulatedContent.trimmed().isEmpty()) { - QString trimmed = m_accumulatedContent.trimmed(); - - if (trimmed.startsWith('{') - && (trimmed.contains("\"name\"") || trimmed.contains("\"arguments\""))) { - LOG_MESSAGE( - QString("OllamaMessage: Skipping invalid/incomplete tool call JSON (length=%1)") - .arg(trimmed.length())); - - for (auto it = m_currentBlocks.begin(); it != m_currentBlocks.end();) { - if (qobject_cast(*it)) { - LOG_MESSAGE(QString( - "OllamaMessage: Removing TextContent block (incomplete tool call)")); - (*it)->deleteLater(); - it = m_currentBlocks.erase(it); - } else { - ++it; - } - } - - m_accumulatedContent.clear(); - } else { - PluginLLMCore::TextContent *textContent = getOrCreateTextContent(); - textContent->setText(m_accumulatedContent); - m_contentAddedToTextBlock = true; - LOG_MESSAGE( - QString( - "OllamaMessage: Added final accumulated content to TextContent, length=%1") - .arg(m_accumulatedContent.length())); - } - } - - updateStateFromDone(); - } -} -bool OllamaMessage::tryParseToolCall() -{ - QString trimmed = m_accumulatedContent.trimmed(); - - if (trimmed.isEmpty()) { - return false; - } - - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(trimmed.toUtf8(), &parseError); - - if (parseError.error != QJsonParseError::NoError) { - LOG_MESSAGE(QString("OllamaMessage: Content is not valid JSON (not a tool call): %1") - .arg(parseError.errorString())); - return false; - } - - if (!doc.isObject()) { - LOG_MESSAGE(QString("OllamaMessage: Content is not a JSON object (not a tool call)")); - return false; - } - - QJsonObject obj = doc.object(); - - if (!obj.contains("name") || !obj.contains("arguments")) { - LOG_MESSAGE( - QString("OllamaMessage: JSON missing 'name' or 'arguments' fields (not a tool call)")); - return false; - } - - QString name = obj["name"].toString(); - QJsonValue argsValue = obj["arguments"]; - QJsonObject arguments; - - if (argsValue.isObject()) { - arguments = argsValue.toObject(); - } else if (argsValue.isString()) { - QJsonDocument argsDoc = QJsonDocument::fromJson(argsValue.toString().toUtf8()); - if (argsDoc.isObject()) { - arguments = argsDoc.object(); - } else { - LOG_MESSAGE(QString("OllamaMessage: Failed to parse arguments as JSON object")); - return false; - } - } else { - LOG_MESSAGE(QString("OllamaMessage: Arguments field is neither object nor string")); - return false; - } - - if (name.isEmpty()) { - LOG_MESSAGE(QString("OllamaMessage: Tool name is empty")); - return false; - } - - QString toolId = QString("call_%1_%2").arg(name).arg(QDateTime::currentMSecsSinceEpoch()); - - for (auto block : m_currentBlocks) { - if (qobject_cast(block)) { - LOG_MESSAGE(QString("OllamaMessage: Removing TextContent block (tool call detected)")); - } - } - m_currentBlocks.clear(); - - addCurrentContent(toolId, name, arguments); - - LOG_MESSAGE( - QString( - "OllamaMessage: Successfully parsed tool call from legacy format - name=%1, id=%2, " - "args=%3") - .arg( - name, - toolId, - QString::fromUtf8(QJsonDocument(arguments).toJson(QJsonDocument::Compact)))); - - return true; -} - -bool OllamaMessage::isLikelyToolCallJson(const QString &content) const -{ - QString trimmed = content.trimmed(); - - if (trimmed.startsWith('{')) { - if (trimmed.contains("\"name\"") && trimmed.contains("\"arguments\"")) { - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(trimmed.toUtf8(), &parseError); - - if (parseError.error == QJsonParseError::NoError && doc.isObject()) { - QJsonObject obj = doc.object(); - if (obj.contains("name") && obj.contains("arguments")) { - return true; - } - } - } - } - - return false; -} - -QJsonObject OllamaMessage::toProviderFormat() const -{ - QJsonObject message; - message["role"] = "assistant"; - - QString textContent; - QJsonArray toolCalls; - QString thinkingContent; - - for (auto block : m_currentBlocks) { - if (!block) - continue; - - if (auto text = qobject_cast(block)) { - textContent += text->text(); - } else if (auto tool = qobject_cast(block)) { - QJsonObject toolCall; - toolCall["type"] = "function"; - toolCall["function"] = QJsonObject{{"name", tool->name()}, {"arguments", tool->input()}}; - toolCalls.append(toolCall); - } else if (auto thinking = qobject_cast(block)) { - thinkingContent += thinking->thinking(); - } - } - - if (!thinkingContent.isEmpty()) { - message["thinking"] = thinkingContent; - } - - if (!textContent.isEmpty()) { - message["content"] = textContent; - } - - if (!toolCalls.isEmpty()) { - message["tool_calls"] = toolCalls; - } - - return message; -} - -QJsonArray OllamaMessage::createToolResultMessages(const QHash &toolResults) const -{ - QJsonArray messages; - - for (auto toolContent : getCurrentToolUseContent()) { - if (toolResults.contains(toolContent->id())) { - QJsonObject toolMessage; - toolMessage["role"] = "tool"; - toolMessage["content"] = toolResults[toolContent->id()]; - messages.append(toolMessage); - - LOG_MESSAGE(QString( - "OllamaMessage: Created tool result message for tool %1 (id=%2), " - "content length=%3") - .arg(toolContent->name(), toolContent->id()) - .arg(toolResults[toolContent->id()].length())); - } - } - - return messages; -} - -QList OllamaMessage::getCurrentToolUseContent() const -{ - QList toolBlocks; - for (auto block : m_currentBlocks) { - if (auto toolContent = qobject_cast(block)) { - toolBlocks.append(toolContent); - } - } - return toolBlocks; -} - -QList OllamaMessage::getCurrentThinkingContent() const -{ - QList thinkingBlocks; - for (auto block : m_currentBlocks) { - if (auto thinkingContent = qobject_cast(block)) { - thinkingBlocks.append(thinkingContent); - } - } - return thinkingBlocks; -} - -void OllamaMessage::startNewContinuation() -{ - LOG_MESSAGE(QString("OllamaMessage: Starting new continuation")); - - m_currentBlocks.clear(); - m_accumulatedContent.clear(); - m_done = false; - m_state = PluginLLMCore::MessageState::Building; - m_contentAddedToTextBlock = false; - m_currentThinkingContent = nullptr; -} - -void OllamaMessage::updateStateFromDone() -{ - if (!getCurrentToolUseContent().empty()) { - m_state = PluginLLMCore::MessageState::RequiresToolExecution; - LOG_MESSAGE(QString("OllamaMessage: State set to RequiresToolExecution, tools count=%1") - .arg(getCurrentToolUseContent().size())); - } else { - m_state = PluginLLMCore::MessageState::Final; - LOG_MESSAGE(QString("OllamaMessage: State set to Final")); - } -} - -PluginLLMCore::TextContent *OllamaMessage::getOrCreateTextContent() -{ - for (auto block : m_currentBlocks) { - if (auto textContent = qobject_cast(block)) { - return textContent; - } - } - - return addCurrentContent(); -} - -PluginLLMCore::ThinkingContent *OllamaMessage::getOrCreateThinkingContent() -{ - if (m_currentThinkingContent) { - return m_currentThinkingContent; - } - - for (auto block : m_currentBlocks) { - if (auto thinkingContent = qobject_cast(block)) { - m_currentThinkingContent = thinkingContent; - return m_currentThinkingContent; - } - } - - m_currentThinkingContent = addCurrentContent(); - LOG_MESSAGE(QString("OllamaMessage: Created new ThinkingContent block")); - return m_currentThinkingContent; -} - -} // namespace QodeAssist::Providers diff --git a/providers/OllamaMessage.hpp b/providers/OllamaMessage.hpp deleted file mode 100644 index f471bcb..0000000 --- a/providers/OllamaMessage.hpp +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -namespace QodeAssist::Providers { - -class OllamaMessage : public QObject -{ - Q_OBJECT -public: - explicit OllamaMessage(QObject *parent = nullptr); - - void handleContentDelta(const QString &content); - void handleToolCall(const QJsonObject &toolCall); - void handleThinkingDelta(const QString &thinking); - void handleThinkingComplete(const QString &signature); - void handleDone(bool done); - - QJsonObject toProviderFormat() const; - QJsonArray createToolResultMessages(const QHash &toolResults) const; - - PluginLLMCore::MessageState state() const { return m_state; } - QList getCurrentToolUseContent() const; - QList getCurrentThinkingContent() const; - QList currentBlocks() const { return m_currentBlocks; } - - void startNewContinuation(); - -private: - bool m_done = false; - PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building; - QList m_currentBlocks; - QString m_accumulatedContent; - bool m_contentAddedToTextBlock = false; - PluginLLMCore::ThinkingContent *m_currentThinkingContent = nullptr; - - void updateStateFromDone(); - bool tryParseToolCall(); - bool isLikelyToolCallJson(const QString &content) const; - PluginLLMCore::TextContent *getOrCreateTextContent(); - PluginLLMCore::ThinkingContent *getOrCreateThinkingContent(); - - template - T *addCurrentContent(Args &&...args) - { - T *content = new T(std::forward(args)...); - content->setParent(this); - m_currentBlocks.append(content); - return content; - } -}; - -} // namespace QodeAssist::Providers diff --git a/providers/OpenAIMessage.cpp b/providers/OpenAIMessage.cpp deleted file mode 100644 index 53d0d81..0000000 --- a/providers/OpenAIMessage.cpp +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "OpenAIMessage.hpp" - -#include "logger/Logger.hpp" - -#include -#include - -namespace QodeAssist::Providers { - -OpenAIMessage::OpenAIMessage(QObject *parent) - : QObject(parent) -{} - -void OpenAIMessage::handleContentDelta(const QString &content) -{ - auto textContent = getOrCreateTextContent(); - textContent->appendText(content); -} - -void OpenAIMessage::handleToolCallStart(int index, const QString &id, const QString &name) -{ - LOG_MESSAGE(QString("OpenAIMessage: handleToolCallStart index=%1, id=%2, name=%3") - .arg(index) - .arg(id, name)); - - while (m_currentBlocks.size() <= index) { - m_currentBlocks.append(nullptr); - } - - auto toolContent = new PluginLLMCore::ToolUseContent(id, name); - toolContent->setParent(this); - m_currentBlocks[index] = toolContent; - m_pendingToolArguments[index] = ""; -} - -void OpenAIMessage::handleToolCallDelta(int index, const QString &argumentsDelta) -{ - if (m_pendingToolArguments.contains(index)) { - m_pendingToolArguments[index] += argumentsDelta; - } -} - -void OpenAIMessage::handleToolCallComplete(int index) -{ - if (m_pendingToolArguments.contains(index)) { - QString jsonArgs = m_pendingToolArguments[index]; - QJsonObject argsObject; - - if (!jsonArgs.isEmpty()) { - QJsonDocument doc = QJsonDocument::fromJson(jsonArgs.toUtf8()); - if (doc.isObject()) { - argsObject = doc.object(); - } - } - - if (index < m_currentBlocks.size()) { - if (auto toolContent = qobject_cast(m_currentBlocks[index])) { - toolContent->setInput(argsObject); - } - } - - m_pendingToolArguments.remove(index); - } -} - -void OpenAIMessage::handleFinishReason(const QString &finishReason) -{ - m_finishReason = finishReason; - updateStateFromFinishReason(); -} - -QJsonObject OpenAIMessage::toProviderFormat() const -{ - QJsonObject message; - message["role"] = "assistant"; - - QString textContent; - QJsonArray toolCalls; - - for (auto block : m_currentBlocks) { - if (!block) - continue; - - if (auto text = qobject_cast(block)) { - textContent += text->text(); - } else if (auto tool = qobject_cast(block)) { - toolCalls.append(tool->toJson(PluginLLMCore::ProviderFormat::OpenAI)); - } - } - - if (!textContent.isEmpty()) { - message["content"] = textContent; - } else { - message["content"] = QJsonValue(); - } - - if (!toolCalls.isEmpty()) { - message["tool_calls"] = toolCalls; - } - - return message; -} - -QJsonArray OpenAIMessage::createToolResultMessages(const QHash &toolResults) const -{ - QJsonArray messages; - - for (auto toolContent : getCurrentToolUseContent()) { - if (toolResults.contains(toolContent->id())) { - auto toolResult = std::make_unique( - toolContent->id(), toolResults[toolContent->id()]); - messages.append(toolResult->toJson(PluginLLMCore::ProviderFormat::OpenAI)); - } - } - - return messages; -} - -QList OpenAIMessage::getCurrentToolUseContent() const -{ - QList toolBlocks; - for (auto block : m_currentBlocks) { - if (auto toolContent = qobject_cast(block)) { - toolBlocks.append(toolContent); - } - } - return toolBlocks; -} - -void OpenAIMessage::startNewContinuation() -{ - LOG_MESSAGE(QString("OpenAIAPIMessage: Starting new continuation")); - - m_currentBlocks.clear(); - m_pendingToolArguments.clear(); - m_finishReason.clear(); - m_state = PluginLLMCore::MessageState::Building; -} - -void OpenAIMessage::updateStateFromFinishReason() -{ - if (m_finishReason == "tool_calls" && !getCurrentToolUseContent().empty()) { - m_state = PluginLLMCore::MessageState::RequiresToolExecution; - } else if (m_finishReason == "stop") { - m_state = PluginLLMCore::MessageState::Final; - } else { - m_state = PluginLLMCore::MessageState::Complete; - } -} - -PluginLLMCore::TextContent *OpenAIMessage::getOrCreateTextContent() -{ - for (auto block : m_currentBlocks) { - if (auto textContent = qobject_cast(block)) { - return textContent; - } - } - - return addCurrentContent(); -} - -} // namespace QodeAssist::Providers diff --git a/providers/OpenAIMessage.hpp b/providers/OpenAIMessage.hpp deleted file mode 100644 index c963765..0000000 --- a/providers/OpenAIMessage.hpp +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -namespace QodeAssist::Providers { - -class OpenAIMessage : public QObject -{ - Q_OBJECT -public: - explicit OpenAIMessage(QObject *parent = nullptr); - - void handleContentDelta(const QString &content); - void handleToolCallStart(int index, const QString &id, const QString &name); - void handleToolCallDelta(int index, const QString &argumentsDelta); - void handleToolCallComplete(int index); - void handleFinishReason(const QString &finishReason); - - QJsonObject toProviderFormat() const; - QJsonArray createToolResultMessages(const QHash &toolResults) const; - - PluginLLMCore::MessageState state() const { return m_state; } - QList getCurrentToolUseContent() const; - - void startNewContinuation(); - -private: - QString m_finishReason; - PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building; - QList m_currentBlocks; - QHash m_pendingToolArguments; - - void updateStateFromFinishReason(); - PluginLLMCore::TextContent *getOrCreateTextContent(); - - template - T *addCurrentContent(Args &&...args) - { - T *content = new T(std::forward(args)...); - content->setParent(this); - m_currentBlocks.append(content); - return content; - } -}; - -} // namespace QodeAssist::Providers diff --git a/providers/OpenAIResponses/CancelResponseRequest.hpp b/providers/OpenAIResponses/CancelResponseRequest.hpp deleted file mode 100644 index 1918478..0000000 --- a/providers/OpenAIResponses/CancelResponseRequest.hpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -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 - diff --git a/providers/OpenAIResponses/DeleteResponseRequest.hpp b/providers/OpenAIResponses/DeleteResponseRequest.hpp deleted file mode 100644 index f70acfa..0000000 --- a/providers/OpenAIResponses/DeleteResponseRequest.hpp +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include - -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 - diff --git a/providers/OpenAIResponses/GetResponseRequest.hpp b/providers/OpenAIResponses/GetResponseRequest.hpp deleted file mode 100644 index 2d59ad3..0000000 --- a/providers/OpenAIResponses/GetResponseRequest.hpp +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include -#include - -namespace QodeAssist::OpenAIResponses { - -struct GetResponseRequest -{ - QString responseId; - std::optional include; - std::optional includeObfuscation; - std::optional startingAfter; - std::optional 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 - diff --git a/providers/OpenAIResponses/InputTokensRequest.hpp b/providers/OpenAIResponses/InputTokensRequest.hpp deleted file mode 100644 index 4d3c265..0000000 --- a/providers/OpenAIResponses/InputTokensRequest.hpp +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include "ModelRequest.hpp" - -#include -#include - -namespace QodeAssist::OpenAIResponses { - -struct InputTokensRequest -{ - std::optional conversation; - std::optional input; - std::optional instructions; - std::optional model; - std::optional parallelToolCalls; - std::optional previousResponseId; - std::optional reasoning; - std::optional text; - std::optional toolChoice; - std::optional tools; - std::optional 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 - diff --git a/providers/OpenAIResponses/ItemTypesReference.hpp b/providers/OpenAIResponses/ItemTypesReference.hpp deleted file mode 100644 index 86c274e..0000000 --- a/providers/OpenAIResponses/ItemTypesReference.hpp +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#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 - diff --git a/providers/OpenAIResponses/ListInputItemsRequest.hpp b/providers/OpenAIResponses/ListInputItemsRequest.hpp deleted file mode 100644 index 5a9b7f1..0000000 --- a/providers/OpenAIResponses/ListInputItemsRequest.hpp +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include -#include -#include - -namespace QodeAssist::OpenAIResponses { - -enum class SortOrder { Ascending, Descending }; - -struct ListInputItemsRequest -{ - QString responseId; - std::optional after; - std::optional include; - std::optional limit; - std::optional 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 - diff --git a/providers/OpenAIResponses/ModelRequest.hpp b/providers/OpenAIResponses/ModelRequest.hpp deleted file mode 100644 index cd342ed..0000000 --- a/providers/OpenAIResponses/ModelRequest.hpp +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include -#include -#include -#include - -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 fileId; - std::optional 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 fileId; - std::optional fileUrl; - std::optional fileData; - std::optional 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; - if constexpr (std::is_same_v) { - return content; - } else { - return content.toJson(); - } - }, m_variant); - } - - bool isValid() const noexcept - { - return std::visit([](const auto &content) -> bool { - using T = std::decay_t; - if constexpr (std::is_same_v) { - return !content.isEmpty(); - } else { - return content.isValid(); - } - }, m_variant); - } - -private: - std::variant m_variant; -}; - -struct Message -{ - Role role; - QList content; - std::optional 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 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 maxNumResults; - std::optional 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 m_variant; -}; - -struct TextFormatOptions -{ - TextFormat type = TextFormat::Text; - std::optional name; - std::optional schema; - std::optional description; - std::optional 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 - diff --git a/providers/OpenAIResponses/ResponseObject.hpp b/providers/OpenAIResponses/ResponseObject.hpp deleted file mode 100644 index 4e3d915..0000000 --- a/providers/OpenAIResponses/ResponseObject.hpp +++ /dev/null @@ -1,546 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include -#include -#include -#include - -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 fileCitations; - QList 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 outputTexts; - QList 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 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 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 code; - QString status; - QList 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(m_data) ? &std::get(m_data) - : nullptr; - } - - const FunctionCall *asFunctionCall() const - { - return std::holds_alternative(m_data) ? &std::get(m_data) - : nullptr; - } - - const ReasoningOutput *asReasoning() const - { - return std::holds_alternative(m_data) ? &std::get(m_data) - : nullptr; - } - - const FileSearchCall *asFileSearch() const - { - return std::holds_alternative(m_data) ? &std::get(m_data) - : nullptr; - } - - const CodeInterpreterCall *asCodeInterpreter() const - { - return std::holds_alternative(m_data) - ? &std::get(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 - 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 output; - QString outputText; - std::optional usage; - std::optional error; - std::optional 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 - diff --git a/providers/OpenAIResponsesMessage.cpp b/providers/OpenAIResponsesMessage.cpp deleted file mode 100644 index 72bcf88..0000000 --- a/providers/OpenAIResponsesMessage.cpp +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "OpenAIResponsesMessage.hpp" -#include "OpenAIResponses/ResponseObject.hpp" - -#include "logger/Logger.hpp" - -#include - -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 PluginLLMCore::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 PluginLLMCore::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 OpenAIResponsesMessage::toItemsFormat() const -{ - QList items; - - QString textContent; - QList toolCalls; - - for (const auto *block : m_items) { - if (const auto *text = qobject_cast(block)) { - textContent += text->text(); - } else if (auto *tool = qobject_cast( - const_cast(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 OpenAIResponsesMessage::getCurrentToolUseContent() const -{ - QList toolBlocks; - for (auto *block : m_items) { - if (auto *toolContent = qobject_cast(block)) { - toolBlocks.append(toolContent); - } - } - return toolBlocks; -} - -QList OpenAIResponsesMessage::getCurrentThinkingContent() const -{ - QList thinkingBlocks; - for (auto *block : m_items) { - if (auto *thinkingContent = qobject_cast(block)) { - thinkingBlocks.append(thinkingContent); - } - } - return thinkingBlocks; -} - -QJsonArray OpenAIResponsesMessage::createToolResultItems(const QHash &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(block)) { - text += textContent->text(); - } - } - return text; -} - -void OpenAIResponsesMessage::updateStateFromStatus() -{ - using namespace QodeAssist::OpenAIResponses; - - if (m_status == "completed") { - if (!getCurrentToolUseContent().isEmpty()) { - m_state = PluginLLMCore::MessageState::RequiresToolExecution; - } else { - m_state = PluginLLMCore::MessageState::Complete; - } - } else if (m_status == "in_progress") { - m_state = PluginLLMCore::MessageState::Building; - } else if (m_status == "failed" || m_status == "cancelled" || m_status == "incomplete") { - m_state = PluginLLMCore::MessageState::Final; - } else { - m_state = PluginLLMCore::MessageState::Building; - } -} - -PluginLLMCore::TextContent *OpenAIResponsesMessage::getOrCreateTextItem() -{ - for (auto *block : m_items) { - if (auto *textContent = qobject_cast(block)) { - return textContent; - } - } - - auto *textContent = new PluginLLMCore::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 = PluginLLMCore::MessageState::Building; -} - -} // namespace QodeAssist::Providers - diff --git a/providers/OpenAIResponsesMessage.hpp b/providers/OpenAIResponsesMessage.hpp deleted file mode 100644 index fa94b3e..0000000 --- a/providers/OpenAIResponsesMessage.hpp +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -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 toItemsFormat() const; - QJsonArray createToolResultItems(const QHash &toolResults) const; - - PluginLLMCore::MessageState state() const noexcept { return m_state; } - QString accumulatedText() const; - QList getCurrentToolUseContent() const; - QList getCurrentThinkingContent() const; - - bool hasToolCalls() const noexcept { return !m_toolCalls.isEmpty(); } - bool hasThinkingContent() const noexcept { return !m_thinkingBlocks.isEmpty(); } - - void startNewContinuation(); - -private: - QString m_status; - PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building; - QList m_items; - QHash m_pendingToolArguments; - QHash m_toolCalls; - QHash m_thinkingBlocks; - - void updateStateFromStatus(); - PluginLLMCore::TextContent *getOrCreateTextItem(); -}; - -} // namespace QodeAssist::Providers - diff --git a/providers/OpenAIResponsesRequestBuilder.hpp b/providers/OpenAIResponsesRequestBuilder.hpp deleted file mode 100644 index 3854dec..0000000 --- a/providers/OpenAIResponsesRequestBuilder.hpp +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include "OpenAIResponses/ModelRequest.hpp" - -#include -#include -#include -#include - -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 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 m_messages; - std::optional m_instructions; - QList m_tools; - std::optional m_temperature; - std::optional m_topP; - std::optional m_maxOutputTokens; - bool m_stream = false; - std::optional m_store; - std::optional m_textFormat; - std::optional m_reasoningEffort; - bool m_includeReasoningContent = false; - QMap 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 - diff --git a/templates/OpenAIResponses.hpp b/templates/OpenAIResponses.hpp index e24d58f..e218f02 100644 --- a/templates/OpenAIResponses.hpp +++ b/templates/OpenAIResponses.hpp @@ -4,78 +4,75 @@ #pragma once #include "pluginllmcore/PromptTemplate.hpp" -#include "providers/OpenAIResponsesRequestBuilder.hpp" + +#include +#include namespace QodeAssist::Templates { class OpenAIResponses : public PluginLLMCore::PromptTemplate { public: - PluginLLMCore::TemplateType type() const noexcept override - { - return PluginLLMCore::TemplateType::Chat; - } - - QString name() const override { return "OpenAI Responses"; } - - QStringList stopWords() const override { return {}; } - - void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override + PluginLLMCore::TemplateType type() const noexcept override { - using namespace QodeAssist::OpenAIResponses; - RequestBuilder builder; + return PluginLLMCore::TemplateType::Chat; + } + QString name() const override { return "OpenAI Responses"; } + + QStringList stopWords() const override { return {}; } + + void prepareRequest( + QJsonObject &request, const PluginLLMCore::ContextData &context) const override + { if (context.systemPrompt) { - builder.setInstructions(context.systemPrompt.value()); + request["instructions"] = context.systemPrompt.value(); } if (!context.history || context.history->isEmpty()) { return; } - const auto &history = context.history.value(); - - for (const auto &msg : history) { + QJsonArray input; + for (const auto &msg : context.history.value()) { if (msg.role == "system") { continue; } - Message message; - message.role = roleFromString(msg.role); + QJsonObject message; + message["role"] = msg.role; - if (msg.images && !msg.images->isEmpty()) { - const auto &images = msg.images.value(); - message.content.reserve(1 + images.size()); - + const bool hasImages = msg.images && !msg.images->isEmpty(); + + if (!hasImages) { + message["content"] = msg.content; + } else { + QJsonArray content; if (!msg.content.isEmpty()) { - message.content.append(MessageContent(InputText{msg.content})); + content.append( + QJsonObject{{"type", "input_text"}, {"text", msg.content}}); } - for (const auto &image : images) { - InputImage imgInput; - imgInput.detail = "auto"; - + for (const auto &image : msg.images.value()) { + QJsonObject imgObj{{"type", "input_image"}, {"detail", "auto"}}; if (image.isUrl) { - imgInput.imageUrl = image.data; + imgObj["image_url"] = image.data; } else { - imgInput.imageUrl + imgObj["image_url"] = QString("data:%1;base64,%2").arg(image.mediaType, image.data); } - - message.content.append(MessageContent(std::move(imgInput))); + content.append(imgObj); } - } else { - message.content.append(MessageContent(msg.content)); + + message["content"] = content; } - builder.addMessage(std::move(message)); + input.append(message); } - const QJsonObject builtRequest = builder.toJson(); - for (auto it = builtRequest.constBegin(); it != builtRequest.constEnd(); ++it) { - request[it.key()] = it.value(); - } + request["input"] = input; } + QString description() const override { return "Template for OpenAI Responses API:\n\n" @@ -89,31 +86,13 @@ public: " \"input\": [\n" " {\"role\": \"user\", \"content\": \"\"}\n" " ]\n" - "}\n\n" - "Uses type-safe RequestBuilder for OpenAI Responses API."; + "}"; } + bool isSupportProvider(PluginLLMCore::ProviderID id) const noexcept override { return id == QodeAssist::PluginLLMCore::ProviderID::OpenAIResponses; } - -private: - static QodeAssist::OpenAIResponses::Role roleFromString(const QString &roleStr) noexcept - { - using namespace QodeAssist::OpenAIResponses; - - if (roleStr == "user") - return Role::User; - if (roleStr == "assistant") - return Role::Assistant; - if (roleStr == "system") - return Role::System; - if (roleStr == "developer") - return Role::Developer; - - return Role::User; - } }; } // namespace QodeAssist::Templates - diff --git a/test/MockDocumentReader.hpp b/test/MockDocumentReader.hpp deleted file mode 100644 index 8a28ce3..0000000 --- a/test/MockDocumentReader.hpp +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (C) 2025 Povilas Kanapickas -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include "context/IDocumentReader.hpp" -#include -#include - -namespace QodeAssist { - -class MockDocumentReader : public Context::IDocumentReader -{ -public: - MockDocumentReader() = default; - - Context::DocumentInfo readDocument(const QString &path) const override - { - return m_documentInfo; - } - - void setDocumentInfo(const QString &text, const QString &filePath, const QString &mimeType) - { - m_document = std::make_unique(text); - m_documentInfo.document = m_document.get(); - m_documentInfo.filePath = filePath; - m_documentInfo.mimeType = mimeType; - } - - ~MockDocumentReader() = default; - -private: - Context::DocumentInfo m_documentInfo; - std::unique_ptr m_document; -}; - -} // namespace QodeAssist