diff --git a/.github/workflows/build_cmake.yml b/.github/workflows/build_cmake.yml index ca56ca0..6df1f56 100644 --- a/.github/workflows/build_cmake.yml +++ b/.github/workflows/build_cmake.yml @@ -56,6 +56,8 @@ jobs: steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 + with: + submodules: recursive - name: Checkout submodules id: git diff --git a/.gitmodules b/.gitmodules index e69de29..3f41606 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "sources/external/llmcore"] + path = sources/external/llmcore + url = https://github.com/Palm1r/llmcore.git diff --git a/CMakeLists.txt b/CMakeLists.txt index a334cc3..c703d62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,7 +34,8 @@ add_definitions( -DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH} ) -add_subdirectory(llmcore) +add_subdirectory(sources/external/llmcore) +add_subdirectory(pluginllmcore) add_subdirectory(settings) add_subdirectory(logger) add_subdirectory(UIControls) @@ -61,6 +62,8 @@ add_qtc_plugin(QodeAssist QtCreator::ExtensionSystem QtCreator::Utils QtCreator::CPlusPlus + LLMCore + PluginLLMCore QodeAssistChatViewplugin SOURCES .github/workflows/build_cmake.yml @@ -112,7 +115,6 @@ add_qtc_plugin(QodeAssist providers/OpenAIResponses/ItemTypesReference.hpp providers/OpenAIResponsesRequestBuilder.hpp providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp - providers/OpenAIResponsesMessage.hpp providers/OpenAIResponsesMessage.cpp QodeAssist.qrc LSPCompletion.hpp LLMSuggestion.hpp LLMSuggestion.cpp @@ -141,10 +143,8 @@ add_qtc_plugin(QodeAssist widgets/DiffStatistics.hpp QuickRefactorHandler.hpp QuickRefactorHandler.cpp - tools/ToolsFactory.hpp tools/ToolsFactory.cpp - tools/ToolHandler.hpp tools/ToolHandler.cpp + tools/ToolsRegistration.hpp tools/ToolsRegistration.cpp tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp - tools/ToolsManager.hpp tools/ToolsManager.cpp tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp tools/EditFileTool.hpp tools/EditFileTool.cpp @@ -154,10 +154,6 @@ add_qtc_plugin(QodeAssist tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp tools/TodoTool.hpp tools/TodoTool.cpp - providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp - providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp - providers/OllamaMessage.hpp providers/OllamaMessage.cpp - providers/GoogleMessage.hpp providers/GoogleMessage.cpp ) get_target_property(QtCreatorCorePath QtCreator::Core LOCATION) diff --git a/ChatView/CMakeLists.txt b/ChatView/CMakeLists.txt index 777bbcd..59b2687 100644 --- a/ChatView/CMakeLists.txt +++ b/ChatView/CMakeLists.txt @@ -80,11 +80,12 @@ target_link_libraries(QodeAssistChatView Qt::Network QtCreator::Core QtCreator::Utils - LLMCore + PluginLLMCore QodeAssistSettings Context QodeAssistUIControlsplugin QodeAssistLogger + LLMCore ) target_include_directories(QodeAssistChatView diff --git a/ChatView/ChatCompressor.cpp b/ChatView/ChatCompressor.cpp index 0743e86..4aa5966 100644 --- a/ChatView/ChatCompressor.cpp +++ b/ChatView/ChatCompressor.cpp @@ -18,6 +18,8 @@ */ #include "ChatCompressor.hpp" + +#include #include "ChatModel.hpp" #include "GeneralSettings.hpp" #include "PromptTemplateManager.hpp" @@ -56,7 +58,7 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch } auto providerName = Settings::generalSettings().caProvider(); - m_provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName); + m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); if (!m_provider) { emit compressionFailed(tr("No provider available")); @@ -64,7 +66,7 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch } auto templateName = Settings::generalSettings().caTemplate(); - auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName( + auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName( templateName); if (!promptTemplate) { @@ -76,7 +78,6 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch m_chatModel = chatModel; m_originalChatPath = chatFilePath; m_accumulatedSummary.clear(); - m_currentRequestId = QUuid::createUuid().toString(QUuid::WithoutBraces); emit compressionStarted(); @@ -85,7 +86,7 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch QUrl requestUrl; QJsonObject payload; - if (m_provider->providerID() == LLMCore::ProviderID::GoogleAI) { + if (m_provider->providerID() == PluginLLMCore::ProviderID::GoogleAI) { requestUrl = QUrl(QString("%1/models/%2:streamGenerateContent?alt=sse") .arg(Settings::generalSettings().caUrl(), Settings::generalSettings().caModel())); @@ -98,8 +99,8 @@ void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *ch buildRequestPayload(payload, promptTemplate); + m_currentRequestId = m_provider->sendRequest(requestUrl, payload); LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId)); - m_provider->sendRequest(m_currentRequestId, requestUrl, payload); } bool ChatCompressor::isCompressing() const @@ -188,28 +189,28 @@ QString ChatCompressor::buildCompressionPrompt() const } void ChatCompressor::buildRequestPayload( - QJsonObject &payload, LLMCore::PromptTemplate *promptTemplate) + QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate) { - LLMCore::ContextData context; + PluginLLMCore::ContextData context; context.systemPrompt = QStringLiteral( "You are a helpful assistant that creates concise summaries of conversations. " "Your summaries preserve key information, technical details, and the flow of discussion."); - QVector messages; + QVector messages; for (const auto &msg : m_chatModel->getChatHistory()) { if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit || msg.role == ChatModel::ChatRole::Thinking) continue; - LLMCore::Message apiMessage; + PluginLLMCore::Message apiMessage; apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant"; apiMessage.content = msg.content; messages.append(apiMessage); } - LLMCore::Message compressionRequest; + PluginLLMCore::Message compressionRequest; compressionRequest.role = "user"; compressionRequest.content = buildCompressionPrompt(); messages.append(compressionRequest); @@ -217,7 +218,7 @@ void ChatCompressor::buildRequestPayload( context.history = messages; m_provider->prepareRequest( - payload, promptTemplate, context, LLMCore::RequestType::Chat, false, false); + payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false); } bool ChatCompressor::createCompressedChatFile( @@ -266,23 +267,25 @@ bool ChatCompressor::createCompressedChatFile( void ChatCompressor::connectProviderSignals() { + auto *c = m_provider->client(); + m_connections.append(connect( - m_provider, - &LLMCore::Provider::partialResponseReceived, + c, + &::LLMCore::BaseClient::chunkReceived, this, &ChatCompressor::onPartialResponseReceived, Qt::UniqueConnection)); m_connections.append(connect( - m_provider, - &LLMCore::Provider::fullResponseReceived, + c, + &::LLMCore::BaseClient::requestCompleted, this, &ChatCompressor::onFullResponseReceived, Qt::UniqueConnection)); m_connections.append(connect( - m_provider, - &LLMCore::Provider::requestFailed, + c, + &::LLMCore::BaseClient::requestFailed, this, &ChatCompressor::onRequestFailed, Qt::UniqueConnection)); diff --git a/ChatView/ChatCompressor.hpp b/ChatView/ChatCompressor.hpp index 54ef607..07df7aa 100644 --- a/ChatView/ChatCompressor.hpp +++ b/ChatView/ChatCompressor.hpp @@ -24,10 +24,10 @@ #include #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class Provider; class PromptTemplate; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore namespace QodeAssist::Chat { @@ -64,13 +64,13 @@ private: void disconnectAllSignals(); void cleanupState(); void handleCompressionError(const QString &error); - void buildRequestPayload(QJsonObject &payload, LLMCore::PromptTemplate *promptTemplate); + void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate); bool m_isCompressing = false; QString m_currentRequestId; QString m_originalChatPath; QString m_accumulatedSummary; - LLMCore::Provider *m_provider = nullptr; + PluginLLMCore::Provider *m_provider = nullptr; ChatModel *m_chatModel = nullptr; QList m_connections; diff --git a/ChatView/ChatRootView.cpp b/ChatView/ChatRootView.cpp index 158e8d1..ec6dcea 100644 --- a/ChatView/ChatRootView.cpp +++ b/ChatView/ChatRootView.cpp @@ -51,14 +51,14 @@ #include "context/ChangesManager.h" #include "context/ContextManager.hpp" #include "context/TokenUtils.hpp" -#include "llmcore/RulesLoader.hpp" +#include "pluginllmcore/RulesLoader.hpp" namespace QodeAssist::Chat { ChatRootView::ChatRootView(QQuickItem *parent) : QQuickItem(parent) , m_chatModel(new ChatModel(this)) - , m_promptProvider(LLMCore::PromptTemplateManager::instance()) + , m_promptProvider(PluginLLMCore::PromptTemplateManager::instance()) , m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this)) , m_fileManager(new ChatFileManager(this)) , m_isRequestInProgress(false) @@ -929,7 +929,7 @@ QString ChatRootView::getRuleContent(int index) if (index < 0 || index >= m_activeRules.size()) return QString(); - return LLMCore::RulesLoader::loadRuleFileContent( + return PluginLLMCore::RulesLoader::loadRuleFileContent( m_activeRules[index].toMap()["filePath"].toString()); } @@ -937,7 +937,7 @@ void ChatRootView::refreshRules() { m_activeRules.clear(); - auto project = LLMCore::RulesLoader::getActiveProject(); + auto project = PluginLLMCore::RulesLoader::getActiveProject(); if (!project) { emit activeRulesChanged(); emit activeRulesCountChanged(); @@ -945,7 +945,7 @@ void ChatRootView::refreshRules() } auto ruleFiles - = LLMCore::RulesLoader::getRuleFilesForProject(project, LLMCore::RulesContext::Chat); + = PluginLLMCore::RulesLoader::getRuleFilesForProject(project, PluginLLMCore::RulesContext::Chat); for (const auto &ruleFile : ruleFiles) { QVariantMap ruleMap; @@ -1296,9 +1296,9 @@ QString ChatRootView::lastInfoMessage() const bool ChatRootView::isThinkingSupport() const { auto providerName = Settings::generalSettings().caProvider(); - auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName); + auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); - return provider && provider->supportThinking(); + return provider && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Thinking); } QString ChatRootView::generateChatFileName(const QString &shortMessage, const QString &dir) const diff --git a/ChatView/ChatRootView.hpp b/ChatView/ChatRootView.hpp index 6fbc12c..9003121 100644 --- a/ChatView/ChatRootView.hpp +++ b/ChatView/ChatRootView.hpp @@ -25,7 +25,7 @@ #include "ChatFileManager.hpp" #include "ChatModel.hpp" #include "ClientInterface.hpp" -#include "llmcore/PromptProviderChat.hpp" +#include "pluginllmcore/PromptProviderChat.hpp" #include namespace QodeAssist::Chat { @@ -235,7 +235,7 @@ private: bool hasImageAttachments(const QStringList &attachments) const; ChatModel *m_chatModel; - LLMCore::PromptProviderChat m_promptProvider; + PluginLLMCore::PromptProviderChat m_promptProvider; ClientInterface *m_clientInterface; ChatFileManager *m_fileManager; QString m_currentTemplate; diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index d9f59fc..cd6d21b 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -19,6 +19,8 @@ #include "ClientInterface.hpp" +#include + #include #include #include @@ -40,6 +42,10 @@ #include #include +#include + +#include "tools/TodoTool.hpp" + #include "ChatAssistantSettings.hpp" #include "ChatSerializer.hpp" #include "GeneralSettings.hpp" @@ -53,7 +59,7 @@ namespace QodeAssist::Chat { ClientInterface::ClientInterface( - ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent) + ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent) : QObject(parent) , m_chatModel(chatModel) , m_promptProvider(promptProvider) @@ -138,7 +144,7 @@ void ClientInterface::sendMessage( auto &chatAssistantSettings = Settings::chatAssistantSettings(); auto providerName = Settings::generalSettings().caProvider(); - auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName); + auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); if (!provider) { LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName)); @@ -153,7 +159,7 @@ void ClientInterface::sendMessage( return; } - LLMCore::ContextData context; + PluginLLMCore::ContextData context; const bool isToolsEnabled = useTools; @@ -167,7 +173,7 @@ void ClientInterface::sendMessage( systemPrompt = systemPrompt + "\n\n" + role.systemPrompt; } - auto project = LLMCore::RulesLoader::getActiveProject(); + auto project = PluginLLMCore::RulesLoader::getActiveProject(); if (project) { systemPrompt += QString("\n# Active project name: %1").arg(project->displayName()); @@ -177,12 +183,12 @@ void ClientInterface::sendMessage( if (auto target = project->activeTarget()) { if (auto buildConfig = target->activeBuildConfiguration()) { systemPrompt += QString("\n# Active Build directory: %1") - .arg(buildConfig->buildDirectory().toUrlishString()); + .arg(buildConfig->buildDirectory().toUrlishString()); } } QString projectRules - = LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat); + = PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat); if (!projectRules.isEmpty()) { systemPrompt += QString("\n# Project Rules\n\n") + projectRules; @@ -197,13 +203,13 @@ void ClientInterface::sendMessage( context.systemPrompt = systemPrompt; } - QVector messages; + QVector messages; for (const auto &msg : m_chatModel->getChatHistory()) { if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) { continue; } - LLMCore::Message apiMessage; + PluginLLMCore::Message apiMessage; apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant"; apiMessage.content = msg.content; @@ -223,7 +229,8 @@ void ClientInterface::sendMessage( apiMessage.isRedacted = msg.isRedacted; apiMessage.signature = msg.signature; - if (provider->supportImage() && !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) { + if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image) + && !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) { auto apiImages = loadImagesFromStorage(msg.images); if (!apiImages.isEmpty()) { apiMessage.images = apiImages; @@ -233,18 +240,19 @@ void ClientInterface::sendMessage( messages.append(apiMessage); } - if (!imageFiles.isEmpty() && !provider->supportImage()) { + if (!imageFiles.isEmpty() + && !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) { LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored") .arg(provider->name(), QString::number(imageFiles.size()))); } context.history = messages; - LLMCore::LLMConfig config; - config.requestType = LLMCore::RequestType::Chat; + PluginLLMCore::LLMConfig config; + config.requestType = PluginLLMCore::RequestType::Chat; config.provider = provider; config.promptTemplate = promptTemplate; - if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { + if (provider->providerID() == PluginLLMCore::ProviderID::GoogleAI) { QString stream = QString{"streamGenerateContent?alt=sse"}; config.url = QUrl(QString("%1/models/%2:%3") .arg( @@ -258,87 +266,79 @@ void ClientInterface::sendMessage( = {{"model", Settings::generalSettings().caModel()}, {"stream", true}}; } - config.apiKey = provider->apiKey(); - config.provider->prepareRequest( config.providerRequest, promptTemplate, context, - LLMCore::RequestType::Chat, + PluginLLMCore::RequestType::Chat, useTools, useThinking); - QString requestId = QUuid::createUuid().toString(); + connect( + provider->client(), + &::LLMCore::BaseClient::chunkReceived, + this, + &ClientInterface::handlePartialResponse, + Qt::UniqueConnection); + connect( + provider->client(), + &::LLMCore::BaseClient::requestCompleted, + this, + &ClientInterface::handleFullResponse, + Qt::UniqueConnection); + connect( + provider->client(), + &::LLMCore::BaseClient::requestFailed, + this, + &ClientInterface::handleRequestFailed, + Qt::UniqueConnection); + connect( + provider->client(), + &::LLMCore::BaseClient::toolStarted, + this, + &ClientInterface::handleToolExecutionStarted, + Qt::UniqueConnection); + connect( + provider->client(), + &::LLMCore::BaseClient::toolResultReady, + this, + &ClientInterface::handleToolExecutionCompleted, + Qt::UniqueConnection); + connect( + provider->client(), + &::LLMCore::BaseClient::thinkingBlockReceived, + this, + &ClientInterface::handleThinkingBlockReceived, + Qt::UniqueConnection); + + auto requestId = provider->sendRequest(config.url, config.providerRequest); QJsonObject request{{"id", requestId}}; m_activeRequests[requestId] = {request, provider}; emit requestStarted(requestId); - - connect( - provider, - &LLMCore::Provider::partialResponseReceived, - this, - &ClientInterface::handlePartialResponse, - Qt::UniqueConnection); - connect( - provider, - &LLMCore::Provider::fullResponseReceived, - this, - &ClientInterface::handleFullResponse, - Qt::UniqueConnection); - connect( - provider, - &LLMCore::Provider::requestFailed, - this, - &ClientInterface::handleRequestFailed, - Qt::UniqueConnection); - connect( - provider, - &LLMCore::Provider::toolExecutionStarted, - this, - &ClientInterface::handleToolExecutionStarted, - Qt::UniqueConnection); - connect( - provider, - &LLMCore::Provider::toolExecutionCompleted, - this, - &ClientInterface::handleToolExecutionCompleted, - Qt::UniqueConnection); - connect( - provider, - &LLMCore::Provider::continuationStarted, - this, - &ClientInterface::handleCleanAccumulatedData, - Qt::UniqueConnection); - connect( - provider, - &LLMCore::Provider::thinkingBlockReceived, - this, - &ClientInterface::handleThinkingBlockReceived, - Qt::UniqueConnection); - connect( - provider, - &LLMCore::Provider::redactedThinkingBlockReceived, - this, - &ClientInterface::handleRedactedThinkingBlockReceived, - Qt::UniqueConnection); - - provider->sendRequest(requestId, config.url, config.providerRequest); - if (provider->supportsTools() && provider->toolsManager()) { - provider->toolsManager()->setCurrentSessionId(m_chatFilePath); + if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools) + && provider->toolsManager()) { + if (auto *todoTool = qobject_cast( + provider->toolsManager()->tool("todo_tool"))) { + todoTool->setCurrentSessionId(m_chatFilePath); + } } } void ClientInterface::clearMessages() { const auto providerName = Settings::generalSettings().caProvider(); - auto *provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName); + auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); - if (provider && !m_chatFilePath.isEmpty() && provider->supportsTools() + if (provider && !m_chatFilePath.isEmpty() + && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools) && provider->toolsManager()) { - provider->toolsManager()->clearTodoSession(m_chatFilePath); + if (auto *todoTool = qobject_cast( + provider->toolsManager()->tool("todo_tool"))) { + todoTool->clearSession(m_chatFilePath); + } } m_chatModel->clear(); @@ -346,7 +346,7 @@ void ClientInterface::clearMessages() void ClientInterface::cancelRequest() { - QSet providers; + QSet providers; for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) { if (it.value().provider) { providers.insert(it.value().provider); @@ -354,7 +354,7 @@ void ClientInterface::cancelRequest() } for (auto *provider : providers) { - disconnect(provider, nullptr, this, nullptr); + disconnect(provider->client(), nullptr, this, nullptr); } for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) { @@ -366,6 +366,7 @@ void ClientInterface::cancelRequest() m_activeRequests.clear(); m_accumulatedResponses.clear(); + m_awaitingContinuation.clear(); LOG_MESSAGE("All requests cancelled and state cleared"); } @@ -432,6 +433,12 @@ void ClientInterface::handlePartialResponse(const QString &requestId, const QStr if (it == m_activeRequests.end()) return; + if (m_awaitingContinuation.remove(requestId)) { + m_accumulatedResponses[requestId].clear(); + LOG_MESSAGE( + QString("Cleared accumulated responses for continuation request %1").arg(requestId)); + } + m_accumulatedResponses[requestId] += partialText; const RequestContext &ctx = it.value(); @@ -462,12 +469,9 @@ void ClientInterface::handleFullResponse(const QString &requestId, const QString + ": " + finalText); emit messageReceivedCompletely(); - if (it != m_activeRequests.end()) { - m_activeRequests.erase(it); - } - if (m_accumulatedResponses.contains(requestId)) { - m_accumulatedResponses.remove(requestId); - } + m_activeRequests.erase(it); + m_accumulatedResponses.remove(requestId); + m_awaitingContinuation.remove(requestId); } void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error) @@ -479,18 +483,9 @@ void ClientInterface::handleRequestFailed(const QString &requestId, const QStrin LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error)); emit errorOccurred(error); - if (it != m_activeRequests.end()) { - m_activeRequests.erase(it); - } - if (m_accumulatedResponses.contains(requestId)) { - m_accumulatedResponses.remove(requestId); - } -} - -void ClientInterface::handleCleanAccumulatedData(const QString &requestId) -{ - m_accumulatedResponses[requestId].clear(); - LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(requestId)); + m_activeRequests.erase(it); + m_accumulatedResponses.remove(requestId); + m_awaitingContinuation.remove(requestId); } void ClientInterface::handleThinkingBlockReceived( @@ -501,19 +496,17 @@ void ClientInterface::handleThinkingBlockReceived( return; } - m_chatModel->addThinkingBlock(requestId, thinking, signature); -} - -void ClientInterface::handleRedactedThinkingBlockReceived( - const QString &requestId, const QString &signature) -{ - if (!m_activeRequests.contains(requestId)) { + if (m_awaitingContinuation.remove(requestId)) { + m_accumulatedResponses[requestId].clear(); LOG_MESSAGE( - QString("Ignoring redacted thinking block for non-chat request: %1").arg(requestId)); - return; + QString("Cleared accumulated responses for continuation request %1").arg(requestId)); } - m_chatModel->addRedactedThinkingBlock(requestId, signature); + if (thinking.isEmpty()) { + m_chatModel->addRedactedThinkingBlock(requestId, signature); + } else { + m_chatModel->addThinkingBlock(requestId, thinking, signature); + } } void ClientInterface::handleToolExecutionStarted( @@ -525,6 +518,7 @@ void ClientInterface::handleToolExecutionStarted( } m_chatModel->addToolExecutionStatus(requestId, toolId, toolName); + m_awaitingContinuation.insert(requestId); } void ClientInterface::handleToolExecutionCompleted( @@ -588,10 +582,10 @@ QString ClientInterface::encodeImageToBase64(const QString &filePath) const return imageData.toBase64(); } -QVector ClientInterface::loadImagesFromStorage( +QVector ClientInterface::loadImagesFromStorage( const QList &storedImages) const { - QVector apiImages; + QVector apiImages; for (const auto &storedImage : storedImages) { QString base64Data @@ -601,7 +595,7 @@ QVector ClientInterface::loadImagesFromStorage( continue; } - LLMCore::ImageAttachment apiImage; + PluginLLMCore::ImageAttachment apiImage; apiImage.data = base64Data; apiImage.mediaType = storedImage.mediaType; apiImage.isUrl = false; @@ -616,10 +610,15 @@ void ClientInterface::setChatFilePath(const QString &filePath) { if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) { const auto providerName = Settings::generalSettings().caProvider(); - auto *provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName); + auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName); - if (provider && provider->supportsTools() && provider->toolsManager()) { - provider->toolsManager()->clearTodoSession(m_chatFilePath); + if (provider + && provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools) + && provider->toolsManager()) { + if (auto *todoTool = qobject_cast( + provider->toolsManager()->tool("todo_tool"))) { + todoTool->clearSession(m_chatFilePath); + } } } diff --git a/ChatView/ClientInterface.hpp b/ChatView/ClientInterface.hpp index 5155b36..912b2ae 100644 --- a/ChatView/ClientInterface.hpp +++ b/ChatView/ClientInterface.hpp @@ -20,12 +20,13 @@ #pragma once #include +#include #include #include #include "ChatModel.hpp" #include "Provider.hpp" -#include "llmcore/IPromptProvider.hpp" +#include "pluginllmcore/IPromptProvider.hpp" #include namespace QodeAssist::Chat { @@ -36,7 +37,7 @@ class ClientInterface : public QObject public: explicit ClientInterface( - ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr); + ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr); ~ClientInterface(); void sendMessage( @@ -62,10 +63,8 @@ private slots: void handlePartialResponse(const QString &requestId, const QString &partialText); void handleFullResponse(const QString &requestId, const QString &fullText); void handleRequestFailed(const QString &requestId, const QString &error); - void handleCleanAccumulatedData(const QString &requestId); void handleThinkingBlockReceived( const QString &requestId, const QString &thinking, const QString &signature); - void handleRedactedThinkingBlockReceived(const QString &requestId, const QString &signature); void handleToolExecutionStarted( const QString &requestId, const QString &toolId, const QString &toolName); void handleToolExecutionCompleted( @@ -82,21 +81,22 @@ private: bool isImageFile(const QString &filePath) const; QString getMediaTypeForImage(const QString &filePath) const; QString encodeImageToBase64(const QString &filePath) const; - QVector loadImagesFromStorage(const QList &storedImages) const; + QVector loadImagesFromStorage(const QList &storedImages) const; struct RequestContext { QJsonObject originalRequest; - LLMCore::Provider *provider; + PluginLLMCore::Provider *provider; }; - LLMCore::IPromptProvider *m_promptProvider = nullptr; + PluginLLMCore::IPromptProvider *m_promptProvider = nullptr; ChatModel *m_chatModel; Context::ContextManager *m_contextManager; QString m_chatFilePath; QHash m_activeRequests; QHash m_accumulatedResponses; + QSet m_awaitingContinuation; }; } // namespace QodeAssist::Chat diff --git a/ConfigurationManager.cpp b/ConfigurationManager.cpp index ba8c98f..f1172f6 100644 --- a/ConfigurationManager.cpp +++ b/ConfigurationManager.cpp @@ -41,7 +41,7 @@ void ConfigurationManager::init() void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect) { - LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value()); + PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value()); if (!templ) { return; @@ -65,7 +65,7 @@ void ConfigurationManager::updateAllTemplateDescriptions() void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect) { - LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value()); + PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value()); if (templ->name() == templateAspect.value()) return; @@ -86,8 +86,8 @@ void ConfigurationManager::checkAllTemplate() ConfigurationManager::ConfigurationManager(QObject *parent) : QObject(parent) , m_generalSettings(Settings::generalSettings()) - , m_providersManager(LLMCore::ProvidersManager::instance()) - , m_templateManger(LLMCore::PromptTemplateManager::instance()) + , m_providersManager(PluginLLMCore::ProvidersManager::instance()) + , m_templateManger(PluginLLMCore::PromptTemplateManager::instance()) {} void ConfigurationManager::setupConnections() @@ -176,7 +176,7 @@ void ConfigurationManager::selectModel() : m_generalSettings.caModel); if (auto provider = m_providersManager.getProviderByName(providerName)) { - if (!provider->supportsModelListing()) { + if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) { m_generalSettings.showModelsNotSupportedDialog(*targetSettings); return; } diff --git a/ConfigurationManager.hpp b/ConfigurationManager.hpp index b5be510..35e180f 100644 --- a/ConfigurationManager.hpp +++ b/ConfigurationManager.hpp @@ -21,8 +21,8 @@ #include -#include "llmcore/PromptTemplateManager.hpp" -#include "llmcore/ProvidersManager.hpp" +#include "pluginllmcore/PromptTemplateManager.hpp" +#include "pluginllmcore/ProvidersManager.hpp" #include "settings/GeneralSettings.hpp" namespace QodeAssist { @@ -54,8 +54,8 @@ private: ConfigurationManager &operator=(const ConfigurationManager &) = delete; Settings::GeneralSettings &m_generalSettings; - LLMCore::ProvidersManager &m_providersManager; - LLMCore::PromptTemplateManager &m_templateManger; + PluginLLMCore::ProvidersManager &m_providersManager; + PluginLLMCore::PromptTemplateManager &m_templateManger; void setupConnections(); }; diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp index 869fb0a..eea6ce3 100644 --- a/LLMClientInterface.cpp +++ b/LLMClientInterface.cpp @@ -19,6 +19,7 @@ #include "LLMClientInterface.hpp" +#include #include #include #include @@ -29,16 +30,16 @@ #include "logger/Logger.hpp" #include "settings/CodeCompletionSettings.hpp" #include "settings/GeneralSettings.hpp" -#include -#include +#include +#include namespace QodeAssist { LLMClientInterface::LLMClientInterface( const Settings::GeneralSettings &generalSettings, const Settings::CodeCompletionSettings &completeSettings, - LLMCore::IProviderRegistry &providerRegistry, - LLMCore::IPromptProvider *promptProvider, + PluginLLMCore::IProviderRegistry &providerRegistry, + PluginLLMCore::IPromptProvider *promptProvider, Context::IDocumentReader &documentReader, IRequestPerformanceLogger &performanceLogger) : m_generalSettings(generalSettings) @@ -122,8 +123,6 @@ void LLMClientInterface::sendData(const QByteArray &data) } else if (method == "textDocument/didOpen") { handleTextDocumentDidOpen(request); } else if (method == "getCompletionsCycling") { - QString requestId = request["id"].toString(); - m_performanceLogger.startTimeMeasurement(requestId); handleCompletion(request); } else if (method == "$/cancelRequest") { handleCancelRequest(); @@ -136,7 +135,7 @@ void LLMClientInterface::sendData(const QByteArray &data) void LLMClientInterface::handleCancelRequest() { - QSet providers; + QSet providers; for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) { if (it.value().provider) { providers.insert(it.value().provider); @@ -144,7 +143,7 @@ void LLMClientInterface::handleCancelRequest() } for (auto *provider : providers) { - disconnect(provider, nullptr, this, nullptr); + disconnect(provider->client(), nullptr, this, nullptr); } for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) { @@ -271,12 +270,12 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) } // TODO refactor to dynamic presets system - LLMCore::LLMConfig config; - config.requestType = LLMCore::RequestType::CodeCompletion; + PluginLLMCore::LLMConfig config; + config.requestType = PluginLLMCore::RequestType::CodeCompletion; config.provider = provider; config.promptTemplate = promptTemplate; // TODO refactor networking - if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { + if (provider->providerID() == PluginLLMCore::ProviderID::GoogleAI) { QString stream = QString{"streamGenerateContent?alt=sse"}; config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream)); } else { @@ -284,7 +283,6 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active))); config.providerRequest = {{"model", modelName}, {"stream", true}}; } - config.apiKey = provider->apiKey(); config.multiLineCompletion = m_completeSettings.multiLineCompletion(); const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords()); @@ -295,14 +293,14 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) if (m_completeSettings.useSystemPrompt()) systemPrompt.append( m_completeSettings.useUserMessageTemplateForCC() - && promptTemplate->type() == LLMCore::TemplateType::Chat + && promptTemplate->type() == PluginLLMCore::TemplateType::Chat ? m_completeSettings.systemPromptForNonFimModels() : m_completeSettings.systemPrompt()); - auto project = LLMCore::RulesLoader::getActiveProject(); + auto project = PluginLLMCore::RulesLoader::getActiveProject(); if (project) { QString projectRules - = LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Completions); + = PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions); if (!projectRules.isEmpty()) { systemPrompt += "\n\n# Project Rules\n\n" + projectRules; @@ -314,10 +312,10 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) systemPrompt.append(updatedContext.fileContext.value()); if (m_completeSettings.useOpenFilesContext()) { - if (provider->providerID() == LLMCore::ProviderID::LlamaCpp) { + if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) { for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) { if (!updatedContext.filesMetadata) { - updatedContext.filesMetadata = QList(); + updatedContext.filesMetadata = QList(); } updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second}); } @@ -328,7 +326,7 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) updatedContext.systemPrompt = systemPrompt; - if (promptTemplate->type() == LLMCore::TemplateType::Chat) { + if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) { QString userMessage; if (m_completeSettings.useUserMessageTemplateForCC()) { userMessage = m_completeSettings.processMessageToFIM( @@ -338,7 +336,7 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) } // TODO refactor add message - QVector messages; + QVector messages; messages.append({"user", userMessage}); updatedContext.history = messages; } @@ -347,41 +345,29 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) config.providerRequest, promptTemplate, updatedContext, - LLMCore::RequestType::CodeCompletion, + PluginLLMCore::RequestType::CodeCompletion, false, false); - auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type()); - if (!errors.isEmpty()) { - QString error = QString("Request validation failed: %1").arg(errors.join("; ")); - LOG_MESSAGE("Validate errors for request:"); - LOG_MESSAGES(errors); - sendErrorResponse(request, error); - return; - } - - QString requestId = request["id"].toString(); - m_performanceLogger.startTimeMeasurement(requestId); - - m_activeRequests[requestId] = {request, provider}; - connect( - provider, - &LLMCore::Provider::fullResponseReceived, + provider->client(), + &::LLMCore::BaseClient::requestCompleted, this, &LLMClientInterface::handleFullResponse, Qt::UniqueConnection); connect( - provider, - &LLMCore::Provider::requestFailed, + provider->client(), + &::LLMCore::BaseClient::requestFailed, this, &LLMClientInterface::handleRequestFailed, Qt::UniqueConnection); - provider->sendRequest(requestId, config.url, config.providerRequest); + auto requestId = provider->sendRequest(config.url, config.providerRequest); + m_activeRequests[requestId] = {request, provider}; + m_performanceLogger.startTimeMeasurement(requestId); } -LLMCore::ContextData LLMClientInterface::prepareContext( +PluginLLMCore::ContextData LLMClientInterface::prepareContext( const QJsonObject &request, const Context::DocumentInfo &documentInfo) { QJsonObject params = request["params"].toObject(); @@ -396,13 +382,13 @@ LLMCore::ContextData LLMClientInterface::prepareContext( } QString LLMClientInterface::endpoint( - LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify) + PluginLLMCore::Provider *provider, PluginLLMCore::TemplateType type, bool isLanguageSpecify) { QString endpoint; auto endpointMode = isLanguageSpecify ? m_generalSettings.ccPreset1EndpointMode.stringValue() : m_generalSettings.ccEndpointMode.stringValue(); if (endpointMode == "Auto") { - endpoint = type == LLMCore::TemplateType::FIM ? provider->completionEndpoint() + endpoint = type == PluginLLMCore::TemplateType::FIM ? provider->completionEndpoint() : provider->chatEndpoint(); } else if (endpointMode == "Custom") { endpoint = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint() diff --git a/LLMClientInterface.hpp b/LLMClientInterface.hpp index f1b7b24..7ecfb05 100644 --- a/LLMClientInterface.hpp +++ b/LLMClientInterface.hpp @@ -25,9 +25,9 @@ #include #include #include -#include -#include -#include +#include +#include +#include #include #include #include @@ -45,8 +45,8 @@ public: LLMClientInterface( const Settings::GeneralSettings &generalSettings, const Settings::CodeCompletionSettings &completeSettings, - LLMCore::IProviderRegistry &providerRegistry, - LLMCore::IPromptProvider *promptProvider, + PluginLLMCore::IProviderRegistry &providerRegistry, + PluginLLMCore::IPromptProvider *promptProvider, Context::IDocumentReader &documentReader, IRequestPerformanceLogger &performanceLogger); ~LLMClientInterface() override; @@ -82,17 +82,17 @@ private: struct RequestContext { QJsonObject originalRequest; - LLMCore::Provider *provider; + PluginLLMCore::Provider *provider; }; - LLMCore::ContextData prepareContext( + PluginLLMCore::ContextData prepareContext( const QJsonObject &request, const Context::DocumentInfo &documentInfo); - QString endpoint(LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify); + QString endpoint(PluginLLMCore::Provider *provider, PluginLLMCore::TemplateType type, bool isLanguageSpecify); const Settings::CodeCompletionSettings &m_completeSettings; const Settings::GeneralSettings &m_generalSettings; - LLMCore::IPromptProvider *m_promptProvider = nullptr; - LLMCore::IProviderRegistry &m_providerRegistry; + PluginLLMCore::IPromptProvider *m_promptProvider = nullptr; + PluginLLMCore::IProviderRegistry &m_providerRegistry; Context::IDocumentReader &m_documentReader; IRequestPerformanceLogger &m_performanceLogger; QElapsedTimer m_completionTimer; diff --git a/QodeAssistClient.hpp b/QodeAssistClient.hpp index b930f10..ae30fac 100644 --- a/QodeAssistClient.hpp +++ b/QodeAssistClient.hpp @@ -36,8 +36,8 @@ #include "widgets/EditorChatButtonHandler.hpp" #include "widgets/RefactorWidgetHandler.hpp" #include -#include -#include +#include +#include namespace QodeAssist { diff --git a/QuickRefactorHandler.cpp b/QuickRefactorHandler.cpp index 70a9398..84cbb44 100644 --- a/QuickRefactorHandler.cpp +++ b/QuickRefactorHandler.cpp @@ -19,18 +19,19 @@ #include "QuickRefactorHandler.hpp" +#include #include #include #include #include -#include +#include #include #include -#include -#include -#include -#include +#include +#include +#include +#include #include #include #include @@ -109,8 +110,8 @@ void QuickRefactorHandler::prepareAndSendRequest( { auto &settings = Settings::generalSettings(); - auto &providerRegistry = LLMCore::ProvidersManager::instance(); - auto &promptManager = LLMCore::PromptTemplateManager::instance(); + auto &providerRegistry = PluginLLMCore::ProvidersManager::instance(); + auto &promptManager = PluginLLMCore::PromptTemplateManager::instance(); const auto providerName = settings.qrProvider(); auto provider = providerRegistry.getProviderByName(providerName); @@ -140,14 +141,13 @@ void QuickRefactorHandler::prepareAndSendRequest( return; } - LLMCore::LLMConfig config; - config.requestType = LLMCore::RequestType::QuickRefactoring; + PluginLLMCore::LLMConfig config; + config.requestType = PluginLLMCore::RequestType::QuickRefactoring; config.provider = provider; config.promptTemplate = promptTemplate; config.url = QString("%1%2").arg(settings.qrUrl(), provider->chatEndpoint()); - config.apiKey = provider->apiKey(); - if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { + if (provider->providerID() == PluginLLMCore::ProviderID::GoogleAI) { QString stream = QString{"streamGenerateContent?alt=sse"}; config.url = QUrl(QString("%1/models/%2:%3") .arg( @@ -161,7 +161,7 @@ void QuickRefactorHandler::prepareAndSendRequest( = {{"model", Settings::generalSettings().qrModel()}, {"stream", true}}; } - LLMCore::ContextData context = prepareContext(editor, range, instructions); + PluginLLMCore::ContextData context = prepareContext(editor, range, instructions); bool enableTools = Settings::quickRefactorSettings().useTools(); bool enableThinking = Settings::quickRefactorSettings().useThinking(); @@ -169,41 +169,39 @@ void QuickRefactorHandler::prepareAndSendRequest( config.providerRequest, promptTemplate, context, - LLMCore::RequestType::QuickRefactoring, + PluginLLMCore::RequestType::QuickRefactoring, enableTools, enableThinking); - QString requestId = QUuid::createUuid().toString(); - m_lastRequestId = requestId; - QJsonObject request{{"id", requestId}}; - m_isRefactoringInProgress = true; - m_activeRequests[requestId] = {request, provider}; - connect( - provider, - &LLMCore::Provider::fullResponseReceived, + provider->client(), + &::LLMCore::BaseClient::requestCompleted, this, &QuickRefactorHandler::handleFullResponse, Qt::UniqueConnection); connect( - provider, - &LLMCore::Provider::requestFailed, + provider->client(), + &::LLMCore::BaseClient::requestFailed, this, &QuickRefactorHandler::handleRequestFailed, Qt::UniqueConnection); - provider->sendRequest(requestId, config.url, config.providerRequest); + auto requestId = provider->sendRequest(config.url, config.providerRequest); + m_lastRequestId = requestId; + QJsonObject request{{"id", requestId}}; + + m_activeRequests[requestId] = {request, provider}; } -LLMCore::ContextData QuickRefactorHandler::prepareContext( +PluginLLMCore::ContextData QuickRefactorHandler::prepareContext( TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &instructions) { - LLMCore::ContextData context; + PluginLLMCore::ContextData context; auto textDocument = editor->textDocument(); Context::DocumentReaderQtCreator documentReader; @@ -287,10 +285,10 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext( QString systemPrompt = Settings::quickRefactorSettings().systemPrompt(); - auto project = LLMCore::RulesLoader::getActiveProject(); + auto project = PluginLLMCore::RulesLoader::getActiveProject(); if (project) { - QString projectRules = LLMCore::RulesLoader::loadRulesForProject( - project, LLMCore::RulesContext::QuickRefactor); + QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject( + project, PluginLLMCore::RulesContext::QuickRefactor); if (!projectRules.isEmpty()) { systemPrompt += "\n\n# Project Rules\n\n" + projectRules; @@ -368,7 +366,7 @@ LLMCore::ContextData QuickRefactorHandler::prepareContext( context.systemPrompt = systemPrompt; - QVector messages; + QVector messages; messages.append( {"user", instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability." @@ -387,7 +385,7 @@ void QuickRefactorHandler::handleLLMResponse( if (isComplete) { m_isRefactoringInProgress = false; - QString cleanedResponse = LLMCore::ResponseCleaner::clean(response); + QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response); RefactorResult result; result.newText = cleanedResponse; diff --git a/QuickRefactorHandler.hpp b/QuickRefactorHandler.hpp index 2029f70..b7b51df 100644 --- a/QuickRefactorHandler.hpp +++ b/QuickRefactorHandler.hpp @@ -27,8 +27,8 @@ #include #include -#include -#include +#include +#include namespace QodeAssist { @@ -68,7 +68,7 @@ private: const Utils::Text::Range &range); void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete); - LLMCore::ContextData prepareContext( + PluginLLMCore::ContextData prepareContext( TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &instructions); @@ -76,7 +76,7 @@ private: struct RequestContext { QJsonObject originalRequest; - LLMCore::Provider *provider; + PluginLLMCore::Provider *provider; }; QHash m_activeRequests; diff --git a/context/CMakeLists.txt b/context/CMakeLists.txt index 28f84aa..c5754c1 100644 --- a/context/CMakeLists.txt +++ b/context/CMakeLists.txt @@ -20,7 +20,7 @@ target_link_libraries(Context QtCreator::Utils QtCreator::ProjectExplorer PRIVATE - LLMCore + PluginLLMCore QodeAssistSettings ) diff --git a/context/DocumentContextReader.cpp b/context/DocumentContextReader.cpp index 69e59bd..07362af 100644 --- a/context/DocumentContextReader.cpp +++ b/context/DocumentContextReader.cpp @@ -269,7 +269,7 @@ CopyrightInfo DocumentContextReader::copyrightInfo() const return m_copyrightInfo; } -LLMCore::ContextData DocumentContextReader::prepareContext( +PluginLLMCore::ContextData DocumentContextReader::prepareContext( int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const { QString contextBefore; diff --git a/context/DocumentContextReader.hpp b/context/DocumentContextReader.hpp index 4f055a7..51c592e 100644 --- a/context/DocumentContextReader.hpp +++ b/context/DocumentContextReader.hpp @@ -22,7 +22,7 @@ #include #include -#include +#include #include namespace QodeAssist::Context { @@ -73,7 +73,7 @@ public: CopyrightInfo copyrightInfo() const; - LLMCore::ContextData prepareContext( + PluginLLMCore::ContextData prepareContext( int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const; private: diff --git a/llmcore/Provider.cpp b/llmcore/Provider.cpp deleted file mode 100644 index dbae853..0000000 --- a/llmcore/Provider.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "Provider.hpp" - -#include - -namespace QodeAssist::LLMCore { - -Provider::Provider(QObject *parent) - : QObject(parent) - , m_httpClient(new HttpClient(this)) -{ - connect(m_httpClient, &HttpClient::dataReceived, this, &Provider::onDataReceived); - connect(m_httpClient, &HttpClient::requestFinished, this, &Provider::onRequestFinished); -} - -void Provider::cancelRequest(const RequestID &requestId) -{ - m_httpClient->cancelRequest(requestId); -} - -HttpClient *Provider::httpClient() const -{ - return m_httpClient; -} - -QJsonObject Provider::parseEventLine(const QString &line) -{ - if (!line.startsWith("data: ")) - return QJsonObject(); - - QString jsonStr = line.mid(6); - - QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8()); - return doc.object(); -} - -} // namespace QodeAssist::LLMCore diff --git a/llmcore/Provider.hpp b/llmcore/Provider.hpp deleted file mode 100644 index dbc09c4..0000000 --- a/llmcore/Provider.hpp +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2024-2025 Petr Mironychev - * - * This file is part of QodeAssist. - * - * QodeAssist is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * QodeAssist is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with QodeAssist. If not, see . - */ - -#pragma once - -#include - -#include -#include -#include -#include -#include - -#include "ContextData.hpp" -#include "DataBuffers.hpp" -#include "HttpClient.hpp" -#include "IToolsManager.hpp" -#include "PromptTemplate.hpp" -#include "RequestType.hpp" - -class QNetworkReply; -class QJsonObject; - -namespace QodeAssist::LLMCore { - -class Provider : public QObject -{ - Q_OBJECT -public: - explicit Provider(QObject *parent = nullptr); - - virtual ~Provider() = default; - - virtual QString name() const = 0; - virtual QString url() const = 0; - virtual QString completionEndpoint() const = 0; - virtual QString chatEndpoint() const = 0; - virtual bool supportsModelListing() const = 0; - virtual void prepareRequest( - QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) - = 0; - virtual QFuture> getInstalledModels(const QString &url) = 0; - virtual QList validateRequest(const QJsonObject &request, TemplateType type) = 0; - virtual QString apiKey() const = 0; - virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0; - virtual ProviderID providerID() const = 0; - - virtual void sendRequest(const RequestID &requestId, const QUrl &url, const QJsonObject &payload) - = 0; - - virtual bool supportsTools() const { return false; }; - virtual bool supportThinking() const { return false; }; - virtual bool supportImage() const { return false; }; - - virtual void cancelRequest(const RequestID &requestId); - - virtual IToolsManager *toolsManager() const { return nullptr; } - - HttpClient *httpClient() const; - -public slots: - virtual void onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) - = 0; - virtual void onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, std::optional error) - = 0; - -signals: - void partialResponseReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QString &partialText); - void fullResponseReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QString &fullText); - void requestFailed(const QodeAssist::LLMCore::RequestID &requestId, const QString &error); - void toolExecutionStarted( - const QString &requestId, const QString &toolId, const QString &toolName); - void toolExecutionCompleted( - const QString &requestId, - const QString &toolId, - const QString &toolName, - const QString &result); - void continuationStarted(const QodeAssist::LLMCore::RequestID &requestId); - void thinkingBlockReceived( - const QString &requestId, const QString &thinking, const QString &signature); - void redactedThinkingBlockReceived(const QString &requestId, const QString &signature); - -protected: - QJsonObject parseEventLine(const QString &line); - - QHash m_dataBuffers; - QHash m_requestUrls; - -private: - HttpClient *m_httpClient; -}; - -} // namespace QodeAssist::LLMCore diff --git a/llmcore/ValidationUtils.cpp b/llmcore/ValidationUtils.cpp deleted file mode 100644 index 8d4f72f..0000000 --- a/llmcore/ValidationUtils.cpp +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2024-2025 Petr Mironychev - * - * This file is part of QodeAssist. - * - * QodeAssist is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * QodeAssist is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with QodeAssist. If not, see . - */ - -#include "ValidationUtils.hpp" - -#include - -namespace QodeAssist::LLMCore { - -QStringList ValidationUtils::validateRequestFields( - const QJsonObject &request, const QJsonObject &templateObj) -{ - QStringList errors; - validateFields(request, templateObj, errors); - validateNestedObjects(request, templateObj, errors); - return errors; -} - -void ValidationUtils::validateFields( - const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors) -{ - for (auto it = request.begin(); it != request.end(); ++it) { - if (!templateObj.contains(it.key())) { - errors << QString("unknown field '%1'").arg(it.key()); - } - } -} - -void ValidationUtils::validateNestedObjects( - const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors) -{ - for (auto it = request.begin(); it != request.end(); ++it) { - if (templateObj.contains(it.key()) && it.value().isObject() - && templateObj[it.key()].isObject()) { - validateFields(it.value().toObject(), templateObj[it.key()].toObject(), errors); - validateNestedObjects(it.value().toObject(), templateObj[it.key()].toObject(), errors); - } - } -} - -} // namespace QodeAssist::LLMCore diff --git a/llmcore/BaseTool.cpp b/pluginllmcore/BaseTool.cpp similarity index 96% rename from llmcore/BaseTool.cpp rename to pluginllmcore/BaseTool.cpp index e1594af..ce53d08 100644 --- a/llmcore/BaseTool.cpp +++ b/pluginllmcore/BaseTool.cpp @@ -19,7 +19,7 @@ #include "BaseTool.hpp" -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { BaseTool::BaseTool(QObject *parent) : QObject(parent) @@ -70,4 +70,4 @@ QJsonObject BaseTool::customizeForGoogle(const QJsonObject &baseDefinition) cons return tool; } -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/BaseTool.hpp b/pluginllmcore/BaseTool.hpp similarity index 96% rename from llmcore/BaseTool.hpp rename to pluginllmcore/BaseTool.hpp index ddb5684..7421bb7 100644 --- a/llmcore/BaseTool.hpp +++ b/pluginllmcore/BaseTool.hpp @@ -25,7 +25,7 @@ #include #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { enum class ToolSchemaFormat { OpenAI, Claude, Ollama, Google }; @@ -67,4 +67,4 @@ protected: virtual QJsonObject customizeForGoogle(const QJsonObject &baseDefinition) const; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/CMakeLists.txt b/pluginllmcore/CMakeLists.txt similarity index 80% rename from llmcore/CMakeLists.txt rename to pluginllmcore/CMakeLists.txt index 83a9ab6..9072785 100644 --- a/llmcore/CMakeLists.txt +++ b/pluginllmcore/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(LLMCore STATIC +add_library(PluginLLMCore STATIC RequestType.hpp Provider.hpp Provider.cpp ProvidersManager.hpp ProvidersManager.cpp @@ -10,7 +10,6 @@ add_library(LLMCore STATIC PromptTemplate.hpp PromptTemplateManager.hpp PromptTemplateManager.cpp RequestConfig.hpp - ValidationUtils.hpp ValidationUtils.cpp ProviderID.hpp HttpClient.hpp HttpClient.cpp DataBuffers.hpp @@ -21,15 +20,16 @@ add_library(LLMCore STATIC ResponseCleaner.hpp ) -target_link_libraries(LLMCore +target_link_libraries(PluginLLMCore PUBLIC Qt::Core Qt::Network QtCreator::Core QtCreator::Utils QtCreator::ExtensionSystem + LLMCore PRIVATE QodeAssistLogger ) -target_include_directories(LLMCore PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(PluginLLMCore PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/llmcore/ContentBlocks.hpp b/pluginllmcore/ContentBlocks.hpp similarity index 98% rename from llmcore/ContentBlocks.hpp rename to pluginllmcore/ContentBlocks.hpp index 9286eea..2eb9014 100644 --- a/llmcore/ContentBlocks.hpp +++ b/pluginllmcore/ContentBlocks.hpp @@ -26,7 +26,7 @@ #include #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { enum class MessageState { Building, Complete, RequiresToolExecution, Final }; @@ -249,4 +249,4 @@ private: QString m_signature; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/ContextData.hpp b/pluginllmcore/ContextData.hpp similarity index 95% rename from llmcore/ContextData.hpp rename to pluginllmcore/ContextData.hpp index e95c135..b23cd94 100644 --- a/llmcore/ContextData.hpp +++ b/pluginllmcore/ContextData.hpp @@ -22,7 +22,7 @@ #include #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { struct ImageAttachment { @@ -66,4 +66,4 @@ struct ContextData bool operator==(const ContextData &) const = default; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/DataBuffers.hpp b/pluginllmcore/DataBuffers.hpp similarity index 92% rename from llmcore/DataBuffers.hpp rename to pluginllmcore/DataBuffers.hpp index 4ab5dee..4d1b496 100644 --- a/llmcore/DataBuffers.hpp +++ b/pluginllmcore/DataBuffers.hpp @@ -22,7 +22,7 @@ #include "SSEBuffer.hpp" #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { struct DataBuffers { @@ -36,4 +36,4 @@ struct DataBuffers } }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/HttpClient.cpp b/pluginllmcore/HttpClient.cpp similarity index 99% rename from llmcore/HttpClient.cpp rename to pluginllmcore/HttpClient.cpp index 9fbcb45..6799d29 100644 --- a/llmcore/HttpClient.cpp +++ b/pluginllmcore/HttpClient.cpp @@ -24,7 +24,7 @@ #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { HttpClient::HttpClient(QObject *parent) : QObject(parent) @@ -273,4 +273,4 @@ QString HttpClient::parseErrorFromResponse( return QString("HTTP %1: %2").arg(statusCode).arg(networkErrorString); } -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/HttpClient.hpp b/pluginllmcore/HttpClient.hpp similarity index 96% rename from llmcore/HttpClient.hpp rename to pluginllmcore/HttpClient.hpp index 44a18c9..c39d6bf 100644 --- a/llmcore/HttpClient.hpp +++ b/pluginllmcore/HttpClient.hpp @@ -30,7 +30,7 @@ #include #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class HttpClient : public QObject { @@ -73,4 +73,4 @@ private: mutable QMutex m_mutex; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/IPromptProvider.hpp b/pluginllmcore/IPromptProvider.hpp similarity index 93% rename from llmcore/IPromptProvider.hpp rename to pluginllmcore/IPromptProvider.hpp index 0dde34a..c23bf04 100644 --- a/llmcore/IPromptProvider.hpp +++ b/pluginllmcore/IPromptProvider.hpp @@ -22,7 +22,7 @@ #include "PromptTemplate.hpp" #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class IPromptProvider { @@ -36,4 +36,4 @@ public: virtual QStringList getTemplatesForProvider(ProviderID id) const = 0; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/IProviderRegistry.hpp b/pluginllmcore/IProviderRegistry.hpp similarity index 92% rename from llmcore/IProviderRegistry.hpp rename to pluginllmcore/IProviderRegistry.hpp index 18b9dca..855cee1 100644 --- a/llmcore/IProviderRegistry.hpp +++ b/pluginllmcore/IProviderRegistry.hpp @@ -21,7 +21,7 @@ #include "Provider.hpp" -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class IProviderRegistry { @@ -33,4 +33,4 @@ public: virtual QStringList providersNames() const = 0; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/IToolsManager.hpp b/pluginllmcore/IToolsManager.hpp similarity index 97% rename from llmcore/IToolsManager.hpp rename to pluginllmcore/IToolsManager.hpp index 0637773..92c74d1 100644 --- a/llmcore/IToolsManager.hpp +++ b/pluginllmcore/IToolsManager.hpp @@ -26,7 +26,7 @@ #include "BaseTool.hpp" -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class IToolsManager { diff --git a/llmcore/PromptProviderChat.hpp b/pluginllmcore/PromptProviderChat.hpp similarity index 95% rename from llmcore/PromptProviderChat.hpp rename to pluginllmcore/PromptProviderChat.hpp index 19804f1..5de59d4 100644 --- a/llmcore/PromptProviderChat.hpp +++ b/pluginllmcore/PromptProviderChat.hpp @@ -23,7 +23,7 @@ #include "PromptTemplate.hpp" #include "PromptTemplateManager.hpp" -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class PromptProviderChat : public IPromptProvider { @@ -50,4 +50,4 @@ private: PromptTemplateManager &m_templateManager; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/PromptProviderFim.hpp b/pluginllmcore/PromptProviderFim.hpp similarity index 95% rename from llmcore/PromptProviderFim.hpp rename to pluginllmcore/PromptProviderFim.hpp index 19529c0..6e9d006 100644 --- a/llmcore/PromptProviderFim.hpp +++ b/pluginllmcore/PromptProviderFim.hpp @@ -22,7 +22,7 @@ #include "IPromptProvider.hpp" #include "PromptTemplateManager.hpp" -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class PromptProviderFim : public IPromptProvider { @@ -49,4 +49,4 @@ private: PromptTemplateManager &m_templateManager; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/PromptTemplate.hpp b/pluginllmcore/PromptTemplate.hpp similarity index 94% rename from llmcore/PromptTemplate.hpp rename to pluginllmcore/PromptTemplate.hpp index 07eee24..d320041 100644 --- a/llmcore/PromptTemplate.hpp +++ b/pluginllmcore/PromptTemplate.hpp @@ -26,7 +26,7 @@ #include "ContextData.hpp" #include "ProviderID.hpp" -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { enum class TemplateType { Chat, FIM, FIMOnChat }; @@ -41,4 +41,4 @@ public: virtual QString description() const = 0; virtual bool isSupportProvider(ProviderID id) const = 0; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/PromptTemplateManager.cpp b/pluginllmcore/PromptTemplateManager.cpp similarity index 97% rename from llmcore/PromptTemplateManager.cpp rename to pluginllmcore/PromptTemplateManager.cpp index 804b52c..1ba002f 100644 --- a/llmcore/PromptTemplateManager.cpp +++ b/pluginllmcore/PromptTemplateManager.cpp @@ -21,7 +21,7 @@ #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { PromptTemplateManager &PromptTemplateManager::instance() { @@ -96,4 +96,4 @@ PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &temp return m_chatTemplates[templateName]; } -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/PromptTemplateManager.hpp b/pluginllmcore/PromptTemplateManager.hpp similarity index 96% rename from llmcore/PromptTemplateManager.hpp rename to pluginllmcore/PromptTemplateManager.hpp index 94a9381..7b359c7 100644 --- a/llmcore/PromptTemplateManager.hpp +++ b/pluginllmcore/PromptTemplateManager.hpp @@ -24,7 +24,7 @@ #include "PromptTemplate.hpp" -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class PromptTemplateManager { @@ -62,4 +62,4 @@ private: QMap m_chatTemplates; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/Provider.cpp b/pluginllmcore/Provider.cpp new file mode 100644 index 0000000..fd2e03b --- /dev/null +++ b/pluginllmcore/Provider.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024-2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#include "Provider.hpp" + +#include +#include + +#include + +namespace QodeAssist::PluginLLMCore { + +Provider::Provider(QObject *parent) + : QObject(parent) +{} + +RequestID Provider::sendRequest(const QUrl &url, const QJsonObject &payload) +{ + auto *c = client(); + + QUrl baseUrl(url); + baseUrl.setPath(""); + c->setUrl(baseUrl.toString()); + c->setApiKey(apiKey()); + + auto requestId = c->sendMessage(payload); + + LOG_MESSAGE( + QString("%1: Sending request %2 to %3").arg(name(), requestId, url.toString())); + + return requestId; +} + +void Provider::cancelRequest(const RequestID &requestId) +{ + LOG_MESSAGE(QString("%1: Cancelling request %2").arg(name(), requestId)); + client()->cancelRequest(requestId); +} + +::LLMCore::ToolsManager *Provider::toolsManager() const +{ + return client()->tools(); +} + +} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/Provider.hpp b/pluginllmcore/Provider.hpp new file mode 100644 index 0000000..1c36b13 --- /dev/null +++ b/pluginllmcore/Provider.hpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024-2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "ContextData.hpp" +#include "IToolsManager.hpp" +#include "PromptTemplate.hpp" +#include "RequestType.hpp" + +namespace LLMCore { +class BaseClient; +class ToolsManager; +} + +class QJsonObject; + +namespace QodeAssist::PluginLLMCore { + +enum class ProviderCapability { + Tools = 0x1, + Thinking = 0x2, + Image = 0x4, + ModelListing = 0x8, +}; +Q_DECLARE_FLAGS(ProviderCapabilities, ProviderCapability) +Q_DECLARE_OPERATORS_FOR_FLAGS(ProviderCapabilities) + +class Provider : public QObject +{ + Q_OBJECT +public: + explicit Provider(QObject *parent = nullptr); + + virtual ~Provider() = default; + + virtual QString name() const = 0; + virtual QString url() const = 0; + virtual QString completionEndpoint() const = 0; + virtual QString chatEndpoint() const = 0; + virtual void prepareRequest( + QJsonObject &request, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, + bool isToolsEnabled, + bool isThinkingEnabled) + = 0; + virtual QFuture> getInstalledModels(const QString &url) = 0; + virtual ProviderID providerID() const = 0; + virtual ProviderCapabilities capabilities() const { return {}; } + + virtual ::LLMCore::BaseClient *client() const = 0; + virtual QString apiKey() const = 0; + + RequestID sendRequest(const QUrl &url, const QJsonObject &payload); + void cancelRequest(const RequestID &requestId); + ::LLMCore::ToolsManager *toolsManager() const; +}; + +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/ProviderID.hpp b/pluginllmcore/ProviderID.hpp similarity index 96% rename from llmcore/ProviderID.hpp rename to pluginllmcore/ProviderID.hpp index 19e7b5b..dc1ea6e 100644 --- a/llmcore/ProviderID.hpp +++ b/pluginllmcore/ProviderID.hpp @@ -17,7 +17,7 @@ * along with QodeAssist. If not, see . */ -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { enum class ProviderID { Any, diff --git a/llmcore/ProvidersManager.cpp b/pluginllmcore/ProvidersManager.cpp similarity index 94% rename from llmcore/ProvidersManager.cpp rename to pluginllmcore/ProvidersManager.cpp index 9a48866..a2abf15 100644 --- a/llmcore/ProvidersManager.cpp +++ b/pluginllmcore/ProvidersManager.cpp @@ -19,7 +19,7 @@ #include "ProvidersManager.hpp" -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { ProvidersManager &ProvidersManager::instance() { @@ -44,4 +44,4 @@ Provider *ProvidersManager::getProviderByName(const QString &providerName) return m_providers[providerName]; } -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/ProvidersManager.hpp b/pluginllmcore/ProvidersManager.hpp similarity index 95% rename from llmcore/ProvidersManager.hpp rename to pluginllmcore/ProvidersManager.hpp index a985233..789a946 100644 --- a/llmcore/ProvidersManager.hpp +++ b/pluginllmcore/ProvidersManager.hpp @@ -24,7 +24,7 @@ #include "IProviderRegistry.hpp" #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class ProvidersManager : public IProviderRegistry { @@ -53,4 +53,4 @@ private: QMap m_providers; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/RequestConfig.hpp b/pluginllmcore/RequestConfig.hpp similarity index 92% rename from llmcore/RequestConfig.hpp rename to pluginllmcore/RequestConfig.hpp index e6d5413..113536a 100644 --- a/llmcore/RequestConfig.hpp +++ b/pluginllmcore/RequestConfig.hpp @@ -25,7 +25,7 @@ #include #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { struct LLMConfig { @@ -35,7 +35,6 @@ struct LLMConfig QJsonObject providerRequest; RequestType requestType; bool multiLineCompletion; - QString apiKey; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/RequestHandlerBase.cpp b/pluginllmcore/RequestHandlerBase.cpp similarity index 100% rename from llmcore/RequestHandlerBase.cpp rename to pluginllmcore/RequestHandlerBase.cpp diff --git a/llmcore/RequestHandlerBase.hpp b/pluginllmcore/RequestHandlerBase.hpp similarity index 100% rename from llmcore/RequestHandlerBase.hpp rename to pluginllmcore/RequestHandlerBase.hpp diff --git a/llmcore/RequestType.hpp b/pluginllmcore/RequestType.hpp similarity index 95% rename from llmcore/RequestType.hpp rename to pluginllmcore/RequestType.hpp index 1b9b5aa..3dd47e6 100644 --- a/llmcore/RequestType.hpp +++ b/pluginllmcore/RequestType.hpp @@ -21,7 +21,7 @@ #pragma once -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { enum RequestType { CodeCompletion, Chat, Embedding, QuickRefactoring }; diff --git a/llmcore/ResponseCleaner.hpp b/pluginllmcore/ResponseCleaner.hpp similarity index 97% rename from llmcore/ResponseCleaner.hpp rename to pluginllmcore/ResponseCleaner.hpp index e2dc683..6363045 100644 --- a/llmcore/ResponseCleaner.hpp +++ b/pluginllmcore/ResponseCleaner.hpp @@ -23,7 +23,7 @@ #include #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class ResponseCleaner { @@ -115,5 +115,5 @@ private: } }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/RulesLoader.cpp b/pluginllmcore/RulesLoader.cpp similarity index 98% rename from llmcore/RulesLoader.cpp rename to pluginllmcore/RulesLoader.cpp index abb2100..346a122 100644 --- a/llmcore/RulesLoader.cpp +++ b/pluginllmcore/RulesLoader.cpp @@ -26,7 +26,7 @@ #include #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { QString RulesLoader::loadRules(const QString &projectPath, RulesContext context) { @@ -178,4 +178,4 @@ QVector RulesLoader::collectMarkdownFiles( return result; } -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/RulesLoader.hpp b/pluginllmcore/RulesLoader.hpp similarity index 95% rename from llmcore/RulesLoader.hpp rename to pluginllmcore/RulesLoader.hpp index 2fbe76e..437f75c 100644 --- a/llmcore/RulesLoader.hpp +++ b/pluginllmcore/RulesLoader.hpp @@ -25,7 +25,7 @@ namespace ProjectExplorer { class Project; } -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { enum class RulesContext { Completions, Chat, QuickRefactor }; @@ -54,4 +54,4 @@ private: static QString getProjectPath(ProjectExplorer::Project *project); }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/SSEBuffer.cpp b/pluginllmcore/SSEBuffer.cpp similarity index 93% rename from llmcore/SSEBuffer.cpp rename to pluginllmcore/SSEBuffer.cpp index bc53c29..c0a7948 100644 --- a/llmcore/SSEBuffer.cpp +++ b/pluginllmcore/SSEBuffer.cpp @@ -19,7 +19,7 @@ #include "SSEBuffer.hpp" -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { QStringList SSEBuffer::processData(const QByteArray &data) { @@ -48,4 +48,4 @@ bool SSEBuffer::hasIncompleteData() const return !m_buffer.isEmpty(); } -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/llmcore/SSEBuffer.hpp b/pluginllmcore/SSEBuffer.hpp similarity index 92% rename from llmcore/SSEBuffer.hpp rename to pluginllmcore/SSEBuffer.hpp index 1f05572..629444f 100644 --- a/llmcore/SSEBuffer.hpp +++ b/pluginllmcore/SSEBuffer.hpp @@ -22,7 +22,7 @@ #include #include -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class SSEBuffer { @@ -39,4 +39,4 @@ private: QString m_buffer; }; -} // namespace QodeAssist::LLMCore +} // namespace QodeAssist::PluginLLMCore diff --git a/providers/ClaudeMessage.cpp b/providers/ClaudeMessage.cpp index 29397a4..13f48f5 100644 --- a/providers/ClaudeMessage.cpp +++ b/providers/ClaudeMessage.cpp @@ -37,32 +37,32 @@ void ClaudeMessage::handleContentBlockStart( .arg(blockType)); if (blockType == "text") { - addCurrentContent(); + addCurrentContent(); } else if (blockType == "image") { QJsonObject source = data["source"].toObject(); QString sourceType = source["type"].toString(); QString imageData; QString mediaType; - LLMCore::ImageContent::ImageSourceType imgSourceType = LLMCore::ImageContent::ImageSourceType::Base64; + PluginLLMCore::ImageContent::ImageSourceType imgSourceType = PluginLLMCore::ImageContent::ImageSourceType::Base64; if (sourceType == "base64") { imageData = source["data"].toString(); mediaType = source["media_type"].toString(); - imgSourceType = LLMCore::ImageContent::ImageSourceType::Base64; + imgSourceType = PluginLLMCore::ImageContent::ImageSourceType::Base64; } else if (sourceType == "url") { imageData = source["url"].toString(); - imgSourceType = LLMCore::ImageContent::ImageSourceType::Url; + imgSourceType = PluginLLMCore::ImageContent::ImageSourceType::Url; } - addCurrentContent(imageData, mediaType, imgSourceType); + addCurrentContent(imageData, mediaType, imgSourceType); } else if (blockType == "tool_use") { QString toolId = data["id"].toString(); QString toolName = data["name"].toString(); QJsonObject toolInput = data["input"].toObject(); - addCurrentContent(toolId, toolName, toolInput); + addCurrentContent(toolId, toolName, toolInput); m_pendingToolInputs[index] = ""; } else if (blockType == "thinking") { @@ -70,13 +70,13 @@ void ClaudeMessage::handleContentBlockStart( QString signature = data["signature"].toString(); LOG_MESSAGE(QString("ClaudeMessage: Creating thinking block with signature length=%1") .arg(signature.length())); - addCurrentContent(thinking, signature); + addCurrentContent(thinking, signature); } else if (blockType == "redacted_thinking") { QString signature = data["signature"].toString(); LOG_MESSAGE(QString("ClaudeMessage: Creating redacted_thinking block with signature length=%1") .arg(signature.length())); - addCurrentContent(signature); + addCurrentContent(signature); } } @@ -88,7 +88,7 @@ void ClaudeMessage::handleContentBlockDelta( } if (deltaType == "text_delta") { - if (auto textContent = qobject_cast(m_currentBlocks[index])) { + if (auto textContent = qobject_cast(m_currentBlocks[index])) { textContent->appendText(delta["text"].toString()); } @@ -99,17 +99,17 @@ void ClaudeMessage::handleContentBlockDelta( } } else if (deltaType == "thinking_delta") { - if (auto thinkingContent = qobject_cast(m_currentBlocks[index])) { + if (auto thinkingContent = qobject_cast(m_currentBlocks[index])) { thinkingContent->appendThinking(delta["thinking"].toString()); } } else if (deltaType == "signature_delta") { - if (auto thinkingContent = qobject_cast(m_currentBlocks[index])) { + if (auto thinkingContent = qobject_cast(m_currentBlocks[index])) { QString signature = delta["signature"].toString(); thinkingContent->setSignature(signature); LOG_MESSAGE(QString("Set signature for thinking block %1: length=%2") .arg(index).arg(signature.length())); - } else if (auto redactedContent = qobject_cast(m_currentBlocks[index])) { + } else if (auto redactedContent = qobject_cast(m_currentBlocks[index])) { QString signature = delta["signature"].toString(); redactedContent->setSignature(signature); LOG_MESSAGE(QString("Set signature for redacted_thinking block %1: length=%2") @@ -132,7 +132,7 @@ void ClaudeMessage::handleContentBlockStop(int index) } if (index < m_currentBlocks.size()) { - if (auto toolContent = qobject_cast(m_currentBlocks[index])) { + if (auto toolContent = qobject_cast(m_currentBlocks[index])) { toolContent->setInput(inputObject); } } @@ -155,7 +155,7 @@ QJsonObject ClaudeMessage::toProviderFormat() const QJsonArray content; for (auto block : m_currentBlocks) { - QJsonValue blockJson = block->toJson(LLMCore::ProviderFormat::Claude); + QJsonValue blockJson = block->toJson(PluginLLMCore::ProviderFormat::Claude); content.append(blockJson); } @@ -173,42 +173,42 @@ QJsonArray ClaudeMessage::createToolResultsContent(const QHash for (auto toolContent : getCurrentToolUseContent()) { if (toolResults.contains(toolContent->id())) { - auto toolResult = std::make_unique( + auto toolResult = std::make_unique( toolContent->id(), toolResults[toolContent->id()]); - results.append(toolResult->toJson(LLMCore::ProviderFormat::Claude)); + results.append(toolResult->toJson(PluginLLMCore::ProviderFormat::Claude)); } } return results; } -QList ClaudeMessage::getCurrentToolUseContent() const +QList ClaudeMessage::getCurrentToolUseContent() const { - QList toolBlocks; + QList toolBlocks; for (auto block : m_currentBlocks) { - if (auto toolContent = qobject_cast(block)) { + if (auto toolContent = qobject_cast(block)) { toolBlocks.append(toolContent); } } return toolBlocks; } -QList ClaudeMessage::getCurrentThinkingContent() const +QList ClaudeMessage::getCurrentThinkingContent() const { - QList thinkingBlocks; + QList thinkingBlocks; for (auto block : m_currentBlocks) { - if (auto thinkingContent = qobject_cast(block)) { + if (auto thinkingContent = qobject_cast(block)) { thinkingBlocks.append(thinkingContent); } } return thinkingBlocks; } -QList ClaudeMessage::getCurrentRedactedThinkingContent() const +QList ClaudeMessage::getCurrentRedactedThinkingContent() const { - QList redactedBlocks; + QList redactedBlocks; for (auto block : m_currentBlocks) { - if (auto redactedContent = qobject_cast(block)) { + if (auto redactedContent = qobject_cast(block)) { redactedBlocks.append(redactedContent); } } @@ -222,17 +222,17 @@ void ClaudeMessage::startNewContinuation() m_currentBlocks.clear(); m_pendingToolInputs.clear(); m_stopReason.clear(); - m_state = LLMCore::MessageState::Building; + m_state = PluginLLMCore::MessageState::Building; } void ClaudeMessage::updateStateFromStopReason() { if (m_stopReason == "tool_use" && !getCurrentToolUseContent().empty()) { - m_state = LLMCore::MessageState::RequiresToolExecution; + m_state = PluginLLMCore::MessageState::RequiresToolExecution; } else if (m_stopReason == "end_turn") { - m_state = LLMCore::MessageState::Final; + m_state = PluginLLMCore::MessageState::Final; } else { - m_state = LLMCore::MessageState::Complete; + m_state = PluginLLMCore::MessageState::Complete; } } diff --git a/providers/ClaudeMessage.hpp b/providers/ClaudeMessage.hpp index 5c6b623..68efdc8 100644 --- a/providers/ClaudeMessage.hpp +++ b/providers/ClaudeMessage.hpp @@ -19,7 +19,7 @@ #pragma once -#include +#include namespace QodeAssist { @@ -37,18 +37,18 @@ public: QJsonObject toProviderFormat() const; QJsonArray createToolResultsContent(const QHash &toolResults) const; - LLMCore::MessageState state() const { return m_state; } - QList getCurrentToolUseContent() const; - QList getCurrentThinkingContent() const; - QList getCurrentRedactedThinkingContent() const; - const QList &getCurrentBlocks() const { return m_currentBlocks; } + PluginLLMCore::MessageState state() const { return m_state; } + QList getCurrentToolUseContent() const; + QList getCurrentThinkingContent() const; + QList getCurrentRedactedThinkingContent() const; + const QList &getCurrentBlocks() const { return m_currentBlocks; } void startNewContinuation(); private: QString m_stopReason; - LLMCore::MessageState m_state = LLMCore::MessageState::Building; - QList m_currentBlocks; + PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building; + QList m_currentBlocks; QHash m_pendingToolInputs; void updateStateFromStopReason(); diff --git a/providers/ClaudeProvider.cpp b/providers/ClaudeProvider.cpp index 8094deb..8da5cd4 100644 --- a/providers/ClaudeProvider.cpp +++ b/providers/ClaudeProvider.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -22,27 +22,24 @@ #include #include #include -#include -#include "llmcore/ValidationUtils.hpp" +#include + #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" #include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProviderSettings.hpp" +#include "tools/ToolsRegistration.hpp" namespace QodeAssist::Providers { ClaudeProvider::ClaudeProvider(QObject *parent) - : LLMCore::Provider(parent) - , m_toolsManager(new Tools::ToolsManager(this)) + : PluginLLMCore::Provider(parent) + , m_client(new ::LLMCore::ClaudeClient(QString(), QString(), QString(), this)) { - connect( - m_toolsManager, - &Tools::ToolsManager::toolExecutionComplete, - this, - &ClaudeProvider::onToolExecutionComplete); + Tools::registerQodeAssistTools(m_client->tools()); } QString ClaudeProvider::name() const @@ -50,6 +47,11 @@ QString ClaudeProvider::name() const return "Claude"; } +QString ClaudeProvider::apiKey() const +{ + return Settings::providerSettings().claudeApiKey(); +} + QString ClaudeProvider::url() const { return "https://api.anthropic.com"; @@ -65,16 +67,11 @@ QString ClaudeProvider::chatEndpoint() const return "/v1/messages"; } -bool ClaudeProvider::supportsModelListing() const -{ - return true; -} - void ClaudeProvider::prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) { @@ -102,10 +99,10 @@ void ClaudeProvider::prepareRequest( request["temperature"] = 1.0; }; - if (type == LLMCore::RequestType::CodeCompletion) { + if (type == PluginLLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); request["temperature"] = Settings::codeCompletionSettings().temperature(); - } else if (type == LLMCore::RequestType::QuickRefactoring) { + } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { const auto &qrSettings = Settings::quickRefactorSettings(); applyModelParams(qrSettings); @@ -126,13 +123,8 @@ void ClaudeProvider::prepareRequest( } if (isToolsEnabled) { - LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL; - if (type == LLMCore::RequestType::QuickRefactoring) { - filter = LLMCore::RunToolsFilter::OnlyRead; - } + auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - auto toolsDefinitions = m_toolsManager->getToolsDefinitions( - LLMCore::ToolSchemaFormat::Claude, filter); if (!toolsDefinitions.isEmpty()) { request["tools"] = toolsDefinitions; LOG_MESSAGE(QString("Added %1 tools to Claude request").arg(toolsDefinitions.size())); @@ -142,407 +134,26 @@ void ClaudeProvider::prepareRequest( QFuture> ClaudeProvider::getInstalledModels(const QString &baseUrl) { - QUrl url(baseUrl + "/v1/models"); - QUrlQuery query; - query.addQueryItem("limit", "1000"); - url.setQuery(query); - - QNetworkRequest request(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("anthropic-version", "2023-06-01"); - - if (!apiKey().isEmpty()) { - request.setRawHeader("x-api-key", apiKey().toUtf8()); - } - - return httpClient()->get(request).then([](const QByteArray &data) { - QList models; - QJsonObject jsonObject = QJsonDocument::fromJson(data).object(); - - if (jsonObject.contains("data")) { - QJsonArray modelArray = jsonObject["data"].toArray(); - for (const QJsonValue &value : modelArray) { - QJsonObject modelObject = value.toObject(); - if (modelObject.contains("id")) { - models.append(modelObject["id"].toString()); - } - } - } - return models; - }).onFailed([](const std::exception &e) { - LOG_MESSAGE(QString("Error fetching Claude models: %1").arg(e.what())); - return QList{}; - }); + m_client->setUrl(baseUrl); + m_client->setApiKey(apiKey()); + return m_client->listModels(); } -QList ClaudeProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type) +PluginLLMCore::ProviderID ClaudeProvider::providerID() const { - const auto templateReq = QJsonObject{ - {"model", {}}, - {"system", {}}, - {"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}}, - {"temperature", {}}, - {"max_tokens", {}}, - {"anthropic-version", {}}, - {"top_p", {}}, - {"top_k", {}}, - {"stop", QJsonArray{}}, - {"stream", {}}, - {"tools", {}}, - {"thinking", QJsonObject{{"type", {}}, {"budget_tokens", {}}}}}; - - return LLMCore::ValidationUtils::validateRequestFields(request, templateReq); + return PluginLLMCore::ProviderID::Claude; } -QString ClaudeProvider::apiKey() const +PluginLLMCore::ProviderCapabilities ClaudeProvider::capabilities() const { - return Settings::providerSettings().claudeApiKey(); + return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking + | PluginLLMCore::ProviderCapability::Image + | PluginLLMCore::ProviderCapability::ModelListing; } -void ClaudeProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const +::LLMCore::BaseClient *ClaudeProvider::client() const { - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - networkRequest.setRawHeader("anthropic-version", "2023-06-01"); - - if (!apiKey().isEmpty()) { - networkRequest.setRawHeader("x-api-key", apiKey().toUtf8()); - } -} - -LLMCore::ProviderID ClaudeProvider::providerID() const -{ - return LLMCore::ProviderID::Claude; -} - -void ClaudeProvider::sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) -{ - if (!m_messages.contains(requestId)) { - m_dataBuffers[requestId].clear(); - } - - m_requestUrls[requestId] = url; - m_originalRequests[requestId] = payload; - - QNetworkRequest networkRequest(url); - prepareNetworkRequest(networkRequest); - - LOG_MESSAGE(QString("ClaudeProvider: Sending request %1 to %2").arg(requestId, url.toString())); - - httpClient()->postStreaming(requestId, networkRequest, payload); -} - -bool ClaudeProvider::supportsTools() const -{ - return true; -} - -bool ClaudeProvider::supportThinking() const { - return true; -}; - -bool ClaudeProvider::supportImage() const { - return true; -}; - -void ClaudeProvider::cancelRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("ClaudeProvider: Cancelling request %1").arg(requestId)); - LLMCore::Provider::cancelRequest(requestId); - cleanupRequest(requestId); -} - -LLMCore::IToolsManager *ClaudeProvider::toolsManager() const -{ - return m_toolsManager; -} - -void ClaudeProvider::onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) -{ - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - QStringList lines = buffers.rawStreamBuffer.processData(data); - - for (const QString &line : lines) { - QJsonObject responseObj = parseEventLine(line); - if (responseObj.isEmpty()) - continue; - - processStreamEvent(requestId, responseObj); - } -} - -void ClaudeProvider::onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, std::optional error) -{ - if (error) { - LOG_MESSAGE(QString("ClaudeProvider request %1 failed: %2").arg(requestId, *error)); - emit requestFailed(requestId, *error); - cleanupRequest(requestId); - return; - } - - if (m_messages.contains(requestId)) { - ClaudeMessage *message = m_messages[requestId]; - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Waiting for tools to complete for %1").arg(requestId)); - m_dataBuffers.remove(requestId); - return; - } - } - - if (m_dataBuffers.contains(requestId)) { - const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - if (!buffers.responseContent.isEmpty()) { - LOG_MESSAGE(QString("Emitting full response for %1").arg(requestId)); - emit fullResponseReceived(requestId, buffers.responseContent); - } - } - - cleanupRequest(requestId); -} - -void ClaudeProvider::onToolExecutionComplete( - const QString &requestId, const QHash &toolResults) -{ - if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) { - LOG_MESSAGE(QString("ERROR: Missing data for continuation request %1").arg(requestId)); - cleanupRequest(requestId); - return; - } - - LOG_MESSAGE(QString("Tool execution complete for Claude request %1").arg(requestId)); - - for (auto it = toolResults.begin(); it != toolResults.end(); ++it) { - ClaudeMessage *message = m_messages[requestId]; - auto toolContent = message->getCurrentToolUseContent(); - for (auto tool : toolContent) { - if (tool->id() == it.key()) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(tool->name()); - emit toolExecutionCompleted( - requestId, tool->id(), toolStringName, toolResults[tool->id()]); - break; - } - } - } - - ClaudeMessage *message = m_messages[requestId]; - QJsonObject continuationRequest = m_originalRequests[requestId]; - QJsonArray messages = continuationRequest["messages"].toArray(); - - messages.append(message->toProviderFormat()); - - QJsonObject userMessage; - userMessage["role"] = "user"; - userMessage["content"] = message->createToolResultsContent(toolResults); - messages.append(userMessage); - - continuationRequest["messages"] = messages; - - if (continuationRequest.contains("thinking")) { - QJsonObject thinkingObj = continuationRequest["thinking"].toObject(); - LOG_MESSAGE(QString("Thinking mode preserved for continuation: type=%1, budget=%2 tokens") - .arg(thinkingObj["type"].toString()) - .arg(thinkingObj["budget_tokens"].toInt())); - } - - LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results") - .arg(requestId) - .arg(toolResults.size())); - - sendRequest(requestId, m_requestUrls[requestId], continuationRequest); -} - -void ClaudeProvider::processStreamEvent(const QString &requestId, const QJsonObject &event) -{ - QString eventType = event["type"].toString(); - - if (eventType == "message_stop") { - return; - } - - ClaudeMessage *message = m_messages.value(requestId); - if (!message) { - if (eventType == "message_start") { - message = new ClaudeMessage(this); - m_messages[requestId] = message; - LOG_MESSAGE(QString("Created NEW ClaudeMessage for request %1").arg(requestId)); - } else { - return; - } - } - - if (eventType == "message_start") { - message->startNewContinuation(); - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Starting NEW continuation for request %1").arg(requestId)); - - } else if (eventType == "content_block_start") { - int index = event["index"].toInt(); - QJsonObject contentBlock = event["content_block"].toObject(); - QString blockType = contentBlock["type"].toString(); - - LOG_MESSAGE( - QString("Adding new content block: type=%1, index=%2").arg(blockType).arg(index)); - - if (blockType == "thinking" || blockType == "redacted_thinking") { - QJsonDocument eventDoc(event); - LOG_MESSAGE(QString("content_block_start event for %1: %2") - .arg(blockType) - .arg(QString::fromUtf8(eventDoc.toJson(QJsonDocument::Compact)))); - } - - message->handleContentBlockStart(index, blockType, contentBlock); - - } else if (eventType == "content_block_delta") { - int index = event["index"].toInt(); - QJsonObject delta = event["delta"].toObject(); - QString deltaType = delta["type"].toString(); - - message->handleContentBlockDelta(index, deltaType, delta); - - if (deltaType == "text_delta") { - QString text = delta["text"].toString(); - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - buffers.responseContent += text; - emit partialResponseReceived(requestId, text); - } else if (deltaType == "signature_delta") { - QString signature = delta["signature"].toString(); - } - - } else if (eventType == "content_block_stop") { - int index = event["index"].toInt(); - - auto allBlocks = message->getCurrentBlocks(); - if (index < allBlocks.size()) { - QString blockType = allBlocks[index]->type(); - if (blockType == "thinking" || blockType == "redacted_thinking") { - QJsonDocument eventDoc(event); - LOG_MESSAGE(QString("content_block_stop event for %1 at index %2: %3") - .arg(blockType) - .arg(index) - .arg(QString::fromUtf8(eventDoc.toJson(QJsonDocument::Compact)))); - } - } - - if (event.contains("content_block")) { - QJsonObject contentBlock = event["content_block"].toObject(); - QString blockType = contentBlock["type"].toString(); - - if (blockType == "thinking") { - QString signature = contentBlock["signature"].toString(); - if (!signature.isEmpty()) { - auto allBlocks = message->getCurrentBlocks(); - if (index < allBlocks.size()) { - if (auto thinkingContent = qobject_cast(allBlocks[index])) { - thinkingContent->setSignature(signature); - LOG_MESSAGE( - QString("Updated thinking block signature from content_block_stop, " - "signature length=%1") - .arg(signature.length())); - } - } - } - } else if (blockType == "redacted_thinking") { - QString signature = contentBlock["signature"].toString(); - if (!signature.isEmpty()) { - auto allBlocks = message->getCurrentBlocks(); - if (index < allBlocks.size()) { - if (auto redactedContent = qobject_cast(allBlocks[index])) { - redactedContent->setSignature(signature); - LOG_MESSAGE( - QString("Updated redacted_thinking block signature from content_block_stop, " - "signature length=%1") - .arg(signature.length())); - } - } - } - } - } - - message->handleContentBlockStop(index); - - auto thinkingBlocks = message->getCurrentThinkingContent(); - for (auto thinkingContent : thinkingBlocks) { - auto allBlocks = message->getCurrentBlocks(); - if (index < allBlocks.size() && allBlocks[index] == thinkingContent) { - emit thinkingBlockReceived( - requestId, thinkingContent->thinking(), thinkingContent->signature()); - LOG_MESSAGE( - QString("Emitted thinking block for request %1, thinking length=%2, signature length=%3") - .arg(requestId) - .arg(thinkingContent->thinking().length()) - .arg(thinkingContent->signature().length())); - break; - } - } - - auto redactedBlocks = message->getCurrentRedactedThinkingContent(); - for (auto redactedContent : redactedBlocks) { - auto allBlocks = message->getCurrentBlocks(); - if (index < allBlocks.size() && allBlocks[index] == redactedContent) { - emit redactedThinkingBlockReceived(requestId, redactedContent->signature()); - LOG_MESSAGE( - QString("Emitted redacted thinking block for request %1, signature length=%2") - .arg(requestId) - .arg(redactedContent->signature().length())); - break; - } - } - - } else if (eventType == "message_delta") { - QJsonObject delta = event["delta"].toObject(); - if (delta.contains("stop_reason")) { - QString stopReason = delta["stop_reason"].toString(); - message->handleStopReason(stopReason); - handleMessageComplete(requestId); - } - } -} - -void ClaudeProvider::handleMessageComplete(const QString &requestId) -{ - if (!m_messages.contains(requestId)) - return; - - ClaudeMessage *message = m_messages[requestId]; - - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Claude message requires tool execution for %1").arg(requestId)); - - auto toolUseContent = message->getCurrentToolUseContent(); - - if (toolUseContent.isEmpty()) { - LOG_MESSAGE(QString("No tools to execute for %1").arg(requestId)); - return; - } - - for (auto toolContent : toolUseContent) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name()); - emit toolExecutionStarted(requestId, toolContent->id(), toolStringName); - - m_toolsManager->executeToolCall( - requestId, toolContent->id(), toolContent->name(), toolContent->input()); - } - - } else { - LOG_MESSAGE(QString("Claude message marked as complete for %1").arg(requestId)); - } -} - -void ClaudeProvider::cleanupRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("Cleaning up Claude request %1").arg(requestId)); - - if (m_messages.contains(requestId)) { - ClaudeMessage *message = m_messages.take(requestId); - message->deleteLater(); - } - - m_dataBuffers.remove(requestId); - m_requestUrls.remove(requestId); - m_originalRequests.remove(requestId); - m_toolsManager->cleanupRequest(requestId); + return m_client; } } // namespace QodeAssist::Providers diff --git a/providers/ClaudeProvider.hpp b/providers/ClaudeProvider.hpp index 335f6ac..143bab2 100644 --- a/providers/ClaudeProvider.hpp +++ b/providers/ClaudeProvider.hpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,14 +19,13 @@ #pragma once -#include +#include -#include "ClaudeMessage.hpp" -#include "tools/ToolsManager.hpp" +#include namespace QodeAssist::Providers { -class ClaudeProvider : public LLMCore::Provider +class ClaudeProvider : public PluginLLMCore::Provider { Q_OBJECT public: @@ -36,50 +35,22 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; - bool supportsModelListing() const override; void prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) override; QFuture> getInstalledModels(const QString &url) override; - QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + PluginLLMCore::ProviderID providerID() const override; + PluginLLMCore::ProviderCapabilities capabilities() const override; + + ::LLMCore::BaseClient *client() const override; QString apiKey() const override; - void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; - LLMCore::ProviderID providerID() const override; - - void sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override; - - bool supportsTools() const override; - bool supportThinking() const override; - bool supportImage() const override; - void cancelRequest(const LLMCore::RequestID &requestId) override; - - LLMCore::IToolsManager *toolsManager() const override; - -public slots: - void onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; - void onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, - std::optional error) override; - -private slots: - void onToolExecutionComplete( - const QString &requestId, const QHash &toolResults); private: - void processStreamEvent(const QString &requestId, const QJsonObject &event); - void handleMessageComplete(const QString &requestId); - void cleanupRequest(const LLMCore::RequestID &requestId); - - QHash m_messages; - QHash m_requestUrls; - QHash m_originalRequests; - Tools::ToolsManager *m_toolsManager; + ::LLMCore::ClaudeClient *m_client; }; } // namespace QodeAssist::Providers diff --git a/providers/CodestralProvider.cpp b/providers/CodestralProvider.cpp index 8c2902f..bd46639 100644 --- a/providers/CodestralProvider.cpp +++ b/providers/CodestralProvider.cpp @@ -23,24 +23,28 @@ namespace QodeAssist::Providers { +CodestralProvider::CodestralProvider(QObject *parent) + : MistralAIProvider(parent) +{} + QString CodestralProvider::name() const { return "Codestral"; } -QString CodestralProvider::url() const -{ - return "https://codestral.mistral.ai"; -} - -bool CodestralProvider::supportsModelListing() const -{ - return false; -} - QString CodestralProvider::apiKey() const { return Settings::providerSettings().codestralApiKey(); } +QString CodestralProvider::url() const +{ + return "https://codestral.mistral.ai"; +} + +PluginLLMCore::ProviderCapabilities CodestralProvider::capabilities() const +{ + return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image; +} + } // namespace QodeAssist::Providers diff --git a/providers/CodestralProvider.hpp b/providers/CodestralProvider.hpp index e4ea63c..80f27d4 100644 --- a/providers/CodestralProvider.hpp +++ b/providers/CodestralProvider.hpp @@ -26,10 +26,12 @@ namespace QodeAssist::Providers { class CodestralProvider : public MistralAIProvider { public: + explicit CodestralProvider(QObject *parent = nullptr); + QString name() const override; QString url() const override; - bool supportsModelListing() const override; QString apiKey() const override; + PluginLLMCore::ProviderCapabilities capabilities() const override; }; } // namespace QodeAssist::Providers diff --git a/providers/GoogleAIProvider.cpp b/providers/GoogleAIProvider.cpp index 599780c..f37855c 100644 --- a/providers/GoogleAIProvider.cpp +++ b/providers/GoogleAIProvider.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,12 +19,14 @@ #include "GoogleAIProvider.hpp" +#include + #include +#include "tools/ToolsRegistration.hpp" #include #include #include -#include "llmcore/ValidationUtils.hpp" #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" @@ -35,14 +37,10 @@ namespace QodeAssist::Providers { GoogleAIProvider::GoogleAIProvider(QObject *parent) - : LLMCore::Provider(parent) - , m_toolsManager(new Tools::ToolsManager(this)) + : PluginLLMCore::Provider(parent) + , m_client(new ::LLMCore::GoogleAIClient(QString(), QString(), QString(), this)) { - connect( - m_toolsManager, - &Tools::ToolsManager::toolExecutionComplete, - this, - &GoogleAIProvider::onToolExecutionComplete); + Tools::registerQodeAssistTools(m_client->tools()); } QString GoogleAIProvider::name() const @@ -50,6 +48,11 @@ QString GoogleAIProvider::name() const return "Google AI"; } +QString GoogleAIProvider::apiKey() const +{ + return Settings::providerSettings().googleAiApiKey(); +} + QString GoogleAIProvider::url() const { return "https://generativelanguage.googleapis.com/v1beta"; @@ -65,16 +68,11 @@ QString GoogleAIProvider::chatEndpoint() const return {}; } -bool GoogleAIProvider::supportsModelListing() const -{ - return true; -} - void GoogleAIProvider::prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) { @@ -119,9 +117,9 @@ void GoogleAIProvider::prepareRequest( request["generationConfig"] = generationConfig; }; - if (type == LLMCore::RequestType::CodeCompletion) { + if (type == PluginLLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); - } else if (type == LLMCore::RequestType::QuickRefactoring) { + } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { const auto &qrSettings = Settings::quickRefactorSettings(); if (isThinkingEnabled) { @@ -140,13 +138,7 @@ void GoogleAIProvider::prepareRequest( } if (isToolsEnabled) { - LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL; - if (type == LLMCore::RequestType::QuickRefactoring) { - filter = LLMCore::RunToolsFilter::OnlyRead; - } - - auto toolsDefinitions = m_toolsManager->getToolsDefinitions( - LLMCore::ToolSchemaFormat::Google, filter); + auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); if (!toolsDefinitions.isEmpty()) { request["tools"] = toolsDefinitions; LOG_MESSAGE(QString("Added %1 tools to Google AI request").arg(toolsDefinitions.size())); @@ -154,422 +146,28 @@ void GoogleAIProvider::prepareRequest( } } -QFuture> GoogleAIProvider::getInstalledModels(const QString &url) +QFuture> GoogleAIProvider::getInstalledModels(const QString &baseUrl) { - QNetworkRequest request(QString("%1/models?key=%2").arg(url, apiKey())); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - return httpClient()->get(request).then([](const QByteArray &data) { - QList models; - QJsonObject jsonObject = QJsonDocument::fromJson(data).object(); - - if (jsonObject.contains("models")) { - QJsonArray modelArray = jsonObject["models"].toArray(); - for (const QJsonValue &value : modelArray) { - QJsonObject modelObject = value.toObject(); - if (modelObject.contains("name")) { - QString modelName = modelObject["name"].toString(); - if (modelName.contains("/")) { - modelName = modelName.split("/").last(); - } - models.append(modelName); - } - } - } - return models; - }).onFailed([](const std::exception &e) { - LOG_MESSAGE(QString("Error fetching Google AI models: %1").arg(e.what())); - return QList{}; - }); + m_client->setUrl(baseUrl); + m_client->setApiKey(apiKey()); + return m_client->listModels(); } -QList GoogleAIProvider::validateRequest( - const QJsonObject &request, LLMCore::TemplateType type) +PluginLLMCore::ProviderID GoogleAIProvider::providerID() const { - QJsonObject templateReq; - - templateReq = QJsonObject{ - {"contents", QJsonArray{}}, - {"system_instruction", QJsonArray{}}, - {"generationConfig", - QJsonObject{ - {"temperature", {}}, - {"maxOutputTokens", {}}, - {"topP", {}}, - {"topK", {}}, - {"thinkingConfig", - QJsonObject{{"thinkingBudget", {}}, {"includeThoughts", {}}}}}}, - {"safetySettings", QJsonArray{}}, - {"tools", QJsonArray{}}}; - - return LLMCore::ValidationUtils::validateRequestFields(request, templateReq); + return PluginLLMCore::ProviderID::GoogleAI; } -QString GoogleAIProvider::apiKey() const +PluginLLMCore::ProviderCapabilities GoogleAIProvider::capabilities() const { - return Settings::providerSettings().googleAiApiKey(); + return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking + | PluginLLMCore::ProviderCapability::Image + | PluginLLMCore::ProviderCapability::ModelListing; } -void GoogleAIProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const +::LLMCore::BaseClient *GoogleAIProvider::client() const { - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - QUrl url = networkRequest.url(); - QUrlQuery query(url.query()); - query.addQueryItem("key", apiKey()); - url.setQuery(query); - networkRequest.setUrl(url); -} - -LLMCore::ProviderID GoogleAIProvider::providerID() const -{ - return LLMCore::ProviderID::GoogleAI; -} - -void GoogleAIProvider::sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) -{ - if (!m_messages.contains(requestId)) { - m_dataBuffers[requestId].clear(); - } - - m_requestUrls[requestId] = url; - m_originalRequests[requestId] = payload; - - QNetworkRequest networkRequest(url); - prepareNetworkRequest(networkRequest); - - LOG_MESSAGE( - QString("GoogleAIProvider: Sending request %1 to %2").arg(requestId, url.toString())); - - httpClient()->postStreaming(requestId, networkRequest, payload); -} - -bool GoogleAIProvider::supportsTools() const -{ - return true; -} - -bool GoogleAIProvider::supportThinking() const -{ - return true; -} - -bool GoogleAIProvider::supportImage() const -{ - return true; -} - -void GoogleAIProvider::cancelRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("GoogleAIProvider: Cancelling request %1").arg(requestId)); - LLMCore::Provider::cancelRequest(requestId); - cleanupRequest(requestId); -} - -void GoogleAIProvider::onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) -{ - if (data.isEmpty()) { - return; - } - - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); - if (!doc.isNull() && doc.isObject()) { - QJsonObject obj = doc.object(); - if (obj.contains("error")) { - QJsonObject error = obj["error"].toObject(); - QString errorMessage = error["message"].toString(); - int errorCode = error["code"].toInt(); - QString fullError - = QString("Google AI API Error %1: %2").arg(errorCode).arg(errorMessage); - - LOG_MESSAGE(fullError); - emit requestFailed(requestId, fullError); - cleanupRequest(requestId); - return; - } - } - - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - QStringList lines = buffers.rawStreamBuffer.processData(data); - - for (const QString &line : lines) { - if (line.trimmed().isEmpty()) { - continue; - } - - QJsonObject chunk = parseEventLine(line); - if (chunk.isEmpty()) - continue; - - processStreamChunk(requestId, chunk); - } -} - -void GoogleAIProvider::onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, std::optional error) -{ - if (error) { - LOG_MESSAGE(QString("GoogleAIProvider request %1 failed: %2").arg(requestId, *error)); - emit requestFailed(requestId, *error); - cleanupRequest(requestId); - return; - } - - if (m_failedRequests.contains(requestId)) { - cleanupRequest(requestId); - return; - } - - emitPendingThinkingBlocks(requestId); - - if (m_messages.contains(requestId)) { - GoogleMessage *message = m_messages[requestId]; - - handleMessageComplete(requestId); - - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Waiting for tools to complete for %1").arg(requestId)); - m_dataBuffers.remove(requestId); - return; - } - } - - if (m_dataBuffers.contains(requestId)) { - const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - if (!buffers.responseContent.isEmpty()) { - emit fullResponseReceived(requestId, buffers.responseContent); - } else { - emit fullResponseReceived(requestId, QString()); - } - } else { - emit fullResponseReceived(requestId, QString()); - } - - cleanupRequest(requestId); -} - -void GoogleAIProvider::onToolExecutionComplete( - const QString &requestId, const QHash &toolResults) -{ - if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) { - LOG_MESSAGE(QString("ERROR: Missing data for continuation request %1").arg(requestId)); - cleanupRequest(requestId); - return; - } - - for (auto it = toolResults.begin(); it != toolResults.end(); ++it) { - GoogleMessage *message = m_messages[requestId]; - auto toolContent = message->getCurrentToolUseContent(); - for (auto tool : toolContent) { - if (tool->id() == it.key()) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(tool->name()); - emit toolExecutionCompleted( - requestId, tool->id(), toolStringName, toolResults[tool->id()]); - break; - } - } - } - - GoogleMessage *message = m_messages[requestId]; - QJsonObject continuationRequest = m_originalRequests[requestId]; - QJsonArray contents = continuationRequest["contents"].toArray(); - - contents.append(message->toProviderFormat()); - - QJsonObject userMessage; - userMessage["role"] = "user"; - userMessage["parts"] = message->createToolResultParts(toolResults); - contents.append(userMessage); - - continuationRequest["contents"] = contents; - - sendRequest(requestId, m_requestUrls[requestId], continuationRequest); -} - -void GoogleAIProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk) -{ - if (!chunk.contains("candidates")) { - return; - } - - GoogleMessage *message = m_messages.value(requestId); - if (!message) { - message = new GoogleMessage(this); - m_messages[requestId] = message; - LOG_MESSAGE(QString("Created NEW GoogleMessage for request %1").arg(requestId)); - - if (m_dataBuffers.contains(requestId)) { - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); - } - } else if ( - m_dataBuffers.contains(requestId) - && message->state() == LLMCore::MessageState::RequiresToolExecution) { - message->startNewContinuation(); - m_emittedThinkingBlocksCount[requestId] = 0; - LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); - } - - QJsonArray candidates = chunk["candidates"].toArray(); - for (const QJsonValue &candidate : candidates) { - QJsonObject candidateObj = candidate.toObject(); - - if (candidateObj.contains("content")) { - QJsonObject content = candidateObj["content"].toObject(); - if (content.contains("parts")) { - QJsonArray parts = content["parts"].toArray(); - for (const QJsonValue &part : parts) { - QJsonObject partObj = part.toObject(); - - if (partObj.contains("text")) { - QString text = partObj["text"].toString(); - bool isThought = partObj.value("thought").toBool(false); - - if (isThought) { - message->handleThoughtDelta(text); - - if (partObj.contains("signature")) { - QString signature = partObj["signature"].toString(); - message->handleThoughtSignature(signature); - } - } else { - emitPendingThinkingBlocks(requestId); - - message->handleContentDelta(text); - - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - buffers.responseContent += text; - emit partialResponseReceived(requestId, text); - } - } - - if (partObj.contains("thoughtSignature")) { - QString signature = partObj["thoughtSignature"].toString(); - message->handleThoughtSignature(signature); - } - - if (partObj.contains("functionCall")) { - emitPendingThinkingBlocks(requestId); - - QJsonObject functionCall = partObj["functionCall"].toObject(); - QString name = functionCall["name"].toString(); - QJsonObject args = functionCall["args"].toObject(); - - message->handleFunctionCallStart(name); - message->handleFunctionCallArgsDelta( - QString::fromUtf8(QJsonDocument(args).toJson(QJsonDocument::Compact))); - message->handleFunctionCallComplete(); - } - } - } - } - - if (candidateObj.contains("finishReason")) { - QString finishReason = candidateObj["finishReason"].toString(); - message->handleFinishReason(finishReason); - - if (message->isErrorFinishReason()) { - QString errorMessage = message->getErrorMessage(); - LOG_MESSAGE(QString("Google AI error: %1").arg(errorMessage)); - m_failedRequests.insert(requestId); - emit requestFailed(requestId, errorMessage); - return; - } - } - } - - if (chunk.contains("usageMetadata")) { - QJsonObject usageMetadata = chunk["usageMetadata"].toObject(); - int thoughtsTokenCount = usageMetadata.value("thoughtsTokenCount").toInt(0); - int candidatesTokenCount = usageMetadata.value("candidatesTokenCount").toInt(0); - int totalTokenCount = usageMetadata.value("totalTokenCount").toInt(0); - - if (totalTokenCount > 0) { - LOG_MESSAGE(QString("Google AI tokens: %1 (thoughts: %2, output: %3)") - .arg(totalTokenCount) - .arg(thoughtsTokenCount) - .arg(candidatesTokenCount)); - } - } -} - -void GoogleAIProvider::emitPendingThinkingBlocks(const QString &requestId) -{ - if (!m_messages.contains(requestId)) - return; - - GoogleMessage *message = m_messages[requestId]; - auto thinkingBlocks = message->getCurrentThinkingContent(); - - if (thinkingBlocks.isEmpty()) - return; - - int alreadyEmitted = m_emittedThinkingBlocksCount.value(requestId, 0); - int totalBlocks = thinkingBlocks.size(); - - for (int i = alreadyEmitted; i < totalBlocks; ++i) { - auto thinkingContent = thinkingBlocks[i]; - - if (thinkingContent->thinking().trimmed().isEmpty()) { - continue; - } - - emit thinkingBlockReceived( - requestId, - thinkingContent->thinking(), - thinkingContent->signature()); - } - - m_emittedThinkingBlocksCount[requestId] = totalBlocks; -} - -void GoogleAIProvider::handleMessageComplete(const QString &requestId) -{ - if (!m_messages.contains(requestId)) - return; - - GoogleMessage *message = m_messages[requestId]; - - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Google AI message requires tool execution for %1").arg(requestId)); - - auto toolUseContent = message->getCurrentToolUseContent(); - - if (toolUseContent.isEmpty()) { - LOG_MESSAGE(QString("No tools to execute for %1").arg(requestId)); - return; - } - - for (auto toolContent : toolUseContent) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name()); - emit toolExecutionStarted(requestId, toolContent->id(), toolStringName); - m_toolsManager->executeToolCall( - requestId, toolContent->id(), toolContent->name(), toolContent->input()); - } - - } else { - LOG_MESSAGE(QString("Google AI message marked as complete for %1").arg(requestId)); - } -} - -void GoogleAIProvider::cleanupRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("Cleaning up Google AI request %1").arg(requestId)); - - if (m_messages.contains(requestId)) { - GoogleMessage *message = m_messages.take(requestId); - message->deleteLater(); - } - - m_dataBuffers.remove(requestId); - m_requestUrls.remove(requestId); - m_originalRequests.remove(requestId); - m_emittedThinkingBlocksCount.remove(requestId); - m_failedRequests.remove(requestId); - m_toolsManager->cleanupRequest(requestId); + return m_client; } } // namespace QodeAssist::Providers diff --git a/providers/GoogleAIProvider.hpp b/providers/GoogleAIProvider.hpp index f228c99..67a5b40 100644 --- a/providers/GoogleAIProvider.hpp +++ b/providers/GoogleAIProvider.hpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,13 +19,13 @@ #pragma once -#include "GoogleMessage.hpp" -#include "llmcore/Provider.hpp" -#include "tools/ToolsManager.hpp" +#include + +#include namespace QodeAssist::Providers { -class GoogleAIProvider : public LLMCore::Provider +class GoogleAIProvider : public PluginLLMCore::Provider { Q_OBJECT public: @@ -35,51 +35,22 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; - bool supportsModelListing() const override; void prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) override; QFuture> getInstalledModels(const QString &url) override; - QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + PluginLLMCore::ProviderID providerID() const override; + PluginLLMCore::ProviderCapabilities capabilities() const override; + + ::LLMCore::BaseClient *client() const override; QString apiKey() const override; - void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; - LLMCore::ProviderID providerID() const override; - - void sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override; - - bool supportsTools() const override; - bool supportThinking() const override; - bool supportImage() const override; - void cancelRequest(const LLMCore::RequestID &requestId) override; - -public slots: - void onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; - void onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, - std::optional error) override; - -private slots: - void onToolExecutionComplete( - const QString &requestId, const QHash &toolResults); private: - void processStreamChunk(const QString &requestId, const QJsonObject &chunk); - void handleMessageComplete(const QString &requestId); - void emitPendingThinkingBlocks(const QString &requestId); - void cleanupRequest(const LLMCore::RequestID &requestId); - - QHash m_messages; - QHash m_requestUrls; - QHash m_originalRequests; - QHash m_emittedThinkingBlocksCount; - QSet m_failedRequests; - Tools::ToolsManager *m_toolsManager; + ::LLMCore::GoogleAIClient *m_client; }; } // namespace QodeAssist::Providers diff --git a/providers/GoogleMessage.cpp b/providers/GoogleMessage.cpp index c2bfeeb..33be078 100644 --- a/providers/GoogleMessage.cpp +++ b/providers/GoogleMessage.cpp @@ -32,26 +32,26 @@ GoogleMessage::GoogleMessage(QObject *parent) void GoogleMessage::handleContentDelta(const QString &text) { - if (m_currentBlocks.isEmpty() || !qobject_cast(m_currentBlocks.last())) { - auto textContent = new LLMCore::TextContent(); + if (m_currentBlocks.isEmpty() || !qobject_cast(m_currentBlocks.last())) { + auto textContent = new PluginLLMCore::TextContent(); textContent->setParent(this); m_currentBlocks.append(textContent); } - if (auto textContent = qobject_cast(m_currentBlocks.last())) { + if (auto textContent = qobject_cast(m_currentBlocks.last())) { textContent->appendText(text); } } void GoogleMessage::handleThoughtDelta(const QString &text) { - if (m_currentBlocks.isEmpty() || !qobject_cast(m_currentBlocks.last())) { - auto thinkingContent = new LLMCore::ThinkingContent(); + if (m_currentBlocks.isEmpty() || !qobject_cast(m_currentBlocks.last())) { + auto thinkingContent = new PluginLLMCore::ThinkingContent(); thinkingContent->setParent(this); m_currentBlocks.append(thinkingContent); } - if (auto thinkingContent = qobject_cast(m_currentBlocks.last())) { + if (auto thinkingContent = qobject_cast(m_currentBlocks.last())) { thinkingContent->appendThinking(text); } } @@ -59,13 +59,13 @@ void GoogleMessage::handleThoughtDelta(const QString &text) void GoogleMessage::handleThoughtSignature(const QString &signature) { for (int i = m_currentBlocks.size() - 1; i >= 0; --i) { - if (auto thinkingContent = qobject_cast(m_currentBlocks[i])) { + if (auto thinkingContent = qobject_cast(m_currentBlocks[i])) { thinkingContent->setSignature(signature); return; } } - auto thinkingContent = new LLMCore::ThinkingContent(); + auto thinkingContent = new PluginLLMCore::ThinkingContent(); thinkingContent->setParent(this); thinkingContent->setSignature(signature); m_currentBlocks.append(thinkingContent); @@ -97,7 +97,7 @@ void GoogleMessage::handleFunctionCallComplete() } QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); - auto toolContent = new LLMCore::ToolUseContent(id, m_currentFunctionName, args); + auto toolContent = new PluginLLMCore::ToolUseContent(id, m_currentFunctionName, args); toolContent->setParent(this); m_currentBlocks.append(toolContent); @@ -122,14 +122,14 @@ QJsonObject GoogleMessage::toProviderFormat() const if (!block) continue; - if (auto text = qobject_cast(block)) { + if (auto text = qobject_cast(block)) { parts.append(QJsonObject{{"text", text->text()}}); - } else if (auto tool = qobject_cast(block)) { + } else if (auto tool = qobject_cast(block)) { QJsonObject functionCall; functionCall["name"] = tool->name(); functionCall["args"] = tool->input(); parts.append(QJsonObject{{"functionCall", functionCall}}); - } else if (auto thinking = qobject_cast(block)) { + } else if (auto thinking = qobject_cast(block)) { // Include thinking blocks with their text QJsonObject thinkingPart; thinkingPart["text"] = thinking->thinking(); @@ -169,22 +169,22 @@ QJsonArray GoogleMessage::createToolResultParts(const QHash &t return parts; } -QList GoogleMessage::getCurrentToolUseContent() const +QList GoogleMessage::getCurrentToolUseContent() const { - QList toolBlocks; + QList toolBlocks; for (auto block : m_currentBlocks) { - if (auto toolContent = qobject_cast(block)) { + if (auto toolContent = qobject_cast(block)) { toolBlocks.append(toolContent); } } return toolBlocks; } -QList GoogleMessage::getCurrentThinkingContent() const +QList GoogleMessage::getCurrentThinkingContent() const { - QList thinkingBlocks; + QList thinkingBlocks; for (auto block : m_currentBlocks) { - if (auto thinkingContent = qobject_cast(block)) { + if (auto thinkingContent = qobject_cast(block)) { thinkingBlocks.append(thinkingContent); } } @@ -199,7 +199,7 @@ void GoogleMessage::startNewContinuation() m_pendingFunctionArgs.clear(); m_currentFunctionName.clear(); m_finishReason.clear(); - m_state = LLMCore::MessageState::Building; + m_state = PluginLLMCore::MessageState::Building; } bool GoogleMessage::isErrorFinishReason() const @@ -234,10 +234,10 @@ void GoogleMessage::updateStateFromFinishReason() { if (m_finishReason == "STOP" || m_finishReason == "MAX_TOKENS") { m_state = getCurrentToolUseContent().isEmpty() - ? LLMCore::MessageState::Complete - : LLMCore::MessageState::RequiresToolExecution; + ? PluginLLMCore::MessageState::Complete + : PluginLLMCore::MessageState::RequiresToolExecution; } else { - m_state = LLMCore::MessageState::Complete; + m_state = PluginLLMCore::MessageState::Complete; } } diff --git a/providers/GoogleMessage.hpp b/providers/GoogleMessage.hpp index 036e8d1..39d9855 100644 --- a/providers/GoogleMessage.hpp +++ b/providers/GoogleMessage.hpp @@ -24,7 +24,7 @@ #include #include -#include +#include namespace QodeAssist::Providers { @@ -45,11 +45,11 @@ public: QJsonObject toProviderFormat() const; QJsonArray createToolResultParts(const QHash &toolResults) const; - QList getCurrentToolUseContent() const; - QList getCurrentThinkingContent() const; - QList currentBlocks() const { return m_currentBlocks; } + QList getCurrentToolUseContent() const; + QList getCurrentThinkingContent() const; + QList currentBlocks() const { return m_currentBlocks; } - LLMCore::MessageState state() const { return m_state; } + PluginLLMCore::MessageState state() const { return m_state; } QString finishReason() const { return m_finishReason; } bool isErrorFinishReason() const; QString getErrorMessage() const; @@ -58,11 +58,11 @@ public: private: void updateStateFromFinishReason(); - QList m_currentBlocks; + QList m_currentBlocks; QString m_pendingFunctionArgs; QString m_currentFunctionName; QString m_finishReason; - LLMCore::MessageState m_state = LLMCore::MessageState::Building; + PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building; }; } // namespace QodeAssist::Providers diff --git a/providers/LMStudioProvider.cpp b/providers/LMStudioProvider.cpp index 18b0dcf..eb42b2c 100644 --- a/providers/LMStudioProvider.cpp +++ b/providers/LMStudioProvider.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,7 +19,9 @@ #include "LMStudioProvider.hpp" -#include "llmcore/ValidationUtils.hpp" +#include + +#include "tools/ToolsRegistration.hpp" #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" @@ -34,14 +36,10 @@ namespace QodeAssist::Providers { LMStudioProvider::LMStudioProvider(QObject *parent) - : LLMCore::Provider(parent) - , m_toolsManager(new Tools::ToolsManager(this)) + : PluginLLMCore::Provider(parent) + , m_client(new ::LLMCore::OpenAIClient(QString(), QString(), QString(), this)) { - connect( - m_toolsManager, - &Tools::ToolsManager::toolExecutionComplete, - this, - &LMStudioProvider::onToolExecutionComplete); + Tools::registerQodeAssistTools(m_client->tools()); } QString LMStudioProvider::name() const @@ -49,6 +47,11 @@ QString LMStudioProvider::name() const return "LM Studio"; } +QString LMStudioProvider::apiKey() const +{ + return {}; +} + QString LMStudioProvider::url() const { return "http://localhost:1234"; @@ -64,155 +67,29 @@ QString LMStudioProvider::chatEndpoint() const return "/v1/chat/completions"; } -bool LMStudioProvider::supportsModelListing() const -{ - return true; -} - QFuture> LMStudioProvider::getInstalledModels(const QString &url) { - QNetworkRequest request(QString("%1%2").arg(url, "/v1/models")); - - return httpClient()->get(request).then([](const QByteArray &data) { - QList models; - QJsonObject jsonObject = QJsonDocument::fromJson(data).object(); - QJsonArray modelArray = jsonObject["data"].toArray(); - - for (const QJsonValue &value : modelArray) { - QJsonObject modelObject = value.toObject(); - models.append(modelObject["id"].toString()); - } - return models; - }).onFailed([](const std::exception &e) { - LOG_MESSAGE(QString("Error fetching LMStudio models: %1").arg(e.what())); - return QList{}; - }); + m_client->setUrl(url); + m_client->setApiKey(apiKey()); + return m_client->listModels(); } -QList LMStudioProvider::validateRequest( - const QJsonObject &request, LLMCore::TemplateType type) +PluginLLMCore::ProviderID LMStudioProvider::providerID() const { - const auto templateReq = QJsonObject{ - {"model", {}}, - {"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}}, - {"temperature", {}}, - {"max_tokens", {}}, - {"top_p", {}}, - {"top_k", {}}, - {"frequency_penalty", {}}, - {"presence_penalty", {}}, - {"stop", QJsonArray{}}, - {"stream", {}}, - {"tools", {}}}; - - return LLMCore::ValidationUtils::validateRequestFields(request, templateReq); + return PluginLLMCore::ProviderID::LMStudio; } -QString LMStudioProvider::apiKey() const +PluginLLMCore::ProviderCapabilities LMStudioProvider::capabilities() const { - return {}; -} - -void LMStudioProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const -{ - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); -} - -LLMCore::ProviderID LMStudioProvider::providerID() const -{ - return LLMCore::ProviderID::LMStudio; -} - -void LMStudioProvider::sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) -{ - if (!m_messages.contains(requestId)) { - m_dataBuffers[requestId].clear(); - } - - m_requestUrls[requestId] = url; - m_originalRequests[requestId] = payload; - - QNetworkRequest networkRequest(url); - prepareNetworkRequest(networkRequest); - - LOG_MESSAGE( - QString("LMStudioProvider: Sending request %1 to %2").arg(requestId, url.toString())); - - httpClient()->postStreaming(requestId, networkRequest, payload); -} - -bool LMStudioProvider::supportsTools() const -{ - return true; -} - -bool LMStudioProvider::supportImage() const -{ - return true; -} - -void LMStudioProvider::cancelRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("LMStudioProvider: Cancelling request %1").arg(requestId)); - LLMCore::Provider::cancelRequest(requestId); - cleanupRequest(requestId); -} - -void LMStudioProvider::onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) -{ - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - QStringList lines = buffers.rawStreamBuffer.processData(data); - - for (const QString &line : lines) { - if (line.trimmed().isEmpty() || line == "data: [DONE]") { - continue; - } - - QJsonObject chunk = parseEventLine(line); - if (chunk.isEmpty()) - continue; - - processStreamChunk(requestId, chunk); - } -} - -void LMStudioProvider::onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, std::optional error) -{ - if (error) { - LOG_MESSAGE(QString("LMStudioProvider request %1 failed: %2").arg(requestId, *error)); - emit requestFailed(requestId, *error); - cleanupRequest(requestId); - return; - } - - if (m_messages.contains(requestId)) { - OpenAIMessage *message = m_messages[requestId]; - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Waiting for tools to complete for %1").arg(requestId)); - m_dataBuffers.remove(requestId); - return; - } - } - - if (m_dataBuffers.contains(requestId)) { - const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - if (!buffers.responseContent.isEmpty()) { - LOG_MESSAGE(QString("Emitting full response for %1").arg(requestId)); - emit fullResponseReceived(requestId, buffers.responseContent); - } - } - - cleanupRequest(requestId); + return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image + | PluginLLMCore::ProviderCapability::ModelListing; } void LMStudioProvider::prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) { @@ -236,22 +113,16 @@ void LMStudioProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == LLMCore::RequestType::CodeCompletion) { + if (type == PluginLLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); - } else if (type == LLMCore::RequestType::QuickRefactoring) { + } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); } if (isToolsEnabled) { - LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL; - if (type == LLMCore::RequestType::QuickRefactoring) { - filter = LLMCore::RunToolsFilter::OnlyRead; - } - - auto toolsDefinitions = m_toolsManager->getToolsDefinitions( - LLMCore::ToolSchemaFormat::OpenAI, filter); + auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); if (!toolsDefinitions.isEmpty()) { request["tools"] = toolsDefinitions; LOG_MESSAGE(QString("Added %1 tools to LMStudio request").arg(toolsDefinitions.size())); @@ -259,163 +130,9 @@ void LMStudioProvider::prepareRequest( } } -void LMStudioProvider::onToolExecutionComplete( - const QString &requestId, const QHash &toolResults) +::LLMCore::BaseClient *LMStudioProvider::client() const { - if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) { - LOG_MESSAGE(QString("ERROR: Missing data for continuation request %1").arg(requestId)); - cleanupRequest(requestId); - return; - } - - LOG_MESSAGE(QString("Tool execution complete for LMStudio request %1").arg(requestId)); - - for (auto it = toolResults.begin(); it != toolResults.end(); ++it) { - OpenAIMessage *message = m_messages[requestId]; - auto toolContent = message->getCurrentToolUseContent(); - for (auto tool : toolContent) { - if (tool->id() == it.key()) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(tool->name()); - emit toolExecutionCompleted( - requestId, tool->id(), toolStringName, toolResults[tool->id()]); - break; - } - } - } - - OpenAIMessage *message = m_messages[requestId]; - QJsonObject continuationRequest = m_originalRequests[requestId]; - QJsonArray messages = continuationRequest["messages"].toArray(); - - messages.append(message->toProviderFormat()); - - QJsonArray toolResultMessages = message->createToolResultMessages(toolResults); - for (const auto &toolMsg : toolResultMessages) { - messages.append(toolMsg); - } - - continuationRequest["messages"] = messages; - - LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results") - .arg(requestId) - .arg(toolResults.size())); - - sendRequest(requestId, m_requestUrls[requestId], continuationRequest); -} - -void LMStudioProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk) -{ - QJsonArray choices = chunk["choices"].toArray(); - if (choices.isEmpty()) { - return; - } - - QJsonObject choice = choices[0].toObject(); - QJsonObject delta = choice["delta"].toObject(); - QString finishReason = choice["finish_reason"].toString(); - - OpenAIMessage *message = m_messages.value(requestId); - if (!message) { - message = new OpenAIMessage(this); - m_messages[requestId] = message; - LOG_MESSAGE(QString("Created NEW OpenAIMessage for request %1").arg(requestId)); - - if (m_dataBuffers.contains(requestId)) { - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); - } - } else if ( - m_dataBuffers.contains(requestId) - && message->state() == LLMCore::MessageState::RequiresToolExecution) { - message->startNewContinuation(); - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); - } - - if (delta.contains("content") && !delta["content"].isNull()) { - QString content = delta["content"].toString(); - message->handleContentDelta(content); - - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - buffers.responseContent += content; - emit partialResponseReceived(requestId, content); - } - - if (delta.contains("tool_calls")) { - QJsonArray toolCalls = delta["tool_calls"].toArray(); - for (const auto &toolCallValue : toolCalls) { - QJsonObject toolCall = toolCallValue.toObject(); - int index = toolCall["index"].toInt(); - - if (toolCall.contains("id")) { - QString id = toolCall["id"].toString(); - QJsonObject function = toolCall["function"].toObject(); - QString name = function["name"].toString(); - message->handleToolCallStart(index, id, name); - } - - if (toolCall.contains("function")) { - QJsonObject function = toolCall["function"].toObject(); - if (function.contains("arguments")) { - QString args = function["arguments"].toString(); - message->handleToolCallDelta(index, args); - } - } - } - } - - if (!finishReason.isEmpty() && finishReason != "null") { - for (int i = 0; i < 10; ++i) { - message->handleToolCallComplete(i); - } - - message->handleFinishReason(finishReason); - handleMessageComplete(requestId); - } -} - -void LMStudioProvider::handleMessageComplete(const QString &requestId) -{ - if (!m_messages.contains(requestId)) - return; - - OpenAIMessage *message = m_messages[requestId]; - - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("LMStudio message requires tool execution for %1").arg(requestId)); - - auto toolUseContent = message->getCurrentToolUseContent(); - - if (toolUseContent.isEmpty()) { - LOG_MESSAGE(QString("No tools to execute for %1").arg(requestId)); - return; - } - - for (auto toolContent : toolUseContent) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name()); - emit toolExecutionStarted(requestId, toolContent->id(), toolStringName); - m_toolsManager->executeToolCall( - requestId, toolContent->id(), toolContent->name(), toolContent->input()); - } - - } else { - LOG_MESSAGE(QString("LMStudio message marked as complete for %1").arg(requestId)); - } -} - -void LMStudioProvider::cleanupRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("Cleaning up LMStudio request %1").arg(requestId)); - - if (m_messages.contains(requestId)) { - OpenAIMessage *message = m_messages.take(requestId); - message->deleteLater(); - } - - m_dataBuffers.remove(requestId); - m_requestUrls.remove(requestId); - m_originalRequests.remove(requestId); - m_toolsManager->cleanupRequest(requestId); + return m_client; } } // namespace QodeAssist::Providers diff --git a/providers/LMStudioProvider.hpp b/providers/LMStudioProvider.hpp index 2aeb140..b3ec744 100644 --- a/providers/LMStudioProvider.hpp +++ b/providers/LMStudioProvider.hpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,13 +19,12 @@ #pragma once -#include "OpenAIMessage.hpp" -#include "tools/ToolsManager.hpp" -#include +#include +#include namespace QodeAssist::Providers { -class LMStudioProvider : public LLMCore::Provider +class LMStudioProvider : public PluginLLMCore::Provider { Q_OBJECT public: @@ -35,47 +34,22 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; - bool supportsModelListing() const override; void prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) override; QFuture> getInstalledModels(const QString &url) override; - QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + PluginLLMCore::ProviderID providerID() const override; + PluginLLMCore::ProviderCapabilities capabilities() const override; + + ::LLMCore::BaseClient *client() const override; QString apiKey() const override; - void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; - LLMCore::ProviderID providerID() const override; - - void sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override; - - bool supportsTools() const override; - bool supportImage() const override; - void cancelRequest(const LLMCore::RequestID &requestId) override; - -public slots: - void onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; - void onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, - std::optional error) override; - -private slots: - void onToolExecutionComplete( - const QString &requestId, const QHash &toolResults); private: - void processStreamChunk(const QString &requestId, const QJsonObject &chunk); - void handleMessageComplete(const QString &requestId); - void cleanupRequest(const LLMCore::RequestID &requestId); - - QHash m_messages; - QHash m_requestUrls; - QHash m_originalRequests; - Tools::ToolsManager *m_toolsManager; + ::LLMCore::OpenAIClient *m_client; }; } // namespace QodeAssist::Providers diff --git a/providers/LlamaCppProvider.cpp b/providers/LlamaCppProvider.cpp index 563076b..35065f5 100644 --- a/providers/LlamaCppProvider.cpp +++ b/providers/LlamaCppProvider.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,12 +19,13 @@ #include "LlamaCppProvider.hpp" -#include "llmcore/ValidationUtils.hpp" +#include #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" #include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" +#include "tools/ToolsRegistration.hpp" #include #include @@ -33,14 +34,10 @@ namespace QodeAssist::Providers { LlamaCppProvider::LlamaCppProvider(QObject *parent) - : LLMCore::Provider(parent) - , m_toolsManager(new Tools::ToolsManager(this)) + : PluginLLMCore::Provider(parent) + , m_client(new ::LLMCore::LlamaCppClient(QString(), QString(), QString(), this)) { - connect( - m_toolsManager, - &Tools::ToolsManager::toolExecutionComplete, - this, - &LlamaCppProvider::onToolExecutionComplete); + Tools::registerQodeAssistTools(m_client->tools()); } QString LlamaCppProvider::name() const @@ -48,6 +45,11 @@ QString LlamaCppProvider::name() const return "llama.cpp"; } +QString LlamaCppProvider::apiKey() const +{ + return {}; +} + QString LlamaCppProvider::url() const { return "http://localhost:8080"; @@ -63,16 +65,11 @@ QString LlamaCppProvider::chatEndpoint() const return "/v1/chat/completions"; } -bool LlamaCppProvider::supportsModelListing() const -{ - return false; -} - void LlamaCppProvider::prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) { @@ -96,22 +93,16 @@ void LlamaCppProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == LLMCore::RequestType::CodeCompletion) { + if (type == PluginLLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); - } else if (type == LLMCore::RequestType::QuickRefactoring) { + } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); } if (isToolsEnabled) { - LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL; - if (type == LLMCore::RequestType::QuickRefactoring) { - filter = LLMCore::RunToolsFilter::OnlyRead; - } - - auto toolsDefinitions = m_toolsManager->getToolsDefinitions( - LLMCore::ToolSchemaFormat::OpenAI, filter); + auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); if (!toolsDefinitions.isEmpty()) { request["tools"] = toolsDefinitions; LOG_MESSAGE(QString("Added %1 tools to llama.cpp request").arg(toolsDefinitions.size())); @@ -124,313 +115,19 @@ QFuture> LlamaCppProvider::getInstalledModels(const QString &) return QtFuture::makeReadyFuture(QList{}); } -QList LlamaCppProvider::validateRequest( - const QJsonObject &request, LLMCore::TemplateType type) +PluginLLMCore::ProviderID LlamaCppProvider::providerID() const { - if (type == LLMCore::TemplateType::FIM) { - const auto infillReq = QJsonObject{ - {"model", {}}, - {"input_prefix", {}}, - {"input_suffix", {}}, - {"input_extra", {}}, - {"prompt", {}}, - {"temperature", {}}, - {"top_p", {}}, - {"top_k", {}}, - {"max_tokens", {}}, - {"frequency_penalty", {}}, - {"presence_penalty", {}}, - {"stop", QJsonArray{}}, - {"stream", {}}}; - - return LLMCore::ValidationUtils::validateRequestFields(request, infillReq); - } else { - const auto chatReq = QJsonObject{ - {"model", {}}, - {"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}}, - {"temperature", {}}, - {"max_tokens", {}}, - {"top_p", {}}, - {"top_k", {}}, - {"frequency_penalty", {}}, - {"presence_penalty", {}}, - {"stop", QJsonArray{}}, - {"stream", {}}, - {"tools", {}}}; - - return LLMCore::ValidationUtils::validateRequestFields(request, chatReq); - } + return PluginLLMCore::ProviderID::LlamaCpp; } -QString LlamaCppProvider::apiKey() const +PluginLLMCore::ProviderCapabilities LlamaCppProvider::capabilities() const { - return {}; + return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image; } -void LlamaCppProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const +::LLMCore::BaseClient *LlamaCppProvider::client() const { - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); -} - -LLMCore::ProviderID LlamaCppProvider::providerID() const -{ - return LLMCore::ProviderID::LlamaCpp; -} - -void LlamaCppProvider::sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) -{ - if (!m_messages.contains(requestId)) { - m_dataBuffers[requestId].clear(); - } - - m_requestUrls[requestId] = url; - m_originalRequests[requestId] = payload; - - QNetworkRequest networkRequest(url); - prepareNetworkRequest(networkRequest); - - LOG_MESSAGE( - QString("LlamaCppProvider: Sending request %1 to %2").arg(requestId, url.toString())); - - httpClient()->postStreaming(requestId, networkRequest, payload); -} - -bool LlamaCppProvider::supportsTools() const -{ - return true; -} - -bool LlamaCppProvider::supportImage() const -{ - return true; -} - -void LlamaCppProvider::cancelRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("LlamaCppProvider: Cancelling request %1").arg(requestId)); - LLMCore::Provider::cancelRequest(requestId); - cleanupRequest(requestId); -} - -void LlamaCppProvider::onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) -{ - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - QStringList lines = buffers.rawStreamBuffer.processData(data); - - for (const QString &line : lines) { - if (line.trimmed().isEmpty() || line == "data: [DONE]") { - continue; - } - - QJsonObject chunk = parseEventLine(line); - if (chunk.isEmpty()) - continue; - - if (chunk.contains("content")) { - QString content = chunk["content"].toString(); - if (!content.isEmpty()) { - buffers.responseContent += content; - emit partialResponseReceived(requestId, content); - } - if (chunk["stop"].toBool()) { - emit fullResponseReceived(requestId, buffers.responseContent); - m_dataBuffers.remove(requestId); - } - } else if (chunk.contains("choices")) { - processStreamChunk(requestId, chunk); - } - } -} - -void LlamaCppProvider::onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, std::optional error) -{ - if (error) { - LOG_MESSAGE(QString("LlamaCppProvider request %1 failed: %2").arg(requestId, *error)); - emit requestFailed(requestId, *error); - cleanupRequest(requestId); - return; - } - - if (m_messages.contains(requestId)) { - OpenAIMessage *message = m_messages[requestId]; - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Waiting for tools to complete for %1").arg(requestId)); - m_dataBuffers.remove(requestId); - return; - } - } - - if (m_dataBuffers.contains(requestId)) { - const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - if (!buffers.responseContent.isEmpty()) { - LOG_MESSAGE(QString("Emitting full response for %1").arg(requestId)); - emit fullResponseReceived(requestId, buffers.responseContent); - } - } - - cleanupRequest(requestId); -} - -void LlamaCppProvider::onToolExecutionComplete( - const QString &requestId, const QHash &toolResults) -{ - if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) { - LOG_MESSAGE(QString("ERROR: Missing data for continuation request %1").arg(requestId)); - cleanupRequest(requestId); - return; - } - - LOG_MESSAGE(QString("Tool execution complete for llama.cpp request %1").arg(requestId)); - - for (auto it = toolResults.begin(); it != toolResults.end(); ++it) { - OpenAIMessage *message = m_messages[requestId]; - auto toolContent = message->getCurrentToolUseContent(); - for (auto tool : toolContent) { - if (tool->id() == it.key()) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(tool->name()); - emit toolExecutionCompleted( - requestId, tool->id(), toolStringName, toolResults[tool->id()]); - break; - } - } - } - - OpenAIMessage *message = m_messages[requestId]; - QJsonObject continuationRequest = m_originalRequests[requestId]; - QJsonArray messages = continuationRequest["messages"].toArray(); - - messages.append(message->toProviderFormat()); - - QJsonArray toolResultMessages = message->createToolResultMessages(toolResults); - for (const auto &toolMsg : toolResultMessages) { - messages.append(toolMsg); - } - - continuationRequest["messages"] = messages; - - LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results") - .arg(requestId) - .arg(toolResults.size())); - - sendRequest(requestId, m_requestUrls[requestId], continuationRequest); -} - -void LlamaCppProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk) -{ - QJsonArray choices = chunk["choices"].toArray(); - if (choices.isEmpty()) { - return; - } - - QJsonObject choice = choices[0].toObject(); - QJsonObject delta = choice["delta"].toObject(); - QString finishReason = choice["finish_reason"].toString(); - - OpenAIMessage *message = m_messages.value(requestId); - if (!message) { - message = new OpenAIMessage(this); - m_messages[requestId] = message; - LOG_MESSAGE(QString("Created NEW OpenAIMessage for llama.cpp request %1").arg(requestId)); - - if (m_dataBuffers.contains(requestId)) { - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); - } - } else if ( - m_dataBuffers.contains(requestId) - && message->state() == LLMCore::MessageState::RequiresToolExecution) { - message->startNewContinuation(); - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); - } - - if (delta.contains("content") && !delta["content"].isNull()) { - QString content = delta["content"].toString(); - message->handleContentDelta(content); - - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - buffers.responseContent += content; - emit partialResponseReceived(requestId, content); - } - - if (delta.contains("tool_calls")) { - QJsonArray toolCalls = delta["tool_calls"].toArray(); - for (const auto &toolCallValue : toolCalls) { - QJsonObject toolCall = toolCallValue.toObject(); - int index = toolCall["index"].toInt(); - - if (toolCall.contains("id")) { - QString id = toolCall["id"].toString(); - QJsonObject function = toolCall["function"].toObject(); - QString name = function["name"].toString(); - message->handleToolCallStart(index, id, name); - } - - if (toolCall.contains("function")) { - QJsonObject function = toolCall["function"].toObject(); - if (function.contains("arguments")) { - QString args = function["arguments"].toString(); - message->handleToolCallDelta(index, args); - } - } - } - } - - if (!finishReason.isEmpty() && finishReason != "null") { - for (int i = 0; i < 10; ++i) { - message->handleToolCallComplete(i); - } - - message->handleFinishReason(finishReason); - handleMessageComplete(requestId); - } -} - -void LlamaCppProvider::handleMessageComplete(const QString &requestId) -{ - if (!m_messages.contains(requestId)) - return; - - OpenAIMessage *message = m_messages[requestId]; - - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("llama.cpp message requires tool execution for %1").arg(requestId)); - - auto toolUseContent = message->getCurrentToolUseContent(); - - if (toolUseContent.isEmpty()) { - LOG_MESSAGE(QString("No tools to execute for %1").arg(requestId)); - return; - } - - for (auto toolContent : toolUseContent) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name()); - emit toolExecutionStarted(requestId, toolContent->id(), toolStringName); - m_toolsManager->executeToolCall( - requestId, toolContent->id(), toolContent->name(), toolContent->input()); - } - - } else { - LOG_MESSAGE(QString("llama.cpp message marked as complete for %1").arg(requestId)); - } -} - -void LlamaCppProvider::cleanupRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("Cleaning up llama.cpp request %1").arg(requestId)); - - if (m_messages.contains(requestId)) { - OpenAIMessage *message = m_messages.take(requestId); - message->deleteLater(); - } - - m_dataBuffers.remove(requestId); - m_requestUrls.remove(requestId); - m_originalRequests.remove(requestId); - m_toolsManager->cleanupRequest(requestId); + return m_client; } } // namespace QodeAssist::Providers diff --git a/providers/LlamaCppProvider.hpp b/providers/LlamaCppProvider.hpp index b88216e..ccc8137 100644 --- a/providers/LlamaCppProvider.hpp +++ b/providers/LlamaCppProvider.hpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,13 +19,13 @@ #pragma once -#include "OpenAIMessage.hpp" -#include "tools/ToolsManager.hpp" -#include +#include + +#include namespace QodeAssist::Providers { -class LlamaCppProvider : public LLMCore::Provider +class LlamaCppProvider : public PluginLLMCore::Provider { Q_OBJECT public: @@ -35,47 +35,22 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; - bool supportsModelListing() const override; void prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) override; QFuture> getInstalledModels(const QString &url) override; - QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + PluginLLMCore::ProviderID providerID() const override; + PluginLLMCore::ProviderCapabilities capabilities() const override; + + ::LLMCore::BaseClient *client() const override; QString apiKey() const override; - void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; - LLMCore::ProviderID providerID() const override; - - void sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override; - - bool supportsTools() const override; - bool supportImage() const override; - void cancelRequest(const LLMCore::RequestID &requestId) override; - -public slots: - void onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; - void onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, - std::optional error) override; - -private slots: - void onToolExecutionComplete( - const QString &requestId, const QHash &toolResults); private: - void processStreamChunk(const QString &requestId, const QJsonObject &chunk); - void handleMessageComplete(const QString &requestId); - void cleanupRequest(const LLMCore::RequestID &requestId); - - QHash m_messages; - QHash m_requestUrls; - QHash m_originalRequests; - Tools::ToolsManager *m_toolsManager; + ::LLMCore::LlamaCppClient *m_client; }; } // namespace QodeAssist::Providers diff --git a/providers/MistralAIProvider.cpp b/providers/MistralAIProvider.cpp index 59baef2..8249446 100644 --- a/providers/MistralAIProvider.cpp +++ b/providers/MistralAIProvider.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,13 +19,14 @@ #include "MistralAIProvider.hpp" -#include "llmcore/ValidationUtils.hpp" +#include #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" #include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProviderSettings.hpp" +#include "tools/ToolsRegistration.hpp" #include #include @@ -34,14 +35,10 @@ namespace QodeAssist::Providers { MistralAIProvider::MistralAIProvider(QObject *parent) - : LLMCore::Provider(parent) - , m_toolsManager(new Tools::ToolsManager(this)) + : PluginLLMCore::Provider(parent) + , m_client(new ::LLMCore::OpenAIClient(QString(), QString(), QString(), this)) { - connect( - m_toolsManager, - &Tools::ToolsManager::toolExecutionComplete, - this, - &MistralAIProvider::onToolExecutionComplete); + Tools::registerQodeAssistTools(m_client->tools()); } QString MistralAIProvider::name() const @@ -49,6 +46,11 @@ QString MistralAIProvider::name() const return "Mistral AI"; } +QString MistralAIProvider::apiKey() const +{ + return Settings::providerSettings().mistralAiApiKey(); +} + QString MistralAIProvider::url() const { return "https://api.mistral.ai"; @@ -64,176 +66,29 @@ QString MistralAIProvider::chatEndpoint() const return "/v1/chat/completions"; } -bool MistralAIProvider::supportsModelListing() const -{ - return true; -} - QFuture> MistralAIProvider::getInstalledModels(const QString &url) { - QNetworkRequest request(QString("%1/v1/models").arg(url)); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - if (!apiKey().isEmpty()) { - request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8()); - } - - return httpClient()->get(request).then([](const QByteArray &data) { - QList models; - QJsonObject jsonObject = QJsonDocument::fromJson(data).object(); - - if (jsonObject.contains("data") && jsonObject["object"].toString() == "list") { - QJsonArray modelArray = jsonObject["data"].toArray(); - for (const QJsonValue &value : modelArray) { - QJsonObject modelObject = value.toObject(); - if (modelObject.contains("id")) { - models.append(modelObject["id"].toString()); - } - } - } - return models; - }).onFailed([](const std::exception &e) { - LOG_MESSAGE(QString("Error fetching Mistral AI models: %1").arg(e.what())); - return QList{}; - }); + m_client->setUrl(url); + m_client->setApiKey(apiKey()); + return m_client->listModels(); } -QList MistralAIProvider::validateRequest( - const QJsonObject &request, LLMCore::TemplateType type) +PluginLLMCore::ProviderID MistralAIProvider::providerID() const { - const auto fimReq = QJsonObject{ - {"model", {}}, - {"max_tokens", {}}, - {"stream", {}}, - {"temperature", {}}, - {"prompt", {}}, - {"suffix", {}}}; - - const auto templateReq = QJsonObject{ - {"model", {}}, - {"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}}, - {"temperature", {}}, - {"max_tokens", {}}, - {"top_p", {}}, - {"top_k", {}}, - {"frequency_penalty", {}}, - {"presence_penalty", {}}, - {"stop", QJsonArray{}}, - {"stream", {}}, - {"tools", {}}}; - - return LLMCore::ValidationUtils::validateRequestFields( - request, type == LLMCore::TemplateType::FIM ? fimReq : templateReq); + return PluginLLMCore::ProviderID::MistralAI; } -QString MistralAIProvider::apiKey() const +PluginLLMCore::ProviderCapabilities MistralAIProvider::capabilities() const { - return Settings::providerSettings().mistralAiApiKey(); -} - -void MistralAIProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const -{ - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - if (!apiKey().isEmpty()) { - networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8()); - } -} - -LLMCore::ProviderID MistralAIProvider::providerID() const -{ - return LLMCore::ProviderID::MistralAI; -} - -void MistralAIProvider::sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) -{ - if (!m_messages.contains(requestId)) { - m_dataBuffers[requestId].clear(); - } - - m_requestUrls[requestId] = url; - m_originalRequests[requestId] = payload; - - QNetworkRequest networkRequest(url); - prepareNetworkRequest(networkRequest); - - LOG_MESSAGE( - QString("MistralAIProvider: Sending request %1 to %2").arg(requestId, url.toString())); - - httpClient()->postStreaming(requestId, networkRequest, payload); -} - -bool MistralAIProvider::supportsTools() const -{ - return true; -} - -bool MistralAIProvider::supportImage() const -{ - return true; -} - -void MistralAIProvider::cancelRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("MistralAIProvider: Cancelling request %1").arg(requestId)); - LLMCore::Provider::cancelRequest(requestId); - cleanupRequest(requestId); -} - -void MistralAIProvider::onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) -{ - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - QStringList lines = buffers.rawStreamBuffer.processData(data); - - for (const QString &line : lines) { - if (line.trimmed().isEmpty() || line == "data: [DONE]") { - continue; - } - - QJsonObject chunk = parseEventLine(line); - if (chunk.isEmpty()) - continue; - - processStreamChunk(requestId, chunk); - } -} - -void MistralAIProvider::onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, std::optional error) -{ - if (error) { - LOG_MESSAGE(QString("MistralAIProvider request %1 failed: %2").arg(requestId, *error)); - emit requestFailed(requestId, *error); - cleanupRequest(requestId); - return; - } - - if (m_messages.contains(requestId)) { - OpenAIMessage *message = m_messages[requestId]; - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Waiting for tools to complete for %1").arg(requestId)); - m_dataBuffers.remove(requestId); - return; - } - } - - if (m_dataBuffers.contains(requestId)) { - const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - if (!buffers.responseContent.isEmpty()) { - LOG_MESSAGE(QString("Emitting full response for %1").arg(requestId)); - emit fullResponseReceived(requestId, buffers.responseContent); - } - } - - cleanupRequest(requestId); + return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image + | PluginLLMCore::ProviderCapability::ModelListing; } void MistralAIProvider::prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) { @@ -257,22 +112,16 @@ void MistralAIProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == LLMCore::RequestType::CodeCompletion) { + if (type == PluginLLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); - } else if (type == LLMCore::RequestType::QuickRefactoring) { + } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); } if (isToolsEnabled) { - LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL; - if (type == LLMCore::RequestType::QuickRefactoring) { - filter = LLMCore::RunToolsFilter::OnlyRead; - } - - auto toolsDefinitions = m_toolsManager->getToolsDefinitions( - LLMCore::ToolSchemaFormat::OpenAI, filter); + auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); if (!toolsDefinitions.isEmpty()) { request["tools"] = toolsDefinitions; LOG_MESSAGE(QString("Added %1 tools to Mistral request").arg(toolsDefinitions.size())); @@ -280,163 +129,9 @@ void MistralAIProvider::prepareRequest( } } -void MistralAIProvider::onToolExecutionComplete( - const QString &requestId, const QHash &toolResults) +::LLMCore::BaseClient *MistralAIProvider::client() const { - if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) { - LOG_MESSAGE(QString("ERROR: Missing data for continuation request %1").arg(requestId)); - cleanupRequest(requestId); - return; - } - - LOG_MESSAGE(QString("Tool execution complete for Mistral request %1").arg(requestId)); - - for (auto it = toolResults.begin(); it != toolResults.end(); ++it) { - OpenAIMessage *message = m_messages[requestId]; - auto toolContent = message->getCurrentToolUseContent(); - for (auto tool : toolContent) { - if (tool->id() == it.key()) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(tool->name()); - emit toolExecutionCompleted( - requestId, tool->id(), toolStringName, toolResults[tool->id()]); - break; - } - } - } - - OpenAIMessage *message = m_messages[requestId]; - QJsonObject continuationRequest = m_originalRequests[requestId]; - QJsonArray messages = continuationRequest["messages"].toArray(); - - messages.append(message->toProviderFormat()); - - QJsonArray toolResultMessages = message->createToolResultMessages(toolResults); - for (const auto &toolMsg : toolResultMessages) { - messages.append(toolMsg); - } - - continuationRequest["messages"] = messages; - - LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results") - .arg(requestId) - .arg(toolResults.size())); - - sendRequest(requestId, m_requestUrls[requestId], continuationRequest); -} - -void MistralAIProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk) -{ - QJsonArray choices = chunk["choices"].toArray(); - if (choices.isEmpty()) { - return; - } - - QJsonObject choice = choices[0].toObject(); - QJsonObject delta = choice["delta"].toObject(); - QString finishReason = choice["finish_reason"].toString(); - - OpenAIMessage *message = m_messages.value(requestId); - if (!message) { - message = new OpenAIMessage(this); - m_messages[requestId] = message; - LOG_MESSAGE(QString("Created NEW OpenAIMessage for Mistral request %1").arg(requestId)); - - if (m_dataBuffers.contains(requestId)) { - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); - } - } else if ( - m_dataBuffers.contains(requestId) - && message->state() == LLMCore::MessageState::RequiresToolExecution) { - message->startNewContinuation(); - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); - } - - if (delta.contains("content") && !delta["content"].isNull()) { - QString content = delta["content"].toString(); - message->handleContentDelta(content); - - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - buffers.responseContent += content; - emit partialResponseReceived(requestId, content); - } - - if (delta.contains("tool_calls")) { - QJsonArray toolCalls = delta["tool_calls"].toArray(); - for (const auto &toolCallValue : toolCalls) { - QJsonObject toolCall = toolCallValue.toObject(); - int index = toolCall["index"].toInt(); - - if (toolCall.contains("id")) { - QString id = toolCall["id"].toString(); - QJsonObject function = toolCall["function"].toObject(); - QString name = function["name"].toString(); - message->handleToolCallStart(index, id, name); - } - - if (toolCall.contains("function")) { - QJsonObject function = toolCall["function"].toObject(); - if (function.contains("arguments")) { - QString args = function["arguments"].toString(); - message->handleToolCallDelta(index, args); - } - } - } - } - - if (!finishReason.isEmpty() && finishReason != "null") { - for (int i = 0; i < 10; ++i) { - message->handleToolCallComplete(i); - } - - message->handleFinishReason(finishReason); - handleMessageComplete(requestId); - } -} - -void MistralAIProvider::handleMessageComplete(const QString &requestId) -{ - if (!m_messages.contains(requestId)) - return; - - OpenAIMessage *message = m_messages[requestId]; - - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Mistral message requires tool execution for %1").arg(requestId)); - - auto toolUseContent = message->getCurrentToolUseContent(); - - if (toolUseContent.isEmpty()) { - LOG_MESSAGE(QString("No tools to execute for %1").arg(requestId)); - return; - } - - for (auto toolContent : toolUseContent) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name()); - emit toolExecutionStarted(requestId, toolContent->id(), toolStringName); - m_toolsManager->executeToolCall( - requestId, toolContent->id(), toolContent->name(), toolContent->input()); - } - - } else { - LOG_MESSAGE(QString("Mistral message marked as complete for %1").arg(requestId)); - } -} - -void MistralAIProvider::cleanupRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("Cleaning up Mistral request %1").arg(requestId)); - - if (m_messages.contains(requestId)) { - OpenAIMessage *message = m_messages.take(requestId); - message->deleteLater(); - } - - m_dataBuffers.remove(requestId); - m_requestUrls.remove(requestId); - m_originalRequests.remove(requestId); - m_toolsManager->cleanupRequest(requestId); + return m_client; } } // namespace QodeAssist::Providers diff --git a/providers/MistralAIProvider.hpp b/providers/MistralAIProvider.hpp index 6a68c03..57d2137 100644 --- a/providers/MistralAIProvider.hpp +++ b/providers/MistralAIProvider.hpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,13 +19,12 @@ #pragma once -#include "OpenAIMessage.hpp" -#include "tools/ToolsManager.hpp" -#include +#include +#include namespace QodeAssist::Providers { -class MistralAIProvider : public LLMCore::Provider +class MistralAIProvider : public PluginLLMCore::Provider { Q_OBJECT public: @@ -35,47 +34,22 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; - bool supportsModelListing() const override; void prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) override; QFuture> getInstalledModels(const QString &url) override; - QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + PluginLLMCore::ProviderID providerID() const override; + PluginLLMCore::ProviderCapabilities capabilities() const override; + + ::LLMCore::BaseClient *client() const override; QString apiKey() const override; - void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; - LLMCore::ProviderID providerID() const override; - - void sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override; - - bool supportsTools() const override; - bool supportImage() const override; - void cancelRequest(const LLMCore::RequestID &requestId) override; - -public slots: - void onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; - void onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, - std::optional error) override; - -private slots: - void onToolExecutionComplete( - const QString &requestId, const QHash &toolResults); private: - void processStreamChunk(const QString &requestId, const QJsonObject &chunk); - void handleMessageComplete(const QString &requestId); - void cleanupRequest(const LLMCore::RequestID &requestId); - - QHash m_messages; - QHash m_requestUrls; - QHash m_originalRequests; - Tools::ToolsManager *m_toolsManager; + ::LLMCore::OpenAIClient *m_client; }; } // namespace QodeAssist::Providers diff --git a/providers/OllamaMessage.cpp b/providers/OllamaMessage.cpp index c97576b..57f373d 100644 --- a/providers/OllamaMessage.cpp +++ b/providers/OllamaMessage.cpp @@ -39,13 +39,13 @@ void OllamaMessage::handleContentDelta(const QString &content) } if (!m_contentAddedToTextBlock) { - LLMCore::TextContent *textContent = getOrCreateTextContent(); + 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 { - LLMCore::TextContent *textContent = getOrCreateTextContent(); + PluginLLMCore::TextContent *textContent = getOrCreateTextContent(); textContent->appendText(content); } } @@ -65,7 +65,7 @@ void OllamaMessage::handleToolCall(const QJsonObject &toolCall) m_accumulatedContent.clear(); } - addCurrentContent(toolId, name, arguments); + addCurrentContent(toolId, name, arguments); LOG_MESSAGE( QString("OllamaMessage: Structured tool call detected - name=%1, id=%2").arg(name, toolId)); @@ -73,7 +73,7 @@ void OllamaMessage::handleToolCall(const QJsonObject &toolCall) void OllamaMessage::handleThinkingDelta(const QString &thinking) { - LLMCore::ThinkingContent *thinkingContent = getOrCreateThinkingContent(); + PluginLLMCore::ThinkingContent *thinkingContent = getOrCreateThinkingContent(); thinkingContent->appendThinking(thinking); } @@ -102,7 +102,7 @@ void OllamaMessage::handleDone(bool done) .arg(trimmed.length())); for (auto it = m_currentBlocks.begin(); it != m_currentBlocks.end();) { - if (qobject_cast(*it)) { + if (qobject_cast(*it)) { LOG_MESSAGE(QString( "OllamaMessage: Removing TextContent block (incomplete tool call)")); (*it)->deleteLater(); @@ -114,7 +114,7 @@ void OllamaMessage::handleDone(bool done) m_accumulatedContent.clear(); } else { - LLMCore::TextContent *textContent = getOrCreateTextContent(); + PluginLLMCore::TextContent *textContent = getOrCreateTextContent(); textContent->setText(m_accumulatedContent); m_contentAddedToTextBlock = true; LOG_MESSAGE( @@ -184,13 +184,13 @@ bool OllamaMessage::tryParseToolCall() QString toolId = QString("call_%1_%2").arg(name).arg(QDateTime::currentMSecsSinceEpoch()); for (auto block : m_currentBlocks) { - if (qobject_cast(block)) { + if (qobject_cast(block)) { LOG_MESSAGE(QString("OllamaMessage: Removing TextContent block (tool call detected)")); } } m_currentBlocks.clear(); - addCurrentContent(toolId, name, arguments); + addCurrentContent(toolId, name, arguments); LOG_MESSAGE( QString( @@ -238,14 +238,14 @@ QJsonObject OllamaMessage::toProviderFormat() const if (!block) continue; - if (auto text = qobject_cast(block)) { + if (auto text = qobject_cast(block)) { textContent += text->text(); - } else if (auto tool = qobject_cast(block)) { + } else if (auto tool = qobject_cast(block)) { QJsonObject toolCall; toolCall["type"] = "function"; toolCall["function"] = QJsonObject{{"name", tool->name()}, {"arguments", tool->input()}}; toolCalls.append(toolCall); - } else if (auto thinking = qobject_cast(block)) { + } else if (auto thinking = qobject_cast(block)) { thinkingContent += thinking->thinking(); } } @@ -287,22 +287,22 @@ QJsonArray OllamaMessage::createToolResultMessages(const QHash return messages; } -QList OllamaMessage::getCurrentToolUseContent() const +QList OllamaMessage::getCurrentToolUseContent() const { - QList toolBlocks; + QList toolBlocks; for (auto block : m_currentBlocks) { - if (auto toolContent = qobject_cast(block)) { + if (auto toolContent = qobject_cast(block)) { toolBlocks.append(toolContent); } } return toolBlocks; } -QList OllamaMessage::getCurrentThinkingContent() const +QList OllamaMessage::getCurrentThinkingContent() const { - QList thinkingBlocks; + QList thinkingBlocks; for (auto block : m_currentBlocks) { - if (auto thinkingContent = qobject_cast(block)) { + if (auto thinkingContent = qobject_cast(block)) { thinkingBlocks.append(thinkingContent); } } @@ -316,7 +316,7 @@ void OllamaMessage::startNewContinuation() m_currentBlocks.clear(); m_accumulatedContent.clear(); m_done = false; - m_state = LLMCore::MessageState::Building; + m_state = PluginLLMCore::MessageState::Building; m_contentAddedToTextBlock = false; m_currentThinkingContent = nullptr; } @@ -324,40 +324,40 @@ void OllamaMessage::startNewContinuation() void OllamaMessage::updateStateFromDone() { if (!getCurrentToolUseContent().empty()) { - m_state = LLMCore::MessageState::RequiresToolExecution; + m_state = PluginLLMCore::MessageState::RequiresToolExecution; LOG_MESSAGE(QString("OllamaMessage: State set to RequiresToolExecution, tools count=%1") .arg(getCurrentToolUseContent().size())); } else { - m_state = LLMCore::MessageState::Final; + m_state = PluginLLMCore::MessageState::Final; LOG_MESSAGE(QString("OllamaMessage: State set to Final")); } } -LLMCore::TextContent *OllamaMessage::getOrCreateTextContent() +PluginLLMCore::TextContent *OllamaMessage::getOrCreateTextContent() { for (auto block : m_currentBlocks) { - if (auto textContent = qobject_cast(block)) { + if (auto textContent = qobject_cast(block)) { return textContent; } } - return addCurrentContent(); + return addCurrentContent(); } -LLMCore::ThinkingContent *OllamaMessage::getOrCreateThinkingContent() +PluginLLMCore::ThinkingContent *OllamaMessage::getOrCreateThinkingContent() { if (m_currentThinkingContent) { return m_currentThinkingContent; } for (auto block : m_currentBlocks) { - if (auto thinkingContent = qobject_cast(block)) { + if (auto thinkingContent = qobject_cast(block)) { m_currentThinkingContent = thinkingContent; return m_currentThinkingContent; } } - m_currentThinkingContent = addCurrentContent(); + m_currentThinkingContent = addCurrentContent(); LOG_MESSAGE(QString("OllamaMessage: Created new ThinkingContent block")); return m_currentThinkingContent; } diff --git a/providers/OllamaMessage.hpp b/providers/OllamaMessage.hpp index 123cfcc..b02546d 100644 --- a/providers/OllamaMessage.hpp +++ b/providers/OllamaMessage.hpp @@ -19,7 +19,7 @@ #pragma once -#include +#include namespace QodeAssist::Providers { @@ -38,26 +38,26 @@ public: QJsonObject toProviderFormat() const; QJsonArray createToolResultMessages(const QHash &toolResults) const; - LLMCore::MessageState state() const { return m_state; } - QList getCurrentToolUseContent() const; - QList getCurrentThinkingContent() const; - QList currentBlocks() const { return m_currentBlocks; } + PluginLLMCore::MessageState state() const { return m_state; } + QList getCurrentToolUseContent() const; + QList getCurrentThinkingContent() const; + QList currentBlocks() const { return m_currentBlocks; } void startNewContinuation(); private: bool m_done = false; - LLMCore::MessageState m_state = LLMCore::MessageState::Building; - QList m_currentBlocks; + PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building; + QList m_currentBlocks; QString m_accumulatedContent; bool m_contentAddedToTextBlock = false; - LLMCore::ThinkingContent *m_currentThinkingContent = nullptr; + PluginLLMCore::ThinkingContent *m_currentThinkingContent = nullptr; void updateStateFromDone(); bool tryParseToolCall(); bool isLikelyToolCallJson(const QString &content) const; - LLMCore::TextContent *getOrCreateTextContent(); - LLMCore::ThinkingContent *getOrCreateThinkingContent(); + PluginLLMCore::TextContent *getOrCreateTextContent(); + PluginLLMCore::ThinkingContent *getOrCreateThinkingContent(); template T *addCurrentContent(Args &&...args) diff --git a/providers/OllamaProvider.cpp b/providers/OllamaProvider.cpp index e29590f..4801eaa 100644 --- a/providers/OllamaProvider.cpp +++ b/providers/OllamaProvider.cpp @@ -19,29 +19,27 @@ #include "OllamaProvider.hpp" +#include + #include #include #include -#include "llmcore/ValidationUtils.hpp" #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" #include "settings/QuickRefactorSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProviderSettings.hpp" +#include "tools/ToolsRegistration.hpp" namespace QodeAssist::Providers { OllamaProvider::OllamaProvider(QObject *parent) - : LLMCore::Provider(parent) - , m_toolsManager(new Tools::ToolsManager(this)) + : PluginLLMCore::Provider(parent) + , m_client(new ::LLMCore::OllamaClient(QString(), QString(), QString(), this)) { - connect( - m_toolsManager, - &Tools::ToolsManager::toolExecutionComplete, - this, - &OllamaProvider::onToolExecutionComplete); + Tools::registerQodeAssistTools(m_client->tools()); } QString OllamaProvider::name() const @@ -49,6 +47,11 @@ QString OllamaProvider::name() const return "Ollama"; } +QString OllamaProvider::apiKey() const +{ + return Settings::providerSettings().ollamaBasicAuthApiKey(); +} + QString OllamaProvider::url() const { return "http://localhost:11434"; @@ -64,16 +67,11 @@ QString OllamaProvider::chatEndpoint() const return "/api/chat"; } -bool OllamaProvider::supportsModelListing() const -{ - return true; -} - void OllamaProvider::prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) { @@ -109,12 +107,12 @@ void OllamaProvider::prepareRequest( request["options"] = options; }; - if (type == LLMCore::RequestType::CodeCompletion) { + if (type == PluginLLMCore::RequestType::CodeCompletion) { applySettings(Settings::codeCompletionSettings()); - } else if (type == LLMCore::RequestType::QuickRefactoring) { + } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { const auto &qrSettings = Settings::quickRefactorSettings(); applySettings(qrSettings); - + if (isThinkingEnabled) { applyThinkingMode(); LOG_MESSAGE(QString("OllamaProvider: Thinking mode enabled for QuickRefactoring")); @@ -130,13 +128,7 @@ void OllamaProvider::prepareRequest( } if (isToolsEnabled) { - LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL; - if (type == LLMCore::RequestType::QuickRefactoring) { - filter = LLMCore::RunToolsFilter::OnlyRead; - } - - auto toolsDefinitions = m_toolsManager->toolsFactory()->getToolsDefinitions( - LLMCore::ToolSchemaFormat::Ollama, filter); + auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); if (!toolsDefinitions.isEmpty()) { request["tools"] = toolsDefinitions; LOG_MESSAGE( @@ -145,453 +137,28 @@ void OllamaProvider::prepareRequest( } } -QFuture> OllamaProvider::getInstalledModels(const QString &url) +QFuture> OllamaProvider::getInstalledModels(const QString &baseUrl) { - QNetworkRequest request(QString("%1%2").arg(url, "/api/tags")); - prepareNetworkRequest(request); - - return httpClient()->get(request).then([](const QByteArray &data) { - QList models; - QJsonObject jsonObject = QJsonDocument::fromJson(data).object(); - QJsonArray modelArray = jsonObject["models"].toArray(); - - for (const QJsonValue &value : modelArray) { - QJsonObject modelObject = value.toObject(); - models.append(modelObject["name"].toString()); - } - return models; - }).onFailed([](const std::exception &e) { - LOG_MESSAGE(QString("Error fetching models: %1").arg(e.what())); - return QList{}; - }); + m_client->setUrl(baseUrl); + m_client->setApiKey(Settings::providerSettings().ollamaBasicAuthApiKey()); + return m_client->listModels(); } -QList OllamaProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type) +PluginLLMCore::ProviderID OllamaProvider::providerID() const { - const auto fimReq = QJsonObject{ - {"keep_alive", {}}, - {"model", {}}, - {"stream", {}}, - {"prompt", {}}, - {"suffix", {}}, - {"system", {}}, - {"images", QJsonArray{}}, - {"options", - QJsonObject{ - {"temperature", {}}, - {"stop", {}}, - {"top_p", {}}, - {"top_k", {}}, - {"num_predict", {}}, - {"frequency_penalty", {}}, - {"presence_penalty", {}}}}}; - - const auto messageReq = QJsonObject{ - {"keep_alive", {}}, - {"model", {}}, - {"stream", {}}, - {"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}, {"images", QJsonArray{}}}}}}, - {"tools", QJsonArray{}}, - {"options", - QJsonObject{ - {"temperature", {}}, - {"stop", {}}, - {"top_p", {}}, - {"top_k", {}}, - {"num_predict", {}}, - {"frequency_penalty", {}}, - {"presence_penalty", {}}}}}; - - return LLMCore::ValidationUtils::validateRequestFields( - request, type == LLMCore::TemplateType::FIM ? fimReq : messageReq); + return PluginLLMCore::ProviderID::Ollama; } -QString OllamaProvider::apiKey() const +PluginLLMCore::ProviderCapabilities OllamaProvider::capabilities() const { - return {}; + return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking + | PluginLLMCore::ProviderCapability::Image + | PluginLLMCore::ProviderCapability::ModelListing; } -void OllamaProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const +::LLMCore::BaseClient *OllamaProvider::client() const { - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - const auto key = Settings::providerSettings().ollamaBasicAuthApiKey(); - if (!key.isEmpty()) { - networkRequest.setRawHeader("Authorization", "Basic " + key.toLatin1()); - } -} - -LLMCore::ProviderID OllamaProvider::providerID() const -{ - return LLMCore::ProviderID::Ollama; -} - -void OllamaProvider::sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) -{ - m_dataBuffers[requestId].clear(); - - m_requestUrls[requestId] = url; - m_originalRequests[requestId] = payload; - - QNetworkRequest networkRequest(url); - prepareNetworkRequest(networkRequest); - - LOG_MESSAGE(QString("OllamaProvider: Sending request %1 to %2").arg(requestId, url.toString())); - - httpClient()->postStreaming(requestId, networkRequest, payload); -} - -bool OllamaProvider::supportsTools() const -{ - return true; -} - -bool OllamaProvider::supportImage() const -{ - return true; -} - -bool OllamaProvider::supportThinking() const -{ - return true; -} - -void OllamaProvider::cancelRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("OllamaProvider: Cancelling request %1").arg(requestId)); - LLMCore::Provider::cancelRequest(requestId); - cleanupRequest(requestId); -} - -void OllamaProvider::onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) -{ - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - QStringList lines = buffers.rawStreamBuffer.processData(data); - - if (data.isEmpty()) { - return; - } - - for (const QString &line : lines) { - if (line.trimmed().isEmpty()) { - continue; - } - - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(line.toUtf8(), &error); - if (doc.isNull()) { - LOG_MESSAGE(QString("Failed to parse JSON: %1").arg(error.errorString())); - continue; - } - - QJsonObject obj = doc.object(); - - if (obj.contains("error") && !obj["error"].toString().isEmpty()) { - LOG_MESSAGE("Error in Ollama response: " + obj["error"].toString()); - continue; - } - - processStreamData(requestId, obj); - } -} - -void OllamaProvider::onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, std::optional error) -{ - if (error) { - LOG_MESSAGE(QString("OllamaProvider request %1 failed: %2").arg(requestId, *error)); - emit requestFailed(requestId, *error); - cleanupRequest(requestId); - return; - } - - if (m_messages.contains(requestId)) { - OllamaMessage *message = m_messages[requestId]; - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Waiting for tools to complete for %1").arg(requestId)); - return; - } - } - - QString finalText; - if (m_messages.contains(requestId)) { - OllamaMessage *message = m_messages[requestId]; - - for (auto block : message->currentBlocks()) { - if (auto textContent = qobject_cast(block)) { - finalText += textContent->text(); - } - } - - if (!finalText.isEmpty()) { - LOG_MESSAGE(QString("Emitting full response for %1, length=%2") - .arg(requestId) - .arg(finalText.length())); - emit fullResponseReceived(requestId, finalText); - } - } - - cleanupRequest(requestId); -} - -void OllamaProvider::onToolExecutionComplete( - const QString &requestId, const QHash &toolResults) -{ - if (!m_messages.contains(requestId)) { - LOG_MESSAGE(QString("ERROR: No message found for request %1").arg(requestId)); - cleanupRequest(requestId); - return; - } - - if (!m_requestUrls.contains(requestId) || !m_originalRequests.contains(requestId)) { - LOG_MESSAGE(QString("ERROR: Missing data for continuation request %1").arg(requestId)); - cleanupRequest(requestId); - return; - } - - LOG_MESSAGE(QString("Tool execution complete for Ollama request %1").arg(requestId)); - - OllamaMessage *message = m_messages[requestId]; - - for (auto it = toolResults.begin(); it != toolResults.end(); ++it) { - auto toolContent = message->getCurrentToolUseContent(); - for (auto tool : toolContent) { - if (tool->id() == it.key()) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(tool->name()); - emit toolExecutionCompleted(requestId, tool->id(), toolStringName, it.value()); - break; - } - } - } - - QJsonObject continuationRequest = m_originalRequests[requestId]; - QJsonArray messages = continuationRequest["messages"].toArray(); - - QJsonObject assistantMessage = message->toProviderFormat(); - messages.append(assistantMessage); - - LOG_MESSAGE(QString("Assistant message with tool_calls:\n%1") - .arg( - QString::fromUtf8( - QJsonDocument(assistantMessage).toJson(QJsonDocument::Indented)))); - - QJsonArray toolResultMessages = message->createToolResultMessages(toolResults); - for (const auto &toolMsg : toolResultMessages) { - messages.append(toolMsg); - LOG_MESSAGE(QString("Tool result message:\n%1") - .arg( - QString::fromUtf8( - QJsonDocument(toolMsg.toObject()).toJson(QJsonDocument::Indented)))); - } - - continuationRequest["messages"] = messages; - - LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results") - .arg(requestId) - .arg(toolResults.size())); - - sendRequest(requestId, m_requestUrls[requestId], continuationRequest); -} - -void OllamaProvider::processStreamData(const QString &requestId, const QJsonObject &data) -{ - OllamaMessage *message = m_messages.value(requestId); - if (!message) { - message = new OllamaMessage(this); - m_messages[requestId] = message; - LOG_MESSAGE(QString("Created NEW OllamaMessage for request %1").arg(requestId)); - - if (m_dataBuffers.contains(requestId)) { - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); - } - } else if ( - m_dataBuffers.contains(requestId) - && message->state() == LLMCore::MessageState::RequiresToolExecution) { - message->startNewContinuation(); - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); - } - - if (data.contains("thinking")) { - QString thinkingDelta = data["thinking"].toString(); - if (!thinkingDelta.isEmpty()) { - message->handleThinkingDelta(thinkingDelta); - LOG_MESSAGE(QString("OllamaProvider: Received thinking delta, length=%1") - .arg(thinkingDelta.length())); - } - } - - if (data.contains("message")) { - QJsonObject messageObj = data["message"].toObject(); - - if (messageObj.contains("thinking")) { - QString thinkingDelta = messageObj["thinking"].toString(); - if (!thinkingDelta.isEmpty()) { - message->handleThinkingDelta(thinkingDelta); - - if (!m_thinkingStarted.contains(requestId)) { - auto thinkingBlocks = message->getCurrentThinkingContent(); - if (!thinkingBlocks.isEmpty() && thinkingBlocks.first()) { - QString currentThinking = thinkingBlocks.first()->thinking(); - QString displayThinking = currentThinking.length() > 50 - ? QString("%1...").arg(currentThinking.left(50)) - : currentThinking; - - emit thinkingBlockReceived(requestId, displayThinking, ""); - m_thinkingStarted.insert(requestId); - } - } - } - } - - if (messageObj.contains("content")) { - QString content = messageObj["content"].toString(); - if (!content.isEmpty()) { - emitThinkingBlocks(requestId, message); - - message->handleContentDelta(content); - - bool hasTextContent = false; - for (auto block : message->currentBlocks()) { - if (qobject_cast(block)) { - hasTextContent = true; - break; - } - } - - if (hasTextContent) { - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - buffers.responseContent += content; - emit partialResponseReceived(requestId, content); - } - } - } - - if (messageObj.contains("tool_calls")) { - QJsonArray toolCalls = messageObj["tool_calls"].toArray(); - LOG_MESSAGE( - QString("OllamaProvider: Found %1 structured tool calls").arg(toolCalls.size())); - for (const auto &toolCallValue : toolCalls) { - message->handleToolCall(toolCallValue.toObject()); - } - } - } - else if (data.contains("response")) { - QString content = data["response"].toString(); - if (!content.isEmpty()) { - message->handleContentDelta(content); - - bool hasTextContent = false; - for (auto block : message->currentBlocks()) { - if (qobject_cast(block)) { - hasTextContent = true; - break; - } - } - - if (hasTextContent) { - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - buffers.responseContent += content; - emit partialResponseReceived(requestId, content); - } - } - } - - if (data["done"].toBool()) { - if (data.contains("signature")) { - QString signature = data["signature"].toString(); - message->handleThinkingComplete(signature); - LOG_MESSAGE(QString("OllamaProvider: Set thinking signature, length=%1") - .arg(signature.length())); - } - - message->handleDone(true); - handleMessageComplete(requestId); - } -} - -void OllamaProvider::handleMessageComplete(const QString &requestId) -{ - if (!m_messages.contains(requestId)) - return; - - OllamaMessage *message = m_messages[requestId]; - - emitThinkingBlocks(requestId, message); - - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Ollama message requires tool execution for %1").arg(requestId)); - - auto toolUseContent = message->getCurrentToolUseContent(); - - if (toolUseContent.isEmpty()) { - LOG_MESSAGE( - QString("WARNING: No tools to execute for %1 despite RequiresToolExecution state") - .arg(requestId)); - return; - } - - for (auto toolContent : toolUseContent) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name()); - emit toolExecutionStarted(requestId, toolContent->id(), toolStringName); - - LOG_MESSAGE( - QString("Executing tool: name=%1, id=%2, input=%3") - .arg(toolContent->name()) - .arg(toolContent->id()) - .arg( - QString::fromUtf8( - QJsonDocument(toolContent->input()).toJson(QJsonDocument::Compact)))); - - m_toolsManager->executeToolCall( - requestId, toolContent->id(), toolContent->name(), toolContent->input()); - } - - } else { - LOG_MESSAGE(QString("Ollama message marked as complete for %1").arg(requestId)); - } -} - -void OllamaProvider::cleanupRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("Cleaning up Ollama request %1").arg(requestId)); - - if (m_messages.contains(requestId)) { - auto msg = m_messages.take(requestId); - msg->deleteLater(); - } - - m_dataBuffers.remove(requestId); - m_requestUrls.remove(requestId); - m_originalRequests.remove(requestId); - m_thinkingEmitted.remove(requestId); - m_thinkingStarted.remove(requestId); - m_toolsManager->cleanupRequest(requestId); -} - -void OllamaProvider::emitThinkingBlocks(const QString &requestId, OllamaMessage *message) -{ - if (!message || m_thinkingEmitted.contains(requestId)) { - return; - } - - auto thinkingBlocks = message->getCurrentThinkingContent(); - if (thinkingBlocks.isEmpty()) { - return; - } - - for (auto thinkingContent : thinkingBlocks) { - emit thinkingBlockReceived( - requestId, thinkingContent->thinking(), thinkingContent->signature()); - LOG_MESSAGE(QString("Emitted thinking block for request %1, thinking length=%2, signature " - "length=%3") - .arg(requestId) - .arg(thinkingContent->thinking().length()) - .arg(thinkingContent->signature().length())); - } - m_thinkingEmitted.insert(requestId); + return m_client; } } // namespace QodeAssist::Providers diff --git a/providers/OllamaProvider.hpp b/providers/OllamaProvider.hpp index 34d2665..04c47d4 100644 --- a/providers/OllamaProvider.hpp +++ b/providers/OllamaProvider.hpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,14 +19,13 @@ #pragma once -#include +#include -#include "OllamaMessage.hpp" -#include "tools/ToolsManager.hpp" +#include namespace QodeAssist::Providers { -class OllamaProvider : public LLMCore::Provider +class OllamaProvider : public PluginLLMCore::Provider { Q_OBJECT public: @@ -36,51 +35,22 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; - bool supportsModelListing() const override; void prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) override; QFuture> getInstalledModels(const QString &url) override; - QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + PluginLLMCore::ProviderID providerID() const override; + PluginLLMCore::ProviderCapabilities capabilities() const override; + + ::LLMCore::BaseClient *client() const override; QString apiKey() const override; - void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; - LLMCore::ProviderID providerID() const override; - - void sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override; - - bool supportsTools() const override; - bool supportImage() const override; - bool supportThinking() const override; - void cancelRequest(const LLMCore::RequestID &requestId) override; - -public slots: - void onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; - void onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, - std::optional error) override; - -private slots: - void onToolExecutionComplete( - const QString &requestId, const QHash &toolResults); private: - void processStreamData(const QString &requestId, const QJsonObject &data); - void handleMessageComplete(const QString &requestId); - void cleanupRequest(const LLMCore::RequestID &requestId); - void emitThinkingBlocks(const QString &requestId, OllamaMessage *message); - - QHash m_messages; - QHash m_requestUrls; - QHash m_originalRequests; - QSet m_thinkingEmitted; - QSet m_thinkingStarted; - Tools::ToolsManager *m_toolsManager; + ::LLMCore::OllamaClient *m_client; }; } // namespace QodeAssist::Providers diff --git a/providers/OpenAICompatProvider.cpp b/providers/OpenAICompatProvider.cpp index e71dc31..309a172 100644 --- a/providers/OpenAICompatProvider.cpp +++ b/providers/OpenAICompatProvider.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -18,8 +18,9 @@ */ #include "OpenAICompatProvider.hpp" +#include -#include "llmcore/ValidationUtils.hpp" +#include "tools/ToolsRegistration.hpp" #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" @@ -30,19 +31,14 @@ #include #include #include -#include namespace QodeAssist::Providers { OpenAICompatProvider::OpenAICompatProvider(QObject *parent) - : LLMCore::Provider(parent) - , m_toolsManager(new Tools::ToolsManager(this)) + : PluginLLMCore::Provider(parent) + , m_client(new ::LLMCore::OpenAIClient(QString(), QString(), QString(), this)) { - connect( - m_toolsManager, - &Tools::ToolsManager::toolExecutionComplete, - this, - &OpenAICompatProvider::onToolExecutionComplete); + Tools::registerQodeAssistTools(m_client->tools()); } QString OpenAICompatProvider::name() const @@ -50,6 +46,11 @@ QString OpenAICompatProvider::name() const return "OpenAI Compatible"; } +QString OpenAICompatProvider::apiKey() const +{ + return Settings::providerSettings().openAiCompatApiKey(); +} + QString OpenAICompatProvider::url() const { return "http://localhost:1234"; @@ -65,16 +66,11 @@ QString OpenAICompatProvider::chatEndpoint() const return "/v1/chat/completions"; } -bool OpenAICompatProvider::supportsModelListing() const -{ - return false; -} - void OpenAICompatProvider::prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) { @@ -98,22 +94,16 @@ void OpenAICompatProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == LLMCore::RequestType::CodeCompletion) { + if (type == PluginLLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); - } else if (type == LLMCore::RequestType::QuickRefactoring) { + } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); } if (isToolsEnabled) { - LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL; - if (type == LLMCore::RequestType::QuickRefactoring) { - filter = LLMCore::RunToolsFilter::OnlyRead; - } - - auto toolsDefinitions = m_toolsManager->getToolsDefinitions( - LLMCore::ToolSchemaFormat::OpenAI, filter); + auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); if (!toolsDefinitions.isEmpty()) { request["tools"] = toolsDefinitions; LOG_MESSAGE( @@ -127,286 +117,19 @@ QFuture> OpenAICompatProvider::getInstalledModels(const QString & return QtFuture::makeReadyFuture(QList{}); } -QList OpenAICompatProvider::validateRequest( - const QJsonObject &request, LLMCore::TemplateType type) +PluginLLMCore::ProviderID OpenAICompatProvider::providerID() const { - const auto templateReq = QJsonObject{ - {"model", {}}, - {"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}}, - {"temperature", {}}, - {"max_tokens", {}}, - {"top_p", {}}, - {"top_k", {}}, - {"frequency_penalty", {}}, - {"presence_penalty", {}}, - {"stop", QJsonArray{}}, - {"stream", {}}, - {"tools", {}}}; - - return LLMCore::ValidationUtils::validateRequestFields(request, templateReq); + return PluginLLMCore::ProviderID::OpenAICompatible; } -QString OpenAICompatProvider::apiKey() const +PluginLLMCore::ProviderCapabilities OpenAICompatProvider::capabilities() const { - return Settings::providerSettings().openAiCompatApiKey(); + return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image; } -void OpenAICompatProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const +::LLMCore::BaseClient *OpenAICompatProvider::client() const { - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - if (!apiKey().isEmpty()) { - networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8()); - } -} - -LLMCore::ProviderID OpenAICompatProvider::providerID() const -{ - return LLMCore::ProviderID::OpenAICompatible; -} - -void OpenAICompatProvider::sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) -{ - if (!m_messages.contains(requestId)) { - m_dataBuffers[requestId].clear(); - } - - m_requestUrls[requestId] = url; - m_originalRequests[requestId] = payload; - - QNetworkRequest networkRequest(url); - prepareNetworkRequest(networkRequest); - - LOG_MESSAGE( - QString("OpenAICompatProvider: Sending request %1 to %2").arg(requestId, url.toString())); - - httpClient()->postStreaming(requestId, networkRequest, payload); -} - -bool OpenAICompatProvider::supportsTools() const -{ - return true; -} - -bool OpenAICompatProvider::supportImage() const -{ - return true; -} - -void OpenAICompatProvider::cancelRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("OpenAICompatProvider: Cancelling request %1").arg(requestId)); - LLMCore::Provider::cancelRequest(requestId); - cleanupRequest(requestId); -} - -void OpenAICompatProvider::onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) -{ - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - QStringList lines = buffers.rawStreamBuffer.processData(data); - - for (const QString &line : lines) { - if (line.trimmed().isEmpty() || line == "data: [DONE]") { - continue; - } - - QJsonObject chunk = parseEventLine(line); - if (chunk.isEmpty()) - continue; - - processStreamChunk(requestId, chunk); - } -} - -void OpenAICompatProvider::onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, std::optional error) -{ - if (error) { - LOG_MESSAGE(QString("OpenAICompatProvider request %1 failed: %2").arg(requestId, *error)); - emit requestFailed(requestId, *error); - cleanupRequest(requestId); - return; - } - - if (m_messages.contains(requestId)) { - OpenAIMessage *message = m_messages[requestId]; - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Waiting for tools to complete for %1").arg(requestId)); - m_dataBuffers.remove(requestId); - return; - } - } - - if (m_dataBuffers.contains(requestId)) { - const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - if (!buffers.responseContent.isEmpty()) { - LOG_MESSAGE(QString("Emitting full response for %1").arg(requestId)); - emit fullResponseReceived(requestId, buffers.responseContent); - } - } - - cleanupRequest(requestId); -} - -void OpenAICompatProvider::onToolExecutionComplete( - const QString &requestId, const QHash &toolResults) -{ - if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) { - LOG_MESSAGE(QString("ERROR: Missing data for continuation request %1").arg(requestId)); - cleanupRequest(requestId); - return; - } - - LOG_MESSAGE(QString("Tool execution complete for OpenAICompat request %1").arg(requestId)); - - for (auto it = toolResults.begin(); it != toolResults.end(); ++it) { - OpenAIMessage *message = m_messages[requestId]; - auto toolContent = message->getCurrentToolUseContent(); - for (auto tool : toolContent) { - if (tool->id() == it.key()) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(tool->name()); - emit toolExecutionCompleted( - requestId, tool->id(), toolStringName, toolResults[tool->id()]); - break; - } - } - } - - OpenAIMessage *message = m_messages[requestId]; - QJsonObject continuationRequest = m_originalRequests[requestId]; - QJsonArray messages = continuationRequest["messages"].toArray(); - - messages.append(message->toProviderFormat()); - - QJsonArray toolResultMessages = message->createToolResultMessages(toolResults); - for (const auto &toolMsg : toolResultMessages) { - messages.append(toolMsg); - } - - continuationRequest["messages"] = messages; - - LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results") - .arg(requestId) - .arg(toolResults.size())); - - sendRequest(requestId, m_requestUrls[requestId], continuationRequest); -} - -void OpenAICompatProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk) -{ - QJsonArray choices = chunk["choices"].toArray(); - if (choices.isEmpty()) { - return; - } - - QJsonObject choice = choices[0].toObject(); - QJsonObject delta = choice["delta"].toObject(); - QString finishReason = choice["finish_reason"].toString(); - - OpenAIMessage *message = m_messages.value(requestId); - if (!message) { - message = new OpenAIMessage(this); - m_messages[requestId] = message; - LOG_MESSAGE(QString("Created NEW OpenAIMessage for request %1").arg(requestId)); - - if (m_dataBuffers.contains(requestId)) { - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); - } - } else if ( - m_dataBuffers.contains(requestId) - && message->state() == LLMCore::MessageState::RequiresToolExecution) { - message->startNewContinuation(); - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); - } - - if (delta.contains("content") && !delta["content"].isNull()) { - QString content = delta["content"].toString(); - message->handleContentDelta(content); - - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - buffers.responseContent += content; - emit partialResponseReceived(requestId, content); - } - - if (delta.contains("tool_calls")) { - QJsonArray toolCalls = delta["tool_calls"].toArray(); - for (const auto &toolCallValue : toolCalls) { - QJsonObject toolCall = toolCallValue.toObject(); - int index = toolCall["index"].toInt(); - - if (toolCall.contains("id")) { - QString id = toolCall["id"].toString(); - QJsonObject function = toolCall["function"].toObject(); - QString name = function["name"].toString(); - message->handleToolCallStart(index, id, name); - } - - if (toolCall.contains("function")) { - QJsonObject function = toolCall["function"].toObject(); - if (function.contains("arguments")) { - QString args = function["arguments"].toString(); - message->handleToolCallDelta(index, args); - } - } - } - } - - if (!finishReason.isEmpty() && finishReason != "null") { - for (int i = 0; i < 10; ++i) { - message->handleToolCallComplete(i); - } - - message->handleFinishReason(finishReason); - handleMessageComplete(requestId); - } -} - -void OpenAICompatProvider::handleMessageComplete(const QString &requestId) -{ - if (!m_messages.contains(requestId)) - return; - - OpenAIMessage *message = m_messages[requestId]; - - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("OpenAICompat message requires tool execution for %1").arg(requestId)); - - auto toolUseContent = message->getCurrentToolUseContent(); - - if (toolUseContent.isEmpty()) { - LOG_MESSAGE(QString("No tools to execute for %1").arg(requestId)); - return; - } - - for (auto toolContent : toolUseContent) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name()); - emit toolExecutionStarted(requestId, toolContent->id(), toolStringName); - m_toolsManager->executeToolCall( - requestId, toolContent->id(), toolContent->name(), toolContent->input()); - } - - } else { - LOG_MESSAGE(QString("OpenAICompat message marked as complete for %1").arg(requestId)); - } -} - -void OpenAICompatProvider::cleanupRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("Cleaning up OpenAICompat request %1").arg(requestId)); - - if (m_messages.contains(requestId)) { - OpenAIMessage *message = m_messages.take(requestId); - message->deleteLater(); - } - - m_dataBuffers.remove(requestId); - m_requestUrls.remove(requestId); - m_originalRequests.remove(requestId); - m_toolsManager->cleanupRequest(requestId); + return m_client; } } // namespace QodeAssist::Providers diff --git a/providers/OpenAICompatProvider.hpp b/providers/OpenAICompatProvider.hpp index e6b70f3..4a7766f 100644 --- a/providers/OpenAICompatProvider.hpp +++ b/providers/OpenAICompatProvider.hpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,13 +19,12 @@ #pragma once -#include "OpenAIMessage.hpp" -#include "tools/ToolsManager.hpp" -#include +#include +#include namespace QodeAssist::Providers { -class OpenAICompatProvider : public LLMCore::Provider +class OpenAICompatProvider : public PluginLLMCore::Provider { Q_OBJECT public: @@ -35,47 +34,22 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; - bool supportsModelListing() const override; void prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) override; QFuture> getInstalledModels(const QString &url) override; - QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + PluginLLMCore::ProviderID providerID() const override; + PluginLLMCore::ProviderCapabilities capabilities() const override; + + ::LLMCore::BaseClient *client() const override; QString apiKey() const override; - void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; - LLMCore::ProviderID providerID() const override; - - void sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override; - - bool supportsTools() const override; - bool supportImage() const override; - void cancelRequest(const LLMCore::RequestID &requestId) override; - -public slots: - void onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; - void onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, - std::optional error) override; - -private slots: - void onToolExecutionComplete( - const QString &requestId, const QHash &toolResults); private: - void processStreamChunk(const QString &requestId, const QJsonObject &chunk); - void handleMessageComplete(const QString &requestId); - void cleanupRequest(const LLMCore::RequestID &requestId); - - QHash m_messages; - QHash m_requestUrls; - QHash m_originalRequests; - Tools::ToolsManager *m_toolsManager; + ::LLMCore::OpenAIClient *m_client; }; } // namespace QodeAssist::Providers diff --git a/providers/OpenAIMessage.cpp b/providers/OpenAIMessage.cpp index 9417cd5..06335e9 100644 --- a/providers/OpenAIMessage.cpp +++ b/providers/OpenAIMessage.cpp @@ -46,7 +46,7 @@ void OpenAIMessage::handleToolCallStart(int index, const QString &id, const QStr m_currentBlocks.append(nullptr); } - auto toolContent = new LLMCore::ToolUseContent(id, name); + auto toolContent = new PluginLLMCore::ToolUseContent(id, name); toolContent->setParent(this); m_currentBlocks[index] = toolContent; m_pendingToolArguments[index] = ""; @@ -73,7 +73,7 @@ void OpenAIMessage::handleToolCallComplete(int index) } if (index < m_currentBlocks.size()) { - if (auto toolContent = qobject_cast(m_currentBlocks[index])) { + if (auto toolContent = qobject_cast(m_currentBlocks[index])) { toolContent->setInput(argsObject); } } @@ -100,10 +100,10 @@ QJsonObject OpenAIMessage::toProviderFormat() const if (!block) continue; - if (auto text = qobject_cast(block)) { + if (auto text = qobject_cast(block)) { textContent += text->text(); - } else if (auto tool = qobject_cast(block)) { - toolCalls.append(tool->toJson(LLMCore::ProviderFormat::OpenAI)); + } else if (auto tool = qobject_cast(block)) { + toolCalls.append(tool->toJson(PluginLLMCore::ProviderFormat::OpenAI)); } } @@ -126,20 +126,20 @@ QJsonArray OpenAIMessage::createToolResultMessages(const QHash for (auto toolContent : getCurrentToolUseContent()) { if (toolResults.contains(toolContent->id())) { - auto toolResult = std::make_unique( + auto toolResult = std::make_unique( toolContent->id(), toolResults[toolContent->id()]); - messages.append(toolResult->toJson(LLMCore::ProviderFormat::OpenAI)); + messages.append(toolResult->toJson(PluginLLMCore::ProviderFormat::OpenAI)); } } return messages; } -QList OpenAIMessage::getCurrentToolUseContent() const +QList OpenAIMessage::getCurrentToolUseContent() const { - QList toolBlocks; + QList toolBlocks; for (auto block : m_currentBlocks) { - if (auto toolContent = qobject_cast(block)) { + if (auto toolContent = qobject_cast(block)) { toolBlocks.append(toolContent); } } @@ -153,29 +153,29 @@ void OpenAIMessage::startNewContinuation() m_currentBlocks.clear(); m_pendingToolArguments.clear(); m_finishReason.clear(); - m_state = LLMCore::MessageState::Building; + m_state = PluginLLMCore::MessageState::Building; } void OpenAIMessage::updateStateFromFinishReason() { if (m_finishReason == "tool_calls" && !getCurrentToolUseContent().empty()) { - m_state = LLMCore::MessageState::RequiresToolExecution; + m_state = PluginLLMCore::MessageState::RequiresToolExecution; } else if (m_finishReason == "stop") { - m_state = LLMCore::MessageState::Final; + m_state = PluginLLMCore::MessageState::Final; } else { - m_state = LLMCore::MessageState::Complete; + m_state = PluginLLMCore::MessageState::Complete; } } -LLMCore::TextContent *OpenAIMessage::getOrCreateTextContent() +PluginLLMCore::TextContent *OpenAIMessage::getOrCreateTextContent() { for (auto block : m_currentBlocks) { - if (auto textContent = qobject_cast(block)) { + if (auto textContent = qobject_cast(block)) { return textContent; } } - return addCurrentContent(); + return addCurrentContent(); } } // namespace QodeAssist::Providers diff --git a/providers/OpenAIMessage.hpp b/providers/OpenAIMessage.hpp index 8a1fe3e..ab00334 100644 --- a/providers/OpenAIMessage.hpp +++ b/providers/OpenAIMessage.hpp @@ -19,7 +19,7 @@ #pragma once -#include +#include namespace QodeAssist::Providers { @@ -38,19 +38,19 @@ public: QJsonObject toProviderFormat() const; QJsonArray createToolResultMessages(const QHash &toolResults) const; - LLMCore::MessageState state() const { return m_state; } - QList getCurrentToolUseContent() const; + PluginLLMCore::MessageState state() const { return m_state; } + QList getCurrentToolUseContent() const; void startNewContinuation(); private: QString m_finishReason; - LLMCore::MessageState m_state = LLMCore::MessageState::Building; - QList m_currentBlocks; + PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building; + QList m_currentBlocks; QHash m_pendingToolArguments; void updateStateFromFinishReason(); - LLMCore::TextContent *getOrCreateTextContent(); + PluginLLMCore::TextContent *getOrCreateTextContent(); template T *addCurrentContent(Args &&...args) diff --git a/providers/OpenAIProvider.cpp b/providers/OpenAIProvider.cpp index aa5df7f..6d20ab7 100644 --- a/providers/OpenAIProvider.cpp +++ b/providers/OpenAIProvider.cpp @@ -19,7 +19,8 @@ #include "OpenAIProvider.hpp" -#include "llmcore/ValidationUtils.hpp" +#include +#include "tools/ToolsRegistration.hpp" #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" @@ -34,14 +35,10 @@ namespace QodeAssist::Providers { OpenAIProvider::OpenAIProvider(QObject *parent) - : LLMCore::Provider(parent) - , m_toolsManager(new Tools::ToolsManager(this)) + : PluginLLMCore::Provider(parent) + , m_client(new ::LLMCore::OpenAIClient(QString(), QString(), QString(), this)) { - connect( - m_toolsManager, - &Tools::ToolsManager::toolExecutionComplete, - this, - &OpenAIProvider::onToolExecutionComplete); + Tools::registerQodeAssistTools(m_client->tools()); } QString OpenAIProvider::name() const @@ -49,6 +46,11 @@ QString OpenAIProvider::name() const return "OpenAI"; } +QString OpenAIProvider::apiKey() const +{ + return Settings::providerSettings().openAiApiKey(); +} + QString OpenAIProvider::url() const { return "https://api.openai.com"; @@ -64,16 +66,11 @@ QString OpenAIProvider::chatEndpoint() const return "/v1/chat/completions"; } -bool OpenAIProvider::supportsModelListing() const -{ - return true; -} - void OpenAIProvider::prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) { @@ -116,22 +113,16 @@ void OpenAIProvider::prepareRequest( request["presence_penalty"] = settings.presencePenalty(); }; - if (type == LLMCore::RequestType::CodeCompletion) { + if (type == PluginLLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); - } else if (type == LLMCore::RequestType::QuickRefactoring) { + } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { applyModelParams(Settings::quickRefactorSettings()); } else { applyModelParams(Settings::chatAssistantSettings()); } if (isToolsEnabled) { - LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL; - if (type == LLMCore::RequestType::QuickRefactoring) { - filter = LLMCore::RunToolsFilter::OnlyRead; - } - - auto toolsDefinitions = m_toolsManager->getToolsDefinitions( - LLMCore::ToolSchemaFormat::OpenAI, filter); + auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); if (!toolsDefinitions.isEmpty()) { request["tools"] = toolsDefinitions; LOG_MESSAGE(QString("Added %1 tools to OpenAI request").arg(toolsDefinitions.size())); @@ -139,318 +130,37 @@ void OpenAIProvider::prepareRequest( } } -QFuture> OpenAIProvider::getInstalledModels(const QString &url) +QFuture> OpenAIProvider::getInstalledModels(const QString &baseUrl) { - QNetworkRequest request(QString("%1/v1/models").arg(url)); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - if (!apiKey().isEmpty()) { - request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8()); - } - - return httpClient()->get(request).then([](const QByteArray &data) { - QList models; - QJsonObject jsonObject = QJsonDocument::fromJson(data).object(); - - if (jsonObject.contains("data")) { - QJsonArray modelArray = jsonObject["data"].toArray(); - for (const QJsonValue &value : modelArray) { - QJsonObject modelObject = value.toObject(); - if (modelObject.contains("id")) { - QString modelId = modelObject["id"].toString(); - if (!modelId.contains("dall-e") && !modelId.contains("whisper") - && !modelId.contains("tts") && !modelId.contains("davinci") - && !modelId.contains("babbage") && !modelId.contains("omni")) { - models.append(modelId); - } - } + m_client->setUrl(baseUrl); + m_client->setApiKey(apiKey()); + return m_client->listModels().then([](const QList &allModels) { + QList filtered; + for (const QString &modelId : allModels) { + if (!modelId.contains("dall-e") && !modelId.contains("whisper") + && !modelId.contains("tts") && !modelId.contains("davinci") + && !modelId.contains("babbage") && !modelId.contains("omni")) { + filtered.append(modelId); } } - return models; - }).onFailed([](const std::exception &e) { - LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(e.what())); - return QList{}; + return filtered; }); } -QList OpenAIProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type) +PluginLLMCore::ProviderID OpenAIProvider::providerID() const { - const auto templateReq = QJsonObject{ - {"model", {}}, - {"messages", QJsonArray{{QJsonObject{{"role", {}}, {"content", {}}}}}}, - {"temperature", {}}, - {"max_tokens", {}}, - {"max_completion_tokens", {}}, // New parameter for newer models - {"top_p", {}}, - {"top_k", {}}, - {"frequency_penalty", {}}, - {"presence_penalty", {}}, - {"stop", QJsonArray{}}, - {"stream", {}}, - {"tools", {}}}; - - return LLMCore::ValidationUtils::validateRequestFields(request, templateReq); + return PluginLLMCore::ProviderID::OpenAI; } -QString OpenAIProvider::apiKey() const +PluginLLMCore::ProviderCapabilities OpenAIProvider::capabilities() const { - return Settings::providerSettings().openAiApiKey(); + return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image + | PluginLLMCore::ProviderCapability::ModelListing; } -void OpenAIProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const +::LLMCore::BaseClient *OpenAIProvider::client() const { - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - if (!apiKey().isEmpty()) { - networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8()); - } -} - -LLMCore::ProviderID OpenAIProvider::providerID() const -{ - return LLMCore::ProviderID::OpenAI; -} - -void OpenAIProvider::sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) -{ - if (!m_messages.contains(requestId)) { - m_dataBuffers[requestId].clear(); - } - - m_requestUrls[requestId] = url; - m_originalRequests[requestId] = payload; - - QNetworkRequest networkRequest(url); - prepareNetworkRequest(networkRequest); - - LOG_MESSAGE(QString("OpenAIProvider: Sending request %1 to %2").arg(requestId, url.toString())); - - httpClient()->postStreaming(requestId, networkRequest, payload); -} - -bool OpenAIProvider::supportsTools() const -{ - return true; -} - -bool OpenAIProvider::supportImage() const -{ - return true; -} - -void OpenAIProvider::cancelRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("OpenAIProvider: Cancelling request %1").arg(requestId)); - LLMCore::Provider::cancelRequest(requestId); - cleanupRequest(requestId); -} - -void OpenAIProvider::onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) -{ - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - QStringList lines = buffers.rawStreamBuffer.processData(data); - - for (const QString &line : lines) { - if (line.trimmed().isEmpty() || line == "data: [DONE]") { - continue; - } - - QJsonObject chunk = parseEventLine(line); - if (chunk.isEmpty()) - continue; - - processStreamChunk(requestId, chunk); - } -} - -void OpenAIProvider::onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, std::optional error) -{ - if (error) { - LOG_MESSAGE(QString("OpenAIProvider request %1 failed: %2").arg(requestId, *error)); - emit requestFailed(requestId, *error); - cleanupRequest(requestId); - return; - } - - if (m_messages.contains(requestId)) { - OpenAIMessage *message = m_messages[requestId]; - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("Waiting for tools to complete for %1").arg(requestId)); - m_dataBuffers.remove(requestId); - return; - } - } - - if (m_dataBuffers.contains(requestId)) { - const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - if (!buffers.responseContent.isEmpty()) { - LOG_MESSAGE(QString("Emitting full response for %1").arg(requestId)); - emit fullResponseReceived(requestId, buffers.responseContent); - } - } - - cleanupRequest(requestId); -} - -void OpenAIProvider::onToolExecutionComplete( - const QString &requestId, const QHash &toolResults) -{ - if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) { - LOG_MESSAGE(QString("ERROR: Missing data for continuation request %1").arg(requestId)); - cleanupRequest(requestId); - return; - } - - LOG_MESSAGE(QString("Tool execution complete for OpenAI request %1").arg(requestId)); - - for (auto it = toolResults.begin(); it != toolResults.end(); ++it) { - OpenAIMessage *message = m_messages[requestId]; - auto toolContent = message->getCurrentToolUseContent(); - for (auto tool : toolContent) { - if (tool->id() == it.key()) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(tool->name()); - emit toolExecutionCompleted( - requestId, tool->id(), toolStringName, toolResults[tool->id()]); - break; - } - } - } - - OpenAIMessage *message = m_messages[requestId]; - QJsonObject continuationRequest = m_originalRequests[requestId]; - QJsonArray messages = continuationRequest["messages"].toArray(); - - messages.append(message->toProviderFormat()); - - QJsonArray toolResultMessages = message->createToolResultMessages(toolResults); - for (const auto &toolMsg : toolResultMessages) { - messages.append(toolMsg); - } - - continuationRequest["messages"] = messages; - - LOG_MESSAGE(QString("Sending continuation request for %1 with %2 tool results") - .arg(requestId) - .arg(toolResults.size())); - - sendRequest(requestId, m_requestUrls[requestId], continuationRequest); -} - -void OpenAIProvider::processStreamChunk(const QString &requestId, const QJsonObject &chunk) -{ - QJsonArray choices = chunk["choices"].toArray(); - if (choices.isEmpty()) { - return; - } - - QJsonObject choice = choices[0].toObject(); - QJsonObject delta = choice["delta"].toObject(); - QString finishReason = choice["finish_reason"].toString(); - - OpenAIMessage *message = m_messages.value(requestId); - if (!message) { - message = new OpenAIMessage(this); - m_messages[requestId] = message; - LOG_MESSAGE(QString("Created NEW OpenAIAPIMessage for request %1").arg(requestId)); - - if (m_dataBuffers.contains(requestId)) { - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Starting continuation for request %1").arg(requestId)); - } - } else if ( - m_dataBuffers.contains(requestId) - && message->state() == LLMCore::MessageState::RequiresToolExecution) { - message->startNewContinuation(); - emit continuationStarted(requestId); - LOG_MESSAGE(QString("Cleared message state for continuation request %1").arg(requestId)); - } - - if (delta.contains("content") && !delta["content"].isNull()) { - QString content = delta["content"].toString(); - message->handleContentDelta(content); - - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - buffers.responseContent += content; - emit partialResponseReceived(requestId, content); - } - - if (delta.contains("tool_calls")) { - QJsonArray toolCalls = delta["tool_calls"].toArray(); - for (const auto &toolCallValue : toolCalls) { - QJsonObject toolCall = toolCallValue.toObject(); - int index = toolCall["index"].toInt(); - - if (toolCall.contains("id")) { - QString id = toolCall["id"].toString(); - QJsonObject function = toolCall["function"].toObject(); - QString name = function["name"].toString(); - message->handleToolCallStart(index, id, name); - } - - if (toolCall.contains("function")) { - QJsonObject function = toolCall["function"].toObject(); - if (function.contains("arguments")) { - QString args = function["arguments"].toString(); - message->handleToolCallDelta(index, args); - } - } - } - } - - if (!finishReason.isEmpty() && finishReason != "null") { - for (int i = 0; i < 10; ++i) { - message->handleToolCallComplete(i); - } - - message->handleFinishReason(finishReason); - handleMessageComplete(requestId); - } -} - -void OpenAIProvider::handleMessageComplete(const QString &requestId) -{ - if (!m_messages.contains(requestId)) - return; - - OpenAIMessage *message = m_messages[requestId]; - - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - LOG_MESSAGE(QString("OpenAI message requires tool execution for %1").arg(requestId)); - - auto toolUseContent = message->getCurrentToolUseContent(); - - if (toolUseContent.isEmpty()) { - LOG_MESSAGE(QString("No tools to execute for %1").arg(requestId)); - return; - } - - for (auto toolContent : toolUseContent) { - auto toolStringName = m_toolsManager->toolsFactory()->getStringName(toolContent->name()); - emit toolExecutionStarted(requestId, toolContent->id(), toolStringName); - m_toolsManager->executeToolCall( - requestId, toolContent->id(), toolContent->name(), toolContent->input()); - } - - } else { - LOG_MESSAGE(QString("OpenAI message marked as complete for %1").arg(requestId)); - } -} - -void OpenAIProvider::cleanupRequest(const LLMCore::RequestID &requestId) -{ - LOG_MESSAGE(QString("Cleaning up OpenAI request %1").arg(requestId)); - - if (m_messages.contains(requestId)) { - OpenAIMessage *message = m_messages.take(requestId); - message->deleteLater(); - } - - m_dataBuffers.remove(requestId); - m_requestUrls.remove(requestId); - m_originalRequests.remove(requestId); - m_toolsManager->cleanupRequest(requestId); + return m_client; } } // namespace QodeAssist::Providers diff --git a/providers/OpenAIProvider.hpp b/providers/OpenAIProvider.hpp index 52535a7..bf24f0f 100644 --- a/providers/OpenAIProvider.hpp +++ b/providers/OpenAIProvider.hpp @@ -19,13 +19,12 @@ #pragma once -#include "OpenAIMessage.hpp" -#include "tools/ToolsManager.hpp" -#include +#include +#include namespace QodeAssist::Providers { -class OpenAIProvider : public LLMCore::Provider +class OpenAIProvider : public PluginLLMCore::Provider { Q_OBJECT public: @@ -35,47 +34,22 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; - bool supportsModelListing() const override; void prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) override; QFuture> getInstalledModels(const QString &url) override; - QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + PluginLLMCore::ProviderID providerID() const override; + PluginLLMCore::ProviderCapabilities capabilities() const override; + + ::LLMCore::BaseClient *client() const override; QString apiKey() const override; - void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; - LLMCore::ProviderID providerID() const override; - - void sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override; - - bool supportsTools() const override; - bool supportImage() const override; - void cancelRequest(const LLMCore::RequestID &requestId) override; - -public slots: - void onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; - void onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, - std::optional error) override; - -private slots: - void onToolExecutionComplete( - const QString &requestId, const QHash &toolResults); private: - void processStreamChunk(const QString &requestId, const QJsonObject &chunk); - void handleMessageComplete(const QString &requestId); - void cleanupRequest(const LLMCore::RequestID &requestId); - - QHash m_messages; - QHash m_requestUrls; - QHash m_originalRequests; - Tools::ToolsManager *m_toolsManager; + ::LLMCore::OpenAIClient *m_client; }; } // namespace QodeAssist::Providers diff --git a/providers/OpenAIResponsesMessage.cpp b/providers/OpenAIResponsesMessage.cpp index 8806262..d356e70 100644 --- a/providers/OpenAIResponsesMessage.cpp +++ b/providers/OpenAIResponsesMessage.cpp @@ -52,7 +52,7 @@ void OpenAIResponsesMessage::handleItemDelta(const QJsonObject &item) void OpenAIResponsesMessage::handleToolCallStart(const QString &callId, const QString &name) { - auto toolContent = new LLMCore::ToolUseContent(callId, name); + auto toolContent = new PluginLLMCore::ToolUseContent(callId, name); toolContent->setParent(this); m_items.append(toolContent); m_toolCalls[callId] = toolContent; @@ -86,7 +86,7 @@ void OpenAIResponsesMessage::handleToolCallComplete(const QString &callId) void OpenAIResponsesMessage::handleReasoningStart(const QString &itemId) { - auto thinkingContent = new LLMCore::ThinkingContent(); + auto thinkingContent = new PluginLLMCore::ThinkingContent(); thinkingContent->setParent(this); m_items.append(thinkingContent); m_thinkingBlocks[itemId] = thinkingContent; @@ -115,13 +115,13 @@ QList OpenAIResponsesMessage::toItemsFormat() const QList items; QString textContent; - QList toolCalls; + QList toolCalls; for (const auto *block : m_items) { - if (const auto *text = qobject_cast(block)) { + if (const auto *text = qobject_cast(block)) { textContent += text->text(); - } else if (auto *tool = qobject_cast( - const_cast(block))) { + } else if (auto *tool = qobject_cast( + const_cast(block))) { toolCalls.append(tool); } } @@ -146,22 +146,22 @@ QList OpenAIResponsesMessage::toItemsFormat() const return items; } -QList OpenAIResponsesMessage::getCurrentToolUseContent() const +QList OpenAIResponsesMessage::getCurrentToolUseContent() const { - QList toolBlocks; + QList toolBlocks; for (auto *block : m_items) { - if (auto *toolContent = qobject_cast(block)) { + if (auto *toolContent = qobject_cast(block)) { toolBlocks.append(toolContent); } } return toolBlocks; } -QList OpenAIResponsesMessage::getCurrentThinkingContent() const +QList OpenAIResponsesMessage::getCurrentThinkingContent() const { - QList thinkingBlocks; + QList thinkingBlocks; for (auto *block : m_items) { - if (auto *thinkingContent = qobject_cast(block)) { + if (auto *thinkingContent = qobject_cast(block)) { thinkingBlocks.append(thinkingContent); } } @@ -189,7 +189,7 @@ QString OpenAIResponsesMessage::accumulatedText() const { QString text; for (const auto *block : m_items) { - if (const auto *textContent = qobject_cast(block)) { + if (const auto *textContent = qobject_cast(block)) { text += textContent->text(); } } @@ -202,28 +202,28 @@ void OpenAIResponsesMessage::updateStateFromStatus() if (m_status == "completed") { if (!getCurrentToolUseContent().isEmpty()) { - m_state = LLMCore::MessageState::RequiresToolExecution; + m_state = PluginLLMCore::MessageState::RequiresToolExecution; } else { - m_state = LLMCore::MessageState::Complete; + m_state = PluginLLMCore::MessageState::Complete; } } else if (m_status == "in_progress") { - m_state = LLMCore::MessageState::Building; + m_state = PluginLLMCore::MessageState::Building; } else if (m_status == "failed" || m_status == "cancelled" || m_status == "incomplete") { - m_state = LLMCore::MessageState::Final; + m_state = PluginLLMCore::MessageState::Final; } else { - m_state = LLMCore::MessageState::Building; + m_state = PluginLLMCore::MessageState::Building; } } -LLMCore::TextContent *OpenAIResponsesMessage::getOrCreateTextItem() +PluginLLMCore::TextContent *OpenAIResponsesMessage::getOrCreateTextItem() { for (auto *block : m_items) { - if (auto *textContent = qobject_cast(block)) { + if (auto *textContent = qobject_cast(block)) { return textContent; } } - auto *textContent = new LLMCore::TextContent(); + auto *textContent = new PluginLLMCore::TextContent(); textContent->setParent(this); m_items.append(textContent); return textContent; @@ -239,7 +239,7 @@ void OpenAIResponsesMessage::startNewContinuation() m_pendingToolArguments.clear(); m_status.clear(); - m_state = LLMCore::MessageState::Building; + m_state = PluginLLMCore::MessageState::Building; } } // namespace QodeAssist::Providers diff --git a/providers/OpenAIResponsesMessage.hpp b/providers/OpenAIResponsesMessage.hpp index 0b0a497..f4e0569 100644 --- a/providers/OpenAIResponsesMessage.hpp +++ b/providers/OpenAIResponsesMessage.hpp @@ -19,7 +19,7 @@ #pragma once -#include +#include namespace QodeAssist::Providers { @@ -41,10 +41,10 @@ public: QList toItemsFormat() const; QJsonArray createToolResultItems(const QHash &toolResults) const; - LLMCore::MessageState state() const noexcept { return m_state; } + PluginLLMCore::MessageState state() const noexcept { return m_state; } QString accumulatedText() const; - QList getCurrentToolUseContent() const; - QList getCurrentThinkingContent() const; + QList getCurrentToolUseContent() const; + QList getCurrentThinkingContent() const; bool hasToolCalls() const noexcept { return !m_toolCalls.isEmpty(); } bool hasThinkingContent() const noexcept { return !m_thinkingBlocks.isEmpty(); } @@ -53,14 +53,14 @@ public: private: QString m_status; - LLMCore::MessageState m_state = LLMCore::MessageState::Building; - QList m_items; + PluginLLMCore::MessageState m_state = PluginLLMCore::MessageState::Building; + QList m_items; QHash m_pendingToolArguments; - QHash m_toolCalls; - QHash m_thinkingBlocks; + QHash m_toolCalls; + QHash m_thinkingBlocks; void updateStateFromStatus(); - LLMCore::TextContent *getOrCreateTextItem(); + PluginLLMCore::TextContent *getOrCreateTextItem(); }; } // namespace QodeAssist::Providers diff --git a/providers/OpenAIResponsesProvider.cpp b/providers/OpenAIResponsesProvider.cpp index fa7c444..d08920e 100644 --- a/providers/OpenAIResponsesProvider.cpp +++ b/providers/OpenAIResponsesProvider.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -18,9 +18,9 @@ */ #include "OpenAIResponsesProvider.hpp" -#include "OpenAIResponses/ResponseObject.hpp" +#include +#include "tools/ToolsRegistration.hpp" -#include "llmcore/ValidationUtils.hpp" #include "logger/Logger.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/CodeCompletionSettings.hpp" @@ -35,14 +35,10 @@ namespace QodeAssist::Providers { OpenAIResponsesProvider::OpenAIResponsesProvider(QObject *parent) - : LLMCore::Provider(parent) - , m_toolsManager(new Tools::ToolsManager(this)) + : PluginLLMCore::Provider(parent) + , m_client(new ::LLMCore::OpenAIResponsesClient(QString(), QString(), QString(), this)) { - connect( - m_toolsManager, - &Tools::ToolsManager::toolExecutionComplete, - this, - &OpenAIResponsesProvider::onToolExecutionComplete); + Tools::registerQodeAssistTools(m_client->tools()); } QString OpenAIResponsesProvider::name() const @@ -50,6 +46,11 @@ QString OpenAIResponsesProvider::name() const return "OpenAI Responses"; } +QString OpenAIResponsesProvider::apiKey() const +{ + return Settings::providerSettings().openAiApiKey(); +} + QString OpenAIResponsesProvider::url() const { return "https://api.openai.com"; @@ -65,16 +66,11 @@ QString OpenAIResponsesProvider::chatEndpoint() const return "/v1/responses"; } -bool OpenAIResponsesProvider::supportsModelListing() const -{ - return true; -} - void OpenAIResponsesProvider::prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) { @@ -97,7 +93,7 @@ void OpenAIResponsesProvider::prepareRequest( if (effortStr.isEmpty()) { effortStr = "medium"; } - + QJsonObject reasoning; reasoning["effort"] = effortStr; request["reasoning"] = reasoning; @@ -109,9 +105,9 @@ void OpenAIResponsesProvider::prepareRequest( request["include"] = include; }; - if (type == LLMCore::RequestType::CodeCompletion) { + if (type == PluginLLMCore::RequestType::CodeCompletion) { applyModelParams(Settings::codeCompletionSettings()); - } else if (type == LLMCore::RequestType::QuickRefactoring) { + } else if (type == PluginLLMCore::RequestType::QuickRefactoring) { const auto &qrSettings = Settings::quickRefactorSettings(); applyModelParams(qrSettings); @@ -128,12 +124,8 @@ void OpenAIResponsesProvider::prepareRequest( } if (isToolsEnabled) { - const LLMCore::RunToolsFilter filter = (type == LLMCore::RequestType::QuickRefactoring) - ? LLMCore::RunToolsFilter::OnlyRead - : LLMCore::RunToolsFilter::ALL; - const auto toolsDefinitions - = m_toolsManager->getToolsDefinitions(LLMCore::ToolSchemaFormat::OpenAI, filter); + = m_client->tools()->getToolsDefinitions(); if (!toolsDefinitions.isEmpty()) { QJsonArray responsesTools; @@ -156,496 +148,40 @@ void OpenAIResponsesProvider::prepareRequest( request["stream"] = true; } -QFuture> OpenAIResponsesProvider::getInstalledModels(const QString &url) +QFuture> OpenAIResponsesProvider::getInstalledModels(const QString &baseUrl) { - QNetworkRequest request(QString("%1/v1/models").arg(url)); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - if (!apiKey().isEmpty()) { - request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8()); - } - - return httpClient()->get(request).then([](const QByteArray &data) { - QList models; - const QJsonObject jsonObject = QJsonDocument::fromJson(data).object(); - - if (jsonObject.contains("data")) { - const QJsonArray modelArray = jsonObject["data"].toArray(); - models.reserve(modelArray.size()); - - static const QStringList modelPrefixes = {"gpt-5", "o1", "o2", "o3", "o4"}; - - for (const QJsonValue &value : modelArray) { - const QJsonObject modelObject = value.toObject(); - if (!modelObject.contains("id")) { - continue; - } - - const QString modelId = modelObject["id"].toString(); - for (const QString &prefix : modelPrefixes) { - if (modelId.contains(prefix)) { - models.append(modelId); - break; - } + m_client->setUrl(baseUrl); + m_client->setApiKey(apiKey()); + return m_client->listModels().then([](const QList &models) { + QList filtered; + static const QStringList modelPrefixes = {"gpt-5", "o1", "o2", "o3", "o4"}; + for (const QString &modelId : models) { + for (const QString &prefix : modelPrefixes) { + if (modelId.contains(prefix)) { + filtered.append(modelId); + break; } } } - return models; - }).onFailed([](const std::exception &e) { - LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(e.what())); - return QList{}; + return filtered; }); } -QList OpenAIResponsesProvider::validateRequest( - const QJsonObject &request, LLMCore::TemplateType type) +PluginLLMCore::ProviderID OpenAIResponsesProvider::providerID() const { - Q_UNUSED(type); - - QList errors; - - if (!request.contains("input")) { - errors.append("Missing required field: input"); - return errors; - } - - const QJsonValue inputValue = request["input"]; - if (!inputValue.isString() && !inputValue.isArray()) { - errors.append("Field 'input' must be either a string or an array"); - } - - if (request.contains("max_output_tokens") && !request["max_output_tokens"].isDouble()) { - errors.append("Field 'max_output_tokens' must be a number"); - } - - if (request.contains("top_p") && !request["top_p"].isDouble()) { - errors.append("Field 'top_p' must be a number"); - } - - if (request.contains("reasoning") && !request["reasoning"].isObject()) { - errors.append("Field 'reasoning' must be an object"); - } - - if (request.contains("stream") && !request["stream"].isBool()) { - errors.append("Field 'stream' must be a boolean"); - } - - if (request.contains("tools") && !request["tools"].isArray()) { - errors.append("Field 'tools' must be an array"); - } - - return errors; + return PluginLLMCore::ProviderID::OpenAIResponses; } -QString OpenAIResponsesProvider::apiKey() const +PluginLLMCore::ProviderCapabilities OpenAIResponsesProvider::capabilities() const { - return Settings::providerSettings().openAiApiKey(); + return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking + | PluginLLMCore::ProviderCapability::Image + | PluginLLMCore::ProviderCapability::ModelListing; } -void OpenAIResponsesProvider::prepareNetworkRequest(QNetworkRequest &networkRequest) const +::LLMCore::BaseClient *OpenAIResponsesProvider::client() const { - networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - - if (!apiKey().isEmpty()) { - networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8()); - } -} - -LLMCore::ProviderID OpenAIResponsesProvider::providerID() const -{ - return LLMCore::ProviderID::OpenAIResponses; -} - -void OpenAIResponsesProvider::sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) -{ - if (!m_messages.contains(requestId)) { - m_dataBuffers[requestId].clear(); - } - - m_requestUrls[requestId] = url; - m_originalRequests[requestId] = payload; - - QNetworkRequest networkRequest(url); - prepareNetworkRequest(networkRequest); - - httpClient()->postStreaming(requestId, networkRequest, payload); -} - -bool OpenAIResponsesProvider::supportsTools() const -{ - return true; -} - -bool OpenAIResponsesProvider::supportImage() const -{ - return true; -} - -bool OpenAIResponsesProvider::supportThinking() const -{ - return true; -} - -void OpenAIResponsesProvider::cancelRequest(const LLMCore::RequestID &requestId) -{ - LLMCore::Provider::cancelRequest(requestId); - cleanupRequest(requestId); -} - -void OpenAIResponsesProvider::onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) -{ - LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - const QStringList lines = buffers.rawStreamBuffer.processData(data); - - QString currentEventType; - - for (const QString &line : lines) { - const QString trimmedLine = line.trimmed(); - if (trimmedLine.isEmpty()) { - continue; - } - - if (line == "data: [DONE]") { - continue; - } - - if (line.startsWith("event: ")) { - currentEventType = line.mid(7).trimmed(); - continue; - } - - QString dataLine = line; - if (line.startsWith("data: ")) { - dataLine = line.mid(6); - } - - const QJsonDocument doc = QJsonDocument::fromJson(dataLine.toUtf8()); - if (doc.isObject()) { - const QJsonObject obj = doc.object(); - processStreamEvent(requestId, currentEventType, obj); - } - } -} - -void OpenAIResponsesProvider::onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, std::optional error) -{ - if (error) { - LOG_MESSAGE(QString("OpenAIResponses request %1 failed: %2").arg(requestId, *error)); - emit requestFailed(requestId, *error); - cleanupRequest(requestId); - return; - } - - if (m_messages.contains(requestId)) { - OpenAIResponsesMessage *message = m_messages[requestId]; - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - return; - } - } - - if (m_dataBuffers.contains(requestId)) { - const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; - if (!buffers.responseContent.isEmpty()) { - emit fullResponseReceived(requestId, buffers.responseContent); - } else { - LOG_MESSAGE(QString("WARNING: OpenAIResponses - Response content is empty for %1, " - "emitting empty response") - .arg(requestId)); - emit fullResponseReceived(requestId, ""); - } - } else { - LOG_MESSAGE( - QString("WARNING: OpenAIResponses - No data buffer found for %1").arg(requestId)); - } - - cleanupRequest(requestId); -} - -void OpenAIResponsesProvider::processStreamEvent( - const QString &requestId, const QString &eventType, const QJsonObject &data) -{ - OpenAIResponsesMessage *message = m_messages.value(requestId); - if (!message) { - message = new OpenAIResponsesMessage(this); - m_messages[requestId] = message; - - if (m_dataBuffers.contains(requestId)) { - emit continuationStarted(requestId); - } - } else if ( - m_dataBuffers.contains(requestId) - && message->state() == LLMCore::MessageState::RequiresToolExecution) { - message->startNewContinuation(); - emit continuationStarted(requestId); - } - - if (eventType == "response.content_part.added") { - } else if (eventType == "response.output_text.delta") { - const QString delta = data["delta"].toString(); - if (!delta.isEmpty()) { - m_dataBuffers[requestId].responseContent += delta; - emit partialResponseReceived(requestId, delta); - } - } else if (eventType == "response.output_text.done") { - const QString fullText = data["text"].toString(); - if (!fullText.isEmpty()) { - m_dataBuffers[requestId].responseContent = fullText; - } - } else if (eventType == "response.content_part.done") { - } else if (eventType == "response.output_item.added") { - using namespace QodeAssist::OpenAIResponses; - const QJsonObject item = data["item"].toObject(); - OutputItem outputItem = OutputItem::fromJson(item); - - if (const auto *functionCall = outputItem.asFunctionCall()) { - if (!functionCall->callId.isEmpty() && !functionCall->name.isEmpty()) { - if (!m_itemIdToCallId.contains(requestId)) { - m_itemIdToCallId[requestId] = QHash(); - } - m_itemIdToCallId[requestId][functionCall->id] = functionCall->callId; - message->handleToolCallStart(functionCall->callId, functionCall->name); - } - } else if (const auto *reasoning = outputItem.asReasoning()) { - if (!reasoning->id.isEmpty()) { - message->handleReasoningStart(reasoning->id); - } - } - } else if (eventType == "response.reasoning_content.delta") { - const QString itemId = data["item_id"].toString(); - const QString delta = data["delta"].toString(); - if (!itemId.isEmpty() && !delta.isEmpty()) { - message->handleReasoningDelta(itemId, delta); - } - } else if (eventType == "response.reasoning_content.done") { - const QString itemId = data["item_id"].toString(); - if (!itemId.isEmpty()) { - message->handleReasoningComplete(itemId); - emitPendingThinkingBlocks(requestId); - } - } else if (eventType == "response.function_call_arguments.delta") { - const QString itemId = data["item_id"].toString(); - const QString delta = data["delta"].toString(); - if (!itemId.isEmpty() && !delta.isEmpty()) { - const QString callId = m_itemIdToCallId.value(requestId).value(itemId); - if (!callId.isEmpty()) { - message->handleToolCallDelta(callId, delta); - } else { - LOG_MESSAGE(QString("ERROR: No call_id mapping found for item_id: %1").arg(itemId)); - } - } - } else if ( - eventType == "response.function_call_arguments.done" - || eventType == "response.output_item.done") { - const QString itemId = data["item_id"].toString(); - const QJsonObject item = data["item"].toObject(); - - if (!item.isEmpty() && item["type"].toString() == "reasoning") { - using namespace QodeAssist::OpenAIResponses; - - const QString finalItemId = itemId.isEmpty() ? item["id"].toString() : itemId; - - ReasoningOutput reasoningOutput = ReasoningOutput::fromJson(item); - QString reasoningText; - - if (!reasoningOutput.summaryText.isEmpty()) { - reasoningText = reasoningOutput.summaryText; - } else if (!reasoningOutput.contentTexts.isEmpty()) { - reasoningText = reasoningOutput.contentTexts.join("\n"); - } - - if (reasoningText.isEmpty()) { - reasoningText = QString( - "[Reasoning process completed, but detailed thinking is not available in " - "streaming mode. The model has processed your request with extended reasoning.]"); - } - - if (!finalItemId.isEmpty()) { - message->handleReasoningDelta(finalItemId, reasoningText); - message->handleReasoningComplete(finalItemId); - emitPendingThinkingBlocks(requestId); - } - } else if (item.isEmpty() && !itemId.isEmpty()) { - const QString callId = m_itemIdToCallId.value(requestId).value(itemId); - if (!callId.isEmpty()) { - message->handleToolCallComplete(callId); - } else { - LOG_MESSAGE( - QString("ERROR: OpenAIResponses - No call_id mapping found for item_id: %1") - .arg(itemId)); - } - } else if (!item.isEmpty() && item["type"].toString() == "function_call") { - const QString callId = item["call_id"].toString(); - if (!callId.isEmpty()) { - message->handleToolCallComplete(callId); - } else { - LOG_MESSAGE( - QString("ERROR: OpenAIResponses - Function call done but call_id is empty")); - } - } - } else if (eventType == "response.created") { - } else if (eventType == "response.in_progress") { - } else if (eventType == "response.completed") { - using namespace QodeAssist::OpenAIResponses; - const QJsonObject responseObj = data["response"].toObject(); - Response response = Response::fromJson(responseObj); - - const QString statusStr = responseObj["status"].toString(); - - if (m_dataBuffers[requestId].responseContent.isEmpty()) { - const QString aggregatedText = response.getAggregatedText(); - if (!aggregatedText.isEmpty()) { - m_dataBuffers[requestId].responseContent = aggregatedText; - } - } - - message->handleStatus(statusStr); - handleMessageComplete(requestId); - } else if (eventType == "response.incomplete") { - using namespace QodeAssist::OpenAIResponses; - const QJsonObject responseObj = data["response"].toObject(); - - if (!responseObj.isEmpty()) { - Response response = Response::fromJson(responseObj); - const QString statusStr = responseObj["status"].toString(); - - if (m_dataBuffers[requestId].responseContent.isEmpty()) { - const QString aggregatedText = response.getAggregatedText(); - if (!aggregatedText.isEmpty()) { - m_dataBuffers[requestId].responseContent = aggregatedText; - } - } - - message->handleStatus(statusStr); - } else { - message->handleStatus("incomplete"); - } - - handleMessageComplete(requestId); - } else if (!eventType.isEmpty()) { - LOG_MESSAGE(QString("WARNING: OpenAIResponses - Unhandled event type '%1' for request %2\nData: %3") - .arg(eventType) - .arg(requestId) - .arg(QString::fromUtf8(QJsonDocument(data).toJson(QJsonDocument::Compact)))); - } -} - -void OpenAIResponsesProvider::emitPendingThinkingBlocks(const QString &requestId) -{ - if (!m_messages.contains(requestId)) { - return; - } - - OpenAIResponsesMessage *message = m_messages[requestId]; - const auto thinkingBlocks = message->getCurrentThinkingContent(); - - if (thinkingBlocks.isEmpty()) { - return; - } - - const int alreadyEmitted = m_emittedThinkingBlocksCount.value(requestId, 0); - const int totalBlocks = thinkingBlocks.size(); - - for (int i = alreadyEmitted; i < totalBlocks; ++i) { - const auto *thinkingContent = thinkingBlocks[i]; - - if (thinkingContent->thinking().trimmed().isEmpty()) { - continue; - } - - emit thinkingBlockReceived( - requestId, thinkingContent->thinking(), thinkingContent->signature()); - } - - m_emittedThinkingBlocksCount[requestId] = totalBlocks; -} - -void OpenAIResponsesProvider::handleMessageComplete(const QString &requestId) -{ - if (!m_messages.contains(requestId)) { - return; - } - - OpenAIResponsesMessage *message = m_messages[requestId]; - - emitPendingThinkingBlocks(requestId); - - if (message->state() == LLMCore::MessageState::RequiresToolExecution) { - const auto toolUseContent = message->getCurrentToolUseContent(); - - if (toolUseContent.isEmpty()) { - return; - } - - for (const auto *toolContent : toolUseContent) { - const auto toolStringName = m_toolsManager->toolsFactory()->getStringName( - toolContent->name()); - emit toolExecutionStarted(requestId, toolContent->id(), toolStringName); - m_toolsManager->executeToolCall( - requestId, toolContent->id(), toolContent->name(), toolContent->input()); - } - } -} - -void OpenAIResponsesProvider::onToolExecutionComplete( - const QString &requestId, const QHash &toolResults) -{ - if (!m_messages.contains(requestId) || !m_requestUrls.contains(requestId)) { - LOG_MESSAGE(QString("ERROR: OpenAIResponses - Missing data for continuation request %1") - .arg(requestId)); - cleanupRequest(requestId); - return; - } - - OpenAIResponsesMessage *message = m_messages[requestId]; - const auto toolContent = message->getCurrentToolUseContent(); - - for (auto it = toolResults.constBegin(); it != toolResults.constEnd(); ++it) { - for (const auto *tool : toolContent) { - if (tool->id() == it.key()) { - const auto toolStringName = m_toolsManager->toolsFactory()->getStringName( - tool->name()); - emit toolExecutionCompleted( - requestId, tool->id(), toolStringName, toolResults[tool->id()]); - break; - } - } - } - - QJsonObject continuationRequest = m_originalRequests[requestId]; - QJsonArray input = continuationRequest["input"].toArray(); - - const QList assistantItems = message->toItemsFormat(); - for (const QJsonObject &item : assistantItems) { - input.append(item); - } - - const QJsonArray toolResultItems = message->createToolResultItems(toolResults); - for (const QJsonValue &item : toolResultItems) { - input.append(item); - } - - continuationRequest["input"] = input; - - m_dataBuffers[requestId].responseContent.clear(); - - sendRequest(requestId, m_requestUrls[requestId], continuationRequest); -} - -void OpenAIResponsesProvider::cleanupRequest(const LLMCore::RequestID &requestId) -{ - if (m_messages.contains(requestId)) { - OpenAIResponsesMessage *message = m_messages.take(requestId); - message->deleteLater(); - } - - m_dataBuffers.remove(requestId); - m_requestUrls.remove(requestId); - m_originalRequests.remove(requestId); - m_itemIdToCallId.remove(requestId); - m_emittedThinkingBlocksCount.remove(requestId); - m_toolsManager->cleanupRequest(requestId); + return m_client; } } // namespace QodeAssist::Providers diff --git a/providers/OpenAIResponsesProvider.hpp b/providers/OpenAIResponsesProvider.hpp index 420985f..fd6a324 100644 --- a/providers/OpenAIResponsesProvider.hpp +++ b/providers/OpenAIResponsesProvider.hpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2024-2025 Petr Mironychev * * This file is part of QodeAssist. @@ -19,13 +19,12 @@ #pragma once -#include "OpenAIResponsesMessage.hpp" -#include "tools/ToolsManager.hpp" -#include +#include +#include namespace QodeAssist::Providers { -class OpenAIResponsesProvider : public LLMCore::Provider +class OpenAIResponsesProvider : public PluginLLMCore::Provider { Q_OBJECT public: @@ -35,52 +34,22 @@ public: QString url() const override; QString completionEndpoint() const override; QString chatEndpoint() const override; - bool supportsModelListing() const override; void prepareRequest( QJsonObject &request, - LLMCore::PromptTemplate *prompt, - LLMCore::ContextData context, - LLMCore::RequestType type, + PluginLLMCore::PromptTemplate *prompt, + PluginLLMCore::ContextData context, + PluginLLMCore::RequestType type, bool isToolsEnabled, bool isThinkingEnabled) override; QFuture> getInstalledModels(const QString &url) override; - QList validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; + PluginLLMCore::ProviderID providerID() const override; + PluginLLMCore::ProviderCapabilities capabilities() const override; + + ::LLMCore::BaseClient *client() const override; QString apiKey() const override; - void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; - LLMCore::ProviderID providerID() const override; - - void sendRequest( - const LLMCore::RequestID &requestId, const QUrl &url, const QJsonObject &payload) override; - - bool supportsTools() const override; - bool supportImage() const override; - bool supportThinking() const override; - void cancelRequest(const LLMCore::RequestID &requestId) override; - -public slots: - void onDataReceived( - const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; - void onRequestFinished( - const QodeAssist::LLMCore::RequestID &requestId, - std::optional error) override; - -private slots: - void onToolExecutionComplete( - const QString &requestId, const QHash &toolResults); private: - void processStreamEvent(const QString &requestId, const QString &eventType, const QJsonObject &data); - void emitPendingThinkingBlocks(const QString &requestId); - void handleMessageComplete(const QString &requestId); - void cleanupRequest(const LLMCore::RequestID &requestId); - - QHash m_messages; - QHash m_requestUrls; - QHash m_originalRequests; - QHash> m_itemIdToCallId; - QHash m_emittedThinkingBlocksCount; - Tools::ToolsManager *m_toolsManager; + ::LLMCore::OpenAIResponsesClient *m_client; }; } // namespace QodeAssist::Providers - diff --git a/providers/OpenRouterAIProvider.cpp b/providers/OpenRouterAIProvider.cpp index e64afec..56484ec 100644 --- a/providers/OpenRouterAIProvider.cpp +++ b/providers/OpenRouterAIProvider.cpp @@ -28,24 +28,28 @@ namespace QodeAssist::Providers { +OpenRouterProvider::OpenRouterProvider(QObject *parent) + : OpenAICompatProvider(parent) +{} + QString OpenRouterProvider::name() const { return "OpenRouter"; } -QString OpenRouterProvider::url() const -{ - return "https://openrouter.ai/api"; -} - QString OpenRouterProvider::apiKey() const { return Settings::providerSettings().openRouterApiKey(); } -LLMCore::ProviderID OpenRouterProvider::providerID() const +QString OpenRouterProvider::url() const { - return LLMCore::ProviderID::OpenRouter; + return "https://openrouter.ai/api"; +} + +PluginLLMCore::ProviderID OpenRouterProvider::providerID() const +{ + return PluginLLMCore::ProviderID::OpenRouter; } } // namespace QodeAssist::Providers diff --git a/providers/OpenRouterAIProvider.hpp b/providers/OpenRouterAIProvider.hpp index 012aca8..035b9be 100644 --- a/providers/OpenRouterAIProvider.hpp +++ b/providers/OpenRouterAIProvider.hpp @@ -26,10 +26,12 @@ namespace QodeAssist::Providers { class OpenRouterProvider : public OpenAICompatProvider { public: + explicit OpenRouterProvider(QObject *parent = nullptr); + QString name() const override; QString url() const override; QString apiKey() const override; - LLMCore::ProviderID providerID() const override; + PluginLLMCore::ProviderID providerID() const override; }; } // namespace QodeAssist::Providers diff --git a/providers/Providers.hpp b/providers/Providers.hpp index 7876888..f77fe88 100644 --- a/providers/Providers.hpp +++ b/providers/Providers.hpp @@ -19,7 +19,7 @@ #pragma once -#include "llmcore/ProvidersManager.hpp" +#include "pluginllmcore/ProvidersManager.hpp" #include "providers/ClaudeProvider.hpp" #include "providers/CodestralProvider.hpp" #include "providers/GoogleAIProvider.hpp" @@ -36,7 +36,7 @@ namespace QodeAssist::Providers { inline void registerProviders() { - auto &providerManager = LLMCore::ProvidersManager::instance(); + auto &providerManager = PluginLLMCore::ProvidersManager::instance(); providerManager.registerProvider(); providerManager.registerProvider(); providerManager.registerProvider(); diff --git a/qodeassist.cpp b/qodeassist.cpp index a205aab..75d3f07 100644 --- a/qodeassist.cpp +++ b/qodeassist.cpp @@ -50,8 +50,8 @@ #include "chat/ChatOutputPane.h" #include "chat/NavigationPanel.hpp" #include "context/DocumentReaderQtCreator.hpp" -#include "llmcore/PromptProviderFim.hpp" -#include "llmcore/ProvidersManager.hpp" +#include "pluginllmcore/PromptProviderFim.hpp" +#include "pluginllmcore/ProvidersManager.hpp" #include "logger/RequestPerformanceLogger.hpp" #include "providers/Providers.hpp" #include "settings/ChatAssistantSettings.hpp" @@ -84,7 +84,7 @@ class QodeAssistPlugin final : public ExtensionSystem::IPlugin public: QodeAssistPlugin() : m_updater(new PluginUpdater(this)) - , m_promptProvider(LLMCore::PromptTemplateManager::instance()) + , m_promptProvider(PluginLLMCore::PromptTemplateManager::instance()) {} ~QodeAssistPlugin() final @@ -263,7 +263,7 @@ public: m_qodeAssistClient = new QodeAssistClient(new LLMClientInterface( Settings::generalSettings(), Settings::codeCompletionSettings(), - LLMCore::ProvidersManager::instance(), + PluginLLMCore::ProvidersManager::instance(), &m_promptProvider, m_documentReader, m_performanceLogger)); @@ -305,7 +305,7 @@ private: } QPointer m_qodeAssistClient; - LLMCore::PromptProviderFim m_promptProvider; + PluginLLMCore::PromptProviderFim m_promptProvider; Context::DocumentReaderQtCreator m_documentReader; RequestPerformanceLogger m_performanceLogger; QPointer m_chatOutputPane; diff --git a/settings/GeneralSettings.hpp b/settings/GeneralSettings.hpp index 14b036b..d7bdc08 100644 --- a/settings/GeneralSettings.hpp +++ b/settings/GeneralSettings.hpp @@ -29,7 +29,7 @@ namespace Utils { class DetailsWidget; } -namespace QodeAssist::LLMCore { +namespace QodeAssist::PluginLLMCore { class Provider; } namespace QodeAssist::Settings { diff --git a/sources/external/llmcore b/sources/external/llmcore new file mode 160000 index 0000000..d1fb0dc --- /dev/null +++ b/sources/external/llmcore @@ -0,0 +1 @@ +Subproject commit d1fb0dca95d0c6960b2499265dec34f8a78e34f5 diff --git a/templates/Alpaca.hpp b/templates/Alpaca.hpp index fb2c54f..cfccc4d 100644 --- a/templates/Alpaca.hpp +++ b/templates/Alpaca.hpp @@ -19,21 +19,21 @@ #pragma once -#include "llmcore/PromptTemplate.hpp" +#include "pluginllmcore/PromptTemplate.hpp" #include namespace QodeAssist::Templates { -class Alpaca : public LLMCore::PromptTemplate +class Alpaca : public PluginLLMCore::PromptTemplate { public: QString name() const override { return "Alpaca"; } - LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; } + PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; } QStringList stopWords() const override { return QStringList() << "### Instruction:" << "### Response:"; } - void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override + void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override { QJsonArray messages; @@ -72,14 +72,14 @@ public: "}\n\n" "Combines all messages into a single formatted prompt."; } - bool isSupportProvider(LLMCore::ProviderID id) const override + bool isSupportProvider(PluginLLMCore::ProviderID id) const override { switch (id) { - case LLMCore::ProviderID::Ollama: - case LLMCore::ProviderID::LMStudio: - case LLMCore::ProviderID::OpenRouter: - case LLMCore::ProviderID::OpenAICompatible: - case LLMCore::ProviderID::LlamaCpp: + case PluginLLMCore::ProviderID::Ollama: + case PluginLLMCore::ProviderID::LMStudio: + case PluginLLMCore::ProviderID::OpenRouter: + case PluginLLMCore::ProviderID::OpenAICompatible: + case PluginLLMCore::ProviderID::LlamaCpp: return true; default: return false; diff --git a/templates/ChatML.hpp b/templates/ChatML.hpp index 7b1bf16..62be0e8 100644 --- a/templates/ChatML.hpp +++ b/templates/ChatML.hpp @@ -21,20 +21,20 @@ #include -#include "llmcore/PromptTemplate.hpp" +#include "pluginllmcore/PromptTemplate.hpp" namespace QodeAssist::Templates { -class ChatML : public LLMCore::PromptTemplate +class ChatML : public PluginLLMCore::PromptTemplate { public: QString name() const override { return "ChatML"; } - LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; } + PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; } QStringList stopWords() const override { return QStringList() << "<|im_start|>" << "<|im_end|>"; } - void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override + void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override { QJsonArray messages; @@ -73,14 +73,14 @@ public: "}\n\n" "Compatible with multiple providers supporting the ChatML token format."; } - bool isSupportProvider(LLMCore::ProviderID id) const override + bool isSupportProvider(PluginLLMCore::ProviderID id) const override { switch (id) { - case LLMCore::ProviderID::Ollama: - case LLMCore::ProviderID::LMStudio: - case LLMCore::ProviderID::OpenRouter: - case LLMCore::ProviderID::OpenAICompatible: - case LLMCore::ProviderID::LlamaCpp: + case PluginLLMCore::ProviderID::Ollama: + case PluginLLMCore::ProviderID::LMStudio: + case PluginLLMCore::ProviderID::OpenRouter: + case PluginLLMCore::ProviderID::OpenAICompatible: + case PluginLLMCore::ProviderID::LlamaCpp: return true; default: return false; diff --git a/templates/Claude.hpp b/templates/Claude.hpp index 53475ee..b54301e 100644 --- a/templates/Claude.hpp +++ b/templates/Claude.hpp @@ -21,17 +21,17 @@ #include -#include "llmcore/PromptTemplate.hpp" +#include "pluginllmcore/PromptTemplate.hpp" namespace QodeAssist::Templates { -class Claude : public LLMCore::PromptTemplate +class Claude : public PluginLLMCore::PromptTemplate { public: - LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; } + PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; } QString name() const override { return "Claude"; } QStringList stopWords() const override { return QStringList(); } - void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override + void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override { QJsonArray messages; @@ -111,10 +111,10 @@ public: "}\n\n" "Formats content according to Claude API specifications."; } - bool isSupportProvider(LLMCore::ProviderID id) const override + bool isSupportProvider(PluginLLMCore::ProviderID id) const override { switch (id) { - case QodeAssist::LLMCore::ProviderID::Claude: + case QodeAssist::PluginLLMCore::ProviderID::Claude: return true; default: return false; diff --git a/templates/CodeLlamaFim.hpp b/templates/CodeLlamaFim.hpp index 90e4299..9b8da89 100644 --- a/templates/CodeLlamaFim.hpp +++ b/templates/CodeLlamaFim.hpp @@ -19,20 +19,20 @@ #pragma once -#include "llmcore/PromptTemplate.hpp" +#include "pluginllmcore/PromptTemplate.hpp" namespace QodeAssist::Templates { -class CodeLlamaFim : public LLMCore::PromptTemplate +class CodeLlamaFim : public PluginLLMCore::PromptTemplate { public: - LLMCore::TemplateType type() const override { return LLMCore::TemplateType::FIM; } + PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; } QString name() const override { return "CodeLlama FIM"; } QStringList stopWords() const override { return QStringList() << "" << "
" << "";
     }
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         request["prompt"] = QString("
 %1 %2 ")
                                 .arg(context.prefix.value_or(""), context.suffix.value_or(""));
@@ -47,10 +47,10 @@ public:
                "}\n\n"
                "Optimized for code completion with CodeLlama models.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case QodeAssist::LLMCore::ProviderID::Ollama:
+        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
             return true;
         default:
             return false;
diff --git a/templates/CodeLlamaQMLFim.hpp b/templates/CodeLlamaQMLFim.hpp
index e5a0f51..6af51bf 100644
--- a/templates/CodeLlamaQMLFim.hpp
+++ b/templates/CodeLlamaQMLFim.hpp
@@ -19,21 +19,21 @@
 
 #pragma once
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 
 namespace QodeAssist::Templates {
 
-class CodeLlamaQMLFim : public LLMCore::PromptTemplate
+class CodeLlamaQMLFim : public PluginLLMCore::PromptTemplate
 {
 public:
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::FIM; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
     QString name() const override { return "CodeLlama QML FIM"; }
     QStringList stopWords() const override
     {
         return QStringList() << "" << "
" << "
" << "
" << "< EOT >" << "\\end" << "" << "" << "##"; } - void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override + void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override { request["prompt"] = QString("%1
%2")
                                 .arg(context.suffix.value_or(""), context.prefix.value_or(""));
@@ -48,10 +48,10 @@ public:
                "}\n\n"
                "Specifically optimized for QML/JavaScript code completion.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case QodeAssist::LLMCore::ProviderID::Ollama:
+        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
             return true;
         default:
             return false;
diff --git a/templates/GoogleAI.hpp b/templates/GoogleAI.hpp
index 4a61ed0..bbf0e37 100644
--- a/templates/GoogleAI.hpp
+++ b/templates/GoogleAI.hpp
@@ -22,18 +22,18 @@
 #include 
 #include 
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 
 namespace QodeAssist::Templates {
 
-class GoogleAI : public LLMCore::PromptTemplate
+class GoogleAI : public PluginLLMCore::PromptTemplate
 {
 public:
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
     QString name() const override { return "Google AI"; }
     QStringList stopWords() const override { return QStringList(); }
 
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         QJsonArray contents;
 
@@ -128,9 +128,9 @@ public:
                "Supports proper role mapping (model/user roles), images, and thinking blocks.";
     }
 
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
-        return id == QodeAssist::LLMCore::ProviderID::GoogleAI;
+        return id == QodeAssist::PluginLLMCore::ProviderID::GoogleAI;
     }
 };
 
diff --git a/templates/Llama2.hpp b/templates/Llama2.hpp
index 76261d9..038d1a4 100644
--- a/templates/Llama2.hpp
+++ b/templates/Llama2.hpp
@@ -19,18 +19,18 @@
 
 #pragma once
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 #include 
 
 namespace QodeAssist::Templates {
 
-class Llama2 : public LLMCore::PromptTemplate
+class Llama2 : public PluginLLMCore::PromptTemplate
 {
 public:
     QString name() const override { return "Llama 2"; }
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
     QStringList stopWords() const override { return QStringList() << "[INST]"; }
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         QJsonArray messages;
 
@@ -70,14 +70,14 @@ public:
                "}\n\n"
                "Compatible with Ollama, LM Studio, and other services for Llama 2.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case LLMCore::ProviderID::Ollama:
-        case LLMCore::ProviderID::LMStudio:
-        case LLMCore::ProviderID::OpenRouter:
-        case LLMCore::ProviderID::OpenAICompatible:
-        case LLMCore::ProviderID::LlamaCpp:
+        case PluginLLMCore::ProviderID::Ollama:
+        case PluginLLMCore::ProviderID::LMStudio:
+        case PluginLLMCore::ProviderID::OpenRouter:
+        case PluginLLMCore::ProviderID::OpenAICompatible:
+        case PluginLLMCore::ProviderID::LlamaCpp:
             return true;
         default:
             return false;
diff --git a/templates/Llama3.hpp b/templates/Llama3.hpp
index fc151d0..5dd6cf0 100644
--- a/templates/Llama3.hpp
+++ b/templates/Llama3.hpp
@@ -21,20 +21,20 @@
 
 #include 
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 
 namespace QodeAssist::Templates {
 
-class Llama3 : public LLMCore::PromptTemplate
+class Llama3 : public PluginLLMCore::PromptTemplate
 {
 public:
     QString name() const override { return "Llama 3"; }
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
     QStringList stopWords() const override
     {
         return QStringList() << "<|start_header_id|>" << "<|end_header_id|>" << "<|eot_id|>";
     }
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         QJsonArray messages;
 
@@ -77,14 +77,14 @@ public:
                "}\n\n"
                "Compatible with Ollama, LM Studio, and OpenAI-compatible services for Llama 3.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case LLMCore::ProviderID::Ollama:
-        case LLMCore::ProviderID::LMStudio:
-        case LLMCore::ProviderID::OpenRouter:
-        case LLMCore::ProviderID::OpenAICompatible:
-        case LLMCore::ProviderID::LlamaCpp:
+        case PluginLLMCore::ProviderID::Ollama:
+        case PluginLLMCore::ProviderID::LMStudio:
+        case PluginLLMCore::ProviderID::OpenRouter:
+        case PluginLLMCore::ProviderID::OpenAICompatible:
+        case PluginLLMCore::ProviderID::LlamaCpp:
             return true;
         default:
             return false;
diff --git a/templates/LlamaCppFim.hpp b/templates/LlamaCppFim.hpp
index bf64234..d9fa286 100644
--- a/templates/LlamaCppFim.hpp
+++ b/templates/LlamaCppFim.hpp
@@ -21,18 +21,18 @@
 
 #include 
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 
 namespace QodeAssist::Templates {
 
-class LlamaCppFim : public LLMCore::PromptTemplate
+class LlamaCppFim : public PluginLLMCore::PromptTemplate
 {
 public:
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::FIM; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
     QString name() const override { return "llama.cpp FIM"; }
     QStringList stopWords() const override { return {}; }
 
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         request["input_prefix"] = context.prefix.value_or("");
         request["input_suffix"] = context.suffix.value_or("");
@@ -60,9 +60,9 @@ public:
                "Recommended for models with FIM capability.";
     }
 
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
-        return id == QodeAssist::LLMCore::ProviderID::LlamaCpp;
+        return id == QodeAssist::PluginLLMCore::ProviderID::LlamaCpp;
     }
 };
 
diff --git a/templates/MistralAI.hpp b/templates/MistralAI.hpp
index b9315ce..fdf8f91 100644
--- a/templates/MistralAI.hpp
+++ b/templates/MistralAI.hpp
@@ -21,17 +21,17 @@
 
 #include 
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 
 namespace QodeAssist::Templates {
 
-class MistralAIFim : public LLMCore::PromptTemplate
+class MistralAIFim : public PluginLLMCore::PromptTemplate
 {
 public:
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::FIM; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
     QString name() const override { return "Mistral AI FIM"; }
     QStringList stopWords() const override { return QStringList(); }
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         request["prompt"] = context.prefix.value_or("");
         request["suffix"] = context.suffix.value_or("");
@@ -45,10 +45,10 @@ public:
                "}\n\n"
                "Optimized for code completion with MistralAI models.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case QodeAssist::LLMCore::ProviderID::MistralAI:
+        case QodeAssist::PluginLLMCore::ProviderID::MistralAI:
             return true;
         default:
             return false;
@@ -56,14 +56,14 @@ public:
     }
 };
 
-class MistralAIChat : public LLMCore::PromptTemplate
+class MistralAIChat : public PluginLLMCore::PromptTemplate
 {
 public:
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
     QString name() const override { return "Mistral AI Chat"; }
     QStringList stopWords() const override { return QStringList(); }
 
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         QJsonArray messages;
 
@@ -116,10 +116,10 @@ public:
                "}\n\n"
                "Supports system messages, conversation history, and images.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case QodeAssist::LLMCore::ProviderID::MistralAI:
+        case QodeAssist::PluginLLMCore::ProviderID::MistralAI:
             return true;
         default:
             return false;
diff --git a/templates/Ollama.hpp b/templates/Ollama.hpp
index 2890f2d..de0f799 100644
--- a/templates/Ollama.hpp
+++ b/templates/Ollama.hpp
@@ -21,17 +21,17 @@
 
 #include 
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 
 namespace QodeAssist::Templates {
 
-class OllamaFim : public LLMCore::PromptTemplate
+class OllamaFim : public PluginLLMCore::PromptTemplate
 {
 public:
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::FIM; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
     QString name() const override { return "Ollama FIM"; }
     QStringList stopWords() const override { return QStringList() << ""; }
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         request["prompt"] = context.prefix.value_or("");
         request["suffix"] = context.suffix.value_or("");
@@ -47,10 +47,10 @@ public:
                "}\n\n"
                "Recommended for Ollama models with FIM capability.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case QodeAssist::LLMCore::ProviderID::Ollama:
+        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
             return true;
         default:
             return false;
@@ -58,14 +58,14 @@ public:
     }
 };
 
-class OllamaChat : public LLMCore::PromptTemplate
+class OllamaChat : public PluginLLMCore::PromptTemplate
 {
 public:
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
     QString name() const override { return "Ollama Chat"; }
     QStringList stopWords() const override { return QStringList(); }
 
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         QJsonArray messages;
 
@@ -107,10 +107,10 @@ public:
                "Recommended for Ollama models with chat capability.\n"
                "Supports images for multimodal models (e.g., llava).";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case QodeAssist::LLMCore::ProviderID::Ollama:
+        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
             return true;
         default:
             return false;
diff --git a/templates/OpenAI.hpp b/templates/OpenAI.hpp
index 0b9f7aa..72a80c0 100644
--- a/templates/OpenAI.hpp
+++ b/templates/OpenAI.hpp
@@ -21,17 +21,17 @@
 
 #include 
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 
 namespace QodeAssist::Templates {
 
-class OpenAI : public LLMCore::PromptTemplate
+class OpenAI : public PluginLLMCore::PromptTemplate
 {
 public:
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
     QString name() const override { return "OpenAI"; }
     QStringList stopWords() const override { return QStringList(); }
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         QJsonArray messages;
 
@@ -84,10 +84,10 @@ public:
                "}\n\n"
                "Standard Chat API format for OpenAI.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case QodeAssist::LLMCore::ProviderID::OpenAI:
+        case QodeAssist::PluginLLMCore::ProviderID::OpenAI:
             return true;
         default:
             return false;
diff --git a/templates/OpenAICompatible.hpp b/templates/OpenAICompatible.hpp
index 6484aa1..9f9cc24 100644
--- a/templates/OpenAICompatible.hpp
+++ b/templates/OpenAICompatible.hpp
@@ -21,17 +21,17 @@
 
 #include 
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 
 namespace QodeAssist::Templates {
 
-class OpenAICompatible : public LLMCore::PromptTemplate
+class OpenAICompatible : public PluginLLMCore::PromptTemplate
 {
 public:
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
     QString name() const override { return "OpenAI Compatible"; }
     QStringList stopWords() const override { return QStringList(); }
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         QJsonArray messages;
 
@@ -85,13 +85,13 @@ public:
                "Works with any service implementing the OpenAI Chat API specification.\n"
                "Supports images.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case LLMCore::ProviderID::OpenAICompatible:
-        case LLMCore::ProviderID::OpenRouter:
-        case LLMCore::ProviderID::LMStudio:
-        case LLMCore::ProviderID::LlamaCpp:
+        case PluginLLMCore::ProviderID::OpenAICompatible:
+        case PluginLLMCore::ProviderID::OpenRouter:
+        case PluginLLMCore::ProviderID::LMStudio:
+        case PluginLLMCore::ProviderID::LlamaCpp:
             return true;
         default:
             return false;
diff --git a/templates/OpenAIResponses.hpp b/templates/OpenAIResponses.hpp
index 662ebf4..fab26da 100644
--- a/templates/OpenAIResponses.hpp
+++ b/templates/OpenAIResponses.hpp
@@ -19,24 +19,24 @@
 
 #pragma once
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 #include "providers/OpenAIResponsesRequestBuilder.hpp"
 
 namespace QodeAssist::Templates {
 
-class OpenAIResponses : public LLMCore::PromptTemplate
+class OpenAIResponses : public PluginLLMCore::PromptTemplate
 {
 public:
-    LLMCore::TemplateType type() const noexcept override 
+    PluginLLMCore::TemplateType type() const noexcept override 
     { 
-        return LLMCore::TemplateType::Chat; 
+        return PluginLLMCore::TemplateType::Chat; 
     }
     
     QString name() const override { return "OpenAI Responses"; }
     
     QStringList stopWords() const override { return {}; }
     
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         using namespace QodeAssist::OpenAIResponses;
         RequestBuilder builder;
@@ -108,9 +108,9 @@ public:
                "}\n\n"
                "Uses type-safe RequestBuilder for OpenAI Responses API.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const noexcept override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const noexcept override
     {
-        return id == QodeAssist::LLMCore::ProviderID::OpenAIResponses;
+        return id == QodeAssist::PluginLLMCore::ProviderID::OpenAIResponses;
     }
 
 private:
diff --git a/templates/Qwen25CoderFIM.hpp b/templates/Qwen25CoderFIM.hpp
index cad83da..bda401e 100644
--- a/templates/Qwen25CoderFIM.hpp
+++ b/templates/Qwen25CoderFIM.hpp
@@ -19,18 +19,18 @@
 
 #pragma once
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 #include 
 
 namespace QodeAssist::Templates {
 
-class Qwen25CoderFIM : public LLMCore::PromptTemplate
+class Qwen25CoderFIM : public PluginLLMCore::PromptTemplate
 {
 public:
     QString name() const override { return "Qwen2.5 Coder FIM"; }
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::FIM; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
     QStringList stopWords() const override { return QStringList() << "<|endoftext|>" << "<|EOT|>"; }
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         request["prompt"] = QString("<|fim_prefix|>%1<|fim_suffix|>%2<|fim_middle|>")
                                 .arg(context.prefix.value_or(""), context.suffix.value_or(""));
@@ -46,10 +46,10 @@ public:
                "}\n\n"
                "Ideal for code completion with Qwen models.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case QodeAssist::LLMCore::ProviderID::Ollama:
+        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
             return true;
         default:
             return false;
diff --git a/templates/Qwen3CoderFIM.hpp b/templates/Qwen3CoderFIM.hpp
index 94af77e..b4ab172 100644
--- a/templates/Qwen3CoderFIM.hpp
+++ b/templates/Qwen3CoderFIM.hpp
@@ -21,17 +21,17 @@
 
 #include 
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 
 namespace QodeAssist::Templates {
 
-class Qwen3CoderFIM : public LLMCore::PromptTemplate
+class Qwen3CoderFIM : public PluginLLMCore::PromptTemplate
 {
 public:
     QString name() const override { return "Qwen3 Coder FIM"; }
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::FIMOnChat; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIMOnChat; }
     QStringList stopWords() const override { return QStringList() << "<|im_end|>"; }
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         QJsonArray messages;
 
@@ -62,14 +62,14 @@ public:
                "  ]\n"
                "}\n\n";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case LLMCore::ProviderID::Ollama:
-        case LLMCore::ProviderID::LMStudio:
-        case LLMCore::ProviderID::OpenRouter:
-        case LLMCore::ProviderID::OpenAICompatible:
-        case LLMCore::ProviderID::LlamaCpp:
+        case PluginLLMCore::ProviderID::Ollama:
+        case PluginLLMCore::ProviderID::LMStudio:
+        case PluginLLMCore::ProviderID::OpenRouter:
+        case PluginLLMCore::ProviderID::OpenAICompatible:
+        case PluginLLMCore::ProviderID::LlamaCpp:
             return true;
         default:
             return false;
diff --git a/templates/StarCoder2Fim.hpp b/templates/StarCoder2Fim.hpp
index b204478..71e20f6 100644
--- a/templates/StarCoder2Fim.hpp
+++ b/templates/StarCoder2Fim.hpp
@@ -19,21 +19,21 @@
 
 #pragma once
 
-#include "llmcore/PromptTemplate.hpp"
+#include "pluginllmcore/PromptTemplate.hpp"
 
 namespace QodeAssist::Templates {
 
-class StarCoder2Fim : public LLMCore::PromptTemplate
+class StarCoder2Fim : public PluginLLMCore::PromptTemplate
 {
 public:
-    LLMCore::TemplateType type() const override { return LLMCore::TemplateType::FIM; }
+    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
     QString name() const override { return "StarCoder2 FIM"; }
     QStringList stopWords() const override
     {
         return QStringList() << "<|endoftext|>" << "" << "" << ""
                              << "";
     }
-    void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
+    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
     {
         request["prompt"] = QString("%1%2")
                                 .arg(context.prefix.value_or(""), context.suffix.value_or(""));
@@ -48,10 +48,10 @@ public:
                "}\n\n"
                "Includes stop words to prevent token duplication.";
     }
-    bool isSupportProvider(LLMCore::ProviderID id) const override
+    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
     {
         switch (id) {
-        case QodeAssist::LLMCore::ProviderID::Ollama:
+        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
             return true;
         default:
             return false;
diff --git a/templates/Templates.hpp b/templates/Templates.hpp
index f73cb41..940f653 100644
--- a/templates/Templates.hpp
+++ b/templates/Templates.hpp
@@ -19,7 +19,7 @@
 
 #pragma once
 
-#include "llmcore/PromptTemplateManager.hpp"
+#include "pluginllmcore/PromptTemplateManager.hpp"
 #include "templates/Alpaca.hpp"
 #include "templates/ChatML.hpp"
 #include "templates/Claude.hpp"
@@ -44,7 +44,7 @@ namespace QodeAssist::Templates {
 
 inline void registerTemplates()
 {
-    auto &templateManager = LLMCore::PromptTemplateManager::instance();
+    auto &templateManager = PluginLLMCore::PromptTemplateManager::instance();
     templateManager.registerTemplate();
     templateManager.registerTemplate();
     templateManager.registerTemplate();
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 4e167b3..581bc88 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -17,6 +17,8 @@ target_link_libraries(QodeAssistTest PRIVATE
     GTest::Main
     QtCreator::LanguageClient
     Context
+    PluginLLMCore
+    LLMCore
 )
 
 target_compile_definitions(QodeAssistTest PRIVATE CMAKE_CURRENT_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
diff --git a/test/DocumentContextReaderTest.cpp b/test/DocumentContextReaderTest.cpp
index 76969d8..91ce5b0 100644
--- a/test/DocumentContextReaderTest.cpp
+++ b/test/DocumentContextReaderTest.cpp
@@ -24,6 +24,19 @@
 #include 
 #include 
 
+namespace QodeAssist::PluginLLMCore {
+
+void PrintTo(const ContextData &data, std::ostream *os)
+{
+    *os << "ContextData{prefix="
+        << (data.prefix ? data.prefix->toStdString() : "")
+        << ", suffix=" << (data.suffix ? data.suffix->toStdString() : "")
+        << ", fileContext=" << (data.fileContext ? data.fileContext->toStdString() : "")
+        << "}";
+}
+
+} // namespace QodeAssist::PluginLLMCore
+
 using namespace QodeAssist::Context;
 using namespace QodeAssist::LLMCore;
 using namespace QodeAssist::Settings;
@@ -369,7 +382,7 @@ TEST_F(DocumentContextReaderTest, testPrepareContext)
 
     EXPECT_EQ(
         reader.prepareContext(2, 3, *createSettingsForWholeFile()),
-        (ContextData{
+        (QodeAssist::PluginLLMCore::ContextData{
             .prefix = "Line 0\nLine 1\nLin",
             .suffix = "e 2\nLine 3\nLine 4",
             .fileContext = "\n Language:  (MIME: text/python) filepath: /path/to/file()\n\n"
@@ -377,7 +390,7 @@ TEST_F(DocumentContextReaderTest, testPrepareContext)
 
     EXPECT_EQ(
         reader.prepareContext(2, 3, *createSettingsForLines(1, 1)),
-        (ContextData{
+        (QodeAssist::PluginLLMCore::ContextData{
             .prefix = "Line 1\nLin",
             .suffix = "e 2\nLine 3",
             .fileContext = "\n Language:  (MIME: text/python) filepath: /path/to/file()\n\n"
@@ -385,7 +398,7 @@ TEST_F(DocumentContextReaderTest, testPrepareContext)
 
     EXPECT_EQ(
         reader.prepareContext(2, 3, *createSettingsForLines(2, 2)),
-        (ContextData{
+        (QodeAssist::PluginLLMCore::ContextData{
             .prefix = "Line 0\nLine 1\nLin",
             .suffix = "e 2\nLine 3\nLine 4",
             .fileContext = "\n Language:  (MIME: text/python) filepath: /path/to/file()\n\n"
diff --git a/test/LLMClientInterfaceTests.cpp b/test/LLMClientInterfaceTests.cpp
index 1f93507..0b31cc5 100644
--- a/test/LLMClientInterfaceTests.cpp
+++ b/test/LLMClientInterfaceTests.cpp
@@ -62,8 +62,6 @@ public:
     QString url() const override { return "https://mock_url"; }
     QString completionEndpoint() const override { return "/v1/completions"; }
     QString chatEndpoint() const override { return "/v1/chat/completions"; }
-    bool supportsModelListing() const override { return false; }
-
     void prepareRequest(
         QJsonObject &request,
         LLMCore::PromptTemplate *promptTemplate,
@@ -85,14 +83,6 @@ public:
         return QtFuture::makeReadyFuture(QList{});
     }
 
-    QStringList validateRequest(
-        const QJsonObject &request, LLMCore::TemplateType templateType) override
-    {
-        return {};
-    }
-
-    QString apiKey() const override { return "mock_api_key"; }
-    void prepareNetworkRequest(QNetworkRequest &request) const override {}
     LLMCore::ProviderID providerID() const override { return LLMCore::ProviderID::OpenAI; }
 };
 
diff --git a/test/TestUtils.hpp b/test/TestUtils.hpp
index 533bb8f..00567b2 100644
--- a/test/TestUtils.hpp
+++ b/test/TestUtils.hpp
@@ -18,7 +18,7 @@
  */
 
 #include 
-#include 
+#include 
 #include 
 
 QT_BEGIN_NAMESPACE
@@ -61,14 +61,14 @@ std::ostream &operator<<(std::ostream &out, const std::optional &value)
 
 namespace QodeAssist::LLMCore {
 
-inline std::ostream &operator<<(std::ostream &out, const Message &value)
+inline std::ostream &operator<<(std::ostream &out, const PluginLLMCore::Message &value)
 {
     out << "Message{"
         << "role=" << value.role << "content=" << value.content << "}";
     return out;
 }
 
-inline std::ostream &operator<<(std::ostream &out, const ContextData &value)
+inline std::ostream &operator<<(std::ostream &out, const PluginLLMCore::ContextData &value)
 {
     out << "ContextData{"
         << "\n  systemPrompt=" << value.systemPrompt << "\n  prefix=" << value.prefix
diff --git a/tools/BuildProjectTool.cpp b/tools/BuildProjectTool.cpp
index bf6e43e..93fdb76 100644
--- a/tools/BuildProjectTool.cpp
+++ b/tools/BuildProjectTool.cpp
@@ -59,12 +59,12 @@ BuildProjectTool::~BuildProjectTool()
     m_activeBuilds.clear();
 }
 
-QString BuildProjectTool::name() const
+QString BuildProjectTool::id() const
 {
     return "build_project";
 }
 
-QString BuildProjectTool::stringName() const
+QString BuildProjectTool::displayName() const
 {
     return "Building and running project";
 }
@@ -80,7 +80,7 @@ QString BuildProjectTool::description() const
            "Note: This operation may take some time depending on project size.";
 }
 
-QJsonObject BuildProjectTool::getDefinition(LLMCore::ToolSchemaFormat format) const
+QJsonObject BuildProjectTool::parametersSchema() const
 {
     QJsonObject definition;
     definition["type"] = "object";
@@ -96,26 +96,9 @@ QJsonObject BuildProjectTool::getDefinition(LLMCore::ToolSchemaFormat format) co
     definition["properties"] = properties;
     definition["required"] = QJsonArray();
 
-    switch (format) {
-    case LLMCore::ToolSchemaFormat::OpenAI:
-        return customizeForOpenAI(definition);
-    case LLMCore::ToolSchemaFormat::Claude:
-        return customizeForClaude(definition);
-    case LLMCore::ToolSchemaFormat::Ollama:
-        return customizeForOllama(definition);
-    case LLMCore::ToolSchemaFormat::Google:
-        return customizeForGoogle(definition);
-    }
-
     return definition;
 }
 
-LLMCore::ToolPermissions BuildProjectTool::requiredPermissions() const
-{
-    return LLMCore::ToolPermission::FileSystemRead 
-         | LLMCore::ToolPermission::FileSystemWrite;
-}
-
 QFuture BuildProjectTool::executeAsync(const QJsonObject &input)
 {
     auto *project = ProjectExplorer::ProjectManager::startupProject();
diff --git a/tools/BuildProjectTool.hpp b/tools/BuildProjectTool.hpp
index 2c0ade5..72a6bb7 100644
--- a/tools/BuildProjectTool.hpp
+++ b/tools/BuildProjectTool.hpp
@@ -19,7 +19,7 @@
 
 #pragma once
 
-#include 
+#include 
 #include 
 #include 
 #include 
@@ -42,18 +42,17 @@ struct BuildInfo
     QMetaObject::Connection buildFinishedConnection;
 };
 
-class BuildProjectTool : public LLMCore::BaseTool
+class BuildProjectTool : public ::LLMCore::BaseTool
 {
     Q_OBJECT
 public:
     explicit BuildProjectTool(QObject *parent = nullptr);
     ~BuildProjectTool() override;
 
-    QString name() const override;
-    QString stringName() const override;
+    QString id() const override;
+    QString displayName() const override;
     QString description() const override;
-    QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
-    LLMCore::ToolPermissions requiredPermissions() const override;
+    QJsonObject parametersSchema() const override;
 
     QFuture executeAsync(const QJsonObject &input = QJsonObject()) override;
 
diff --git a/tools/CreateNewFileTool.cpp b/tools/CreateNewFileTool.cpp
index 6aed97e..1777521 100644
--- a/tools/CreateNewFileTool.cpp
+++ b/tools/CreateNewFileTool.cpp
@@ -36,12 +36,12 @@ CreateNewFileTool::CreateNewFileTool(QObject *parent)
     : BaseTool(parent)
 {}
 
-QString CreateNewFileTool::name() const
+QString CreateNewFileTool::id() const
 {
     return "create_new_file";
 }
 
-QString CreateNewFileTool::stringName() const
+QString CreateNewFileTool::displayName() const
 {
     return {"Creating new file"};
 }
@@ -54,7 +54,7 @@ QString CreateNewFileTool::description() const
            "to the project file";
 }
 
-QJsonObject CreateNewFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const
+QJsonObject CreateNewFileTool::parametersSchema() const
 {
     QJsonObject properties;
 
@@ -70,25 +70,9 @@ QJsonObject CreateNewFileTool::getDefinition(LLMCore::ToolSchemaFormat format) c
     required.append("filepath");
     definition["required"] = required;
 
-    switch (format) {
-    case LLMCore::ToolSchemaFormat::OpenAI:
-        return customizeForOpenAI(definition);
-    case LLMCore::ToolSchemaFormat::Claude:
-        return customizeForClaude(definition);
-    case LLMCore::ToolSchemaFormat::Ollama:
-        return customizeForOllama(definition);
-    case LLMCore::ToolSchemaFormat::Google:
-        return customizeForGoogle(definition);
-    }
-
     return definition;
 }
 
-LLMCore::ToolPermissions CreateNewFileTool::requiredPermissions() const
-{
-    return LLMCore::ToolPermission::FileSystemWrite;
-}
-
 QFuture CreateNewFileTool::executeAsync(const QJsonObject &input)
 {
     return QtConcurrent::run([this, input]() -> QString {
diff --git a/tools/CreateNewFileTool.hpp b/tools/CreateNewFileTool.hpp
index 1c16325..71b2c3e 100644
--- a/tools/CreateNewFileTool.hpp
+++ b/tools/CreateNewFileTool.hpp
@@ -19,21 +19,20 @@
 
 #pragma once
 
-#include 
+#include 
 
 namespace QodeAssist::Tools {
 
-class CreateNewFileTool : public LLMCore::BaseTool
+class CreateNewFileTool : public ::LLMCore::BaseTool
 {
     Q_OBJECT
 public:
     explicit CreateNewFileTool(QObject *parent = nullptr);
 
-    QString name() const override;
-    QString stringName() const override;
+    QString id() const override;
+    QString displayName() const override;
     QString description() const override;
-    QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
-    LLMCore::ToolPermissions requiredPermissions() const override;
+    QJsonObject parametersSchema() const override;
 
     QFuture executeAsync(const QJsonObject &input = QJsonObject()) override;
 };
diff --git a/tools/EditFileTool.cpp b/tools/EditFileTool.cpp
index adfb60e..f3a656b 100644
--- a/tools/EditFileTool.cpp
+++ b/tools/EditFileTool.cpp
@@ -39,12 +39,12 @@ EditFileTool::EditFileTool(QObject *parent)
     : BaseTool(parent)
 {}
 
-QString EditFileTool::name() const
+QString EditFileTool::id() const
 {
     return "edit_file";
 }
 
-QString EditFileTool::stringName() const
+QString EditFileTool::displayName() const
 {
     return {"Editing file"};
 }
@@ -71,7 +71,7 @@ QString EditFileTool::description() const
            "disabled auto-apply. DO NOT retry the same edit - wait for user action.";
 }
 
-QJsonObject EditFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const
+QJsonObject EditFileTool::parametersSchema() const
 {
     QJsonObject properties;
 
@@ -104,25 +104,9 @@ QJsonObject EditFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const
     required.append("new_content");
     definition["required"] = required;
 
-    switch (format) {
-    case LLMCore::ToolSchemaFormat::OpenAI:
-        return customizeForOpenAI(definition);
-    case LLMCore::ToolSchemaFormat::Claude:
-        return customizeForClaude(definition);
-    case LLMCore::ToolSchemaFormat::Ollama:
-        return customizeForOllama(definition);
-    case LLMCore::ToolSchemaFormat::Google:
-        return customizeForGoogle(definition);
-    }
-
     return definition;
 }
 
-LLMCore::ToolPermissions EditFileTool::requiredPermissions() const
-{
-    return LLMCore::ToolPermission::FileSystemWrite;
-}
-
 QFuture EditFileTool::executeAsync(const QJsonObject &input)
 {
     return QtConcurrent::run([this, input]() -> QString {
diff --git a/tools/EditFileTool.hpp b/tools/EditFileTool.hpp
index 8f888a0..f3957a1 100644
--- a/tools/EditFileTool.hpp
+++ b/tools/EditFileTool.hpp
@@ -19,21 +19,20 @@
 
 #pragma once
 
-#include 
+#include 
 
 namespace QodeAssist::Tools {
 
-class EditFileTool : public LLMCore::BaseTool
+class EditFileTool : public ::LLMCore::BaseTool
 {
     Q_OBJECT
 public:
     explicit EditFileTool(QObject *parent = nullptr);
 
-    QString name() const override;
-    QString stringName() const override;
+    QString id() const override;
+    QString displayName() const override;
     QString description() const override;
-    QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
-    LLMCore::ToolPermissions requiredPermissions() const override;
+    QJsonObject parametersSchema() const override;
 
     QFuture executeAsync(const QJsonObject &input = QJsonObject()) override;
 };
diff --git a/tools/ExecuteTerminalCommandTool.cpp b/tools/ExecuteTerminalCommandTool.cpp
index d114500..f0d8a2a 100644
--- a/tools/ExecuteTerminalCommandTool.cpp
+++ b/tools/ExecuteTerminalCommandTool.cpp
@@ -41,12 +41,12 @@ ExecuteTerminalCommandTool::ExecuteTerminalCommandTool(QObject *parent)
 {
 }
 
-QString ExecuteTerminalCommandTool::name() const
+QString ExecuteTerminalCommandTool::id() const
 {
     return "execute_terminal_command";
 }
 
-QString ExecuteTerminalCommandTool::stringName() const
+QString ExecuteTerminalCommandTool::displayName() const
 {
     return "Executing terminal command";
 }
@@ -56,7 +56,7 @@ QString ExecuteTerminalCommandTool::description() const
     return getCommandDescription();
 }
 
-QJsonObject ExecuteTerminalCommandTool::getDefinition(LLMCore::ToolSchemaFormat format) const
+QJsonObject ExecuteTerminalCommandTool::parametersSchema() const
 {
     QJsonObject definition;
     definition["type"] = "object";
@@ -70,34 +70,16 @@ QJsonObject ExecuteTerminalCommandTool::getDefinition(LLMCore::ToolSchemaFormat
 
     properties["args"] = QJsonObject{
         {"type", "string"},
-        {"description", 
+        {"description",
          "Optional arguments for the command. Arguments with spaces should be properly quoted. "
          "Example: '--file \"path with spaces.txt\" --verbose'"}};
 
     definition["properties"] = properties;
     definition["required"] = QJsonArray{"command"};
 
-    switch (format) {
-    case LLMCore::ToolSchemaFormat::OpenAI:
-        return customizeForOpenAI(definition);
-    case LLMCore::ToolSchemaFormat::Claude:
-        return customizeForClaude(definition);
-    case LLMCore::ToolSchemaFormat::Ollama:
-        return customizeForOllama(definition);
-    case LLMCore::ToolSchemaFormat::Google:
-        return customizeForGoogle(definition);
-    }
-
     return definition;
 }
 
-LLMCore::ToolPermissions ExecuteTerminalCommandTool::requiredPermissions() const
-{
-    return LLMCore::ToolPermission::FileSystemRead 
-         | LLMCore::ToolPermission::FileSystemWrite 
-         | LLMCore::ToolPermission::NetworkAccess;
-}
-
 QFuture ExecuteTerminalCommandTool::executeAsync(const QJsonObject &input)
 {
     const QString command = input.value("command").toString().trimmed();
diff --git a/tools/ExecuteTerminalCommandTool.hpp b/tools/ExecuteTerminalCommandTool.hpp
index 27b04be..e497b23 100644
--- a/tools/ExecuteTerminalCommandTool.hpp
+++ b/tools/ExecuteTerminalCommandTool.hpp
@@ -19,22 +19,21 @@
 
 #pragma once
 
-#include 
+#include 
 #include 
 
 namespace QodeAssist::Tools {
 
-class ExecuteTerminalCommandTool : public LLMCore::BaseTool
+class ExecuteTerminalCommandTool : public ::LLMCore::BaseTool
 {
     Q_OBJECT
 public:
     explicit ExecuteTerminalCommandTool(QObject *parent = nullptr);
 
-    QString name() const override;
-    QString stringName() const override;
+    QString id() const override;
+    QString displayName() const override;
     QString description() const override;
-    QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
-    LLMCore::ToolPermissions requiredPermissions() const override;
+    QJsonObject parametersSchema() const override;
 
     QFuture executeAsync(const QJsonObject &input = QJsonObject()) override;
 
diff --git a/tools/FindAndReadFileTool.cpp b/tools/FindAndReadFileTool.cpp
index 6ef9594..1493ac0 100644
--- a/tools/FindAndReadFileTool.cpp
+++ b/tools/FindAndReadFileTool.cpp
@@ -32,12 +32,12 @@ FindAndReadFileTool::FindAndReadFileTool(QObject *parent)
     , m_ignoreManager(new Context::IgnoreManager(this))
 {}
 
-QString FindAndReadFileTool::name() const
+QString FindAndReadFileTool::id() const
 {
     return "find_and_read_file";
 }
 
-QString FindAndReadFileTool::stringName() const
+QString FindAndReadFileTool::displayName() const
 {
     return "Finding and reading file";
 }
@@ -48,7 +48,7 @@ QString FindAndReadFileTool::description() const
            "Returns the best matching file and its content.";
 }
 
-QJsonObject FindAndReadFileTool::getDefinition(LLMCore::ToolSchemaFormat format) const
+QJsonObject FindAndReadFileTool::parametersSchema() const
 {
     QJsonObject properties;
 
@@ -68,24 +68,9 @@ QJsonObject FindAndReadFileTool::getDefinition(LLMCore::ToolSchemaFormat format)
     definition["properties"] = properties;
     definition["required"] = QJsonArray{"query"};
 
-    switch (format) {
-    case LLMCore::ToolSchemaFormat::OpenAI:
-        return customizeForOpenAI(definition);
-    case LLMCore::ToolSchemaFormat::Claude:
-        return customizeForClaude(definition);
-    case LLMCore::ToolSchemaFormat::Ollama:
-        return customizeForOllama(definition);
-    case LLMCore::ToolSchemaFormat::Google:
-        return customizeForGoogle(definition);
-    }
     return definition;
 }
 
-LLMCore::ToolPermissions FindAndReadFileTool::requiredPermissions() const
-{
-    return LLMCore::ToolPermission::FileSystemRead;
-}
-
 QFuture FindAndReadFileTool::executeAsync(const QJsonObject &input)
 {
     return QtConcurrent::run([this, input]() -> QString {
diff --git a/tools/FindAndReadFileTool.hpp b/tools/FindAndReadFileTool.hpp
index 7311529..dc22cc5 100644
--- a/tools/FindAndReadFileTool.hpp
+++ b/tools/FindAndReadFileTool.hpp
@@ -22,25 +22,24 @@
 #include "FileSearchUtils.hpp"
 
 #include 
-#include 
+#include 
 #include 
 #include 
 #include 
 
 namespace QodeAssist::Tools {
 
-class FindAndReadFileTool : public LLMCore::BaseTool
+class FindAndReadFileTool : public ::LLMCore::BaseTool
 {
     Q_OBJECT
 
 public:
     explicit FindAndReadFileTool(QObject *parent = nullptr);
 
-    QString name() const override;
-    QString stringName() const override;
+    QString id() const override;
+    QString displayName() const override;
     QString description() const override;
-    QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
-    LLMCore::ToolPermissions requiredPermissions() const override;
+    QJsonObject parametersSchema() const override;
     QFuture executeAsync(const QJsonObject &input) override;
 
 private:
diff --git a/tools/GetIssuesListTool.cpp b/tools/GetIssuesListTool.cpp
index c274f2b..70f2700 100644
--- a/tools/GetIssuesListTool.cpp
+++ b/tools/GetIssuesListTool.cpp
@@ -121,12 +121,12 @@ GetIssuesListTool::GetIssuesListTool(QObject *parent)
     IssuesTracker::instance();
 }
 
-QString GetIssuesListTool::name() const
+QString GetIssuesListTool::id() const
 {
     return "get_issues_list";
 }
 
-QString GetIssuesListTool::stringName() const
+QString GetIssuesListTool::displayName() const
 {
     return "Getting issues list from Qt Creator";
 }
@@ -138,7 +138,7 @@ QString GetIssuesListTool::description() const
            "Optional severity filter: 'error', 'warning', or 'all' (default).";
 }
 
-QJsonObject GetIssuesListTool::getDefinition(LLMCore::ToolSchemaFormat format) const
+QJsonObject GetIssuesListTool::parametersSchema() const
 {
     QJsonObject definition;
     definition["type"] = "object";
@@ -152,25 +152,9 @@ QJsonObject GetIssuesListTool::getDefinition(LLMCore::ToolSchemaFormat format) c
     definition["properties"] = properties;
     definition["required"] = QJsonArray();
 
-    switch (format) {
-    case LLMCore::ToolSchemaFormat::OpenAI:
-        return customizeForOpenAI(definition);
-    case LLMCore::ToolSchemaFormat::Claude:
-        return customizeForClaude(definition);
-    case LLMCore::ToolSchemaFormat::Ollama:
-        return customizeForOllama(definition);
-    case LLMCore::ToolSchemaFormat::Google:
-        return customizeForGoogle(definition);
-    }
-
     return definition;
 }
 
-LLMCore::ToolPermissions GetIssuesListTool::requiredPermissions() const
-{
-    return LLMCore::ToolPermission::FileSystemRead;
-}
-
 QFuture GetIssuesListTool::executeAsync(const QJsonObject &input)
 {
     return QtConcurrent::run([input]() -> QString {
diff --git a/tools/GetIssuesListTool.hpp b/tools/GetIssuesListTool.hpp
index ea2c1ef..a56941b 100644
--- a/tools/GetIssuesListTool.hpp
+++ b/tools/GetIssuesListTool.hpp
@@ -19,7 +19,7 @@
 
 #pragma once
 
-#include 
+#include 
 #include 
 #include 
 #include 
@@ -46,17 +46,16 @@ private:
     mutable QMutex m_mutex;
 };
 
-class GetIssuesListTool : public LLMCore::BaseTool
+class GetIssuesListTool : public ::LLMCore::BaseTool
 {
     Q_OBJECT
 public:
     explicit GetIssuesListTool(QObject *parent = nullptr);
 
-    QString name() const override;
-    QString stringName() const override;
+    QString id() const override;
+    QString displayName() const override;
     QString description() const override;
-    QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
-    LLMCore::ToolPermissions requiredPermissions() const override;
+    QJsonObject parametersSchema() const override;
 
     QFuture executeAsync(const QJsonObject &input = QJsonObject()) override;
 };
diff --git a/tools/ListProjectFilesTool.cpp b/tools/ListProjectFilesTool.cpp
index f39e45b..c91f9e0 100644
--- a/tools/ListProjectFilesTool.cpp
+++ b/tools/ListProjectFilesTool.cpp
@@ -37,12 +37,12 @@ ListProjectFilesTool::ListProjectFilesTool(QObject *parent)
 
 {}
 
-QString ListProjectFilesTool::name() const
+QString ListProjectFilesTool::id() const
 {
     return "list_project_files";
 }
 
-QString ListProjectFilesTool::stringName() const
+QString ListProjectFilesTool::displayName() const
 {
     return {"Reading project files list"};
 }
@@ -53,32 +53,16 @@ QString ListProjectFilesTool::description() const
            "Useful for understanding project structure. No parameters required.";
 }
 
-QJsonObject ListProjectFilesTool::getDefinition(LLMCore::ToolSchemaFormat format) const
+QJsonObject ListProjectFilesTool::parametersSchema() const
 {
     QJsonObject definition;
     definition["type"] = "object";
     definition["properties"] = QJsonObject();
     definition["required"] = QJsonArray();
 
-    switch (format) {
-    case LLMCore::ToolSchemaFormat::OpenAI:
-        return customizeForOpenAI(definition);
-    case LLMCore::ToolSchemaFormat::Claude:
-        return customizeForClaude(definition);
-    case LLMCore::ToolSchemaFormat::Ollama:
-        return customizeForOllama(definition);
-    case LLMCore::ToolSchemaFormat::Google:
-        return customizeForGoogle(definition);
-    }
-
     return definition;
 }
 
-LLMCore::ToolPermissions ListProjectFilesTool::requiredPermissions() const
-{
-    return LLMCore::ToolPermission::FileSystemRead;
-}
-
 QFuture ListProjectFilesTool::executeAsync(const QJsonObject &input)
 {
     Q_UNUSED(input)
diff --git a/tools/ListProjectFilesTool.hpp b/tools/ListProjectFilesTool.hpp
index e61dc43..58a219f 100644
--- a/tools/ListProjectFilesTool.hpp
+++ b/tools/ListProjectFilesTool.hpp
@@ -19,23 +19,22 @@
 
 #pragma once
 
-#include 
+#include 
 
 #include 
 
 namespace QodeAssist::Tools {
 
-class ListProjectFilesTool : public LLMCore::BaseTool
+class ListProjectFilesTool : public ::LLMCore::BaseTool
 {
     Q_OBJECT
 public:
     explicit ListProjectFilesTool(QObject *parent = nullptr);
 
-    QString name() const override;
-    QString stringName() const override;
+    QString id() const override;
+    QString displayName() const override;
     QString description() const override;
-    QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
-    LLMCore::ToolPermissions requiredPermissions() const override;
+    QJsonObject parametersSchema() const override;
 
     QFuture executeAsync(const QJsonObject &input = QJsonObject()) override;
 
diff --git a/tools/ProjectSearchTool.cpp b/tools/ProjectSearchTool.cpp
index a1bfc65..49420a9 100644
--- a/tools/ProjectSearchTool.cpp
+++ b/tools/ProjectSearchTool.cpp
@@ -43,12 +43,12 @@ ProjectSearchTool::ProjectSearchTool(QObject *parent)
     , m_ignoreManager(new Context::IgnoreManager(this))
 {}
 
-QString ProjectSearchTool::name() const
+QString ProjectSearchTool::id() const
 {
     return "search_project";
 }
 
-QString ProjectSearchTool::stringName() const
+QString ProjectSearchTool::displayName() const
 {
     return "Searching in project";
 }
@@ -60,7 +60,7 @@ QString ProjectSearchTool::description() const
            "Symbol mode: finds C++ definitions (classes, functions, etc).";
 }
 
-QJsonObject ProjectSearchTool::getDefinition(LLMCore::ToolSchemaFormat format) const
+QJsonObject ProjectSearchTool::parametersSchema() const
 {
     QJsonObject properties;
 
@@ -94,24 +94,9 @@ QJsonObject ProjectSearchTool::getDefinition(LLMCore::ToolSchemaFormat format) c
     definition["properties"] = properties;
     definition["required"] = QJsonArray{"query", "search_type"};
 
-    switch (format) {
-    case LLMCore::ToolSchemaFormat::OpenAI:
-        return customizeForOpenAI(definition);
-    case LLMCore::ToolSchemaFormat::Claude:
-        return customizeForClaude(definition);
-    case LLMCore::ToolSchemaFormat::Ollama:
-        return customizeForOllama(definition);
-    case LLMCore::ToolSchemaFormat::Google:
-        return customizeForGoogle(definition);
-    }
     return definition;
 }
 
-LLMCore::ToolPermissions ProjectSearchTool::requiredPermissions() const
-{
-    return LLMCore::ToolPermission::FileSystemRead;
-}
-
 QFuture ProjectSearchTool::executeAsync(const QJsonObject &input)
 {
     return QtConcurrent::run([this, input]() -> QString {
diff --git a/tools/ProjectSearchTool.hpp b/tools/ProjectSearchTool.hpp
index 40874ad..a4b09fd 100644
--- a/tools/ProjectSearchTool.hpp
+++ b/tools/ProjectSearchTool.hpp
@@ -20,25 +20,24 @@
 #pragma once
 
 #include 
-#include 
+#include 
 #include 
 #include 
 #include 
 
 namespace QodeAssist::Tools {
 
-class ProjectSearchTool : public LLMCore::BaseTool
+class ProjectSearchTool : public ::LLMCore::BaseTool
 {
     Q_OBJECT
 
 public:
     explicit ProjectSearchTool(QObject *parent = nullptr);
 
-    QString name() const override;
-    QString stringName() const override;
+    QString id() const override;
+    QString displayName() const override;
     QString description() const override;
-    QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
-    LLMCore::ToolPermissions requiredPermissions() const override;
+    QJsonObject parametersSchema() const override;
     QFuture executeAsync(const QJsonObject &input) override;
 
 private:
diff --git a/tools/TodoTool.cpp b/tools/TodoTool.cpp
index e58bcea..cbea19b 100644
--- a/tools/TodoTool.cpp
+++ b/tools/TodoTool.cpp
@@ -31,12 +31,12 @@ TodoTool::TodoTool(QObject *parent)
     : BaseTool(parent)
 {}
 
-QString TodoTool::name() const
+QString TodoTool::id() const
 {
     return "todo_tool";
 }
 
-QString TodoTool::stringName() const
+QString TodoTool::displayName() const
 {
     return "Managing TODO list for task tracking";
 }
@@ -53,7 +53,7 @@ QString TodoTool::description() const
            "The list persists throughout the conversation.";
 }
 
-QJsonObject TodoTool::getDefinition(LLMCore::ToolSchemaFormat format) const
+QJsonObject TodoTool::parametersSchema() const
 {
     QJsonObject definition;
     definition["type"] = "object";
@@ -97,32 +97,15 @@ QJsonObject TodoTool::getDefinition(LLMCore::ToolSchemaFormat format) const
     required.append("operation");
     definition["required"] = required;
 
-    switch (format) {
-    case LLMCore::ToolSchemaFormat::OpenAI:
-        return customizeForOpenAI(definition);
-    case LLMCore::ToolSchemaFormat::Claude:
-        return customizeForClaude(definition);
-    case LLMCore::ToolSchemaFormat::Ollama:
-        return customizeForOllama(definition);
-    case LLMCore::ToolSchemaFormat::Google:
-        return customizeForGoogle(definition);
-    }
-
     return definition;
 }
 
-LLMCore::ToolPermissions TodoTool::requiredPermissions() const
-{
-    return LLMCore::ToolPermission::None;
-}
-
 QFuture TodoTool::executeAsync(const QJsonObject &input)
 {
     return QtConcurrent::run([this, input]() -> QString {
-        QString sessionId = input.value("session_id").toString();
-        if (sessionId.isEmpty()) {
-            sessionId = "current";
-        }
+        QMutexLocker sessionLocker(&m_mutex);
+        QString sessionId = m_currentSessionId.isEmpty() ? "current" : m_currentSessionId;
+        sessionLocker.unlock();
 
         const QString operation = input.value("operation").toString();
 
@@ -194,6 +177,12 @@ QFuture TodoTool::executeAsync(const QJsonObject &input)
     });
 }
 
+void TodoTool::setCurrentSessionId(const QString &sessionId)
+{
+    QMutexLocker locker(&m_mutex);
+    m_currentSessionId = sessionId;
+}
+
 void TodoTool::clearSession(const QString &sessionId)
 {
     QMutexLocker locker(&m_mutex);
diff --git a/tools/TodoTool.hpp b/tools/TodoTool.hpp
index 2454f82..8c0ef34 100644
--- a/tools/TodoTool.hpp
+++ b/tools/TodoTool.hpp
@@ -19,7 +19,7 @@
 
 #pragma once
 
-#include 
+#include 
 
 #include 
 #include 
@@ -34,21 +34,21 @@ struct TodoItem
     bool completed;
 };
 
-class TodoTool : public LLMCore::BaseTool
+class TodoTool : public ::LLMCore::BaseTool
 {
     Q_OBJECT
 
 public:
     explicit TodoTool(QObject *parent = nullptr);
 
-    QString name() const override;
-    QString stringName() const override;
+    QString id() const override;
+    QString displayName() const override;
     QString description() const override;
-    QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
-    LLMCore::ToolPermissions requiredPermissions() const override;
+    QJsonObject parametersSchema() const override;
 
     QFuture executeAsync(const QJsonObject &input = QJsonObject()) override;
 
+    void setCurrentSessionId(const QString &sessionId);
     void clearSession(const QString &sessionId);
 
 private:
@@ -59,6 +59,7 @@ private:
     QString listRemainingTodosLocked(const QString &sessionId) const;
 
     mutable QMutex m_mutex;
+    QString m_currentSessionId;
     QHash> m_sessionTodos;
     QHash m_sessionNextId;
 };
diff --git a/tools/ToolHandler.cpp b/tools/ToolHandler.cpp
index 93b0c50..ebc5bca 100644
--- a/tools/ToolHandler.cpp
+++ b/tools/ToolHandler.cpp
@@ -35,7 +35,7 @@ ToolHandler::ToolHandler(QObject *parent)
 QFuture ToolHandler::executeToolAsync(
     const QString &requestId,
     const QString &toolId,
-    LLMCore::BaseTool *tool,
+    PluginLLMCore::BaseTool *tool,
     const QJsonObject &input)
 {
     if (!tool) {
diff --git a/tools/ToolHandler.hpp b/tools/ToolHandler.hpp
index c8b2367..ba31f81 100644
--- a/tools/ToolHandler.hpp
+++ b/tools/ToolHandler.hpp
@@ -25,7 +25,7 @@
 #include 
 #include 
 
-#include 
+#include 
 
 namespace QodeAssist::Tools {
 
@@ -39,7 +39,7 @@ public:
     QFuture executeToolAsync(
         const QString &requestId,
         const QString &toolId,
-        LLMCore::BaseTool *tool,
+        PluginLLMCore::BaseTool *tool,
         const QJsonObject &input);
 
     void cleanupRequest(const QString &requestId);
diff --git a/tools/ToolsFactory.cpp b/tools/ToolsFactory.cpp
index 5fe2e19..dabd424 100644
--- a/tools/ToolsFactory.cpp
+++ b/tools/ToolsFactory.cpp
@@ -58,7 +58,7 @@ void ToolsFactory::registerTools()
     LOG_MESSAGE(QString("Registered %1 tools").arg(m_tools.size()));
 }
 
-void ToolsFactory::registerTool(LLMCore::BaseTool *tool)
+void ToolsFactory::registerTool(PluginLLMCore::BaseTool *tool)
 {
     if (!tool) {
         LOG_MESSAGE("Warning: Attempted to register null tool");
@@ -73,18 +73,18 @@ void ToolsFactory::registerTool(LLMCore::BaseTool *tool)
     m_tools.insert(toolName, tool);
 }
 
-QList ToolsFactory::getAvailableTools() const
+QList ToolsFactory::getAvailableTools() const
 {
     return m_tools.values();
 }
 
-LLMCore::BaseTool *ToolsFactory::getToolByName(const QString &name) const
+PluginLLMCore::BaseTool *ToolsFactory::getToolByName(const QString &name) const
 {
     return m_tools.value(name, nullptr);
 }
 
 QJsonArray ToolsFactory::getToolsDefinitions(
-    LLMCore::ToolSchemaFormat format, LLMCore::RunToolsFilter filter) const
+    PluginLLMCore::ToolSchemaFormat format, PluginLLMCore::RunToolsFilter filter) const
 {
     QJsonArray toolsArray;
     const auto &settings = Settings::toolsSettings();
@@ -113,30 +113,30 @@ QJsonArray ToolsFactory::getToolsDefinitions(
 
         const auto requiredPerms = it.value()->requiredPermissions();
 
-        if (filter != LLMCore::RunToolsFilter::ALL) {
+        if (filter != PluginLLMCore::RunToolsFilter::ALL) {
             bool matchesFilter = false;
 
             switch (filter) {
-            case LLMCore::RunToolsFilter::OnlyRead:
-                if (requiredPerms == LLMCore::ToolPermission::None
-                    || requiredPerms.testFlag(LLMCore::ToolPermission::FileSystemRead)) {
+            case PluginLLMCore::RunToolsFilter::OnlyRead:
+                if (requiredPerms == PluginLLMCore::ToolPermission::None
+                    || requiredPerms.testFlag(PluginLLMCore::ToolPermission::FileSystemRead)) {
                     matchesFilter = true;
                 }
                 break;
 
-            case LLMCore::RunToolsFilter::OnlyWrite:
-                if (requiredPerms.testFlag(LLMCore::ToolPermission::FileSystemWrite)) {
+            case PluginLLMCore::RunToolsFilter::OnlyWrite:
+                if (requiredPerms.testFlag(PluginLLMCore::ToolPermission::FileSystemWrite)) {
                     matchesFilter = true;
                 }
                 break;
 
-            case LLMCore::RunToolsFilter::OnlyNetworking:
-                if (requiredPerms.testFlag(LLMCore::ToolPermission::NetworkAccess)) {
+            case PluginLLMCore::RunToolsFilter::OnlyNetworking:
+                if (requiredPerms.testFlag(PluginLLMCore::ToolPermission::NetworkAccess)) {
                     matchesFilter = true;
                 }
                 break;
 
-            case LLMCore::RunToolsFilter::ALL:
+            case PluginLLMCore::RunToolsFilter::ALL:
                 matchesFilter = true;
                 break;
             }
@@ -150,19 +150,19 @@ QJsonArray ToolsFactory::getToolsDefinitions(
 
         bool hasPermission = true;
 
-        if (requiredPerms.testFlag(LLMCore::ToolPermission::FileSystemRead)) {
+        if (requiredPerms.testFlag(PluginLLMCore::ToolPermission::FileSystemRead)) {
             if (!settings.allowFileSystemRead()) {
                 hasPermission = false;
             }
         }
 
-        if (requiredPerms.testFlag(LLMCore::ToolPermission::FileSystemWrite)) {
+        if (requiredPerms.testFlag(PluginLLMCore::ToolPermission::FileSystemWrite)) {
             if (!settings.allowFileSystemWrite()) {
                 hasPermission = false;
             }
         }
 
-        if (requiredPerms.testFlag(LLMCore::ToolPermission::NetworkAccess)) {
+        if (requiredPerms.testFlag(PluginLLMCore::ToolPermission::NetworkAccess)) {
             if (!settings.allowNetworkAccess()) {
                 hasPermission = false;
             }
diff --git a/tools/ToolsFactory.hpp b/tools/ToolsFactory.hpp
index abb0e1a..e14f7d9 100644
--- a/tools/ToolsFactory.hpp
+++ b/tools/ToolsFactory.hpp
@@ -21,7 +21,7 @@
 
 #include 
 
-#include 
+#include 
 
 namespace QodeAssist::Tools {
 
@@ -32,17 +32,17 @@ public:
     ToolsFactory(QObject *parent = nullptr);
     ~ToolsFactory() override = default;
 
-    QList getAvailableTools() const;
-    LLMCore::BaseTool *getToolByName(const QString &name) const;
+    QList getAvailableTools() const;
+    PluginLLMCore::BaseTool *getToolByName(const QString &name) const;
     QJsonArray getToolsDefinitions(
-        LLMCore::ToolSchemaFormat format,
-        LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL) const;
+        PluginLLMCore::ToolSchemaFormat format,
+        PluginLLMCore::RunToolsFilter filter = PluginLLMCore::RunToolsFilter::ALL) const;
     QString getStringName(const QString &name) const;
 
 private:
     void registerTools();
-    void registerTool(LLMCore::BaseTool *tool);
+    void registerTool(PluginLLMCore::BaseTool *tool);
 
-    QHash m_tools;
+    QHash m_tools;
 };
 } // namespace QodeAssist::Tools
diff --git a/tools/ToolsManager.cpp b/tools/ToolsManager.cpp
index 797b6a0..1edd39f 100644
--- a/tools/ToolsManager.cpp
+++ b/tools/ToolsManager.cpp
@@ -138,7 +138,7 @@ void ToolsManager::executeNextTool(const QString &requestId)
 }
 
 QJsonArray ToolsManager::getToolsDefinitions(
-    LLMCore::ToolSchemaFormat format, LLMCore::RunToolsFilter filter) const
+    PluginLLMCore::ToolSchemaFormat format, PluginLLMCore::RunToolsFilter filter) const
 {
     if (!m_toolsFactory) {
         return QJsonArray();
diff --git a/tools/ToolsManager.hpp b/tools/ToolsManager.hpp
index 381c48d..a7d4f96 100644
--- a/tools/ToolsManager.hpp
+++ b/tools/ToolsManager.hpp
@@ -26,8 +26,8 @@
 
 #include "ToolHandler.hpp"
 #include "ToolsFactory.hpp"
-#include 
-#include 
+#include 
+#include 
 
 namespace QodeAssist::Tools {
 
@@ -47,7 +47,7 @@ struct ToolQueue
     bool isExecuting = false;
 };
 
-class ToolsManager : public QObject, public LLMCore::IToolsManager
+class ToolsManager : public QObject, public PluginLLMCore::IToolsManager
 {
     Q_OBJECT
 
@@ -61,8 +61,8 @@ public:
         const QJsonObject &input) override;
 
     QJsonArray getToolsDefinitions(
-        LLMCore::ToolSchemaFormat format,
-        LLMCore::RunToolsFilter filter = LLMCore::RunToolsFilter::ALL) const override;
+        PluginLLMCore::ToolSchemaFormat format,
+        PluginLLMCore::RunToolsFilter filter = PluginLLMCore::RunToolsFilter::ALL) const override;
     
     void cleanupRequest(const QString &requestId) override;
     void setCurrentSessionId(const QString &sessionId) override;
diff --git a/tools/ToolsRegistration.cpp b/tools/ToolsRegistration.cpp
new file mode 100644
index 0000000..62c4267
--- /dev/null
+++ b/tools/ToolsRegistration.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2026 Petr Mironychev
+ *
+ * This file is part of QodeAssist.
+ *
+ * QodeAssist is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * QodeAssist is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with QodeAssist. If not, see .
+ */
+
+#include "ToolsRegistration.hpp"
+
+#include 
+
+#include "BuildProjectTool.hpp"
+#include "CreateNewFileTool.hpp"
+#include "EditFileTool.hpp"
+#include "ExecuteTerminalCommandTool.hpp"
+#include "FindAndReadFileTool.hpp"
+#include "GetIssuesListTool.hpp"
+#include "ListProjectFilesTool.hpp"
+#include "ProjectSearchTool.hpp"
+#include "TodoTool.hpp"
+
+namespace QodeAssist::Tools {
+
+void registerQodeAssistTools(::LLMCore::ToolsManager *manager)
+{
+    manager->addTool(new ListProjectFilesTool(manager));
+    manager->addTool(new GetIssuesListTool(manager));
+    manager->addTool(new CreateNewFileTool(manager));
+    manager->addTool(new EditFileTool(manager));
+    manager->addTool(new BuildProjectTool(manager));
+    manager->addTool(new ExecuteTerminalCommandTool(manager));
+    manager->addTool(new ProjectSearchTool(manager));
+    manager->addTool(new FindAndReadFileTool(manager));
+    manager->addTool(new TodoTool(manager));
+}
+
+} // namespace QodeAssist::Tools
diff --git a/llmcore/ValidationUtils.hpp b/tools/ToolsRegistration.hpp
similarity index 55%
rename from llmcore/ValidationUtils.hpp
rename to tools/ToolsRegistration.hpp
index b77ab97..617768f 100644
--- a/llmcore/ValidationUtils.hpp
+++ b/tools/ToolsRegistration.hpp
@@ -1,5 +1,5 @@
-/* 
- * Copyright (C) 2024-2025 Petr Mironychev
+/*
+ * Copyright (C) 2025 Petr Mironychev
  *
  * This file is part of QodeAssist.
  *
@@ -19,23 +19,12 @@
 
 #pragma once
 
-#include 
-#include 
+namespace LLMCore {
+class ToolsManager;
+}
 
-namespace QodeAssist::LLMCore {
+namespace QodeAssist::Tools {
 
-class ValidationUtils
-{
-public:
-    static QStringList validateRequestFields(
-        const QJsonObject &request, const QJsonObject &templateObj);
+void registerQodeAssistTools(::LLMCore::ToolsManager *manager);
 
-private:
-    static void validateFields(
-        const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors);
-
-    static void validateNestedObjects(
-        const QJsonObject &request, const QJsonObject &templateObj, QStringList &errors);
-};
-
-} // namespace QodeAssist::LLMCore
+} // namespace QodeAssist::Tools