diff --git a/CMakeLists.txt b/CMakeLists.txt index e6c0fbe..0cc2d00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required(VERSION 3.16) +list(APPEND CMAKE_PREFIX_PATH "/Users/palm1r/Qt/Qt Creator.sdk/lib/cmake/QtCreator") + project(QodeAssist) set(CMAKE_AUTOMOC ON) @@ -38,7 +40,6 @@ add_definitions( add_subdirectory(sources) add_subdirectory(logger) -add_subdirectory(pluginllmcore) add_subdirectory(settings) add_subdirectory(UIControls) add_subdirectory(ChatView) @@ -70,7 +71,6 @@ add_qtc_plugin(QodeAssist QtCreator::Utils QtCreator::CPlusPlus LLMQore - PluginLLMCore ProvidersConfig Agents Skills @@ -84,42 +84,6 @@ add_qtc_plugin(QodeAssist QodeAssisttr.h LLMClientInterface.hpp LLMClientInterface.cpp RefactorContextHelper.hpp - templates/Templates.hpp - templates/CodeLlamaFim.hpp - templates/Ollama.hpp - templates/Claude.hpp - templates/OpenAI.hpp - templates/MistralAI.hpp - templates/StarCoder2Fim.hpp - templates/Qwen25CoderFIM.hpp - templates/OpenAICompatible.hpp - templates/Llama3.hpp - templates/ChatML.hpp - templates/Alpaca.hpp - templates/Llama2.hpp - templates/CodeLlamaQMLFim.hpp - templates/GoogleAI.hpp - templates/LlamaCppFim.hpp - templates/Qwen3CoderFIM.hpp - templates/OpenAIResponses.hpp - providers/Providers.hpp - providers/ProviderUrlUtils.hpp - providers/OllamaProvider.hpp providers/OllamaProvider.cpp - providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp - providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp - providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp - providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp - providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp - providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp - providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp - providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp - providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp - providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp - providers/CodestralProvider.hpp providers/CodestralProvider.cpp - providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp - providers/QwenProvider.hpp providers/QwenProvider.cpp - providers/QwenResponsesProvider.hpp providers/QwenResponsesProvider.cpp - providers/DeepSeekProvider.hpp providers/DeepSeekProvider.cpp QodeAssist.qrc LSPCompletion.hpp LLMSuggestion.hpp LLMSuggestion.cpp diff --git a/ChatView/ChatRootView.cpp b/ChatView/ChatRootView.cpp index 177ae0f..95aa34f 100644 --- a/ChatView/ChatRootView.cpp +++ b/ChatView/ChatRootView.cpp @@ -32,6 +32,7 @@ #include #include "ChatAgentController.hpp" +#include "AgentRole.hpp" #include "ChatAssistantSettings.hpp" #include "ChatCompressor.hpp" #include "ChatHistoryStore.hpp" @@ -393,6 +394,43 @@ void ChatRootView::setCurrentChatAgent(const QString &name) m_agentController->setCurrentAgent(name); } +QStringList ChatRootView::availableRoles() const +{ + return m_availableRoles; +} + +QString ChatRootView::currentRole() const +{ + return m_currentRole; +} + +void ChatRootView::setCurrentRole(const QString &roleId) +{ + if (m_currentRole == roleId) + return; + m_currentRole = roleId; + emit currentRoleChanged(); +} + +void ChatRootView::loadAvailableRoles() +{ + QStringList ids; + const QList roles = Settings::AgentRolesManager::loadAllRoles(); + ids.reserve(roles.size()); + for (const auto &r : roles) + ids << r.id; + + if (ids != m_availableRoles) { + m_availableRoles = ids; + emit availableRolesChanged(); + } + + if (!m_availableRoles.isEmpty() && !m_availableRoles.contains(m_currentRole)) + setCurrentRole(m_availableRoles.contains(QStringLiteral("developer")) + ? QStringLiteral("developer") + : m_availableRoles.first()); +} + QVariantList ChatRootView::searchSkills(const QString &query) const { QVariantList results; @@ -501,6 +539,7 @@ void ChatRootView::dispatchSend( m_clientInterface->setSkillsManager(skillsManager()); m_clientInterface->setSessionManager(sessionManager()); m_clientInterface->setActiveAgent(currentChatAgent()); + m_clientInterface->setActiveRole(currentRole()); m_clientInterface->sendMessage(message, attachments, linkedFiles); m_fileManager->clearIntermediateStorage(); diff --git a/ChatView/ChatRootView.hpp b/ChatView/ChatRootView.hpp index 079cbae..cad0245 100644 --- a/ChatView/ChatRootView.hpp +++ b/ChatView/ChatRootView.hpp @@ -59,6 +59,8 @@ class ChatRootView : public QQuickItem Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL) Q_PROPERTY(QStringList availableChatAgents READ availableChatAgents NOTIFY availableChatAgentsChanged FINAL) Q_PROPERTY(QString currentChatAgent READ currentChatAgent WRITE setCurrentChatAgent NOTIFY currentChatAgentChanged FINAL) + Q_PROPERTY(QStringList availableRoles READ availableRoles NOTIFY availableRolesChanged FINAL) + Q_PROPERTY(QString currentRole READ currentRole WRITE setCurrentRole NOTIFY currentRoleChanged FINAL) Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL) Q_PROPERTY(bool isInEditor READ isInEditor NOTIFY isInEditorChanged FINAL) Q_PROPERTY(QString chatTitle READ chatTitle NOTIFY chatTitleChanged FINAL) @@ -155,6 +157,11 @@ public: QString currentChatAgent() const; void setCurrentChatAgent(const QString &name); + Q_INVOKABLE void loadAvailableRoles(); + QStringList availableRoles() const; + QString currentRole() const; + void setCurrentRole(const QString &roleId); + int currentMessageTotalEdits() const; int currentMessageAppliedEdits() const; int currentMessagePendingEdits() const; @@ -208,6 +215,8 @@ signals: void availableChatAgentsChanged(); void currentChatAgentChanged(); + void availableRolesChanged(); + void currentRoleChanged(); void isCompressingChanged(); void compressionCompleted(const QString &compressedChatPath); @@ -262,6 +271,9 @@ private: QString m_lastInfoMessage; + QString m_currentRole = QStringLiteral("developer"); + QStringList m_availableRoles; + ChatCompressor *m_chatCompressor; ChatAgentController *m_agentController; FileEditController *m_fileEditController; diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index 568b980..7376a50 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -30,7 +30,10 @@ #include #include +#include #include + +#include #include #include @@ -75,6 +78,11 @@ void ClientInterface::setActiveAgent(const QString &agentName) m_activeAgent = agentName; } +void ClientInterface::setActiveRole(const QString &roleId) +{ + m_activeRoleId = roleId; +} + void ClientInterface::sendMessage( const QString &message, const QList &attachments, @@ -175,9 +183,15 @@ void ClientInterface::sendMessage( return; } - Tools::registerQodeAssistTools(client->tools()); - if (m_skillsManager) - Tools::registerSkillTool(client->tools(), m_skillsManager); + auto *project = ProjectExplorer::ProjectManager::startupProject(); + Templates::ContextRenderer::Bindings bindings; + bindings.projectDir = project ? project->projectDirectory().toFSPathString() : QString(); + bindings.homeDir = QDir::homePath(); + bindings.roleId = m_activeRoleId; + session->setContextBindings(bindings); + + if (m_sessionManager) + m_sessionManager->toolContributors().contribute(client->tools()); client->setMaxToolContinuations(Settings::toolsSettings().maxToolContinuations()); client->setTransferTimeout( static_cast(Settings::generalSettings().requestTimeout() * 1000)); diff --git a/ChatView/ClientInterface.hpp b/ChatView/ClientInterface.hpp index 89592e9..549b09b 100644 --- a/ChatView/ClientInterface.hpp +++ b/ChatView/ClientInterface.hpp @@ -37,6 +37,7 @@ public: void setSkillsManager(Skills::SkillsManager *skillsManager); void setSessionManager(SessionManager *sessionManager); void setActiveAgent(const QString &agentName); + void setActiveRole(const QString &roleId); void sendMessage( const QString &message, @@ -98,6 +99,7 @@ private: Skills::SkillsManager *m_skillsManager = nullptr; QPointer m_sessionManager; QString m_activeAgent; + QString m_activeRoleId; QString m_chatFilePath; QHash m_activeRequests; diff --git a/ChatView/qml/RootItem.qml b/ChatView/qml/RootItem.qml index 4bf5d5d..b65ca11 100644 --- a/ChatView/qml/RootItem.qml +++ b/ChatView/qml/RootItem.qml @@ -152,6 +152,19 @@ ChatRootView { root.loadAvailableChatAgents() } } + roleSelector { + model: root.availableRoles + displayText: root.currentRole + onActivated: function(index) { + root.currentRole = root.availableRoles[index] + } + + Component.onCompleted: root.loadAvailableRoles() + + popup.onAboutToShow: { + root.loadAvailableRoles() + } + } } RowLayout { diff --git a/ChatView/qml/controls/TopBar.qml b/ChatView/qml/controls/TopBar.qml index 1b64d7c..1b17383 100644 --- a/ChatView/qml/controls/TopBar.qml +++ b/ChatView/qml/controls/TopBar.qml @@ -25,6 +25,7 @@ Rectangle { property alias contextButton: contextButtonId property alias settingsButton: settingsButtonId property alias agentSelector: agentSelectorId + property alias roleSelector: roleSelectorId property alias relocateTooltip: relocateTooltipId color: palette.window.hslLightness > 0.5 ? @@ -141,7 +142,22 @@ Rectangle { QoAToolTip { visible: agentSelectorId.hovered delay: 250 - text: qsTr("Select chat agent (provider, model and role come from the agent)") + text: qsTr("Select chat agent (provider and model come from the agent)") + } + } + + QoAComboBox { + id: roleSelectorId + + implicitHeight: 25 + + model: [] + currentIndex: 0 + + QoAToolTip { + visible: roleSelectorId.hovered + delay: 250 + text: qsTr("Select the role (system prompt) for the chat") } } } diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp index 38b9515..17ad1cb 100644 --- a/LLMClientInterface.cpp +++ b/LLMClientInterface.cpp @@ -334,14 +334,7 @@ Templates::ContextData LLMClientInterface::prepareContext( Context::DocumentContextReader reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath); - const PluginLLMCore::ContextData legacy - = reader.prepareContext(lineNumber, cursorPosition, m_completeSettings); - - Templates::ContextData context; - context.prefix = legacy.prefix; - context.suffix = legacy.suffix; - context.fileContext = legacy.fileContext; - return context; + return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings); } Context::ContextManager *LLMClientInterface::contextManager() const diff --git a/QodeAssistClient.hpp b/QodeAssistClient.hpp index de2e688..e8cbc1b 100644 --- a/QodeAssistClient.hpp +++ b/QodeAssistClient.hpp @@ -17,8 +17,6 @@ #include "widgets/EditorChatButtonHandler.hpp" #include "widgets/RefactorWidgetHandler.hpp" #include -#include -#include namespace QodeAssist { diff --git a/QuickRefactorHandler.cpp b/QuickRefactorHandler.cpp index a8cb0c6..45d354f 100644 --- a/QuickRefactorHandler.cpp +++ b/QuickRefactorHandler.cpp @@ -21,7 +21,7 @@ #include #include #include -#include +#include #include #include @@ -378,7 +378,7 @@ void QuickRefactorHandler::handleLLMResponse( if (isComplete) { m_isRefactoringInProgress = false; - QString cleanedResponse = PluginLLMCore::ResponseCleaner::clean(response); + QString cleanedResponse = ResponseCleaner::clean(response); RefactorResult result; result.newText = cleanedResponse; diff --git a/context/CMakeLists.txt b/context/CMakeLists.txt index 0ef3d9f..3c94d99 100644 --- a/context/CMakeLists.txt +++ b/context/CMakeLists.txt @@ -21,7 +21,7 @@ target_link_libraries(Context QtCreator::Utils QtCreator::ProjectExplorer PRIVATE - PluginLLMCore + Common QodeAssistSettings ) diff --git a/context/DocumentContextReader.cpp b/context/DocumentContextReader.cpp index cc4db07..d84f877 100644 --- a/context/DocumentContextReader.cpp +++ b/context/DocumentContextReader.cpp @@ -254,7 +254,7 @@ CopyrightInfo DocumentContextReader::copyrightInfo() const return m_copyrightInfo; } -PluginLLMCore::ContextData DocumentContextReader::prepareContext( +Templates::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 8361eef..2c3f0f6 100644 --- a/context/DocumentContextReader.hpp +++ b/context/DocumentContextReader.hpp @@ -7,7 +7,7 @@ #include #include -#include +#include #include namespace QodeAssist::Context { @@ -58,7 +58,7 @@ public: CopyrightInfo copyrightInfo() const; - PluginLLMCore::ContextData prepareContext( + Templates::ContextData prepareContext( int lineNumber, int cursorPosition, const Settings::CodeCompletionSettings &settings) const; private: diff --git a/docs/agent-templates-design.md b/docs/agent-templates-design.md index 18aa07f..7f9be5c 100644 --- a/docs/agent-templates-design.md +++ b/docs/agent-templates-design.md @@ -43,18 +43,26 @@ exactly like the curl body. No runtime toggles: thinking / tools / streaming are **fixed per agent**. A thinking agent literally carries the `thinking` fields; a non-thinking variant is a separate -file. There is no `{% if thinking %}` and no `thinkingEnabled` flag threaded into -rendering. `system` uses `{% if existsIn(ctx, "system_prompt") %}` only because that -is about *presence of data*, not a mode toggle. +file. There is no `{% if thinking %}` in the body. `system` uses +`{% if existsIn(ctx, "system_prompt") %}` only because that is about *presence of +data*, not a mode toggle. `enable_thinking` / `enable_tools` are **capability hints** +(used for UI badges and to decide tool-definition injection) — the body is the source +of truth for what is actually sent, so a thinking agent's body must carry the thinking +fields regardless of the flag. Outside the body: -- `model` — supplied by the **client** from its own settings; never in the profile. - Google embeds the model in the URL, so its `endpoint` uses a `${MODEL}` placeholder - the client resolves (same substitution style as `${PROJECT_DIR}` / `${HOME}`). +- `model` — the TOML `model` is the **default**; a per-agent override chosen in + QodeAssist settings wins. Overrides are stored in `agent_models.json` + (agentName → model) and applied by `AgentFactory` when it builds the agent + (`AgentFactory::effectiveModel`/`setModelOverride`); `Session` still seeds the + payload `model` from the resolved `cfg.model`. URL-model providers (Google) put a + `${MODEL}` placeholder in `endpoint`; `Session` substitutes the resolved model into + the endpoint before sending (same substitution style as `${PROJECT_DIR}`/`${HOME}`), + so the override drives the URL too. - `tools` — injected by the **provider** when `enable_tools` is set (tool definitions are dynamic, from `ToolsManager`; they can't be authored in TOML). - `stream` — always on. Literal `"stream": true` in the body for OpenAI / Claude / - Mistral; encoded in the `endpoint` URL for Google. + Mistral / Responses / Ollama; encoded in the `endpoint` URL for Google. ### 2. `include` re-enabled as whitelisted partials @@ -71,19 +79,20 @@ A missing/typo'd partial is a **load-time** error. ### 3. `extends` shares config down a hierarchy `extends` already exists (`resolveExtends` + `deepMerge` + `abstract`/`hidden`); it -keeps doing what it does, now over the structured `[body]` too. Typical 2–3 levels: +keeps doing what it does, now over the structured `[body]` too. A flat 2 levels — each +provider base sets `system_prompt = """{{ agent_role("developer") }}"""` (the role text +comes from the role JSON via the `agent_role` callback; see below). No shared root base: ``` -chat_base (abstract) → system_prompt (shared by all) - ├─ openai_base (abstract) → provider/endpoint/enable_tools + [body] - │ ├─ openai_chat → name - │ ├─ mistral_chat → name, provider, endpoint - │ └─ mistral_reasoning → + [body].reasoning_effort - ├─ anthropic_base (abstract) → provider/endpoint/thinking + [body] - │ ├─ claude_chat → name - │ └─ claude_sonnet → + [body.output_config].effort - └─ google_base (abstract) → provider/endpoint + [body] - └─ gemini_chat → name +openai_base (abstract) → system_prompt + provider/endpoint/enable_tools + [body] + ├─ openai_chat → name + ├─ mistral_chat → name, provider, endpoint + └─ mistral_reasoning → + enable_thinking +anthropic_base (abstract) → system_prompt + provider/endpoint + [body] + ├─ claude_chat → name + └─ claude_sonnet → + [body] thinking / output_config +google_base (abstract) → system_prompt + provider + [body] + └─ gemini_chat → endpoint (${MODEL}) + [body.generationConfig] thinkingConfig ``` Notes: @@ -98,21 +107,33 @@ Notes: by `model` become one agent (the client picks the model). A separate file is only needed when the body genuinely differs (effort, no-thinking, …). -### `role` + `context` merged into `system_prompt` +### System prompt — a composable template with building blocks -The old `role` (static) and `context` (jinja, reads files) are two layers of the -same system prompt (`SystemPromptBuilder` layers `agent.role` / `agent.context`). -Merge into one `system_prompt` field, always rendered through `ContextRenderer` -(static text passes through; dynamic parts use `{% %}`), e.g. README via -`file_exists` instead of the `set readme / if length` dance. `Session` collapses the -two layers into one rendered layer. +The old `role` (static text) and `context` (jinja) layers collapse into one +`agent.system` layer in `Session`, rendered through `ContextRenderer`. The agent's +`system_prompt` field IS that template, and the user edits it (in the profile) to +compose the prompt from building-block callbacks: + +- `{{ agent_role("") }}` — insert a role's text (Developer/Reviewer/Researcher…). + Implemented as a `ContextRenderer` callback (`registerAgentRole`) that reads + `userResourcePath("qodeassist/agent_roles/.json")["systemPrompt"]`. Returns "" if + missing. Lives in `sources/agents` (no dependency on `settings/`), so it works in the + plugin and bench. The role text lives once in the role JSON (managed by the settings + Roles UI); the chat bases just carry `system_prompt = """{{ agent_role("developer") }}"""`. +- `{{ read_file("...") }}` / `file_exists` / `${PROJECT_DIR}` / `${HOME}` — existing + `ContextRenderer` helpers, composable in the same template. + +So a profile can do `system_prompt = """{{ agent_role("developer") }}\n\n{{ read_file("…") }}"""`. +`qodeassist.cpp` calls `AgentRolesManager::ensureDefaultRoles()` at startup so the default +role JSONs exist before agents load. There is NO per-agent settings override — the edit +point is the profile's `system_prompt`. Code-completion/FIM agents set no `system_prompt`. ## Worked examples OpenAI base: ```toml -extends = "Chat Base" abstract = true +system_prompt = """{{ agent_role("developer") }}""" provider_instance = "OpenAI (Chat Completions)" endpoint = "/chat/completions" enable_tools = true @@ -140,8 +161,8 @@ reasoning_effort = "medium" Claude base (literally the curl body): ```toml -extends = "Chat Base" abstract = true +system_prompt = """{{ agent_role("developer") }}""" provider_instance = "Claude" endpoint = "/v1/messages" enable_thinking = true @@ -170,8 +191,8 @@ effort = "medium" Google base (`${MODEL}` in endpoint; streaming in the URL): ```toml -extends = "Chat Base" abstract = true +system_prompt = """{{ agent_role("developer") }}""" provider_instance = "Google AI" endpoint = "/models/${MODEL}:streamGenerateContent?alt=sse" enable_thinking = true @@ -337,10 +358,16 @@ In `AgentConfig` / `AgentLoader`: as JSON (render once against a synthetic context with tool_use / tool_result / image). Catches breakage at startup, not mid-conversation. -In the client / provider layer: -- The client sets `model` from its settings (and resolves `${MODEL}` in the - endpoint); `Session` no longer seeds the payload with `cfg.model`. -- The provider keeps injecting `tools` when `enable_tools` is set. +Model selection (per-agent override): +- `AgentFactory` owns an agentName → model map loaded from `agent_models.json` + (`loadModelOverrides`/`saveModelOverrides`). `create()`/`createFromFile()` apply the + override into the built `AgentConfig`; `effectiveModel()` exposes the resolved value; + `setModelOverride()` persists. The settings UI (`AgentDetailPane`) edits it via an + editable Model field; list/roster widgets display `effectiveModel`. +- `Session` substitutes `${MODEL}` in `cfg.endpoint` with the resolved model before + `sendRequest` (covers Google, whose model lives in the URL), and still seeds the + payload `model` from `cfg.model`. The provider keeps injecting `tools` when + `enable_tools` is set. In `Session`: - Collapse the `agent.role` + `agent.context` system-prompt layers into one rendered @@ -353,6 +380,7 @@ In `Session`: 2. `[body]`-table model in `JsonPromptTemplate` + loader; delete sampling/thinking/`applySampling`; drop `thinkingEnabled`. 3. `system_prompt` merge in loader + `Session`. -4. `model` from client (+ `${MODEL}` endpoint substitution); convert bundled agents - to the base/partials/`extends` layout. +4. Per-agent model override in `AgentFactory` (`agent_models.json`) + `${MODEL}` + endpoint substitution in `Session`; editable Model field in settings; convert + bundled agents to the base/partials/`extends` layout. 5. Load-time validation (partial resolves, body parses). diff --git a/mcp/McpClientsManager.cpp b/mcp/McpClientsManager.cpp index 0c9975a..88dd99b 100644 --- a/mcp/McpClientsManager.cpp +++ b/mcp/McpClientsManager.cpp @@ -15,9 +15,8 @@ #include #include +#include #include -#include -#include #include namespace QodeAssist::Mcp { @@ -176,18 +175,14 @@ QList McpClientsManager::connections() const return m_connections; } -QList McpClientsManager::toolsCapableProviders() const +void McpClientsManager::registerToolsOn(::LLMQore::ToolsManager *tools) const { - QList out; - auto &pm = PluginLLMCore::ProvidersManager::instance(); - for (const QString &name : pm.providersNames()) { - auto *p = pm.getProviderByName(name); - if (!p) - continue; - if (p->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)) - out.append(p); + if (!tools) + return; + for (auto *c : m_connections) { + if (c) + c->registerToolsOn(tools); } - return out; } QJsonObject McpClientsManager::builtinServers() @@ -319,8 +314,6 @@ void McpClientsManager::loadFromDisk() newConfigs.append(McpServerConfig::fromJson(it.key(), it.value().toObject())); } - const auto providers = toolsCapableProviders(); - const bool masterEnabled = Settings::mcpSettings().enableMcpClients(); QList keep; @@ -350,7 +343,6 @@ void McpClientsManager::loadFromDisk() existing->deleteLater(); } c = new McpServerConnection(cfg, this); - c->setProviders(providers); connect( c, &McpServerConnection::stateChanged, diff --git a/mcp/McpClientsManager.hpp b/mcp/McpClientsManager.hpp index 31aa079..4f32da5 100644 --- a/mcp/McpClientsManager.hpp +++ b/mcp/McpClientsManager.hpp @@ -35,6 +35,8 @@ public: bool removeServer(const QString &name); void reload(); + void registerToolsOn(::LLMQore::ToolsManager *tools) const; + signals: void serversChanged(); void writeFailed(const QString &reason); @@ -50,7 +52,6 @@ private: void setupWatcher(); void updateWatchedPaths(); - QList toolsCapableProviders() const; static QJsonObject builtinServers(); QJsonObject readRoot() const; bool writeRoot(const QJsonObject &root); diff --git a/mcp/McpServerConnection.cpp b/mcp/McpServerConnection.cpp index 736cad7..766e8de 100644 --- a/mcp/McpServerConnection.cpp +++ b/mcp/McpServerConnection.cpp @@ -23,7 +23,6 @@ #include #include -#include #include namespace QodeAssist::Mcp { @@ -35,13 +34,6 @@ QString transportToString(McpTransportKind k) return k == McpTransportKind::Http ? QStringLiteral("http") : QStringLiteral("stdio"); } -bool providerSupportsTools(PluginLLMCore::Provider *p) -{ - if (!p) - return false; - return p->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools); -} - } // namespace McpServerConfig McpServerConfig::fromJson(const QString &name, const QJsonObject &obj) @@ -133,15 +125,6 @@ McpServerConnection::~McpServerConnection() disconnectFromServer(); } -void McpServerConnection::setProviders(const QList &providers) -{ - m_providers.clear(); - for (auto *p : providers) { - if (providerSupportsTools(p)) - m_providers.append(p); - } -} - ::LLMQore::Mcp::McpTransport *McpServerConnection::createTransport() { if (m_config.transport == McpTransportKind::Http) { @@ -293,40 +276,20 @@ void McpServerConnection::fetchAndRegisterTools() [this](const QList<::LLMQore::Mcp::ToolInfo> &tools) { if (m_listToolsWatchdog) m_listToolsWatchdog->stop(); - if (m_providers.isEmpty()) { - LOG_MESSAGE(QString("MCP client [%1]: no tools-capable providers to " - "register %2 tools into") - .arg(m_config.name) - .arg(tools.size())); - setState( - McpConnectionState::Connected, - QStringLiteral("Connected (%1 tools)").arg(tools.size())); - return; - } + m_tools.clear(); for (const auto &info : tools) { if (info.name.isEmpty()) continue; - m_toolIds.append(info.name); - for (const auto &p : m_providers) { - if (!p) - continue; - auto *tm = p->toolsManager(); - if (!tm) - continue; - auto *remote = new ::LLMQore::Mcp::McpRemoteTool( - m_client.data(), info, tm); - tm->addTool(remote); - } + m_tools.append(info); } - LOG_MESSAGE(QString("MCP client [%1]: registered %2 tools across %3 providers") + LOG_MESSAGE(QString("MCP client [%1]: discovered %2 tools") .arg(m_config.name) - .arg(tools.size()) - .arg(m_providers.size())); + .arg(m_tools.size())); setState( McpConnectionState::Connected, - QStringLiteral("Connected (%1 tools)").arg(tools.size())); + QStringLiteral("Connected (%1 tools)").arg(m_tools.size())); }) .onFailed(this, [this](const std::exception &e) { if (m_listToolsWatchdog) @@ -337,21 +300,19 @@ void McpServerConnection::fetchAndRegisterTools() }); } +void McpServerConnection::registerToolsOn(::LLMQore::ToolsManager *tools) +{ + if (!tools || !m_client || m_state != McpConnectionState::Connected) + return; + for (const auto &info : m_tools) { + auto *remote = new ::LLMQore::Mcp::McpRemoteTool(m_client.data(), info, tools); + tools->addTool(remote); + } +} + void McpServerConnection::unregisterTools() { - if (m_toolIds.isEmpty()) - return; - - for (const auto &p : m_providers) { - if (!p) - continue; - auto *tm = p->toolsManager(); - if (!tm) - continue; - for (const QString &id : m_toolIds) - tm->removeTool(id); - } - m_toolIds.clear(); + m_tools.clear(); } void McpServerConnection::disconnectFromServer() diff --git a/mcp/McpServerConnection.hpp b/mcp/McpServerConnection.hpp index 4b31adc..ba69b05 100644 --- a/mcp/McpServerConnection.hpp +++ b/mcp/McpServerConnection.hpp @@ -14,15 +14,17 @@ #include #include +#include + +namespace LLMQore { +class ToolsManager; +} + namespace LLMQore::Mcp { class McpClient; class McpTransport; } // namespace LLMQore::Mcp -namespace QodeAssist::PluginLLMCore { -class Provider; -} - namespace QodeAssist::Mcp { enum class McpTransportKind { Http, Stdio }; @@ -61,10 +63,17 @@ public: const McpServerConfig &config() const { return m_config; } McpConnectionState state() const { return m_state; } QString statusText() const { return m_statusText; } - int toolCount() const { return m_toolIds.size(); } - QStringList toolNames() const { return m_toolIds; } + int toolCount() const { return m_tools.size(); } + QStringList toolNames() const + { + QStringList names; + names.reserve(m_tools.size()); + for (const auto &tool : m_tools) + names << tool.name; + return names; + } - void setProviders(const QList &providers); + void registerToolsOn(::LLMQore::ToolsManager *tools); void connectToServer(); void disconnectFromServer(); @@ -75,7 +84,6 @@ signals: private: void setState(McpConnectionState state, const QString &text = {}); void fetchAndRegisterTools(); - void registerTools(const QList<::LLMQore::Mcp::McpClient *> & /*unused*/); void unregisterTools(); ::LLMQore::Mcp::McpTransport *createTransport(); @@ -87,8 +95,7 @@ private: QPointer<::LLMQore::Mcp::McpTransport> m_transport; QPointer m_listToolsWatchdog; - QList> m_providers; - QStringList m_toolIds; + QList<::LLMQore::Mcp::ToolInfo> m_tools; }; } // namespace QodeAssist::Mcp diff --git a/pluginllmcore/CMakeLists.txt b/pluginllmcore/CMakeLists.txt deleted file mode 100644 index e2b3c61..0000000 --- a/pluginllmcore/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -add_library(PluginLLMCore STATIC - RequestType.hpp - Provider.hpp Provider.cpp - ProvidersManager.hpp ProvidersManager.cpp - ContextData.hpp - IPromptProvider.hpp - IProviderRegistry.hpp - PromptProviderChat.hpp - PromptProviderFim.hpp - PromptTemplate.hpp - PromptTemplateManager.hpp PromptTemplateManager.cpp - ProviderID.hpp - ResponseCleaner.hpp -) - -target_link_libraries(PluginLLMCore - PUBLIC - Qt::Core - Qt::Network - QtCreator::Core - QtCreator::Utils - QtCreator::ExtensionSystem - LLMQore - PRIVATE - QodeAssistLogger -) - -target_include_directories(PluginLLMCore PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/pluginllmcore/ContextData.hpp b/pluginllmcore/ContextData.hpp deleted file mode 100644 index 11fea58..0000000 --- a/pluginllmcore/ContextData.hpp +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include - -namespace QodeAssist::PluginLLMCore { - -struct ImageAttachment -{ - QString data; // Base64 encoded data or URL - QString mediaType; // e.g., "image/png", "image/jpeg", "image/webp", "image/gif" - bool isUrl = false; // true if data is URL, false if base64 - - bool operator==(const ImageAttachment &) const = default; -}; - -struct ToolCall -{ - QString id; - QString name; - QJsonObject arguments; - - bool operator==(const ToolCall &) const = default; -}; - -struct Message -{ - QString role; - QString content; - QString signature; - bool isThinking = false; - bool isRedacted = false; - std::optional> images; - - QVector toolCalls; - QString toolCallId; - QString toolName; - - // clang-format off - bool operator==(const Message&) const = default; - // clang-format on -}; - -struct FileMetadata -{ - QString filePath; - QString content; - bool operator==(const FileMetadata &) const = default; -}; - -struct ContextData -{ - std::optional systemPrompt; - std::optional prefix; - std::optional suffix; - std::optional fileContext; - std::optional> history; - std::optional> filesMetadata; - - bool operator==(const ContextData &) const = default; -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/IPromptProvider.hpp b/pluginllmcore/IPromptProvider.hpp deleted file mode 100644 index 120e3e1..0000000 --- a/pluginllmcore/IPromptProvider.hpp +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2025 Povilas Kanapickas -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include "PromptTemplate.hpp" -#include - -namespace QodeAssist::PluginLLMCore { - -class IPromptProvider -{ -public: - virtual ~IPromptProvider() = default; - - virtual PromptTemplate *getTemplateByName(const QString &templateName) const = 0; - - virtual QStringList templatesNames() const = 0; - - virtual QStringList getTemplatesForProvider(ProviderID id) const = 0; -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/IProviderRegistry.hpp b/pluginllmcore/IProviderRegistry.hpp deleted file mode 100644 index d5df8d1..0000000 --- a/pluginllmcore/IProviderRegistry.hpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (C) 2025 Povilas Kanapickas -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include "Provider.hpp" - -namespace QodeAssist::PluginLLMCore { - -class IProviderRegistry -{ -public: - virtual ~IProviderRegistry() = default; - - virtual Provider *getProviderByName(const QString &providerName) = 0; - - virtual QStringList providersNames() const = 0; -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/PromptProviderChat.hpp b/pluginllmcore/PromptProviderChat.hpp deleted file mode 100644 index 84a73c7..0000000 --- a/pluginllmcore/PromptProviderChat.hpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2025 Povilas Kanapickas -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include "IPromptProvider.hpp" -#include "PromptTemplate.hpp" -#include "PromptTemplateManager.hpp" - -namespace QodeAssist::PluginLLMCore { - -class PromptProviderChat : public IPromptProvider -{ -public: - explicit PromptProviderChat(PromptTemplateManager &templateManager) - : m_templateManager(templateManager) - {} - - ~PromptProviderChat() = default; - - PromptTemplate *getTemplateByName(const QString &templateName) const override - { - return m_templateManager.getChatTemplateByName(templateName); - } - - QStringList templatesNames() const override { return m_templateManager.chatTemplatesNames(); } - - QStringList getTemplatesForProvider(ProviderID id) const override - { - return m_templateManager.getChatTemplatesForProvider(id); - } - -private: - PromptTemplateManager &m_templateManager; -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/PromptProviderFim.hpp b/pluginllmcore/PromptProviderFim.hpp deleted file mode 100644 index a8c96c6..0000000 --- a/pluginllmcore/PromptProviderFim.hpp +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (C) 2025 Povilas Kanapickas -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include "IPromptProvider.hpp" -#include "PromptTemplateManager.hpp" - -namespace QodeAssist::PluginLLMCore { - -class PromptProviderFim : public IPromptProvider -{ -public: - explicit PromptProviderFim(PromptTemplateManager &templateManager) - : m_templateManager(templateManager) - {} - - ~PromptProviderFim() = default; - - PromptTemplate *getTemplateByName(const QString &templateName) const override - { - return m_templateManager.getFimTemplateByName(templateName); - } - - QStringList templatesNames() const override { return m_templateManager.fimTemplatesNames(); } - - QStringList getTemplatesForProvider(ProviderID id) const override - { - return m_templateManager.getFimTemplatesForProvider(id); - } - -private: - PromptTemplateManager &m_templateManager; -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/PromptTemplate.hpp b/pluginllmcore/PromptTemplate.hpp deleted file mode 100644 index 89e0d33..0000000 --- a/pluginllmcore/PromptTemplate.hpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include - -#include "ContextData.hpp" -#include "ProviderID.hpp" - -namespace QodeAssist::PluginLLMCore { - -enum class TemplateType { Chat, FIM, FIMOnChat }; - -class PromptTemplate -{ -public: - virtual ~PromptTemplate() = default; - virtual TemplateType type() const = 0; - virtual QString name() const = 0; - virtual QStringList stopWords() const = 0; - virtual void prepareRequest(QJsonObject &request, const ContextData &context) const = 0; - virtual QString description() const = 0; - virtual bool isSupportProvider(ProviderID id) const = 0; - - virtual QString endpoint() const { return {}; } - - virtual bool supportsToolHistory() const { return false; } -}; -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/PromptTemplateManager.cpp b/pluginllmcore/PromptTemplateManager.cpp deleted file mode 100644 index 0e314f3..0000000 --- a/pluginllmcore/PromptTemplateManager.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "PromptTemplateManager.hpp" - -#include - -namespace QodeAssist::PluginLLMCore { - -PromptTemplateManager &PromptTemplateManager::instance() -{ - static PromptTemplateManager instance; - return instance; -} - -QStringList PromptTemplateManager::fimTemplatesNames() const -{ - return m_fimTemplates.keys(); -} - -QStringList PromptTemplateManager::chatTemplatesNames() const -{ - return m_chatTemplates.keys(); -} - -QStringList PromptTemplateManager::getFimTemplatesForProvider(ProviderID id) -{ - QStringList templateList; - - for (const auto tmpl : m_fimTemplates) { - if (tmpl->isSupportProvider(id)) { - templateList.append(tmpl->name()); - } - } - - return templateList; -} - -QStringList PromptTemplateManager::getChatTemplatesForProvider(ProviderID id) -{ - QStringList templateList; - - for (const auto tmpl : m_chatTemplates) { - if (tmpl->isSupportProvider(id)) { - templateList.append(tmpl->name()); - } - } - - return templateList; -} - -PromptTemplateManager::~PromptTemplateManager() -{ - qDeleteAll(m_fimTemplates); -} - -PromptTemplate *PromptTemplateManager::getFimTemplateByName(const QString &templateName) -{ - if (!m_fimTemplates.contains(templateName)) { - QMessageBox::warning( - nullptr, - QObject::tr("Template Not Found"), - QObject::tr("Template '%1' was not found or has been updated. Please re-set new one.") - .arg(templateName)); - return m_fimTemplates.first(); - } - return m_fimTemplates[templateName]; -} - -PromptTemplate *PromptTemplateManager::getChatTemplateByName(const QString &templateName) -{ - if (!m_chatTemplates.contains(templateName)) { - QMessageBox::warning( - nullptr, - QObject::tr("Template Not Found"), - QObject::tr("Template '%1' was not found or has been updated. Please re-set new one.") - .arg(templateName)); - return m_chatTemplates.first(); - } - return m_chatTemplates[templateName]; -} - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/PromptTemplateManager.hpp b/pluginllmcore/PromptTemplateManager.hpp deleted file mode 100644 index 3040d83..0000000 --- a/pluginllmcore/PromptTemplateManager.hpp +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -#include "PromptTemplate.hpp" - -namespace QodeAssist::PluginLLMCore { - -class PromptTemplateManager -{ -public: - static PromptTemplateManager &instance(); - ~PromptTemplateManager(); - - template - void registerTemplate() - { - static_assert(std::is_base_of::value, "T must inherit from PromptTemplate"); - T *template_ptr = new T(); - QString name = template_ptr->name(); - m_fimTemplates[name] = template_ptr; - if (template_ptr->type() == TemplateType::Chat) { - m_chatTemplates[name] = template_ptr; - } - } - - PromptTemplate *getFimTemplateByName(const QString &templateName); - PromptTemplate *getChatTemplateByName(const QString &templateName); - - QStringList fimTemplatesNames() const; - QStringList chatTemplatesNames() const; - - QStringList getFimTemplatesForProvider(ProviderID id); - QStringList getChatTemplatesForProvider(ProviderID id); - -private: - PromptTemplateManager() = default; - PromptTemplateManager(const PromptTemplateManager &) = delete; - PromptTemplateManager &operator=(const PromptTemplateManager &) = delete; - - QMap m_fimTemplates; - QMap m_chatTemplates; -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/Provider.cpp b/pluginllmcore/Provider.cpp deleted file mode 100644 index e9be5a1..0000000 --- a/pluginllmcore/Provider.cpp +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "Provider.hpp" - -#include -#include - -#include - -#include - -namespace QodeAssist::PluginLLMCore { - -Provider::Provider(QObject *parent) - : QObject(parent) -{} - -LLMQore::RequestID Provider::sendRequest( - const QUrl &url, const QJsonObject &payload, const QString &endpoint) -{ - auto *c = client(); - - c->setUrl(url.toString()); - c->setApiKey(apiKey()); - - auto requestId = c->sendMessage(payload, endpoint); - - LOG_MESSAGE( - QString("%1: Sending request %2 to %3%4").arg(name(), requestId, url.toString(), endpoint)); - LOG_MESSAGE( - QString("%1: Payload:\n%2") - .arg(name(), QString::fromUtf8(QJsonDocument(payload).toJson(QJsonDocument::Indented)))); - - return requestId; -} - -void Provider::cancelRequest(const LLMQore::RequestID &requestId) -{ - LOG_MESSAGE(QString("%1: Cancelling request %2").arg(name(), requestId)); - client()->cancelRequest(requestId); -} - -::LLMQore::ToolsManager *Provider::toolsManager() const -{ - return client()->tools(); -} - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/Provider.hpp b/pluginllmcore/Provider.hpp deleted file mode 100644 index 5063d57..0000000 --- a/pluginllmcore/Provider.hpp +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include -#include -#include - -#include "ContextData.hpp" -#include "PromptTemplate.hpp" -#include "LLMQore/BaseClient.hpp" -#include "RequestType.hpp" - -namespace LLMQore { -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 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 ::LLMQore::BaseClient *client() const = 0; - virtual QString apiKey() const = 0; - - virtual LLMQore::RequestID sendRequest( - const QUrl &url, const QJsonObject &payload, const QString &endpoint); - void cancelRequest(const LLMQore::RequestID &requestId); - ::LLMQore::ToolsManager *toolsManager() const; -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/ProviderID.hpp b/pluginllmcore/ProviderID.hpp deleted file mode 100644 index 6fefb23..0000000 --- a/pluginllmcore/ProviderID.hpp +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -namespace QodeAssist::PluginLLMCore { - -enum class ProviderID { - Any, - Ollama, - LMStudio, - Claude, - OpenAI, - OpenAICompatible, - OpenAIResponses, - MistralAI, - OpenRouter, - GoogleAI, - LlamaCpp, - Qwen, - DeepSeek -}; -} diff --git a/pluginllmcore/ProvidersManager.cpp b/pluginllmcore/ProvidersManager.cpp deleted file mode 100644 index a2b0898..0000000 --- a/pluginllmcore/ProvidersManager.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "ProvidersManager.hpp" - -namespace QodeAssist::PluginLLMCore { - -ProvidersManager &ProvidersManager::instance() -{ - static ProvidersManager instance; - return instance; -} - -QStringList ProvidersManager::providersNames() const -{ - return m_providers.keys(); -} - -ProvidersManager::~ProvidersManager() -{ - qDeleteAll(m_providers); -} - -Provider *ProvidersManager::getProviderByName(const QString &providerName) -{ - if (!m_providers.contains(providerName)) - return m_providers.first(); - return m_providers[providerName]; -} - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/ProvidersManager.hpp b/pluginllmcore/ProvidersManager.hpp deleted file mode 100644 index 21dced4..0000000 --- a/pluginllmcore/ProvidersManager.hpp +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include "IProviderRegistry.hpp" -#include - -namespace QodeAssist::PluginLLMCore { - -class ProvidersManager : public IProviderRegistry -{ -public: - static ProvidersManager &instance(); - ~ProvidersManager(); - - template - void registerProvider() - { - static_assert(std::is_base_of::value, "T must inherit from Provider"); - T *provider = new T(); - QString name = provider->name(); - m_providers[name] = provider; - } - - Provider *getProviderByName(const QString &providerName) override; - - QStringList providersNames() const override; - -private: - ProvidersManager() = default; - ProvidersManager(const ProvidersManager &) = delete; - ProvidersManager &operator=(const ProvidersManager &) = delete; - - QMap m_providers; -}; - -} // namespace QodeAssist::PluginLLMCore diff --git a/pluginllmcore/RequestType.hpp b/pluginllmcore/RequestType.hpp deleted file mode 100644 index 5dffc48..0000000 --- a/pluginllmcore/RequestType.hpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include - -#pragma once - -namespace QodeAssist::PluginLLMCore { - -enum RequestType { CodeCompletion, Chat, Embedding, QuickRefactoring }; - -} diff --git a/providers/ClaudeCacheControl.hpp b/providers/ClaudeCacheControl.hpp deleted file mode 100644 index f116375..0000000 --- a/providers/ClaudeCacheControl.hpp +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include -#include -#include - -namespace QodeAssist::Providers::ClaudeCacheControl { - -inline QJsonObject buildBreakpoint(bool extendedTtl) -{ - QJsonObject cacheControl{{"type", "ephemeral"}}; - if (extendedTtl) - cacheControl["ttl"] = "1h"; - return cacheControl; -} - -inline void markLastBlock(QJsonArray &blocks, const QJsonObject &cacheControl) -{ - if (blocks.isEmpty()) - return; - QJsonObject last = blocks.last().toObject(); - last["cache_control"] = cacheControl; - blocks.replace(blocks.size() - 1, last); -} - -inline void applyToSystem(QJsonObject &request, const QJsonObject &cacheControl) -{ - if (!request.contains("system")) - return; - - const QJsonValue sys = request.value("system"); - if (sys.isString()) { - const QString text = sys.toString(); - if (!text.isEmpty()) { - request["system"] = QJsonArray{QJsonObject{ - {"type", "text"}, {"text", text}, {"cache_control", cacheControl}}}; - } - } else if (sys.isArray()) { - QJsonArray blocks = sys.toArray(); - markLastBlock(blocks, cacheControl); - request["system"] = blocks; - } -} - -inline void applyToTools(QJsonObject &request, const QJsonObject &cacheControl) -{ - if (!request.contains("tools")) - return; - QJsonArray tools = request.value("tools").toArray(); - markLastBlock(tools, cacheControl); - request["tools"] = tools; -} - -inline void applyToHistory(QJsonObject &request, const QJsonObject &cacheControl) -{ - if (!request.contains("messages")) - return; - QJsonArray messages = request.value("messages").toArray(); - if (messages.size() < 2) - return; - - const int idx = messages.size() - 2; - QJsonObject msg = messages[idx].toObject(); - const QJsonValue content = msg.value("content"); - if (content.isString()) { - msg["content"] = QJsonArray{QJsonObject{ - {"type", "text"}, {"text", content.toString()}, {"cache_control", cacheControl}}}; - } else if (content.isArray()) { - QJsonArray blocks = content.toArray(); - markLastBlock(blocks, cacheControl); - msg["content"] = blocks; - } - messages.replace(idx, msg); - request["messages"] = messages; -} - -inline void apply(QJsonObject &request, bool extendedTtl) -{ - const QJsonObject cacheControl = buildBreakpoint(extendedTtl); - applyToSystem(request, cacheControl); - applyToTools(request, cacheControl); - applyToHistory(request, cacheControl); -} - -} // namespace QodeAssist::Providers::ClaudeCacheControl diff --git a/providers/ClaudeProvider.cpp b/providers/ClaudeProvider.cpp deleted file mode 100644 index 6cf87b0..0000000 --- a/providers/ClaudeProvider.cpp +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "ClaudeProvider.hpp" - -#include -#include -#include - -#include - -#include "ClaudeCacheControl.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 { - -ClaudeProvider::ClaudeProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::ClaudeClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString ClaudeProvider::name() const -{ - return "Claude"; -} - -QString ClaudeProvider::apiKey() const -{ - return Settings::providerSettings().claudeApiKey(); -} - -QString ClaudeProvider::url() const -{ - return "https://api.anthropic.com"; -} - -void ClaudeProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - request["max_tokens"] = settings.maxTokens(); - if (settings.useTopP()) - request["top_p"] = settings.topP(); - if (settings.useTopK()) - request["top_k"] = settings.topK(); - request["stream"] = true; - }; - - auto applyThinkingMode = [&request](const auto &settings) { - const QString model = request.value("model").toString().toLower(); - const bool useAdaptiveThinking = model.contains("opus-4-8") || model.contains("opus-4-7") - || model.contains("opus-4-6") || model.contains("sonnet-4-6"); - - QJsonObject thinkingObj; - if (useAdaptiveThinking) { - thinkingObj["type"] = "adaptive"; - - const int budget = settings.thinkingBudgetTokens(); - QString effort = "high"; - if (budget < 8000) - effort = "low"; - else if (budget < 24000) - effort = "medium"; - - QJsonObject outputConfig; - outputConfig["effort"] = effort; - request["output_config"] = outputConfig; - } else { - thinkingObj["type"] = "enabled"; - thinkingObj["budget_tokens"] = settings.thinkingBudgetTokens(); - } - request["thinking"] = thinkingObj; - request["max_tokens"] = settings.thinkingMaxTokens(); - request["temperature"] = 1.0; - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - const auto &qrSettings = Settings::quickRefactorSettings(); - applyModelParams(qrSettings); - - if (isThinkingEnabled) { - applyThinkingMode(qrSettings); - } else { - request["temperature"] = qrSettings.temperature(); - } - } else { - const auto &chatSettings = Settings::chatAssistantSettings(); - applyModelParams(chatSettings); - - if (isThinkingEnabled) { - applyThinkingMode(chatSettings); - } else { - request["temperature"] = chatSettings.temperature(); - } - } - - if (isToolsEnabled) { - auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE(QString("Added %1 tools to Claude request").arg(toolsDefinitions.size())); - } - } - - const auto &ps = Settings::providerSettings(); - const bool cachingOn = ps.claudeEnablePromptCaching() - && type != PluginLLMCore::RequestType::CodeCompletion; - m_client->setUseExtendedCacheTTL(cachingOn && ps.claudeUseExtendedCacheTTL()); - if (cachingOn) { - ClaudeCacheControl::apply(request, ps.claudeUseExtendedCacheTTL()); - } -} - -QFuture> ClaudeProvider::getInstalledModels(const QString &baseUrl) -{ - m_client->setUrl(baseUrl); - m_client->setApiKey(apiKey()); - return m_client->listModels(); -} - -PluginLLMCore::ProviderID ClaudeProvider::providerID() const -{ - return PluginLLMCore::ProviderID::Claude; -} - -PluginLLMCore::ProviderCapabilities ClaudeProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking - | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::ModelListing; -} - -::LLMQore::BaseClient *ClaudeProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/ClaudeProvider.hpp b/providers/ClaudeProvider.hpp deleted file mode 100644 index d0c6015..0000000 --- a/providers/ClaudeProvider.hpp +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include - -namespace QodeAssist::Providers { - -class ClaudeProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit ClaudeProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - -private: - ::LLMQore::ClaudeClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/CodestralProvider.cpp b/providers/CodestralProvider.cpp deleted file mode 100644 index 624bcef..0000000 --- a/providers/CodestralProvider.cpp +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "CodestralProvider.hpp" - -#include "settings/ProviderSettings.hpp" - -namespace QodeAssist::Providers { - -CodestralProvider::CodestralProvider(QObject *parent) - : MistralAIProvider(parent) -{} - -QString CodestralProvider::name() const -{ - return "Codestral"; -} - -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 deleted file mode 100644 index 8d5b4f6..0000000 --- a/providers/CodestralProvider.hpp +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (C) 2025-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include "MistralAIProvider.hpp" - -namespace QodeAssist::Providers { - -class CodestralProvider : public MistralAIProvider -{ -public: - explicit CodestralProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - QString apiKey() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/DeepSeekProvider.cpp b/providers/DeepSeekProvider.cpp deleted file mode 100644 index 1bda509..0000000 --- a/providers/DeepSeekProvider.cpp +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "DeepSeekProvider.hpp" - -#include -#include "logger/Logger.hpp" -#include "settings/ChatAssistantSettings.hpp" -#include "settings/CodeCompletionSettings.hpp" -#include "settings/GeneralSettings.hpp" -#include "settings/ProviderSettings.hpp" -#include "settings/QuickRefactorSettings.hpp" -#include "tools/ToolsRegistration.hpp" - -#include -#include -#include - -namespace QodeAssist::Providers { - -DeepSeekProvider::DeepSeekProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::OpenAIClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString DeepSeekProvider::name() const -{ - return "DeepSeek"; -} - -QString DeepSeekProvider::apiKey() const -{ - return Settings::providerSettings().deepSeekApiKey(); -} - -QString DeepSeekProvider::url() const -{ - return "https://api.deepseek.com"; -} - -QFuture> DeepSeekProvider::getInstalledModels(const QString &url) -{ - m_client->setUrl(url); - m_client->setApiKey(apiKey()); - return m_client->listModels(); -} - -PluginLLMCore::ProviderID DeepSeekProvider::providerID() const -{ - return PluginLLMCore::ProviderID::DeepSeek; -} - -PluginLLMCore::ProviderCapabilities DeepSeekProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools - | PluginLLMCore::ProviderCapability::Thinking - | PluginLLMCore::ProviderCapability::ModelListing; -} - -void DeepSeekProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - request["max_tokens"] = settings.maxTokens(); - request["temperature"] = settings.temperature(); - - if (settings.useTopP()) - request["top_p"] = settings.topP(); - if (settings.useFrequencyPenalty()) - request["frequency_penalty"] = settings.frequencyPenalty(); - if (settings.usePresencePenalty()) - request["presence_penalty"] = settings.presencePenalty(); - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - applyModelParams(Settings::quickRefactorSettings()); - } else { - applyModelParams(Settings::chatAssistantSettings()); - } - - if (isToolsEnabled) { - auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE(QString("Added %1 tools to DeepSeek request").arg(toolsDefinitions.size())); - } - } -} - -::LLMQore::BaseClient *DeepSeekProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/DeepSeekProvider.hpp b/providers/DeepSeekProvider.hpp deleted file mode 100644 index ab09232..0000000 --- a/providers/DeepSeekProvider.hpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::Providers { - -class DeepSeekProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit DeepSeekProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - -private: - ::LLMQore::OpenAIClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/GoogleAIProvider.cpp b/providers/GoogleAIProvider.cpp deleted file mode 100644 index d2baad9..0000000 --- a/providers/GoogleAIProvider.cpp +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "GoogleAIProvider.hpp" - -#include - -#include -#include "tools/ToolsRegistration.hpp" -#include -#include -#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" - -namespace QodeAssist::Providers { - -GoogleAIProvider::GoogleAIProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::GoogleAIClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -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"; -} - -void GoogleAIProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - QJsonObject generationConfig; - generationConfig["maxOutputTokens"] = settings.maxTokens(); - generationConfig["temperature"] = settings.temperature(); - - if (settings.useTopP()) - generationConfig["topP"] = settings.topP(); - if (settings.useTopK()) - generationConfig["topK"] = settings.topK(); - - request["generationConfig"] = generationConfig; - }; - - auto applyThinkingMode = [&request](const auto &settings) { - QJsonObject generationConfig; - generationConfig["maxOutputTokens"] = settings.thinkingMaxTokens(); - - if (settings.useTopP()) - generationConfig["topP"] = settings.topP(); - if (settings.useTopK()) - generationConfig["topK"] = settings.topK(); - - generationConfig["temperature"] = 1.0; - - QJsonObject thinkingConfig; - thinkingConfig["includeThoughts"] = true; - int budgetTokens = settings.thinkingBudgetTokens(); - if (budgetTokens != -1) { - thinkingConfig["thinkingBudget"] = budgetTokens; - } - - generationConfig["thinkingConfig"] = thinkingConfig; - request["generationConfig"] = generationConfig; - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - const auto &qrSettings = Settings::quickRefactorSettings(); - - if (isThinkingEnabled) { - applyThinkingMode(qrSettings); - } else { - applyModelParams(qrSettings); - } - } else { - const auto &chatSettings = Settings::chatAssistantSettings(); - - if (isThinkingEnabled) { - applyThinkingMode(chatSettings); - } else { - applyModelParams(chatSettings); - } - } - - if (isToolsEnabled) { - 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())); - } - } -} - -QFuture> GoogleAIProvider::getInstalledModels(const QString &baseUrl) -{ - m_client->setUrl(baseUrl); - m_client->setApiKey(apiKey()); - return m_client->listModels(); -} - -PluginLLMCore::ProviderID GoogleAIProvider::providerID() const -{ - return PluginLLMCore::ProviderID::GoogleAI; -} - -PluginLLMCore::ProviderCapabilities GoogleAIProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking - | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::ModelListing; -} - -LLMQore::RequestID GoogleAIProvider::sendRequest( - const QUrl &url, const QJsonObject &payload, const QString &endpoint) -{ - QJsonObject cleaned = payload; - if (cleaned.contains("model")) { - m_client->setModel(cleaned["model"].toString()); - cleaned.remove("model"); - } - cleaned.remove("stream"); - - return PluginLLMCore::Provider::sendRequest(url, cleaned, endpoint); -} - -::LLMQore::BaseClient *GoogleAIProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/GoogleAIProvider.hpp b/providers/GoogleAIProvider.hpp deleted file mode 100644 index beb866d..0000000 --- a/providers/GoogleAIProvider.hpp +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include - -namespace QodeAssist::Providers { - -class GoogleAIProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit GoogleAIProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - LLMQore::RequestID sendRequest( - const QUrl &url, const QJsonObject &payload, const QString &endpoint) override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - -private: - ::LLMQore::GoogleAIClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/LMStudioProvider.cpp b/providers/LMStudioProvider.cpp deleted file mode 100644 index 912ef18..0000000 --- a/providers/LMStudioProvider.cpp +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "LMStudioProvider.hpp" - -#include - -#include "providers/ProviderUrlUtils.hpp" -#include "tools/ToolsRegistration.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 -#include -#include - -namespace QodeAssist::Providers { - -LMStudioProvider::LMStudioProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::OpenAIClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString LMStudioProvider::name() const -{ - return "LM Studio (Chat Completions)"; -} - -QString LMStudioProvider::apiKey() const -{ - return {}; -} - -QString LMStudioProvider::url() const -{ - return "http://localhost:1234"; -} - -QFuture> LMStudioProvider::getInstalledModels(const QString &url) -{ - m_client->setUrl(ensureOpenAIV1Base(url)); - m_client->setApiKey(apiKey()); - return m_client->listModels(); -} - -PluginLLMCore::ProviderID LMStudioProvider::providerID() const -{ - return PluginLLMCore::ProviderID::LMStudio; -} - -PluginLLMCore::ProviderCapabilities LMStudioProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::ModelListing - | PluginLLMCore::ProviderCapability::Thinking; -} - -void LMStudioProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - request["max_tokens"] = settings.maxTokens(); - request["temperature"] = settings.temperature(); - - if (settings.useTopP()) - request["top_p"] = settings.topP(); - if (settings.useTopK()) - request["top_k"] = settings.topK(); - if (settings.useFrequencyPenalty()) - request["frequency_penalty"] = settings.frequencyPenalty(); - if (settings.usePresencePenalty()) - request["presence_penalty"] = settings.presencePenalty(); - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - applyModelParams(Settings::quickRefactorSettings()); - } else { - applyModelParams(Settings::chatAssistantSettings()); - } - - if (isToolsEnabled) { - auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE(QString("Added %1 tools to LMStudio request").arg(toolsDefinitions.size())); - } - } -} - -LLMQore::RequestID LMStudioProvider::sendRequest( - const QUrl &url, const QJsonObject &payload, const QString &endpoint) -{ - return PluginLLMCore::Provider::sendRequest( - QUrl(ensureOpenAIV1Base(url.toString())), payload, endpoint); -} - -::LLMQore::BaseClient *LMStudioProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/LMStudioProvider.hpp b/providers/LMStudioProvider.hpp deleted file mode 100644 index 197250b..0000000 --- a/providers/LMStudioProvider.hpp +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::Providers { - -class LMStudioProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit LMStudioProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - - LLMQore::RequestID sendRequest( - const QUrl &url, const QJsonObject &payload, const QString &endpoint) override; - -private: - ::LLMQore::OpenAIClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/LMStudioResponsesProvider.cpp b/providers/LMStudioResponsesProvider.cpp deleted file mode 100644 index 0569f04..0000000 --- a/providers/LMStudioResponsesProvider.cpp +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "LMStudioResponsesProvider.hpp" - -#include - -#include "logger/Logger.hpp" -#include "providers/ProviderUrlUtils.hpp" -#include "settings/ChatAssistantSettings.hpp" -#include "settings/CodeCompletionSettings.hpp" -#include "settings/GeneralSettings.hpp" -#include "settings/ProviderSettings.hpp" -#include "settings/QuickRefactorSettings.hpp" -#include "tools/ToolsRegistration.hpp" - -#include -#include -#include - -namespace QodeAssist::Providers { - -LMStudioResponsesProvider::LMStudioResponsesProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::OpenAIResponsesClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString LMStudioResponsesProvider::name() const -{ - return "LM Studio (Responses API)"; -} - -QString LMStudioResponsesProvider::apiKey() const -{ - return {}; -} - -QString LMStudioResponsesProvider::url() const -{ - return "http://localhost:1234"; -} - -void LMStudioResponsesProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - request["max_output_tokens"] = settings.maxTokens(); - - if (settings.useTopP()) { - request["top_p"] = settings.topP(); - } - }; - - auto applyThinkingMode = [&request](const auto &settings) { - QString effortStr = settings.openAIResponsesReasoningEffort.stringValue().toLower(); - if (effortStr.isEmpty()) { - effortStr = "medium"; - } - - QJsonObject reasoning; - reasoning["effort"] = effortStr; - request["reasoning"] = reasoning; - request["max_output_tokens"] = settings.thinkingMaxTokens(); - request["store"] = true; - - QJsonArray include; - include.append("reasoning.encrypted_content"); - request["include"] = include; - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - const auto &qrSettings = Settings::quickRefactorSettings(); - applyModelParams(qrSettings); - - if (isThinkingEnabled) { - applyThinkingMode(qrSettings); - } - } else { - const auto &chatSettings = Settings::chatAssistantSettings(); - applyModelParams(chatSettings); - - if (isThinkingEnabled) { - applyThinkingMode(chatSettings); - } - } - - if (isToolsEnabled) { - const auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE(QString("Added %1 tools to LM Studio Responses request") - .arg(toolsDefinitions.size())); - } - } - - request["stream"] = true; -} - -QFuture> LMStudioResponsesProvider::getInstalledModels(const QString &baseUrl) -{ - m_client->setUrl(ensureOpenAIV1Base(baseUrl)); - m_client->setApiKey(apiKey()); - return m_client->listModels(); -} - -LLMQore::RequestID LMStudioResponsesProvider::sendRequest( - const QUrl &url, const QJsonObject &payload, const QString &endpoint) -{ - return PluginLLMCore::Provider::sendRequest( - QUrl(ensureOpenAIV1Base(url.toString())), payload, endpoint); -} - -PluginLLMCore::ProviderID LMStudioResponsesProvider::providerID() const -{ - return PluginLLMCore::ProviderID::OpenAIResponses; -} - -PluginLLMCore::ProviderCapabilities LMStudioResponsesProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking - | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::ModelListing; -} - -::LLMQore::BaseClient *LMStudioResponsesProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/LMStudioResponsesProvider.hpp b/providers/LMStudioResponsesProvider.hpp deleted file mode 100644 index c2b47e4..0000000 --- a/providers/LMStudioResponsesProvider.hpp +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::Providers { - -class LMStudioResponsesProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit LMStudioResponsesProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - - LLMQore::RequestID sendRequest( - const QUrl &url, const QJsonObject &payload, const QString &endpoint) override; - -private: - ::LLMQore::OpenAIResponsesClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/LlamaCppProvider.cpp b/providers/LlamaCppProvider.cpp deleted file mode 100644 index c017867..0000000 --- a/providers/LlamaCppProvider.cpp +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "LlamaCppProvider.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 -#include - -namespace QodeAssist::Providers { - -LlamaCppProvider::LlamaCppProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::LlamaCppClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString LlamaCppProvider::name() const -{ - return "llama.cpp"; -} - -QString LlamaCppProvider::apiKey() const -{ - return Settings::providerSettings().llamaCppApiKey(); -} - -QString LlamaCppProvider::url() const -{ - return "http://localhost:8080"; -} - -void LlamaCppProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - request["max_tokens"] = settings.maxTokens(); - request["temperature"] = settings.temperature(); - - if (settings.useTopP()) - request["top_p"] = settings.topP(); - if (settings.useTopK()) - request["top_k"] = settings.topK(); - if (settings.useFrequencyPenalty()) - request["frequency_penalty"] = settings.frequencyPenalty(); - if (settings.usePresencePenalty()) - request["presence_penalty"] = settings.presencePenalty(); - }; - - auto applyThinkingMode = [&request]() { - QJsonObject chatTemplateKwargs = request["chat_template_kwargs"].toObject(); - chatTemplateKwargs["enable_thinking"] = true; - request["chat_template_kwargs"] = chatTemplateKwargs; - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - applyModelParams(Settings::quickRefactorSettings()); - if (isThinkingEnabled) { - applyThinkingMode(); - LOG_MESSAGE(QString("LlamaCppProvider: Thinking mode enabled for QuickRefactoring")); - } - } else { - applyModelParams(Settings::chatAssistantSettings()); - if (isThinkingEnabled) { - applyThinkingMode(); - LOG_MESSAGE(QString("LlamaCppProvider: Thinking mode enabled for Chat")); - } - } - - if (isToolsEnabled) { - 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())); - } - } -} - -QFuture> LlamaCppProvider::getInstalledModels(const QString &baseUrl) -{ - m_client->setUrl(baseUrl); - m_client->setApiKey(Settings::providerSettings().llamaCppApiKey()); - return m_client->listModels(); -} - -PluginLLMCore::ProviderID LlamaCppProvider::providerID() const -{ - return PluginLLMCore::ProviderID::LlamaCpp; -} - -PluginLLMCore::ProviderCapabilities LlamaCppProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking - | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::ModelListing; -} - -::LLMQore::BaseClient *LlamaCppProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/LlamaCppProvider.hpp b/providers/LlamaCppProvider.hpp deleted file mode 100644 index 6f42126..0000000 --- a/providers/LlamaCppProvider.hpp +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include - -namespace QodeAssist::Providers { - -class LlamaCppProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit LlamaCppProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - -private: - ::LLMQore::LlamaCppClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/MistralAIProvider.cpp b/providers/MistralAIProvider.cpp deleted file mode 100644 index 69314d2..0000000 --- a/providers/MistralAIProvider.cpp +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "MistralAIProvider.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 -#include - -namespace QodeAssist::Providers { - -MistralAIProvider::MistralAIProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::MistralClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString MistralAIProvider::name() const -{ - return "Mistral AI"; -} - -QString MistralAIProvider::apiKey() const -{ - return Settings::providerSettings().mistralAiApiKey(); -} - -QString MistralAIProvider::url() const -{ - return "https://api.mistral.ai"; -} - -QFuture> MistralAIProvider::getInstalledModels(const QString &url) -{ - m_client->setUrl(url); - m_client->setApiKey(apiKey()); - return m_client->listModels(); -} - -PluginLLMCore::ProviderID MistralAIProvider::providerID() const -{ - return PluginLLMCore::ProviderID::MistralAI; -} - -PluginLLMCore::ProviderCapabilities MistralAIProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::ModelListing - | PluginLLMCore::ProviderCapability::Thinking; -} - -void MistralAIProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - request["max_tokens"] = settings.maxTokens(); - request["temperature"] = settings.temperature(); - - if (settings.useTopP()) - request["top_p"] = settings.topP(); - if (settings.useTopK()) - request["top_k"] = settings.topK(); - if (settings.useFrequencyPenalty()) - request["frequency_penalty"] = settings.frequencyPenalty(); - if (settings.usePresencePenalty()) - request["presence_penalty"] = settings.presencePenalty(); - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - applyModelParams(Settings::quickRefactorSettings()); - } else { - applyModelParams(Settings::chatAssistantSettings()); - } - - if (isToolsEnabled) { - auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE(QString("Added %1 tools to Mistral request").arg(toolsDefinitions.size())); - } - } -} - -::LLMQore::BaseClient *MistralAIProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/MistralAIProvider.hpp b/providers/MistralAIProvider.hpp deleted file mode 100644 index 71295ad..0000000 --- a/providers/MistralAIProvider.hpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::Providers { - -class MistralAIProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit MistralAIProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - -private: - ::LLMQore::MistralClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/OllamaCompatProvider.cpp b/providers/OllamaCompatProvider.cpp deleted file mode 100644 index 5ed3678..0000000 --- a/providers/OllamaCompatProvider.cpp +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "OllamaCompatProvider.hpp" - -#include - -#include "logger/Logger.hpp" -#include "settings/ChatAssistantSettings.hpp" -#include "settings/CodeCompletionSettings.hpp" -#include "settings/GeneralSettings.hpp" -#include "settings/ProviderSettings.hpp" -#include "settings/QuickRefactorSettings.hpp" -#include "tools/ToolsRegistration.hpp" - -#include -#include -#include - -namespace QodeAssist::Providers { - -OllamaCompatProvider::OllamaCompatProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::OpenAIClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString OllamaCompatProvider::name() const -{ - return "Ollama (OpenAI-compatible)"; -} - -QString OllamaCompatProvider::apiKey() const -{ - return Settings::providerSettings().ollamaBasicAuthApiKey(); -} - -QString OllamaCompatProvider::url() const -{ - return "http://localhost:11434"; -} - -void OllamaCompatProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - request["max_tokens"] = settings.maxTokens(); - request["temperature"] = settings.temperature(); - - if (settings.useTopP()) - request["top_p"] = settings.topP(); - if (settings.useTopK()) - request["top_k"] = settings.topK(); - if (settings.useFrequencyPenalty()) - request["frequency_penalty"] = settings.frequencyPenalty(); - if (settings.usePresencePenalty()) - request["presence_penalty"] = settings.presencePenalty(); - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - applyModelParams(Settings::quickRefactorSettings()); - } else { - applyModelParams(Settings::chatAssistantSettings()); - } - - if (isToolsEnabled) { - auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE( - QString("Added %1 tools to OllamaCompat request").arg(toolsDefinitions.size())); - } - } -} - -QFuture> OllamaCompatProvider::getInstalledModels(const QString &baseUrl) -{ - QString url = baseUrl; - if (!url.endsWith(QStringLiteral("/v1"))) - url += QStringLiteral("/v1"); - m_client->setUrl(url); - m_client->setApiKey(apiKey()); - return m_client->listModels(); -} - -LLMQore::RequestID OllamaCompatProvider::sendRequest( - const QUrl &url, const QJsonObject &payload, const QString &endpoint) -{ - const QString effectiveEndpoint - = endpoint.isEmpty() ? QStringLiteral("/v1/chat/completions") : endpoint; - return PluginLLMCore::Provider::sendRequest(url, payload, effectiveEndpoint); -} - -PluginLLMCore::ProviderID OllamaCompatProvider::providerID() const -{ - return PluginLLMCore::ProviderID::OpenAICompatible; -} - -PluginLLMCore::ProviderCapabilities OllamaCompatProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::ModelListing - | PluginLLMCore::ProviderCapability::Thinking; -} - -::LLMQore::BaseClient *OllamaCompatProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/OllamaCompatProvider.hpp b/providers/OllamaCompatProvider.hpp deleted file mode 100644 index 83b2959..0000000 --- a/providers/OllamaCompatProvider.hpp +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::Providers { - -class OllamaCompatProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit OllamaCompatProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - - LLMQore::RequestID sendRequest( - const QUrl &url, const QJsonObject &payload, const QString &endpoint) override; - -private: - ::LLMQore::OpenAIClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/OllamaProvider.cpp b/providers/OllamaProvider.cpp deleted file mode 100644 index 9c9d6c4..0000000 --- a/providers/OllamaProvider.cpp +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "OllamaProvider.hpp" - -#include - -#include -#include -#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 { - -OllamaProvider::OllamaProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::OllamaClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString OllamaProvider::name() const -{ - return "Ollama (Native)"; -} - -QString OllamaProvider::apiKey() const -{ - return Settings::providerSettings().ollamaBasicAuthApiKey(); -} - -QString OllamaProvider::url() const -{ - return "http://localhost:11434"; -} - -void OllamaProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applySettings = [&request](const auto &settings) { - QJsonObject options; - options["num_predict"] = settings.maxTokens(); - options["temperature"] = settings.temperature(); - options["stop"] = request.take("stop"); - - if (settings.useTopP()) - options["top_p"] = settings.topP(); - if (settings.useTopK()) - options["top_k"] = settings.topK(); - if (settings.useFrequencyPenalty()) - options["frequency_penalty"] = settings.frequencyPenalty(); - if (settings.usePresencePenalty()) - options["presence_penalty"] = settings.presencePenalty(); - - request["options"] = options; - request["keep_alive"] = settings.ollamaLivetime(); - }; - - auto applyThinkingMode = [&request]() { - request["enable_thinking"] = true; - QJsonObject options = request["options"].toObject(); - options["temperature"] = 1.0; - request["options"] = options; - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - const auto &qrSettings = Settings::quickRefactorSettings(); - applySettings(qrSettings); - - if (isThinkingEnabled) { - applyThinkingMode(); - LOG_MESSAGE(QString("OllamaProvider: Thinking mode enabled for QuickRefactoring")); - } - } else { - const auto &chatSettings = Settings::chatAssistantSettings(); - applySettings(chatSettings); - - if (isThinkingEnabled) { - applyThinkingMode(); - LOG_MESSAGE(QString("OllamaProvider: Thinking mode enabled for Chat")); - } - } - - if (isToolsEnabled) { - auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE( - QString("OllamaProvider: Added %1 tools to request").arg(toolsDefinitions.size())); - } - } -} - -QFuture> OllamaProvider::getInstalledModels(const QString &baseUrl) -{ - m_client->setUrl(baseUrl); - m_client->setApiKey(Settings::providerSettings().ollamaBasicAuthApiKey()); - return m_client->listModels(); -} - -PluginLLMCore::ProviderID OllamaProvider::providerID() const -{ - return PluginLLMCore::ProviderID::Ollama; -} - -PluginLLMCore::ProviderCapabilities OllamaProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking - | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::ModelListing; -} - -::LLMQore::BaseClient *OllamaProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/OllamaProvider.hpp b/providers/OllamaProvider.hpp deleted file mode 100644 index a4ae748..0000000 --- a/providers/OllamaProvider.hpp +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include - -namespace QodeAssist::Providers { - -class OllamaProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit OllamaProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - -private: - ::LLMQore::OllamaClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/OpenAICompatProvider.cpp b/providers/OpenAICompatProvider.cpp deleted file mode 100644 index 0b8ba48..0000000 --- a/providers/OpenAICompatProvider.cpp +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "OpenAICompatProvider.hpp" -#include - -#include "tools/ToolsRegistration.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 -#include -#include - -namespace QodeAssist::Providers { - -OpenAICompatProvider::OpenAICompatProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::OpenAIClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString OpenAICompatProvider::name() const -{ - return "OpenAI Compatible"; -} - -QString OpenAICompatProvider::apiKey() const -{ - return Settings::providerSettings().openAiCompatApiKey(); -} - -QString OpenAICompatProvider::url() const -{ - return "http://localhost:1234/v1"; -} - -void OpenAICompatProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - request["max_tokens"] = settings.maxTokens(); - request["temperature"] = settings.temperature(); - - if (settings.useTopP()) - request["top_p"] = settings.topP(); - if (settings.useTopK()) - request["top_k"] = settings.topK(); - if (settings.useFrequencyPenalty()) - request["frequency_penalty"] = settings.frequencyPenalty(); - if (settings.usePresencePenalty()) - request["presence_penalty"] = settings.presencePenalty(); - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - applyModelParams(Settings::quickRefactorSettings()); - } else { - applyModelParams(Settings::chatAssistantSettings()); - } - - if (isToolsEnabled) { - auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE( - QString("Added %1 tools to OpenAICompat request").arg(toolsDefinitions.size())); - } - } -} - -QFuture> OpenAICompatProvider::getInstalledModels(const QString &) -{ - return QtFuture::makeReadyFuture(QList{}); -} - -PluginLLMCore::ProviderID OpenAICompatProvider::providerID() const -{ - return PluginLLMCore::ProviderID::OpenAICompatible; -} - -PluginLLMCore::ProviderCapabilities OpenAICompatProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::Thinking; -} - -::LLMQore::BaseClient *OpenAICompatProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/OpenAICompatProvider.hpp b/providers/OpenAICompatProvider.hpp deleted file mode 100644 index ed68ea0..0000000 --- a/providers/OpenAICompatProvider.hpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::Providers { - -class OpenAICompatProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit OpenAICompatProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - -private: - ::LLMQore::OpenAIClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/OpenAIProvider.cpp b/providers/OpenAIProvider.cpp deleted file mode 100644 index da4371b..0000000 --- a/providers/OpenAIProvider.cpp +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "OpenAIProvider.hpp" - -#include -#include "tools/ToolsRegistration.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 -#include -#include - -namespace QodeAssist::Providers { - -OpenAIProvider::OpenAIProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::OpenAIClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString OpenAIProvider::name() const -{ - return "OpenAI (Chat Completions)"; -} - -QString OpenAIProvider::apiKey() const -{ - return Settings::providerSettings().openAiApiKey(); -} - -QString OpenAIProvider::url() const -{ - return "https://api.openai.com/v1"; -} - -void OpenAIProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - QString model = request.value("model").toString().toLower(); - bool useNewParameter = model.contains("gpt-4o") || model.contains("gpt-4-turbo") - || model.contains("o1-") || model.contains("gpt-5") - || model.startsWith("o1") || model.contains("o3"); - - bool isReasoningModel = model.contains("o1-") || model.contains("gpt-5") - || model.startsWith("o1") || model.contains("o3"); - - if (useNewParameter) { - request["max_completion_tokens"] = settings.maxTokens(); - } else { - request["max_tokens"] = settings.maxTokens(); - } - - if (!isReasoningModel) { - request["temperature"] = settings.temperature(); - - if (settings.useTopP()) - request["top_p"] = settings.topP(); - if (settings.useTopK()) - request["top_k"] = settings.topK(); - - } else { - request["temperature"] = 1.0; - } - - if (settings.useFrequencyPenalty()) - request["frequency_penalty"] = settings.frequencyPenalty(); - if (settings.usePresencePenalty()) - request["presence_penalty"] = settings.presencePenalty(); - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - applyModelParams(Settings::quickRefactorSettings()); - } else { - applyModelParams(Settings::chatAssistantSettings()); - } - - if (isToolsEnabled) { - auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE(QString("Added %1 tools to OpenAI request").arg(toolsDefinitions.size())); - } - } -} - -QFuture> OpenAIProvider::getInstalledModels(const QString &baseUrl) -{ - 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 filtered; - }); -} - -PluginLLMCore::ProviderID OpenAIProvider::providerID() const -{ - return PluginLLMCore::ProviderID::OpenAI; -} - -PluginLLMCore::ProviderCapabilities OpenAIProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::ModelListing - | PluginLLMCore::ProviderCapability::Thinking; -} - -::LLMQore::BaseClient *OpenAIProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/OpenAIProvider.hpp b/providers/OpenAIProvider.hpp deleted file mode 100644 index 570c50d..0000000 --- a/providers/OpenAIProvider.hpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::Providers { - -class OpenAIProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit OpenAIProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - -private: - ::LLMQore::OpenAIClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/OpenAIResponsesProvider.cpp b/providers/OpenAIResponsesProvider.cpp deleted file mode 100644 index 82e6b5e..0000000 --- a/providers/OpenAIResponsesProvider.cpp +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "OpenAIResponsesProvider.hpp" -#include -#include "tools/ToolsRegistration.hpp" - -#include "logger/Logger.hpp" -#include "settings/ChatAssistantSettings.hpp" -#include "settings/CodeCompletionSettings.hpp" -#include "settings/GeneralSettings.hpp" -#include "settings/ProviderSettings.hpp" -#include "settings/QuickRefactorSettings.hpp" - -#include -#include -#include - -namespace QodeAssist::Providers { - -OpenAIResponsesProvider::OpenAIResponsesProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::OpenAIResponsesClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString OpenAIResponsesProvider::name() const -{ - return "OpenAI (Responses API)"; -} - -QString OpenAIResponsesProvider::apiKey() const -{ - return Settings::providerSettings().openAiApiKey(); -} - -QString OpenAIResponsesProvider::url() const -{ - return "https://api.openai.com/v1"; -} - -void OpenAIResponsesProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - request["max_output_tokens"] = settings.maxTokens(); - - if (settings.useTopP()) { - request["top_p"] = settings.topP(); - } - }; - - auto applyThinkingMode = [&request](const auto &settings) { - QString effortStr = settings.openAIResponsesReasoningEffort.stringValue().toLower(); - if (effortStr.isEmpty()) { - effortStr = "medium"; - } - - QJsonObject reasoning; - reasoning["effort"] = effortStr; - request["reasoning"] = reasoning; - request["max_output_tokens"] = settings.thinkingMaxTokens(); - request["store"] = true; - - QJsonArray include; - include.append("reasoning.encrypted_content"); - request["include"] = include; - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - const auto &qrSettings = Settings::quickRefactorSettings(); - applyModelParams(qrSettings); - - if (isThinkingEnabled) { - applyThinkingMode(qrSettings); - } - } else { - const auto &chatSettings = Settings::chatAssistantSettings(); - applyModelParams(chatSettings); - - if (isThinkingEnabled) { - applyThinkingMode(chatSettings); - } - } - - if (isToolsEnabled) { - const auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE(QString("Added %1 tools to OpenAI Responses request") - .arg(toolsDefinitions.size())); - } - } - - request["stream"] = true; -} - -QFuture> OpenAIResponsesProvider::getInstalledModels(const QString &baseUrl) -{ - 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 filtered; - }); -} - -PluginLLMCore::ProviderID OpenAIResponsesProvider::providerID() const -{ - return PluginLLMCore::ProviderID::OpenAIResponses; -} - -PluginLLMCore::ProviderCapabilities OpenAIResponsesProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking - | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::ModelListing; -} - -::LLMQore::BaseClient *OpenAIResponsesProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/OpenAIResponsesProvider.hpp b/providers/OpenAIResponsesProvider.hpp deleted file mode 100644 index 3d9a1d0..0000000 --- a/providers/OpenAIResponsesProvider.hpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::Providers { - -class OpenAIResponsesProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit OpenAIResponsesProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - -private: - ::LLMQore::OpenAIResponsesClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/OpenRouterAIProvider.cpp b/providers/OpenRouterAIProvider.cpp deleted file mode 100644 index 6f528ca..0000000 --- a/providers/OpenRouterAIProvider.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "OpenRouterAIProvider.hpp" - -#include "settings/ProviderSettings.hpp" - -#include -#include -#include -#include - -namespace QodeAssist::Providers { - -OpenRouterProvider::OpenRouterProvider(QObject *parent) - : OpenAICompatProvider(parent) -{} - -QString OpenRouterProvider::name() const -{ - return "OpenRouter"; -} - -QString OpenRouterProvider::apiKey() const -{ - return Settings::providerSettings().openRouterApiKey(); -} - -QString OpenRouterProvider::url() const -{ - 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 deleted file mode 100644 index 092caa0..0000000 --- a/providers/OpenRouterAIProvider.hpp +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include "providers/OpenAICompatProvider.hpp" - -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; - PluginLLMCore::ProviderID providerID() const override; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/ProviderUrlUtils.hpp b/providers/ProviderUrlUtils.hpp deleted file mode 100644 index eaf56a0..0000000 --- a/providers/ProviderUrlUtils.hpp +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -namespace QodeAssist::Providers { - -// LM Studio presents its OpenAI-compatible API as /v1/..., while the -// OpenAI-style clients expect the base URL to already include /v1. Accept the -// configured URL either with or without the /v1 suffix and return it normalized. -inline QString ensureOpenAIV1Base(const QString &url) -{ - QString base = url.trimmed(); - while (base.endsWith(QLatin1Char('/'))) - base.chop(1); - if (!base.endsWith(QStringLiteral("/v1"))) - base += QStringLiteral("/v1"); - return base; -} - -} // namespace QodeAssist::Providers diff --git a/providers/Providers.hpp b/providers/Providers.hpp deleted file mode 100644 index 9358622..0000000 --- a/providers/Providers.hpp +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include "pluginllmcore/ProvidersManager.hpp" -#include "providers/ClaudeProvider.hpp" -#include "providers/DeepSeekProvider.hpp" -#include "providers/CodestralProvider.hpp" -#include "providers/GoogleAIProvider.hpp" -#include "providers/LMStudioProvider.hpp" -#include "providers/LMStudioResponsesProvider.hpp" -#include "providers/LlamaCppProvider.hpp" -#include "providers/MistralAIProvider.hpp" -#include "providers/OllamaCompatProvider.hpp" -#include "providers/OllamaProvider.hpp" -#include "providers/OpenAICompatProvider.hpp" -#include "providers/OpenAIProvider.hpp" -#include "providers/OpenAIResponsesProvider.hpp" -#include "providers/OpenRouterAIProvider.hpp" -#include "providers/QwenProvider.hpp" -#include "providers/QwenResponsesProvider.hpp" - -namespace QodeAssist::Providers { - -inline void registerProviders() -{ - auto &providerManager = PluginLLMCore::ProvidersManager::instance(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); - providerManager.registerProvider(); -} - -} // namespace QodeAssist::Providers diff --git a/providers/QwenProvider.cpp b/providers/QwenProvider.cpp deleted file mode 100644 index 5b943b1..0000000 --- a/providers/QwenProvider.cpp +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "QwenProvider.hpp" - -#include -#include "logger/Logger.hpp" -#include "settings/ChatAssistantSettings.hpp" -#include "settings/CodeCompletionSettings.hpp" -#include "settings/GeneralSettings.hpp" -#include "settings/ProviderSettings.hpp" -#include "settings/QuickRefactorSettings.hpp" -#include "tools/ToolsRegistration.hpp" - -#include -#include -#include - -namespace QodeAssist::Providers { - -QwenProvider::QwenProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::OpenAIClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString QwenProvider::name() const -{ - return "Qwen (OpenAI)"; -} - -QString QwenProvider::apiKey() const -{ - return Settings::providerSettings().qwenApiKey(); -} - -QString QwenProvider::url() const -{ - return "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"; -} - -QFuture> QwenProvider::getInstalledModels(const QString &url) -{ - m_client->setUrl(url); - m_client->setApiKey(apiKey()); - return m_client->listModels(); -} - -PluginLLMCore::ProviderID QwenProvider::providerID() const -{ - return PluginLLMCore::ProviderID::Qwen; -} - -PluginLLMCore::ProviderCapabilities QwenProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Image - | PluginLLMCore::ProviderCapability::Thinking - | PluginLLMCore::ProviderCapability::ModelListing; -} - -void QwenProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - request["max_tokens"] = settings.maxTokens(); - request["temperature"] = settings.temperature(); - - if (settings.useTopP()) - request["top_p"] = settings.topP(); - if (settings.useTopK()) - request["top_k"] = settings.topK(); - if (settings.useFrequencyPenalty()) - request["frequency_penalty"] = settings.frequencyPenalty(); - if (settings.usePresencePenalty()) - request["presence_penalty"] = settings.presencePenalty(); - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - applyModelParams(Settings::quickRefactorSettings()); - } else { - applyModelParams(Settings::chatAssistantSettings()); - } - - if (isToolsEnabled) { - auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE(QString("Added %1 tools to Qwen request").arg(toolsDefinitions.size())); - } - } -} - -::LLMQore::BaseClient *QwenProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/QwenProvider.hpp b/providers/QwenProvider.hpp deleted file mode 100644 index a7d5d49..0000000 --- a/providers/QwenProvider.hpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::Providers { - -class QwenProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit QwenProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - -private: - ::LLMQore::OpenAIClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/providers/QwenResponsesProvider.cpp b/providers/QwenResponsesProvider.cpp deleted file mode 100644 index 65d6cef..0000000 --- a/providers/QwenResponsesProvider.cpp +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#include "QwenResponsesProvider.hpp" - -#include -#include "logger/Logger.hpp" -#include "settings/ChatAssistantSettings.hpp" -#include "settings/CodeCompletionSettings.hpp" -#include "settings/GeneralSettings.hpp" -#include "settings/ProviderSettings.hpp" -#include "settings/QuickRefactorSettings.hpp" -#include "tools/ToolsRegistration.hpp" - -#include -#include -#include - -namespace QodeAssist::Providers { - -QwenResponsesProvider::QwenResponsesProvider(QObject *parent) - : PluginLLMCore::Provider(parent) - , m_client(new ::LLMQore::OpenAIResponsesClient(QString(), QString(), QString(), this)) -{ - Tools::registerQodeAssistTools(m_client->tools()); -} - -QString QwenResponsesProvider::name() const -{ - return "Qwen (OpenAI Response)"; -} - -QString QwenResponsesProvider::apiKey() const -{ - return Settings::providerSettings().qwenApiKey(); -} - -QString QwenResponsesProvider::url() const -{ - return "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"; -} - -void QwenResponsesProvider::prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) -{ - if (!prompt->isSupportProvider(providerID())) { - LOG_MESSAGE(QString("Template %1 doesn't support %2 provider").arg(name(), prompt->name())); - } - - prompt->prepareRequest(request, context); - - auto applyModelParams = [&request](const auto &settings) { - request["max_output_tokens"] = settings.maxTokens(); - - if (settings.useTopP()) { - request["top_p"] = settings.topP(); - } - }; - - auto applyThinkingMode = [&request](const auto &settings) { - QString effortStr = settings.openAIResponsesReasoningEffort.stringValue().toLower(); - if (effortStr.isEmpty()) { - effortStr = "medium"; - } - - QJsonObject reasoning; - reasoning["effort"] = effortStr; - request["reasoning"] = reasoning; - request["max_output_tokens"] = settings.thinkingMaxTokens(); - request["store"] = true; - - QJsonArray include; - include.append("reasoning.encrypted_content"); - request["include"] = include; - }; - - if (type == PluginLLMCore::RequestType::QuickRefactoring) { - const auto &qrSettings = Settings::quickRefactorSettings(); - applyModelParams(qrSettings); - - if (isThinkingEnabled) { - applyThinkingMode(qrSettings); - } - } else { - const auto &chatSettings = Settings::chatAssistantSettings(); - applyModelParams(chatSettings); - - if (isThinkingEnabled) { - applyThinkingMode(chatSettings); - } - } - - if (isToolsEnabled) { - const auto toolsDefinitions = m_client->tools()->getToolsDefinitions(); - if (!toolsDefinitions.isEmpty()) { - request["tools"] = toolsDefinitions; - LOG_MESSAGE( - QString("Added %1 tools to Qwen Responses request").arg(toolsDefinitions.size())); - } - } - - request["stream"] = true; -} - -QFuture> QwenResponsesProvider::getInstalledModels(const QString &url) -{ - m_client->setUrl(url); - m_client->setApiKey(apiKey()); - return m_client->listModels(); -} - -PluginLLMCore::ProviderID QwenResponsesProvider::providerID() const -{ - return PluginLLMCore::ProviderID::OpenAIResponses; -} - -PluginLLMCore::ProviderCapabilities QwenResponsesProvider::capabilities() const -{ - return PluginLLMCore::ProviderCapability::Tools | PluginLLMCore::ProviderCapability::Thinking - | PluginLLMCore::ProviderCapability::Image | PluginLLMCore::ProviderCapability::ModelListing; -} - -::LLMQore::BaseClient *QwenResponsesProvider::client() const -{ - return m_client; -} - -} // namespace QodeAssist::Providers diff --git a/providers/QwenResponsesProvider.hpp b/providers/QwenResponsesProvider.hpp deleted file mode 100644 index 271716a..0000000 --- a/providers/QwenResponsesProvider.hpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include -#include - -namespace QodeAssist::Providers { - -class QwenResponsesProvider : public PluginLLMCore::Provider -{ - Q_OBJECT -public: - explicit QwenResponsesProvider(QObject *parent = nullptr); - - QString name() const override; - QString url() const override; - void prepareRequest( - QJsonObject &request, - PluginLLMCore::PromptTemplate *prompt, - PluginLLMCore::ContextData context, - PluginLLMCore::RequestType type, - bool isToolsEnabled, - bool isThinkingEnabled) override; - QFuture> getInstalledModels(const QString &url) override; - PluginLLMCore::ProviderID providerID() const override; - PluginLLMCore::ProviderCapabilities capabilities() const override; - - ::LLMQore::BaseClient *client() const override; - QString apiKey() const override; - -private: - ::LLMQore::OpenAIResponsesClient *m_client; -}; - -} // namespace QodeAssist::Providers diff --git a/qodeassist.cpp b/qodeassist.cpp index d395beb..3fb2a1c 100644 --- a/qodeassist.cpp +++ b/qodeassist.cpp @@ -42,13 +42,12 @@ #include "chat/ChatOutputPane.h" #include "chat/NavigationPanel.hpp" #include "context/DocumentReaderQtCreator.hpp" -#include "pluginllmcore/ProvidersManager.hpp" #include "logger/RequestPerformanceLogger.hpp" #include "mcp/McpClientsManager.hpp" #include "mcp/McpServerManager.hpp" #include "sources/skills/SkillsManager.hpp" #include "tools/ToolsRegistration.hpp" -#include "providers/Providers.hpp" +#include "settings/AgentRole.hpp" #include "settings/ChatAssistantSettings.hpp" #include "settings/GeneralSettings.hpp" #include "settings/ProjectSettingsPanel.hpp" @@ -64,7 +63,6 @@ #include #include #include -#include "templates/Templates.hpp" #include "widgets/CustomInstructionsManager.hpp" #include "widgets/QuickRefactorDialog.hpp" #include @@ -141,9 +139,6 @@ public: loadTranslations(); - Providers::registerProviders(); - Templates::registerTemplates(); - CustomInstructionsManager::instance().loadInstructions(); Utils::Icon QCODEASSIST_ICON( @@ -180,16 +175,6 @@ public: m_sessionFileRegistry = new Chat::SessionFileRegistry{this}; m_skillsManager = new Skills::SkillsManager{this}; - { - auto &providers = PluginLLMCore::ProvidersManager::instance(); - for (const QString &providerName : providers.providersNames()) { - if (auto *provider = providers.getProviderByName(providerName)) { - if (auto *toolsManager = provider->toolsManager()) - Tools::registerSkillTool(toolsManager, m_skillsManager); - } - } - } - if (Settings::chatAssistantSettings().enableChatInBottomToolBar()) { m_chatOutputPane = new Chat::ChatOutputPane{ m_engine, m_sessionFileRegistry, m_skillsManager}; @@ -214,8 +199,26 @@ public: m_providerLauncher, m_providersPageNavigator); + // Ensure the default agent roles exist on disk before agents load, so a + // chat agent's `role = ""` resolves to a system prompt even on a fresh + // install where the Roles settings page was never opened. + Settings::AgentRolesManager::ensureDefaultRoles(); + m_agentFactory = new AgentFactory(m_providerInstanceFactory, m_providerSecretsStore, this); m_sessionManager = new SessionManager(m_agentFactory, this); + { + auto &contributors = m_sessionManager->toolContributors(); + contributors.add([](::LLMQore::ToolsManager *tools) { + Tools::registerQodeAssistTools(tools); + }); + contributors.add([skills = m_skillsManager](::LLMQore::ToolsManager *tools) { + if (skills) + Tools::registerSkillTool(tools, skills); + }); + contributors.add([](::LLMQore::ToolsManager *tools) { + Mcp::McpClientsManager::instance().registerToolsOn(tools); + }); + } m_engine->rootContext()->setContextProperty("agentFactory", m_agentFactory); m_engine->rootContext()->setContextProperty("sessionManager", m_sessionManager); m_agentsPageNavigator = new Settings::AgentsPageNavigator(this); diff --git a/settings/AgentListPane.cpp b/settings/AgentListPane.cpp index 14c9470..2b54464 100644 --- a/settings/AgentListPane.cpp +++ b/settings/AgentListPane.cpp @@ -171,7 +171,10 @@ void AgentListPane::rebuildList() const QSet &activeTags = m_tagStrip->activeTags(); auto addAgents = [&](const std::vector &agents) { for (const AgentConfig *cfg : agents) { - auto *item = new AgentListItem(*cfg, content); + AgentConfig display = *cfg; + if (m_factory) + display.model = m_factory->effectiveModel(cfg->name); + auto *item = new AgentListItem(display, content); item->setSelected(cfg->name == m_currentName); item->setActiveTags(activeTags); connect(item, &AgentListItem::clicked, this, &AgentListPane::onRowClicked); diff --git a/sources/Session/CMakeLists.txt b/sources/Session/CMakeLists.txt index 0d4329b..e3751aa 100644 --- a/sources/Session/CMakeLists.txt +++ b/sources/Session/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(Session STATIC Session.hpp Session.cpp SessionManager.hpp SessionManager.cpp SystemPromptBuilder.hpp SystemPromptBuilder.cpp + ToolContributorRegistry.hpp ) target_link_libraries(Session diff --git a/sources/Session/Session.cpp b/sources/Session/Session.cpp index ffc27c2..a285bd0 100644 --- a/sources/Session/Session.cpp +++ b/sources/Session/Session.cpp @@ -210,7 +210,9 @@ LLMQore::RequestID Session::sendCompletion(Templates::ContextData ctx) if (!provider->prepareRequest(payload, tmpl, ctx, /*tools=*/false, /*thinking=*/false)) return {}; - const auto id = provider->sendRequest(QUrl(provider->url()), payload, cfg.endpoint); + QString endpoint = cfg.endpoint; + endpoint.replace(QStringLiteral("${MODEL}"), cfg.model); + const auto id = provider->sendRequest(QUrl(provider->url()), payload, endpoint); if (id.isEmpty()) return {}; @@ -242,7 +244,9 @@ LLMQore::RequestID Session::dispatch( if (!provider->prepareRequest(payload, tmpl, ctx, tools, thinking)) return {}; - const auto id = provider->sendRequest(QUrl(provider->url()), payload, cfg.endpoint); + QString endpoint = cfg.endpoint; + endpoint.replace(QStringLiteral("${MODEL}"), cfg.model); + const auto id = provider->sendRequest(QUrl(provider->url()), payload, endpoint); if (id.isEmpty()) return {}; diff --git a/sources/Session/SessionManager.hpp b/sources/Session/SessionManager.hpp index c36b546..61085a5 100644 --- a/sources/Session/SessionManager.hpp +++ b/sources/Session/SessionManager.hpp @@ -9,6 +9,8 @@ #include #include +#include "ToolContributorRegistry.hpp" + namespace QodeAssist { class AgentFactory; @@ -41,6 +43,9 @@ public: void cancelAll(); + ToolContributorRegistry &toolContributors() noexcept { return m_toolContributors; } + const ToolContributorRegistry &toolContributors() const noexcept { return m_toolContributors; } + signals: void sessionCreated(Session *session); void sessionRemoved(Session *session); @@ -48,6 +53,7 @@ signals: private: QPointer m_agentFactory; QList> m_sessions; + ToolContributorRegistry m_toolContributors; }; } // namespace QodeAssist diff --git a/sources/Session/ToolContributorRegistry.hpp b/sources/Session/ToolContributorRegistry.hpp new file mode 100644 index 0000000..748fa82 --- /dev/null +++ b/sources/Session/ToolContributorRegistry.hpp @@ -0,0 +1,41 @@ +// Copyright (C) 2024-2026 Petr Mironychev +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +namespace LLMQore { +class ToolsManager; +} + +namespace QodeAssist { + +class ToolContributorRegistry +{ +public: + using Contributor = std::function; + + void add(Contributor contributor) + { + if (contributor) + m_contributors.push_back(std::move(contributor)); + } + + void contribute(LLMQore::ToolsManager *tools) const + { + if (!tools) + return; + for (const auto &contributor : m_contributors) + contributor(tools); + } + + void clear() { m_contributors.clear(); } + +private: + std::vector m_contributors; +}; + +} // namespace QodeAssist diff --git a/sources/agents/AgentFactory.cpp b/sources/agents/AgentFactory.cpp index 0f6551c..54ecfcd 100644 --- a/sources/agents/AgentFactory.cpp +++ b/sources/agents/AgentFactory.cpp @@ -248,6 +248,15 @@ void AgentFactory::setModelOverride(const QString &agentName, const QString &mod saveModelOverrides(); } +QString AgentFactory::effectiveModel(const QString &agentName) const +{ + const QString ov = m_modelOverrides.value(agentName); + if (!ov.isEmpty()) + return ov; + const AgentConfig *cfg = configByName(agentName); + return cfg ? cfg->model : QString(); +} + namespace { QString modelOverridesPath() { diff --git a/sources/agents/AgentFactory.hpp b/sources/agents/AgentFactory.hpp index 178ddce..c6caea5 100644 --- a/sources/agents/AgentFactory.hpp +++ b/sources/agents/AgentFactory.hpp @@ -59,6 +59,10 @@ public: [[nodiscard]] QString modelOverride(const QString &agentName) const; void setModelOverride(const QString &agentName, const QString &model); + // The model that will actually be sent for this agent: the settings + // override if set, otherwise the agent TOML's default `model`. + [[nodiscard]] QString effectiveModel(const QString &agentName) const; + [[nodiscard]] Providers::ProviderInstanceFactory *instanceFactory() const noexcept; [[nodiscard]] Providers::ProviderSecretsStore *secretsStore() const noexcept; diff --git a/sources/agents/ContextRenderer.cpp b/sources/agents/ContextRenderer.cpp index abdcc66..b374f39 100644 --- a/sources/agents/ContextRenderer.cpp +++ b/sources/agents/ContextRenderer.cpp @@ -8,8 +8,12 @@ #include #include #include +#include +#include #include +#include + #include #include @@ -155,6 +159,45 @@ void registerStringHelpers(inja::Environment &env) }); } +// Read a role's system prompt from the role JSON written by the settings Roles +// UI (AgentRolesManager). Returns "" if the role doesn't exist. +std::string roleSystemPrompt(const QString &id) +{ + if (id.isEmpty()) + return {}; + const QString path + = Core::ICore::userResourcePath( + QStringLiteral("qodeassist/agent_roles/%1.json").arg(id)) + .toFSPathString(); + QFile f(path); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning("[QodeAssist] agent_role: role '%s' not found at %s", + qUtf8Printable(id), qUtf8Printable(path)); + return {}; + } + return QJsonDocument::fromJson(f.readAll()) + .object() + .value("systemPrompt") + .toString() + .toStdString(); +} + +// Building blocks for composing a profile's `system_prompt` (alongside +// read_file/file_exists): +// {{ agent_role() }} — the runtime-selected role (Bindings.roleId, which +// the chat sets; falls back to "developer") +// {{ agent_role("") }} — a specific role by id +void registerAgentRole(inja::Environment &env, const Bindings &b) +{ + const QString runtimeRole = b.roleId.isEmpty() ? QStringLiteral("developer") : b.roleId; + env.add_callback("agent_role", 0, [runtimeRole](inja::Arguments &) -> nlohmann::json { + return roleSystemPrompt(runtimeRole); + }); + env.add_callback("agent_role", 1, [](inja::Arguments &args) -> nlohmann::json { + return roleSystemPrompt(QString::fromStdString(args.at(0)->get())); + }); +} + void registerSandbox(inja::Environment &env) { @@ -183,6 +226,7 @@ QString render(const QString &templateSource, const Bindings &bindings, QString registerFileExists(env, bindings); registerReadDir(env, bindings); registerStringHelpers(env); + registerAgentRole(env, bindings); inja::Template tpl; try { diff --git a/sources/agents/ContextRenderer.hpp b/sources/agents/ContextRenderer.hpp index e4fb2fd..3c0a73a 100644 --- a/sources/agents/ContextRenderer.hpp +++ b/sources/agents/ContextRenderer.hpp @@ -12,6 +12,9 @@ struct Bindings { QString projectDir; QString homeDir; + // Role id selected at runtime (e.g. in the chat). Used by the no-arg + // `{{ agent_role() }}` template callback; empty falls back to "developer". + QString roleId; }; QString render(const QString &templateSource, const Bindings &bindings, diff --git a/sources/agents/agents.qrc b/sources/agents/agents.qrc index 1b9e93e..b31913e 100644 --- a/sources/agents/agents.qrc +++ b/sources/agents/agents.qrc @@ -12,7 +12,6 @@ partials/google_part.jinja partials/ollama_messages.jinja - chat_base.toml openai_base_chat.toml openai_responses_base.toml anthropic_base_chat.toml diff --git a/sources/agents/anthropic_base_chat.toml b/sources/agents/anthropic_base_chat.toml index ccf0c80..5f7703b 100644 --- a/sources/agents/anthropic_base_chat.toml +++ b/sources/agents/anthropic_base_chat.toml @@ -3,16 +3,18 @@ schema_version = 1 name = "Anthropic Base Chat" description = "Anthropic Messages API request body (/v1/messages). Abstract — extend it and set model." abstract = true -extends = "Chat Base" provider_instance = "Claude" endpoint = "/v1/messages" enable_tools = true tags = ["chat", "claude", "anthropic", "cloud"] +system_prompt = """{{ agent_role() }}""" + [body] max_tokens = 8192 temperature = 1 +stream = true system = """{% if existsIn(ctx, "system_prompt") %}{{ tojson(ctx.system_prompt) }}{% endif %}""" messages = """ [ {% include "partials/anthropic_messages.jinja" %} ] diff --git a/sources/agents/chat_base.toml b/sources/agents/chat_base.toml deleted file mode 100644 index 54b1e3e..0000000 --- a/sources/agents/chat_base.toml +++ /dev/null @@ -1,16 +0,0 @@ -schema_version = 1 - -name = "Chat Base" -description = "Shared system prompt for coding-chat agents. Abstract — not selectable." -abstract = true - -system_prompt = """ -You are a helpful coding assistant integrated into Qt Creator. -Answer concisely. Prefer concrete diffs or minimal patches over rewriting -whole files. Use markdown code blocks with language tags. - -{% if file_exists("${PROJECT_DIR}/README.md") %} -## Project README.md -{{ read_file("${PROJECT_DIR}/README.md") }} -{% endif %} -""" diff --git a/sources/agents/google_base_chat.toml b/sources/agents/google_base_chat.toml index 95571f0..92dbfe7 100644 --- a/sources/agents/google_base_chat.toml +++ b/sources/agents/google_base_chat.toml @@ -3,12 +3,13 @@ schema_version = 1 name = "Google Base Chat" description = "Google Gemini generateContent request body. Abstract — extend it and set model/endpoint." abstract = true -extends = "Chat Base" provider_instance = "Google AI" enable_tools = true tags = ["chat", "gemini", "google", "cloud"] +system_prompt = """{{ agent_role() }}""" + [body] system_instruction = """{% if existsIn(ctx, "system_prompt") %}{ "parts": [ { "text": {{ tojson(ctx.system_prompt) }} } ] }{% endif %}""" contents = """ diff --git a/sources/agents/google_gemini_chat.toml b/sources/agents/google_gemini_chat.toml index b4b02d2..28eef48 100644 --- a/sources/agents/google_gemini_chat.toml +++ b/sources/agents/google_gemini_chat.toml @@ -4,7 +4,7 @@ extends = "Google Base Chat" name = "Gemini Chat" description = "Google Gemini 2.5 Flash (generateContent API) — coding chat with thinking." -endpoint = "/models/gemini-2.5-flash:streamGenerateContent?alt=sse" +endpoint = "/models/${MODEL}:streamGenerateContent?alt=sse" model = "gemini-2.5-flash" enable_thinking = true diff --git a/sources/agents/ollama_base_chat.toml b/sources/agents/ollama_base_chat.toml index a49e782..f02fbbb 100644 --- a/sources/agents/ollama_base_chat.toml +++ b/sources/agents/ollama_base_chat.toml @@ -3,12 +3,13 @@ schema_version = 1 name = "Ollama Base Chat" description = "Ollama native /api/chat request body. Abstract — extend it and set model/options." abstract = true -extends = "Chat Base" provider_instance = "Ollama (Native)" endpoint = "/api/chat" tags = ["ollama", "local"] +system_prompt = """{{ agent_role() }}""" + [body] stream = true messages = """ diff --git a/sources/agents/openai_base_chat.toml b/sources/agents/openai_base_chat.toml index 502ce92..bf9df73 100644 --- a/sources/agents/openai_base_chat.toml +++ b/sources/agents/openai_base_chat.toml @@ -3,16 +3,18 @@ schema_version = 1 name = "OpenAI Base Chat" description = "OpenAI Chat Completions request body. Abstract — extend it and set provider/endpoint/model." abstract = true -extends = "Chat Base" provider_instance = "OpenAI (Chat Completions)" endpoint = "/chat/completions" enable_tools = true tags = ["chat", "openai", "cloud"] +system_prompt = """{{ agent_role() }}""" + [body] max_tokens = 8192 temperature = 0.7 +stream = true messages = """ [ {% include "partials/openai_messages.jinja" %} ] """ diff --git a/sources/agents/openai_responses_base.toml b/sources/agents/openai_responses_base.toml index 98a4252..3d06fe4 100644 --- a/sources/agents/openai_responses_base.toml +++ b/sources/agents/openai_responses_base.toml @@ -3,16 +3,18 @@ schema_version = 1 name = "OpenAI Responses Base" description = "OpenAI Responses API request body (/responses). Abstract — extend it and set provider/endpoint/model." abstract = true -extends = "Chat Base" provider_instance = "OpenAI (Responses API)" endpoint = "/responses" enable_tools = true tags = ["chat", "openai", "responses", "cloud"] +system_prompt = """{{ agent_role() }}""" + [body] max_output_tokens = 8192 temperature = 0.7 +stream = true instructions = """{% if existsIn(ctx, "system_prompt") %}{{ tojson(ctx.system_prompt) }}{% endif %}""" input = """ [ {% include "partials/openai_responses_input.jinja" %} ] diff --git a/sources/common/CMakeLists.txt b/sources/common/CMakeLists.txt index fdb1a7b..d8997c6 100644 --- a/sources/common/CMakeLists.txt +++ b/sources/common/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(Common INTERFACE) target_sources(Common INTERFACE ContextData.hpp + ResponseCleaner.hpp ) target_include_directories(Common INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/pluginllmcore/ResponseCleaner.hpp b/sources/common/ResponseCleaner.hpp similarity index 97% rename from pluginllmcore/ResponseCleaner.hpp rename to sources/common/ResponseCleaner.hpp index 74f4c30..8b2642c 100644 --- a/pluginllmcore/ResponseCleaner.hpp +++ b/sources/common/ResponseCleaner.hpp @@ -8,7 +8,7 @@ #include #include -namespace QodeAssist::PluginLLMCore { +namespace QodeAssist { class ResponseCleaner { @@ -100,5 +100,4 @@ private: } }; -} // namespace QodeAssist::PluginLLMCore - +} // namespace QodeAssist diff --git a/sources/settings/AgentRosterWidget.cpp b/sources/settings/AgentRosterWidget.cpp index ebb134e..185beee 100644 --- a/sources/settings/AgentRosterWidget.cpp +++ b/sources/settings/AgentRosterWidget.cpp @@ -199,6 +199,7 @@ public: AgentRosterRow(int index, const QString &name, const AgentConfig *cfg, + const QString &effectiveModel, bool active, bool first, bool last, @@ -226,6 +227,7 @@ private: AgentRosterRow::AgentRosterRow(int index, const QString &name, const AgentConfig *cfg, + const QString &effectiveModel, bool active, bool first, bool last, @@ -282,7 +284,7 @@ AgentRosterRow::AgentRosterRow(int index, body->setSpacing(2); const QString displayName = cfg ? cfg->name : tr("%1 (missing)").arg(name); - const QString model = cfg ? cfg->model : QString(); + const QString model = effectiveModel; const bool isUser = cfg && cfg->isUserSource(); body->addWidget(buildIdentityLine(displayName, model, active, isUser, theme)); @@ -562,9 +564,11 @@ void AgentRosterWidget::rebuildRows() for (int i = 0; i < m_names.size(); ++i) { const QString &name = m_names.at(i); const AgentConfig *cfg = m_factory ? m_factory->configByName(name) : nullptr; + const QString effModel = m_factory ? m_factory->effectiveModel(name) : QString(); auto *row = new AgentRosterRow(i, name, cfg, + effModel, i == m_activeIndex, /*first*/ i == 0, /*last*/ i == m_names.size() - 1, diff --git a/templates/Alpaca.hpp b/templates/Alpaca.hpp deleted file mode 100644 index 6e96c92..0000000 --- a/templates/Alpaca.hpp +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include "pluginllmcore/PromptTemplate.hpp" -#include - -namespace QodeAssist::Templates { - -class Alpaca : public PluginLLMCore::PromptTemplate -{ -public: - QString name() const override { return "Alpaca"; } - PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; } - QStringList stopWords() const override - { - return QStringList() << "### Instruction:" << "### Response:"; - } - void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override - { - QJsonArray messages; - - QString fullContent; - - if (context.systemPrompt) { - fullContent += context.systemPrompt.value() + "\n\n"; - } - - if (context.history) { - for (const auto &msg : context.history.value()) { - if (msg.role == "user") { - fullContent += QString("### Instruction:\n%1\n\n").arg(msg.content); - } else if (msg.role == "assistant") { - fullContent += QString("### Response:\n%1\n\n").arg(msg.content); - } - } - } - - messages.append(QJsonObject{{"role", "user"}, {"content", fullContent}}); - - request["messages"] = messages; - } - QString description() const override - { - return "Template for models using Alpaca instruction format:\n\n" - "{\n" - " \"messages\": [\n" - " {\n" - " \"role\": \"user\",\n" - " \"content\": \"\\n\\n" - "### Instruction:\\n\\n\\n" - "### Response:\\n\\n\\n\"\n" - " }\n" - " ]\n" - "}\n\n" - "Combines all messages into a single formatted prompt."; - } - bool isSupportProvider(PluginLLMCore::ProviderID id) const override - { - switch (id) { - 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; - } - } -}; - -} // namespace QodeAssist::Templates diff --git a/templates/ChatML.hpp b/templates/ChatML.hpp deleted file mode 100644 index b4e0b7f..0000000 --- a/templates/ChatML.hpp +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include "pluginllmcore/PromptTemplate.hpp" - -namespace QodeAssist::Templates { - -class ChatML : public PluginLLMCore::PromptTemplate -{ -public: - QString name() const override { return "ChatML"; } - PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; } - QStringList stopWords() const override - { - return QStringList() << "<|im_start|>" << "<|im_end|>"; - } - void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override - { - QJsonArray messages; - - if (context.systemPrompt) { - messages.append(QJsonObject{ - {"role", "system"}, - {"content", - QString("<|im_start|>system\n%2\n<|im_end|>").arg(context.systemPrompt.value())}}); - } - - if (context.history) { - for (const auto &msg : context.history.value()) { - messages.append(QJsonObject{ - {"role", msg.role}, - {"content", - QString("<|im_start|>%1\n%2\n<|im_end|>").arg(msg.role, msg.content)}}); - } - } - - request["messages"] = messages; - } - QString description() const override - { - return "Template for models supporting ChatML format:\n\n" - "{\n" - " \"messages\": [\n" - " {\n" - " \"role\": \"system\",\n" - " \"content\": \"<|im_start|>system\\n\\n<|im_end|>\"\n" - " },\n" - " {\n" - " \"role\": \"user\",\n" - " \"content\": \"<|im_start|>user\\n\\n<|im_end|>\"\n" - " }\n" - " ]\n" - "}\n\n" - "Compatible with multiple providers supporting the ChatML token format."; - } - bool isSupportProvider(PluginLLMCore::ProviderID id) const override - { - switch (id) { - 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; - } - } -}; - -} // namespace QodeAssist::Templates diff --git a/templates/Claude.hpp b/templates/Claude.hpp deleted file mode 100644 index d549fd0..0000000 --- a/templates/Claude.hpp +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include - -#include "pluginllmcore/PromptTemplate.hpp" - -namespace QodeAssist::Templates { - -class Claude : public PluginLLMCore::PromptTemplate -{ -public: - PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; } - QString name() const override { return "Claude"; } - QStringList stopWords() const override { return QStringList(); } - bool supportsToolHistory() const override { return true; } - void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override - { - QJsonArray messages; - - if (context.systemPrompt) { - request["system"] = context.systemPrompt.value(); - } - - if (context.history) { - int toolResultUserIdx = -1; - for (const auto &msg : context.history.value()) { - if (msg.role == "system") continue; - - if (!msg.toolCalls.isEmpty()) { - toolResultUserIdx = -1; - QJsonArray content; - if (!msg.content.isEmpty()) { - content.append(QJsonObject{{"type", "text"}, {"text", msg.content}}); - } - for (const auto &call : msg.toolCalls) { - content.append(QJsonObject{ - {"type", "tool_use"}, - {"id", call.id}, - {"name", call.name}, - {"input", call.arguments}}); - } - messages.append(QJsonObject{{"role", "assistant"}, {"content", content}}); - continue; - } - - if (msg.role == "tool") { - QJsonObject resultBlock{ - {"type", "tool_result"}, - {"tool_use_id", msg.toolCallId}, - {"content", msg.content}}; - if (toolResultUserIdx >= 0) { - QJsonObject userMsg = messages[toolResultUserIdx].toObject(); - QJsonArray content = userMsg["content"].toArray(); - content.append(resultBlock); - userMsg["content"] = content; - messages[toolResultUserIdx] = userMsg; - } else { - messages.append(QJsonObject{ - {"role", "user"}, {"content", QJsonArray{resultBlock}}}); - toolResultUserIdx = messages.size() - 1; - } - continue; - } - - toolResultUserIdx = -1; - - if (msg.isThinking) { - // Claude API requires signature for thinking blocks - if (msg.signature.isEmpty()) { - continue; - } - - QJsonArray content; - QJsonObject thinkingBlock; - thinkingBlock["type"] = msg.isRedacted ? "redacted_thinking" : "thinking"; - - QString thinkingText = msg.content; - int signaturePos = thinkingText.indexOf("\n[Signature: "); - if (signaturePos != -1) { - thinkingText = thinkingText.left(signaturePos); - } - - if (!msg.isRedacted) { - thinkingBlock["thinking"] = thinkingText; - } - thinkingBlock["signature"] = msg.signature; - content.append(thinkingBlock); - - messages.append(QJsonObject{{"role", "assistant"}, {"content", content}}); - } else if (msg.images && !msg.images->isEmpty()) { - QJsonArray content; - - if (!msg.content.isEmpty()) { - content.append(QJsonObject{{"type", "text"}, {"text", msg.content}}); - } - - for (const auto &image : msg.images.value()) { - QJsonObject imageBlock; - imageBlock["type"] = "image"; - - QJsonObject source; - if (image.isUrl) { - source["type"] = "url"; - source["url"] = image.data; - } else { - source["type"] = "base64"; - source["media_type"] = image.mediaType; - source["data"] = image.data; - } - imageBlock["source"] = source; - content.append(imageBlock); - } - - messages.append(QJsonObject{{"role", msg.role}, {"content", content}}); - } else { - messages.append(QJsonObject{{"role", msg.role}, {"content", msg.content}}); - } - } - } - - request["messages"] = messages; - } - QString description() const override - { - return "Template for Anthropic's Claude models:\n\n" - "{\n" - " \"system\": \"\",\n" - " \"messages\": [\n" - " {\"role\": \"user\", \"content\": \"\"},\n" - " {\"role\": \"assistant\", \"content\": \"\"}\n" - " ]\n" - "}\n\n" - "Formats content according to Claude API specifications."; - } - bool isSupportProvider(PluginLLMCore::ProviderID id) const override - { - switch (id) { - case QodeAssist::PluginLLMCore::ProviderID::Claude: - return true; - default: - return false; - } - } -}; - -} // namespace QodeAssist::Templates diff --git a/templates/CodeLlamaFim.hpp b/templates/CodeLlamaFim.hpp deleted file mode 100644 index 8162df0..0000000 --- a/templates/CodeLlamaFim.hpp +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) 2024-2026 Petr Mironychev -// SPDX-License-Identifier: GPL-3.0-or-later -// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE - -#pragma once - -#include "pluginllmcore/PromptTemplate.hpp" - -namespace QodeAssist::Templates { - -class CodeLlamaFim : public PluginLLMCore::PromptTemplate -{ -public: - PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; } - QString name() const override { return "CodeLlama FIM"; } - QString endpoint() const override { return QStringLiteral("/api/generate"); } - QStringList stopWords() const override - { - return QStringList() << "" << "
" << "";
-    }
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        request["prompt"] = QString("
 %1 %2 ")
-                                .arg(context.prefix.value_or(""), context.suffix.value_or(""));
-        request["system"] = context.systemPrompt.value_or("");
-    }
-    QString description() const override
-    {
-        return "Specialized template for CodeLlama FIM:\n\n"
-               "{\n"
-               "  \"prompt\": \"
   \",\n"
-               "  \"system\": \"\"\n"
-               "}\n\n"
-               "Optimized for code completion with CodeLlama models.";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
-            return true;
-        default:
-            return false;
-        }
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/CodeLlamaQMLFim.hpp b/templates/CodeLlamaQMLFim.hpp
deleted file mode 100644
index 7732570..0000000
--- a/templates/CodeLlamaQMLFim.hpp
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include "pluginllmcore/PromptTemplate.hpp"
-
-namespace QodeAssist::Templates {
-
-class CodeLlamaQMLFim : public PluginLLMCore::PromptTemplate
-{
-public:
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
-    QString name() const override { return "CodeLlama QML FIM"; }
-    QString endpoint() const override { return QStringLiteral("/api/generate"); }
-    QStringList stopWords() const override
-    {
-        return QStringList() << "" << "
" << "
" << "
" << "< EOT >" << "\\end" - << "" << "" << "##"; - } - void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override - { - request["prompt"] = QString("%1
%2")
-                                .arg(context.suffix.value_or(""), context.prefix.value_or(""));
-        request["system"] = context.systemPrompt.value_or("");
-    }
-    QString description() const override
-    {
-        return "Specialized template for QML code completion with CodeLlama:\n\n"
-               "{\n"
-               "  \"prompt\": \"
\",\n"
-               "  \"system\": \"\"\n"
-               "}\n\n"
-               "Specifically optimized for QML/JavaScript code completion.";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
-            return true;
-        default:
-            return false;
-        }
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/DeepSeekCoderFim.hpp b/templates/DeepSeekCoderFim.hpp
deleted file mode 100644
index 2a8b64f..0000000
--- a/templates/DeepSeekCoderFim.hpp
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include "llmcore/PromptTemplate.hpp"
-
-namespace QodeAssist::Templates {
-
-class DeepSeekCoderFim : public LLMQore::PromptTemplate
-{
-public:
-    LLMQore::TemplateType type() const override { return LLMQore::TemplateType::Fim; }
-    QString name() const override { return "DeepSeekCoder FIM"; }
-    QString promptTemplate() const override
-    {
-        return "<|fim▁begin|>%1<|fim▁hole|>%2<|fim▁end|>";
-    }
-    QStringList stopWords() const override { return QStringList(); }
-    void prepareRequest(QJsonObject &request, const LLMQore::ContextData &context) const override
-    {
-        QString formattedPrompt = promptTemplate().arg(context.prefix, context.suffix);
-        request["prompt"] = formattedPrompt;
-    }
-    QString description() const override
-    {
-        return "The message will contain the following tokens: "
-               "<|fim▁begin|>%1<|fim▁hole|>%2<|fim▁end|>";
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/GoogleAI.hpp b/templates/GoogleAI.hpp
deleted file mode 100644
index 8e21622..0000000
--- a/templates/GoogleAI.hpp
+++ /dev/null
@@ -1,161 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-
-#include "pluginllmcore/PromptTemplate.hpp"
-
-namespace QodeAssist::Templates {
-
-class GoogleAI : public PluginLLMCore::PromptTemplate
-{
-public:
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
-    QString name() const override { return "Google AI"; }
-    QStringList stopWords() const override { return QStringList(); }
-    bool supportsToolHistory() const override { return true; }
-
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        QJsonArray contents;
-
-        if (context.systemPrompt && !context.systemPrompt->isEmpty()) {
-            request["system_instruction"] = QJsonObject{
-                {"parts", QJsonObject{{"text", context.systemPrompt.value()}}}};
-        }
-
-        int toolResultIdx = -1;
-        for (const auto &msg : context.history.value()) {
-            if (!msg.toolCalls.isEmpty()) {
-                toolResultIdx = -1;
-                QJsonArray callParts;
-                if (!msg.content.isEmpty()) {
-                    callParts.append(QJsonObject{{"text", msg.content}});
-                }
-                for (const auto &call : msg.toolCalls) {
-                    callParts.append(QJsonObject{
-                        {"functionCall",
-                         QJsonObject{{"name", call.name}, {"args", call.arguments}}}});
-                }
-                contents.append(QJsonObject{{"role", "model"}, {"parts", callParts}});
-                continue;
-            }
-
-            if (msg.role == "tool") {
-                QJsonObject responsePart{
-                    {"functionResponse",
-                     QJsonObject{
-                         {"name", msg.toolName},
-                         {"response", QJsonObject{{"result", msg.content}}}}}};
-                if (toolResultIdx >= 0) {
-                    QJsonObject fnMsg = contents[toolResultIdx].toObject();
-                    QJsonArray fnParts = fnMsg["parts"].toArray();
-                    fnParts.append(responsePart);
-                    fnMsg["parts"] = fnParts;
-                    contents[toolResultIdx] = fnMsg;
-                } else {
-                    contents.append(
-                        QJsonObject{{"role", "function"}, {"parts", QJsonArray{responsePart}}});
-                    toolResultIdx = contents.size() - 1;
-                }
-                continue;
-            }
-
-            toolResultIdx = -1;
-
-            QJsonObject content;
-            QJsonArray parts;
-
-            if (msg.isThinking) {
-                if (!msg.content.isEmpty()) {
-                    QJsonObject thinkingPart;
-                    thinkingPart["text"] = msg.content;
-                    thinkingPart["thought"] = true;
-                    parts.append(thinkingPart);
-                }
-                
-                if (!msg.signature.isEmpty()) {
-                    QJsonObject signaturePart;
-                    signaturePart["thoughtSignature"] = msg.signature;
-                    parts.append(signaturePart);
-                }
-                
-                if (parts.isEmpty()) {
-                    continue;
-                }
-                
-                content["role"] = "model";
-            } else {
-                if (!msg.content.isEmpty()) {
-                    parts.append(QJsonObject{{"text", msg.content}});
-                }
-
-                if (msg.images && !msg.images->isEmpty()) {
-                    for (const auto &image : msg.images.value()) {
-                        QJsonObject imagePart;
-                        
-                        if (image.isUrl) {
-                            QJsonObject fileData;
-                            fileData["mime_type"] = image.mediaType;
-                            fileData["file_uri"] = image.data;
-                            imagePart["file_data"] = fileData;
-                        } else {
-                            QJsonObject inlineData;
-                            inlineData["mime_type"] = image.mediaType;
-                            inlineData["data"] = image.data;
-                            imagePart["inline_data"] = inlineData;
-                        }
-                        
-                        parts.append(imagePart);
-                    }
-                }
-
-                QString role = msg.role;
-                if (role == "assistant") {
-                    role = "model";
-                }
-                
-                content["role"] = role;
-            }
-
-            content["parts"] = parts;
-            contents.append(content);
-        }
-
-        request["contents"] = contents;
-    }
-
-    QString description() const override
-    {
-        return "Template for Google AI models (Gemini):\n\n"
-               "{\n"
-               "  \"system_instruction\": {\"parts\": {\"text\": \"\"}},\n"
-               "  \"contents\": [\n"
-               "    {\n"
-               "      \"role\": \"user\",\n"
-               "      \"parts\": [{\"text\": \"\"}]\n"
-               "    },\n"
-               "    {\n"
-               "      \"role\": \"model\",\n"
-               "      \"parts\": [\n"
-               "        {\"text\": \"\", \"thought\": true},\n"
-               "        {\"thoughtSignature\": \"\"},\n"
-               "        {\"text\": \"\"}\n"
-               "      ]\n"
-               "    }\n"
-               "  ]\n"
-               "}\n\n"
-               "Supports proper role mapping (model/user roles), images, and thinking blocks.";
-    }
-
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        return id == QodeAssist::PluginLLMCore::ProviderID::GoogleAI;
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/Llama2.hpp b/templates/Llama2.hpp
deleted file mode 100644
index 39cfc11..0000000
--- a/templates/Llama2.hpp
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include "pluginllmcore/PromptTemplate.hpp"
-#include 
-
-namespace QodeAssist::Templates {
-
-class Llama2 : public PluginLLMCore::PromptTemplate
-{
-public:
-    QString name() const override { return "Llama 2"; }
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
-    QStringList stopWords() const override { return QStringList() << "[INST]"; }
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        QJsonArray messages;
-
-        QString fullContent;
-
-        if (context.systemPrompt) {
-            fullContent
-                += QString("[INST]<>\n%1\n<>[/INST]\n").arg(context.systemPrompt.value());
-        }
-
-        if (context.history) {
-            for (const auto &msg : context.history.value()) {
-                if (msg.role == "user") {
-                    fullContent += QString("[INST]%1[/INST]\n").arg(msg.content);
-                } else if (msg.role == "assistant") {
-                    fullContent += msg.content + "\n";
-                }
-            }
-        }
-
-        messages.append(QJsonObject{{"role", "user"}, {"content", fullContent}});
-
-        request["messages"] = messages;
-    }
-    QString description() const override
-    {
-        return "Template for Llama 2 models:\n\n"
-               "{\n"
-               "  \"messages\": [\n"
-               "    {\n"
-               "      \"role\": \"user\",\n"
-               "      \"content\": \"[INST]<>\\n\\n<>[/INST]\\n"
-               "\\n"
-               "[INST][/INST]\\n\"\n"
-               "    }\n"
-               "  ]\n"
-               "}\n\n"
-               "Compatible with Ollama, LM Studio, and other services for Llama 2.";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        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;
-        }
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/Llama3.hpp b/templates/Llama3.hpp
deleted file mode 100644
index 9b591bd..0000000
--- a/templates/Llama3.hpp
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include "pluginllmcore/PromptTemplate.hpp"
-
-namespace QodeAssist::Templates {
-
-class Llama3 : public PluginLLMCore::PromptTemplate
-{
-public:
-    QString name() const override { return "Llama 3"; }
-    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 PluginLLMCore::ContextData &context) const override
-    {
-        QJsonArray messages;
-
-        if (context.systemPrompt) {
-            messages.append(QJsonObject{
-                {"role", "system"},
-                {"content",
-                 QString("<|start_header_id|>system<|end_header_id|>%2<|eot_id|>")
-                     .arg(context.systemPrompt.value())}});
-        }
-
-        if (context.history) {
-            for (const auto &msg : context.history.value()) {
-                messages.append(QJsonObject{
-                    {"role", msg.role},
-                    {"content",
-                     QString("<|start_header_id|>%1<|end_header_id|>%2<|eot_id|>")
-                         .arg(msg.role, msg.content)}});
-            }
-        }
-
-        request["messages"] = messages;
-    }
-    QString description() const override
-    {
-        return "Template for Llama 3 models:\n\n"
-               "{\n"
-               "  \"messages\": [\n"
-               "    {\n"
-               "      \"role\": \"system\",\n"
-               "      \"content\": \"<|start_header_id|>system<|end_header_id|><|eot_id|>\"\n"
-               "    },\n"
-               "    {\n"
-               "      \"role\": \"user\",\n"
-               "      \"content\": \"<|start_header_id|>user<|end_header_id|><|eot_id|>\"\n"
-               "    }\n"
-               "  ]\n"
-               "}\n\n"
-               "Compatible with Ollama, LM Studio, and OpenAI-compatible services for Llama 3.";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        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;
-        }
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/LlamaCppFim.hpp b/templates/LlamaCppFim.hpp
deleted file mode 100644
index 7f80d69..0000000
--- a/templates/LlamaCppFim.hpp
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include "pluginllmcore/PromptTemplate.hpp"
-
-namespace QodeAssist::Templates {
-
-class LlamaCppFim : public PluginLLMCore::PromptTemplate
-{
-public:
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
-    QString name() const override { return "llama.cpp FIM"; }
-    QString endpoint() const override { return QStringLiteral("/infill"); }
-    QStringList stopWords() const override { return {}; }
-
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        request["input_prefix"] = context.prefix.value_or("");
-        request["input_suffix"] = context.suffix.value_or("");
-
-        if (context.filesMetadata && !context.filesMetadata->isEmpty()) {
-            QJsonArray filesArray;
-            for (const auto &file : *context.filesMetadata) {
-                QJsonObject fileObj;
-                fileObj["filename"] = file.filePath;
-                fileObj["text"] = file.content;
-                filesArray.append(fileObj);
-            }
-            request["input_extra"] = filesArray;
-        }
-    }
-
-    QString description() const override
-    {
-        return "Default llama.cpp FIM (Fill-in-Middle) /infill template with native format:\n\n"
-               "{\n"
-               "  \"input_prefix\": \"\",\n"
-               "  \"input_suffix\": \"\",\n"
-               "  \"input_extra\": \"\"\n"
-               "}\n\n"
-               "Recommended for models with FIM capability.";
-    }
-
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        return id == QodeAssist::PluginLLMCore::ProviderID::LlamaCpp;
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/MistralAI.hpp b/templates/MistralAI.hpp
deleted file mode 100644
index e396325..0000000
--- a/templates/MistralAI.hpp
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include "pluginllmcore/PromptTemplate.hpp"
-#include "templates/ToolMessages.hpp"
-
-namespace QodeAssist::Templates {
-
-class MistralAIFim : public PluginLLMCore::PromptTemplate
-{
-public:
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
-    QString name() const override { return "Mistral AI FIM"; }
-    QString endpoint() const override { return QStringLiteral("/v1/fim/completions"); }
-    QStringList stopWords() const override { return QStringList(); }
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        request["prompt"] = context.prefix.value_or("");
-        request["suffix"] = context.suffix.value_or("");
-    }
-    QString description() const override
-    {
-        return "Template for MistralAI models with FIM support:\n\n"
-               "{\n"
-               "  \"prompt\": \"\",\n"
-               "  \"suffix\": \"\"\n"
-               "}\n\n"
-               "Optimized for code completion with MistralAI models.";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        case QodeAssist::PluginLLMCore::ProviderID::MistralAI:
-            return true;
-        default:
-            return false;
-        }
-    }
-};
-
-class MistralAIChat : public PluginLLMCore::PromptTemplate
-{
-public:
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
-    QString name() const override { return "Mistral AI Chat"; }
-    QStringList stopWords() const override { return QStringList(); }
-    bool supportsToolHistory() const override { return true; }
-
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        QJsonArray messages;
-
-        if (context.systemPrompt) {
-            messages.append(
-                QJsonObject{{"role", "system"}, {"content", context.systemPrompt.value()}});
-        }
-
-        if (context.history) {
-            for (const auto &msg : context.history.value()) {
-                if (appendOpenAIToolMessage(messages, msg)) {
-                    continue;
-                }
-                if (msg.images && !msg.images->isEmpty()) {
-                    QJsonArray content;
-                    
-                    if (!msg.content.isEmpty()) {
-                        content.append(QJsonObject{{"type", "text"}, {"text", msg.content}});
-                    }
-                    
-                    for (const auto &image : msg.images.value()) {
-                        QJsonObject imageBlock;
-                        imageBlock["type"] = "image_url";
-                        
-                        QJsonObject imageUrl;
-                        if (image.isUrl) {
-                            imageUrl["url"] = image.data;
-                        } else {
-                            imageUrl["url"] = QString("data:%1;base64,%2").arg(image.mediaType, image.data);
-                        }
-                        imageBlock["image_url"] = imageUrl;
-                        content.append(imageBlock);
-                    }
-                    
-                    messages.append(QJsonObject{{"role", msg.role}, {"content", content}});
-                } else {
-                    messages.append(QJsonObject{{"role", msg.role}, {"content", msg.content}});
-                }
-            }
-        }
-
-        request["messages"] = messages;
-    }
-    QString description() const override
-    {
-        return "Template for MistralAI chat-capable models:\n\n"
-               "{\n"
-               "  \"messages\": [\n"
-               "    {\"role\": \"system\", \"content\": \"\"},\n"
-               "    {\"role\": \"user\", \"content\": \"\"},\n"
-               "    {\"role\": \"assistant\", \"content\": \"\"}\n"
-               "  ]\n"
-               "}\n\n"
-               "Supports system messages, conversation history, and images.";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        case QodeAssist::PluginLLMCore::ProviderID::MistralAI:
-            return true;
-        default:
-            return false;
-        }
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/Ollama.hpp b/templates/Ollama.hpp
deleted file mode 100644
index a711c24..0000000
--- a/templates/Ollama.hpp
+++ /dev/null
@@ -1,128 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include "pluginllmcore/PromptTemplate.hpp"
-
-namespace QodeAssist::Templates {
-
-class OllamaFim : public PluginLLMCore::PromptTemplate
-{
-public:
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
-    QString name() const override { return "Ollama FIM"; }
-    QString endpoint() const override { return QStringLiteral("/api/generate"); }
-    QStringList stopWords() const override { return QStringList() << ""; }
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        request["prompt"] = context.prefix.value_or("");
-        request["suffix"] = context.suffix.value_or("");
-        request["system"] = context.systemPrompt.value_or("");
-    }
-    QString description() const override
-    {
-        return "Default Ollama FIM (Fill-in-Middle) template with native format:\n\n"
-               "{\n"
-               "  \"prompt\": \"\",\n"
-               "  \"suffix\": \"\",\n"
-               "  \"system\": \"\"\n"
-               "}\n\n"
-               "Recommended for Ollama models with FIM capability.";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
-            return true;
-        default:
-            return false;
-        }
-    }
-};
-
-class OllamaChat : public PluginLLMCore::PromptTemplate
-{
-public:
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
-    QString name() const override { return "Ollama Chat"; }
-    QStringList stopWords() const override { return QStringList(); }
-    bool supportsToolHistory() const override { return true; }
-
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        QJsonArray messages;
-
-        if (context.systemPrompt) {
-            messages.append(
-                QJsonObject{{"role", "system"}, {"content", context.systemPrompt.value()}});
-        }
-
-        if (context.history) {
-            for (const auto &msg : context.history.value()) {
-                QJsonObject messageObj;
-                messageObj["role"] = msg.role;
-
-                if (!msg.toolCalls.isEmpty()) {
-                    QJsonArray toolCalls;
-                    for (const auto &call : msg.toolCalls) {
-                        toolCalls.append(QJsonObject{
-                            {"type", "function"},
-                            {"function",
-                             QJsonObject{{"name", call.name}, {"arguments", call.arguments}}}});
-                    }
-                    messageObj["tool_calls"] = toolCalls;
-                    if (!msg.content.isEmpty()) {
-                        messageObj["content"] = msg.content;
-                    }
-                } else {
-                    messageObj["content"] = msg.content;
-                    // Ollama correlates a tool result to its originating
-                    // call by tool_name; omitting it breaks multi-tool turns.
-                    if (msg.role == QLatin1String("tool") && !msg.toolName.isEmpty()) {
-                        messageObj["tool_name"] = msg.toolName;
-                    }
-                }
-
-                if (msg.images && !msg.images->isEmpty()) {
-                    QJsonArray images;
-                    for (const auto &image : msg.images.value()) {
-                        images.append(image.data);
-                    }
-                    messageObj["images"] = images;
-                }
-                
-                messages.append(messageObj);
-            }
-        }
-
-        request["messages"] = messages;
-    }
-    QString description() const override
-    {
-        return "Template for Ollama Chat with message array format:\n\n"
-               "{\n"
-               "  \"messages\": [\n"
-               "    {\"role\": \"system\", \"content\": \"\"},\n"
-               "    {\"role\": \"user\", \"content\": \"\", \"images\": [\"\"]},\n"
-               "    {\"role\": \"assistant\", \"content\": \"\"}\n"
-               "  ]\n"
-               "}\n\n"
-               "Recommended for Ollama models with chat capability.\n"
-               "Supports images for multimodal models (e.g., llava).";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
-            return true;
-        default:
-            return false;
-        }
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/OpenAI.hpp b/templates/OpenAI.hpp
deleted file mode 100644
index 6762b3f..0000000
--- a/templates/OpenAI.hpp
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include "pluginllmcore/PromptTemplate.hpp"
-#include "templates/ToolMessages.hpp"
-
-namespace QodeAssist::Templates {
-
-class OpenAI : public PluginLLMCore::PromptTemplate
-{
-public:
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
-    QString name() const override { return "OpenAI"; }
-    QStringList stopWords() const override { return QStringList(); }
-    bool supportsToolHistory() const override { return true; }
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        QJsonArray messages;
-
-        if (context.systemPrompt) {
-            messages.append(
-                QJsonObject{{"role", "system"}, {"content", context.systemPrompt.value()}});
-        }
-
-        if (context.history) {
-            for (const auto &msg : context.history.value()) {
-                if (appendOpenAIToolMessage(messages, msg)) {
-                    continue;
-                }
-                if (msg.images && !msg.images->isEmpty()) {
-                    QJsonArray content;
-                    
-                    if (!msg.content.isEmpty()) {
-                        content.append(QJsonObject{{"type", "text"}, {"text", msg.content}});
-                    }
-                    
-                    for (const auto &image : msg.images.value()) {
-                        QJsonObject imageBlock;
-                        imageBlock["type"] = "image_url";
-                        
-                        QJsonObject imageUrl;
-                        if (image.isUrl) {
-                            imageUrl["url"] = image.data;
-                        } else {
-                            imageUrl["url"] = QString("data:%1;base64,%2").arg(image.mediaType, image.data);
-                        }
-                        imageBlock["image_url"] = imageUrl;
-                        content.append(imageBlock);
-                    }
-                    
-                    messages.append(QJsonObject{{"role", msg.role}, {"content", content}});
-                } else {
-                    messages.append(QJsonObject{{"role", msg.role}, {"content", msg.content}});
-                }
-            }
-        }
-
-        request["messages"] = messages;
-    }
-    QString description() const override
-    {
-        return "Template for OpenAI models (GPT series):\n\n"
-               "{\n"
-               "  \"messages\": [\n"
-               "    {\"role\": \"system\", \"content\": \"\"},\n"
-               "    {\"role\": \"user\", \"content\": \"\"},\n"
-               "    {\"role\": \"assistant\", \"content\": \"\"}\n"
-               "  ]\n"
-               "}\n\n"
-               "Standard Chat API format for OpenAI.";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        case QodeAssist::PluginLLMCore::ProviderID::OpenAI:
-            return true;
-        default:
-            return false;
-        }
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/OpenAICompatible.hpp b/templates/OpenAICompatible.hpp
deleted file mode 100644
index 7ef4b14..0000000
--- a/templates/OpenAICompatible.hpp
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include "pluginllmcore/PromptTemplate.hpp"
-#include "templates/ToolMessages.hpp"
-
-namespace QodeAssist::Templates {
-
-class OpenAICompatible : public PluginLLMCore::PromptTemplate
-{
-public:
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
-    QString name() const override { return "OpenAI Compatible"; }
-    QStringList stopWords() const override { return QStringList(); }
-    bool supportsToolHistory() const override { return true; }
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        QJsonArray messages;
-
-        if (context.systemPrompt) {
-            messages.append(
-                QJsonObject{{"role", "system"}, {"content", context.systemPrompt.value()}});
-        }
-
-        if (context.history) {
-            for (const auto &msg : context.history.value()) {
-                if (appendOpenAIToolMessage(messages, msg)) {
-                    continue;
-                }
-                if (msg.images && !msg.images->isEmpty()) {
-                    QJsonArray content;
-                    
-                    if (!msg.content.isEmpty()) {
-                        content.append(QJsonObject{{"type", "text"}, {"text", msg.content}});
-                    }
-                    
-                    for (const auto &image : msg.images.value()) {
-                        QJsonObject imageBlock;
-                        imageBlock["type"] = "image_url";
-                        
-                        QJsonObject imageUrl;
-                        if (image.isUrl) {
-                            imageUrl["url"] = image.data;
-                        } else {
-                            imageUrl["url"] = QString("data:%1;base64,%2").arg(image.mediaType, image.data);
-                        }
-                        imageBlock["image_url"] = imageUrl;
-                        content.append(imageBlock);
-                    }
-                    
-                    messages.append(QJsonObject{{"role", msg.role}, {"content", content}});
-                } else {
-                    messages.append(QJsonObject{{"role", msg.role}, {"content", msg.content}});
-                }
-            }
-        }
-
-        request["messages"] = messages;
-    }
-    QString description() const override
-    {
-        return "Generic template for OpenAI API-compatible services:\n\n"
-               "{\n"
-               "  \"messages\": [\n"
-               "    {\"role\": \"system\", \"content\": \"\"},\n"
-               "    {\"role\": \"user\", \"content\": \"\"},\n"
-               "    {\"role\": \"assistant\", \"content\": \"\"}\n"
-               "  ]\n"
-               "}\n\n"
-               "Works with any service implementing the OpenAI Chat API specification.\n"
-               "Supports images.";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        case PluginLLMCore::ProviderID::OpenAICompatible:
-        case PluginLLMCore::ProviderID::OpenRouter:
-        case PluginLLMCore::ProviderID::LMStudio:
-        case PluginLLMCore::ProviderID::LlamaCpp:
-        case PluginLLMCore::ProviderID::Qwen:
-        case PluginLLMCore::ProviderID::DeepSeek:
-            return true;
-        default:
-            return false;
-        }
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/OpenAIResponses.hpp b/templates/OpenAIResponses.hpp
deleted file mode 100644
index e1b417c..0000000
--- a/templates/OpenAIResponses.hpp
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include "pluginllmcore/PromptTemplate.hpp"
-
-#include 
-#include 
-#include 
-
-namespace QodeAssist::Templates {
-
-class OpenAIResponses : public PluginLLMCore::PromptTemplate
-{
-public:
-    PluginLLMCore::TemplateType type() const noexcept override
-    {
-        return PluginLLMCore::TemplateType::Chat;
-    }
-
-    QString name() const override { return "OpenAI Responses"; }
-
-    QStringList stopWords() const override { return {}; }
-
-    bool supportsToolHistory() const override { return true; }
-
-    void prepareRequest(
-        QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        if (context.systemPrompt) {
-            request["instructions"] = context.systemPrompt.value();
-        }
-
-        if (!context.history || context.history->isEmpty()) {
-            return;
-        }
-
-        QJsonArray input;
-        for (const auto &msg : context.history.value()) {
-            if (msg.role == "system") {
-                continue;
-            }
-
-            if (!msg.toolCalls.isEmpty()) {
-                if (!msg.content.isEmpty()) {
-                    input.append(QJsonObject{{"role", "assistant"}, {"content", msg.content}});
-                }
-                for (const auto &call : msg.toolCalls) {
-                    input.append(QJsonObject{
-                        {"type", "function_call"},
-                        {"call_id", call.id},
-                        {"name", call.name},
-                        {"arguments",
-                         QString::fromUtf8(
-                             QJsonDocument(call.arguments).toJson(QJsonDocument::Compact))}});
-                }
-                continue;
-            }
-
-            if (msg.role == "tool") {
-                input.append(QJsonObject{
-                    {"type", "function_call_output"},
-                    {"call_id", msg.toolCallId},
-                    {"output", msg.content}});
-                continue;
-            }
-
-            QJsonObject message;
-            message["role"] = msg.role;
-
-            const bool hasImages = msg.images && !msg.images->isEmpty();
-
-            if (!hasImages) {
-                message["content"] = msg.content;
-            } else {
-                QJsonArray content;
-                if (!msg.content.isEmpty()) {
-                    content.append(
-                        QJsonObject{{"type", "input_text"}, {"text", msg.content}});
-                }
-
-                for (const auto &image : msg.images.value()) {
-                    QJsonObject imgObj{{"type", "input_image"}, {"detail", "auto"}};
-                    if (image.isUrl) {
-                        imgObj["image_url"] = image.data;
-                    } else {
-                        imgObj["image_url"]
-                            = QString("data:%1;base64,%2").arg(image.mediaType, image.data);
-                    }
-                    content.append(imgObj);
-                }
-
-                message["content"] = content;
-            }
-
-            input.append(message);
-        }
-
-        request["input"] = input;
-    }
-
-    QString description() const override
-    {
-        return "Template for OpenAI Responses API:\n\n"
-               "Simple request:\n"
-               "{\n"
-               "  \"input\": \"\"\n"
-               "}\n\n"
-               "Multi-turn conversation:\n"
-               "{\n"
-               "  \"instructions\": \"\",\n"
-               "  \"input\": [\n"
-               "    {\"role\": \"user\", \"content\": \"\"}\n"
-               "  ]\n"
-               "}";
-    }
-
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const noexcept override
-    {
-        return id == QodeAssist::PluginLLMCore::ProviderID::OpenAIResponses;
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/Qwen25CoderFIM.hpp b/templates/Qwen25CoderFIM.hpp
deleted file mode 100644
index 08e7e9a..0000000
--- a/templates/Qwen25CoderFIM.hpp
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include "pluginllmcore/PromptTemplate.hpp"
-#include 
-
-namespace QodeAssist::Templates {
-
-class Qwen25CoderFIM : public PluginLLMCore::PromptTemplate
-{
-public:
-    QString name() const override { return "Qwen2.5 Coder FIM"; }
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
-    QString endpoint() const override { return QStringLiteral("/api/generate"); }
-    QStringList stopWords() const override { return QStringList() << "<|endoftext|>" << "<|EOT|>"; }
-    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(""));
-        request["system"] = context.systemPrompt.value_or("");
-    }
-    QString description() const override
-    {
-        return "Template for Qwen models with FIM support:\n\n"
-               "{\n"
-               "  \"prompt\": \"<|fim_prefix|><|fim_suffix|><|fim_middle|>\",\n"
-               "  \"system\": \"\"\n"
-               "}\n\n"
-               "Ideal for code completion with Qwen models.";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
-            return true;
-        default:
-            return false;
-        }
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/Qwen3CoderFIM.hpp b/templates/Qwen3CoderFIM.hpp
deleted file mode 100644
index d4dceee..0000000
--- a/templates/Qwen3CoderFIM.hpp
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-
-#include "pluginllmcore/PromptTemplate.hpp"
-
-namespace QodeAssist::Templates {
-
-class Qwen3CoderFIM : public PluginLLMCore::PromptTemplate
-{
-public:
-    QString name() const override { return "Qwen3 Coder FIM"; }
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIMOnChat; }
-    QStringList stopWords() const override { return QStringList() << "<|im_end|>"; }
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        QJsonArray messages;
-
-        messages.append(
-            QJsonObject{{"role", "system"}, {"content", context.systemPrompt.value_or("")}});
-
-        messages.append(
-            QJsonObject{
-                {"role", "user"},
-                {"content",
-                 QString("<|fim_prefix|>%1<|fim_suffix|>%2<|fim_middle|>")
-                     .arg(context.prefix.value_or(""), context.suffix.value_or(""))}});
-        request["messages"] = messages;
-    }
-    QString description() const override
-    {
-        return "Template for supporting Qwen3 Coder FIM format via chat template:\n\n"
-               "{\n"
-               "  \"messages\": [\n"
-               "    {\n"
-               "      \"role\": \"system\",\n"
-               "      \"content\": \"You are a code completion assistant.\"\n"
-               "    },\n"
-               "    {\n"
-               "      \"role\": \"user\",\n"
-               "      \"content\": \"<|fim_prefix|>code<|fim_suffix|>code<|fim_middle|>\"\n"
-               "    }\n"
-               "  ]\n"
-               "}\n\n";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        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;
-        }
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/StarCoder2Fim.hpp b/templates/StarCoder2Fim.hpp
deleted file mode 100644
index 8ebb02f..0000000
--- a/templates/StarCoder2Fim.hpp
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include "pluginllmcore/PromptTemplate.hpp"
-
-namespace QodeAssist::Templates {
-
-class StarCoder2Fim : public PluginLLMCore::PromptTemplate
-{
-public:
-    PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::FIM; }
-    QString name() const override { return "StarCoder2 FIM"; }
-    QString endpoint() const override { return QStringLiteral("/api/generate"); }
-    QStringList stopWords() const override
-    {
-        return QStringList() << "<|endoftext|>" << "" << "" << ""
-                             << "";
-    }
-    void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
-    {
-        request["prompt"] = QString("%1%2")
-                                .arg(context.prefix.value_or(""), context.suffix.value_or(""));
-        request["system"] = context.systemPrompt.value_or("");
-    }
-    QString description() const override
-    {
-        return "Template for StarCoder2 with FIM format:\n\n"
-               "{\n"
-               "  \"prompt\": \"\",\n"
-               "  \"system\": \"\"\n"
-               "}\n\n"
-               "Includes stop words to prevent token duplication.";
-    }
-    bool isSupportProvider(PluginLLMCore::ProviderID id) const override
-    {
-        switch (id) {
-        case QodeAssist::PluginLLMCore::ProviderID::Ollama:
-            return true;
-        default:
-            return false;
-        }
-    }
-};
-
-} // namespace QodeAssist::Templates
diff --git a/templates/Templates.hpp b/templates/Templates.hpp
deleted file mode 100644
index a35c9a1..0000000
--- a/templates/Templates.hpp
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include "pluginllmcore/PromptTemplateManager.hpp"
-#include "templates/Alpaca.hpp"
-#include "templates/ChatML.hpp"
-#include "templates/Claude.hpp"
-#include "templates/CodeLlamaFim.hpp"
-#include "templates/CodeLlamaQMLFim.hpp"
-#include "templates/MistralAI.hpp"
-#include "templates/Ollama.hpp"
-#include "templates/OpenAI.hpp"
-#include "templates/OpenAICompatible.hpp"
-#include "templates/OpenAIResponses.hpp"
-#include "templates/GoogleAI.hpp"
-#include "templates/Llama2.hpp"
-#include "templates/Llama3.hpp"
-#include "templates/LlamaCppFim.hpp"
-#include "templates/Qwen25CoderFIM.hpp"
-#include "templates/Qwen3CoderFIM.hpp"
-#include "templates/StarCoder2Fim.hpp"
-
-namespace QodeAssist::Templates {
-
-inline void registerTemplates()
-{
-    auto &templateManager = PluginLLMCore::PromptTemplateManager::instance();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-    templateManager.registerTemplate();
-}
-
-} // namespace QodeAssist::Templates
diff --git a/templates/ToolMessages.hpp b/templates/ToolMessages.hpp
deleted file mode 100644
index 9bc4f21..0000000
--- a/templates/ToolMessages.hpp
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#pragma once
-
-#include 
-#include 
-#include 
-#include 
-
-#include "pluginllmcore/ContextData.hpp"
-
-namespace QodeAssist::Templates {
-
-inline bool appendOpenAIToolMessage(QJsonArray &messages, const PluginLLMCore::Message &msg)
-{
-    if (!msg.toolCalls.isEmpty()) {
-        QJsonArray toolCalls;
-        for (const auto &call : msg.toolCalls) {
-            toolCalls.append(QJsonObject{
-                {"id", call.id},
-                {"type", "function"},
-                {"function",
-                 QJsonObject{
-                     {"name", call.name},
-                     {"arguments",
-                      QString::fromUtf8(
-                          QJsonDocument(call.arguments).toJson(QJsonDocument::Compact))}}}});
-        }
-        QJsonObject toolMessage{{"role", "assistant"}, {"tool_calls", toolCalls}};
-        toolMessage["content"] = msg.content.isEmpty() ? QJsonValue() : QJsonValue(msg.content);
-        messages.append(toolMessage);
-        return true;
-    }
-
-    if (msg.role == QLatin1String("tool")) {
-        messages.append(QJsonObject{
-            {"role", "tool"}, {"tool_call_id", msg.toolCallId}, {"content", msg.content}});
-        return true;
-    }
-
-    return false;
-}
-
-} // namespace QodeAssist::Templates
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 8140f69..e6d2fd5 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -2,7 +2,6 @@ add_executable(QodeAssistTest
     ../CodeHandler.cpp
     ../LLMSuggestion.cpp
     CodeHandlerTest.cpp
-    ClaudeCacheControlTest.cpp
     DocumentContextReaderTest.cpp
     LLMSuggestionTest.cpp
     # LLMClientInterfaceTests.cpp
@@ -17,7 +16,7 @@ target_link_libraries(QodeAssistTest PRIVATE
     GTest::Main
     QtCreator::LanguageClient
     Context
-    PluginLLMCore
+    Common
     LLMQore
 )
 
diff --git a/test/ClaudeCacheControlTest.cpp b/test/ClaudeCacheControlTest.cpp
deleted file mode 100644
index 86bf688..0000000
--- a/test/ClaudeCacheControlTest.cpp
+++ /dev/null
@@ -1,182 +0,0 @@
-// Copyright (C) 2024-2026 Petr Mironychev
-// SPDX-License-Identifier: GPL-3.0-or-later
-// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
-
-#include 
-
-#include 
-#include 
-
-#include "providers/ClaudeCacheControl.hpp"
-
-using namespace QodeAssist::Providers::ClaudeCacheControl;
-
-namespace {
-
-QJsonObject expectedEphemeral(bool extendedTtl)
-{
-    QJsonObject obj{{"type", "ephemeral"}};
-    if (extendedTtl)
-        obj["ttl"] = "1h";
-    return obj;
-}
-
-} // namespace
-
-TEST(ClaudeCacheControlTest, BreakpointWithoutExtendedTTL)
-{
-    const QJsonObject cc = buildBreakpoint(false);
-    EXPECT_EQ(cc.value("type").toString(), "ephemeral");
-    EXPECT_FALSE(cc.contains("ttl"));
-}
-
-TEST(ClaudeCacheControlTest, BreakpointWithExtendedTTL)
-{
-    const QJsonObject cc = buildBreakpoint(true);
-    EXPECT_EQ(cc.value("type").toString(), "ephemeral");
-    EXPECT_EQ(cc.value("ttl").toString(), "1h");
-}
-
-TEST(ClaudeCacheControlTest, SystemAsStringWrappedIntoArray)
-{
-    QJsonObject request;
-    request["system"] = "you are a helpful agent";
-
-    apply(request, false);
-
-    ASSERT_TRUE(request.value("system").isArray());
-    const QJsonArray sys = request.value("system").toArray();
-    ASSERT_EQ(sys.size(), 1);
-
-    const QJsonObject block = sys.first().toObject();
-    EXPECT_EQ(block.value("type").toString(), "text");
-    EXPECT_EQ(block.value("text").toString(), "you are a helpful agent");
-    EXPECT_EQ(block.value("cache_control").toObject(), expectedEphemeral(false));
-}
-
-TEST(ClaudeCacheControlTest, EmptySystemStringIsNotWrapped)
-{
-    QJsonObject request;
-    request["system"] = "";
-
-    apply(request, false);
-
-    EXPECT_TRUE(request.value("system").isString());
-}
-
-TEST(ClaudeCacheControlTest, SystemAsArrayMarksLastBlock)
-{
-    QJsonObject request;
-    request["system"] = QJsonArray{
-        QJsonObject{{"type", "text"}, {"text", "a"}},
-        QJsonObject{{"type", "text"}, {"text", "b"}}};
-
-    apply(request, false);
-
-    const QJsonArray sys = request.value("system").toArray();
-    ASSERT_EQ(sys.size(), 2);
-    EXPECT_FALSE(sys[0].toObject().contains("cache_control"));
-    EXPECT_EQ(sys[1].toObject().value("cache_control").toObject(), expectedEphemeral(false));
-}
-
-TEST(ClaudeCacheControlTest, ToolsLastEntryGetsCacheControl)
-{
-    QJsonObject request;
-    request["tools"] = QJsonArray{
-        QJsonObject{{"name", "read_file"}},
-        QJsonObject{{"name", "edit_file"}},
-        QJsonObject{{"name", "search"}}};
-
-    apply(request, true);
-
-    const QJsonArray tools = request.value("tools").toArray();
-    ASSERT_EQ(tools.size(), 3);
-    EXPECT_FALSE(tools[0].toObject().contains("cache_control"));
-    EXPECT_FALSE(tools[1].toObject().contains("cache_control"));
-    EXPECT_EQ(tools[2].toObject().value("cache_control").toObject(), expectedEphemeral(true));
-}
-
-TEST(ClaudeCacheControlTest, SingleMessageHistorySkipped)
-{
-    QJsonObject request;
-    request["messages"]
-        = QJsonArray{QJsonObject{{"role", "user"}, {"content", "first message"}}};
-
-    apply(request, false);
-
-    const QJsonArray msgs = request.value("messages").toArray();
-    ASSERT_EQ(msgs.size(), 1);
-    EXPECT_TRUE(msgs[0].toObject().value("content").isString());
-}
-
-TEST(ClaudeCacheControlTest, HistoryBreakpointOnSecondToLastMessage)
-{
-    QJsonObject request;
-    request["messages"] = QJsonArray{
-        QJsonObject{{"role", "user"}, {"content", "u1"}},
-        QJsonObject{{"role", "assistant"}, {"content", "a1"}},
-        QJsonObject{{"role", "user"}, {"content", "u2-current"}}};
-
-    apply(request, false);
-
-    const QJsonArray msgs = request.value("messages").toArray();
-    ASSERT_EQ(msgs.size(), 3);
-
-    EXPECT_TRUE(msgs[0].toObject().value("content").isString());
-
-    const QJsonArray a1Content = msgs[1].toObject().value("content").toArray();
-    ASSERT_EQ(a1Content.size(), 1);
-    EXPECT_EQ(a1Content.first().toObject().value("text").toString(), "a1");
-    EXPECT_EQ(
-        a1Content.first().toObject().value("cache_control").toObject(),
-        expectedEphemeral(false));
-
-    EXPECT_TRUE(msgs[2].toObject().value("content").isString());
-}
-
-TEST(ClaudeCacheControlTest, HistoryArrayContentMarksLastBlock)
-{
-    QJsonObject request;
-    request["messages"] = QJsonArray{
-        QJsonObject{
-            {"role", "user"},
-            {"content",
-             QJsonArray{
-                 QJsonObject{{"type", "text"}, {"text", "describe this"}},
-                 QJsonObject{{"type", "image"}}}}},
-        QJsonObject{{"role", "assistant"}, {"content", "ok"}}};
-
-    apply(request, false);
-
-    const QJsonArray msgs = request.value("messages").toArray();
-    const QJsonArray content = msgs[0].toObject().value("content").toArray();
-    ASSERT_EQ(content.size(), 2);
-    EXPECT_FALSE(content[0].toObject().contains("cache_control"));
-    EXPECT_EQ(content[1].toObject().value("cache_control").toObject(), expectedEphemeral(false));
-}
-
-TEST(ClaudeCacheControlTest, NoSystemNoToolsNoMessagesIsNoop)
-{
-    QJsonObject request;
-    request["model"] = "claude-sonnet-4-5";
-    request["max_tokens"] = 1024;
-
-    apply(request, false);
-
-    EXPECT_EQ(request.value("model").toString(), "claude-sonnet-4-5");
-    EXPECT_EQ(request.value("max_tokens").toInt(), 1024);
-    EXPECT_FALSE(request.contains("system"));
-    EXPECT_FALSE(request.contains("tools"));
-    EXPECT_FALSE(request.contains("messages"));
-}
-
-TEST(ClaudeCacheControlTest, EmptyToolsArrayIsNoop)
-{
-    QJsonObject request;
-    request["tools"] = QJsonArray{};
-
-    apply(request, false);
-
-    EXPECT_TRUE(request.value("tools").isArray());
-    EXPECT_TRUE(request.value("tools").toArray().isEmpty());
-}
diff --git a/test/DocumentContextReaderTest.cpp b/test/DocumentContextReaderTest.cpp
index 390e258..7d53cff 100644
--- a/test/DocumentContextReaderTest.cpp
+++ b/test/DocumentContextReaderTest.cpp
@@ -9,7 +9,7 @@
 #include 
 #include 
 
-namespace QodeAssist::PluginLLMCore {
+namespace QodeAssist::Templates {
 
 void PrintTo(const ContextData &data, std::ostream *os)
 {
@@ -20,10 +20,10 @@ void PrintTo(const ContextData &data, std::ostream *os)
         << "}";
 }
 
-} // namespace QodeAssist::PluginLLMCore
+} // namespace QodeAssist::Templates
 
 using namespace QodeAssist::Context;
-using namespace QodeAssist::PluginLLMCore;
+using namespace QodeAssist::Templates;
 using namespace QodeAssist::Settings;
 
 class DocumentContextReaderTest : public QObject, public testing::Test
@@ -367,7 +367,7 @@ TEST_F(DocumentContextReaderTest, testPrepareContext)
 
     EXPECT_EQ(
         reader.prepareContext(2, 3, *createSettingsForWholeFile()),
-        (QodeAssist::PluginLLMCore::ContextData{
+        (QodeAssist::Templates::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"
@@ -375,7 +375,7 @@ TEST_F(DocumentContextReaderTest, testPrepareContext)
 
     EXPECT_EQ(
         reader.prepareContext(2, 3, *createSettingsForLines(1, 1)),
-        (QodeAssist::PluginLLMCore::ContextData{
+        (QodeAssist::Templates::ContextData{
             .prefix = "Line 1\nLin",
             .suffix = "e 2\nLine 3",
             .fileContext = "\n Language:  (MIME: text/python) filepath: /path/to/file()\n\n"
@@ -383,7 +383,7 @@ TEST_F(DocumentContextReaderTest, testPrepareContext)
 
     EXPECT_EQ(
         reader.prepareContext(2, 3, *createSettingsForLines(2, 2)),
-        (QodeAssist::PluginLLMCore::ContextData{
+        (QodeAssist::Templates::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/TestUtils.hpp b/test/TestUtils.hpp
index 1fbb61c..78ec61e 100644
--- a/test/TestUtils.hpp
+++ b/test/TestUtils.hpp
@@ -3,7 +3,7 @@
 // Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
 
 #include 
-#include 
+#include 
 #include 
 
 QT_BEGIN_NAMESPACE
@@ -44,12 +44,11 @@ std::ostream &operator<<(std::ostream &out, const std::optional &value)
     return out;
 }
 
-namespace QodeAssist::PluginLLMCore {
+namespace QodeAssist::Templates {
 
 inline std::ostream &operator<<(std::ostream &out, const Message &value)
 {
-    out << "Message{"
-        << "role=" << value.role << "content=" << value.content << "}";
+    out << "Message{role=" << value.role << ", blocks=" << value.blocks.size() << "}";
     return out;
 }
 
@@ -62,4 +61,4 @@ inline std::ostream &operator<<(std::ostream &out, const ContextData &value)
     return out;
 }
 
-} // namespace QodeAssist::PluginLLMCore
+} // namespace QodeAssist::Templates