Compare commits

...

15 Commits

Author SHA1 Message Date
Jared Van Bortel
4e8b82c71d changelog: add this PR
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-07 14:44:12 -04:00
Jared Van Bortel
1cbea1027f modellist: fix models.json cache location
- The filename must have a version number, or we will possibly load the
  cache for the wrong version of GPT4All.
- The file should be stored in an appropriate folder for cache, not in the
  settings location.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-07 14:39:56 -04:00
Jared Van Bortel
8a64b039dc fix removal of models in subdirectories
removeModel is starting to make sense now.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-07 13:42:19 -04:00
Jared Van Bortel
f8143361d3 modellist: provide a path property for convenience
This should always be used instead of joining the current model path
setting with the filename, as models may be in subdirectories. But
fixing this correctly is not in scope for this PR.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-07 13:42:13 -04:00
Jared Van Bortel
291b4bf5f6 modellist: fix removal and improve locking
There were issues with removing clones or models with clones from the
model page, as well as trying to remove a model that no longer exists on
disk, despite being in settings. These should now be resolved.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-04 19:03:07 -04:00
Jared Van Bortel
3cfcb2a4d6 modellist: emit dataChanged on the correct index
dataChanged will not work correctly if we emit it on an old index after
sorting.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-04 18:52:00 -04:00
Jared Van Bortel
348c0515d8 modellist: emit layoutChanged when sorting, but not otherwise
Also, hint to layoutChanged about the specific way in which the model
was sorted.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-04 18:48:42 -04:00
Jared Van Bortel
62186a007e modellist: reduce updateData indent
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-04 18:42:47 -04:00
Jared Van Bortel
9ec0f92f67 mysettings: eraseModel only needs an id
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-04 18:36:17 -04:00
Jared Van Bortel
d0158d2013 modellist: assert that update functions are called from main thread
The locking strategy used by ModelList assumes that only one thread will
be trying to update the model list at a time, as writes cannot be
implemented in a fully threadsafe manner.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-04 18:29:15 -04:00
Jared Van Bortel
3dea7e2bfa modellist: fix missing #include
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-04 18:18:13 -04:00
Jared Van Bortel
45120d4d96 do not connect layoutChanged to countChanged
layoutChanged is only used when the *order* changes. When rows are
inserted or removed, one of the other three signals is fired.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-04 18:14:11 -04:00
Jared Van Bortel
d3840fb817 modellist: prefer const iterators to avoid detach
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-04 18:10:38 -04:00
Jared Van Bortel
6cde7cb671 fix double semicolons
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2024-10-04 18:05:07 -04:00
Adam Treat
158f9b9ee7 Revert "modellist: work around filtered item models getting out of sync (#2545)"
This is what caused regression seen in issue #2943

This reverts commit 30692a2dfc.

Signed-off-by: Adam Treat <treat.adam@gmail.com>
2024-10-03 08:57:43 -04:00
12 changed files with 385 additions and 331 deletions

View File

