refactor: Remove non-streaming support (#229)

This commit is contained in:
Petr Mironychev
2025-09-17 19:38:27 +02:00
committed by GitHub
parent 561661b476
commit ec1b5bdf5f
33 changed files with 412 additions and 408 deletions

View File

@ -98,8 +98,7 @@ void ClientInterface::sendMessage(
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
QString stream = chatAssistantSettings.stream() ? QString{"streamGenerateContent?alt=sse"} QString stream = QString{"streamGenerateContent?alt=sse"};
: QString{"generateContent?"};
config.url = QUrl(QString("%1/models/%2:%3") config.url = QUrl(QString("%1/models/%2:%3")
.arg( .arg(
Settings::generalSettings().caUrl(), Settings::generalSettings().caUrl(),
@ -109,8 +108,7 @@ void ClientInterface::sendMessage(
config.url config.url
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint()); = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest config.providerRequest
= {{"model", Settings::generalSettings().caModel()}, = {{"model", Settings::generalSettings().caModel()}, {"stream", true}};
{"stream", chatAssistantSettings.stream()}};
} }
config.apiKey = provider->apiKey(); config.apiKey = provider->apiKey();

View File

@ -224,13 +224,12 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
// TODO refactor networking // TODO refactor networking
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
QString stream = m_completeSettings.stream() ? QString{"streamGenerateContent?alt=sse"} QString stream = QString{"streamGenerateContent?alt=sse"};
: QString{"generateContent?"};
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream)); config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
} else { } else {
config.url = QUrl( config.url = QUrl(
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active))); 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.apiKey = provider->apiKey();
config.multiLineCompletion = m_completeSettings.multiLineCompletion(); config.multiLineCompletion = m_completeSettings.multiLineCompletion();

View File

@ -138,8 +138,7 @@ void QuickRefactorHandler::prepareAndSendRequest(
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
config.url = QString("%1%2").arg(settings.caUrl(), provider->chatEndpoint()); config.url = QString("%1%2").arg(settings.caUrl(), provider->chatEndpoint());
config.providerRequest config.providerRequest = {{"model", settings.caModel()}, {"stream", true}};
= {{"model", settings.caModel()}, {"stream", Settings::chatAssistantSettings().stream()}};
config.apiKey = provider->apiKey(); config.apiKey = provider->apiKey();
LLMCore::ContextData context = prepareContext(editor, range, instructions); LLMCore::ContextData context = prepareContext(editor, range, instructions);

View File

@ -15,6 +15,8 @@ add_library(LLMCore STATIC
ValidationUtils.hpp ValidationUtils.cpp ValidationUtils.hpp ValidationUtils.cpp
ProviderID.hpp ProviderID.hpp
HttpClient.hpp HttpClient.cpp HttpClient.hpp HttpClient.cpp
DataBuffers.hpp
SSEBuffer.hpp SSEBuffer.cpp
) )
target_link_libraries(LLMCore target_link_libraries(LLMCore

39
llmcore/DataBuffers.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include "SSEBuffer.hpp"
#include <QString>
namespace QodeAssist::LLMCore {
struct DataBuffers
{
SSEBuffer rawStreamBuffer;
QString responseContent;
void clear()
{
rawStreamBuffer.clear();
responseContent.clear();
}
};
} // namespace QodeAssist::LLMCore

View File

@ -1,18 +1,31 @@
#include "Provider.hpp" #include "Provider.hpp"
#include <QJsonDocument>
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
Provider::Provider(QObject *parent) Provider::Provider(QObject *parent)
: QObject(parent) : QObject(parent)
, m_httpClient(std::make_unique<HttpClient>()) , m_httpClient(new HttpClient(this))
{ {
connect(m_httpClient.get(), &HttpClient::dataReceived, this, &Provider::onDataReceived); connect(m_httpClient, &HttpClient::dataReceived, this, &Provider::onDataReceived);
connect(m_httpClient.get(), &HttpClient::requestFinished, this, &Provider::onRequestFinished); connect(m_httpClient, &HttpClient::requestFinished, this, &Provider::onRequestFinished);
} }
HttpClient *Provider::httpClient() const 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 } // namespace QodeAssist::LLMCore

View File

@ -25,6 +25,7 @@
#include <QString> #include <QString>
#include "ContextData.hpp" #include "ContextData.hpp"
#include "DataBuffers.hpp"
#include "HttpClient.hpp" #include "HttpClient.hpp"
#include "PromptTemplate.hpp" #include "PromptTemplate.hpp"
#include "RequestType.hpp" #include "RequestType.hpp"
@ -73,8 +74,14 @@ signals:
void fullResponseReceived(const QString &requestId, const QString &fullText); void fullResponseReceived(const QString &requestId, const QString &fullText);
void requestFailed(const QString &requestId, const QString &error); void requestFailed(const QString &requestId, const QString &error);
protected:
QJsonObject parseEventLine(const QString &line);
QHash<RequestID, DataBuffers> m_dataBuffers;
QHash<RequestID, QUrl> m_requestUrls;
private: private:
std::unique_ptr<HttpClient> m_httpClient; HttpClient *m_httpClient;
}; };
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@ -17,9 +17,13 @@
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>. * along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include <QString>
#pragma once #pragma once
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
enum RequestType { CodeCompletion, Chat, Embedding }; enum RequestType { CodeCompletion, Chat, Embedding };
using RequestID = QString;
} }

