mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-13 09:49:12 -04:00
651 lines
20 KiB
C++
651 lines
20 KiB
C++
// Copyright (C) 2024-2026 Petr Mironychev
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
|
|
|
#include "ChatModel.hpp"
|
|
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QJsonDocument>
|
|
#include <QRegularExpression>
|
|
#include <QUrl>
|
|
|
|
#include <LLMQore/ContentBlocks.hpp>
|
|
|
|
#include <ConversationHistory.hpp>
|
|
#include <Message.hpp>
|
|
#include <PluginBlocks.hpp>
|
|
|
|
#include "context/ChangesManager.h"
|
|
|
|
namespace QodeAssist::Chat {
|
|
|
|
namespace {
|
|
|
|
const QString kFileEditMarker = QStringLiteral("QODEASSIST_FILE_EDIT:");
|
|
|
|
QString changesStatusToString(Context::ChangesManager::FileEditStatus status)
|
|
{
|
|
switch (status) {
|
|
case Context::ChangesManager::Pending: return QStringLiteral("pending");
|
|
case Context::ChangesManager::Applied: return QStringLiteral("applied");
|
|
case Context::ChangesManager::Rejected: return QStringLiteral("rejected");
|
|
case Context::ChangesManager::Archived: return QStringLiteral("archived");
|
|
}
|
|
return QStringLiteral("pending");
|
|
}
|
|
|
|
QString parseEditId(const QString &markerContent)
|
|
{
|
|
const int pos = markerContent.indexOf(kFileEditMarker);
|
|
if (pos < 0)
|
|
return {};
|
|
const QString jsonStr = markerContent.mid(pos + kFileEditMarker.length());
|
|
const QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
|
if (!doc.isObject())
|
|
return {};
|
|
return doc.object().value(QStringLiteral("edit_id")).toString();
|
|
}
|
|
|
|
QString collectText(const Message &m)
|
|
{
|
|
QString text;
|
|
for (const auto &block : m.blocks()) {
|
|
if (auto *t = dynamic_cast<LLMQore::TextContent *>(block.get())) {
|
|
if (!text.isEmpty())
|
|
text += QLatin1Char('\n');
|
|
text += t->text();
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
|
|
bool messageIsToolResultsOnly(const Message &m)
|
|
{
|
|
bool hasToolResult = false;
|
|
for (const auto &block : m.blocks()) {
|
|
if (dynamic_cast<LLMQore::ToolResultContent *>(block.get()))
|
|
hasToolResult = true;
|
|
else
|
|
return false;
|
|
}
|
|
return hasToolResult;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ChatModel::ChatModel(QObject *parent)
|
|
: QAbstractListModel(parent)
|
|
{
|
|
auto &changes = Context::ChangesManager::instance();
|
|
connect(
|
|
&changes, &Context::ChangesManager::fileEditApplied,
|
|
this, &ChatModel::onFileEditStatusChanged);
|
|
connect(
|
|
&changes, &Context::ChangesManager::fileEditRejected,
|
|
this, &ChatModel::onFileEditStatusChanged);
|
|
connect(
|
|
&changes, &Context::ChangesManager::fileEditUndone,
|
|
this, &ChatModel::onFileEditStatusChanged);
|
|
connect(
|
|
&changes, &Context::ChangesManager::fileEditArchived,
|
|
this, &ChatModel::onFileEditStatusChanged);
|
|
}
|
|
|
|
void ChatModel::setHistory(ConversationHistory *history)
|
|
{
|
|
if (m_history == history)
|
|
return;
|
|
|
|
if (m_history)
|
|
m_history->disconnect(this);
|
|
|
|
m_history = history;
|
|
|
|
if (m_history) {
|
|
connect(
|
|
m_history, &ConversationHistory::messageAdded,
|
|
this, &ChatModel::onHistoryMessageAdded);
|
|
connect(
|
|
m_history, &ConversationHistory::messageUpdated,
|
|
this, &ChatModel::onHistoryMessageUpdated);
|
|
connect(m_history, &ConversationHistory::cleared, this, &ChatModel::onHistoryCleared);
|
|
connect(m_history, &ConversationHistory::reset, this, &ChatModel::onHistoryReset);
|
|
}
|
|
|
|
beginResetModel();
|
|
rebuildAll();
|
|
endResetModel();
|
|
emit sessionUsageChanged();
|
|
}
|
|
|
|
int ChatModel::rowCount(const QModelIndex &parent) const
|
|
{
|
|
if (parent.isValid())
|
|
return 0;
|
|
return m_rows.size();
|
|
}
|
|
|
|
QVariant ChatModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
if (!index.isValid() || index.row() < 0 || index.row() >= m_rows.size())
|
|
return QVariant();
|
|
|
|
const Row &row = m_rows[index.row()];
|
|
switch (static_cast<Roles>(role)) {
|
|
case Roles::RoleType:
|
|
return QVariant::fromValue(row.kind);
|
|
case Roles::Content:
|
|
if (row.kind == ChatRole::FileEdit)
|
|
return overlayFileEditStatus(row.content, row.editId);
|
|
return row.content;
|
|
case Roles::Attachments:
|
|
return buildAttachmentList(row.attachments);
|
|
case Roles::Images:
|
|
return buildImageList(row.images);
|
|
case Roles::IsRedacted:
|
|
return row.isRedacted;
|
|
case Roles::PromptTokens:
|
|
return m_usageByMessageId.value(row.messageId).prompt;
|
|
case Roles::CompletionTokens:
|
|
return m_usageByMessageId.value(row.messageId).completion;
|
|
case Roles::CachedPromptTokens:
|
|
return m_usageByMessageId.value(row.messageId).cached;
|
|
case Roles::ReasoningTokens:
|
|
return m_usageByMessageId.value(row.messageId).reasoning;
|
|
case Roles::TotalTokens: {
|
|
const Usage u = m_usageByMessageId.value(row.messageId);
|
|
return u.prompt + u.completion;
|
|
}
|
|
default:
|
|
return QVariant();
|
|
}
|
|
}
|
|
|
|
QHash<int, QByteArray> ChatModel::roleNames() const
|
|
{
|
|
QHash<int, QByteArray> roles;
|
|
roles[Roles::RoleType] = "roleType";
|
|
roles[Roles::Content] = "content";
|
|
roles[Roles::Attachments] = "attachments";
|
|
roles[Roles::IsRedacted] = "isRedacted";
|
|
roles[Roles::Images] = "images";
|
|
roles[Roles::PromptTokens] = "promptTokens";
|
|
roles[Roles::CompletionTokens] = "completionTokens";
|
|
roles[Roles::CachedPromptTokens] = "cachedPromptTokens";
|
|
roles[Roles::ReasoningTokens] = "reasoningTokens";
|
|
roles[Roles::TotalTokens] = "totalTokens";
|
|
return roles;
|
|
}
|
|
|
|
QVariantList ChatModel::buildAttachmentList(const QVector<AttachmentRef> &attachments) const
|
|
{
|
|
QVariantList list;
|
|
for (const auto &attachment : attachments) {
|
|
QVariantMap map;
|
|
map["fileName"] = attachment.fileName;
|
|
map["storedPath"] = attachment.storedPath;
|
|
if (!m_chatFilePath.isEmpty()) {
|
|
QFileInfo fileInfo(m_chatFilePath);
|
|
const QString contentFolder
|
|
= QDir(fileInfo.absolutePath()).filePath(fileInfo.completeBaseName() + "_content");
|
|
map["filePath"] = QDir(contentFolder).filePath(attachment.storedPath);
|
|
} else {
|
|
map["filePath"] = QString();
|
|
}
|
|
list.append(map);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
QVariantList ChatModel::buildImageList(const QVector<ImageRef> &images) const
|
|
{
|
|
QVariantList list;
|
|
for (const auto &image : images) {
|
|
QVariantMap map;
|
|
map["fileName"] = image.fileName;
|
|
map["storedPath"] = image.storedPath;
|
|
map["mediaType"] = image.mediaType;
|
|
if (!m_chatFilePath.isEmpty()) {
|
|
QFileInfo fileInfo(m_chatFilePath);
|
|
const QString contentFolder
|
|
= QDir(fileInfo.absolutePath()).filePath(fileInfo.completeBaseName() + "_content");
|
|
const QString fullPath = QDir(contentFolder).filePath(image.storedPath);
|
|
map["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
|
|
map["filePath"] = fullPath;
|
|
} else {
|
|
map["imageUrl"] = QString();
|
|
map["filePath"] = QString();
|
|
}
|
|
list.append(map);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
QString ChatModel::overlayFileEditStatus(const QString &content, const QString &editId) const
|
|
{
|
|
const int pos = content.indexOf(kFileEditMarker);
|
|
if (pos < 0)
|
|
return content;
|
|
|
|
const QString jsonStr = content.mid(pos + kFileEditMarker.length());
|
|
const QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
|
|
if (!doc.isObject())
|
|
return content;
|
|
|
|
QJsonObject obj = doc.object();
|
|
if (!editId.isEmpty()) {
|
|
const auto edit = Context::ChangesManager::instance().getFileEdit(editId);
|
|
if (!edit.editId.isEmpty()) {
|
|
obj["status"] = changesStatusToString(edit.status);
|
|
if (!edit.statusMessage.isEmpty())
|
|
obj["status_message"] = edit.statusMessage;
|
|
}
|
|
}
|
|
return kFileEditMarker
|
|
+ QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
|
}
|
|
|
|
QHash<QString, QString> ChatModel::buildToolResultMap() const
|
|
{
|
|
QHash<QString, QString> results;
|
|
if (!m_history)
|
|
return results;
|
|
for (const auto &m : m_history->messages()) {
|
|
for (const auto &block : m.blocks()) {
|
|
if (auto *tr = dynamic_cast<LLMQore::ToolResultContent *>(block.get()))
|
|
results.insert(tr->toolUseId(), tr->result());
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
void ChatModel::appendRowsForMessage(
|
|
int messageIndex, const QHash<QString, QString> &toolResults, QVector<Row> &out) const
|
|
{
|
|
if (!m_history || messageIndex < 0 || messageIndex >= m_history->size())
|
|
return;
|
|
|
|
const Message &m = m_history->messages()[static_cast<size_t>(messageIndex)];
|
|
const QString id = m.id();
|
|
|
|
switch (m.role()) {
|
|
case Message::Role::System: {
|
|
const QString text = collectText(m);
|
|
if (!text.trimmed().isEmpty()) {
|
|
Row row;
|
|
row.kind = ChatRole::System;
|
|
row.messageIndex = messageIndex;
|
|
row.messageId = id;
|
|
row.content = text;
|
|
out.append(std::move(row));
|
|
}
|
|
break;
|
|
}
|
|
case Message::Role::User: {
|
|
QString text;
|
|
QVector<AttachmentRef> attachments;
|
|
QVector<ImageRef> images;
|
|
bool hasDisplayable = false;
|
|
for (const auto &block : m.blocks()) {
|
|
if (auto *t = dynamic_cast<LLMQore::TextContent *>(block.get())) {
|
|
if (!text.isEmpty())
|
|
text += QLatin1Char('\n');
|
|
text += t->text();
|
|
hasDisplayable = true;
|
|
} else if (auto *sa = dynamic_cast<StoredAttachmentContent *>(block.get())) {
|
|
attachments.append({sa->fileName(), sa->storedPath()});
|
|
hasDisplayable = true;
|
|
} else if (auto *si = dynamic_cast<StoredImageContent *>(block.get())) {
|
|
images.append({si->fileName(), si->storedPath(), si->mediaType()});
|
|
hasDisplayable = true;
|
|
}
|
|
}
|
|
if (hasDisplayable) {
|
|
Row row;
|
|
row.kind = ChatRole::User;
|
|
row.messageIndex = messageIndex;
|
|
row.messageId = id;
|
|
row.content = text;
|
|
row.attachments = std::move(attachments);
|
|
row.images = std::move(images);
|
|
out.append(std::move(row));
|
|
}
|
|
break;
|
|
}
|
|
case Message::Role::Assistant: {
|
|
for (const auto &block : m.blocks()) {
|
|
if (auto *th = dynamic_cast<LLMQore::ThinkingContent *>(block.get())) {
|
|
QString content = th->thinking();
|
|
if (!th->signature().isEmpty())
|
|
content += QStringLiteral("\n[Signature: ") + th->signature().left(40)
|
|
+ QStringLiteral("...]");
|
|
Row row;
|
|
row.kind = ChatRole::Thinking;
|
|
row.messageIndex = messageIndex;
|
|
row.messageId = id;
|
|
row.content = content;
|
|
out.append(std::move(row));
|
|
} else if (
|
|
auto *rth = dynamic_cast<LLMQore::RedactedThinkingContent *>(block.get())) {
|
|
QString content = QStringLiteral("[Thinking content redacted by safety systems]");
|
|
if (!rth->signature().isEmpty())
|
|
content += QStringLiteral("\n[Signature: ") + rth->signature().left(40)
|
|
+ QStringLiteral("...]");
|
|
Row row;
|
|
row.kind = ChatRole::Thinking;
|
|
row.messageIndex = messageIndex;
|
|
row.messageId = id;
|
|
row.content = content;
|
|
row.isRedacted = true;
|
|
out.append(std::move(row));
|
|
} else if (auto *t = dynamic_cast<LLMQore::TextContent *>(block.get())) {
|
|
if (!t->text().trimmed().isEmpty()) {
|
|
Row row;
|
|
row.kind = ChatRole::Assistant;
|
|
row.messageIndex = messageIndex;
|
|
row.messageId = id;
|
|
row.content = t->text();
|
|
out.append(std::move(row));
|
|
}
|
|
} else if (auto *tu = dynamic_cast<LLMQore::ToolUseContent *>(block.get())) {
|
|
const QString result = toolResults.value(tu->id());
|
|
Row toolRow;
|
|
toolRow.kind = ChatRole::Tool;
|
|
toolRow.messageIndex = messageIndex;
|
|
toolRow.messageId = id;
|
|
toolRow.content = tu->name() + QLatin1Char('\n') + result;
|
|
out.append(std::move(toolRow));
|
|
|
|
if (result.contains(kFileEditMarker)) {
|
|
Row editRow;
|
|
editRow.kind = ChatRole::FileEdit;
|
|
editRow.messageIndex = messageIndex;
|
|
editRow.messageId = id;
|
|
editRow.content = result;
|
|
editRow.editId = parseEditId(result);
|
|
out.append(std::move(editRow));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ChatModel::rebuildAll()
|
|
{
|
|
m_rows.clear();
|
|
if (!m_history)
|
|
return;
|
|
const QHash<QString, QString> toolResults = buildToolResultMap();
|
|
for (int mi = 0; mi < m_history->size(); ++mi)
|
|
appendRowsForMessage(mi, toolResults, m_rows);
|
|
}
|
|
|
|
int ChatModel::firstRowForMessage(int messageIndex) const
|
|
{
|
|
for (int i = 0; i < m_rows.size(); ++i) {
|
|
if (m_rows[i].messageIndex >= messageIndex)
|
|
return i;
|
|
}
|
|
return m_rows.size();
|
|
}
|
|
|
|
int ChatModel::startMessageIndexFor(int messageIndex) const
|
|
{
|
|
if (!m_history || messageIndex < 0 || messageIndex >= m_history->size())
|
|
return messageIndex;
|
|
|
|
const auto &msgs = m_history->messages();
|
|
const Message &m = msgs[static_cast<size_t>(messageIndex)];
|
|
if (m.role() == Message::Role::User && messageIsToolResultsOnly(m)) {
|
|
for (int j = messageIndex - 1; j >= 0; --j) {
|
|
if (msgs[static_cast<size_t>(j)].role() == Message::Role::Assistant)
|
|
return j;
|
|
}
|
|
}
|
|
return messageIndex;
|
|
}
|
|
|
|
void ChatModel::reprojectTail(int startMessageIndex)
|
|
{
|
|
if (!m_history)
|
|
return;
|
|
|
|
const int oldStart = firstRowForMessage(startMessageIndex);
|
|
const QHash<QString, QString> toolResults = buildToolResultMap();
|
|
|
|
QVector<Row> newTail;
|
|
for (int mi = startMessageIndex; mi < m_history->size(); ++mi)
|
|
appendRowsForMessage(mi, toolResults, newTail);
|
|
|
|
const int oldCount = m_rows.size() - oldStart;
|
|
const int newCount = newTail.size();
|
|
const int common = qMin(oldCount, newCount);
|
|
|
|
for (int i = 0; i < common; ++i)
|
|
m_rows[oldStart + i] = newTail[i];
|
|
if (common > 0)
|
|
emit dataChanged(index(oldStart), index(oldStart + common - 1));
|
|
|
|
if (newCount > oldCount) {
|
|
beginInsertRows(QModelIndex(), oldStart + oldCount, oldStart + newCount - 1);
|
|
for (int i = oldCount; i < newCount; ++i)
|
|
m_rows.append(newTail[i]);
|
|
endInsertRows();
|
|
} else if (newCount < oldCount) {
|
|
beginRemoveRows(QModelIndex(), oldStart + newCount, oldStart + oldCount - 1);
|
|
m_rows.remove(oldStart + newCount, oldCount - newCount);
|
|
endRemoveRows();
|
|
}
|
|
}
|
|
|
|
void ChatModel::onHistoryMessageAdded(int index)
|
|
{
|
|
reprojectTail(startMessageIndexFor(index));
|
|
}
|
|
|
|
void ChatModel::onHistoryMessageUpdated(int index)
|
|
{
|
|
reprojectTail(startMessageIndexFor(index));
|
|
}
|
|
|
|
void ChatModel::onHistoryCleared()
|
|
{
|
|
beginResetModel();
|
|
m_rows.clear();
|
|
m_usageByMessageId.clear();
|
|
endResetModel();
|
|
emit modelReseted();
|
|
emit sessionUsageChanged();
|
|
}
|
|
|
|
void ChatModel::onHistoryReset()
|
|
{
|
|
beginResetModel();
|
|
rebuildAll();
|
|
endResetModel();
|
|
emit sessionUsageChanged();
|
|
}
|
|
|
|
void ChatModel::onFileEditStatusChanged(const QString &editId)
|
|
{
|
|
for (int i = 0; i < m_rows.size(); ++i) {
|
|
if (m_rows[i].kind == ChatRole::FileEdit && m_rows[i].editId == editId)
|
|
emit dataChanged(index(i), index(i), {Roles::Content});
|
|
}
|
|
}
|
|
|
|
void ChatModel::clear()
|
|
{
|
|
if (m_history)
|
|
m_history->clear();
|
|
else
|
|
onHistoryCleared();
|
|
}
|
|
|
|
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
|
|
{
|
|
QList<MessagePart> parts;
|
|
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
|
|
int lastIndex = 0;
|
|
auto blockMatches = codeBlockRegex.globalMatch(content);
|
|
|
|
while (blockMatches.hasNext()) {
|
|
auto match = blockMatches.next();
|
|
if (match.capturedStart() > lastIndex) {
|
|
QString textBetween
|
|
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
|
if (!textBetween.isEmpty()) {
|
|
MessagePart part;
|
|
part.type = MessagePartType::Text;
|
|
part.text = textBetween;
|
|
parts.append(part);
|
|
}
|
|
}
|
|
|
|
MessagePart codePart;
|
|
codePart.type = MessagePartType::Code;
|
|
codePart.text = match.captured(2).trimmed();
|
|
codePart.language = match.captured(1);
|
|
parts.append(codePart);
|
|
|
|
lastIndex = match.capturedEnd();
|
|
}
|
|
|
|
if (lastIndex < content.length()) {
|
|
QString remainingText = content.mid(lastIndex).trimmed();
|
|
|
|
QRegularExpression unclosedBlockRegex("```(\\w*)\\n?([\\s\\S]*)$");
|
|
auto unclosedMatch = unclosedBlockRegex.match(remainingText);
|
|
|
|
if (unclosedMatch.hasMatch()) {
|
|
QString beforeCodeBlock = remainingText.left(unclosedMatch.capturedStart()).trimmed();
|
|
if (!beforeCodeBlock.isEmpty()) {
|
|
MessagePart part;
|
|
part.type = MessagePartType::Text;
|
|
part.text = beforeCodeBlock;
|
|
parts.append(part);
|
|
}
|
|
|
|
MessagePart codePart;
|
|
codePart.type = MessagePartType::Code;
|
|
codePart.text = unclosedMatch.captured(2).trimmed();
|
|
codePart.language = unclosedMatch.captured(1);
|
|
parts.append(codePart);
|
|
} else if (!remainingText.isEmpty()) {
|
|
MessagePart part;
|
|
part.type = MessagePartType::Text;
|
|
part.text = remainingText;
|
|
parts.append(part);
|
|
}
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
void ChatModel::resetModelTo(int index)
|
|
{
|
|
if (!m_history || index < 0 || index >= m_rows.size())
|
|
return;
|
|
m_history->resetTo(m_rows[index].messageIndex);
|
|
}
|
|
|
|
QVariantList ChatModel::userMessagePreviews(int maxLength) const
|
|
{
|
|
QVariantList result;
|
|
const int limit = maxLength > 4 ? maxLength : 80;
|
|
for (int i = 0; i < m_rows.size(); ++i) {
|
|
if (m_rows[i].kind != ChatRole::User)
|
|
continue;
|
|
QString preview = m_rows[i].content;
|
|
preview.replace(QLatin1Char('\n'), QLatin1Char(' '));
|
|
preview.replace(QLatin1Char('\r'), QLatin1Char(' '));
|
|
preview.replace(QLatin1Char('\t'), QLatin1Char(' '));
|
|
preview = preview.simplified();
|
|
if (preview.size() > limit)
|
|
preview = preview.left(limit - 1).trimmed() + QChar(0x2026);
|
|
QVariantMap entry;
|
|
entry[QStringLiteral("messageIndex")] = i;
|
|
entry[QStringLiteral("preview")] = preview;
|
|
result.append(entry);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void ChatModel::setMessageUsage(
|
|
const QString &messageId,
|
|
int promptTokens,
|
|
int completionTokens,
|
|
int cachedPromptTokens,
|
|
int reasoningTokens)
|
|
{
|
|
if (messageId.isEmpty())
|
|
return;
|
|
|
|
m_usageByMessageId[messageId]
|
|
= Usage{promptTokens, completionTokens, cachedPromptTokens, reasoningTokens};
|
|
|
|
for (int i = 0; i < m_rows.size(); ++i) {
|
|
if (m_rows[i].messageId == messageId) {
|
|
emit dataChanged(
|
|
index(i),
|
|
index(i),
|
|
{Roles::PromptTokens,
|
|
Roles::CompletionTokens,
|
|
Roles::CachedPromptTokens,
|
|
Roles::ReasoningTokens,
|
|
Roles::TotalTokens});
|
|
}
|
|
}
|
|
emit sessionUsageChanged();
|
|
}
|
|
|
|
int ChatModel::sessionPromptTokens() const
|
|
{
|
|
int total = 0;
|
|
if (m_history) {
|
|
for (const auto &m : m_history->messages())
|
|
total += m_usageByMessageId.value(m.id()).prompt;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
int ChatModel::sessionCompletionTokens() const
|
|
{
|
|
int total = 0;
|
|
if (m_history) {
|
|
for (const auto &m : m_history->messages())
|
|
total += m_usageByMessageId.value(m.id()).completion;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
int ChatModel::sessionCachedPromptTokens() const
|
|
{
|
|
int total = 0;
|
|
if (m_history) {
|
|
for (const auto &m : m_history->messages())
|
|
total += m_usageByMessageId.value(m.id()).cached;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
int ChatModel::sessionTotalTokens() const
|
|
{
|
|
return sessionPromptTokens() + sessionCompletionTokens();
|
|
}
|
|
|
|
void ChatModel::setChatFilePath(const QString &filePath)
|
|
{
|
|
m_chatFilePath = filePath;
|
|
}
|
|
|
|
QString ChatModel::chatFilePath() const
|
|
{
|
|
return m_chatFilePath;
|
|
}
|
|
|
|
} // namespace QodeAssist::Chat
|