This commit is contained in:
Jared Van Bortel 2024-08-23 11:50:36 -04:00
parent f6c8c7cb90
commit c13b33fb4d
8 changed files with 256 additions and 157 deletions

View File

@ -109,7 +109,7 @@ endif()
qt_add_executable(chat qt_add_executable(chat
main.cpp main.cpp
chat.h chat.cpp chat.h chat.cpp
llmodel.h llmodel.h llmodel.cpp
llamacpp_model.h llamacpp_model.cpp llamacpp_model.h llamacpp_model.cpp
chatmodel.h chatlistmodel.h chatlistmodel.cpp chatmodel.h chatlistmodel.h chatlistmodel.cpp
chatapi.h chatapi.cpp chatapi.h chatapi.cpp

View File

@ -94,7 +94,7 @@ public:
Q_INVOKABLE void reloadModel(); Q_INVOKABLE void reloadModel();
Q_INVOKABLE void forceUnloadModel(); Q_INVOKABLE void forceUnloadModel();
Q_INVOKABLE void forceReloadModel(); Q_INVOKABLE void forceReloadModel();
Q_INVOKABLE void trySwitchContextOfLoadedModel(); void trySwitchContextOfLoadedModel();
void unloadAndDeleteLater(); void unloadAndDeleteLater();
void markForDeletion(); void markForDeletion();

View File

@ -263,16 +263,17 @@ void Download::installModel(const QString &modelFile, const QString &apiKey)
QFile file(filePath); QFile file(filePath);
if (file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Text)) { if (file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Text)) {
QJsonObject obj;
QString modelName(modelFile); QString modelName(modelFile);
modelName.remove(0, 8); // strip "gpt4all-" prefix modelName.remove(0, 8); // strip "gpt4all-" prefix
modelName.chop(7); // strip ".rmodel" extension modelName.chop(7); // strip ".rmodel" extension
obj.insert("apiKey", apiKey); QJsonObject obj {
obj.insert("modelName", modelName); { "type", ... },
QJsonDocument doc(obj); { "apiKey", apiKey },
{ "modelName", modelName },
};
QTextStream stream(&file); QTextStream stream(&file);
stream << doc.toJson(); stream << QJsonDocument(doc).toJson();
file.close(); file.close();
ModelList::globalInstance()->updateModelsFromDirectory(); ModelList::globalInstance()->updateModelsFromDirectory();
emit toastMessage(tr("Model \"%1\" is installed successfully.").arg(modelName)); 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; QString filePath = MySettings::globalInstance()->modelPath() + modelFile;
QFile file(filePath); QFile file(filePath);
if (file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Text)) { if (file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Text)) {
QJsonObject obj; QJsonObject obj {
obj.insert("apiKey", apiKey); { "type", "openai-generic" },
obj.insert("modelName", modelName); { "apiKey", apiKey },
obj.insert("baseUrl", apiBaseUrl.toString()); { "modelName", modelName },
QJsonDocument doc(obj); { "baseUrl", apiBaseUrl.toString() },
};
QTextStream stream(&file); QTextStream stream(&file);
stream << doc.toJson(); stream << QJsonDocument(obj).toJson();
file.close(); file.close();
ModelList::globalInstance()->updateModelsFromDirectory(); ModelList::globalInstance()->updateModelsFromDirectory();
emit toastMessage(tr("Model \"%1 (%2)\" is installed successfully.").arg(modelName, baseUrl)); emit toastMessage(tr("Model \"%1 (%2)\" is installed successfully.").arg(modelName, baseUrl));
@ -336,20 +338,26 @@ void Download::removeModel(const QString &modelFile)
incompleteFile.remove(); incompleteFile.remove();
} }
bool shouldRemoveInstalled = false; bool removedFromList = false;
QFile file(filePath); QFile file(filePath);
if (file.exists()) { if (file.exists()) {
const ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile); const ModelInfo info = ModelList::globalInstance()->modelInfoByFilename(modelFile);
MySettings::globalInstance()->eraseModel(info); MySettings::globalInstance()->eraseModel(info);
shouldRemoveInstalled = info.installed && !info.isClone() && (info.isDiscovered() || info.isCompatibleApi || info.description() == "" /*indicates sideloaded*/); if (
if (shouldRemoveInstalled) info.installed && !info.isClone() && (
info.isDiscovered() || info.description() == "" /*indicates sideloaded*/
|| info.provider == ModelInfo::Provider::OpenAIGeneric
)
) {
ModelList::globalInstance()->removeInstalled(info); ModelList::globalInstance()->removeInstalled(info);
removedFromList = true;
}
Network::globalInstance()->trackEvent("remove_model", { {"model", modelFile} }); Network::globalInstance()->trackEvent("remove_model", { {"model", modelFile} });
file.remove(); file.remove();
emit toastMessage(tr("Model \"%1\" is removed.").arg(info.name())); emit toastMessage(tr("Model \"%1\" is removed.").arg(info.name()));
} }
if (!shouldRemoveInstalled) { if (!removedFromList) {
QVector<QPair<int, QVariant>> data { QVector<QPair<int, QVariant>> data {
{ ModelList::InstalledRole, false }, { ModelList::InstalledRole, false },
{ ModelList::BytesReceivedRole, 0 }, { ModelList::BytesReceivedRole, 0 },

View File

@ -579,34 +579,6 @@ bool LlamaCppModel::isModelLoaded() const
return m_llModelInfo.model && m_llModelInfo.model->isModelLoaded(); 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 // FIXME(jared): we don't actually have to re-decode the prompt to generate a new response
void LlamaCppModel::regenerateResponse() void LlamaCppModel::regenerateResponse()
{ {

34
gpt4all-chat/llmodel.cpp Normal file
View File

@ -0,0 +1,34 @@
#include "llmodel.h"
#include <algorithm>
#include <cctype>
#include <string>
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);
}

View File

@ -73,3 +73,6 @@ Q_SIGNALS:
void databaseResultsChanged(const QList<ResultInfo> &results); void databaseResultsChanged(const QList<ResultInfo> &results);
void modelInfoChanged(const ModelInfo &modelInfo); void modelInfoChanged(const ModelInfo &modelInfo);
}; };
std::string remove_leading_whitespace(const std::string &input);
std::string trim_whitespace(const std::string &input);

View File

@ -36,6 +36,8 @@
#include <algorithm> #include <algorithm>
#include <cstddef> #include <cstddef>
#include <iterator> #include <iterator>
#include <optional>
#include <stdexcept>
#include <string> #include <string>
#include <utility> #include <utility>
@ -43,8 +45,33 @@ using namespace Qt::Literals::StringLiterals;
//#define USE_LOCAL_MODELSJSON //#define USE_LOCAL_MODELSJSON
static const QStringList FILENAME_BLACKLIST { u"gpt4all-nomic-embed-text-v1.rmodel"_s }; 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<QString, ModelInfo::Provider> 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<QString, ModelInfo::Provider> 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 QString ModelInfo::id() const
{ {
return m_id; return m_id;
@ -694,6 +721,8 @@ int ModelList::rowCount(const QModelIndex &parent) const
QVariant ModelList::dataInternal(const ModelInfo *info, int role) const QVariant ModelList::dataInternal(const ModelInfo *info, int role) const
{ {
switch (role) { switch (role) {
case ProviderRole:
return info->provider();
case IdRole: case IdRole:
return info->id(); return info->id();
case NameRole: case NameRole:
@ -714,10 +743,6 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const
return info->installed; return info->installed;
case DefaultRole: case DefaultRole:
return info->isDefault; return info->isDefault;
case OnlineRole:
return info->isOnline;
case CompatibleApiRole:
return info->isCompatibleApi;
case DescriptionRole: case DescriptionRole:
return info->description(); return info->description();
case RequiresVersionRole: case RequiresVersionRole:
@ -846,6 +871,8 @@ void ModelList::updateData(const QString &id, const QVector<QPair<int, QVariant>
const int role = d.first; const int role = d.first;
const QVariant value = d.second; const QVariant value = d.second;
switch (role) { switch (role) {
case ProviderRole:
info->m_provider = value.value<ModelInfo::Provider>();
case IdRole: case IdRole:
{ {
if (info->id() != value.toString()) { if (info->id() != value.toString()) {
@ -865,17 +892,13 @@ void ModelList::updateData(const QString &id, const QVector<QPair<int, QVariant>
case HashRole: case HashRole:
info->hash = value.toByteArray(); break; info->hash = value.toByteArray(); break;
case HashAlgorithmRole: case HashAlgorithmRole:
info->hashAlgorithm = static_cast<ModelInfo::HashAlgorithm>(value.toInt()); break; info->hashAlgorithm = value.value<ModelInfo::HashAlgorithm>(); break;
case CalcHashRole: case CalcHashRole:
info->calcHash = value.toBool(); break; info->calcHash = value.toBool(); break;
case InstalledRole: case InstalledRole:
info->installed = value.toBool(); break; info->installed = value.toBool(); break;
case DefaultRole: case DefaultRole:
info->isDefault = value.toBool(); break; info->isDefault = value.toBool(); break;
case OnlineRole:
info->isOnline = value.toBool(); break;
case CompatibleApiRole:
info->isCompatibleApi = value.toBool(); break;
case DescriptionRole: case DescriptionRole:
info->setDescription(value.toString()); break; info->setDescription(value.toString()); break;
case RequiresVersionRole: case RequiresVersionRole:
@ -1090,13 +1113,12 @@ QString ModelList::clone(const ModelInfo &model)
addModel(id); addModel(id);
QVector<QPair<int, QVariant>> data { QVector<QPair<int, QVariant>> data {
{ ModelList::ProviderRole, model.provider },
{ ModelList::InstalledRole, model.installed }, { ModelList::InstalledRole, model.installed },
{ ModelList::IsCloneRole, true }, { ModelList::IsCloneRole, true },
{ ModelList::NameRole, uniqueModelName(model) }, { ModelList::NameRole, uniqueModelName(model) },
{ ModelList::FilenameRole, model.filename() }, { ModelList::FilenameRole, model.filename() },
{ ModelList::DirpathRole, model.dirpath }, { ModelList::DirpathRole, model.dirpath },
{ ModelList::OnlineRole, model.isOnline },
{ ModelList::CompatibleApiRole, model.isCompatibleApi },
{ ModelList::IsEmbeddingModelRole, model.isEmbeddingModel }, { ModelList::IsEmbeddingModelRole, model.isEmbeddingModel },
{ ModelList::TemperatureRole, model.temperature() }, { ModelList::TemperatureRole, model.temperature() },
{ ModelList::TopPRole, model.topP() }, { ModelList::TopPRole, model.topP() },
@ -1129,9 +1151,9 @@ void ModelList::removeClone(const ModelInfo &model)
void ModelList::removeInstalled(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.installed);
Q_ASSERT(!model.isClone()); Q_ASSERT(!model.isClone());
Q_ASSERT(model.isDiscovered() || model.isCompatibleApi || model.description() == "" /*indicates sideloaded*/);
removeInternal(model); removeInternal(model);
emit layoutChanged(); emit layoutChanged();
} }
@ -1212,53 +1234,144 @@ bool ModelList::modelExists(const QString &modelFilename) const
static void updateOldRemoteModels(const QString &path) static void updateOldRemoteModels(const QString &path)
{ {
QDirIterator it(path, QDirIterator::Subdirectories); QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) { while (it.hasNext()) {
it.next(); QFileInfo info = it.nextFileInfo();
if (!it.fileInfo().isDir()) { QString filename = info.fileName();
QString filename = it.fileName(); if (!filename.startsWith("chatgpt-") || !filename.endsWith(".txt"))
if (filename.startsWith("chatgpt-") && filename.endsWith(".txt")) { continue;
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();
}
QJsonObject obj; QString apikey;
obj.insert("apiKey", apikey); QString modelname(filename);
obj.insert("modelName", modelname); modelname.chop(4); // strip ".txt" extension
QJsonDocument doc(obj); 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); QTextStream in(&file);
if (newfile.open(QIODevice::ReadWrite)) { apikey = in.readAll();
QTextStream out(&newfile); file.close();
out << doc.toJson(); }
newfile.close();
} QJsonObject obj {
file.remove(); { "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<QPair<int, QVariant>> &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<ModelInfo::Provider> 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("<strong>OpenAI-Compatible API Model</strong><br>"
"<ul><li>API Key: %1</li>"
"<li>Base URL: %2</li>"
"<li>Model Name: %3</li></ul>")
.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<QPair<int, QVariant>> {
{ 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<QPair<int, QVariant>> {
{ ProviderRole, provider },
};
return true;
bad_data:
qWarning(tr("bad rmodel \"%s\": unrecognized data"), info.filePath());
return false;
} }
void ModelList::processModelDirectory(const QString &path) void ModelList::processModelDirectory(const QString &path)
{ {
QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories); QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) { 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)) if (filename.startsWith("incomplete") || FILENAME_BLACKLIST.contains(filename))
continue; continue;
if (!filename.endsWith(".gguf") && !filename.endsWith(".rmodel")) if (!filename.endsWith(".gguf") && !filename.endsWith(".rmodel"))
continue; continue;
QVector<QPair<int, QVariant>> props;
if (!filename.endswith(".rmodel")) {
props.emplaceBack(ProviderRole, ModelInfo::Provider::LlamaCpp);
} else if (!parseRemoteModel(props, info))
continue;
QVector<QString> modelsById; QVector<QString> modelsById;
{ {
QMutexLocker locker(&m_mutex); QMutexLocker locker(&m_mutex);
@ -1273,56 +1386,15 @@ void ModelList::processModelDirectory(const QString &path)
modelsById.append(filename); 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("<strong>OpenAI-Compatible API Model</strong><br>"
"<ul><li>API Key: %1</li>"
"<li>Base URL: %2</li>"
"<li>Model Name: %3</li></ul>")
.arg(apiKey, baseUrl, modelName);
}
}
for (const QString &id : modelsById) { for (const QString &id : modelsById) {
QVector<QPair<int, QVariant>> data { props << QVector<QPair<int, QVariant>> {
{ InstalledRole, true }, { ProviderRole, provider },
{ FilenameRole, filename }, { InstalledRole, true },
{ OnlineRole, isOnline }, { FilenameRole, filename },
{ CompatibleApiRole, isCompatibleApi }, { DirpathRole, info.dir().absolutePath() + "/" },
{ DirpathRole, info.dir().absolutePath() + "/" }, { FilesizeRole, info.size() },
{ FilesizeRole, info.size() },
}; };
if (isCompatibleApi) { updateData(id, props);
// 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);
} }
} }
} }
@ -1571,6 +1643,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
if (!contains(id)) if (!contains(id))
addModel(id); addModel(id);
QVector<QPair<int, QVariant>> data { QVector<QPair<int, QVariant>> data {
{ ModelList::ProviderRole, BUILTIN_RMODEL_FILENAMES. },
{ ModelList::NameRole, modelName }, { ModelList::NameRole, modelName },
{ ModelList::FilenameRole, modelFilename }, { ModelList::FilenameRole, modelFilename },
{ ModelList::FilesizeRole, 0 }, { ModelList::FilesizeRole, 0 },
@ -1599,6 +1672,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
if (!contains(id)) if (!contains(id))
addModel(id); addModel(id);
QVector<QPair<int, QVariant>> data { QVector<QPair<int, QVariant>> data {
{ ModelList::ProviderRole, getBuiltinRmodelFilename(modelFilename) },
{ ModelList::NameRole, modelName }, { ModelList::NameRole, modelName },
{ ModelList::FilenameRole, modelFilename }, { ModelList::FilenameRole, modelFilename },
{ ModelList::FilesizeRole, 0 }, { ModelList::FilesizeRole, 0 },
@ -1630,6 +1704,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
if (!contains(id)) if (!contains(id))
addModel(id); addModel(id);
QVector<QPair<int, QVariant>> data { QVector<QPair<int, QVariant>> data {
{ ModelList::ProviderRole, getBuiltinRmodelFilename(modelFilename) },
{ ModelList::NameRole, modelName }, { ModelList::NameRole, modelName },
{ ModelList::FilenameRole, modelFilename }, { ModelList::FilenameRole, modelFilename },
{ ModelList::FilesizeRole, 0 }, { ModelList::FilesizeRole, 0 },
@ -1655,6 +1730,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
if (!contains(id)) if (!contains(id))
addModel(id); addModel(id);
QVector<QPair<int, QVariant>> data { QVector<QPair<int, QVariant>> data {
{ ModelList::ProviderRole, getBuiltinRmodelFilename(modelFilename) },
{ ModelList::NameRole, modelName }, { ModelList::NameRole, modelName },
{ ModelList::FilenameRole, modelFilename }, { ModelList::FilenameRole, modelFilename },
{ ModelList::FilesizeRole, 0 }, { ModelList::FilesizeRole, 0 },
@ -1681,10 +1757,10 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
if (!contains(id)) if (!contains(id))
addModel(id); addModel(id);
QVector<QPair<int, QVariant>> data { QVector<QPair<int, QVariant>> data {
{ ModelList::ProviderRole, getBuiltinRmodelFilename(modelFilename) },
{ ModelList::NameRole, modelName }, { ModelList::NameRole, modelName },
{ ModelList::FilenameRole, modelFilename }, { ModelList::FilenameRole, modelFilename },
{ ModelList::FilesizeRole, 0 }, { ModelList::FilesizeRole, 0 },
{ ModelList::OnlineRole, true },
{ ModelList::DescriptionRole, { ModelList::DescriptionRole,
tr("<strong>Mistral Medium model</strong><br> %1").arg(mistralDesc) }, tr("<strong>Mistral Medium model</strong><br> %1").arg(mistralDesc) },
{ ModelList::RequiresVersionRole, "2.7.4" }, { ModelList::RequiresVersionRole, "2.7.4" },
@ -1710,10 +1786,9 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
if (!contains(id)) if (!contains(id))
addModel(id); addModel(id);
QVector<QPair<int, QVariant>> data { QVector<QPair<int, QVariant>> data {
{ ModelList::ProviderRole, ModelInfo::Provider::OpenAIGeneric },
{ ModelList::NameRole, modelName }, { ModelList::NameRole, modelName },
{ ModelList::FilesizeRole, 0 }, { ModelList::FilesizeRole, 0 },
{ ModelList::OnlineRole, true },
{ ModelList::CompatibleApiRole, true },
{ ModelList::DescriptionRole, { ModelList::DescriptionRole,
tr("<strong>Connect to OpenAI-compatible API server</strong><br> %1").arg(compatibleDesc) }, tr("<strong>Connect to OpenAI-compatible API server</strong><br> %1").arg(compatibleDesc) },
{ ModelList::RequiresVersionRole, "2.7.4" }, { ModelList::RequiresVersionRole, "2.7.4" },

View File

@ -25,6 +25,7 @@ using namespace Qt::Literals::StringLiterals;
struct ModelInfo { struct ModelInfo {
Q_GADGET Q_GADGET
Q_PROPERTY(Provider provider READ provider)
Q_PROPERTY(QString id READ id WRITE setId) Q_PROPERTY(QString id READ id WRITE setId)
Q_PROPERTY(QString name READ name WRITE setName) Q_PROPERTY(QString name READ name WRITE setName)
Q_PROPERTY(QString filename READ filename WRITE setFilename) Q_PROPERTY(QString filename READ filename WRITE setFilename)
@ -35,8 +36,7 @@ struct ModelInfo {
Q_PROPERTY(bool calcHash MEMBER calcHash) Q_PROPERTY(bool calcHash MEMBER calcHash)
Q_PROPERTY(bool installed MEMBER installed) Q_PROPERTY(bool installed MEMBER installed)
Q_PROPERTY(bool isDefault MEMBER isDefault) Q_PROPERTY(bool isDefault MEMBER isDefault)
Q_PROPERTY(bool isOnline MEMBER isOnline) Q_PROPERTY(bool isOnline READ isOnline)
Q_PROPERTY(bool isCompatibleApi MEMBER isCompatibleApi)
Q_PROPERTY(QString description READ description WRITE setDescription) Q_PROPERTY(QString description READ description WRITE setDescription)
Q_PROPERTY(QString requiresVersion MEMBER requiresVersion) Q_PROPERTY(QString requiresVersion MEMBER requiresVersion)
Q_PROPERTY(QString versionRemoved MEMBER versionRemoved) Q_PROPERTY(QString versionRemoved MEMBER versionRemoved)
@ -77,10 +77,27 @@ struct ModelInfo {
Q_PROPERTY(QDateTime recency READ recency WRITE setRecency) Q_PROPERTY(QDateTime recency READ recency WRITE setRecency)
public: 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, Md5,
Sha256 Sha256
}; };
Q_ENUM(HashAlgorithm)
Provider provider() const { return m_provider; }
bool isOnline() const { return m_provider != Provider::LlamaCpp; }
QString id() const; QString id() const;
void setId(const QString &id); void setId(const QString &id);
@ -109,8 +126,8 @@ public:
QString description() const; QString description() const;
void setDescription(const QString &d); void setDescription(const QString &d);
/* For built-in OpenAI-compatible models (isOnline && !isCompatibleApi), this is the full completions endpoint URL. /* For built-in OpenAI-compatible models, this is the full completions endpoint URL.
* For custom OpenAI-compatible models (isCompatibleApi), this is not set. * For custom OpenAI-compatible models (Provider::OpenAIGeneric), this is not set.
* For discovered models (isDiscovered), this is the resolved URL of the GGUF file. */ * For discovered models (isDiscovered), this is the resolved URL of the GGUF file. */
QString url() const; QString url() const;
void setUrl(const QString &u); void setUrl(const QString &u);
@ -142,17 +159,6 @@ public:
bool calcHash = false; bool calcHash = false;
bool installed = false; bool installed = false;
bool isDefault = 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 requiresVersion;
QString versionRemoved; QString versionRemoved;
qint64 bytesReceived = 0; qint64 bytesReceived = 0;
@ -206,6 +212,7 @@ public:
private: private:
QVariantMap getFields() const; QVariantMap getFields() const;
Provider m_provider;
QString m_id; QString m_id;
QString m_name; QString m_name;
QString m_filename; QString m_filename;
@ -369,6 +376,7 @@ public:
QHash<int, QByteArray> roleNames() const override QHash<int, QByteArray> roleNames() const override
{ {
static const QHash<int, QByteArray> roles { static const QHash<int, QByteArray> roles {
{ ProviderRole, "provider" },
{ IdRole, "id" }, { IdRole, "id" },
{ NameRole, "name" }, { NameRole, "name" },
{ FilenameRole, "filename" }, { FilenameRole, "filename" },
@ -379,8 +387,6 @@ public:
{ CalcHashRole, "calcHash" }, { CalcHashRole, "calcHash" },
{ InstalledRole, "installed" }, { InstalledRole, "installed" },
{ DefaultRole, "isDefault" }, { DefaultRole, "isDefault" },
{ OnlineRole, "isOnline" },
{ CompatibleApiRole, "isCompatibleApi" },
{ DescriptionRole, "description" }, { DescriptionRole, "description" },
{ RequiresVersionRole, "requiresVersion" }, { RequiresVersionRole, "requiresVersion" },
{ VersionRemovedRole, "versionRemoved" }, { VersionRemovedRole, "versionRemoved" },
@ -437,7 +443,8 @@ public:
Q_INVOKABLE bool isUniqueName(const QString &name) const; Q_INVOKABLE bool isUniqueName(const QString &name) const;
Q_INVOKABLE QString clone(const ModelInfo &model); Q_INVOKABLE QString clone(const ModelInfo &model);
Q_INVOKABLE void removeClone(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; ModelInfo defaultModelInfo() const;
void addModel(const QString &id); void addModel(const QString &id);