Skip to content

Commit

Permalink
modellist: fix a few issues with loading remote models (#2875)
Browse files Browse the repository at this point in the history
Signed-off-by: Jared Van Bortel <[email protected]>
  • Loading branch information
cebtenzzre committed Aug 30, 2024
1 parent 813ccaf commit 55946ff
Show file tree
Hide file tree
Showing 10 changed files with 3,595 additions and 3,508 deletions.
3 changes: 2 additions & 1 deletion gpt4all-chat/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Set the window icon on Linux ([#2880](https://github.com/nomic-ai/gpt4all/pull/2880))
- Corrections to the Romanian translation (by [@SINAPSA-IC](https://github.com/SINAPSA-IC) in [#2890](https://github.com/nomic-ai/gpt4all/pull/2890))
- Fix singular/plural forms of LocalDocs "x Sources" (by [@cosmic-snow](https://github.com/cosmic-snow) in [#2885](https://github.com/nomic-ai/gpt4all/pull/2885))
- Fixed typo in several files. (by [@3Simplex](https://github.com/3Simplex) in [#2916](https://github.com/nomic-ai/gpt4all/pull/2916))
- Fix a typo in Model Settings (by [@3Simplex](https://github.com/3Simplex) in [#2916](https://github.com/nomic-ai/gpt4all/pull/2916))
- Fix the antenna icon tooltip when using the local server ([#2922](https://github.com/nomic-ai/gpt4all/pull/2922))
- Fix a few issues with locating files and handling errors when loading remote models on startup ([#2875](https://github.com/nomic-ai/gpt4all/pull/2875))

## [3.2.1] - 2024-08-13

Expand Down
217 changes: 112 additions & 105 deletions gpt4all-chat/src/modellist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1208,132 +1208,139 @@ bool ModelList::modelExists(const QString &modelFilename) const
return false;
}

void ModelList::updateModelsFromDirectory()
void ModelList::updateOldRemoteModels(const QString &path)
{
const QString exePath = QCoreApplication::applicationDirPath() + QDir::separator();
const QString localPath = MySettings::globalInstance()->modelPath();
QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) {
QFileInfo info = it.nextFileInfo();
QString filename = it.fileName();
if (!filename.startsWith("chatgpt-") || !filename.endsWith(".txt"))
continue;

auto updateOldRemoteModels = [&](const QString& path) {
QDirIterator it(path, 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();
}
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(QIODevice::ReadOnly)) {
qWarning().noquote() << tr("cannot open \"%1\": %2").arg(file.fileName(), file.errorString());
continue;
}

QJsonObject obj;
obj.insert("apiKey", apikey);
obj.insert("modelName", modelname);
QJsonDocument doc(obj);

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();
}
};

auto processDirectory = [&](const QString& path) {
QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) {
it.next();
QFile newfile(u"%1/gpt4all-%2.rmodel"_s.arg(info.dir().path(), modelname));
if (!newfile.open(QIODevice::ReadWrite)) {
qWarning().noquote() << tr("cannot create \"%1\": %2").arg(newfile.fileName(), file.errorString());
continue;
}

QString filename = it.fileName();
if (filename.startsWith("incomplete") || FILENAME_BLACKLIST.contains(filename))
continue;
if (!filename.endsWith(".gguf") && !filename.endsWith(".rmodel"))
continue;
QJsonObject obj {
{ "apiKey", apikey },
{ "modelName", modelname },
};

QVector<QString> modelsById;
{
QMutexLocker locker(&m_mutex);
for (ModelInfo *info : m_models)
if (info->filename() == filename)
modelsById.append(info->id());
}
QTextStream out(&newfile);
out << QJsonDocument(obj).toJson();
newfile.close();

if (modelsById.isEmpty()) {
if (!contains(filename))
addModel(filename);
modelsById.append(filename);
}
file.remove();
}
}

QFileInfo info = it.fileInfo();
void ModelList::processModelDirectory(const QString &path)
{
QDirIterator it(path, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) {
QFileInfo info = it.nextFileInfo();

bool isOnline(filename.endsWith(".rmodel"));
bool isCompatibleApi(filename.endsWith("-capi.rmodel"));
QString filename = it.fileName();
if (filename.startsWith("incomplete") || FILENAME_BLACKLIST.contains(filename))
continue;
if (!filename.endsWith(".gguf") && !filename.endsWith(".rmodel"))
continue;

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);
bool isOnline(filename.endsWith(".rmodel"));
bool isCompatibleApi(filename.endsWith("-capi.rmodel"));

QString name;
QString description;
if (isCompatibleApi) {
QJsonObject obj;
{
QFile file(info.filePath());
if (!file.open(QIODeviceBase::ReadOnly)) {
qWarning().noquote() << tr("cannot open \"%1\": %2").arg(file.fileName(), file.errorString());
continue;
}
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) {
QVector<QPair<int, QVariant>> data {
{ InstalledRole, true },
{ FilenameRole, filename },
{ OnlineRole, isOnline },
{ CompatibleApiRole, isCompatibleApi },
{ DirpathRole, info.dir().absolutePath() + "/" },
{ FilesizeRole, toFileSize(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);
QVector<QString> modelsById;
{
QMutexLocker locker(&m_mutex);
for (ModelInfo *info : m_models)
if (info->filename() == filename)
modelsById.append(info->id());
}

if (modelsById.isEmpty()) {
if (!contains(filename))
addModel(filename);
modelsById.append(filename);
}

for (const QString &id : modelsById) {
QVector<QPair<int, QVariant>> data {
{ InstalledRole, true },
{ FilenameRole, filename },
{ OnlineRole, isOnline },
{ CompatibleApiRole, isCompatibleApi },
{ DirpathRole, info.dir().absolutePath() + "/" },
{ FilesizeRole, toFileSize(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);
}
};
}
}

void ModelList::updateModelsFromDirectory()
{
const QString exePath = QCoreApplication::applicationDirPath() + QDir::separator();
const QString localPath = MySettings::globalInstance()->modelPath();

updateOldRemoteModels(exePath);
processDirectory(exePath);
processModelDirectory(exePath);
if (localPath != exePath) {
updateOldRemoteModels(localPath);
processDirectory(localPath);
processModelDirectory(localPath);
}
}

Expand Down
2 changes: 2 additions & 0 deletions gpt4all-chat/src/modellist.h
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,8 @@ private Q_SLOTS:
void parseModelsJsonFile(const QByteArray &jsonData, bool save);
void parseDiscoveryJsonFile(const QByteArray &jsonData);
QString uniqueModelName(const ModelInfo &model) const;
void updateOldRemoteModels(const QString &path);
void processModelDirectory(const QString &path);

private:
mutable QMutex m_mutex;
Expand Down
Loading

0 comments on commit 55946ff

Please sign in to comment.