@@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Fix the local server rejecting min\_p/top\_p less than 1 ([#2996](https://github.com/nomic-ai/gpt4all/pull/2996))
- Fix "regenerate" always forgetting the most recent message ([#3011](https://github.com/nomic-ai/gpt4all/pull/3011))
- Fix loaded chats forgetting context when there is a system prompt ([#3015](https://github.com/nomic-ai/gpt4all/pull/3015))
- Fix scroll reset upon download, model removal sometimes not working, and models.json cache location ([#3034](https://github.com/nomic-ai/gpt4all/pull/3034))
## [3.3.1] - 2024-09-27 ([v3.3.y](https://github.com/nomic-ai/gpt4all/tree/v3.3.y))

View File

@@ -441,9 +441,7 @@ Rectangle {
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
visible: !isDownloading && (installed || isIncomplete)
Accessible.description: qsTr("Remove model from filesystem")
onClicked: {
Download.removeModel(filename);
}
onClicked: Download.removeModel(id)
}
MySettingsButton {

View File

@@ -92,7 +92,7 @@ MySettingsTab {
enabled: root.currentModelInfo.isClone
text: qsTr("Remove")
onClicked: {
ModelList.removeClone(root.currentModelInfo);
ModelList.uninstall(root.currentModelInfo);
comboBox.currentIndex = 0;
}
}

View File

@@ -221,9 +221,7 @@ Rectangle {
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
visible: !isDownloading && (installed || isIncomplete)
Accessible.description: qsTr("Remove model from filesystem")
onClicked: {
Download.removeModel(filename);
}
onClicked: Download.removeModel(id)
}
MySettingsButton {

View File

@@ -1659,7 +1659,7 @@ void Database::scanQueue()
if (info.isPdf()) {
QPdfDocument doc;
if (doc.load(document_path) != QPdfDocument::Error::None) {
qWarning() << "ERROR: Could not load pdf" << document_id << document_path;;
qWarning() << "ERROR: Could not load pdf" << document_id << document_path;
return updateFolderToIndex(folder_id, countForFolder);
}
title = doc.metaData(QPdfDocument::MetaDataField::Title).toString();

View File

@@ -328,38 +328,30 @@ void Download::installCompatibleModel(const QString &modelName, const QString &a
ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::InstalledRole, true }});
}
void Download::removeModel(const QString &modelFile)
// FIXME(jared): With the current implementation, it is not possible to remove a duplicate
// model file (same filename, different subdirectory) from within GPT4All
// without restarting it.
void Download::removeModel(const QString &id)
{
const QString filePath = MySettings::globalInstance()->modelPath() + modelFile;
QFile incompleteFile(ModelList::globalInstance()->incompleteDownloadPath(modelFile));
if (incompleteFile.exists()) {
incompleteFile.remove();
}
auto *modelList = ModelList::globalInstance();
bool shouldRemoveInstalled = false;
QFile file(filePath);
if (file.exists()) {
const ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile);
MySettings::globalInstance()->eraseModel(info);
shouldRemoveInstalled = info.installed && !info.isClone() && (info.isDiscovered() || info.isCompatibleApi || info.description() == "" /*indicates sideloaded*/);
if (shouldRemoveInstalled)
ModelList::globalInstance()->removeInstalled(info);
Network::globalInstance()->trackEvent("remove_model", { {"model", modelFile} });
file.remove();
emit toastMessage(tr("Model \"%1\" is removed.").arg(info.name()));
}
auto info = modelList->modelInfo(id);
if (info.id().isEmpty())
return;
if (!shouldRemoveInstalled) {
QVector<QPair<int, QVariant>> data {
{ ModelList::InstalledRole, false },
{ ModelList::BytesReceivedRole, 0 },
{ ModelList::BytesTotalRole, 0 },
{ ModelList::TimestampRole, 0 },
{ ModelList::SpeedRole, QString() },
{ ModelList::DownloadErrorRole, QString() },
};
ModelList::globalInstance()->updateDataByFilename(modelFile, data);
}
Network::globalInstance()->trackEvent("remove_model", { {"model", info.filename()} });
// remove incomplete download
QFile(modelList->incompleteDownloadPath(info.filename())).remove();
// remove file, if this is a real model
if (!info.isClone())
QFile(info.path()).remove();
// remove model list entry
modelList->uninstall(info);
emit toastMessage(tr("Model \"%1\" is removed.").arg(info.name()));
}
void Download::handleSslErrors(QNetworkReply *reply, const QList<QSslError> &errors)

View File

@@ -65,7 +65,7 @@ public:
Q_INVOKABLE void cancelDownload(const QString &modelFile);
Q_INVOKABLE void installModel(const QString &modelFile, const QString &apiKey);
Q_INVOKABLE void installCompatibleModel(const QString &modelName, const QString &apiKey, const QString &baseUrl);
Q_INVOKABLE void removeModel(const QString &modelFile);
Q_INVOKABLE void removeModel(const QString &id);
Q_INVOKABLE bool isFirstStart(bool writeVersion = false) const;
public Q_SLOTS:

View File

@@ -20,7 +20,6 @@ LocalDocsCollectionsModel::LocalDocsCollectionsModel(QObject *parent)
connect(this, &LocalDocsCollectionsModel::rowsInserted, this, &LocalDocsCollectionsModel::countChanged);
connect(this, &LocalDocsCollectionsModel::rowsRemoved, this, &LocalDocsCollectionsModel::countChanged);
connect(this, &LocalDocsCollectionsModel::modelReset, this, &LocalDocsCollectionsModel::countChanged);
connect(this, &LocalDocsCollectionsModel::layoutChanged, this, &LocalDocsCollectionsModel::countChanged);
}
bool LocalDocsCollectionsModel::filterAcceptsRow(int sourceRow,
@@ -67,7 +66,6 @@ LocalDocsModel::LocalDocsModel(QObject *parent)
connect(this, &LocalDocsModel::rowsInserted, this, &LocalDocsModel::countChanged);
connect(this, &LocalDocsModel::rowsRemoved, this, &LocalDocsModel::countChanged);
connect(this, &LocalDocsModel::modelReset, this, &LocalDocsModel::countChanged);
connect(this, &LocalDocsModel::layoutChanged, this, &LocalDocsModel::countChanged);
}
int LocalDocsModel::rowCount(const QModelIndex &parent) const

View File

@@ -21,12 +21,14 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QMutexLocker>
#include <QNetworkRequest>
#include <QObject>
#include <QRegularExpression>
#include <QSettings>
#include <QSslConfiguration>
#include <QSslSocket>
#include <QStandardPaths>
#include <QStringList>
#include <QTextStream>
#include <QTimer>
@@ -43,6 +45,8 @@ using namespace Qt::Literals::StringLiterals;
//#define USE_LOCAL_MODELSJSON
#define MODELS_JSON_VERSION "3"
static const QStringList FILENAME_BLACKLIST { u"gpt4all-nomic-embed-text-v1.rmodel"_s };
QString ModelInfo::id() const
@@ -257,7 +261,7 @@ int ModelInfo::maxContextLength() const
{
if (!installed || isOnline) return -1;
if (m_maxContextLength != -1) return m_maxContextLength;
auto path = (dirpath + filename()).toStdString();
auto path = this->path().toStdString();
int n_ctx = LLModel::Implementation::maxContextLength(path);
if (n_ctx < 0) {
n_ctx = 4096; // fallback value
@@ -281,7 +285,7 @@ int ModelInfo::maxGpuLayers() const
{
if (!installed || isOnline) return -1;
if (m_maxGpuLayers != -1) return m_maxGpuLayers;
auto path = (dirpath + filename()).toStdString();
auto path = this->path().toStdString();
int layers = LLModel::Implementation::layerCount(path);
if (layers < 0) {
layers = 100; // fallback value
@@ -398,7 +402,6 @@ InstalledModels::InstalledModels(QObject *parent, bool selectable)
connect(this, &InstalledModels::rowsInserted, this, &InstalledModels::countChanged);
connect(this, &InstalledModels::rowsRemoved, this, &InstalledModels::countChanged);
connect(this, &InstalledModels::modelReset, this, &InstalledModels::countChanged);
connect(this, &InstalledModels::layoutChanged, this, &InstalledModels::countChanged);
}
bool InstalledModels::filterAcceptsRow(int sourceRow,
@@ -423,7 +426,6 @@ DownloadableModels::DownloadableModels(QObject *parent)
connect(this, &DownloadableModels::rowsInserted, this, &DownloadableModels::countChanged);
connect(this, &DownloadableModels::rowsRemoved, this, &DownloadableModels::countChanged);
connect(this, &DownloadableModels::modelReset, this, &DownloadableModels::countChanged);
connect(this, &DownloadableModels::layoutChanged, this, &DownloadableModels::countChanged);
}
bool DownloadableModels::filterAcceptsRow(int sourceRow,
@@ -502,7 +504,7 @@ ModelList::ModelList()
connect(MySettings::globalInstance(), &MySettings::contextLengthChanged, this, &ModelList::updateDataForSettings);
connect(MySettings::globalInstance(), &MySettings::gpuLayersChanged, this, &ModelList::updateDataForSettings);
connect(MySettings::globalInstance(), &MySettings::repeatPenaltyChanged, this, &ModelList::updateDataForSettings);
connect(MySettings::globalInstance(), &MySettings::repeatPenaltyTokensChanged, this, &ModelList::updateDataForSettings);;
connect(MySettings::globalInstance(), &MySettings::repeatPenaltyTokensChanged, this, &ModelList::updateDataForSettings);
connect(MySettings::globalInstance(), &MySettings::promptTemplateChanged, this, &ModelList::updateDataForSettings);
connect(MySettings::globalInstance(), &MySettings::systemPromptChanged, this, &ModelList::updateDataForSettings);
connect(&m_networkManager, &QNetworkAccessManager::sslErrors, this, &ModelList::handleSslErrors);
@@ -542,7 +544,7 @@ const QList<ModelInfo> ModelList::selectableModelList() const
// FIXME: This needs to be kept in sync with m_selectableModels so should probably be merged
QMutexLocker locker(&m_mutex);
QList<ModelInfo> infos;
for (ModelInfo *info : m_models)
for (auto *info : std::as_const(m_models))
if (info->installed && !info->isEmbeddingModel)
infos.append(*info);
return infos;
@@ -560,7 +562,7 @@ ModelInfo ModelList::defaultModelInfo() const
const bool hasUserDefaultName = !userDefaultModelName.isEmpty() && userDefaultModelName != "Application default";
ModelInfo *defaultModel = nullptr;
for (ModelInfo *info : m_models) {
for (auto *info : std::as_const(m_models)) {
if (!info->installed)
continue;
defaultModel = info;
@@ -589,7 +591,7 @@ bool ModelList::contains(const QString &id) const
bool ModelList::containsByFilename(const QString &filename) const
{
QMutexLocker locker(&m_mutex);
for (ModelInfo *info : m_models)
for (auto *info : std::as_const(m_models))
if (info->filename() == filename)
return true;
return false;
@@ -633,6 +635,7 @@ bool ModelList::lessThan(const ModelInfo* a, const ModelInfo* b, DiscoverSort s,
void ModelList::addModel(const QString &id)
{
Q_ASSERT(QThread::currentThread() == thread());
const bool hasModel = contains(id);
Q_ASSERT(!hasModel);
if (hasModel) {
@@ -804,7 +807,7 @@ QVariant ModelList::data(const QString &id, int role) const
QVariant ModelList::dataByFilename(const QString &filename, int role) const
{
QMutexLocker locker(&m_mutex);
for (ModelInfo *info : m_models)
for (auto *info : std::as_const(m_models))
if (info->filename() == filename)
return dataInternal(info, role);
return QVariant();
@@ -821,207 +824,209 @@ QVariant ModelList::data(const QModelIndex &index, int role) const
void ModelList::updateData(const QString &id, const QVector<QPair<int, QVariant>> &data)
{
int index;
{
QMutexLocker locker(&m_mutex);
if (!m_modelMap.contains(id)) {
qWarning() << "ERROR: cannot update as model map does not contain" << id;
return;
}
QMutexLocker lock(&m_mutex);
updateDataInternal(id, data, lock, /*relock*/ false);
}
ModelInfo *info = m_modelMap.value(id);
index = m_models.indexOf(info);
if (index == -1) {
qWarning() << "ERROR: cannot update as model list does not contain" << id;
return;
}
void ModelList::updateDataInternal(const QString &id, const QVector<QPair<int, QVariant>> &data,
QMutexLocker<QMutex> &lock, bool relock)
{
Q_ASSERT(QThread::currentThread() == thread());
Q_ASSERT(lock.isLocked());
// We only sort when one of the fields used by the sorting algorithm actually changes that
// is implicated or used by the sorting algorithm
bool shouldSort = false;
if (!m_modelMap.contains(id)) {
qWarning() << "ERROR: cannot update as model map does not contain" << id;
return;
}
for (const auto &d : data) {
const int role = d.first;
const QVariant value = d.second;
switch (role) {
case IdRole:
{
if (info->id() != value.toString()) {
info->setId(value.toString());
shouldSort = true;
}
break;
ModelInfo *info = m_modelMap.value(id);
int index = m_models.indexOf(info);
if (index == -1) {
qWarning() << "ERROR: cannot update as model list does not contain" << id;
return;
}
// We only sort when one of the fields used by the sorting algorithm actually changes that
// is implicated or used by the sorting algorithm
bool shouldSort = false;
for (const auto &d : data) {
const int role = d.first;
const QVariant value = d.second;
switch (role) {
case IdRole:
{
if (info->id() != value.toString()) {
info->setId(value.toString());
shouldSort = true;
}
case NameRole:
info->setName(value.toString()); break;
case FilenameRole:
info->setFilename(value.toString()); break;
case DirpathRole:
info->dirpath = value.toString(); break;
case FilesizeRole:
info->filesize = value.toString(); break;
case HashRole:
info->hash = value.toByteArray(); break;
case HashAlgorithmRole:
info->hashAlgorithm = static_cast<ModelInfo::HashAlgorithm>(value.toInt()); break;
case CalcHashRole:
info->calcHash = value.toBool(); break;
case InstalledRole:
info->installed = value.toBool(); break;
case DefaultRole:
info->isDefault = value.toBool(); break;
case OnlineRole:
info->isOnline = value.toBool(); break;
case CompatibleApiRole:
info->isCompatibleApi = value.toBool(); break;
case DescriptionRole:
info->setDescription(value.toString()); break;
case RequiresVersionRole:
info->requiresVersion = value.toString(); break;
case VersionRemovedRole:
info->versionRemoved = value.toString(); break;
case UrlRole:
info->setUrl(value.toString()); break;
case BytesReceivedRole:
info->bytesReceived = value.toLongLong(); break;
case BytesTotalRole:
info->bytesTotal = value.toLongLong(); break;
case TimestampRole:
info->timestamp = value.toLongLong(); break;
case SpeedRole:
info->speed = value.toString(); break;
case DownloadingRole:
info->isDownloading = value.toBool(); break;
case IncompleteRole:
info->isIncomplete = value.toBool(); break;
case DownloadErrorRole:
info->downloadError = value.toString(); break;
case OrderRole:
{
if (info->order != value.toString()) {
info->order = value.toString();
shouldSort = true;
}
break;
break;
}
case NameRole:
info->setName(value.toString()); break;
case FilenameRole:
info->setFilename(value.toString()); break;
case DirpathRole:
info->dirpath = value.toString(); break;
case FilesizeRole:
info->filesize = value.toString(); break;
case HashRole:
info->hash = value.toByteArray(); break;
case HashAlgorithmRole:
info->hashAlgorithm = static_cast<ModelInfo::HashAlgorithm>(value.toInt()); break;
case CalcHashRole:
info->calcHash = value.toBool(); break;
case InstalledRole:
info->installed = value.toBool(); break;
case DefaultRole:
info->isDefault = value.toBool(); break;
case OnlineRole:
info->isOnline = value.toBool(); break;
case CompatibleApiRole:
info->isCompatibleApi = value.toBool(); break;
case DescriptionRole:
info->setDescription(value.toString()); break;
case RequiresVersionRole:
info->requiresVersion = value.toString(); break;
case VersionRemovedRole:
info->versionRemoved = value.toString(); break;
case UrlRole:
info->setUrl(value.toString()); break;
case BytesReceivedRole:
info->bytesReceived = value.toLongLong(); break;
case BytesTotalRole:
info->bytesTotal = value.toLongLong(); break;
case TimestampRole:
info->timestamp = value.toLongLong(); break;
case SpeedRole:
info->speed = value.toString(); break;
case DownloadingRole:
info->isDownloading = value.toBool(); break;
case IncompleteRole:
info->isIncomplete = value.toBool(); break;
case DownloadErrorRole:
info->downloadError = value.toString(); break;
case OrderRole:
{
if (info->order != value.toString()) {
info->order = value.toString();
shouldSort = true;
}
case RamrequiredRole:
info->ramrequired = value.toInt(); break;
case ParametersRole:
info->parameters = value.toString(); break;
case QuantRole:
info->setQuant(value.toString()); break;
case TypeRole:
info->setType(value.toString()); break;
case IsCloneRole:
{
if (info->isClone() != value.toBool()) {
info->setIsClone(value.toBool());
shouldSort = true;
}
break;
break;
}
case RamrequiredRole:
info->ramrequired = value.toInt(); break;
case ParametersRole:
info->parameters = value.toString(); break;
case QuantRole:
info->setQuant(value.toString()); break;
case TypeRole:
info->setType(value.toString()); break;
case IsCloneRole:
{
if (info->isClone() != value.toBool()) {
info->setIsClone(value.toBool());
shouldSort = true;
}
case IsDiscoveredRole:
{
if (info->isDiscovered() != value.toBool()) {
info->setIsDiscovered(value.toBool());
shouldSort = true;
}
break;
break;
}
case IsDiscoveredRole:
{
if (info->isDiscovered() != value.toBool()) {
info->setIsDiscovered(value.toBool());
shouldSort = true;
}
case IsEmbeddingModelRole:
info->isEmbeddingModel = value.toBool(); break;
case TemperatureRole:
info->setTemperature(value.toDouble()); break;
case TopPRole:
info->setTopP(value.toDouble()); break;
case MinPRole:
info->setMinP(value.toDouble()); break;
case TopKRole:
info->setTopK(value.toInt()); break;
case MaxLengthRole:
info->setMaxLength(value.toInt()); break;
case PromptBatchSizeRole:
info->setPromptBatchSize(value.toInt()); break;
case ContextLengthRole:
info->setContextLength(value.toInt()); break;
case GpuLayersRole:
info->setGpuLayers(value.toInt()); break;
case RepeatPenaltyRole:
info->setRepeatPenalty(value.toDouble()); break;
case RepeatPenaltyTokensRole:
info->setRepeatPenaltyTokens(value.toInt()); break;
case PromptTemplateRole:
info->setPromptTemplate(value.toString()); break;
case SystemPromptRole:
info->setSystemPrompt(value.toString()); break;
case ChatNamePromptRole:
info->setChatNamePrompt(value.toString()); break;
case SuggestedFollowUpPromptRole:
info->setSuggestedFollowUpPrompt(value.toString()); break;
case LikesRole:
{
if (info->likes() != value.toInt()) {
info->setLikes(value.toInt());
shouldSort = true;
}
break;
break;
}
case IsEmbeddingModelRole:
info->isEmbeddingModel = value.toBool(); break;
case TemperatureRole:
info->setTemperature(value.toDouble()); break;
case TopPRole:
info->setTopP(value.toDouble()); break;
case MinPRole:
info->setMinP(value.toDouble()); break;
case TopKRole:
info->setTopK(value.toInt()); break;
case MaxLengthRole:
info->setMaxLength(value.toInt()); break;
case PromptBatchSizeRole:
info->setPromptBatchSize(value.toInt()); break;
case ContextLengthRole:
info->setContextLength(value.toInt()); break;
case GpuLayersRole:
info->setGpuLayers(value.toInt()); break;
case RepeatPenaltyRole:
info->setRepeatPenalty(value.toDouble()); break;
case RepeatPenaltyTokensRole:
info->setRepeatPenaltyTokens(value.toInt()); break;
case PromptTemplateRole:
info->setPromptTemplate(value.toString()); break;
case SystemPromptRole:
info->setSystemPrompt(value.toString()); break;
case ChatNamePromptRole:
info->setChatNamePrompt(value.toString()); break;
case SuggestedFollowUpPromptRole:
info->setSuggestedFollowUpPrompt(value.toString()); break;
case LikesRole:
{
if (info->likes() != value.toInt()) {
info->setLikes(value.toInt());
shouldSort = true;
}
case DownloadsRole:
{
if (info->downloads() != value.toInt()) {
info->setDownloads(value.toInt());
shouldSort = true;
}
break;
break;
}
case DownloadsRole:
{
if (info->downloads() != value.toInt()) {
info->setDownloads(value.toInt());
shouldSort = true;
}
case RecencyRole:
{
if (info->recency() != value.toDateTime()) {
info->setRecency(value.toDateTime());
shouldSort = true;
}
break;
break;
}
case RecencyRole:
{
if (info->recency() != value.toDateTime()) {
info->setRecency(value.toDateTime());
shouldSort = true;
}
break;
}
}
// Extra guarantee that these always remains in sync with filesystem
QString modelPath = info->dirpath + info->filename();
const QFileInfo fileInfo(modelPath);
info->installed = fileInfo.exists();
const QFileInfo incompleteInfo(incompleteDownloadPath(info->filename()));
info->isIncomplete = incompleteInfo.exists();
// check installed, discovered/sideloaded models only (including clones)
if (!info->checkedEmbeddingModel && !info->isEmbeddingModel && info->installed
&& (info->isDiscovered() || info->description().isEmpty()))
{
// read GGUF and decide based on model architecture
info->isEmbeddingModel = LLModel::Implementation::isEmbeddingModel(modelPath.toStdString());
info->checkedEmbeddingModel = true;
}
if (shouldSort) {
auto s = m_discoverSort;
auto d = m_discoverSortDirection;
std::stable_sort(m_models.begin(), m_models.end(), [s, d](const ModelInfo* lhs, const ModelInfo* rhs) {
return ModelList::lessThan(lhs, rhs, s, d);
});
}
}
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
// FIXME(jared): for some reason these don't update correctly when the source model changes, so we explicitly invalidate them
m_selectableModels->invalidate();
m_installedModels->invalidate();
m_downloadableModels->invalidate();
// Extra guarantee that these always remains in sync with filesystem
QString modelPath = info->path();
const QFileInfo fileInfo(modelPath);
info->installed = fileInfo.exists();
const QFileInfo incompleteInfo(incompleteDownloadPath(info->filename()));
info->isIncomplete = incompleteInfo.exists();
// check installed, discovered/sideloaded models only (including clones)
if (!info->checkedEmbeddingModel && !info->isEmbeddingModel && info->installed
&& (info->isDiscovered() || info->description().isEmpty()))
{
// read GGUF and decide based on model architecture
info->isEmbeddingModel = LLModel::Implementation::isEmbeddingModel(modelPath.toStdString());
info->checkedEmbeddingModel = true;
}
lock.unlock();
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
if (shouldSort)
resortModel();
emit selectableModelListChanged();
if (relock)
lock.relock();
}
void ModelList::resortModel()
{
emit layoutAboutToBeChanged();
const QList<QPersistentModelIndex> parents { QModelIndex() };
emit layoutAboutToBeChanged(parents, QAbstractItemModel::VerticalSortHint);
{
QMutexLocker locker(&m_mutex);
auto s = m_discoverSort;
@@ -1030,29 +1035,30 @@ void ModelList::resortModel()
return ModelList::lessThan(lhs, rhs, s, d);
});
}
emit layoutChanged();
emit layoutChanged(parents, QAbstractItemModel::VerticalSortHint);
}
void ModelList::updateDataByFilename(const QString &filename, QVector<QPair<int, QVariant>> data)
{
Q_ASSERT(QThread::currentThread() == thread());
if (data.isEmpty())
return; // no-op
QVector<QString> modelsById;
{
QMutexLocker locker(&m_mutex);
for (ModelInfo *info : m_models)
QStringList modelsById;
for (auto *info : std::as_const(m_models))
if (info->filename() == filename)
modelsById.append(info->id());
}
if (modelsById.isEmpty()) {
qWarning() << "ERROR: cannot update model as list does not contain file" << filename;
return;
}
if (modelsById.isEmpty()) {
qWarning() << "ERROR: cannot update model as list does not contain file" << filename;
return;
}
for (const QString &id : modelsById)
updateData(id, data);
for (auto &id : std::as_const(modelsById))
updateDataInternal(id, data, locker, /*relock*/ &id != &modelsById.constLast());
}
}
ModelInfo ModelList::modelInfo(const QString &id) const
@@ -1066,7 +1072,7 @@ ModelInfo ModelList::modelInfo(const QString &id) const
ModelInfo ModelList::modelInfoByFilename(const QString &filename) const
{
QMutexLocker locker(&m_mutex);
for (ModelInfo *info : m_models)
for (auto *info : std::as_const(m_models))
if (info->filename() == filename)
return *info;
return ModelInfo();
@@ -1075,15 +1081,15 @@ ModelInfo ModelList::modelInfoByFilename(const QString &filename) const
bool ModelList::isUniqueName(const QString &name) const
{
QMutexLocker locker(&m_mutex);
for (const ModelInfo *info : m_models) {
if(info->name() == name)
for (auto *info : std::as_const(m_models))
if (info->name() == name)
return false;
}
return true;
}
QString ModelList::clone(const ModelInfo &model)
{
Q_ASSERT(QThread::currentThread() == thread());
const QString id = Network::globalInstance()->generateUniqueId();
addModel(id);
@@ -1115,50 +1121,84 @@ QString ModelList::clone(const ModelInfo &model)
return id;
}
void ModelList::removeClone(const ModelInfo &model)
void ModelList::uninstall(const ModelInfo &model)
{
Q_ASSERT(model.isClone());
if (!model.isClone())
return;
Q_ASSERT(QThread::currentThread() == thread());
removeInternal(model);
emit layoutChanged();
{
QMutexLocker lock(&m_mutex);
if (!model.isClone()) {
QStringList modelsById;
auto filename = model.filename();
for (const auto *info : std::as_const(m_models))
if (info->filename() == filename && info->isClone())
modelsById << info->id();
for (const auto &id : std::as_const(modelsById))
removeInternal(id, lock);
}
auto id = model.id();
if (model.isClone() || model.isDiscovered() || model.isCompatibleApi
|| model.description() == "" /*sideloaded*/
) {
// model can be completely removed from list
removeInternal(id, lock, /*relock*/ false);
emit selectableModelListChanged();
} else {
// Model came from models.json and needs to be reset instead of removed
QVector<QPair<int, QVariant>> data {
{ ModelList::InstalledRole, false },
{ ModelList::BytesReceivedRole, 0 },
{ ModelList::BytesTotalRole, 0 },
{ ModelList::TimestampRole, 0 },
{ ModelList::SpeedRole, QString() },
{ ModelList::DownloadErrorRole, QString() },
};
updateDataInternal(id, data, lock, /*relock*/ false);
// erase settings
MySettings::globalInstance()->eraseModel(id);
}
}
}
void ModelList::removeInstalled(const ModelInfo &model)
void ModelList::removeInternal(const QString &id, QMutexLocker<QMutex> &lock, bool relock)
{
Q_ASSERT(model.installed);
Q_ASSERT(!model.isClone());
Q_ASSERT(model.isDiscovered() || model.isCompatibleApi || model.description() == "" /*indicates sideloaded*/);
removeInternal(model);
emit layoutChanged();
}
Q_ASSERT(QThread::currentThread() == thread());
Q_ASSERT(lock.isLocked());
void ModelList::removeInternal(const ModelInfo &model)
{
const bool hasModel = contains(model.id());
bool hasModel = m_modelMap.contains(id);
Q_ASSERT(hasModel);
if (!hasModel) {
qWarning() << "ERROR: model list does not contain" << model.id();
qWarning() << "ERROR: model list does not contain" << id;
return;
}
int indexOfModel = 0;
{
QMutexLocker locker(&m_mutex);
ModelInfo *info = m_modelMap.value(model.id());
indexOfModel = m_models.indexOf(info);
}
beginRemoveRows(QModelIndex(), indexOfModel, indexOfModel);
{
QMutexLocker locker(&m_mutex);
ModelInfo *info = m_models.takeAt(indexOfModel);
m_modelMap.remove(info->id());
delete info;
}
auto mapIt = std::as_const(m_modelMap).find(id);
qsizetype listIdx = m_models.indexOf(*mapIt);
lock.unlock();
beginRemoveRows({}, listIdx, listIdx);
lock.relock();
// remove entry
auto *info = *mapIt;
Q_ASSERT(std::as_const(m_modelMap).find(id) == mapIt);
Q_ASSERT(m_models.indexOf(info) == listIdx);
m_modelMap.erase(mapIt);
m_models.remove(listIdx);
delete info;
lock.unlock();
endRemoveRows();
emit selectableModelListChanged();
MySettings::globalInstance()->eraseModel(model);
// erase settings
MySettings::globalInstance()->eraseModel(id);
if (relock)
lock.relock();
}
QString ModelList::uniqueModelName(const ModelInfo &model) const
@@ -1175,8 +1215,8 @@ QString ModelList::uniqueModelName(const ModelInfo &model) const
int maxSuffixNumber = 0;
bool baseNameExists = false;
for (const ModelInfo *info : m_models) {
if(info->name() == baseName)
for (auto *info : std::as_const(m_models)) {
if (info->name() == baseName)
baseNameExists = true;
QRegularExpressionMatch match = re.match(info->name());
@@ -1297,7 +1337,7 @@ void ModelList::processModelDirectory(const QString &path)
QVector<QString> modelsById;
{
QMutexLocker locker(&m_mutex);
for (ModelInfo *info : m_models)
for (auto *info : std::as_const(m_models))
if (info->filename() == filename)
modelsById.append(info->id());
}
@@ -1308,7 +1348,7 @@ void ModelList::processModelDirectory(const QString &path)
modelsById.append(filename);
}
for (const QString &id : modelsById) {
for (auto &id : std::as_const(modelsById)) {
QVector<QPair<int, QVariant>> data {
{ InstalledRole, true },
{ FilenameRole, filename },
@@ -1333,6 +1373,7 @@ void ModelList::processModelDirectory(const QString &path)
void ModelList::updateModelsFromDirectory()
{
Q_ASSERT(QThread::currentThread() == thread());
const QString exePath = QCoreApplication::applicationDirPath() + QDir::separator();
const QString localPath = MySettings::globalInstance()->modelPath();
@@ -1344,15 +1385,32 @@ void ModelList::updateModelsFromDirectory()
}
}
#define MODELS_VERSION 3
static QString modelsJsonFilename()
{
return QStringLiteral("models" MODELS_JSON_VERSION ".json");
}
static std::optional<QFile> modelsJsonCacheFile()
{
constexpr auto loc = QStandardPaths::CacheLocation;
QString modelsJsonFname = modelsJsonFilename();
if (auto path = QStandardPaths::locate(loc, modelsJsonFname); !path.isEmpty())
return std::make_optional<QFile>(path);
if (auto path = QStandardPaths::writableLocation(loc); !path.isEmpty())
return std::make_optional<QFile>(path);
return std::nullopt;
}
void ModelList::updateModelsFromJson()
{
QString modelsJsonFname = modelsJsonFilename();
#if defined(USE_LOCAL_MODELSJSON)
QUrl jsonUrl("file://" + QDir::homePath() + u"/dev/large_language_models/gpt4all/gpt4all-chat/metadata/models%1.json"_s.arg(MODELS_VERSION));
QUrl jsonUrl(u"file://%1/dev/large_language_models/gpt4all/gpt4all-chat/metadata/%2"_s.arg(QDir::homePath(), modelsJsonFname));
#else
QUrl jsonUrl(u"http://gpt4all.io/models/models%1.json"_s.arg(MODELS_VERSION));
QUrl jsonUrl(u"http://gpt4all.io/models/%1"_s.arg(modelsJsonFname));
#endif
QNetworkRequest request(jsonUrl);
QSslConfiguration conf = request.sslConfiguration();
conf.setPeerVerifyMode(QSslSocket::VerifyNone);
@@ -1371,18 +1429,15 @@ void ModelList::updateModelsFromJson()
qWarning() << "WARNING: Could not download models.json synchronously";
updateModelsFromJsonAsync();
QSettings settings;
QFileInfo info(settings.fileName());
QString dirPath = info.canonicalPath();
const QString modelsConfig = dirPath + "/models.json";
QFile file(modelsConfig);
if (!file.open(QIODeviceBase::ReadOnly)) {
qWarning() << "ERROR: Couldn't read models config file: " << modelsConfig;
} else {
QByteArray jsonData = file.readAll();
file.close();
auto cacheFile = modelsJsonCacheFile();
if (!cacheFile) {
// no known location
} else if (cacheFile->open(QIODeviceBase::ReadOnly)) {
QByteArray jsonData = cacheFile->readAll();
cacheFile->close();
parseModelsJsonFile(jsonData, false);
}
} else if (cacheFile->exists())
qWarning() << "ERROR: Couldn't read models.json cache file: " << cacheFile->fileName();
}
delete jsonReply;
}
@@ -1391,12 +1446,14 @@ void ModelList::updateModelsFromJsonAsync()
{
m_asyncModelRequestOngoing = true;
emit asyncModelRequestOngoingChanged();
QString modelsJsonFname = modelsJsonFilename();
#if defined(USE_LOCAL_MODELSJSON)
QUrl jsonUrl("file://" + QDir::homePath() + u"/dev/large_language_models/gpt4all/gpt4all-chat/metadata/models%1.json"_s.arg(MODELS_VERSION));
QUrl jsonUrl(u"file://%1/dev/large_language_models/gpt4all/gpt4all-chat/metadata/%2"_s.arg(QDir::homePath(), modelsJsonFname));
#else
QUrl jsonUrl(u"http://gpt4all.io/models/models%1.json"_s.arg(MODELS_VERSION));
QUrl jsonUrl(u"http://gpt4all.io/models/%1"_s.arg(modelsJsonFname));
#endif
QNetworkRequest request(jsonUrl);
QSslConfiguration conf = request.sslConfiguration();
conf.setPeerVerifyMode(QSslSocket::VerifyNone);
@@ -1459,23 +1516,20 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
}
if (save) {
QSettings settings;
QFileInfo info(settings.fileName());
QString dirPath = info.canonicalPath();
const QString modelsConfig = dirPath + "/models.json";
QFile file(modelsConfig);
if (!file.open(QIODeviceBase::WriteOnly)) {
qWarning() << "ERROR: Couldn't write models config file: " << modelsConfig;
} else {
file.write(jsonData);
file.close();
}
auto cacheFile = modelsJsonCacheFile();
if (!cacheFile) {
// no known location
} else if (QFileInfo(*cacheFile).dir().mkpath(u"."_s) && cacheFile->open(QIODeviceBase::WriteOnly)) {
cacheFile->write(jsonData);
cacheFile->close();
} else
qWarning() << "ERROR: Couldn't write models config file: " << cacheFile->fileName();
}
QJsonArray jsonArray = document.array();
const QString currentVersion = QCoreApplication::applicationVersion();
for (const QJsonValue &value : jsonArray) {
for (auto &value : std::as_const(jsonArray)) {
QJsonObject obj = value.toObject();
QString modelName = obj["name"].toString();
@@ -1735,6 +1789,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
void ModelList::updateDiscoveredInstalled(const ModelInfo &info)
{
Q_ASSERT(QThread::currentThread() == thread());
QVector<QPair<int, QVariant>> data {
{ ModelList::InstalledRole, true },
{ ModelList::IsDiscoveredRole, true },
@@ -1755,7 +1810,7 @@ void ModelList::updateModelsFromSettings()
{
QSettings settings;
QStringList groups = settings.childGroups();
for (const QString &g: groups) {
for (auto &g : std::as_const(groups)) {
if (!g.startsWith("model-"))
continue;
@@ -1919,16 +1974,14 @@ void ModelList::setDiscoverSort(DiscoverSort sort)
void ModelList::clearDiscoveredModels()
{
// NOTE: This could be made much more efficient
QList<ModelInfo> infos;
{
QMutexLocker locker(&m_mutex);
for (ModelInfo *info : m_models)
if (info->isDiscovered() && !info->installed)
infos.append(*info);
}
for (ModelInfo &info : infos)
removeInternal(info);
emit layoutChanged();
QMutexLocker locker(&m_mutex);
QStringList ids;
for (auto *info : std::as_const(m_models))
if (info->isDiscovered() && !info->installed)
ids << info->id();
for (auto &id : std::as_const(ids))
removeInternal(id, locker, /*relock*/ &id != &ids.constLast());
}
float ModelList::discoverProgress() const
@@ -1945,6 +1998,7 @@ bool ModelList::discoverInProgress() const
void ModelList::discoverSearch(const QString &search)
{
Q_ASSERT(QThread::currentThread() == thread());
Q_ASSERT(!m_discoverInProgress);
clearDiscoveredModels();
@@ -2053,7 +2107,7 @@ void ModelList::parseDiscoveryJsonFile(const QByteArray &jsonData)
QJsonArray jsonArray = document.array();
for (const QJsonValue &value : jsonArray) {
for (auto &value : std::as_const(jsonArray)) {
QJsonObject obj = value.toObject();
QJsonDocument jsonDocument(obj);
QByteArray jsonData = jsonDocument.toJson();
@@ -2061,7 +2115,7 @@ void ModelList::parseDiscoveryJsonFile(const QByteArray &jsonData)
QString repo_id = obj["id"].toString();
QJsonArray siblingsArray = obj["siblings"].toArray();
QList<QPair<QuantType, QString>> filteredAndSortedFilenames;
for (const QJsonValue &sibling : siblingsArray) {
for (auto &sibling : std::as_const(siblingsArray)) {
QJsonObject s = sibling.toObject();
QString filename = s["rfilename"].toString();
@@ -2100,7 +2154,7 @@ void ModelList::parseDiscoveryJsonFile(const QByteArray &jsonData)
emit discoverProgressChanged();
if (!m_discoverNumberOfResults) {
m_discoverInProgress = false;
emit discoverInProgressChanged();;
emit discoverInProgressChanged();
}
}
@@ -2176,9 +2230,8 @@ void ModelList::handleDiscoveryItemFinished()
emit discoverProgressChanged();
if (discoverProgress() >= 1.0) {
emit layoutChanged();
m_discoverInProgress = false;
emit discoverInProgressChanged();;
emit discoverInProgressChanged();
}
reply->deleteLater();

View File

@@ -23,12 +23,15 @@
using namespace Qt::Literals::StringLiterals;
template <typename> class QMutexLocker;
struct ModelInfo {
Q_GADGET
Q_PROPERTY(QString id READ id WRITE setId)
Q_PROPERTY(QString name READ name WRITE setName)
Q_PROPERTY(QString filename READ filename WRITE setFilename)
Q_PROPERTY(QString path READ path)
Q_PROPERTY(QString dirpath MEMBER dirpath)
Q_PROPERTY(QString filesize MEMBER filesize)
Q_PROPERTY(QByteArray hash MEMBER hash)
@@ -92,6 +95,9 @@ public:
QString filename() const;
void setFilename(const QString &name);
// FIXME(jared): This is the one true path of the model. Never use anything else.
QString path() const { return dirpath + filename(); }
QString description() const;
void setDescription(const QString &d);
@@ -119,6 +125,7 @@ public:
QDateTime recency() const;
void setRecency(const QDateTime &r);
// FIXME(jared): a class with getters should not also have public mutable fields
QString dirpath;
QString filesize;
QByteArray hash;
@@ -419,8 +426,8 @@ public:
Q_INVOKABLE ModelInfo modelInfoByFilename(const QString &filename) const;
Q_INVOKABLE bool isUniqueName(const QString &name) const;
Q_INVOKABLE QString clone(const ModelInfo &model);
Q_INVOKABLE void removeClone(const ModelInfo &model);
Q_INVOKABLE void removeInstalled(const ModelInfo &model);
// delist a model and erase its settings, or mark it as not installed if built-in
void uninstall(const ModelInfo &model);
ModelInfo defaultModelInfo() const;
void addModel(const QString &id);
@@ -495,7 +502,13 @@ private Q_SLOTS:
void handleSslErrors(QNetworkReply *reply, const QList<QSslError> &errors);
private:
void removeInternal(const ModelInfo &model);
// call with lock held
void updateDataInternal(const QString &id, const QVector<QPair<int, QVariant>> &data, QMutexLocker<QMutex> &lock,
bool relock = true);
// call with lock held
void removeInternal(const QString &id, QMutexLocker<QMutex> &lock, bool relock = true);
void clearDiscoveredModels();
bool modelExists(const QString &fileName) const;
int indexForModel(ModelInfo *model);

View File

@@ -186,7 +186,7 @@ void MySettings::restoreModelDefaults(const ModelInfo &info)
setModelTemperature(info, info.m_temperature);
setModelTopP(info, info.m_topP);
setModelMinP(info, info.m_minP);
setModelTopK(info, info.m_topK);;
setModelTopK(info, info.m_topK);
setModelMaxLength(info, info.m_maxLength);
setModelPromptBatchSize(info, info.m_promptBatchSize);
setModelContextLength(info, info.m_contextLength);
@@ -226,9 +226,9 @@ void MySettings::restoreLocalDocsDefaults()
setLocalDocsEmbedDevice(basicDefaults.value("localdocs/embedDevice").toString());
}
void MySettings::eraseModel(const ModelInfo &info)
void MySettings::eraseModel(QStringView id)
{
m_settings.remove(u"model-%1"_s.arg(info.id()));
m_settings.remove(u"model-%1"_s.arg(id));
}
QString MySettings::modelName(const ModelInfo &info) const

View File

@@ -8,6 +8,7 @@
#include <QSettings>
#include <QString>
#include <QStringList>
#include <QStringView>
#include <QTranslator>
#include <QVector>
@@ -83,7 +84,7 @@ public:
Q_INVOKABLE void restoreLocalDocsDefaults();
// Model/Character settings
void eraseModel(const ModelInfo &info);
void eraseModel(QStringView id);
QString modelName(const ModelInfo &info) const;
Q_INVOKABLE void setModelName(const ModelInfo &info, const QString &name, bool force = false);
QString modelFilename(const ModelInfo &info) const;