51
llmcore/SSEBuffer.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#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

42
llmcore/SSEBuffer.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QString>
#include <QStringList>
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

View File

@ -174,6 +174,9 @@ LLMCore::ProviderID ClaudeProvider::providerID() const
void ClaudeProvider::sendRequest( void ClaudeProvider::sendRequest(
const QString &requestId, const QUrl &url, const QJsonObject &payload) const QString &requestId, const QUrl &url, const QJsonObject &payload)
{ {
m_dataBuffers[requestId].clear();
m_requestUrls[requestId] = url;
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
@ -187,50 +190,49 @@ void ClaudeProvider::sendRequest(
void ClaudeProvider::onDataReceived(const QString &requestId, const QByteArray &data) 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; QString tempResponse;
bool isComplete = false; bool isComplete = false;
QByteArrayList lines = data.split('\n'); for (const QString &line : lines) {
for (const QByteArray &line : lines) { QJsonObject responseObj = parseEventLine(line);
QByteArray trimmedLine = line.trimmed(); if (responseObj.isEmpty())
if (trimmedLine.isEmpty())
continue; 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(); QString eventType = responseObj["type"].toString();
if (eventType == "message_delta") { if (eventType == "message_start") {
if (responseObj.contains("delta")) { QString messageId = responseObj["message"].toObject()["id"].toString();
QJsonObject delta = responseObj["delta"].toObject(); LOG_MESSAGE(QString("Claude message started: %1").arg(messageId));
if (delta.contains("stop_reason")) {
isComplete = true;
}
}
} else if (eventType == "content_block_delta") { } else if (eventType == "content_block_delta") {
QJsonObject delta = responseObj["delta"].toObject(); QJsonObject delta = responseObj["delta"].toObject();
if (delta["type"].toString() == "text_delta") { if (delta["type"].toString() == "text_delta") {
tempResponse += delta["text"].toString(); 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()) { if (!tempResponse.isEmpty()) {
accumulatedResponse += tempResponse; buffers.responseContent += tempResponse;
emit partialResponseReceived(requestId, tempResponse); emit partialResponseReceived(requestId, tempResponse);
} }
if (isComplete) { if (isComplete) {
emit fullResponseReceived(requestId, accumulatedResponse); emit fullResponseReceived(requestId, buffers.responseContent);
m_accumulatedResponses.remove(requestId); 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)); LOG_MESSAGE(QString("ClaudeProvider request %1 failed: %2").arg(requestId, error));
emit requestFailed(requestId, error); emit requestFailed(requestId, error);
} else { } else {
if (m_accumulatedResponses.contains(requestId)) { if (m_dataBuffers.contains(requestId)) {
const QString fullResponse = m_accumulatedResponses[requestId]; const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
if (!fullResponse.isEmpty()) { if (!buffers.responseContent.isEmpty()) {
emit fullResponseReceived(requestId, fullResponse); emit fullResponseReceived(requestId, buffers.responseContent);
} }
} }
} }
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
m_requestUrls.remove(requestId);
} }
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -47,9 +47,6 @@ public:
public slots: public slots:
void onDataReceived(const QString &requestId, const QByteArray &data) override; void onDataReceived(const QString &requestId, const QByteArray &data) override;
void onRequestFinished(const QString &requestId, bool success, const QString &error) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override;
private:
QHash<QString, QString> m_accumulatedResponses;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -172,6 +172,9 @@ LLMCore::ProviderID GoogleAIProvider::providerID() const
void GoogleAIProvider::sendRequest( void GoogleAIProvider::sendRequest(
const QString &requestId, const QUrl &url, const QJsonObject &payload) const QString &requestId, const QUrl &url, const QJsonObject &payload)
{ {
m_dataBuffers[requestId].clear();
m_requestUrls[requestId] = url;
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
@ -186,8 +189,6 @@ void GoogleAIProvider::sendRequest(
void GoogleAIProvider::onDataReceived(const QString &requestId, const QByteArray &data) void GoogleAIProvider::onDataReceived(const QString &requestId, const QByteArray &data)
{ {
QString &accumulatedResponse = m_accumulatedResponses[requestId];
if (data.isEmpty()) { if (data.isEmpty()) {
return; return;
} }
@ -205,204 +206,85 @@ void GoogleAIProvider::onDataReceived(const QString &requestId, const QByteArray
LOG_MESSAGE(fullError); LOG_MESSAGE(fullError);
emit requestFailed(requestId, fullError); emit requestFailed(requestId, fullError);
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
return; return;
} }
} }
bool isDone = false; bool isDone = handleStreamResponse(requestId, data);
if (data.startsWith("data: ")) {
isDone = handleStreamResponse(requestId, data, accumulatedResponse);
} else {
isDone = handleRegularResponse(requestId, data, accumulatedResponse);
}
if (isDone) { if (isDone) {
emit fullResponseReceived(requestId, accumulatedResponse); LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
m_accumulatedResponses.remove(requestId); emit fullResponseReceived(requestId, buffers.responseContent);
m_dataBuffers.remove(requestId);
} }
} }
void GoogleAIProvider::onRequestFinished(const QString &requestId, bool success, const QString &error) void GoogleAIProvider::onRequestFinished(const QString &requestId, bool success, const QString &error)
{ {
if (!success) { if (!success) {
QString detailedError = error; LOG_MESSAGE(QString("GoogleAIProvider request %1 failed: %2").arg(requestId, error));
emit requestFailed(requestId, 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);
} else { } else {
if (m_accumulatedResponses.contains(requestId)) { if (m_dataBuffers.contains(requestId)) {
const QString fullResponse = m_accumulatedResponses[requestId]; const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
if (!fullResponse.isEmpty()) { if (!buffers.responseContent.isEmpty()) {
emit fullResponseReceived(requestId, fullResponse); emit fullResponseReceived(requestId, buffers.responseContent);
} }
} }
} }
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
m_requestUrls.remove(requestId);
} }
bool GoogleAIProvider::handleStreamResponse( bool GoogleAIProvider::handleStreamResponse(const QString &requestId, const QByteArray &data)
const QString &requestId, const QByteArray &data, QString &accumulatedResponse)
{ {
QByteArrayList lines = data.split('\n'); LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
QStringList lines = buffers.rawStreamBuffer.processData(data);
bool isDone = false; bool isDone = false;
QString tempResponse;
for (const QByteArray &line : lines) { for (const QString &line : lines) {
QByteArray trimmedLine = line.trimmed(); if (line.trimmed().isEmpty()) {
if (trimmedLine.isEmpty()) {
continue; continue;
} }
if (trimmedLine == "data: [DONE]") { QJsonObject responseObj = parseEventLine(line);
isDone = true; if (responseObj.isEmpty())
continue; continue;
}
if (trimmedLine.startsWith("data: ")) { if (responseObj.contains("candidates")) {
QByteArray jsonData = trimmedLine.mid(6); QJsonArray candidates = responseObj["candidates"].toArray();
QJsonParseError parseError; for (const QJsonValue &candidate : candidates) {
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError); QJsonObject candidateObj = candidate.toObject();
if (doc.isNull() || !doc.isObject()) { if (candidateObj.contains("content")) {
if (parseError.error != QJsonParseError::NoError) { QJsonObject content = candidateObj["content"].toObject();
LOG_MESSAGE(QString("JSON parse error in GoogleAI stream: %1") if (content.contains("parts")) {
.arg(parseError.errorString())); QJsonArray parts = content["parts"].toArray();
} for (const QJsonValue &part : parts) {
continue; QJsonObject partObj = part.toObject();
} if (partObj.contains("text")) {
tempResponse += partObj["text"].toString();
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 (candidateObj.contains("finishReason")) {
isDone = true;
}
} }
} }
} }
if (!tempResponse.isEmpty()) {
buffers.responseContent += tempResponse;
emit partialResponseReceived(requestId, tempResponse);
}
return isDone; 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 } // namespace QodeAssist::Providers

View File

@ -49,11 +49,7 @@ public slots:
void onRequestFinished(const QString &requestId, bool success, const QString &error) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override;
private: private:
QHash<QString, QString> m_accumulatedResponses; bool handleStreamResponse(const QString &requestId, const QByteArray &data);
bool handleStreamResponse(
const QString &requestId, const QByteArray &data, QString &accumulatedResponse);
bool handleRegularResponse(
const QString &requestId, const QByteArray &data, QString &accumulatedResponse);
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -125,6 +125,9 @@ LLMCore::ProviderID LMStudioProvider::providerID() const
void LMStudioProvider::sendRequest( void LMStudioProvider::sendRequest(
const QString &requestId, const QUrl &url, const QJsonObject &payload) const QString &requestId, const QUrl &url, const QJsonObject &payload)
{ {
m_dataBuffers[requestId].clear();
m_requestUrls[requestId] = url;
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
@ -139,16 +142,17 @@ void LMStudioProvider::sendRequest(
void LMStudioProvider::onDataReceived(const QString &requestId, const QByteArray &data) 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()) { if (data.isEmpty()) {
return; return;
} }
bool isDone = false; bool isDone = false;
QByteArrayList lines = data.split('\n'); QString tempResponse;
for (const QByteArray &line : lines) { for (const QString &line : lines) {
if (line.trimmed().isEmpty()) { if (line.trimmed().isEmpty()) {
continue; continue;
} }
@ -158,19 +162,11 @@ void LMStudioProvider::onDataReceived(const QString &requestId, const QByteArray
continue; continue;
} }
QByteArray jsonData = line; QJsonObject responseObj = parseEventLine(line);
if (line.startsWith("data: ")) { if (responseObj.isEmpty())
jsonData = line.mid(6);
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue; continue;
}
auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); auto message = LLMCore::OpenAIMessage::fromJson(responseObj);
if (message.hasError()) { if (message.hasError()) {
LOG_MESSAGE("Error in LMStudio response: " + message.error); LOG_MESSAGE("Error in LMStudio response: " + message.error);
continue; continue;
@ -178,8 +174,7 @@ void LMStudioProvider::onDataReceived(const QString &requestId, const QByteArray
QString content = message.getContent(); QString content = message.getContent();
if (!content.isEmpty()) { if (!content.isEmpty()) {
accumulatedResponse += content; tempResponse += content;
emit partialResponseReceived(requestId, content);
} }
if (message.isDone()) { 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) { if (isDone) {
emit fullResponseReceived(requestId, accumulatedResponse); emit fullResponseReceived(requestId, buffers.responseContent);
m_accumulatedResponses.remove(requestId); 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)); LOG_MESSAGE(QString("LMStudioProvider request %1 failed: %2").arg(requestId, error));
emit requestFailed(requestId, error); emit requestFailed(requestId, error);
} else { } else {
if (m_accumulatedResponses.contains(requestId)) { if (m_dataBuffers.contains(requestId)) {
const QString fullResponse = m_accumulatedResponses[requestId]; const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
if (!fullResponse.isEmpty()) { if (!buffers.responseContent.isEmpty()) {
emit fullResponseReceived(requestId, fullResponse); emit fullResponseReceived(requestId, buffers.responseContent);
} }
} }
} }
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
m_requestUrls.remove(requestId);
} }
void QodeAssist::Providers::LMStudioProvider::prepareRequest( void QodeAssist::Providers::LMStudioProvider::prepareRequest(

View File

@ -47,9 +47,6 @@ public:
public slots: public slots:
void onDataReceived(const QString &requestId, const QByteArray &data) override; void onDataReceived(const QString &requestId, const QByteArray &data) override;
void onRequestFinished(const QString &requestId, bool success, const QString &error) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override;
private:
QHash<QString, QString> m_accumulatedResponses;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -151,6 +151,9 @@ LLMCore::ProviderID LlamaCppProvider::providerID() const
void LlamaCppProvider::sendRequest( void LlamaCppProvider::sendRequest(
const QString &requestId, const QUrl &url, const QJsonObject &payload) const QString &requestId, const QUrl &url, const QJsonObject &payload)
{ {
m_dataBuffers[requestId].clear();
m_requestUrls[requestId] = url;
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
@ -165,16 +168,17 @@ void LlamaCppProvider::sendRequest(
void LlamaCppProvider::onDataReceived(const QString &requestId, const QByteArray &data) 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()) { if (data.isEmpty()) {
return; return;
} }
bool isDone = data.contains("\"stop\":true") || data.contains("data: [DONE]"); bool isDone = data.contains("\"stop\":true") || data.contains("data: [DONE]");
QString tempResponse;
QByteArrayList lines = data.split('\n'); for (const QString &line : lines) {
for (const QByteArray &line : lines) {
if (line.trimmed().isEmpty()) { if (line.trimmed().isEmpty()) {
continue; continue;
} }
@ -184,25 +188,15 @@ void LlamaCppProvider::onDataReceived(const QString &requestId, const QByteArray
continue; continue;
} }
QByteArray jsonData = line; QJsonObject obj = parseEventLine(line);
if (line.startsWith("data: ")) { if (obj.isEmpty())
jsonData = line.mid(6);
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue; continue;
}
QJsonObject obj = doc.object();
QString content; QString content;
if (obj.contains("content")) { if (obj.contains("content")) {
content = obj["content"].toString(); content = obj["content"].toString();
if (!content.isEmpty()) { if (!content.isEmpty()) {
accumulatedResponse += content; tempResponse += content;
emit partialResponseReceived(requestId, content);
} }
} else if (obj.contains("choices")) { } else if (obj.contains("choices")) {
auto message = LLMCore::OpenAIMessage::fromJson(obj); auto message = LLMCore::OpenAIMessage::fromJson(obj);
@ -213,8 +207,7 @@ void LlamaCppProvider::onDataReceived(const QString &requestId, const QByteArray
content = message.getContent(); content = message.getContent();
if (!content.isEmpty()) { if (!content.isEmpty()) {
accumulatedResponse += content; tempResponse += content;
emit partialResponseReceived(requestId, content);
} }
if (message.isDone()) { 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) { if (isDone) {
emit fullResponseReceived(requestId, accumulatedResponse); emit fullResponseReceived(requestId, buffers.responseContent);
m_accumulatedResponses.remove(requestId); 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)); LOG_MESSAGE(QString("LlamaCppProvider request %1 failed: %2").arg(requestId, error));
emit requestFailed(requestId, error); emit requestFailed(requestId, error);
} else { } else {
if (m_accumulatedResponses.contains(requestId)) { if (m_dataBuffers.contains(requestId)) {
const QString fullResponse = m_accumulatedResponses[requestId]; const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
if (!fullResponse.isEmpty()) { if (!buffers.responseContent.isEmpty()) {
emit fullResponseReceived(requestId, fullResponse); emit fullResponseReceived(requestId, buffers.responseContent);
} }
} }
} }
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
m_requestUrls.remove(requestId);
} }
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -47,9 +47,6 @@ public:
public slots: public slots:
void onDataReceived(const QString &requestId, const QByteArray &data) override; void onDataReceived(const QString &requestId, const QByteArray &data) override;
void onRequestFinished(const QString &requestId, bool success, const QString &error) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override;
private:
QHash<QString, QString> m_accumulatedResponses;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -128,6 +128,9 @@ LLMCore::ProviderID MistralAIProvider::providerID() const
void MistralAIProvider::sendRequest( void MistralAIProvider::sendRequest(
const QString &requestId, const QUrl &url, const QJsonObject &payload) const QString &requestId, const QUrl &url, const QJsonObject &payload)
{ {
m_dataBuffers[requestId].clear();
m_requestUrls[requestId] = url;
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
@ -142,16 +145,17 @@ void MistralAIProvider::sendRequest(
void MistralAIProvider::onDataReceived(const QString &requestId, const QByteArray &data) 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()) { if (data.isEmpty()) {
return; return;
} }
bool isDone = false; bool isDone = false;
QByteArrayList lines = data.split('\n'); QString tempResponse;
for (const QByteArray &line : lines) { for (const QString &line : lines) {
if (line.trimmed().isEmpty()) { if (line.trimmed().isEmpty()) {
continue; continue;
} }
@ -161,19 +165,11 @@ void MistralAIProvider::onDataReceived(const QString &requestId, const QByteArra
continue; continue;
} }
QByteArray jsonData = line; QJsonObject responseObj = parseEventLine(line);
if (line.startsWith("data: ")) { if (responseObj.isEmpty())
jsonData = line.mid(6);
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue; continue;
}
auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); auto message = LLMCore::OpenAIMessage::fromJson(responseObj);
if (message.hasError()) { if (message.hasError()) {
LOG_MESSAGE("Error in MistralAI response: " + message.error); LOG_MESSAGE("Error in MistralAI response: " + message.error);
continue; continue;
@ -181,8 +177,7 @@ void MistralAIProvider::onDataReceived(const QString &requestId, const QByteArra
QString content = message.getContent(); QString content = message.getContent();
if (!content.isEmpty()) { if (!content.isEmpty()) {
accumulatedResponse += content; tempResponse += content;
emit partialResponseReceived(requestId, content);
} }
if (message.isDone()) { 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) { if (isDone) {
emit fullResponseReceived(requestId, accumulatedResponse); emit fullResponseReceived(requestId, buffers.responseContent);
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
} }
} }
@ -203,15 +203,16 @@ void MistralAIProvider::onRequestFinished(
LOG_MESSAGE(QString("MistralAIProvider request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("MistralAIProvider request %1 failed: %2").arg(requestId, error));
emit requestFailed(requestId, error); emit requestFailed(requestId, error);
} else { } else {
if (m_accumulatedResponses.contains(requestId)) { if (m_dataBuffers.contains(requestId)) {
const QString fullResponse = m_accumulatedResponses[requestId]; const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
if (!fullResponse.isEmpty()) { if (!buffers.responseContent.isEmpty()) {
emit fullResponseReceived(requestId, fullResponse); emit fullResponseReceived(requestId, buffers.responseContent);
} }
} }
} }
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
m_requestUrls.remove(requestId);
} }
void MistralAIProvider::prepareRequest( void MistralAIProvider::prepareRequest(

View File

@ -47,9 +47,6 @@ public:
public slots: public slots:
void onDataReceived(const QString &requestId, const QByteArray &data) override; void onDataReceived(const QString &requestId, const QByteArray &data) override;
void onRequestFinished(const QString &requestId, bool success, const QString &error) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override;
private:
QHash<QString, QString> m_accumulatedResponses;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -188,6 +188,9 @@ LLMCore::ProviderID OllamaProvider::providerID() const
void OllamaProvider::sendRequest( void OllamaProvider::sendRequest(
const QString &requestId, const QUrl &url, const QJsonObject &payload) const QString &requestId, const QUrl &url, const QJsonObject &payload)
{ {
m_dataBuffers[requestId].clear();
m_requestUrls[requestId] = url;
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
@ -201,22 +204,23 @@ void OllamaProvider::sendRequest(
void OllamaProvider::onDataReceived(const QString &requestId, const QByteArray &data) 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()) { if (data.isEmpty()) {
return; return;
} }
QByteArrayList lines = data.split('\n');
bool isDone = false; bool isDone = false;
QString tempResponse;
for (const QByteArray &line : lines) { for (const QString &line : lines) {
if (line.trimmed().isEmpty()) { if (line.trimmed().isEmpty()) {
continue; continue;
} }
QJsonParseError error; QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(line, &error); QJsonDocument doc = QJsonDocument::fromJson(line.toUtf8(), &error);
if (doc.isNull()) { if (doc.isNull()) {
continue; continue;
} }
@ -238,8 +242,7 @@ void OllamaProvider::onDataReceived(const QString &requestId, const QByteArray &
} }
if (!content.isEmpty()) { if (!content.isEmpty()) {
accumulatedResponse += content; tempResponse += content;
emit partialResponseReceived(requestId, content);
} }
if (obj["done"].toBool()) { 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) { if (isDone) {
emit fullResponseReceived(requestId, accumulatedResponse); emit fullResponseReceived(requestId, buffers.responseContent);
m_accumulatedResponses.remove(requestId); 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)); LOG_MESSAGE(QString("OllamaProvider request %1 failed: %2").arg(requestId, error));
emit requestFailed(requestId, error); emit requestFailed(requestId, error);
} else { } else {
if (m_accumulatedResponses.contains(requestId)) { if (m_dataBuffers.contains(requestId)) {
const QString fullResponse = m_accumulatedResponses[requestId]; const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
if (!fullResponse.isEmpty()) { if (!buffers.responseContent.isEmpty()) {
emit fullResponseReceived(requestId, fullResponse); emit fullResponseReceived(requestId, buffers.responseContent);
} }
} }
} }
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
m_requestUrls.remove(requestId);
} }
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -47,9 +47,6 @@ public:
public slots: public slots:
void onDataReceived(const QString &requestId, const QByteArray &data) override; void onDataReceived(const QString &requestId, const QByteArray &data) override;
void onRequestFinished(const QString &requestId, bool success, const QString &error) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override;
private:
QHash<QString, QString> m_accumulatedResponses;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -137,6 +137,9 @@ LLMCore::ProviderID OpenAICompatProvider::providerID() const
void OpenAICompatProvider::sendRequest( void OpenAICompatProvider::sendRequest(
const QString &requestId, const QUrl &url, const QJsonObject &payload) const QString &requestId, const QUrl &url, const QJsonObject &payload)
{ {
m_dataBuffers[requestId].clear();
m_requestUrls[requestId] = url;
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
@ -151,16 +154,17 @@ void OpenAICompatProvider::sendRequest(
void OpenAICompatProvider::onDataReceived(const QString &requestId, const QByteArray &data) 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()) { if (data.isEmpty()) {
return; return;
} }
bool isDone = false; bool isDone = false;
QByteArrayList lines = data.split('\n'); QString tempResponse;
for (const QByteArray &line : lines) { for (const QString &line : lines) {
if (line.trimmed().isEmpty()) { if (line.trimmed().isEmpty()) {
continue; continue;
} }
@ -170,19 +174,11 @@ void OpenAICompatProvider::onDataReceived(const QString &requestId, const QByteA
continue; continue;
} }
QByteArray jsonData = line; QJsonObject responseObj = parseEventLine(line);
if (line.startsWith("data: ")) { if (responseObj.isEmpty())
jsonData = line.mid(6);
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue; continue;
}
auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); auto message = LLMCore::OpenAIMessage::fromJson(responseObj);
if (message.hasError()) { if (message.hasError()) {
LOG_MESSAGE("Error in OpenAI response: " + message.error); LOG_MESSAGE("Error in OpenAI response: " + message.error);
continue; continue;
@ -190,8 +186,7 @@ void OpenAICompatProvider::onDataReceived(const QString &requestId, const QByteA
QString content = message.getContent(); QString content = message.getContent();
if (!content.isEmpty()) { if (!content.isEmpty()) {
accumulatedResponse += content; tempResponse += content;
emit partialResponseReceived(requestId, content);
} }
if (message.isDone()) { 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) { if (isDone) {
emit fullResponseReceived(requestId, accumulatedResponse); emit fullResponseReceived(requestId, buffers.responseContent);
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
} }
} }
@ -209,18 +209,19 @@ void OpenAICompatProvider::onRequestFinished(
const QString &requestId, bool success, const QString &error) const QString &requestId, bool success, const QString &error)
{ {
if (!success) { 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); emit requestFailed(requestId, error);
} else { } else {
if (m_accumulatedResponses.contains(requestId)) { if (m_dataBuffers.contains(requestId)) {
const QString fullResponse = m_accumulatedResponses[requestId]; const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
if (!fullResponse.isEmpty()) { if (!buffers.responseContent.isEmpty()) {
emit fullResponseReceived(requestId, fullResponse); emit fullResponseReceived(requestId, buffers.responseContent);
} }
} }
} }
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
m_requestUrls.remove(requestId);
} }
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -47,9 +47,6 @@ public:
public slots: public slots:
void onDataReceived(const QString &requestId, const QByteArray &data) override; void onDataReceived(const QString &requestId, const QByteArray &data) override;
void onRequestFinished(const QString &requestId, bool success, const QString &error) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override;
private:
QHash<QString, QString> m_accumulatedResponses;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -175,6 +175,9 @@ LLMCore::ProviderID OpenAIProvider::providerID() const
void OpenAIProvider::sendRequest( void OpenAIProvider::sendRequest(
const QString &requestId, const QUrl &url, const QJsonObject &payload) const QString &requestId, const QUrl &url, const QJsonObject &payload)
{ {
m_dataBuffers[requestId].clear();
m_requestUrls[requestId] = url;
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
@ -188,16 +191,17 @@ void OpenAIProvider::sendRequest(
void OpenAIProvider::onDataReceived(const QString &requestId, const QByteArray &data) 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()) { if (data.isEmpty()) {
return; return;
} }
bool isDone = false; bool isDone = false;
QByteArrayList lines = data.split('\n'); QString tempResponse;
for (const QByteArray &line : lines) { for (const QString &line : lines) {
if (line.trimmed().isEmpty()) { if (line.trimmed().isEmpty()) {
continue; continue;
} }
@ -207,19 +211,11 @@ void OpenAIProvider::onDataReceived(const QString &requestId, const QByteArray &
continue; continue;
} }
QByteArray jsonData = line; QJsonObject responseObj = parseEventLine(line);
if (line.startsWith("data: ")) { if (responseObj.isEmpty())
jsonData = line.mid(6);
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue; continue;
}
auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); auto message = LLMCore::OpenAIMessage::fromJson(responseObj);
if (message.hasError()) { if (message.hasError()) {
LOG_MESSAGE("Error in OpenAI response: " + message.error); LOG_MESSAGE("Error in OpenAI response: " + message.error);
continue; continue;
@ -227,8 +223,7 @@ void OpenAIProvider::onDataReceived(const QString &requestId, const QByteArray &
QString content = message.getContent(); QString content = message.getContent();
if (!content.isEmpty()) { if (!content.isEmpty()) {
accumulatedResponse += content; tempResponse += content;
emit partialResponseReceived(requestId, content);
} }
if (message.isDone()) { 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) { if (isDone) {
emit fullResponseReceived(requestId, accumulatedResponse); emit fullResponseReceived(requestId, buffers.responseContent);
m_accumulatedResponses.remove(requestId); 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)); LOG_MESSAGE(QString("OpenAIProvider request %1 failed: %2").arg(requestId, error));
emit requestFailed(requestId, error); emit requestFailed(requestId, error);
} else { } else {
if (m_accumulatedResponses.contains(requestId)) { if (m_dataBuffers.contains(requestId)) {
const QString fullResponse = m_accumulatedResponses[requestId]; const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
if (!fullResponse.isEmpty()) { if (!buffers.responseContent.isEmpty()) {
emit fullResponseReceived(requestId, fullResponse); emit fullResponseReceived(requestId, buffers.responseContent);
} }
} }
} }
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
m_requestUrls.remove(requestId);
} }
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -47,9 +47,6 @@ public:
public slots: public slots:
void onDataReceived(const QString &requestId, const QByteArray &data) override; void onDataReceived(const QString &requestId, const QByteArray &data) override;
void onRequestFinished(const QString &requestId, bool success, const QString &error) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override;
private:
QHash<QString, QString> m_accumulatedResponses;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -53,16 +53,17 @@ LLMCore::ProviderID OpenRouterProvider::providerID() const
void OpenRouterProvider::onDataReceived(const QString &requestId, const QByteArray &data) 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()) { if (data.isEmpty()) {
return; return;
} }
bool isDone = false; 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")) { if (line.trimmed().isEmpty() || line.contains("OPENROUTER PROCESSING")) {
continue; continue;
} }
@ -72,28 +73,19 @@ void OpenRouterProvider::onDataReceived(const QString &requestId, const QByteArr
continue; continue;
} }
QByteArray jsonData = line; QJsonObject responseObj = parseEventLine(line);
if (line.startsWith("data: ")) { if (responseObj.isEmpty())
jsonData = line.mid(6);
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
if (doc.isNull()) {
continue; continue;
}
auto message = LLMCore::OpenAIMessage::fromJson(doc.object()); auto message = LLMCore::OpenAIMessage::fromJson(responseObj);
if (message.hasError()) { if (message.hasError()) {
LOG_MESSAGE("Error in OpenAI response: " + message.error); LOG_MESSAGE("Error in OpenRouter response: " + message.error);
continue; continue;
} }
QString content = message.getContent(); QString content = message.getContent();
if (!content.isEmpty()) { if (!content.isEmpty()) {
accumulatedResponse += content; tempResponse += content;
emit partialResponseReceived(requestId, content);
} }
if (message.isDone()) { 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) { if (isDone) {
emit fullResponseReceived(requestId, accumulatedResponse); emit fullResponseReceived(requestId, buffers.responseContent);
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
} }
} }
@ -114,15 +111,16 @@ void OpenRouterProvider::onRequestFinished(
LOG_MESSAGE(QString("OpenRouterProvider request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("OpenRouterProvider request %1 failed: %2").arg(requestId, error));
emit requestFailed(requestId, error); emit requestFailed(requestId, error);
} else { } else {
if (m_accumulatedResponses.contains(requestId)) { if (m_dataBuffers.contains(requestId)) {
const QString fullResponse = m_accumulatedResponses[requestId]; const LLMCore::DataBuffers &buffers = m_dataBuffers[requestId];
if (!fullResponse.isEmpty()) { if (!buffers.responseContent.isEmpty()) {
emit fullResponseReceived(requestId, fullResponse); emit fullResponseReceived(requestId, buffers.responseContent);
} }
} }
} }
m_accumulatedResponses.remove(requestId); m_dataBuffers.remove(requestId);
m_requestUrls.remove(requestId);
} }
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -19,7 +19,6 @@
#pragma once #pragma once
#include "llmcore/Provider.hpp"
#include "providers/OpenAICompatProvider.hpp" #include "providers/OpenAICompatProvider.hpp"
namespace QodeAssist::Providers { namespace QodeAssist::Providers {
@ -35,9 +34,6 @@ public:
public slots: public slots:
void onDataReceived(const QString &requestId, const QByteArray &data) override; void onDataReceived(const QString &requestId, const QByteArray &data) override;
void onRequestFinished(const QString &requestId, bool success, const QString &error) override; void onRequestFinished(const QString &requestId, bool success, const QString &error) override;
private:
QHash<QString, QString> m_accumulatedResponses;
}; };
} // namespace QodeAssist::Providers } // namespace QodeAssist::Providers

View File

@ -56,10 +56,6 @@ ChatAssistantSettings::ChatAssistantSettings()
linkOpenFiles.setLabelText(Tr::tr("Sync open files with assistant by default")); linkOpenFiles.setLabelText(Tr::tr("Sync open files with assistant by default"));
linkOpenFiles.setDefaultValue(false); linkOpenFiles.setDefaultValue(false);
stream.setSettingsKey(Constants::CA_STREAM);
stream.setDefaultValue(true);
stream.setLabelText(Tr::tr("Enable stream option"));
autosave.setSettingsKey(Constants::CA_AUTOSAVE); autosave.setSettingsKey(Constants::CA_AUTOSAVE);
autosave.setDefaultValue(true); autosave.setDefaultValue(true);
autosave.setLabelText(Tr::tr("Enable autosave when message received")); autosave.setLabelText(Tr::tr("Enable autosave when message received"));
@ -251,7 +247,6 @@ ChatAssistantSettings::ChatAssistantSettings()
Group{title(Tr::tr("Chat Settings")), Group{title(Tr::tr("Chat Settings")),
Column{Row{chatTokensThreshold, Stretch{1}}, Column{Row{chatTokensThreshold, Stretch{1}},
linkOpenFiles, linkOpenFiles,
stream,
autosave, autosave,
enableChatInBottomToolBar, enableChatInBottomToolBar,
enableChatInNavigationPanel}}, enableChatInNavigationPanel}},
@ -294,7 +289,6 @@ void ChatAssistantSettings::resetSettingsToDefaults()
QMessageBox::Yes | QMessageBox::No); QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) { if (reply == QMessageBox::Yes) {
resetAspect(stream);
resetAspect(chatTokensThreshold); resetAspect(chatTokensThreshold);
resetAspect(temperature); resetAspect(temperature);
resetAspect(maxTokens); resetAspect(maxTokens);

View File

@ -35,7 +35,6 @@ public:
// Chat settings // Chat settings
Utils::IntegerAspect chatTokensThreshold{this}; Utils::IntegerAspect chatTokensThreshold{this};
Utils::BoolAspect linkOpenFiles{this}; Utils::BoolAspect linkOpenFiles{this};
Utils::BoolAspect stream{this};
Utils::BoolAspect autosave{this}; Utils::BoolAspect autosave{this};
Utils::BoolAspect enableChatInBottomToolBar{this}; Utils::BoolAspect enableChatInBottomToolBar{this};
Utils::BoolAspect enableChatInNavigationPanel{this}; Utils::BoolAspect enableChatInNavigationPanel{this};

View File

@ -51,10 +51,6 @@ CodeCompletionSettings::CodeCompletionSettings()
multiLineCompletion.setDefaultValue(true); multiLineCompletion.setDefaultValue(true);
multiLineCompletion.setLabelText(Tr::tr("Enable Multiline Completion")); 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.setLabelText(Tr::tr("Text output proccessing mode:"));
modelOutputHandler.setSettingsKey(Constants::CC_MODEL_OUTPUT_HANDLER); modelOutputHandler.setSettingsKey(Constants::CC_MODEL_OUTPUT_HANDLER);
modelOutputHandler.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox); modelOutputHandler.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
@ -303,7 +299,6 @@ CodeCompletionSettings::CodeCompletionSettings()
Column{autoCompletion, Column{autoCompletion,
Space{8}, Space{8},
multiLineCompletion, multiLineCompletion,
stream,
Row{modelOutputHandler, Stretch{1}}, Row{modelOutputHandler, Stretch{1}},
Row{autoCompletionCharThreshold, Row{autoCompletionCharThreshold,
autoCompletionTypingInterval, autoCompletionTypingInterval,
@ -365,7 +360,6 @@ void CodeCompletionSettings::resetSettingsToDefaults()
if (reply == QMessageBox::Yes) { if (reply == QMessageBox::Yes) {
resetAspect(autoCompletion); resetAspect(autoCompletion);
resetAspect(multiLineCompletion); resetAspect(multiLineCompletion);
resetAspect(stream);
resetAspect(temperature); resetAspect(temperature);
resetAspect(maxTokens); resetAspect(maxTokens);
resetAspect(useTopP); resetAspect(useTopP);

View File

@ -35,7 +35,6 @@ public:
// Auto Completion Settings // Auto Completion Settings
Utils::BoolAspect autoCompletion{this}; Utils::BoolAspect autoCompletion{this};
Utils::BoolAspect multiLineCompletion{this}; Utils::BoolAspect multiLineCompletion{this};
Utils::BoolAspect stream{this};
Utils::SelectionAspect modelOutputHandler{this}; Utils::SelectionAspect modelOutputHandler{this};
Utils::IntegerAspect startSuggestionTimer{this}; Utils::IntegerAspect startSuggestionTimer{this};

View File

@ -75,12 +75,10 @@ const char СС_AUTO_COMPLETION_CHAR_THRESHOLD[] = "QodeAssist.autoCompletionCha
const char СС_AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval"; const char СС_AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval";
const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold"; const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold";
const char CC_MULTILINE_COMPLETION[] = "QodeAssist.ccMultilineCompletion"; const char CC_MULTILINE_COMPLETION[] = "QodeAssist.ccMultilineCompletion";
const char CC_STREAM[] = "QodeAssist.ccStream";
const char CC_MODEL_OUTPUT_HANDLER[] = "QodeAssist.ccModelOutputHandler"; const char CC_MODEL_OUTPUT_HANDLER[] = "QodeAssist.ccModelOutputHandler";
const char CUSTOM_JSON_TEMPLATE[] = "QodeAssist.customJsonTemplate"; const char CUSTOM_JSON_TEMPLATE[] = "QodeAssist.customJsonTemplate";
const char CA_TOKENS_THRESHOLD[] = "QodeAssist.caTokensThreshold"; const char CA_TOKENS_THRESHOLD[] = "QodeAssist.caTokensThreshold";
const char CA_LINK_OPEN_FILES[] = "QodeAssist.caLinkOpenFiles"; const char CA_LINK_OPEN_FILES[] = "QodeAssist.caLinkOpenFiles";
const char CA_STREAM[] = "QodeAssist.caStream";
const char CA_AUTOSAVE[] = "QodeAssist.caAutosave"; const char CA_AUTOSAVE[] = "QodeAssist.caAutosave";
const char CC_CUSTOM_LANGUAGES[] = "QodeAssist.ccCustomLanguages"; const char CC_CUSTOM_LANGUAGES[] = "QodeAssist.ccCustomLanguages";