refactor: Remove legacy code (#344)

This commit is contained in:
Petr Mironychev
2026-04-25 18:48:28 +02:00
committed by GitHub
parent 7b4e08859c
commit 4d320bc065
31 changed files with 38 additions and 4019 deletions

View File

@@ -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

View File

@@ -1,5 +1,4 @@
add_library(QodeAssistLogger STATIC
EmptyRequestPerformanceLogger.hpp
IRequestPerformanceLogger.hpp
Logger.cpp
Logger.hpp

View File

@@ -1,16 +0,0 @@
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
// 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

View File

@@ -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
)

View File

@@ -1,236 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QString>
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

View File

@@ -1,23 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "SSEBuffer.hpp"
#include <QString>
namespace QodeAssist::PluginLLMCore {
struct DataBuffers
{
SSEBuffer rawStreamBuffer;
QString responseContent;
void clear()
{
rawStreamBuffer.clear();
responseContent.clear();
}
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -1,260 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "HttpClient.hpp"
#include <QJsonDocument>
#include <QMutexLocker>
#include <Logger.hpp>
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<QByteArray> HttpClient::get(const QNetworkRequest &request)
{
LOG_MESSAGE(QString("HttpClient: GET %1").arg(request.url().toString()));
auto promise = std::make_shared<QPromise<QByteArray>>();
promise->start();
QNetworkReply *reply = m_manager->get(request);
setupNonStreamingReply(reply, promise);
return promise->future();
}
QFuture<QByteArray> 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<QPromise<QByteArray>>();
promise->start();
QNetworkReply *reply = m_manager->post(request, doc.toJson(QJsonDocument::Compact));
setupNonStreamingReply(reply, promise);
return promise->future();
}
QFuture<QByteArray> HttpClient::del(const QNetworkRequest &request,
std::optional<QJsonObject> payload)
{
auto promise = std::make_shared<QPromise<QByteArray>>();
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<QPromise<QByteArray>> 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<QNetworkReply *>(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<QNetworkReply *>(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<QString> 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

View File

@@ -1,60 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <optional>
#include <QFuture>
#include <QHash>
#include <QJsonObject>
#include <QMutex>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
#include <QPromise>
namespace QodeAssist::PluginLLMCore {
class HttpClient : public QObject
{
Q_OBJECT
public:
HttpClient(QObject *parent = nullptr);
~HttpClient();
// Non-streaming — return QFuture with full response
QFuture<QByteArray> get(const QNetworkRequest &request);
QFuture<QByteArray> post(const QNetworkRequest &request, const QJsonObject &payload);
QFuture<QByteArray> del(const QNetworkRequest &request,
std::optional<QJsonObject> 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<QString> error);
private slots:
void onReadyRead();
void onStreamingFinished();
private:
void setupNonStreamingReply(QNetworkReply *reply, std::shared_ptr<QPromise<QByteArray>> 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<QString, QNetworkReply *> m_activeRequests;
mutable QMutex m_mutex;
};
} // namespace QodeAssist::PluginLLMCore

View File

@@ -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

View File

@@ -1,26 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QString>
#include <QStringList>
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

View File

@@ -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 <QJsonArray>
#include <QJsonDocument>
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<PluginLLMCore::TextContent>();
} 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<PluginLLMCore::ImageContent>(imageData, mediaType, imgSourceType);
} else if (blockType == "tool_use") {
QString toolId = data["id"].toString();
QString toolName = data["name"].toString();
QJsonObject toolInput = data["input"].toObject();
addCurrentContent<PluginLLMCore::ToolUseContent>(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<PluginLLMCore::ThinkingContent>(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<PluginLLMCore::RedactedThinkingContent>(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<PluginLLMCore::TextContent *>(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<PluginLLMCore::ThinkingContent *>(m_currentBlocks[index])) {
thinkingContent->appendThinking(delta["thinking"].toString());
}
} else if (deltaType == "signature_delta") {
if (auto thinkingContent = qobject_cast<PluginLLMCore::ThinkingContent *>(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<PluginLLMCore::RedactedThinkingContent *>(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<PluginLLMCore::ToolUseContent *>(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<QString, QString> &toolResults) const
{
QJsonArray results;
for (auto toolContent : getCurrentToolUseContent()) {
if (toolResults.contains(toolContent->id())) {
auto toolResult = std::make_unique<PluginLLMCore::ToolResultContent>(
toolContent->id(), toolResults[toolContent->id()]);
results.append(toolResult->toJson(PluginLLMCore::ProviderFormat::Claude));
}
}
return results;
}
QList<PluginLLMCore::ToolUseContent *> ClaudeMessage::getCurrentToolUseContent() const
{
QList<PluginLLMCore::ToolUseContent *> toolBlocks;
for (auto block : m_currentBlocks) {
if (auto toolContent = qobject_cast<PluginLLMCore::ToolUseContent *>(block)) {
toolBlocks.append(toolContent);
}
}
return toolBlocks;
}
QList<PluginLLMCore::ThinkingContent *> ClaudeMessage::getCurrentThinkingContent() const
{
QList<PluginLLMCore::ThinkingContent *> thinkingBlocks;
for (auto block : m_currentBlocks) {
if (auto thinkingContent = qobject_cast<PluginLLMCore::ThinkingContent *>(block)) {
thinkingBlocks.append(thinkingContent);
}
}
return thinkingBlocks;
}
QList<PluginLLMCore::RedactedThinkingContent *> ClaudeMessage::getCurrentRedactedThinkingContent() const
{
QList<PluginLLMCore::RedactedThinkingContent *> redactedBlocks;
for (auto block : m_currentBlocks) {
if (auto redactedContent = qobject_cast<PluginLLMCore::RedactedThinkingContent *>(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

View File

@@ -1,50 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <pluginllmcore/ContentBlocks.hpp>
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<QString, QString> &toolResults) const;
PluginLLMCore::MessageState state() const { return m_state; }
QList<PluginLLMCore::ToolUseContent *> getCurrentToolUseContent() const;
QList<PluginLLMCore::ThinkingContent *> getCurrentThinkingContent() const;
QList<PluginLLMCore::RedactedThinkingContent *> getCurrentRedactedThinkingContent() const;
const QList<PluginLLMCore::ContentBlock *> &getCurrentBlocks() const { return m_currentBlocks; }
void startNewContinuation();
private:
QString m_stopReason;
PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building;
QList<PluginLLMCore::ContentBlock *> m_currentBlocks;
QHash<int, QString> m_pendingToolInputs;
void updateStateFromStopReason();
template<typename T, typename... Args>
T *addCurrentContent(Args &&...args)
{
T *content = new T(std::forward<Args>(args)...);
content->setParent(this);
m_currentBlocks.append(content);
return content;
}
};
} // namespace QodeAssist

View File

@@ -1,228 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "GoogleMessage.hpp"
#include <QJsonDocument>
#include <QUuid>
#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<PluginLLMCore::TextContent *>(m_currentBlocks.last())) {
auto textContent = new PluginLLMCore::TextContent();
textContent->setParent(this);
m_currentBlocks.append(textContent);
}
if (auto textContent = qobject_cast<PluginLLMCore::TextContent *>(m_currentBlocks.last())) {
textContent->appendText(text);
}
}
void GoogleMessage::handleThoughtDelta(const QString &text)
{
if (m_currentBlocks.isEmpty() || !qobject_cast<PluginLLMCore::ThinkingContent *>(m_currentBlocks.last())) {
auto thinkingContent = new PluginLLMCore::ThinkingContent();
thinkingContent->setParent(this);
m_currentBlocks.append(thinkingContent);
}
if (auto thinkingContent = qobject_cast<PluginLLMCore::ThinkingContent *>(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<PluginLLMCore::ThinkingContent *>(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<PluginLLMCore::TextContent *>(block)) {
parts.append(QJsonObject{{"text", text->text()}});
} else if (auto tool = qobject_cast<PluginLLMCore::ToolUseContent *>(block)) {
QJsonObject functionCall;
functionCall["name"] = tool->name();
functionCall["args"] = tool->input();
parts.append(QJsonObject{{"functionCall", functionCall}});
} else if (auto thinking = qobject_cast<PluginLLMCore::ThinkingContent *>(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<QString, QString> &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<PluginLLMCore::ToolUseContent *> GoogleMessage::getCurrentToolUseContent() const
{
QList<PluginLLMCore::ToolUseContent *> toolBlocks;
for (auto block : m_currentBlocks) {
if (auto toolContent = qobject_cast<PluginLLMCore::ToolUseContent *>(block)) {
toolBlocks.append(toolContent);
}
}
return toolBlocks;
}
QList<PluginLLMCore::ThinkingContent *> GoogleMessage::getCurrentThinkingContent() const
{
QList<PluginLLMCore::ThinkingContent *> thinkingBlocks;
for (auto block : m_currentBlocks) {
if (auto thinkingContent = qobject_cast<PluginLLMCore::ThinkingContent *>(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

View File

@@ -1,52 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QHash>
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <pluginllmcore/ContentBlocks.hpp>
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<QString, QString> &toolResults) const;
QList<PluginLLMCore::ToolUseContent *> getCurrentToolUseContent() const;
QList<PluginLLMCore::ThinkingContent *> getCurrentThinkingContent() const;
QList<PluginLLMCore::ContentBlock *> 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<PluginLLMCore::ContentBlock *> m_currentBlocks;
QString m_pendingFunctionArgs;
QString m_currentFunctionName;
QString m_finishReason;
PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building;
};
} // namespace QodeAssist::Providers

View File

@@ -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 <QJsonArray>
#include <QJsonDocument>
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<PluginLLMCore::ToolUseContent>(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<PluginLLMCore::TextContent *>(*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<PluginLLMCore::TextContent *>(block)) {
LOG_MESSAGE(QString("OllamaMessage: Removing TextContent block (tool call detected)"));
}
}
m_currentBlocks.clear();
addCurrentContent<PluginLLMCore::ToolUseContent>(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<PluginLLMCore::TextContent *>(block)) {
textContent += text->text();
} else if (auto tool = qobject_cast<PluginLLMCore::ToolUseContent *>(block)) {
QJsonObject toolCall;
toolCall["type"] = "function";
toolCall["function"] = QJsonObject{{"name", tool->name()}, {"arguments", tool->input()}};
toolCalls.append(toolCall);
} else if (auto thinking = qobject_cast<PluginLLMCore::ThinkingContent *>(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<QString, QString> &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<PluginLLMCore::ToolUseContent *> OllamaMessage::getCurrentToolUseContent() const
{
QList<PluginLLMCore::ToolUseContent *> toolBlocks;
for (auto block : m_currentBlocks) {
if (auto toolContent = qobject_cast<PluginLLMCore::ToolUseContent *>(block)) {
toolBlocks.append(toolContent);
}
}
return toolBlocks;
}
QList<PluginLLMCore::ThinkingContent *> OllamaMessage::getCurrentThinkingContent() const
{
QList<PluginLLMCore::ThinkingContent *> thinkingBlocks;
for (auto block : m_currentBlocks) {
if (auto thinkingContent = qobject_cast<PluginLLMCore::ThinkingContent *>(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<PluginLLMCore::TextContent *>(block)) {
return textContent;
}
}
return addCurrentContent<PluginLLMCore::TextContent>();
}
PluginLLMCore::ThinkingContent *OllamaMessage::getOrCreateThinkingContent()
{
if (m_currentThinkingContent) {
return m_currentThinkingContent;
}
for (auto block : m_currentBlocks) {
if (auto thinkingContent = qobject_cast<PluginLLMCore::ThinkingContent *>(block)) {
m_currentThinkingContent = thinkingContent;
return m_currentThinkingContent;
}
}
m_currentThinkingContent = addCurrentContent<PluginLLMCore::ThinkingContent>();
LOG_MESSAGE(QString("OllamaMessage: Created new ThinkingContent block"));
return m_currentThinkingContent;
}
} // namespace QodeAssist::Providers

View File

@@ -1,56 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <pluginllmcore/ContentBlocks.hpp>
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<QString, QString> &toolResults) const;
PluginLLMCore::MessageState state() const { return m_state; }
QList<PluginLLMCore::ToolUseContent *> getCurrentToolUseContent() const;
QList<PluginLLMCore::ThinkingContent *> getCurrentThinkingContent() const;
QList<PluginLLMCore::ContentBlock *> currentBlocks() const { return m_currentBlocks; }
void startNewContinuation();
private:
bool m_done = false;
PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building;
QList<PluginLLMCore::ContentBlock *> 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<typename T, typename... Args>
T *addCurrentContent(Args &&...args)
{
T *content = new T(std::forward<Args>(args)...);
content->setParent(this);
m_currentBlocks.append(content);
return content;
}
};
} // namespace QodeAssist::Providers

View File

@@ -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 <QJsonArray>
#include <QJsonDocument>
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<PluginLLMCore::ToolUseContent *>(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<PluginLLMCore::TextContent *>(block)) {
textContent += text->text();
} else if (auto tool = qobject_cast<PluginLLMCore::ToolUseContent *>(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<QString, QString> &toolResults) const
{
QJsonArray messages;
for (auto toolContent : getCurrentToolUseContent()) {
if (toolResults.contains(toolContent->id())) {
auto toolResult = std::make_unique<PluginLLMCore::ToolResultContent>(
toolContent->id(), toolResults[toolContent->id()]);
messages.append(toolResult->toJson(PluginLLMCore::ProviderFormat::OpenAI));
}
}
return messages;
}
QList<PluginLLMCore::ToolUseContent *> OpenAIMessage::getCurrentToolUseContent() const
{
QList<PluginLLMCore::ToolUseContent *> toolBlocks;
for (auto block : m_currentBlocks) {
if (auto toolContent = qobject_cast<PluginLLMCore::ToolUseContent *>(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<PluginLLMCore::TextContent *>(block)) {
return textContent;
}
}
return addCurrentContent<PluginLLMCore::TextContent>();
}
} // namespace QodeAssist::Providers

View File

@@ -1,49 +0,0 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <pluginllmcore/ContentBlocks.hpp>
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<QString, QString> &toolResults) const;
PluginLLMCore::MessageState state() const { return m_state; }
QList<PluginLLMCore::ToolUseContent *> getCurrentToolUseContent() const;
void startNewContinuation();
private:
QString m_finishReason;
PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building;
QList<PluginLLMCore::ContentBlock *> m_currentBlocks;
QHash<int, QString> m_pendingToolArguments;
void updateStateFromFinishReason();
PluginLLMCore::TextContent *getOrCreateTextContent();
template<typename T, typename... Args>
T *addCurrentContent(Args &&...args)
{
T *content = new T(std::forward<Args>(args)...);
content->setParent(this);
m_currentBlocks.append(content);
return content;
}
};
} // namespace QodeAssist::Providers

View File

@@ -1,38 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QString>
namespace QodeAssist::OpenAIResponses {
struct CancelResponseRequest
{
QString responseId;
QString buildUrl(const QString &baseUrl) const
{
return QString("%1/v1/responses/%2/cancel").arg(baseUrl, responseId);
}
bool isValid() const { return !responseId.isEmpty(); }
};
class CancelResponseRequestBuilder
{
public:
CancelResponseRequestBuilder &setResponseId(const QString &id)
{
m_request.responseId = id;
return *this;
}
CancelResponseRequest build() const { return m_request; }
private:
CancelResponseRequest m_request;
};
} // namespace QodeAssist::OpenAIResponses

View File

@@ -1,53 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QJsonObject>
#include <QString>
namespace QodeAssist::OpenAIResponses {
struct DeleteResponseRequest
{
QString responseId;
QString buildUrl(const QString &baseUrl) const
{
return QString("%1/v1/responses/%2").arg(baseUrl, responseId);
}
bool isValid() const { return !responseId.isEmpty(); }
};
class DeleteResponseRequestBuilder
{
public:
DeleteResponseRequestBuilder &setResponseId(const QString &id)
{
m_request.responseId = id;
return *this;
}
DeleteResponseRequest build() const { return m_request; }
private:
DeleteResponseRequest m_request;
};
struct DeleteResponseResult
{
bool success = false;
QString message;
static DeleteResponseResult fromJson(const QJsonObject &obj)
{
DeleteResponseResult result;
result.success = obj["success"].toBool();
result.message = obj["message"].toString();
return result;
}
};
} // namespace QodeAssist::OpenAIResponses

View File

@@ -1,104 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QJsonObject>
#include <QString>
#include <QStringList>
#include <optional>
namespace QodeAssist::OpenAIResponses {
struct GetResponseRequest
{
QString responseId;
std::optional<QStringList> include;
std::optional<bool> includeObfuscation;
std::optional<int> startingAfter;
std::optional<bool> stream;
QString buildUrl(const QString &baseUrl) const
{
QString url = QString("%1/v1/responses/%2").arg(baseUrl, responseId);
QStringList queryParams;
if (include && !include->isEmpty()) {
for (const auto &item : *include) {
queryParams.append(QString("include=%1").arg(item));
}
}
if (includeObfuscation) {
queryParams.append(
QString("include_obfuscation=%1").arg(*includeObfuscation ? "true" : "false"));
}
if (startingAfter) {
queryParams.append(QString("starting_after=%1").arg(*startingAfter));
}
if (stream) {
queryParams.append(QString("stream=%1").arg(*stream ? "true" : "false"));
}
if (!queryParams.isEmpty()) {
url += "?" + queryParams.join("&");
}
return url;
}
bool isValid() const { return !responseId.isEmpty(); }
};
class GetResponseRequestBuilder
{
public:
GetResponseRequestBuilder &setResponseId(const QString &id)
{
m_request.responseId = id;
return *this;
}
GetResponseRequestBuilder &setInclude(const QStringList &include)
{
m_request.include = include;
return *this;
}
GetResponseRequestBuilder &addInclude(const QString &item)
{
if (!m_request.include) {
m_request.include = QStringList();
}
m_request.include->append(item);
return *this;
}
GetResponseRequestBuilder &setIncludeObfuscation(bool enabled)
{
m_request.includeObfuscation = enabled;
return *this;
}
GetResponseRequestBuilder &setStartingAfter(int sequence)
{
m_request.startingAfter = sequence;
return *this;
}
GetResponseRequestBuilder &setStream(bool enabled)
{
m_request.stream = enabled;
return *this;
}
GetResponseRequest build() const { return m_request; }
private:
GetResponseRequest m_request;
};
} // namespace QodeAssist::OpenAIResponses

View File

@@ -1,203 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "ModelRequest.hpp"
#include <QJsonObject>
#include <QString>
namespace QodeAssist::OpenAIResponses {
struct InputTokensRequest
{
std::optional<QString> conversation;
std::optional<QJsonArray> input;
std::optional<QString> instructions;
std::optional<QString> model;
std::optional<bool> parallelToolCalls;
std::optional<QString> previousResponseId;
std::optional<QJsonObject> reasoning;
std::optional<QJsonObject> text;
std::optional<QJsonValue> toolChoice;
std::optional<QJsonArray> tools;
std::optional<QString> truncation;
QString buildUrl(const QString &baseUrl) const
{
return QString("%1/v1/responses/input_tokens").arg(baseUrl);
}
QJsonObject toJson() const
{
QJsonObject obj;
if (conversation)
obj["conversation"] = *conversation;
if (input)
obj["input"] = *input;
if (instructions)
obj["instructions"] = *instructions;
if (model)
obj["model"] = *model;
if (parallelToolCalls)
obj["parallel_tool_calls"] = *parallelToolCalls;
if (previousResponseId)
obj["previous_response_id"] = *previousResponseId;
if (reasoning)
obj["reasoning"] = *reasoning;
if (text)
obj["text"] = *text;
if (toolChoice)
obj["tool_choice"] = *toolChoice;
if (tools)
obj["tools"] = *tools;
if (truncation)
obj["truncation"] = *truncation;
return obj;
}
bool isValid() const { return input.has_value() || previousResponseId.has_value(); }
};
class InputTokensRequestBuilder
{
public:
InputTokensRequestBuilder &setConversation(const QString &conversationId)
{
m_request.conversation = conversationId;
return *this;
}
InputTokensRequestBuilder &setInput(const QJsonArray &input)
{
m_request.input = input;
return *this;
}
InputTokensRequestBuilder &addInputMessage(const Message &message)
{
if (!m_request.input) {
m_request.input = QJsonArray();
}
m_request.input->append(message.toJson());
return *this;
}
InputTokensRequestBuilder &setInstructions(const QString &instructions)
{
m_request.instructions = instructions;
return *this;
}
InputTokensRequestBuilder &setModel(const QString &model)
{
m_request.model = model;
return *this;
}
InputTokensRequestBuilder &setParallelToolCalls(bool enabled)
{
m_request.parallelToolCalls = enabled;
return *this;
}
InputTokensRequestBuilder &setPreviousResponseId(const QString &responseId)
{
m_request.previousResponseId = responseId;
return *this;
}
InputTokensRequestBuilder &setReasoning(const QJsonObject &reasoning)
{
m_request.reasoning = reasoning;
return *this;
}
InputTokensRequestBuilder &setReasoningEffort(ReasoningEffort effort)
{
QString effortStr;
switch (effort) {
case ReasoningEffort::None:
effortStr = "none";
break;
case ReasoningEffort::Minimal:
effortStr = "minimal";
break;
case ReasoningEffort::Low:
effortStr = "low";
break;
case ReasoningEffort::Medium:
effortStr = "medium";
break;
case ReasoningEffort::High:
effortStr = "high";
break;
}
m_request.reasoning = QJsonObject{{"effort", effortStr}};
return *this;
}
InputTokensRequestBuilder &setText(const QJsonObject &text)
{
m_request.text = text;
return *this;
}
InputTokensRequestBuilder &setTextFormat(const TextFormatOptions &format)
{
m_request.text = format.toJson();
return *this;
}
InputTokensRequestBuilder &setToolChoice(const QJsonValue &toolChoice)
{
m_request.toolChoice = toolChoice;
return *this;
}
InputTokensRequestBuilder &setTools(const QJsonArray &tools)
{
m_request.tools = tools;
return *this;
}
InputTokensRequestBuilder &addTool(const Tool &tool)
{
if (!m_request.tools) {
m_request.tools = QJsonArray();
}
m_request.tools->append(tool.toJson());
return *this;
}
InputTokensRequestBuilder &setTruncation(const QString &truncation)
{
m_request.truncation = truncation;
return *this;
}
InputTokensRequest build() const { return m_request; }
private:
InputTokensRequest m_request;
};
struct InputTokensResponse
{
QString object;
int inputTokens = 0;
static InputTokensResponse fromJson(const QJsonObject &obj)
{
InputTokensResponse result;
result.object = obj["object"].toString();
result.inputTokens = obj["input_tokens"].toInt();
return result;
}
};
} // namespace QodeAssist::OpenAIResponses

View File

@@ -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

View File

@@ -1,150 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QStringList>
#include <optional>
namespace QodeAssist::OpenAIResponses {
enum class SortOrder { Ascending, Descending };
struct ListInputItemsRequest
{
QString responseId;
std::optional<QString> after;
std::optional<QStringList> include;
std::optional<int> limit;
std::optional<SortOrder> order;
QString buildUrl(const QString &baseUrl) const
{
QString url = QString("%1/v1/responses/%2/input_items").arg(baseUrl, responseId);
QStringList queryParams;
if (after) {
queryParams.append(QString("after=%1").arg(*after));
}
if (include && !include->isEmpty()) {
for (const auto &item : *include) {
queryParams.append(QString("include=%1").arg(item));
}
}
if (limit) {
queryParams.append(QString("limit=%1").arg(*limit));
}
if (order) {
QString orderStr = (*order == SortOrder::Ascending) ? "asc" : "desc";
queryParams.append(QString("order=%1").arg(orderStr));
}
if (!queryParams.isEmpty()) {
url += "?" + queryParams.join("&");
}
return url;
}
bool isValid() const
{
if (responseId.isEmpty()) {
return false;
}
if (limit && (*limit < 1 || *limit > 100)) {
return false;
}
return true;
}
};
class ListInputItemsRequestBuilder
{
public:
ListInputItemsRequestBuilder &setResponseId(const QString &id)
{
m_request.responseId = id;
return *this;
}
ListInputItemsRequestBuilder &setAfter(const QString &itemId)
{
m_request.after = itemId;
return *this;
}
ListInputItemsRequestBuilder &setInclude(const QStringList &include)
{
m_request.include = include;
return *this;
}
ListInputItemsRequestBuilder &addInclude(const QString &item)
{
if (!m_request.include) {
m_request.include = QStringList();
}
m_request.include->append(item);
return *this;
}
ListInputItemsRequestBuilder &setLimit(int limit)
{
m_request.limit = limit;
return *this;
}
ListInputItemsRequestBuilder &setOrder(SortOrder order)
{
m_request.order = order;
return *this;
}
ListInputItemsRequestBuilder &setAscendingOrder()
{
m_request.order = SortOrder::Ascending;
return *this;
}
ListInputItemsRequestBuilder &setDescendingOrder()
{
m_request.order = SortOrder::Descending;
return *this;
}
ListInputItemsRequest build() const { return m_request; }
private:
ListInputItemsRequest m_request;
};
struct ListInputItemsResponse
{
QJsonArray data;
QString firstId;
QString lastId;
bool hasMore = false;
QString object;
static ListInputItemsResponse fromJson(const QJsonObject &obj)
{
ListInputItemsResponse result;
result.data = obj["data"].toArray();
result.firstId = obj["first_id"].toString();
result.lastId = obj["last_id"].toString();
result.hasMore = obj["has_more"].toBool();
result.object = obj["object"].toString();
return result;
}
};
} // namespace QodeAssist::OpenAIResponses

View File

@@ -1,338 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVariant>
#include <optional>
#include <variant>
namespace QodeAssist::OpenAIResponses {
enum class Role { User, Assistant, System, Developer };
enum class MessageStatus { InProgress, Completed, Incomplete };
enum class ReasoningEffort { None, Minimal, Low, Medium, High };
enum class TextFormat { Text, JsonSchema, JsonObject };
struct InputText
{
QString text;
QJsonObject toJson() const
{
return QJsonObject{{"type", "input_text"}, {"text", text}};
}
bool isValid() const noexcept { return !text.isEmpty(); }
};
struct InputImage
{
std::optional<QString> fileId;
std::optional<QString> imageUrl;
QString detail = "auto";
QJsonObject toJson() const
{
QJsonObject obj{{"type", "input_image"}, {"detail", detail}};
if (fileId)
obj["file_id"] = *fileId;
if (imageUrl)
obj["image_url"] = *imageUrl;
return obj;
}
bool isValid() const noexcept { return fileId.has_value() || imageUrl.has_value(); }
};
struct InputFile
{
std::optional<QString> fileId;
std::optional<QString> fileUrl;
std::optional<QString> fileData;
std::optional<QString> filename;
QJsonObject toJson() const
{
QJsonObject obj{{"type", "input_file"}};
if (fileId)
obj["file_id"] = *fileId;
if (fileUrl)
obj["file_url"] = *fileUrl;
if (fileData)
obj["file_data"] = *fileData;
if (filename)
obj["filename"] = *filename;
return obj;
}
bool isValid() const noexcept
{
return fileId.has_value() || fileUrl.has_value() || fileData.has_value();
}
};
class MessageContent
{
public:
MessageContent(QString text) : m_variant(std::move(text)) {}
MessageContent(InputText text) : m_variant(std::move(text)) {}
MessageContent(InputImage image) : m_variant(std::move(image)) {}
MessageContent(InputFile file) : m_variant(std::move(file)) {}
QJsonValue toJson() const
{
return std::visit([](const auto &content) -> QJsonValue {
using T = std::decay_t<decltype(content)>;
if constexpr (std::is_same_v<T, QString>) {
return content;
} else {
return content.toJson();
}
}, m_variant);
}
bool isValid() const noexcept
{
return std::visit([](const auto &content) -> bool {
using T = std::decay_t<decltype(content)>;
if constexpr (std::is_same_v<T, QString>) {
return !content.isEmpty();
} else {
return content.isValid();
}
}, m_variant);
}
private:
std::variant<QString, InputText, InputImage, InputFile> m_variant;
};
struct Message
{
Role role;
QList<MessageContent> content;
std::optional<MessageStatus> status;
QJsonObject toJson() const
{
QJsonObject obj;
obj["role"] = roleToString(role);
if (content.size() == 1) {
obj["content"] = content[0].toJson();
} else {
QJsonArray arr;
for (const auto &c : content) {
arr.append(c.toJson());
}
obj["content"] = arr;
}
if (status) {
obj["status"] = statusToString(*status);
}
return obj;
}
bool isValid() const noexcept
{
if (content.isEmpty()) {
return false;
}
for (const auto &c : content) {
if (!c.isValid()) {
return false;
}
}
return true;
}
static QString roleToString(Role r) noexcept
{
switch (r) {
case Role::User:
return "user";
case Role::Assistant:
return "assistant";
case Role::System:
return "system";
case Role::Developer:
return "developer";
}
return "user";
}
static QString statusToString(MessageStatus s) noexcept
{
switch (s) {
case MessageStatus::InProgress:
return "in_progress";
case MessageStatus::Completed:
return "completed";
case MessageStatus::Incomplete:
return "incomplete";
}
return "in_progress";
}
};
struct FunctionTool
{
QString name;
QJsonObject parameters;
std::optional<QString> description;
bool strict = true;
QJsonObject toJson() const
{
QJsonObject obj{{"type", "function"},
{"name", name},
{"parameters", parameters},
{"strict", strict}};
if (description)
obj["description"] = *description;
return obj;
}
bool isValid() const noexcept
{
return !name.isEmpty() && !parameters.isEmpty();
}
};
struct FileSearchTool
{
QStringList vectorStoreIds;
std::optional<int> maxNumResults;
std::optional<double> scoreThreshold;
QJsonObject toJson() const
{
QJsonObject obj{{"type", "file_search"}};
QJsonArray ids;
for (const auto &id : vectorStoreIds) {
ids.append(id);
}
obj["vector_store_ids"] = ids;
if (maxNumResults)
obj["max_num_results"] = *maxNumResults;
if (scoreThreshold)
obj["score_threshold"] = *scoreThreshold;
return obj;
}
bool isValid() const noexcept
{
return !vectorStoreIds.isEmpty();
}
};
struct WebSearchTool
{
QString searchContextSize = "medium";
QJsonObject toJson() const
{
return QJsonObject{{"type", "web_search"}, {"search_context_size", searchContextSize}};
}
bool isValid() const noexcept
{
return !searchContextSize.isEmpty();
}
};
struct CodeInterpreterTool
{
QString container;
QJsonObject toJson() const
{
return QJsonObject{{"type", "code_interpreter"}, {"container", container}};
}
bool isValid() const noexcept
{
return !container.isEmpty();
}
};
class Tool
{
public:
Tool(FunctionTool tool) : m_variant(std::move(tool)) {}
Tool(FileSearchTool tool) : m_variant(std::move(tool)) {}
Tool(WebSearchTool tool) : m_variant(std::move(tool)) {}
Tool(CodeInterpreterTool tool) : m_variant(std::move(tool)) {}
QJsonObject toJson() const
{
return std::visit([](const auto &t) { return t.toJson(); }, m_variant);
}
bool isValid() const noexcept
{
return std::visit([](const auto &t) { return t.isValid(); }, m_variant);
}
private:
std::variant<FunctionTool, FileSearchTool, WebSearchTool, CodeInterpreterTool> m_variant;
};
struct TextFormatOptions
{
TextFormat type = TextFormat::Text;
std::optional<QString> name;
std::optional<QJsonObject> schema;
std::optional<QString> description;
std::optional<bool> strict;
QJsonObject toJson() const
{
QJsonObject obj;
switch (type) {
case TextFormat::Text:
obj["type"] = "text";
break;
case TextFormat::JsonSchema:
obj["type"] = "json_schema";
if (name)
obj["name"] = *name;
if (schema)
obj["schema"] = *schema;
if (description)
obj["description"] = *description;
if (strict)
obj["strict"] = *strict;
break;
case TextFormat::JsonObject:
obj["type"] = "json_object";
break;
}
return obj;
}
bool isValid() const noexcept
{
if (type == TextFormat::JsonSchema) {
return name.has_value() && schema.has_value();
}
return true;
}
};
} // namespace QodeAssist::OpenAIResponses

View File

@@ -1,546 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <optional>
#include <variant>
#include <QJsonArray>
#include <QJsonObject>
#include <QList>
#include <QString>
namespace QodeAssist::OpenAIResponses {
enum class ResponseStatus { Completed, Failed, InProgress, Cancelled, Queued, Incomplete };
enum class ItemStatus { InProgress, Completed, Incomplete };
struct FileCitation
{
QString fileId;
QString filename;
int index = 0;
static FileCitation fromJson(const QJsonObject &obj)
{
return {obj["file_id"].toString(), obj["filename"].toString(), obj["index"].toInt()};
}
bool isValid() const noexcept { return !fileId.isEmpty(); }
};
struct UrlCitation
{
QString url;
QString title;
int startIndex = 0;
int endIndex = 0;
static UrlCitation fromJson(const QJsonObject &obj)
{
return {
obj["url"].toString(),
obj["title"].toString(),
obj["start_index"].toInt(),
obj["end_index"].toInt()};
}
bool isValid() const noexcept { return !url.isEmpty(); }
};
struct OutputText
{
QString text;
QList<FileCitation> fileCitations;
QList<UrlCitation> urlCitations;
static OutputText fromJson(const QJsonObject &obj)
{
OutputText result;
result.text = obj["text"].toString();
if (obj.contains("annotations")) {
const QJsonArray annotations = obj["annotations"].toArray();
result.fileCitations.reserve(annotations.size());
result.urlCitations.reserve(annotations.size());
for (const auto &annValue : annotations) {
const QJsonObject ann = annValue.toObject();
const QString type = ann["type"].toString();
if (type == "file_citation") {
result.fileCitations.append(FileCitation::fromJson(ann));
} else if (type == "url_citation") {
result.urlCitations.append(UrlCitation::fromJson(ann));
}
}
}
return result;
}
bool isValid() const noexcept { return !text.isEmpty(); }
};
struct Refusal
{
QString refusal;
static Refusal fromJson(const QJsonObject &obj)
{
return {obj["refusal"].toString()};
}
bool isValid() const noexcept { return !refusal.isEmpty(); }
};
struct MessageOutput
{
QString id;
QString role;
ItemStatus status = ItemStatus::InProgress;
QList<OutputText> outputTexts;
QList<Refusal> refusals;
static MessageOutput fromJson(const QJsonObject &obj)
{
MessageOutput result;
result.id = obj["id"].toString();
result.role = obj["role"].toString();
const QString statusStr = obj["status"].toString();
if (statusStr == "in_progress")
result.status = ItemStatus::InProgress;
else if (statusStr == "completed")
result.status = ItemStatus::Completed;
else
result.status = ItemStatus::Incomplete;
if (obj.contains("content")) {
const QJsonArray content = obj["content"].toArray();
result.outputTexts.reserve(content.size());
result.refusals.reserve(content.size());
for (const auto &item : content) {
const QJsonObject itemObj = item.toObject();
const QString type = itemObj["type"].toString();
if (type == "output_text") {
result.outputTexts.append(OutputText::fromJson(itemObj));
} else if (type == "refusal") {
result.refusals.append(Refusal::fromJson(itemObj));
}
}
}
return result;
}
bool isValid() const noexcept { return !id.isEmpty(); }
bool hasContent() const noexcept { return !outputTexts.isEmpty() || !refusals.isEmpty(); }
};
struct FunctionCall
{
QString id;
QString callId;
QString name;
QString arguments;
ItemStatus status = ItemStatus::InProgress;
static FunctionCall fromJson(const QJsonObject &obj)
{
FunctionCall result;
result.id = obj["id"].toString();
result.callId = obj["call_id"].toString();
result.name = obj["name"].toString();
result.arguments = obj["arguments"].toString();
const QString statusStr = obj["status"].toString();
if (statusStr == "in_progress")
result.status = ItemStatus::InProgress;
else if (statusStr == "completed")
result.status = ItemStatus::Completed;
else
result.status = ItemStatus::Incomplete;
return result;
}
bool isValid() const noexcept { return !id.isEmpty() && !callId.isEmpty() && !name.isEmpty(); }
};
struct ReasoningOutput
{
QString id;
ItemStatus status = ItemStatus::InProgress;
QString summaryText;
QString encryptedContent;
QList<QString> contentTexts;
static ReasoningOutput fromJson(const QJsonObject &obj)
{
ReasoningOutput result;
result.id = obj["id"].toString();
const QString statusStr = obj["status"].toString();
if (statusStr == "in_progress")
result.status = ItemStatus::InProgress;
else if (statusStr == "completed")
result.status = ItemStatus::Completed;
else
result.status = ItemStatus::Incomplete;
if (obj.contains("summary")) {
const QJsonArray summary = obj["summary"].toArray();
for (const auto &item : summary) {
const QJsonObject itemObj = item.toObject();
if (itemObj["type"].toString() == "summary_text") {
result.summaryText = itemObj["text"].toString();
break;
}
}
}
if (obj.contains("content")) {
const QJsonArray content = obj["content"].toArray();
result.contentTexts.reserve(content.size());
for (const auto &item : content) {
const QJsonObject itemObj = item.toObject();
if (itemObj["type"].toString() == "reasoning_text") {
result.contentTexts.append(itemObj["text"].toString());
}
}
}
if (obj.contains("encrypted_content")) {
result.encryptedContent = obj["encrypted_content"].toString();
}
return result;
}
bool isValid() const noexcept { return !id.isEmpty(); }
bool hasContent() const noexcept
{
return !summaryText.isEmpty() || !contentTexts.isEmpty() || !encryptedContent.isEmpty();
}
};
struct FileSearchResult
{
QString fileId;
QString filename;
QString text;
double score = 0.0;
static FileSearchResult fromJson(const QJsonObject &obj)
{
return {
obj["file_id"].toString(),
obj["filename"].toString(),
obj["text"].toString(),
obj["score"].toDouble()};
}
bool isValid() const noexcept { return !fileId.isEmpty(); }
};
struct FileSearchCall
{
QString id;
QString status;
QStringList queries;
QList<FileSearchResult> results;
static FileSearchCall fromJson(const QJsonObject &obj)
{
FileSearchCall result;
result.id = obj["id"].toString();
result.status = obj["status"].toString();
if (obj.contains("queries")) {
const QJsonArray queries = obj["queries"].toArray();
result.queries.reserve(queries.size());
for (const auto &q : queries) {
result.queries.append(q.toString());
}
}
if (obj.contains("results")) {
const QJsonArray results = obj["results"].toArray();
result.results.reserve(results.size());
for (const auto &r : results) {
result.results.append(FileSearchResult::fromJson(r.toObject()));
}
}
return result;
}
bool isValid() const noexcept { return !id.isEmpty(); }
};
struct CodeInterpreterOutput
{
QString type;
QString logs;
QString imageUrl;
static CodeInterpreterOutput fromJson(const QJsonObject &obj)
{
CodeInterpreterOutput result;
result.type = obj["type"].toString();
if (result.type == "logs") {
result.logs = obj["logs"].toString();
} else if (result.type == "image") {
result.imageUrl = obj["url"].toString();
}
return result;
}
bool isValid() const noexcept
{
return !type.isEmpty() && (!logs.isEmpty() || !imageUrl.isEmpty());
}
};
struct CodeInterpreterCall
{
QString id;
QString containerId;
std::optional<QString> code;
QString status;
QList<CodeInterpreterOutput> outputs;
static CodeInterpreterCall fromJson(const QJsonObject &obj)
{
CodeInterpreterCall result;
result.id = obj["id"].toString();
result.containerId = obj["container_id"].toString();
result.status = obj["status"].toString();
if (obj.contains("code") && !obj["code"].isNull()) {
result.code = obj["code"].toString();
}
if (obj.contains("outputs")) {
const QJsonArray outputs = obj["outputs"].toArray();
result.outputs.reserve(outputs.size());
for (const auto &o : outputs) {
result.outputs.append(CodeInterpreterOutput::fromJson(o.toObject()));
}
}
return result;
}
bool isValid() const noexcept { return !id.isEmpty() && !containerId.isEmpty(); }
};
class OutputItem
{
public:
enum class Type { Message, FunctionCall, Reasoning, FileSearch, CodeInterpreter, Unknown };
explicit OutputItem(const MessageOutput &msg)
: m_type(Type::Message)
, m_data(msg)
{}
explicit OutputItem(const FunctionCall &call)
: m_type(Type::FunctionCall)
, m_data(call)
{}
explicit OutputItem(const ReasoningOutput &reasoning)
: m_type(Type::Reasoning)
, m_data(reasoning)
{}
explicit OutputItem(const FileSearchCall &search)
: m_type(Type::FileSearch)
, m_data(search)
{}
explicit OutputItem(const CodeInterpreterCall &interpreter)
: m_type(Type::CodeInterpreter)
, m_data(interpreter)
{}
Type type() const { return m_type; }
const MessageOutput *asMessage() const
{
return std::holds_alternative<MessageOutput>(m_data) ? &std::get<MessageOutput>(m_data)
: nullptr;
}
const FunctionCall *asFunctionCall() const
{
return std::holds_alternative<FunctionCall>(m_data) ? &std::get<FunctionCall>(m_data)
: nullptr;
}
const ReasoningOutput *asReasoning() const
{
return std::holds_alternative<ReasoningOutput>(m_data) ? &std::get<ReasoningOutput>(m_data)
: nullptr;
}
const FileSearchCall *asFileSearch() const
{
return std::holds_alternative<FileSearchCall>(m_data) ? &std::get<FileSearchCall>(m_data)
: nullptr;
}
const CodeInterpreterCall *asCodeInterpreter() const
{
return std::holds_alternative<CodeInterpreterCall>(m_data)
? &std::get<CodeInterpreterCall>(m_data)
: nullptr;
}
static OutputItem fromJson(const QJsonObject &obj)
{
const QString type = obj["type"].toString();
if (type == "message") {
return OutputItem(MessageOutput::fromJson(obj));
} else if (type == "function_call") {
return OutputItem(FunctionCall::fromJson(obj));
} else if (type == "reasoning") {
return OutputItem(ReasoningOutput::fromJson(obj));
} else if (type == "file_search_call") {
return OutputItem(FileSearchCall::fromJson(obj));
} else if (type == "code_interpreter_call") {
return OutputItem(CodeInterpreterCall::fromJson(obj));
}
return OutputItem(MessageOutput{});
}
private:
Type m_type;
std::variant<MessageOutput, FunctionCall, ReasoningOutput, FileSearchCall, CodeInterpreterCall>
m_data;
};
struct Usage
{
int inputTokens = 0;
int outputTokens = 0;
int totalTokens = 0;
static Usage fromJson(const QJsonObject &obj)
{
return {
obj["input_tokens"].toInt(),
obj["output_tokens"].toInt(),
obj["total_tokens"].toInt()
};
}
bool isValid() const noexcept { return totalTokens > 0; }
};
struct ResponseError
{
QString code;
QString message;
static ResponseError fromJson(const QJsonObject &obj)
{
return {obj["code"].toString(), obj["message"].toString()};
}
bool isValid() const noexcept { return !code.isEmpty() && !message.isEmpty(); }
};
struct Response
{
QString id;
qint64 createdAt = 0;
QString model;
ResponseStatus status = ResponseStatus::InProgress;
QList<OutputItem> output;
QString outputText;
std::optional<Usage> usage;
std::optional<ResponseError> error;
std::optional<QString> conversationId;
static Response fromJson(const QJsonObject &obj)
{
Response result;
result.id = obj["id"].toString();
result.createdAt = obj["created_at"].toInteger();
result.model = obj["model"].toString();
const QString statusStr = obj["status"].toString();
if (statusStr == "completed")
result.status = ResponseStatus::Completed;
else if (statusStr == "failed")
result.status = ResponseStatus::Failed;
else if (statusStr == "in_progress")
result.status = ResponseStatus::InProgress;
else if (statusStr == "cancelled")
result.status = ResponseStatus::Cancelled;
else if (statusStr == "queued")
result.status = ResponseStatus::Queued;
else
result.status = ResponseStatus::Incomplete;
if (obj.contains("output")) {
const QJsonArray output = obj["output"].toArray();
result.output.reserve(output.size());
for (const auto &item : output) {
result.output.append(OutputItem::fromJson(item.toObject()));
}
}
if (obj.contains("output_text")) {
result.outputText = obj["output_text"].toString();
}
if (obj.contains("usage")) {
result.usage = Usage::fromJson(obj["usage"].toObject());
}
if (obj.contains("error")) {
result.error = ResponseError::fromJson(obj["error"].toObject());
}
if (obj.contains("conversation")) {
const QJsonObject conv = obj["conversation"].toObject();
result.conversationId = conv["id"].toString();
}
return result;
}
QString getAggregatedText() const
{
if (!outputText.isEmpty()) {
return outputText;
}
QString aggregated;
for (const auto &item : output) {
if (const auto *msg = item.asMessage()) {
for (const auto &text : msg->outputTexts) {
aggregated += text.text;
}
}
}
return aggregated;
}
bool isValid() const noexcept { return !id.isEmpty(); }
bool hasError() const noexcept { return error.has_value(); }
bool isCompleted() const noexcept { return status == ResponseStatus::Completed; }
bool isFailed() const noexcept { return status == ResponseStatus::Failed; }
};
} // namespace QodeAssist::OpenAIResponses

View File

@@ -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 <QJsonArray>
namespace QodeAssist::Providers {
OpenAIResponsesMessage::OpenAIResponsesMessage(QObject *parent)
: QObject(parent)
{}
void OpenAIResponsesMessage::handleItemDelta(const QJsonObject &item)
{
using namespace QodeAssist::OpenAIResponses;
const QString itemType = item["type"].toString();
if (itemType == "message" || (itemType.isEmpty() && item.contains("content"))) {
OutputItem outputItem = OutputItem::fromJson(item);
if (const auto *msg = outputItem.asMessage()) {
for (const auto &outputText : msg->outputTexts) {
if (!outputText.text.isEmpty()) {
auto textItem = getOrCreateTextItem();
textItem->appendText(outputText.text);
}
}
}
}
}
void OpenAIResponsesMessage::handleToolCallStart(const QString &callId, const QString &name)
{
auto toolContent = new 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<QJsonObject> OpenAIResponsesMessage::toItemsFormat() const
{
QList<QJsonObject> items;
QString textContent;
QList<PluginLLMCore::ToolUseContent *> toolCalls;
for (const auto *block : m_items) {
if (const auto *text = qobject_cast<const PluginLLMCore::TextContent *>(block)) {
textContent += text->text();
} else if (auto *tool = qobject_cast<PluginLLMCore::ToolUseContent *>(
const_cast<PluginLLMCore::ContentBlock *>(block))) {
toolCalls.append(tool);
}
}
if (!textContent.isEmpty()) {
QJsonObject message;
message["role"] = "assistant";
message["content"] = textContent;
items.append(message);
}
for (const auto *tool : toolCalls) {
QJsonObject functionCallItem;
functionCallItem["type"] = "function_call";
functionCallItem["call_id"] = tool->id();
functionCallItem["name"] = tool->name();
functionCallItem["arguments"] = QString::fromUtf8(
QJsonDocument(tool->input()).toJson(QJsonDocument::Compact));
items.append(functionCallItem);
}
return items;
}
QList<PluginLLMCore::ToolUseContent *> OpenAIResponsesMessage::getCurrentToolUseContent() const
{
QList<PluginLLMCore::ToolUseContent *> toolBlocks;
for (auto *block : m_items) {
if (auto *toolContent = qobject_cast<PluginLLMCore::ToolUseContent *>(block)) {
toolBlocks.append(toolContent);
}
}
return toolBlocks;
}
QList<PluginLLMCore::ThinkingContent *> OpenAIResponsesMessage::getCurrentThinkingContent() const
{
QList<PluginLLMCore::ThinkingContent *> thinkingBlocks;
for (auto *block : m_items) {
if (auto *thinkingContent = qobject_cast<PluginLLMCore::ThinkingContent *>(block)) {
thinkingBlocks.append(thinkingContent);
}
}
return thinkingBlocks;
}
QJsonArray OpenAIResponsesMessage::createToolResultItems(const QHash<QString, QString> &toolResults) const
{
QJsonArray items;
for (const auto *toolContent : getCurrentToolUseContent()) {
if (toolResults.contains(toolContent->id())) {
QJsonObject toolResultItem;
toolResultItem["type"] = "function_call_output";
toolResultItem["call_id"] = toolContent->id();
toolResultItem["output"] = toolResults[toolContent->id()];
items.append(toolResultItem);
}
}
return items;
}
QString OpenAIResponsesMessage::accumulatedText() const
{
QString text;
for (const auto *block : m_items) {
if (const auto *textContent = qobject_cast<const PluginLLMCore::TextContent *>(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<PluginLLMCore::TextContent *>(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

View File

@@ -1,51 +0,0 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <pluginllmcore/ContentBlocks.hpp>
namespace QodeAssist::Providers {
class OpenAIResponsesMessage : public QObject
{
Q_OBJECT
public:
explicit OpenAIResponsesMessage(QObject *parent = nullptr);
void handleItemDelta(const QJsonObject &item);
void handleToolCallStart(const QString &callId, const QString &name);
void handleToolCallDelta(const QString &callId, const QString &argumentsDelta);
void handleToolCallComplete(const QString &callId);
void handleReasoningStart(const QString &itemId);
void handleReasoningDelta(const QString &itemId, const QString &text);
void handleReasoningComplete(const QString &itemId);
void handleStatus(const QString &status);
QList<QJsonObject> toItemsFormat() const;
QJsonArray createToolResultItems(const QHash<QString, QString> &toolResults) const;
PluginLLMCore::MessageState state() const noexcept { return m_state; }
QString accumulatedText() const;
QList<PluginLLMCore::ToolUseContent *> getCurrentToolUseContent() const;
QList<PluginLLMCore::ThinkingContent *> getCurrentThinkingContent() const;
bool hasToolCalls() const noexcept { return !m_toolCalls.isEmpty(); }
bool hasThinkingContent() const noexcept { return !m_thinkingBlocks.isEmpty(); }
void startNewContinuation();
private:
QString m_status;
PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building;
QList<PluginLLMCore::ContentBlock *> m_items;
QHash<QString, QString> m_pendingToolArguments;
QHash<QString, PluginLLMCore::ToolUseContent *> m_toolCalls;
QHash<QString, PluginLLMCore::ThinkingContent *> m_thinkingBlocks;
void updateStateFromStatus();
PluginLLMCore::TextContent *getOrCreateTextItem();
};
} // namespace QodeAssist::Providers

View File

@@ -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 <QJsonObject>
#include <QList>
#include <QMap>
#include <QString>
namespace QodeAssist::OpenAIResponses {
class RequestBuilder
{
public:
RequestBuilder() = default;
RequestBuilder &setModel(QString model)
{
m_model = std::move(model);
return *this;
}
RequestBuilder &addMessage(Role role, QString content)
{
Message msg;
msg.role = role;
msg.content.append(MessageContent(std::move(content)));
m_messages.append(std::move(msg));
return *this;
}
RequestBuilder &addMessage(Message msg)
{
m_messages.append(std::move(msg));
return *this;
}
RequestBuilder &setInstructions(QString instructions)
{
m_instructions = std::move(instructions);
return *this;
}
RequestBuilder &addTool(Tool tool)
{
m_tools.append(std::move(tool));
return *this;
}
RequestBuilder &setTemperature(double temp) noexcept
{
m_temperature = temp;
return *this;
}
RequestBuilder &setTopP(double topP) noexcept
{
m_topP = topP;
return *this;
}
RequestBuilder &setMaxOutputTokens(int tokens) noexcept
{
m_maxOutputTokens = tokens;
return *this;
}
RequestBuilder &setStream(bool stream) noexcept
{
m_stream = stream;
return *this;
}
RequestBuilder &setStore(bool store) noexcept
{
m_store = store;
return *this;
}
RequestBuilder &setTextFormat(TextFormatOptions format)
{
m_textFormat = std::move(format);
return *this;
}
RequestBuilder &setReasoningEffort(ReasoningEffort effort) noexcept
{
m_reasoningEffort = effort;
return *this;
}
RequestBuilder &setMetadata(QMap<QString, QVariant> metadata)
{
m_metadata = std::move(metadata);
return *this;
}
RequestBuilder &setIncludeReasoningContent(bool include) noexcept
{
m_includeReasoningContent = include;
return *this;
}
RequestBuilder &clear() noexcept
{
m_model.clear();
m_messages.clear();
m_instructions.reset();
m_tools.clear();
m_temperature.reset();
m_topP.reset();
m_maxOutputTokens.reset();
m_stream = false;
m_store.reset();
m_textFormat.reset();
m_reasoningEffort.reset();
m_includeReasoningContent = false;
m_metadata.clear();
return *this;
}
QJsonObject toJson() const
{
QJsonObject obj;
if (!m_model.isEmpty()) {
obj["model"] = m_model;
}
if (!m_messages.isEmpty()) {
if (m_messages.size() == 1 && m_messages[0].role == Role::User
&& m_messages[0].content.size() == 1) {
obj["input"] = m_messages[0].content[0].toJson();
} else {
QJsonArray input;
for (const auto &msg : m_messages) {
input.append(msg.toJson());
}
obj["input"] = input;
}
}
if (m_instructions) {
obj["instructions"] = *m_instructions;
}
if (!m_tools.isEmpty()) {
QJsonArray tools;
for (const auto &tool : m_tools) {
tools.append(tool.toJson());
}
obj["tools"] = tools;
}
if (m_temperature) {
obj["temperature"] = *m_temperature;
}
if (m_topP) {
obj["top_p"] = *m_topP;
}
if (m_maxOutputTokens) {
obj["max_output_tokens"] = *m_maxOutputTokens;
}
obj["stream"] = m_stream;
if (m_store) {
obj["store"] = *m_store;
}
if (m_textFormat) {
QJsonObject textObj;
textObj["format"] = m_textFormat->toJson();
obj["text"] = textObj;
}
if (m_reasoningEffort) {
QJsonObject reasoning;
reasoning["effort"] = effortToString(*m_reasoningEffort);
obj["reasoning"] = reasoning;
}
if (m_includeReasoningContent) {
QJsonArray include;
include.append("reasoning.encrypted_content");
obj["include"] = include;
}
if (!m_metadata.isEmpty()) {
QJsonObject metadata;
for (auto it = m_metadata.constBegin(); it != m_metadata.constEnd(); ++it) {
metadata[it.key()] = QJsonValue::fromVariant(it.value());
}
obj["metadata"] = metadata;
}
return obj;
}
private:
QString m_model;
QList<Message> m_messages;
std::optional<QString> m_instructions;
QList<Tool> m_tools;
std::optional<double> m_temperature;
std::optional<double> m_topP;
std::optional<int> m_maxOutputTokens;
bool m_stream = false;
std::optional<bool> m_store;
std::optional<TextFormatOptions> m_textFormat;
std::optional<ReasoningEffort> m_reasoningEffort;
bool m_includeReasoningContent = false;
QMap<QString, QVariant> m_metadata;
static QString effortToString(ReasoningEffort e)
{
switch (e) {
case ReasoningEffort::None:
return "none";
case ReasoningEffort::Minimal:
return "minimal";
case ReasoningEffort::Low:
return "low";
case ReasoningEffort::Medium:
return "medium";
case ReasoningEffort::High:
return "high";
}
return "medium";
}
};
} // namespace QodeAssist::OpenAIResponses

View File

@@ -4,78 +4,75 @@
#pragma once
#include "pluginllmcore/PromptTemplate.hpp"
#include "providers/OpenAIResponsesRequestBuilder.hpp"
#include <QJsonArray>
#include <QJsonObject>
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\": \"<message>\"}\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

View File

@@ -1,37 +0,0 @@
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "context/IDocumentReader.hpp"
#include <memory>
#include <QTextDocument>
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<QTextDocument>(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<QTextDocument> m_document;
};
} // namespace QodeAssist