chore: format llm c files
C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
JS/TS / lint (pull_request) Successful in 9s
JS/TS / fmt (pull_request) Successful in 16s
C++ / clang-tidy (pull_request) Failing after 43s
Python / static (pull_request) Failing after 57s
Rust / fmt (pull_request) Successful in 1m30s
Rust / build (pull_request) Successful in 2m5s
Rust / clippy (pull_request) Successful in 1m55s
Python / verify (pull_request) Successful in 2m23s
C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
JS/TS / lint (pull_request) Successful in 9s
JS/TS / fmt (pull_request) Successful in 16s
C++ / clang-tidy (pull_request) Failing after 43s
Python / static (pull_request) Failing after 57s
Rust / fmt (pull_request) Successful in 1m30s
Rust / build (pull_request) Successful in 2m5s
Rust / clippy (pull_request) Successful in 1m55s
Python / verify (pull_request) Successful in 2m23s
This commit is contained in:
+151
-241
@@ -22,8 +22,7 @@ QString LlmClient::completionsPath(
|
||||
QString base = endpoint.trimmed();
|
||||
while (base.endsWith('/'))
|
||||
base.chop(1);
|
||||
if (!base.endsWith("/v1"))
|
||||
base += "/v1";
|
||||
if (!base.endsWith("/v1")) base += "/v1";
|
||||
return base + subpath;
|
||||
}
|
||||
|
||||
@@ -37,28 +36,24 @@ QString LlmClient::serverErrorMessage(
|
||||
if (errorValue.isObject()) {
|
||||
const QString message =
|
||||
errorValue.toObject()["message"].toString();
|
||||
if (!message.isEmpty())
|
||||
return message;
|
||||
if (!message.isEmpty()) return message;
|
||||
} else if (!errorValue.toString().isEmpty()) {
|
||||
return errorValue.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback.isEmpty()
|
||||
? QStringLiteral("Request to LLM server failed")
|
||||
: fallback;
|
||||
return fallback.isEmpty() ? QStringLiteral("Request to LLM server failed")
|
||||
: fallback;
|
||||
}
|
||||
|
||||
void LlmClient::setBusy(bool value) {
|
||||
if (m_busy == value)
|
||||
return;
|
||||
if (m_busy == value) return;
|
||||
m_busy = value;
|
||||
Q_EMIT busyChanged();
|
||||
}
|
||||
|
||||
void LlmClient::setStreamingChatId(const QString& id) {
|
||||
if (m_streamingChatId == id)
|
||||
return;
|
||||
if (m_streamingChatId == id) return;
|
||||
m_streamingChatId = id;
|
||||
Q_EMIT streamingChatIdChanged();
|
||||
}
|
||||
@@ -74,39 +69,32 @@ LlmClient::LlmClient(QObject* parent) : QObject(parent) {
|
||||
}
|
||||
|
||||
LlmClient::~LlmClient() {
|
||||
if (m_reply)
|
||||
m_reply->abort();
|
||||
if (m_reply) m_reply->abort();
|
||||
m_tools->cancelAll();
|
||||
endStream();
|
||||
}
|
||||
|
||||
void LlmClient::setEndpoint(const QString& value) {
|
||||
if (m_endpoint == value)
|
||||
return;
|
||||
if (m_endpoint == value) return;
|
||||
m_endpoint = value;
|
||||
Q_EMIT endpointChanged();
|
||||
probeContextSize();
|
||||
if (m_model.isEmpty())
|
||||
refreshModels();
|
||||
if (m_model.isEmpty()) refreshModels();
|
||||
}
|
||||
|
||||
void LlmClient::setModel(const QString& value) {
|
||||
if (m_model == value)
|
||||
return;
|
||||
if (m_model == value) return;
|
||||
m_model = value;
|
||||
Q_EMIT modelChanged();
|
||||
if (m_model.isEmpty())
|
||||
refreshModels();
|
||||
if (m_model.isEmpty()) refreshModels();
|
||||
}
|
||||
|
||||
void LlmClient::setTemperature(double value) {
|
||||
m_temperature = value;
|
||||
}
|
||||
|
||||
void LlmClient::startGeneration(
|
||||
ChatSession* session, ChatGeneration* target) {
|
||||
if (m_busy || !session || !target)
|
||||
return;
|
||||
void LlmClient::startGeneration(ChatSession* session, ChatGeneration* target) {
|
||||
if (m_busy || !session || !target) return;
|
||||
m_active = session;
|
||||
m_streaming = target;
|
||||
m_streaming->setStreaming(true);
|
||||
@@ -124,8 +112,9 @@ void LlmClient::startGeneration(
|
||||
const int targetRow =
|
||||
model->rowOf(qobject_cast<ChatMessage*>(target->parent()));
|
||||
if (targetRow < 0) {
|
||||
fail(QStringLiteral("Internal error: generation target is not in the "
|
||||
"session"));
|
||||
fail(QStringLiteral(
|
||||
"Internal error: generation target is not in the "
|
||||
"session"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,8 +130,7 @@ void LlmClient::startGeneration(
|
||||
}
|
||||
|
||||
void LlmClient::sendRound() {
|
||||
if (!m_active || !m_streaming)
|
||||
return;
|
||||
if (!m_active || !m_streaming) return;
|
||||
m_finishReason.clear();
|
||||
m_callBuilders.clear();
|
||||
m_callResults.clear();
|
||||
@@ -162,13 +150,12 @@ void LlmClient::sendRound() {
|
||||
const int targetRow =
|
||||
model->rowOf(qobject_cast<ChatMessage*>(m_streaming->parent()));
|
||||
if (targetRow < 0) {
|
||||
fail(QStringLiteral("Internal error: generation target is not in the "
|
||||
"session"));
|
||||
fail(QStringLiteral(
|
||||
"Internal error: generation target is not in the "
|
||||
"session"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Context: every message older than the target, oldest first, plus
|
||||
// the tool exchanges of the current turn so far.
|
||||
QJsonArray messages = buildContextMessages(m_active, targetRow);
|
||||
for (const QJsonValue& value : m_transcript)
|
||||
messages.append(value);
|
||||
@@ -180,11 +167,9 @@ void LlmClient::sendRound() {
|
||||
body[QStringLiteral("messages")] = messages;
|
||||
body[QStringLiteral("stream")] = true;
|
||||
body[QStringLiteral("temperature")] = m_temperature;
|
||||
if (!m_model.isEmpty())
|
||||
body[QStringLiteral("model")] = m_model;
|
||||
if (!m_model.isEmpty()) body[QStringLiteral("model")] = m_model;
|
||||
const QJsonArray toolSpecs = m_tools->specifications();
|
||||
if (!toolSpecs.isEmpty())
|
||||
body[QStringLiteral("tools")] = toolSpecs;
|
||||
if (!toolSpecs.isEmpty()) body[QStringLiteral("tools")] = toolSpecs;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
@@ -193,26 +178,22 @@ void LlmClient::sendRound() {
|
||||
m_reply = m_manager.post(request, QJsonDocument(body).toJson());
|
||||
|
||||
connect(m_reply, &QNetworkReply::readyRead, this, [this]() {
|
||||
if (m_reply)
|
||||
m_buffer.append(m_reply->readAll());
|
||||
if (m_reply) m_buffer.append(m_reply->readAll());
|
||||
drainBuffer();
|
||||
});
|
||||
connect(m_reply, &QNetworkReply::finished, this, [this]() {
|
||||
QNetworkReply* reply = m_reply;
|
||||
if (!reply)
|
||||
return;
|
||||
if (!reply) return;
|
||||
m_reply = nullptr;
|
||||
|
||||
const QNetworkReply::NetworkError error = reply->error();
|
||||
const QString errorString = reply->errorString();
|
||||
// Data not already consumed by readyRead is only reachable here.
|
||||
const QByteArray responseBody = reply->readAll();
|
||||
m_buffer.append(responseBody);
|
||||
reply->deleteLater();
|
||||
|
||||
drainBuffer();
|
||||
if (!m_streaming)
|
||||
return;
|
||||
if (!m_streaming) return;
|
||||
|
||||
if (error == QNetworkReply::NoError)
|
||||
roundFinished();
|
||||
@@ -230,8 +211,7 @@ QJsonArray LlmClient::buildContextMessages(
|
||||
for (int row = model->rowCount() - 1; row > stopBeforeRow; --row) {
|
||||
const auto* message = model->at(row);
|
||||
const auto* generation = message->activeGeneration();
|
||||
if (!generation)
|
||||
continue;
|
||||
if (!generation) continue;
|
||||
|
||||
if (message->role() == ChatMessage::Role::User) {
|
||||
QJsonObject user;
|
||||
@@ -241,8 +221,6 @@ QJsonArray LlmClient::buildContextMessages(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assistant message: replay its tool calls (and their results)
|
||||
// so the model keeps the full history of the turn.
|
||||
QList<const LlmSegment*> toolSegments;
|
||||
for (const auto* segment : generation->segments())
|
||||
if (segment->type() == LlmSegment::Type::ToolCall)
|
||||
@@ -278,8 +256,7 @@ QJsonArray LlmClient::buildContextMessages(
|
||||
for (const auto* segment : toolSegments) {
|
||||
QJsonObject toolMessage;
|
||||
toolMessage[QStringLiteral("role")] = QStringLiteral("tool");
|
||||
toolMessage[QStringLiteral("tool_call_id")] =
|
||||
segment->toolCallId();
|
||||
toolMessage[QStringLiteral("tool_call_id")] = segment->toolCallId();
|
||||
toolMessage[QStringLiteral("content")] = segment->result();
|
||||
messages.append(toolMessage);
|
||||
}
|
||||
@@ -288,51 +265,37 @@ QJsonArray LlmClient::buildContextMessages(
|
||||
}
|
||||
|
||||
void LlmClient::applyToolCallDelta(const QJsonObject& call) {
|
||||
if (!m_streaming)
|
||||
return;
|
||||
if (!m_streaming) return;
|
||||
const int index = call[QStringLiteral("index")].toInt(-1);
|
||||
if (index < 0)
|
||||
return;
|
||||
if (index < 0) return;
|
||||
while (m_callBuilders.size() <= index)
|
||||
m_callBuilders.append(ToolCallBuilder{});
|
||||
ToolCallBuilder& builder = m_callBuilders[index];
|
||||
builder.seen = true;
|
||||
const QString id = call[QStringLiteral("id")].toString();
|
||||
if (!id.isEmpty())
|
||||
builder.id = id;
|
||||
if (!id.isEmpty()) builder.id = id;
|
||||
const QJsonObject function = call[QStringLiteral("function")].toObject();
|
||||
const QString name = function[QStringLiteral("name")].toString();
|
||||
if (!name.isEmpty())
|
||||
builder.name = name;
|
||||
const QString arguments =
|
||||
function[QStringLiteral("arguments")].toString();
|
||||
if (!arguments.isEmpty())
|
||||
builder.arguments += arguments;
|
||||
if (!name.isEmpty()) builder.name = name;
|
||||
const QString arguments = function[QStringLiteral("arguments")].toString();
|
||||
if (!arguments.isEmpty()) builder.arguments += arguments;
|
||||
|
||||
if (!builder.segment) {
|
||||
// A new call: close the in-flight text segments and open a
|
||||
// running tool-call segment so the UI can track it live.
|
||||
m_streaming->closeOpenSegments();
|
||||
builder.segment =
|
||||
m_streaming->beginToolCall(builder.name, builder.id);
|
||||
builder.segment = m_streaming->beginToolCall(builder.name, builder.id);
|
||||
}
|
||||
builder.segment->setName(builder.name);
|
||||
builder.segment->setToolCallId(builder.id);
|
||||
if (!arguments.isEmpty())
|
||||
builder.segment->appendArguments(arguments);
|
||||
if (!arguments.isEmpty()) builder.segment->appendArguments(arguments);
|
||||
}
|
||||
|
||||
void LlmClient::roundFinished() {
|
||||
// [DONE] and the reply's finished signal both funnel here; only the
|
||||
// first may act.
|
||||
if (m_roundDone || !m_streaming)
|
||||
return;
|
||||
if (m_roundDone || !m_streaming) return;
|
||||
m_roundDone = true;
|
||||
|
||||
bool hasCalls = false;
|
||||
for (const auto& builder : m_callBuilders)
|
||||
if (builder.seen)
|
||||
hasCalls = true;
|
||||
if (builder.seen) hasCalls = true;
|
||||
if (m_finishReason != QLatin1String("tool_calls") && !hasCalls) {
|
||||
finishTurn();
|
||||
return;
|
||||
@@ -345,8 +308,6 @@ void LlmClient::roundFinished() {
|
||||
|
||||
m_streaming->closeOpenSegments();
|
||||
|
||||
// Record the assistant's tool-call message in the transcript so the
|
||||
// next round (and the model) can see it.
|
||||
const QString content = m_streaming->content();
|
||||
const QString reasoning = m_streaming->reasoning();
|
||||
QJsonObject assistant;
|
||||
@@ -362,8 +323,7 @@ void LlmClient::roundFinished() {
|
||||
}
|
||||
QJsonArray calls;
|
||||
for (const auto& builder : m_callBuilders) {
|
||||
if (!builder.seen)
|
||||
continue;
|
||||
if (!builder.seen) continue;
|
||||
QJsonObject function;
|
||||
function[QStringLiteral("name")] = builder.name;
|
||||
function[QStringLiteral("arguments")] = builder.arguments;
|
||||
@@ -387,112 +347,95 @@ void LlmClient::executeAllCalls() {
|
||||
|
||||
for (int i = 0; i < m_callBuilders.size(); ++i) {
|
||||
const auto& call = m_callBuilders.at(i);
|
||||
if (!call.seen)
|
||||
continue;
|
||||
if (!call.seen) continue;
|
||||
|
||||
LlmTool* tool = m_tools->tool(call.name);
|
||||
QJsonObject args;
|
||||
QString errorText;
|
||||
if (!tool) {
|
||||
errorText = QStringLiteral("Error: unknown tool '%1'")
|
||||
.arg(call.name);
|
||||
errorText =
|
||||
QStringLiteral("Error: unknown tool '%1'").arg(call.name);
|
||||
} else if (!call.arguments.isEmpty()) {
|
||||
const QJsonDocument doc =
|
||||
QJsonDocument::fromJson(call.arguments.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
errorText =
|
||||
QStringLiteral("Error: tool arguments are not valid "
|
||||
"JSON: %1")
|
||||
.arg(call.arguments);
|
||||
errorText = QStringLiteral(
|
||||
"Error: tool arguments are not valid "
|
||||
"JSON: %1")
|
||||
.arg(call.arguments);
|
||||
} else {
|
||||
args = doc.object();
|
||||
}
|
||||
}
|
||||
if (!errorText.isEmpty()) {
|
||||
m_callResults[i] = { errorText, false };
|
||||
m_callResults[i] = {errorText, false};
|
||||
if (LlmSegment* segment = call.segment)
|
||||
segment->finishTool(errorText, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
++m_pendingCalls;
|
||||
tool->execute(
|
||||
args,
|
||||
[this, i, call](const QJsonObject& result) {
|
||||
if (!m_streaming)
|
||||
return;
|
||||
const bool success =
|
||||
result.contains(QStringLiteral("output"));
|
||||
const QString content = success
|
||||
? result[QStringLiteral("output")].toString()
|
||||
: QStringLiteral("Error: ") +
|
||||
result[QStringLiteral("error")].toString();
|
||||
m_callResults[i] = { content, success };
|
||||
if (LlmSegment* segment = call.segment)
|
||||
segment->finishTool(content, success);
|
||||
if (--m_pendingCalls == 0)
|
||||
flushCallResults();
|
||||
});
|
||||
tool->execute(args, [this, i, call](const QJsonObject& result) {
|
||||
if (!m_streaming) return;
|
||||
const bool success = result.contains(QStringLiteral("output"));
|
||||
const QString content =
|
||||
success ? result[QStringLiteral("output")].toString()
|
||||
: QStringLiteral("Error: ") +
|
||||
result[QStringLiteral("error")].toString();
|
||||
m_callResults[i] = {content, success};
|
||||
if (LlmSegment* segment = call.segment)
|
||||
segment->finishTool(content, success);
|
||||
if (--m_pendingCalls == 0) flushCallResults();
|
||||
});
|
||||
}
|
||||
if (m_pendingCalls == 0)
|
||||
flushCallResults();
|
||||
if (m_pendingCalls == 0) flushCallResults();
|
||||
}
|
||||
|
||||
void LlmClient::flushCallResults() {
|
||||
if (!m_toolPhase)
|
||||
return;
|
||||
if (!m_toolPhase) return;
|
||||
m_toolPhase = false;
|
||||
if (!m_streaming)
|
||||
return;
|
||||
if (!m_streaming) return;
|
||||
for (int i = 0; i < m_callBuilders.size(); ++i) {
|
||||
const auto& call = m_callBuilders.at(i);
|
||||
if (!call.seen)
|
||||
continue;
|
||||
if (!call.seen) continue;
|
||||
QJsonObject toolMessage;
|
||||
toolMessage[QStringLiteral("role")] = QStringLiteral("tool");
|
||||
toolMessage[QStringLiteral("tool_call_id")] = call.id;
|
||||
toolMessage[QStringLiteral("content")] =
|
||||
m_callResults.at(i).content;
|
||||
toolMessage[QStringLiteral("content")] = m_callResults.at(i).content;
|
||||
m_transcript.append(toolMessage);
|
||||
}
|
||||
sendRound();
|
||||
}
|
||||
|
||||
void LlmClient::stop() {
|
||||
if (!m_busy)
|
||||
return;
|
||||
if (!m_busy) return;
|
||||
if (m_toolPhase) {
|
||||
m_tools->cancelAll();
|
||||
if (m_streaming) {
|
||||
for (auto* segment : m_streaming->segments()) {
|
||||
if (segment->type() == LlmSegment::Type::ToolCall &&
|
||||
segment->running())
|
||||
segment->finishTool(
|
||||
QStringLiteral("Cancelled"), false);
|
||||
segment->running())
|
||||
segment->finishTool(QStringLiteral("Cancelled"), false);
|
||||
}
|
||||
}
|
||||
finishTurn();
|
||||
return;
|
||||
}
|
||||
if (m_reply)
|
||||
m_reply->abort();
|
||||
if (m_reply) m_reply->abort();
|
||||
}
|
||||
|
||||
void LlmClient::endStream() {
|
||||
if (!m_streaming)
|
||||
return;
|
||||
if (!m_streaming) return;
|
||||
auto* generation = m_streaming;
|
||||
auto* session = m_active;
|
||||
m_streaming = nullptr;
|
||||
m_active = nullptr;
|
||||
generation->setStreaming(false);
|
||||
if (generation->content().isEmpty() &&
|
||||
generation->reasoning().isEmpty() &&
|
||||
generation->toolCallCount() == 0) {
|
||||
if (generation->content().isEmpty() && generation->reasoning().isEmpty() &&
|
||||
generation->toolCallCount() == 0) {
|
||||
if (auto* message = qobject_cast<ChatMessage*>(generation->parent())) {
|
||||
if (message->generationCount() <= 1) {
|
||||
if (session)
|
||||
session->removeMessage(message);
|
||||
if (session) session->removeMessage(message);
|
||||
} else {
|
||||
message->removeGeneration(generation);
|
||||
}
|
||||
@@ -503,16 +446,14 @@ void LlmClient::endStream() {
|
||||
}
|
||||
|
||||
void LlmClient::finishTurn() {
|
||||
if (!m_streaming)
|
||||
return;
|
||||
if (!m_streaming) return;
|
||||
ChatSession* session = m_active;
|
||||
endStream();
|
||||
if (session && m_pendingClear == session) {
|
||||
session->clearMessages();
|
||||
m_pendingClear.clear();
|
||||
}
|
||||
if (session)
|
||||
session->persist();
|
||||
if (session) session->persist();
|
||||
}
|
||||
|
||||
void LlmClient::clearOnFinish(ChatSession* session) {
|
||||
@@ -520,8 +461,7 @@ void LlmClient::clearOnFinish(ChatSession* session) {
|
||||
}
|
||||
|
||||
void LlmClient::sessionRemoved(ChatSession* session) {
|
||||
if (m_pendingClear == session)
|
||||
m_pendingClear.clear();
|
||||
if (m_pendingClear == session) m_pendingClear.clear();
|
||||
if (m_active == session) {
|
||||
stop();
|
||||
endStream();
|
||||
@@ -537,8 +477,7 @@ void LlmClient::fail(const QString& message) {
|
||||
void LlmClient::drainBuffer() {
|
||||
while (true) {
|
||||
const qsizetype newline = m_buffer.indexOf('\n');
|
||||
if (newline < 0)
|
||||
break;
|
||||
if (newline < 0) break;
|
||||
const QByteArray line = m_buffer.left(newline).trimmed();
|
||||
m_buffer.remove(0, newline + 1);
|
||||
handleLine(line);
|
||||
@@ -546,8 +485,7 @@ void LlmClient::drainBuffer() {
|
||||
}
|
||||
|
||||
void LlmClient::handleLine(const QByteArray& line) {
|
||||
if (!m_streaming || line.isEmpty() || !line.startsWith("data:"))
|
||||
return;
|
||||
if (!m_streaming || line.isEmpty() || !line.startsWith("data:")) return;
|
||||
|
||||
const QByteArray data = line.mid(5).trimmed();
|
||||
if (data == "[DONE]") {
|
||||
@@ -556,42 +494,35 @@ void LlmClient::handleLine(const QByteArray& line) {
|
||||
}
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (!doc.isObject())
|
||||
return;
|
||||
if (!doc.isObject()) return;
|
||||
const QJsonObject obj = doc.object();
|
||||
updateTokenUsage(obj);
|
||||
|
||||
if (obj.contains("error")) {
|
||||
const QJsonObject error = obj["error"].toObject();
|
||||
const QString message = error["message"].toString();
|
||||
fail(message.isEmpty()
|
||||
? QStringLiteral("LLM server returned an error")
|
||||
: message);
|
||||
fail(
|
||||
message.isEmpty() ? QStringLiteral("LLM server returned an error")
|
||||
: message);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QJsonValue& choiceValue : obj["choices"].toArray()) {
|
||||
if (!m_streaming)
|
||||
continue;
|
||||
if (!m_streaming) continue;
|
||||
const QJsonObject choice = choiceValue.toObject();
|
||||
const QJsonObject delta = choice["delta"].toObject();
|
||||
|
||||
const QString finishReason =
|
||||
choice[QStringLiteral("finish_reason")].toString();
|
||||
if (!finishReason.isEmpty())
|
||||
m_finishReason = finishReason;
|
||||
if (!finishReason.isEmpty()) m_finishReason = finishReason;
|
||||
|
||||
m_streaming->appendContent(delta["content"].toString());
|
||||
QString reasoning =
|
||||
delta["reasoning_content"].toString();
|
||||
if (reasoning.isEmpty())
|
||||
reasoning = delta["reasoning"].toString();
|
||||
QString reasoning = delta["reasoning_content"].toString();
|
||||
if (reasoning.isEmpty()) reasoning = delta["reasoning"].toString();
|
||||
m_streaming->appendReasoning(reasoning);
|
||||
|
||||
for (const QJsonValue& callValue :
|
||||
delta["tool_calls"].toArray()) {
|
||||
if (!m_streaming)
|
||||
break;
|
||||
for (const QJsonValue& callValue : delta["tool_calls"].toArray()) {
|
||||
if (!m_streaming) break;
|
||||
applyToolCallDelta(callValue.toObject());
|
||||
}
|
||||
}
|
||||
@@ -600,8 +531,7 @@ void LlmClient::handleLine(const QByteArray& line) {
|
||||
void LlmClient::refreshModels() {
|
||||
const QUrl url =
|
||||
QUrl::fromUserInput(completionsPath(m_endpoint, "/models"));
|
||||
if (!url.isValid() || url.host().isEmpty())
|
||||
return;
|
||||
if (!url.isValid() || url.host().isEmpty()) return;
|
||||
|
||||
auto* reply = m_manager.get(QNetworkRequest(url));
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
@@ -625,8 +555,7 @@ void LlmClient::refreshModels() {
|
||||
models.append(id);
|
||||
}
|
||||
}
|
||||
if (models.isEmpty())
|
||||
return;
|
||||
if (models.isEmpty()) return;
|
||||
|
||||
m_availableModels = models;
|
||||
Q_EMIT availableModelsChanged();
|
||||
@@ -639,66 +568,55 @@ void LlmClient::refreshModels() {
|
||||
}
|
||||
|
||||
void LlmClient::setContextSize(int size) {
|
||||
if (size <= 0 || m_contextSize == size)
|
||||
return;
|
||||
if (size <= 0 || m_contextSize == size) return;
|
||||
m_contextSize = size;
|
||||
Q_EMIT contextSizeChanged();
|
||||
}
|
||||
|
||||
void LlmClient::probeContextSize() {
|
||||
// llama.cpp-specific endpoint; other servers fall back to 4096.
|
||||
QString base = m_endpoint.trimmed();
|
||||
while (base.endsWith('/'))
|
||||
base.chop(1);
|
||||
const QUrl url = QUrl::fromUserInput(base + "/props");
|
||||
if (!url.isValid() || url.host().isEmpty())
|
||||
return;
|
||||
if (!url.isValid() || url.host().isEmpty()) return;
|
||||
|
||||
auto* reply = m_manager.get(QNetworkRequest(url));
|
||||
connect(
|
||||
reply,
|
||||
&QNetworkReply::finished,
|
||||
this,
|
||||
[this, reply]() {
|
||||
const QNetworkReply::NetworkError error = reply->error();
|
||||
const QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
const QNetworkReply::NetworkError error = reply->error();
|
||||
const QByteArray data = reply->readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
int size = 0;
|
||||
if (error == QNetworkReply::NoError) {
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (doc.isArray()) {
|
||||
for (const auto& value : doc.array()) {
|
||||
const QJsonObject slot = value.toObject();
|
||||
if (slot.contains("n_ctx")) {
|
||||
size = slot["n_ctx"].toInt(0);
|
||||
if (size > 0)
|
||||
break;
|
||||
}
|
||||
int size = 0;
|
||||
if (error == QNetworkReply::NoError) {
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (doc.isArray()) {
|
||||
for (const auto& value : doc.array()) {
|
||||
const QJsonObject slot = value.toObject();
|
||||
if (slot.contains("n_ctx")) {
|
||||
size = slot["n_ctx"].toInt(0);
|
||||
if (size > 0) break;
|
||||
}
|
||||
} else if (doc.isObject()) {
|
||||
const QJsonObject obj = doc.object();
|
||||
size = obj["n_ctx"].toInt(0);
|
||||
if (size <= 0)
|
||||
size = obj["default_generation_settings"].toObject()
|
||||
["n_ctx"].toInt(0);
|
||||
}
|
||||
} else if (doc.isObject()) {
|
||||
const QJsonObject obj = doc.object();
|
||||
size = obj["n_ctx"].toInt(0);
|
||||
if (size <= 0)
|
||||
size = obj["default_generation_settings"]
|
||||
.toObject()["n_ctx"]
|
||||
.toInt(0);
|
||||
}
|
||||
setContextSize(size > 0 ? size : 4096);
|
||||
});
|
||||
}
|
||||
setContextSize(size > 0 ? size : 4096);
|
||||
});
|
||||
}
|
||||
|
||||
void LlmClient::updateTokenUsage(const QJsonObject& data) {
|
||||
if (!m_active || m_contextSize <= 0)
|
||||
return;
|
||||
if (!m_active || m_contextSize <= 0) return;
|
||||
const QJsonObject usage = data["usage"].toObject();
|
||||
if (usage.isEmpty())
|
||||
return;
|
||||
const double used =
|
||||
usage.value("prompt_tokens").toDouble() +
|
||||
usage.value("completion_tokens").toDouble();
|
||||
if (used > 0)
|
||||
m_active->setLastTokenCount(static_cast<int>(used));
|
||||
if (usage.isEmpty()) return;
|
||||
const double used = usage.value("prompt_tokens").toDouble() +
|
||||
usage.value("completion_tokens").toDouble();
|
||||
if (used > 0) m_active->setLastTokenCount(static_cast<int>(used));
|
||||
}
|
||||
|
||||
void LlmClient::shortRequest(
|
||||
@@ -708,11 +626,10 @@ void LlmClient::shortRequest(
|
||||
std::function<void(QString result)> onResult) {
|
||||
const QUrl url =
|
||||
QUrl::fromUserInput(completionsPath(m_endpoint, "/chat/completions"));
|
||||
if (!url.isValid() || url.host().isEmpty())
|
||||
return;
|
||||
if (!url.isValid() || url.host().isEmpty()) return;
|
||||
|
||||
qInfo() << "LlmClient:" << tag << "request POST" << url.toString()
|
||||
<< "model=" << m_model;
|
||||
<< "model=" << m_model;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
@@ -729,8 +646,7 @@ void LlmClient::shortRequest(
|
||||
messages.append(user);
|
||||
|
||||
QJsonObject body;
|
||||
if (!m_model.isEmpty())
|
||||
body[QStringLiteral("model")] = m_model;
|
||||
if (!m_model.isEmpty()) body[QStringLiteral("model")] = m_model;
|
||||
body[QStringLiteral("stream")] = false;
|
||||
body[QStringLiteral("temperature")] = 0.3;
|
||||
body[QStringLiteral("max_tokens")] = 128;
|
||||
@@ -749,25 +665,25 @@ void LlmClient::shortRequest(
|
||||
reply->deleteLater();
|
||||
|
||||
qInfo() << "LlmClient:" << tag << "request finished"
|
||||
<< "error=" << reply->error() << reply->errorString()
|
||||
<< "http=" << reply->attribute(
|
||||
QNetworkRequest::HttpStatusCodeAttribute)
|
||||
.toInt()
|
||||
<< "response="
|
||||
<< QString::fromUtf8(data.left(400)).simplified();
|
||||
<< "error=" << reply->error() << reply->errorString()
|
||||
<< "http="
|
||||
<< reply
|
||||
->attribute(QNetworkRequest::HttpStatusCodeAttribute)
|
||||
.toInt()
|
||||
<< "response="
|
||||
<< QString::fromUtf8(data.left(400)).simplified();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
return;
|
||||
if (reply->error() != QNetworkReply::NoError) return;
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
const QJsonArray choices =
|
||||
doc.object()[QStringLiteral("choices")].toArray();
|
||||
if (choices.isEmpty())
|
||||
return;
|
||||
const QString result =
|
||||
choices.at(0).toObject()[QStringLiteral("message")].toObject()
|
||||
[QStringLiteral("content")].toString()
|
||||
.trimmed();
|
||||
if (choices.isEmpty()) return;
|
||||
const QString result = choices.at(0)
|
||||
.toObject()[QStringLiteral("message")]
|
||||
.toObject()[QStringLiteral("content")]
|
||||
.toString()
|
||||
.trimmed();
|
||||
qInfo() << "LlmClient:" << tag << "raw result" << result;
|
||||
onResult(result);
|
||||
});
|
||||
@@ -788,24 +704,21 @@ void LlmClient::requestTitle(ChatSession* session, const QString& userText) {
|
||||
}
|
||||
const auto isQuote = [](QChar c) {
|
||||
return c == QLatin1Char('"') || c == QLatin1Char('\'') ||
|
||||
c == QChar(u'\u201C') || c == QChar(u'\u201D') ||
|
||||
c == QChar(u'\u2018') || c == QChar(u'\u2019');
|
||||
c == QChar(u'\u201C') || c == QChar(u'\u201D') ||
|
||||
c == QChar(u'\u2018') || c == QChar(u'\u2019');
|
||||
};
|
||||
while (title.size() >= 2 && isQuote(title.at(0)) &&
|
||||
isQuote(title.at(title.size() - 1)))
|
||||
isQuote(title.at(title.size() - 1)))
|
||||
title = title.mid(1, title.size() - 2).simplified();
|
||||
while (!title.isEmpty() &&
|
||||
(title.endsWith(QLatin1Char('.')) ||
|
||||
title.endsWith(QLatin1Char('!')) ||
|
||||
title.endsWith(QLatin1Char('?'))))
|
||||
while (!title.isEmpty() && (title.endsWith(QLatin1Char('.')) ||
|
||||
title.endsWith(QLatin1Char('!')) ||
|
||||
title.endsWith(QLatin1Char('?'))))
|
||||
title.chop(1);
|
||||
if (title.size() < 2) {
|
||||
qWarning() << "LlmClient: title rejected (too short)"
|
||||
<< title;
|
||||
qWarning() << "LlmClient: title rejected (too short)" << title;
|
||||
return;
|
||||
}
|
||||
if (title.size() > 48)
|
||||
title = title.left(47) + QStringLiteral("…");
|
||||
if (title.size() > 48) title = title.left(47) + QStringLiteral("…");
|
||||
qInfo() << "LlmClient: suggesting title" << title;
|
||||
Q_EMIT titleSuggested(session, title);
|
||||
});
|
||||
@@ -813,13 +726,11 @@ void LlmClient::requestTitle(ChatSession* session, const QString& userText) {
|
||||
|
||||
void LlmClient::requestIcon(ChatSession* session, const QString& userText) {
|
||||
const QStringList icons = {
|
||||
"chat", "lightbulb", "code",
|
||||
"description", "article", "school",
|
||||
"work", "build", "science",
|
||||
"palette", "music_note", "sports_esports",
|
||||
"takeout_dining", "flight", "photo_camera",
|
||||
"psychology_alt", "favorite", "savings",
|
||||
"gamepad", "auto_awesome",
|
||||
"chat", "lightbulb", "code", "description",
|
||||
"article", "school", "work", "build",
|
||||
"science", "palette", "music_note", "sports_esports",
|
||||
"takeout_dining", "flight", "photo_camera", "psychology_alt",
|
||||
"favorite", "savings", "gamepad", "auto_awesome",
|
||||
};
|
||||
const QString prompt =
|
||||
QStringLiteral(
|
||||
@@ -831,8 +742,7 @@ void LlmClient::requestIcon(ChatSession* session, const QString& userText) {
|
||||
QStringLiteral("icon"),
|
||||
prompt,
|
||||
userText,
|
||||
[this, session = QPointer<ChatSession>(session), icons](
|
||||
QString name) {
|
||||
[this, session = QPointer<ChatSession>(session), icons](QString name) {
|
||||
if (!session) {
|
||||
qWarning() << "LlmClient: icon request: session gone";
|
||||
return;
|
||||
@@ -842,12 +752,12 @@ void LlmClient::requestIcon(ChatSession* session, const QString& userText) {
|
||||
return c == QLatin1Char('"') || c == QLatin1Char('\'');
|
||||
};
|
||||
while (name.size() >= 2 && isQuote(name.at(0)) &&
|
||||
isQuote(name.at(name.size() - 1)))
|
||||
isQuote(name.at(name.size() - 1)))
|
||||
name = name.mid(1, name.size() - 2).simplified();
|
||||
name.replace(QLatin1Char(' '), QLatin1Char('_'));
|
||||
if (!icons.contains(name)) {
|
||||
qWarning() << "LlmClient: icon not in list, using default"
|
||||
<< name;
|
||||
<< name;
|
||||
name = QStringLiteral("chat");
|
||||
}
|
||||
qInfo() << "LlmClient: suggesting icon" << name;
|
||||
|
||||
Reference in New Issue
Block a user