From c13b33fb4d3744ef36e8cd562d13b5d5ab323501 Mon Sep 17 00:00:00 2001 From: Jared Van Bortel Date: Fri, 23 Aug 2024 11:50:36 -0400 Subject: [PATCH] WIP --- gpt4all-chat/CMakeLists.txt | 2 +- gpt4all-chat/chat.h | 2 +- gpt4all-chat/download.cpp | 38 +++-- gpt4all-chat/llamacpp_model.cpp | 28 ---- gpt4all-chat/llmodel.cpp | 34 +++++ gpt4all-chat/llmodel.h | 3 + gpt4all-chat/modellist.cpp | 261 ++++++++++++++++++++------------ gpt4all-chat/modellist.h | 45 +++--- 8 files changed, 256 insertions(+), 157 deletions(-) create mode 100644 gpt4all-chat/llmodel.cpp diff --git a/gpt4all-chat/CMakeLists.txt b/gpt4all-chat/CMakeLists.txt index 3165077d..325d21d1 100644 --- a/gpt4all-chat/CMakeLists.txt +++ b/gpt4all-chat/CMakeLists.txt @@ -109,7 +109,7 @@ endif() qt_add_executable(chat main.cpp chat.h chat.cpp - llmodel.h + llmodel.h llmodel.cpp llamacpp_model.h llamacpp_model.cpp chatmodel.h chatlistmodel.h chatlistmodel.cpp chatapi.h chatapi.cpp diff --git a/gpt4all-chat/chat.h b/gpt4all-chat/chat.h index 959004e5..2b4187bf 100644 --- a/gpt4all-chat/chat.h +++ b/gpt4all-chat/chat.h @@ -94,7 +94,7 @@ public: Q_INVOKABLE void reloadModel(); Q_INVOKABLE void forceUnloadModel(); Q_INVOKABLE void forceReloadModel(); - Q_INVOKABLE void trySwitchContextOfLoadedModel(); + void trySwitchContextOfLoadedModel(); void unloadAndDeleteLater(); void markForDeletion(); diff --git a/gpt4all-chat/download.cpp b/gpt4all-chat/download.cpp index 47981d0b..843fefd9 100644 --- a/gpt4all-chat/download.cpp +++ b/gpt4all-chat/download.cpp @@ -263,16 +263,17 @@ void Download::installModel(const QString &modelFile, const QString &apiKey) QFile file(filePath); if (file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Text)) { - QJsonObject obj; QString modelName(modelFile); modelName.remove(0, 8); // strip "gpt4all-" prefix modelName.chop(7); // strip ".rmodel" extension - obj.insert("apiKey", apiKey); - obj.insert("modelName", modelName); - QJsonDocument doc(obj); + QJsonObject obj { + { "type", ... }, + { "apiKey", apiKey }, + { "modelName", modelName }, + }; QTextStream stream(&file); - stream << doc.toJson(); + stream << QJsonDocument(doc).toJson(); file.close(); ModelList::globalInstance()->updateModelsFromDirectory(); emit toastMessage(tr("Model \"%1\" is installed successfully.").arg(modelName)); @@ -312,14 +313,15 @@ void Download::installCompatibleModel(const QString &modelName, const QString &a QString filePath = MySettings::globalInstance()->modelPath() + modelFile; QFile file(filePath); if (file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Text)) { - QJsonObject obj; - obj.insert("apiKey", apiKey); - obj.insert("modelName", modelName); - obj.insert("baseUrl", apiBaseUrl.toString()); - QJsonDocument doc(obj); + QJsonObject obj { + { "type", "openai-generic" }, + { "apiKey", apiKey }, + { "modelName", modelName }, + { "baseUrl", apiBaseUrl.toString() }, + }; QTextStream stream(&file); - stream << doc.toJson(); + stream << QJsonDocument(obj).toJson(); file.close(); ModelList::globalInstance()->updateModelsFromDirectory(); emit toastMessage(tr("Model \"%1 (%2)\" is installed successfully.").arg(modelName, baseUrl)); @@ -336,20 +338,26 @@ void Download::removeModel(const QString &modelFile) incompleteFile.remove(); } - bool shouldRemoveInstalled = false; + bool removedFromList = 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) + if ( + info.installed && !info.isClone() && ( + info.isDiscovered() || info.description() == "" /*indicates sideloaded*/ + || info.provider == ModelInfo::Provider::OpenAIGeneric + ) + ) { ModelList::globalInstance()->removeInstalled(info); + removedFromList = true; + } Network::globalInstance()->trackEvent("remove_model", { {"model", modelFile} }); file.remove(); emit toastMessage(tr("Model \"%1\" is removed.").arg(info.name())); } - if (!shouldRemoveInstalled) { + if (!removedFromList) { QVector> data { { ModelList::InstalledRole, false }, { ModelList::BytesReceivedRole, 0 }, diff --git a/gpt4all-chat/llamacpp_model.cpp b/gpt4all-chat/llamacpp_model.cpp index b9cacd98..9112d563 100644 --- a/gpt4all-chat/llamacpp_model.cpp +++ b/gpt4all-chat/llamacpp_model.cpp @@ -579,34 +579,6 @@ bool LlamaCppModel::isModelLoaded() const return m_llModelInfo.model && m_llModelInfo.model->isModelLoaded(); } -std::string remove_leading_whitespace(const std::string& input) -{ - auto first_non_whitespace = std::find_if(input.begin(), input.end(), [](unsigned char c) { - return !std::isspace(c); - }); - - if (first_non_whitespace == input.end()) - return std::string(); - - return std::string(first_non_whitespace, input.end()); -} - -std::string trim_whitespace(const std::string& input) -{ - auto first_non_whitespace = std::find_if(input.begin(), input.end(), [](unsigned char c) { - return !std::isspace(c); - }); - - if (first_non_whitespace == input.end()) - return std::string(); - - auto last_non_whitespace = std::find_if(input.rbegin(), input.rend(), [](unsigned char c) { - return !std::isspace(c); - }).base(); - - return std::string(first_non_whitespace, last_non_whitespace); -} - // FIXME(jared): we don't actually have to re-decode the prompt to generate a new response void LlamaCppModel::regenerateResponse() { diff --git a/gpt4all-chat/llmodel.cpp b/gpt4all-chat/llmodel.cpp new file mode 100644 index 00000000..4b6ffddb --- /dev/null +++ b/gpt4all-chat/llmodel.cpp @@ -0,0 +1,34 @@ +#include "llmodel.h" + +#include +#include +#include + + +std::string remove_leading_whitespace(const std::string &input) +{ + auto first_non_whitespace = std::find_if(input.begin(), input.end(), [](unsigned char c) { + return !std::isspace(c); + }); + + if (first_non_whitespace == input.end()) + return std::string(); + + return std::string(first_non_whitespace, input.end()); +} + +std::string trim_whitespace(const std::string &input) +{ + auto first_non_whitespace = std::find_if(input.begin(), input.end(), [](unsigned char c) { + return !std::isspace(c); + }); + + if (first_non_whitespace == input.end()) + return std::string(); + + auto last_non_whitespace = std::find_if(input.rbegin(), input.rend(), [](unsigned char c) { + return !std::isspace(c); + }).base(); + + return std::string(first_non_whitespace, last_non_whitespace); +} diff --git a/gpt4all-chat/llmodel.h b/gpt4all-chat/llmodel.h index a1bb975b..e2f915d9 100644 --- a/gpt4all-chat/llmodel.h +++ b/gpt4all-chat/llmodel.h @@ -73,3 +73,6 @@ Q_SIGNALS: void databaseResultsChanged(const QList &results); void modelInfoChanged(const ModelInfo &modelInfo); }; + +std::string remove_leading_whitespace(const std::string &input); +std::string trim_whitespace(const std::string &input); diff --git a/gpt4all-chat/modellist.cpp b/gpt4all-chat/modellist.cpp index 1b624c70..9564bcda 100644 --- a/gpt4all-chat/modellist.cpp +++ b/gpt4all-chat/modellist.cpp @@ -36,6 +36,8 @@ #include #include #include +#include +#include #include #include @@ -43,8 +45,33 @@ using namespace Qt::Literals::StringLiterals; //#define USE_LOCAL_MODELSJSON + static const QStringList FILENAME_BLACKLIST { u"gpt4all-nomic-embed-text-v1.rmodel"_s }; +// Maps "type" of current .rmodel format to a provider. +static const QHash RMODEL_TYPES { + { u"openai"_s, ModelInfo::Provider::OpenAI }, + { u"mistral"_s, ModelInfo::Provider::Mistral }, + { u"openai-generic"_s, ModelInfo::Provider::OpenAIGeneric }, +}; + +// For backwards compatbility only. Do not add to this list. +static const QHash BUILTIN_RMODEL_FILENAMES { + { u"gpt4all-gpt-3.5-turbo.rmodel"_s, ModelInfo::Provider::OpenAI }, + { u"gpt4all-gpt-4.rmodel"_s, ModelInfo::Provider::OpenAI }, + { u"gpt4all-mistral-tiny.rmodel"_s, ModelInfo::Provider::Mistral }, + { u"gpt4all-mistral-small.rmodel"_s, ModelInfo::Provider::Mistral }, + { u"gpt4all-mistral-medium.rmodel"_s, ModelInfo::Provider::Mistral }, +}; + +static ModelInfo::Provider getBuiltinRmodelFilename(const QString &filename) +{ + auto provider = BUILTIN_RMODEL_FILENAMES.value(filename, ModelInfo::INVALID_PROVIDER); + if (provider == ModelInfo::INVALID_PROVIDER) + throw std::invalid_arugment("unrecognized rmodel filename: " + filename.toStdString()); + return provider; +} + QString ModelInfo::id() const { return m_id; @@ -694,6 +721,8 @@ int ModelList::rowCount(const QModelIndex &parent) const QVariant ModelList::dataInternal(const ModelInfo *info, int role) const { switch (role) { + case ProviderRole: + return info->provider(); case IdRole: return info->id(); case NameRole: @@ -714,10 +743,6 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const return info->installed; case DefaultRole: return info->isDefault; - case OnlineRole: - return info->isOnline; - case CompatibleApiRole: - return info->isCompatibleApi; case DescriptionRole: return info->description(); case RequiresVersionRole: @@ -846,6 +871,8 @@ void ModelList::updateData(const QString &id, const QVector const int role = d.first; const QVariant value = d.second; switch (role) { + case ProviderRole: + info->m_provider = value.value(); case IdRole: { if (info->id() != value.toString()) { @@ -865,17 +892,13 @@ void ModelList::updateData(const QString &id, const QVector case HashRole: info->hash = value.toByteArray(); break; case HashAlgorithmRole: - info->hashAlgorithm = static_cast(value.toInt()); break; + info->hashAlgorithm = value.value(); 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: @@ -1090,13 +1113,12 @@ QString ModelList::clone(const ModelInfo &model) addModel(id); QVector> data { + { ModelList::ProviderRole, model.provider }, { ModelList::InstalledRole, model.installed }, { ModelList::IsCloneRole, true }, { ModelList::NameRole, uniqueModelName(model) }, { ModelList::FilenameRole, model.filename() }, { ModelList::DirpathRole, model.dirpath }, - { ModelList::OnlineRole, model.isOnline }, - { ModelList::CompatibleApiRole, model.isCompatibleApi }, { ModelList::IsEmbeddingModelRole, model.isEmbeddingModel }, { ModelList::TemperatureRole, model.temperature() }, { ModelList::TopPRole, model.topP() }, @@ -1129,9 +1151,9 @@ void ModelList::removeClone(const ModelInfo &model) void ModelList::removeInstalled(const ModelInfo &model) { + Q_ASSERT(model.provider == ModelInfo::Provider::LlamaCpp || model.provider == ModelInfo::Provider::OpenAIGeneric); Q_ASSERT(model.installed); Q_ASSERT(!model.isClone()); - Q_ASSERT(model.isDiscovered() || model.isCompatibleApi || model.description() == "" /*indicates sideloaded*/); removeInternal(model); emit layoutChanged(); } @@ -1212,53 +1234,144 @@ bool ModelList::modelExists(const QString &modelFilename) const static void updateOldRemoteModels(const QString &path) { - QDirIterator it(path, QDirIterator::Subdirectories); + QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { - it.next(); - if (!it.fileInfo().isDir()) { - QString filename = it.fileName(); - if (filename.startsWith("chatgpt-") && filename.endsWith(".txt")) { - QString apikey; - QString modelname(filename); - modelname.chop(4); // strip ".txt" extension - modelname.remove(0, 8); // strip "chatgpt-" prefix - QFile file(path + filename); - if (file.open(QIODevice::ReadWrite)) { - QTextStream in(&file); - apikey = in.readAll(); - file.close(); - } + QFileInfo info = it.nextFileInfo(); + QString filename = info.fileName(); + if (!filename.startsWith("chatgpt-") || !filename.endsWith(".txt")) + continue; - QJsonObject obj; - obj.insert("apiKey", apikey); - obj.insert("modelName", modelname); - QJsonDocument doc(obj); + QString apikey; + QString modelname(filename); + modelname.chop(4); // strip ".txt" extension + modelname.remove(0, 8); // strip "chatgpt-" prefix + QFile file(info.filePath()); + if (!file.open(QFile::ReadOnly)) { + qWarning(tr("cannot open \"%s\": %s"), info.filePath(), file.errorString()); + continue; + } - auto newfilename = u"gpt4all-%1.rmodel"_s.arg(modelname); - QFile newfile(path + newfilename); - if (newfile.open(QIODevice::ReadWrite)) { - QTextStream out(&newfile); - out << doc.toJson(); - newfile.close(); - } - file.remove(); - } + { + QTextStream in(&file); + apikey = in.readAll(); + file.close(); + } + + QJsonObject obj { + { "type", "openai" }, + { "apiKey", apikey }, + { "modelName", modelname }, + }; + + QFile newfile(u"%1/gpt4all-%2.rmodel"_s.arg(info.dir().path(), modelname)); + if (!newfile.open(QFile::ReadWrite)) { + qWarning(tr("cannot create \"%s\": %s"), newfile.fileName(), file.errorString()); + continue; + } + + QTextStream out(&newfile); + out << QJsonDocument(obj).toJson(); + newfile.close(); + file.remove(); + } +} + +[[nodiscard]] +static bool parseRemoteModel(QVector> &props, const QFileInfo &info) +{ + QJsonObject remoteModel; + { + QFile file(info.filePath()); + if (!file.open(QFile::ReadOnly)) { + qWarning(tr("cannot open \"%s\": %s"), info.filePath(), file.errorString()); + return false; + } + QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + remoteModel = doc.object(); + } + + ModelInfo::Provider provider; + QString remoteModelName, remoteApiKey; + { + const auto INVALID = ModelInfo::INVALID_PROVIDER; + + std::optional providerJson; + if (auto type = remoteModel["type"]; type.type() != QJsonValue::Unknown) + providerJson.reset(RMODEL_TYPES.value(type, INVALID)); + + auto apiKey = remoteModel["apiKey"]; + auto modelName = remoteModel["modelName"]; + if (modelName.type() != QJsonValue::String || apiKey.type() != QJsonValue::String || providerJson == INVALID) { + qWarning(tr("bad rmodel \"%s\": unrecognized format"), info.filePath()); + return false; + } + remoteModelName = modelName.toString(); + remoteApiKey = apiKey.toString(); + + if (providerJson) { + provider = providerJson.value(); + } else if (auto builtin = BUILTIN_RMODEL_FILENAMES.value(filename, INVALID); builtin != INVALID) { + provider = builtin; + } else { + goto bad_data; } } + + QString name; + QString description; + if (provider == ModelInfo::Provider::OpenAIGeneric) { + auto baseUrl = remoteModel["baseUrl"]; + if (baseUrl.type() != QJsonValue::String) + goto bad_data; + + QString apiKey = remoteApiKey; + apiKey = apiKey.length() < 10 ? "*****" : apiKey.left(5) + "*****"; + QString baseUrl(remoteModel["baseUrl"].toString()); + name = tr("%1 (%2)").arg(remoteModelName, baseUrl); + description = tr("OpenAI-Compatible API Model
" + "
  • API Key: %1
  • " + "
  • Base URL: %2
  • " + "
  • Model Name: %3
") + .arg(apiKey, baseUrl, remoteModelName); + + // The description is hard-coded into "GPT4All.ini" due to performance issue. + // If the description goes to be dynamic from its .rmodel file, it will get high I/O usage while using the ModelList. + props << QVector> { + { NameRole, name }, + { DescriptionRole, description }, + // Prompt template should be clear while using ChatML format which is using in most of OpenAI-Compatible API server. + { PromptTemplateRole, "%1" }, + }; + } + + props << QVector> { + { ProviderRole, provider }, + }; + return true; + +bad_data: + qWarning(tr("bad rmodel \"%s\": unrecognized data"), info.filePath()); + return false; } void ModelList::processModelDirectory(const QString &path) { QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { - it.next(); + QFileInfo info = it.nextFileInfo(); - QString filename = it.fileName(); + QString filename = info.fileName(); if (filename.startsWith("incomplete") || FILENAME_BLACKLIST.contains(filename)) continue; if (!filename.endsWith(".gguf") && !filename.endsWith(".rmodel")) continue; + QVector> props; + if (!filename.endswith(".rmodel")) { + props.emplaceBack(ProviderRole, ModelInfo::Provider::LlamaCpp); + } else if (!parseRemoteModel(props, info)) + continue; + QVector modelsById; { QMutexLocker locker(&m_mutex); @@ -1273,56 +1386,15 @@ void ModelList::processModelDirectory(const QString &path) modelsById.append(filename); } - QFileInfo info = it.fileInfo(); - - bool isOnline(filename.endsWith(".rmodel")); - bool isCompatibleApi(filename.endsWith("-capi.rmodel")); - - QString name; - QString description; - if (isCompatibleApi) { - QJsonObject obj; - { - QFile file(path + filename); - bool success = file.open(QIODeviceBase::ReadOnly); - (void)success; - Q_ASSERT(success); - QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); - obj = doc.object(); - } - { - QString apiKey(obj["apiKey"].toString()); - QString baseUrl(obj["baseUrl"].toString()); - QString modelName(obj["modelName"].toString()); - apiKey = apiKey.length() < 10 ? "*****" : apiKey.left(5) + "*****"; - name = tr("%1 (%2)").arg(modelName, baseUrl); - description = tr("OpenAI-Compatible API Model
" - "
  • API Key: %1
  • " - "
  • Base URL: %2
  • " - "
  • Model Name: %3
") - .arg(apiKey, baseUrl, modelName); - } - } - for (const QString &id : modelsById) { - QVector> data { - { InstalledRole, true }, - { FilenameRole, filename }, - { OnlineRole, isOnline }, - { CompatibleApiRole, isCompatibleApi }, - { DirpathRole, info.dir().absolutePath() + "/" }, - { FilesizeRole, info.size() }, + props << QVector> { + { ProviderRole, provider }, + { InstalledRole, true }, + { FilenameRole, filename }, + { DirpathRole, info.dir().absolutePath() + "/" }, + { FilesizeRole, info.size() }, }; - if (isCompatibleApi) { - // The data will be saved to "GPT4All.ini". - data.append({ NameRole, name }); - // The description is hard-coded into "GPT4All.ini" due to performance issue. - // If the description goes to be dynamic from its .rmodel file, it will get high I/O usage while using the ModelList. - data.append({ DescriptionRole, description }); - // Prompt template should be clear while using ChatML format which is using in most of OpenAI-Compatible API server. - data.append({ PromptTemplateRole, "%1" }); - } - updateData(id, data); + updateData(id, props); } } } @@ -1571,6 +1643,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) if (!contains(id)) addModel(id); QVector> data { + { ModelList::ProviderRole, BUILTIN_RMODEL_FILENAMES. }, { ModelList::NameRole, modelName }, { ModelList::FilenameRole, modelFilename }, { ModelList::FilesizeRole, 0 }, @@ -1599,6 +1672,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) if (!contains(id)) addModel(id); QVector> data { + { ModelList::ProviderRole, getBuiltinRmodelFilename(modelFilename) }, { ModelList::NameRole, modelName }, { ModelList::FilenameRole, modelFilename }, { ModelList::FilesizeRole, 0 }, @@ -1630,6 +1704,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) if (!contains(id)) addModel(id); QVector> data { + { ModelList::ProviderRole, getBuiltinRmodelFilename(modelFilename) }, { ModelList::NameRole, modelName }, { ModelList::FilenameRole, modelFilename }, { ModelList::FilesizeRole, 0 }, @@ -1655,6 +1730,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) if (!contains(id)) addModel(id); QVector> data { + { ModelList::ProviderRole, getBuiltinRmodelFilename(modelFilename) }, { ModelList::NameRole, modelName }, { ModelList::FilenameRole, modelFilename }, { ModelList::FilesizeRole, 0 }, @@ -1681,10 +1757,10 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) if (!contains(id)) addModel(id); QVector> data { + { ModelList::ProviderRole, getBuiltinRmodelFilename(modelFilename) }, { ModelList::NameRole, modelName }, { ModelList::FilenameRole, modelFilename }, { ModelList::FilesizeRole, 0 }, - { ModelList::OnlineRole, true }, { ModelList::DescriptionRole, tr("Mistral Medium model
%1").arg(mistralDesc) }, { ModelList::RequiresVersionRole, "2.7.4" }, @@ -1710,10 +1786,9 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) if (!contains(id)) addModel(id); QVector> data { + { ModelList::ProviderRole, ModelInfo::Provider::OpenAIGeneric }, { ModelList::NameRole, modelName }, { ModelList::FilesizeRole, 0 }, - { ModelList::OnlineRole, true }, - { ModelList::CompatibleApiRole, true }, { ModelList::DescriptionRole, tr("Connect to OpenAI-compatible API server
%1").arg(compatibleDesc) }, { ModelList::RequiresVersionRole, "2.7.4" }, diff --git a/gpt4all-chat/modellist.h b/gpt4all-chat/modellist.h index fb1b7ed2..2515110f 100644 --- a/gpt4all-chat/modellist.h +++ b/gpt4all-chat/modellist.h @@ -25,6 +25,7 @@ using namespace Qt::Literals::StringLiterals; struct ModelInfo { Q_GADGET + Q_PROPERTY(Provider provider READ provider) Q_PROPERTY(QString id READ id WRITE setId) Q_PROPERTY(QString name READ name WRITE setName) Q_PROPERTY(QString filename READ filename WRITE setFilename) @@ -35,8 +36,7 @@ struct ModelInfo { Q_PROPERTY(bool calcHash MEMBER calcHash) Q_PROPERTY(bool installed MEMBER installed) Q_PROPERTY(bool isDefault MEMBER isDefault) - Q_PROPERTY(bool isOnline MEMBER isOnline) - Q_PROPERTY(bool isCompatibleApi MEMBER isCompatibleApi) + Q_PROPERTY(bool isOnline READ isOnline) Q_PROPERTY(QString description READ description WRITE setDescription) Q_PROPERTY(QString requiresVersion MEMBER requiresVersion) Q_PROPERTY(QString versionRemoved MEMBER versionRemoved) @@ -77,10 +77,27 @@ struct ModelInfo { Q_PROPERTY(QDateTime recency READ recency WRITE setRecency) public: - enum HashAlgorithm { + enum class Provider { + LlamaCpp, + // Pre-configured model from openai.com or mistral.ai + OpenAI, + Mistral, + // Model with a custom endpoint configured by the user (stored in *-capi.rmodel) + OpenAIGeneric, + }; + Q_ENUM(Provider) + + // Not a valid member of the Provider enum. Used as a sentinel with Qt containers. + static constexpr Provider INVALID_PROVIDER = Provider(-1); + + enum class HashAlgorithm { Md5, Sha256 }; + Q_ENUM(HashAlgorithm) + + Provider provider() const { return m_provider; } + bool isOnline() const { return m_provider != Provider::LlamaCpp; } QString id() const; void setId(const QString &id); @@ -109,8 +126,8 @@ public: QString description() const; void setDescription(const QString &d); - /* For built-in OpenAI-compatible models (isOnline && !isCompatibleApi), this is the full completions endpoint URL. - * For custom OpenAI-compatible models (isCompatibleApi), this is not set. + /* For built-in OpenAI-compatible models, this is the full completions endpoint URL. + * For custom OpenAI-compatible models (Provider::OpenAIGeneric), this is not set. * For discovered models (isDiscovered), this is the resolved URL of the GGUF file. */ QString url() const; void setUrl(const QString &u); @@ -142,17 +159,6 @@ public: bool calcHash = false; bool installed = false; bool isDefault = false; - // Differences between 'isOnline' and 'isCompatibleApi' in ModelInfo: - // 'isOnline': - // - Indicates whether this is a online model. - // - Linked with the ModelList, fetching info from it. - bool isOnline = false; - // 'isCompatibleApi': - // - Indicates whether the model is using the OpenAI-compatible API which user custom. - // - When the property is true, 'isOnline' should also be true. - // - Does not link to the ModelList directly; instead, fetches info from the *-capi.rmodel file and works standalone. - // - Still needs to copy data from gpt4all.ini and *-capi.rmodel to the ModelList in memory while application getting started(as custom .gguf models do). - bool isCompatibleApi = false; QString requiresVersion; QString versionRemoved; qint64 bytesReceived = 0; @@ -206,6 +212,7 @@ public: private: QVariantMap getFields() const; + Provider m_provider; QString m_id; QString m_name; QString m_filename; @@ -369,6 +376,7 @@ public: QHash roleNames() const override { static const QHash roles { + { ProviderRole, "provider" }, { IdRole, "id" }, { NameRole, "name" }, { FilenameRole, "filename" }, @@ -379,8 +387,6 @@ public: { CalcHashRole, "calcHash" }, { InstalledRole, "installed" }, { DefaultRole, "isDefault" }, - { OnlineRole, "isOnline" }, - { CompatibleApiRole, "isCompatibleApi" }, { DescriptionRole, "description" }, { RequiresVersionRole, "requiresVersion" }, { VersionRemovedRole, "versionRemoved" }, @@ -437,7 +443,8 @@ public: 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 that is about to be removed from the model dir + void removeInstalled(const ModelInfo &model); ModelInfo defaultModelInfo() const; void addModel(const QString &id);