mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-14 02:09:22 -04:00
Compare commits
1 Commits
v0.9.13
...
add-qtc-19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
989063f6c8 |
@@ -81,6 +81,8 @@ add_qtc_plugin(QodeAssist
|
|||||||
templates/OpenAI.hpp
|
templates/OpenAI.hpp
|
||||||
templates/MistralAI.hpp
|
templates/MistralAI.hpp
|
||||||
templates/StarCoder2Fim.hpp
|
templates/StarCoder2Fim.hpp
|
||||||
|
# templates/DeepSeekCoderFim.hpp
|
||||||
|
# templates/CustomFimTemplate.hpp
|
||||||
templates/Qwen25CoderFIM.hpp
|
templates/Qwen25CoderFIM.hpp
|
||||||
templates/OpenAICompatible.hpp
|
templates/OpenAICompatible.hpp
|
||||||
templates/Llama3.hpp
|
templates/Llama3.hpp
|
||||||
@@ -105,6 +107,15 @@ add_qtc_plugin(QodeAssist
|
|||||||
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
||||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
||||||
providers/CodestralProvider.hpp providers/CodestralProvider.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
|
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
|
||||||
QodeAssist.qrc
|
QodeAssist.qrc
|
||||||
LSPCompletion.hpp
|
LSPCompletion.hpp
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Id" : "qodeassist",
|
"Id" : "qodeassist",
|
||||||
"Name" : "QodeAssist",
|
"Name" : "QodeAssist",
|
||||||
"Version" : "0.9.13",
|
"Version" : "0.9.12",
|
||||||
"CompatVersion" : "${IDE_VERSION}",
|
"CompatVersion" : "${IDE_VERSION}",
|
||||||
"Vendor" : "Petr Mironychev",
|
"Vendor" : "Petr Mironychev",
|
||||||
"VendorId" : "petrmironychev",
|
"VendorId" : "petrmironychev",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
add_library(QodeAssistLogger STATIC
|
add_library(QodeAssistLogger STATIC
|
||||||
|
EmptyRequestPerformanceLogger.hpp
|
||||||
IRequestPerformanceLogger.hpp
|
IRequestPerformanceLogger.hpp
|
||||||
Logger.cpp
|
Logger.cpp
|
||||||
Logger.hpp
|
Logger.hpp
|
||||||
|
|||||||
16
logger/EmptyRequestPerformanceLogger.hpp
Normal file
16
logger/EmptyRequestPerformanceLogger.hpp
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// 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
|
||||||
@@ -10,6 +10,10 @@ add_library(PluginLLMCore STATIC
|
|||||||
PromptTemplate.hpp
|
PromptTemplate.hpp
|
||||||
PromptTemplateManager.hpp PromptTemplateManager.cpp
|
PromptTemplateManager.hpp PromptTemplateManager.cpp
|
||||||
ProviderID.hpp
|
ProviderID.hpp
|
||||||
|
HttpClient.hpp HttpClient.cpp
|
||||||
|
DataBuffers.hpp
|
||||||
|
SSEBuffer.hpp SSEBuffer.cpp
|
||||||
|
ContentBlocks.hpp
|
||||||
RulesLoader.hpp RulesLoader.cpp
|
RulesLoader.hpp RulesLoader.cpp
|
||||||
ResponseCleaner.hpp
|
ResponseCleaner.hpp
|
||||||
)
|
)
|
||||||
|
|||||||
236
pluginllmcore/ContentBlocks.hpp
Normal file
236
pluginllmcore/ContentBlocks.hpp
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
// 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
|
||||||
23
pluginllmcore/DataBuffers.hpp
Normal file
23
pluginllmcore/DataBuffers.hpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// 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
|
||||||
260
pluginllmcore/HttpClient.cpp
Normal file
260
pluginllmcore/HttpClient.cpp
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
// 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
|
||||||
60
pluginllmcore/HttpClient.hpp
Normal file
60
pluginllmcore/HttpClient.hpp
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// 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
|
||||||
35
pluginllmcore/SSEBuffer.cpp
Normal file
35
pluginllmcore/SSEBuffer.cpp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// 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
|
||||||
26
pluginllmcore/SSEBuffer.hpp
Normal file
26
pluginllmcore/SSEBuffer.hpp
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// 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
|
||||||
223
providers/ClaudeMessage.cpp
Normal file
223
providers/ClaudeMessage.cpp
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
// 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
|
||||||
50
providers/ClaudeMessage.hpp
Normal file
50
providers/ClaudeMessage.hpp
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// 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
|
||||||
228
providers/GoogleMessage.cpp
Normal file
228
providers/GoogleMessage.cpp
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
// 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
|
||||||
52
providers/GoogleMessage.hpp
Normal file
52
providers/GoogleMessage.hpp
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// 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
|
||||||
@@ -68,26 +68,12 @@ void LlamaCppProvider::prepareRequest(
|
|||||||
request["presence_penalty"] = settings.presencePenalty();
|
request["presence_penalty"] = settings.presencePenalty();
|
||||||
};
|
};
|
||||||
|
|
||||||
auto applyThinkingMode = [&request]() {
|
|
||||||
QJsonObject chatTemplateKwargs = request["chat_template_kwargs"].toObject();
|
|
||||||
chatTemplateKwargs["enable_thinking"] = true;
|
|
||||||
request["chat_template_kwargs"] = chatTemplateKwargs;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
if (type == PluginLLMCore::RequestType::CodeCompletion) {
|
||||||
applyModelParams(Settings::codeCompletionSettings());
|
applyModelParams(Settings::codeCompletionSettings());
|
||||||
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
} else if (type == PluginLLMCore::RequestType::QuickRefactoring) {
|
||||||
applyModelParams(Settings::quickRefactorSettings());
|
applyModelParams(Settings::quickRefactorSettings());
|
||||||
if (isThinkingEnabled) {
|
|
||||||
applyThinkingMode();
|
|
||||||
LOG_MESSAGE(QString("LlamaCppProvider: Thinking mode enabled for QuickRefactoring"));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
applyModelParams(Settings::chatAssistantSettings());
|
applyModelParams(Settings::chatAssistantSettings());
|
||||||
if (isThinkingEnabled) {
|
|
||||||
applyThinkingMode();
|
|
||||||
LOG_MESSAGE(QString("LlamaCppProvider: Thinking mode enabled for Chat"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isToolsEnabled) {
|
if (isToolsEnabled) {
|
||||||
@@ -99,11 +85,9 @@ void LlamaCppProvider::prepareRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QFuture<QList<QString>> LlamaCppProvider::getInstalledModels(const QString &baseUrl)
|
QFuture<QList<QString>> LlamaCppProvider::getInstalledModels(const QString &)
|
||||||
{
|
{
|
||||||
m_client->setUrl(baseUrl);
|
return QtFuture::makeReadyFuture(QList<QString>{});
|
||||||
m_client->setApiKey(Settings::providerSettings().llamaCppApiKey());
|
|
||||||
return m_client->listModels();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginLLMCore::ProviderID LlamaCppProvider::providerID() const
|
PluginLLMCore::ProviderID LlamaCppProvider::providerID() const
|
||||||
@@ -113,9 +97,7 @@ PluginLLMCore::ProviderID LlamaCppProvider::providerID() const
|
|||||||
|
|
||||||
PluginLLMCore::ProviderCapabilities LlamaCppProvider::capabilities() const
|
PluginLLMCore::ProviderCapabilities LlamaCppProvider::capabilities() const
|
||||||
{
|
{
|
||||||
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking
|
return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image;
|
||||||
| PluginLLMCore::ProviderCapability::Image
|
|
||||||
| PluginLLMCore::ProviderCapability::ModelListing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::LLMQore::BaseClient *LlamaCppProvider::client() const
|
::LLMQore::BaseClient *LlamaCppProvider::client() const
|
||||||
|
|||||||
349
providers/OllamaMessage.cpp
Normal file
349
providers/OllamaMessage.cpp
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
// 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
|
||||||
56
providers/OllamaMessage.hpp
Normal file
56
providers/OllamaMessage.hpp
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// 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
|
||||||
165
providers/OpenAIMessage.cpp
Normal file
165
providers/OpenAIMessage.cpp
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
// 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
|
||||||
49
providers/OpenAIMessage.hpp
Normal file
49
providers/OpenAIMessage.hpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// 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
|
||||||
38
providers/OpenAIResponses/CancelResponseRequest.hpp
Normal file
38
providers/OpenAIResponses/CancelResponseRequest.hpp
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
53
providers/OpenAIResponses/DeleteResponseRequest.hpp
Normal file
53
providers/OpenAIResponses/DeleteResponseRequest.hpp
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
104
providers/OpenAIResponses/GetResponseRequest.hpp
Normal file
104
providers/OpenAIResponses/GetResponseRequest.hpp
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
203
providers/OpenAIResponses/InputTokensRequest.hpp
Normal file
203
providers/OpenAIResponses/InputTokensRequest.hpp
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
127
providers/OpenAIResponses/ItemTypesReference.hpp
Normal file
127
providers/OpenAIResponses/ItemTypesReference.hpp
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
150
providers/OpenAIResponses/ListInputItemsRequest.hpp
Normal file
150
providers/OpenAIResponses/ListInputItemsRequest.hpp
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
338
providers/OpenAIResponses/ModelRequest.hpp
Normal file
338
providers/OpenAIResponses/ModelRequest.hpp
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
546
providers/OpenAIResponses/ResponseObject.hpp
Normal file
546
providers/OpenAIResponses/ResponseObject.hpp
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
230
providers/OpenAIResponsesMessage.cpp
Normal file
230
providers/OpenAIResponsesMessage.cpp
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
51
providers/OpenAIResponsesMessage.hpp
Normal file
51
providers/OpenAIResponsesMessage.hpp
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
239
providers/OpenAIResponsesRequestBuilder.hpp
Normal file
239
providers/OpenAIResponsesRequestBuilder.hpp
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ ProviderSettings::ProviderSettings()
|
|||||||
|
|
||||||
// Ollama with BasicAuth Settings
|
// Ollama with BasicAuth Settings
|
||||||
ollamaBasicAuthApiKey.setSettingsKey(Constants::OLLAMA_BASIC_AUTH_API_KEY);
|
ollamaBasicAuthApiKey.setSettingsKey(Constants::OLLAMA_BASIC_AUTH_API_KEY);
|
||||||
ollamaBasicAuthApiKey.setLabelText(Tr::tr("Ollama(Bearer) API Key:"));
|
ollamaBasicAuthApiKey.setLabelText(Tr::tr("Ollama BasicAuth API Key:"));
|
||||||
ollamaBasicAuthApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
ollamaBasicAuthApiKey.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||||
ollamaBasicAuthApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
ollamaBasicAuthApiKey.setPlaceHolderText(Tr::tr("Enter your API key here"));
|
||||||
ollamaBasicAuthApiKey.setHistoryCompleter(Constants::OLLAMA_BASIC_AUTH_API_KEY_HISTORY);
|
ollamaBasicAuthApiKey.setHistoryCompleter(Constants::OLLAMA_BASIC_AUTH_API_KEY_HISTORY);
|
||||||
|
|||||||
2
sources/external/llmqore
vendored
2
sources/external/llmqore
vendored
Submodule sources/external/llmqore updated: 82067dc46a...fcdddaa681
@@ -4,9 +4,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "pluginllmcore/PromptTemplate.hpp"
|
#include "pluginllmcore/PromptTemplate.hpp"
|
||||||
|
#include "providers/OpenAIResponsesRequestBuilder.hpp"
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonObject>
|
|
||||||
|
|
||||||
namespace QodeAssist::Templates {
|
namespace QodeAssist::Templates {
|
||||||
|
|
||||||
@@ -22,57 +20,62 @@ public:
|
|||||||
|
|
||||||
QStringList stopWords() const override { return {}; }
|
QStringList stopWords() const override { return {}; }
|
||||||
|
|
||||||
void prepareRequest(
|
void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
|
||||||
QJsonObject &request, const PluginLLMCore::ContextData &context) const override
|
|
||||||
{
|
{
|
||||||
|
using namespace QodeAssist::OpenAIResponses;
|
||||||
|
RequestBuilder builder;
|
||||||
|
|
||||||
if (context.systemPrompt) {
|
if (context.systemPrompt) {
|
||||||
request["instructions"] = context.systemPrompt.value();
|
builder.setInstructions(context.systemPrompt.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!context.history || context.history->isEmpty()) {
|
if (!context.history || context.history->isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonArray input;
|
const auto &history = context.history.value();
|
||||||
for (const auto &msg : context.history.value()) {
|
|
||||||
|
for (const auto &msg : history) {
|
||||||
if (msg.role == "system") {
|
if (msg.role == "system") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject message;
|
Message message;
|
||||||
message["role"] = msg.role;
|
message.role = roleFromString(msg.role);
|
||||||
|
|
||||||
const bool hasImages = msg.images && !msg.images->isEmpty();
|
if (msg.images && !msg.images->isEmpty()) {
|
||||||
|
const auto &images = msg.images.value();
|
||||||
|
message.content.reserve(1 + images.size());
|
||||||
|
|
||||||
if (!hasImages) {
|
|
||||||
message["content"] = msg.content;
|
|
||||||
} else {
|
|
||||||
QJsonArray content;
|
|
||||||
if (!msg.content.isEmpty()) {
|
if (!msg.content.isEmpty()) {
|
||||||
content.append(
|
message.content.append(MessageContent(InputText{msg.content}));
|
||||||
QJsonObject{{"type", "input_text"}, {"text", msg.content}});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &image : msg.images.value()) {
|
for (const auto &image : images) {
|
||||||
QJsonObject imgObj{{"type", "input_image"}, {"detail", "auto"}};
|
InputImage imgInput;
|
||||||
|
imgInput.detail = "auto";
|
||||||
|
|
||||||
if (image.isUrl) {
|
if (image.isUrl) {
|
||||||
imgObj["image_url"] = image.data;
|
imgInput.imageUrl = image.data;
|
||||||
} else {
|
} else {
|
||||||
imgObj["image_url"]
|
imgInput.imageUrl
|
||||||
= QString("data:%1;base64,%2").arg(image.mediaType, image.data);
|
= QString("data:%1;base64,%2").arg(image.mediaType, image.data);
|
||||||
}
|
}
|
||||||
content.append(imgObj);
|
|
||||||
|
message.content.append(MessageContent(std::move(imgInput)));
|
||||||
|
}
|
||||||
|
} 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
|
QString description() const override
|
||||||
{
|
{
|
||||||
return "Template for OpenAI Responses API:\n\n"
|
return "Template for OpenAI Responses API:\n\n"
|
||||||
@@ -86,13 +89,31 @@ public:
|
|||||||
" \"input\": [\n"
|
" \"input\": [\n"
|
||||||
" {\"role\": \"user\", \"content\": \"<message>\"}\n"
|
" {\"role\": \"user\", \"content\": \"<message>\"}\n"
|
||||||
" ]\n"
|
" ]\n"
|
||||||
"}";
|
"}\n\n"
|
||||||
|
"Uses type-safe RequestBuilder for OpenAI Responses API.";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSupportProvider(PluginLLMCore::ProviderID id) const noexcept override
|
bool isSupportProvider(PluginLLMCore::ProviderID id) const noexcept override
|
||||||
{
|
{
|
||||||
return id == QodeAssist::PluginLLMCore::ProviderID::OpenAIResponses;
|
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
|
} // namespace QodeAssist::Templates
|
||||||
|
|
||||||
|
|||||||
37
test/MockDocumentReader.hpp
Normal file
37
test/MockDocumentReader.hpp
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// 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
|
||||||
Reference in New Issue
Block a user