From ec1b5bdf5ff54fbe4bb5e04470f4976b430cdee1 Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Wed, 17 Sep 2025 19:38:27 +0200 Subject: [PATCH] refactor: Remove non-streaming support (#229) --- ChatView/ClientInterface.cpp | 6 +- LLMClientInterface.cpp | 5 +- QuickRefactorHandler.cpp | 3 +- llmcore/CMakeLists.txt | 2 + llmcore/DataBuffers.hpp | 39 ++++++ llmcore/Provider.cpp | 21 ++- llmcore/Provider.hpp | 9 +- llmcore/RequestType.hpp | 4 + llmcore/SSEBuffer.cpp | 51 +++++++ llmcore/SSEBuffer.hpp | 42 ++++++ providers/ClaudeProvider.cpp | 61 ++++---- providers/ClaudeProvider.hpp | 3 - providers/GoogleAIProvider.cpp | 210 ++++++---------------------- providers/GoogleAIProvider.hpp | 6 +- providers/LMStudioProvider.cpp | 47 ++++--- providers/LMStudioProvider.hpp | 3 - providers/LlamaCppProvider.cpp | 49 ++++--- providers/LlamaCppProvider.hpp | 3 - providers/MistralAIProvider.cpp | 47 ++++--- providers/MistralAIProvider.hpp | 3 - providers/OllamaProvider.cpp | 35 +++-- providers/OllamaProvider.hpp | 3 - providers/OpenAICompatProvider.cpp | 49 +++---- providers/OpenAICompatProvider.hpp | 3 - providers/OpenAIProvider.cpp | 47 ++++--- providers/OpenAIProvider.hpp | 3 - providers/OpenRouterAIProvider.cpp | 46 +++--- providers/OpenRouterAIProvider.hpp | 4 - settings/ChatAssistantSettings.cpp | 6 - settings/ChatAssistantSettings.hpp | 1 - settings/CodeCompletionSettings.cpp | 6 - settings/CodeCompletionSettings.hpp | 1 - settings/SettingsConstants.hpp | 2 - 33 files changed, 412 insertions(+), 408 deletions(-) create mode 100644 llmcore/DataBuffers.hpp create mode 100644 llmcore/SSEBuffer.cpp create mode 100644 llmcore/SSEBuffer.hpp diff --git a/ChatView/ClientInterface.cpp b/ChatView/ClientInterface.cpp index 4e8268c..64cb974 100644 --- a/ChatView/ClientInterface.cpp +++ b/ChatView/ClientInterface.cpp @@ -98,8 +98,7 @@ void ClientInterface::sendMessage( config.provider = provider; config.promptTemplate = promptTemplate; if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { - QString stream = chatAssistantSettings.stream() ? QString{"streamGenerateContent?alt=sse"} - : QString{"generateContent?"}; + QString stream = QString{"streamGenerateContent?alt=sse"}; config.url = QUrl(QString("%1/models/%2:%3") .arg( Settings::generalSettings().caUrl(), @@ -109,8 +108,7 @@ void ClientInterface::sendMessage( config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint()); config.providerRequest - = {{"model", Settings::generalSettings().caModel()}, - {"stream", chatAssistantSettings.stream()}}; + = {{"model", Settings::generalSettings().caModel()}, {"stream", true}}; } config.apiKey = provider->apiKey(); diff --git a/LLMClientInterface.cpp b/LLMClientInterface.cpp index ab8f9ef..92fae6d 100644 --- a/LLMClientInterface.cpp +++ b/LLMClientInterface.cpp @@ -224,13 +224,12 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request) config.promptTemplate = promptTemplate; // TODO refactor networking if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { - QString stream = m_completeSettings.stream() ? QString{"streamGenerateContent?alt=sse"} - : QString{"generateContent?"}; + QString stream = QString{"streamGenerateContent?alt=sse"}; config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream)); } else { config.url = QUrl( QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active))); - config.providerRequest = {{"model", modelName}, {"stream", m_completeSettings.stream()}}; + config.providerRequest = {{"model", modelName}, {"stream", true}}; } config.apiKey = provider->apiKey(); config.multiLineCompletion = m_completeSettings.multiLineCompletion(); diff --git a/QuickRefactorHandler.cpp b/QuickRefactorHandler.cpp index 8bd1039..ae8cda0 100644 --- a/QuickRefactorHandler.cpp +++ b/QuickRefactorHandler.cpp @@ -138,8 +138,7 @@ void QuickRefactorHandler::prepareAndSendRequest( config.provider = provider; config.promptTemplate = promptTemplate; config.url = QString("%1%2").arg(settings.caUrl(), provider->chatEndpoint()); - config.providerRequest - = {{"model", settings.caModel()}, {"stream", Settings::chatAssistantSettings().stream()}}; + config.providerRequest = {{"model", settings.caModel()}, {"stream", true}}; config.apiKey = provider->apiKey(); LLMCore::ContextData context = prepareContext(editor, range, instructions); diff --git a/llmcore/CMakeLists.txt b/llmcore/CMakeLists.txt index 66cba53..fdce389 100644 --- a/llmcore/CMakeLists.txt +++ b/llmcore/CMakeLists.txt @@ -15,6 +15,8 @@ add_library(LLMCore STATIC ValidationUtils.hpp ValidationUtils.cpp ProviderID.hpp HttpClient.hpp HttpClient.cpp + DataBuffers.hpp + SSEBuffer.hpp SSEBuffer.cpp ) target_link_libraries(LLMCore diff --git a/llmcore/DataBuffers.hpp b/llmcore/DataBuffers.hpp new file mode 100644 index 0000000..4ab5dee --- /dev/null +++ b/llmcore/DataBuffers.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include "SSEBuffer.hpp" +#include + +namespace QodeAssist::LLMCore { + +struct DataBuffers +{ + SSEBuffer rawStreamBuffer; + QString responseContent; + + void clear() + { + rawStreamBuffer.clear(); + responseContent.clear(); + } +}; + +} // namespace QodeAssist::LLMCore diff --git a/llmcore/Provider.cpp b/llmcore/Provider.cpp index f9d8e0f..897875e 100644 --- a/llmcore/Provider.cpp +++ b/llmcore/Provider.cpp @@ -1,18 +1,31 @@ #include "Provider.hpp" +#include + namespace QodeAssist::LLMCore { Provider::Provider(QObject *parent) : QObject(parent) - , m_httpClient(std::make_unique()) + , m_httpClient(new HttpClient(this)) { - connect(m_httpClient.get(), &HttpClient::dataReceived, this, &Provider::onDataReceived); - connect(m_httpClient.get(), &HttpClient::requestFinished, this, &Provider::onRequestFinished); + connect(m_httpClient, &HttpClient::dataReceived, this, &Provider::onDataReceived); + connect(m_httpClient, &HttpClient::requestFinished, this, &Provider::onRequestFinished); } HttpClient *Provider::httpClient() const { - return m_httpClient.get(); + return m_httpClient; +} + +QJsonObject Provider::parseEventLine(const QString &line) +{ + if (!line.startsWith("data: ")) + return QJsonObject(); + + QString jsonStr = line.mid(6); + + QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8()); + return doc.object(); } } // namespace QodeAssist::LLMCore diff --git a/llmcore/Provider.hpp b/llmcore/Provider.hpp index 7207c47..5a213cc 100644 --- a/llmcore/Provider.hpp +++ b/llmcore/Provider.hpp @@ -25,6 +25,7 @@ #include #include "ContextData.hpp" +#include "DataBuffers.hpp" #include "HttpClient.hpp" #include "PromptTemplate.hpp" #include "RequestType.hpp" @@ -73,8 +74,14 @@ signals: void fullResponseReceived(const QString &requestId, const QString &fullText); void requestFailed(const QString &requestId, const QString &error); +protected: + QJsonObject parseEventLine(const QString &line); + + QHash m_dataBuffers; + QHash m_requestUrls; + private: - std::unique_ptr m_httpClient; + HttpClient *m_httpClient; }; } // namespace QodeAssist::LLMCore diff --git a/llmcore/RequestType.hpp b/llmcore/RequestType.hpp index 991eca7..ab77c54 100644 --- a/llmcore/RequestType.hpp +++ b/llmcore/RequestType.hpp @@ -17,9 +17,13 @@ * along with QodeAssist. If not, see . */ +#include + #pragma once namespace QodeAssist::LLMCore { enum RequestType { CodeCompletion, Chat, Embedding }; + +using RequestID = QString; } diff --git a/llmcore/SSEBuffer.cpp b/llmcore/SSEBuffer.cpp new file mode 100644 index 0000000..bc53c29 --- /dev/null +++ b/llmcore/SSEBuffer.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#include "SSEBuffer.hpp" + +namespace QodeAssist::LLMCore { + +QStringList SSEBuffer::processData(const QByteArray &data) +{ + m_buffer += QString::fromUtf8(data); + + QStringList lines = m_buffer.split('\n'); + m_buffer = lines.takeLast(); + + lines.removeAll(QString()); + + return lines; +} + +void SSEBuffer::clear() +{ + m_buffer.clear(); +} + +QString SSEBuffer::currentBuffer() const +{ + return m_buffer; +} + +bool SSEBuffer::hasIncompleteData() const +{ + return !m_buffer.isEmpty(); +} + +} // namespace QodeAssist::LLMCore diff --git a/llmcore/SSEBuffer.hpp b/llmcore/SSEBuffer.hpp new file mode 100644 index 0000000..1f05572 --- /dev/null +++ b/llmcore/SSEBuffer.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2025 Petr Mironychev + * + * This file is part of QodeAssist. + * + * QodeAssist is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QodeAssist is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QodeAssist. If not, see . + */ + +#pragma once + +#include +#include + +namespace QodeAssist::LLMCore { + +class SSEBuffer +{ +public: + SSEBuffer() = default; + + QStringList processData(const QByteArray &data); + + void clear(); + QString currentBuffer() const; + bool hasIncompleteData() const; + +private: + QString m_buffer; +}; + +} // namespace QodeAssist::LLMCore diff --git a/providers/ClaudeProvider.cpp b/providers/ClaudeProvider.cpp index 4650f5b..ca444c9 100644 --- a/providers/ClaudeProvider.cpp +++ b/providers/ClaudeProvider.cpp @@ -174,6 +174,9 @@ LLMCore::ProviderID ClaudeProvider::providerID() const void ClaudeProvider::sendRequest( const QString &requestId, const QUrl &url, const QJsonObject &payload) { + m_dataBuffers[requestId].clear(); + m_requestUrls[requestId] = url; + QNetworkRequest networkRequest(url); prepareNetworkRequest(networkRequest); @@ -187,50 +190,49 @@ void ClaudeProvider::sendRequest( void ClaudeProvider::onDataReceived(const QString &requestId, const QByteArray &data) { - QString &accumulatedResponse = m_accumulatedResponses[requestId]; + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + QStringList lines = buffers.rawStreamBuffer.processData(data); + QString tempResponse; bool isComplete = false; - QByteArrayList lines = data.split('\n'); - for (const QByteArray &line : lines) { - QByteArray trimmedLine = line.trimmed(); - if (trimmedLine.isEmpty()) + for (const QString &line : lines) { + QJsonObject responseObj = parseEventLine(line); + if (responseObj.isEmpty()) continue; - if (!trimmedLine.startsWith("data:")) - continue; - trimmedLine = trimmedLine.mid(6); - - QJsonDocument jsonResponse = QJsonDocument::fromJson(trimmedLine); - if (jsonResponse.isNull()) - continue; - - QJsonObject responseObj = jsonResponse.object(); QString eventType = responseObj["type"].toString(); - if (eventType == "message_delta") { - if (responseObj.contains("delta")) { - QJsonObject delta = responseObj["delta"].toObject(); - if (delta.contains("stop_reason")) { - isComplete = true; - } - } + if (eventType == "message_start") { + QString messageId = responseObj["message"].toObject()["id"].toString(); + LOG_MESSAGE(QString("Claude message started: %1").arg(messageId)); + } else if (eventType == "content_block_delta") { QJsonObject delta = responseObj["delta"].toObject(); if (delta["type"].toString() == "text_delta") { tempResponse += delta["text"].toString(); } + + } else if (eventType == "message_delta") { + QJsonObject delta = responseObj["delta"].toObject(); + if (delta.contains("stop_reason")) { + isComplete = true; + QJsonObject usage = responseObj["usage"].toObject(); + LOG_MESSAGE(QString("Tokens: input=%1, output=%2") + .arg(usage["input_tokens"].toInt()) + .arg(usage["output_tokens"].toInt())); + } } } if (!tempResponse.isEmpty()) { - accumulatedResponse += tempResponse; + buffers.responseContent += tempResponse; emit partialResponseReceived(requestId, tempResponse); } if (isComplete) { - emit fullResponseReceived(requestId, accumulatedResponse); - m_accumulatedResponses.remove(requestId); + emit fullResponseReceived(requestId, buffers.responseContent); + m_dataBuffers.remove(requestId); } } @@ -240,15 +242,16 @@ void ClaudeProvider::onRequestFinished(const QString &requestId, bool success, c LOG_MESSAGE(QString("ClaudeProvider request %1 failed: %2").arg(requestId, error)); emit requestFailed(requestId, error); } else { - if (m_accumulatedResponses.contains(requestId)) { - const QString fullResponse = m_accumulatedResponses[requestId]; - if (!fullResponse.isEmpty()) { - emit fullResponseReceived(requestId, fullResponse); + if (m_dataBuffers.contains(requestId)) { + const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + if (!buffers.responseContent.isEmpty()) { + emit fullResponseReceived(requestId, buffers.responseContent); } } } - m_accumulatedResponses.remove(requestId); + m_dataBuffers.remove(requestId); + m_requestUrls.remove(requestId); } } // namespace QodeAssist::Providers diff --git a/providers/ClaudeProvider.hpp b/providers/ClaudeProvider.hpp index 465e7c1..686fdc4 100644 --- a/providers/ClaudeProvider.hpp +++ b/providers/ClaudeProvider.hpp @@ -47,9 +47,6 @@ public: public slots: void onDataReceived(const QString &requestId, const QByteArray &data) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override; - -private: - QHash m_accumulatedResponses; }; } // namespace QodeAssist::Providers diff --git a/providers/GoogleAIProvider.cpp b/providers/GoogleAIProvider.cpp index 59794eb..09dadb6 100644 --- a/providers/GoogleAIProvider.cpp +++ b/providers/GoogleAIProvider.cpp @@ -172,6 +172,9 @@ LLMCore::ProviderID GoogleAIProvider::providerID() const void GoogleAIProvider::sendRequest( const QString &requestId, const QUrl &url, const QJsonObject &payload) { + m_dataBuffers[requestId].clear(); + m_requestUrls[requestId] = url; + QNetworkRequest networkRequest(url); prepareNetworkRequest(networkRequest); @@ -186,8 +189,6 @@ void GoogleAIProvider::sendRequest( void GoogleAIProvider::onDataReceived(const QString &requestId, const QByteArray &data) { - QString &accumulatedResponse = m_accumulatedResponses[requestId]; - if (data.isEmpty()) { return; } @@ -205,204 +206,85 @@ void GoogleAIProvider::onDataReceived(const QString &requestId, const QByteArray LOG_MESSAGE(fullError); emit requestFailed(requestId, fullError); - m_accumulatedResponses.remove(requestId); + m_dataBuffers.remove(requestId); return; } } - bool isDone = false; - - if (data.startsWith("data: ")) { - isDone = handleStreamResponse(requestId, data, accumulatedResponse); - } else { - isDone = handleRegularResponse(requestId, data, accumulatedResponse); - } + bool isDone = handleStreamResponse(requestId, data); if (isDone) { - emit fullResponseReceived(requestId, accumulatedResponse); - m_accumulatedResponses.remove(requestId); + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + emit fullResponseReceived(requestId, buffers.responseContent); + m_dataBuffers.remove(requestId); } } void GoogleAIProvider::onRequestFinished(const QString &requestId, bool success, const QString &error) { if (!success) { - QString detailedError = error; - - if (m_accumulatedResponses.contains(requestId)) { - const QString response = m_accumulatedResponses[requestId]; - if (!response.isEmpty()) { - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8(), &parseError); - if (!doc.isNull() && doc.isObject()) { - QJsonObject obj = doc.object(); - if (obj.contains("error")) { - QJsonObject errorObj = obj["error"].toObject(); - QString apiError = errorObj["message"].toString(); - int errorCode = errorObj["code"].toInt(); - detailedError - = QString("Google AI API Error %1: %2").arg(errorCode).arg(apiError); - } - } - } - } - - LOG_MESSAGE(QString("GoogleAIProvider request %1 failed: %2").arg(requestId, detailedError)); - emit requestFailed(requestId, detailedError); + LOG_MESSAGE(QString("GoogleAIProvider request %1 failed: %2").arg(requestId, error)); + emit requestFailed(requestId, error); } else { - if (m_accumulatedResponses.contains(requestId)) { - const QString fullResponse = m_accumulatedResponses[requestId]; - if (!fullResponse.isEmpty()) { - emit fullResponseReceived(requestId, fullResponse); + if (m_dataBuffers.contains(requestId)) { + const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + if (!buffers.responseContent.isEmpty()) { + emit fullResponseReceived(requestId, buffers.responseContent); } } } - m_accumulatedResponses.remove(requestId); + m_dataBuffers.remove(requestId); + m_requestUrls.remove(requestId); } -bool GoogleAIProvider::handleStreamResponse( - const QString &requestId, const QByteArray &data, QString &accumulatedResponse) +bool GoogleAIProvider::handleStreamResponse(const QString &requestId, const QByteArray &data) { - QByteArrayList lines = data.split('\n'); + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + QStringList lines = buffers.rawStreamBuffer.processData(data); + bool isDone = false; + QString tempResponse; - for (const QByteArray &line : lines) { - QByteArray trimmedLine = line.trimmed(); - if (trimmedLine.isEmpty()) { + for (const QString &line : lines) { + if (line.trimmed().isEmpty()) { continue; } - if (trimmedLine == "data: [DONE]") { - isDone = true; + QJsonObject responseObj = parseEventLine(line); + if (responseObj.isEmpty()) continue; - } - if (trimmedLine.startsWith("data: ")) { - QByteArray jsonData = trimmedLine.mid(6); - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError); - if (doc.isNull() || !doc.isObject()) { - if (parseError.error != QJsonParseError::NoError) { - LOG_MESSAGE(QString("JSON parse error in GoogleAI stream: %1") - .arg(parseError.errorString())); - } - continue; - } - - QJsonObject responseObj = doc.object(); - - if (responseObj.contains("error")) { - QJsonObject error = responseObj["error"].toObject(); - QString errorMessage = error["message"].toString(); - int errorCode = error["code"].toInt(); - QString fullError - = QString("Google AI Stream Error %1: %2").arg(errorCode).arg(errorMessage); - - LOG_MESSAGE(fullError); - emit requestFailed(requestId, fullError); - return true; - } - - if (responseObj.contains("candidates")) { - QJsonArray candidates = responseObj["candidates"].toArray(); - if (!candidates.isEmpty()) { - QJsonObject candidate = candidates.first().toObject(); - - if (candidate.contains("finishReason") - && !candidate["finishReason"].toString().isEmpty()) { - isDone = true; - } - - if (candidate.contains("content")) { - QJsonObject content = candidate["content"].toObject(); - if (content.contains("parts")) { - QJsonArray parts = content["parts"].toArray(); - QString partialContent; - for (const auto &part : parts) { - QJsonObject partObj = part.toObject(); - if (partObj.contains("text")) { - partialContent += partObj["text"].toString(); - } - } - if (!partialContent.isEmpty()) { - accumulatedResponse += partialContent; - emit partialResponseReceived(requestId, partialContent); + if (responseObj.contains("candidates")) { + QJsonArray candidates = responseObj["candidates"].toArray(); + for (const QJsonValue &candidate : candidates) { + QJsonObject candidateObj = candidate.toObject(); + if (candidateObj.contains("content")) { + QJsonObject content = candidateObj["content"].toObject(); + if (content.contains("parts")) { + QJsonArray parts = content["parts"].toArray(); + for (const QJsonValue &part : parts) { + QJsonObject partObj = part.toObject(); + if (partObj.contains("text")) { + tempResponse += partObj["text"].toString(); } } } } + + if (candidateObj.contains("finishReason")) { + isDone = true; + } } } } + if (!tempResponse.isEmpty()) { + buffers.responseContent += tempResponse; + emit partialResponseReceived(requestId, tempResponse); + } + return isDone; } -bool GoogleAIProvider::handleRegularResponse( - const QString &requestId, const QByteArray &data, QString &accumulatedResponse) -{ - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); - if (doc.isNull() || !doc.isObject()) { - QString error - = QString("Invalid JSON response from Google AI API: %1").arg(parseError.errorString()); - LOG_MESSAGE(error); - emit requestFailed(requestId, error); - return false; - } - - QJsonObject response = doc.object(); - - if (response.contains("error")) { - QJsonObject error = response["error"].toObject(); - QString errorMessage = error["message"].toString(); - int errorCode = error["code"].toInt(); - QString fullError = QString("Google AI API Error %1: %2").arg(errorCode).arg(errorMessage); - - LOG_MESSAGE(fullError); - emit requestFailed(requestId, fullError); - return false; - } - - if (!response.contains("candidates") || response["candidates"].toArray().isEmpty()) { - QString error = "No candidates in Google AI response"; - LOG_MESSAGE(error); - emit requestFailed(requestId, error); - return false; - } - - QJsonObject candidate = response["candidates"].toArray().first().toObject(); - if (!candidate.contains("content")) { - QString error = "No content in Google AI response candidate"; - LOG_MESSAGE(error); - emit requestFailed(requestId, error); - return false; - } - - QJsonObject content = candidate["content"].toObject(); - if (!content.contains("parts")) { - QString error = "No parts in Google AI response content"; - LOG_MESSAGE(error); - emit requestFailed(requestId, error); - return false; - } - - QJsonArray parts = content["parts"].toArray(); - QString responseContent; - for (const auto &part : parts) { - QJsonObject partObj = part.toObject(); - if (partObj.contains("text")) { - responseContent += partObj["text"].toString(); - } - } - - if (!responseContent.isEmpty()) { - accumulatedResponse += responseContent; - emit partialResponseReceived(requestId, responseContent); - } - - return true; -} - } // namespace QodeAssist::Providers diff --git a/providers/GoogleAIProvider.hpp b/providers/GoogleAIProvider.hpp index 5af70e9..a595489 100644 --- a/providers/GoogleAIProvider.hpp +++ b/providers/GoogleAIProvider.hpp @@ -49,11 +49,7 @@ public slots: void onRequestFinished(const QString &requestId, bool success, const QString &error) override; private: - QHash m_accumulatedResponses; - bool handleStreamResponse( - const QString &requestId, const QByteArray &data, QString &accumulatedResponse); - bool handleRegularResponse( - const QString &requestId, const QByteArray &data, QString &accumulatedResponse); + bool handleStreamResponse(const QString &requestId, const QByteArray &data); }; } // namespace QodeAssist::Providers diff --git a/providers/LMStudioProvider.cpp b/providers/LMStudioProvider.cpp index 27b4a68..bf867ab 100644 --- a/providers/LMStudioProvider.cpp +++ b/providers/LMStudioProvider.cpp @@ -125,6 +125,9 @@ LLMCore::ProviderID LMStudioProvider::providerID() const void LMStudioProvider::sendRequest( const QString &requestId, const QUrl &url, const QJsonObject &payload) { + m_dataBuffers[requestId].clear(); + m_requestUrls[requestId] = url; + QNetworkRequest networkRequest(url); prepareNetworkRequest(networkRequest); @@ -139,16 +142,17 @@ void LMStudioProvider::sendRequest( void LMStudioProvider::onDataReceived(const QString &requestId, const QByteArray &data) { - QString &accumulatedResponse = m_accumulatedResponses[requestId]; + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + QStringList lines = buffers.rawStreamBuffer.processData(data); if (data.isEmpty()) { return; } bool isDone = false; - QByteArrayList lines = data.split('\n'); + QString tempResponse; - for (const QByteArray &line : lines) { + for (const QString &line : lines) { if (line.trimmed().isEmpty()) { continue; } @@ -158,19 +162,11 @@ void LMStudioProvider::onDataReceived(const QString &requestId, const QByteArray continue; } - QByteArray jsonData = line; - if (line.startsWith("data: ")) { - jsonData = line.mid(6); - } - - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error); - - if (doc.isNull()) { + QJsonObject responseObj = parseEventLine(line); + if (responseObj.isEmpty()) continue; - } - auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); + auto message = LLMCore::OpenAIMessage::fromJson(responseObj); if (message.hasError()) { LOG_MESSAGE("Error in LMStudio response: " + message.error); continue; @@ -178,8 +174,7 @@ void LMStudioProvider::onDataReceived(const QString &requestId, const QByteArray QString content = message.getContent(); if (!content.isEmpty()) { - accumulatedResponse += content; - emit partialResponseReceived(requestId, content); + tempResponse += content; } if (message.isDone()) { @@ -187,9 +182,14 @@ void LMStudioProvider::onDataReceived(const QString &requestId, const QByteArray } } + if (!tempResponse.isEmpty()) { + buffers.responseContent += tempResponse; + emit partialResponseReceived(requestId, tempResponse); + } + if (isDone) { - emit fullResponseReceived(requestId, accumulatedResponse); - m_accumulatedResponses.remove(requestId); + emit fullResponseReceived(requestId, buffers.responseContent); + m_dataBuffers.remove(requestId); } } @@ -199,15 +199,16 @@ void LMStudioProvider::onRequestFinished(const QString &requestId, bool success, LOG_MESSAGE(QString("LMStudioProvider request %1 failed: %2").arg(requestId, error)); emit requestFailed(requestId, error); } else { - if (m_accumulatedResponses.contains(requestId)) { - const QString fullResponse = m_accumulatedResponses[requestId]; - if (!fullResponse.isEmpty()) { - emit fullResponseReceived(requestId, fullResponse); + if (m_dataBuffers.contains(requestId)) { + const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + if (!buffers.responseContent.isEmpty()) { + emit fullResponseReceived(requestId, buffers.responseContent); } } } - m_accumulatedResponses.remove(requestId); + m_dataBuffers.remove(requestId); + m_requestUrls.remove(requestId); } void QodeAssist::Providers::LMStudioProvider::prepareRequest( diff --git a/providers/LMStudioProvider.hpp b/providers/LMStudioProvider.hpp index d2c7900..62d096d 100644 --- a/providers/LMStudioProvider.hpp +++ b/providers/LMStudioProvider.hpp @@ -47,9 +47,6 @@ public: public slots: void onDataReceived(const QString &requestId, const QByteArray &data) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override; - -private: - QHash m_accumulatedResponses; }; } // namespace QodeAssist::Providers diff --git a/providers/LlamaCppProvider.cpp b/providers/LlamaCppProvider.cpp index f6bdc00..3a415dd 100644 --- a/providers/LlamaCppProvider.cpp +++ b/providers/LlamaCppProvider.cpp @@ -151,6 +151,9 @@ LLMCore::ProviderID LlamaCppProvider::providerID() const void LlamaCppProvider::sendRequest( const QString &requestId, const QUrl &url, const QJsonObject &payload) { + m_dataBuffers[requestId].clear(); + m_requestUrls[requestId] = url; + QNetworkRequest networkRequest(url); prepareNetworkRequest(networkRequest); @@ -165,16 +168,17 @@ void LlamaCppProvider::sendRequest( void LlamaCppProvider::onDataReceived(const QString &requestId, const QByteArray &data) { - QString &accumulatedResponse = m_accumulatedResponses[requestId]; + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + QStringList lines = buffers.rawStreamBuffer.processData(data); if (data.isEmpty()) { return; } bool isDone = data.contains("\"stop\":true") || data.contains("data: [DONE]"); + QString tempResponse; - QByteArrayList lines = data.split('\n'); - for (const QByteArray &line : lines) { + for (const QString &line : lines) { if (line.trimmed().isEmpty()) { continue; } @@ -184,25 +188,15 @@ void LlamaCppProvider::onDataReceived(const QString &requestId, const QByteArray continue; } - QByteArray jsonData = line; - if (line.startsWith("data: ")) { - jsonData = line.mid(6); - } - - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error); - if (doc.isNull()) { + QJsonObject obj = parseEventLine(line); + if (obj.isEmpty()) continue; - } - - QJsonObject obj = doc.object(); QString content; if (obj.contains("content")) { content = obj["content"].toString(); if (!content.isEmpty()) { - accumulatedResponse += content; - emit partialResponseReceived(requestId, content); + tempResponse += content; } } else if (obj.contains("choices")) { auto message = LLMCore::OpenAIMessage::fromJson(obj); @@ -213,8 +207,7 @@ void LlamaCppProvider::onDataReceived(const QString &requestId, const QByteArray content = message.getContent(); if (!content.isEmpty()) { - accumulatedResponse += content; - emit partialResponseReceived(requestId, content); + tempResponse += content; } if (message.isDone()) { @@ -227,9 +220,14 @@ void LlamaCppProvider::onDataReceived(const QString &requestId, const QByteArray } } + if (!tempResponse.isEmpty()) { + buffers.responseContent += tempResponse; + emit partialResponseReceived(requestId, tempResponse); + } + if (isDone) { - emit fullResponseReceived(requestId, accumulatedResponse); - m_accumulatedResponses.remove(requestId); + emit fullResponseReceived(requestId, buffers.responseContent); + m_dataBuffers.remove(requestId); } } @@ -239,15 +237,16 @@ void LlamaCppProvider::onRequestFinished(const QString &requestId, bool success, LOG_MESSAGE(QString("LlamaCppProvider request %1 failed: %2").arg(requestId, error)); emit requestFailed(requestId, error); } else { - if (m_accumulatedResponses.contains(requestId)) { - const QString fullResponse = m_accumulatedResponses[requestId]; - if (!fullResponse.isEmpty()) { - emit fullResponseReceived(requestId, fullResponse); + if (m_dataBuffers.contains(requestId)) { + const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + if (!buffers.responseContent.isEmpty()) { + emit fullResponseReceived(requestId, buffers.responseContent); } } } - m_accumulatedResponses.remove(requestId); + m_dataBuffers.remove(requestId); + m_requestUrls.remove(requestId); } } // namespace QodeAssist::Providers diff --git a/providers/LlamaCppProvider.hpp b/providers/LlamaCppProvider.hpp index 8c4425b..740454c 100644 --- a/providers/LlamaCppProvider.hpp +++ b/providers/LlamaCppProvider.hpp @@ -47,9 +47,6 @@ public: public slots: void onDataReceived(const QString &requestId, const QByteArray &data) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override; - -private: - QHash m_accumulatedResponses; }; } // namespace QodeAssist::Providers diff --git a/providers/MistralAIProvider.cpp b/providers/MistralAIProvider.cpp index b9d3ab3..df766b0 100644 --- a/providers/MistralAIProvider.cpp +++ b/providers/MistralAIProvider.cpp @@ -128,6 +128,9 @@ LLMCore::ProviderID MistralAIProvider::providerID() const void MistralAIProvider::sendRequest( const QString &requestId, const QUrl &url, const QJsonObject &payload) { + m_dataBuffers[requestId].clear(); + m_requestUrls[requestId] = url; + QNetworkRequest networkRequest(url); prepareNetworkRequest(networkRequest); @@ -142,16 +145,17 @@ void MistralAIProvider::sendRequest( void MistralAIProvider::onDataReceived(const QString &requestId, const QByteArray &data) { - QString &accumulatedResponse = m_accumulatedResponses[requestId]; + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + QStringList lines = buffers.rawStreamBuffer.processData(data); if (data.isEmpty()) { return; } bool isDone = false; - QByteArrayList lines = data.split('\n'); + QString tempResponse; - for (const QByteArray &line : lines) { + for (const QString &line : lines) { if (line.trimmed().isEmpty()) { continue; } @@ -161,19 +165,11 @@ void MistralAIProvider::onDataReceived(const QString &requestId, const QByteArra continue; } - QByteArray jsonData = line; - if (line.startsWith("data: ")) { - jsonData = line.mid(6); - } - - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error); - - if (doc.isNull()) { + QJsonObject responseObj = parseEventLine(line); + if (responseObj.isEmpty()) continue; - } - auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); + auto message = LLMCore::OpenAIMessage::fromJson(responseObj); if (message.hasError()) { LOG_MESSAGE("Error in MistralAI response: " + message.error); continue; @@ -181,8 +177,7 @@ void MistralAIProvider::onDataReceived(const QString &requestId, const QByteArra QString content = message.getContent(); if (!content.isEmpty()) { - accumulatedResponse += content; - emit partialResponseReceived(requestId, content); + tempResponse += content; } if (message.isDone()) { @@ -190,9 +185,14 @@ void MistralAIProvider::onDataReceived(const QString &requestId, const QByteArra } } + if (!tempResponse.isEmpty()) { + buffers.responseContent += tempResponse; + emit partialResponseReceived(requestId, tempResponse); + } + if (isDone) { - emit fullResponseReceived(requestId, accumulatedResponse); - m_accumulatedResponses.remove(requestId); + emit fullResponseReceived(requestId, buffers.responseContent); + m_dataBuffers.remove(requestId); } } @@ -203,15 +203,16 @@ void MistralAIProvider::onRequestFinished( LOG_MESSAGE(QString("MistralAIProvider request %1 failed: %2").arg(requestId, error)); emit requestFailed(requestId, error); } else { - if (m_accumulatedResponses.contains(requestId)) { - const QString fullResponse = m_accumulatedResponses[requestId]; - if (!fullResponse.isEmpty()) { - emit fullResponseReceived(requestId, fullResponse); + if (m_dataBuffers.contains(requestId)) { + const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + if (!buffers.responseContent.isEmpty()) { + emit fullResponseReceived(requestId, buffers.responseContent); } } } - m_accumulatedResponses.remove(requestId); + m_dataBuffers.remove(requestId); + m_requestUrls.remove(requestId); } void MistralAIProvider::prepareRequest( diff --git a/providers/MistralAIProvider.hpp b/providers/MistralAIProvider.hpp index f338358..14cbd1b 100644 --- a/providers/MistralAIProvider.hpp +++ b/providers/MistralAIProvider.hpp @@ -47,9 +47,6 @@ public: public slots: void onDataReceived(const QString &requestId, const QByteArray &data) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override; - -private: - QHash m_accumulatedResponses; }; } // namespace QodeAssist::Providers diff --git a/providers/OllamaProvider.cpp b/providers/OllamaProvider.cpp index d46dbe9..07fd75e 100644 --- a/providers/OllamaProvider.cpp +++ b/providers/OllamaProvider.cpp @@ -188,6 +188,9 @@ LLMCore::ProviderID OllamaProvider::providerID() const void OllamaProvider::sendRequest( const QString &requestId, const QUrl &url, const QJsonObject &payload) { + m_dataBuffers[requestId].clear(); + m_requestUrls[requestId] = url; + QNetworkRequest networkRequest(url); prepareNetworkRequest(networkRequest); @@ -201,22 +204,23 @@ void OllamaProvider::sendRequest( void OllamaProvider::onDataReceived(const QString &requestId, const QByteArray &data) { - QString &accumulatedResponse = m_accumulatedResponses[requestId]; + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + QStringList lines = buffers.rawStreamBuffer.processData(data); if (data.isEmpty()) { return; } - QByteArrayList lines = data.split('\n'); bool isDone = false; + QString tempResponse; - for (const QByteArray &line : lines) { + for (const QString &line : lines) { if (line.trimmed().isEmpty()) { continue; } QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(line, &error); + QJsonDocument doc = QJsonDocument::fromJson(line.toUtf8(), &error); if (doc.isNull()) { continue; } @@ -238,8 +242,7 @@ void OllamaProvider::onDataReceived(const QString &requestId, const QByteArray & } if (!content.isEmpty()) { - accumulatedResponse += content; - emit partialResponseReceived(requestId, content); + tempResponse += content; } if (obj["done"].toBool()) { @@ -247,9 +250,14 @@ void OllamaProvider::onDataReceived(const QString &requestId, const QByteArray & } } + if (!tempResponse.isEmpty()) { + buffers.responseContent += tempResponse; + emit partialResponseReceived(requestId, tempResponse); + } + if (isDone) { - emit fullResponseReceived(requestId, accumulatedResponse); - m_accumulatedResponses.remove(requestId); + emit fullResponseReceived(requestId, buffers.responseContent); + m_dataBuffers.remove(requestId); } } @@ -259,15 +267,16 @@ void OllamaProvider::onRequestFinished(const QString &requestId, bool success, c LOG_MESSAGE(QString("OllamaProvider request %1 failed: %2").arg(requestId, error)); emit requestFailed(requestId, error); } else { - if (m_accumulatedResponses.contains(requestId)) { - const QString fullResponse = m_accumulatedResponses[requestId]; - if (!fullResponse.isEmpty()) { - emit fullResponseReceived(requestId, fullResponse); + if (m_dataBuffers.contains(requestId)) { + const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + if (!buffers.responseContent.isEmpty()) { + emit fullResponseReceived(requestId, buffers.responseContent); } } } - m_accumulatedResponses.remove(requestId); + m_dataBuffers.remove(requestId); + m_requestUrls.remove(requestId); } } // namespace QodeAssist::Providers diff --git a/providers/OllamaProvider.hpp b/providers/OllamaProvider.hpp index 3abd113..8bc7ae9 100644 --- a/providers/OllamaProvider.hpp +++ b/providers/OllamaProvider.hpp @@ -47,9 +47,6 @@ public: public slots: void onDataReceived(const QString &requestId, const QByteArray &data) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override; - -private: - QHash m_accumulatedResponses; }; } // namespace QodeAssist::Providers diff --git a/providers/OpenAICompatProvider.cpp b/providers/OpenAICompatProvider.cpp index cd7ebc0..6fb85be 100644 --- a/providers/OpenAICompatProvider.cpp +++ b/providers/OpenAICompatProvider.cpp @@ -137,6 +137,9 @@ LLMCore::ProviderID OpenAICompatProvider::providerID() const void OpenAICompatProvider::sendRequest( const QString &requestId, const QUrl &url, const QJsonObject &payload) { + m_dataBuffers[requestId].clear(); + m_requestUrls[requestId] = url; + QNetworkRequest networkRequest(url); prepareNetworkRequest(networkRequest); @@ -151,16 +154,17 @@ void OpenAICompatProvider::sendRequest( void OpenAICompatProvider::onDataReceived(const QString &requestId, const QByteArray &data) { - QString &accumulatedResponse = m_accumulatedResponses[requestId]; + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + QStringList lines = buffers.rawStreamBuffer.processData(data); if (data.isEmpty()) { return; } bool isDone = false; - QByteArrayList lines = data.split('\n'); + QString tempResponse; - for (const QByteArray &line : lines) { + for (const QString &line : lines) { if (line.trimmed().isEmpty()) { continue; } @@ -170,19 +174,11 @@ void OpenAICompatProvider::onDataReceived(const QString &requestId, const QByteA continue; } - QByteArray jsonData = line; - if (line.startsWith("data: ")) { - jsonData = line.mid(6); - } - - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error); - - if (doc.isNull()) { + QJsonObject responseObj = parseEventLine(line); + if (responseObj.isEmpty()) continue; - } - auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); + auto message = LLMCore::OpenAIMessage::fromJson(responseObj); if (message.hasError()) { LOG_MESSAGE("Error in OpenAI response: " + message.error); continue; @@ -190,8 +186,7 @@ void OpenAICompatProvider::onDataReceived(const QString &requestId, const QByteA QString content = message.getContent(); if (!content.isEmpty()) { - accumulatedResponse += content; - emit partialResponseReceived(requestId, content); + tempResponse += content; } if (message.isDone()) { @@ -199,9 +194,14 @@ void OpenAICompatProvider::onDataReceived(const QString &requestId, const QByteA } } + if (!tempResponse.isEmpty()) { + buffers.responseContent += tempResponse; + emit partialResponseReceived(requestId, tempResponse); + } + if (isDone) { - emit fullResponseReceived(requestId, accumulatedResponse); - m_accumulatedResponses.remove(requestId); + emit fullResponseReceived(requestId, buffers.responseContent); + m_dataBuffers.remove(requestId); } } @@ -209,18 +209,19 @@ void OpenAICompatProvider::onRequestFinished( const QString &requestId, bool success, const QString &error) { if (!success) { - LOG_MESSAGE(QString("OpenAIProvider request %1 failed: %2").arg(requestId, error)); + LOG_MESSAGE(QString("OpenAICompatProvider request %1 failed: %2").arg(requestId, error)); emit requestFailed(requestId, error); } else { - if (m_accumulatedResponses.contains(requestId)) { - const QString fullResponse = m_accumulatedResponses[requestId]; - if (!fullResponse.isEmpty()) { - emit fullResponseReceived(requestId, fullResponse); + if (m_dataBuffers.contains(requestId)) { + const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + if (!buffers.responseContent.isEmpty()) { + emit fullResponseReceived(requestId, buffers.responseContent); } } } - m_accumulatedResponses.remove(requestId); + m_dataBuffers.remove(requestId); + m_requestUrls.remove(requestId); } } // namespace QodeAssist::Providers diff --git a/providers/OpenAICompatProvider.hpp b/providers/OpenAICompatProvider.hpp index 36c3cff..9ec2ade 100644 --- a/providers/OpenAICompatProvider.hpp +++ b/providers/OpenAICompatProvider.hpp @@ -47,9 +47,6 @@ public: public slots: void onDataReceived(const QString &requestId, const QByteArray &data) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override; - -private: - QHash m_accumulatedResponses; }; } // namespace QodeAssist::Providers diff --git a/providers/OpenAIProvider.cpp b/providers/OpenAIProvider.cpp index f71aa37..7bde5ba 100644 --- a/providers/OpenAIProvider.cpp +++ b/providers/OpenAIProvider.cpp @@ -175,6 +175,9 @@ LLMCore::ProviderID OpenAIProvider::providerID() const void OpenAIProvider::sendRequest( const QString &requestId, const QUrl &url, const QJsonObject &payload) { + m_dataBuffers[requestId].clear(); + m_requestUrls[requestId] = url; + QNetworkRequest networkRequest(url); prepareNetworkRequest(networkRequest); @@ -188,16 +191,17 @@ void OpenAIProvider::sendRequest( void OpenAIProvider::onDataReceived(const QString &requestId, const QByteArray &data) { - QString &accumulatedResponse = m_accumulatedResponses[requestId]; + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + QStringList lines = buffers.rawStreamBuffer.processData(data); if (data.isEmpty()) { return; } bool isDone = false; - QByteArrayList lines = data.split('\n'); + QString tempResponse; - for (const QByteArray &line : lines) { + for (const QString &line : lines) { if (line.trimmed().isEmpty()) { continue; } @@ -207,19 +211,11 @@ void OpenAIProvider::onDataReceived(const QString &requestId, const QByteArray & continue; } - QByteArray jsonData = line; - if (line.startsWith("data: ")) { - jsonData = line.mid(6); - } - - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error); - - if (doc.isNull()) { + QJsonObject responseObj = parseEventLine(line); + if (responseObj.isEmpty()) continue; - } - auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); + auto message = LLMCore::OpenAIMessage::fromJson(responseObj); if (message.hasError()) { LOG_MESSAGE("Error in OpenAI response: " + message.error); continue; @@ -227,8 +223,7 @@ void OpenAIProvider::onDataReceived(const QString &requestId, const QByteArray & QString content = message.getContent(); if (!content.isEmpty()) { - accumulatedResponse += content; - emit partialResponseReceived(requestId, content); + tempResponse += content; } if (message.isDone()) { @@ -236,9 +231,14 @@ void OpenAIProvider::onDataReceived(const QString &requestId, const QByteArray & } } + if (!tempResponse.isEmpty()) { + buffers.responseContent += tempResponse; + emit partialResponseReceived(requestId, tempResponse); + } + if (isDone) { - emit fullResponseReceived(requestId, accumulatedResponse); - m_accumulatedResponses.remove(requestId); + emit fullResponseReceived(requestId, buffers.responseContent); + m_dataBuffers.remove(requestId); } } @@ -248,15 +248,16 @@ void OpenAIProvider::onRequestFinished(const QString &requestId, bool success, c LOG_MESSAGE(QString("OpenAIProvider request %1 failed: %2").arg(requestId, error)); emit requestFailed(requestId, error); } else { - if (m_accumulatedResponses.contains(requestId)) { - const QString fullResponse = m_accumulatedResponses[requestId]; - if (!fullResponse.isEmpty()) { - emit fullResponseReceived(requestId, fullResponse); + if (m_dataBuffers.contains(requestId)) { + const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + if (!buffers.responseContent.isEmpty()) { + emit fullResponseReceived(requestId, buffers.responseContent); } } } - m_accumulatedResponses.remove(requestId); + m_dataBuffers.remove(requestId); + m_requestUrls.remove(requestId); } } // namespace QodeAssist::Providers diff --git a/providers/OpenAIProvider.hpp b/providers/OpenAIProvider.hpp index 592300c..69919e0 100644 --- a/providers/OpenAIProvider.hpp +++ b/providers/OpenAIProvider.hpp @@ -47,9 +47,6 @@ public: public slots: void onDataReceived(const QString &requestId, const QByteArray &data) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override; - -private: - QHash m_accumulatedResponses; }; } // namespace QodeAssist::Providers diff --git a/providers/OpenRouterAIProvider.cpp b/providers/OpenRouterAIProvider.cpp index e672af7..f0aecec 100644 --- a/providers/OpenRouterAIProvider.cpp +++ b/providers/OpenRouterAIProvider.cpp @@ -53,16 +53,17 @@ LLMCore::ProviderID OpenRouterProvider::providerID() const void OpenRouterProvider::onDataReceived(const QString &requestId, const QByteArray &data) { - QString &accumulatedResponse = m_accumulatedResponses[requestId]; + LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + QStringList lines = buffers.rawStreamBuffer.processData(data); if (data.isEmpty()) { return; } bool isDone = false; - QByteArrayList lines = data.split('\n'); + QString tempResponse; - for (const QByteArray &line : lines) { + for (const QString &line : lines) { if (line.trimmed().isEmpty() || line.contains("OPENROUTER PROCESSING")) { continue; } @@ -72,28 +73,19 @@ void OpenRouterProvider::onDataReceived(const QString &requestId, const QByteArr continue; } - QByteArray jsonData = line; - if (line.startsWith("data: ")) { - jsonData = line.mid(6); - } - - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error); - - if (doc.isNull()) { + QJsonObject responseObj = parseEventLine(line); + if (responseObj.isEmpty()) continue; - } - auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); + auto message = LLMCore::OpenAIMessage::fromJson(responseObj); if (message.hasError()) { - LOG_MESSAGE("Error in OpenAI response: " + message.error); + LOG_MESSAGE("Error in OpenRouter response: " + message.error); continue; } QString content = message.getContent(); if (!content.isEmpty()) { - accumulatedResponse += content; - emit partialResponseReceived(requestId, content); + tempResponse += content; } if (message.isDone()) { @@ -101,9 +93,14 @@ void OpenRouterProvider::onDataReceived(const QString &requestId, const QByteArr } } + if (!tempResponse.isEmpty()) { + buffers.responseContent += tempResponse; + emit partialResponseReceived(requestId, tempResponse); + } + if (isDone) { - emit fullResponseReceived(requestId, accumulatedResponse); - m_accumulatedResponses.remove(requestId); + emit fullResponseReceived(requestId, buffers.responseContent); + m_dataBuffers.remove(requestId); } } @@ -114,15 +111,16 @@ void OpenRouterProvider::onRequestFinished( LOG_MESSAGE(QString("OpenRouterProvider request %1 failed: %2").arg(requestId, error)); emit requestFailed(requestId, error); } else { - if (m_accumulatedResponses.contains(requestId)) { - const QString fullResponse = m_accumulatedResponses[requestId]; - if (!fullResponse.isEmpty()) { - emit fullResponseReceived(requestId, fullResponse); + if (m_dataBuffers.contains(requestId)) { + const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId]; + if (!buffers.responseContent.isEmpty()) { + emit fullResponseReceived(requestId, buffers.responseContent); } } } - m_accumulatedResponses.remove(requestId); + m_dataBuffers.remove(requestId); + m_requestUrls.remove(requestId); } } // namespace QodeAssist::Providers diff --git a/providers/OpenRouterAIProvider.hpp b/providers/OpenRouterAIProvider.hpp index 4bcfeec..2c064d7 100644 --- a/providers/OpenRouterAIProvider.hpp +++ b/providers/OpenRouterAIProvider.hpp @@ -19,7 +19,6 @@ #pragma once -#include "llmcore/Provider.hpp" #include "providers/OpenAICompatProvider.hpp" namespace QodeAssist::Providers { @@ -35,9 +34,6 @@ public: public slots: void onDataReceived(const QString &requestId, const QByteArray &data) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override; - -private: - QHash m_accumulatedResponses; }; } // namespace QodeAssist::Providers diff --git a/settings/ChatAssistantSettings.cpp b/settings/ChatAssistantSettings.cpp index f0330c0..4d67ce9 100644 --- a/settings/ChatAssistantSettings.cpp +++ b/settings/ChatAssistantSettings.cpp @@ -56,10 +56,6 @@ ChatAssistantSettings::ChatAssistantSettings() linkOpenFiles.setLabelText(Tr::tr("Sync open files with assistant by default")); linkOpenFiles.setDefaultValue(false); - stream.setSettingsKey(Constants::CA_STREAM); - stream.setDefaultValue(true); - stream.setLabelText(Tr::tr("Enable stream option")); - autosave.setSettingsKey(Constants::CA_AUTOSAVE); autosave.setDefaultValue(true); autosave.setLabelText(Tr::tr("Enable autosave when message received")); @@ -251,7 +247,6 @@ ChatAssistantSettings::ChatAssistantSettings() Group{title(Tr::tr("Chat Settings")), Column{Row{chatTokensThreshold, Stretch{1}}, linkOpenFiles, - stream, autosave, enableChatInBottomToolBar, enableChatInNavigationPanel}}, @@ -294,7 +289,6 @@ void ChatAssistantSettings::resetSettingsToDefaults() QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { - resetAspect(stream); resetAspect(chatTokensThreshold); resetAspect(temperature); resetAspect(maxTokens); diff --git a/settings/ChatAssistantSettings.hpp b/settings/ChatAssistantSettings.hpp index 0ddec36..984cf8d 100644 --- a/settings/ChatAssistantSettings.hpp +++ b/settings/ChatAssistantSettings.hpp @@ -35,7 +35,6 @@ public: // Chat settings Utils::IntegerAspect chatTokensThreshold{this}; Utils::BoolAspect linkOpenFiles{this}; - Utils::BoolAspect stream{this}; Utils::BoolAspect autosave{this}; Utils::BoolAspect enableChatInBottomToolBar{this}; Utils::BoolAspect enableChatInNavigationPanel{this}; diff --git a/settings/CodeCompletionSettings.cpp b/settings/CodeCompletionSettings.cpp index f9425e4..b0c1243 100644 --- a/settings/CodeCompletionSettings.cpp +++ b/settings/CodeCompletionSettings.cpp @@ -51,10 +51,6 @@ CodeCompletionSettings::CodeCompletionSettings() multiLineCompletion.setDefaultValue(true); multiLineCompletion.setLabelText(Tr::tr("Enable Multiline Completion")); - stream.setSettingsKey(Constants::CC_STREAM); - stream.setDefaultValue(true); - stream.setLabelText(Tr::tr("Enable stream option")); - modelOutputHandler.setLabelText(Tr::tr("Text output proccessing mode:")); modelOutputHandler.setSettingsKey(Constants::CC_MODEL_OUTPUT_HANDLER); modelOutputHandler.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox); @@ -303,7 +299,6 @@ CodeCompletionSettings::CodeCompletionSettings() Column{autoCompletion, Space{8}, multiLineCompletion, - stream, Row{modelOutputHandler, Stretch{1}}, Row{autoCompletionCharThreshold, autoCompletionTypingInterval, @@ -365,7 +360,6 @@ void CodeCompletionSettings::resetSettingsToDefaults() if (reply == QMessageBox::Yes) { resetAspect(autoCompletion); resetAspect(multiLineCompletion); - resetAspect(stream); resetAspect(temperature); resetAspect(maxTokens); resetAspect(useTopP); diff --git a/settings/CodeCompletionSettings.hpp b/settings/CodeCompletionSettings.hpp index 0c4ca8d..ff56aad 100644 --- a/settings/CodeCompletionSettings.hpp +++ b/settings/CodeCompletionSettings.hpp @@ -35,7 +35,6 @@ public: // Auto Completion Settings Utils::BoolAspect autoCompletion{this}; Utils::BoolAspect multiLineCompletion{this}; - Utils::BoolAspect stream{this}; Utils::SelectionAspect modelOutputHandler{this}; Utils::IntegerAspect startSuggestionTimer{this}; diff --git a/settings/SettingsConstants.hpp b/settings/SettingsConstants.hpp index 6f4618f..aaff040 100644 --- a/settings/SettingsConstants.hpp +++ b/settings/SettingsConstants.hpp @@ -75,12 +75,10 @@ const char СС_AUTO_COMPLETION_CHAR_THRESHOLD[] = "QodeAssist.autoCompletionCha const char СС_AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval"; const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold"; const char CC_MULTILINE_COMPLETION[] = "QodeAssist.ccMultilineCompletion"; -const char CC_STREAM[] = "QodeAssist.ccStream"; const char CC_MODEL_OUTPUT_HANDLER[] = "QodeAssist.ccModelOutputHandler"; const char CUSTOM_JSON_TEMPLATE[] = "QodeAssist.customJsonTemplate"; const char CA_TOKENS_THRESHOLD[] = "QodeAssist.caTokensThreshold"; const char CA_LINK_OPEN_FILES[] = "QodeAssist.caLinkOpenFiles"; -const char CA_STREAM[] = "QodeAssist.caStream"; const char CA_AUTOSAVE[] = "QodeAssist.caAutosave"; const char CC_CUSTOM_LANGUAGES[] = "QodeAssist.ccCustomLanguages";