diff --git a/gpt4all-chat/CMakeLists.txt b/gpt4all-chat/CMakeLists.txt
index 91ce25fd..a1b64692 100644
--- a/gpt4all-chat/CMakeLists.txt
+++ b/gpt4all-chat/CMakeLists.txt
@@ -266,6 +266,7 @@ qt_add_qml_module(chat
qml/AddModelView.qml
qml/AddGPT4AllModelView.qml
qml/AddHFModelView.qml
+ qml/AddRemoteModelView.qml
qml/ApplicationSettings.qml
qml/ChatDrawer.qml
qml/ChatCollapsibleItem.qml
@@ -314,6 +315,7 @@ qt_add_qml_module(chat
qml/MyTextField.qml
qml/MyToolButton.qml
qml/MyWelcomeButton.qml
+ qml/RemoteModelCard.qml
RESOURCES
icons/antenna_1.svg
icons/antenna_2.svg
@@ -344,6 +346,7 @@ qt_add_qml_module(chat
icons/gpt4all-48.png
icons/gpt4all.svg
icons/gpt4all_transparent.svg
+ icons/groq.svg
icons/home.svg
icons/image.svg
icons/info.svg
@@ -351,12 +354,14 @@ qt_add_qml_module(chat
icons/left_panel_open.svg
icons/local-docs.svg
icons/models.svg
+ icons/mistral.svg
icons/network.svg
icons/nomic_logo.svg
icons/notes.svg
icons/paperclip.svg
icons/plus.svg
icons/plus_circle.svg
+ icons/openai.svg
icons/recycle.svg
icons/regenerate.svg
icons/search.svg
diff --git a/gpt4all-chat/icons/groq.svg b/gpt4all-chat/icons/groq.svg
new file mode 100644
index 00000000..1f895558
--- /dev/null
+++ b/gpt4all-chat/icons/groq.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/gpt4all-chat/icons/mistral.svg b/gpt4all-chat/icons/mistral.svg
new file mode 100644
index 00000000..0775fe7e
--- /dev/null
+++ b/gpt4all-chat/icons/mistral.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/gpt4all-chat/icons/openai.svg b/gpt4all-chat/icons/openai.svg
new file mode 100644
index 00000000..3b4eff96
--- /dev/null
+++ b/gpt4all-chat/icons/openai.svg
@@ -0,0 +1,2 @@
+
+OpenAI icon
\ No newline at end of file
diff --git a/gpt4all-chat/qml/AddGPT4AllModelView.qml b/gpt4all-chat/qml/AddGPT4AllModelView.qml
index 2a1832af..55154e6f 100644
--- a/gpt4all-chat/qml/AddGPT4AllModelView.qml
+++ b/gpt4all-chat/qml/AddGPT4AllModelView.qml
@@ -204,7 +204,7 @@ ColumnLayout {
Layout.minimumWidth: 200
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
- visible: !isOnline && !installed && !calcHash && downloadError === ""
+ visible: !installed && !calcHash && downloadError === ""
Accessible.description: qsTr("Stop/restart/start the download")
onClicked: {
if (!isDownloading) {
@@ -230,52 +230,6 @@ ColumnLayout {
}
}
- MySettingsButton {
- id: installButton
- visible: !installed && isOnline
- Layout.topMargin: 20
- Layout.leftMargin: 20
- Layout.minimumWidth: 200
- Layout.fillWidth: true
- Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
- text: qsTr("Install")
- font.pixelSize: theme.fontSizeLarge
- onClicked: {
- var apiKeyText = apiKey.text.trim(),
- baseUrlText = baseUrl.text.trim(),
- modelNameText = modelName.text.trim();
-
- var apiKeyOk = apiKeyText !== "",
- baseUrlOk = !isCompatibleApi || baseUrlText !== "",
- modelNameOk = !isCompatibleApi || modelNameText !== "";
-
- if (!apiKeyOk)
- apiKey.showError();
- if (!baseUrlOk)
- baseUrl.showError();
- if (!modelNameOk)
- modelName.showError();
-
- if (!apiKeyOk || !baseUrlOk || !modelNameOk)
- return;
-
- if (!isCompatibleApi)
- Download.installModel(
- filename,
- apiKeyText,
- );
- else
- Download.installCompatibleModel(
- modelNameText,
- apiKeyText,
- baseUrlText,
- );
- }
- Accessible.role: Accessible.Button
- Accessible.name: qsTr("Install")
- Accessible.description: qsTr("Install online model")
- }
-
ColumnLayout {
spacing: 0
Label {
@@ -390,69 +344,6 @@ ColumnLayout {
Accessible.description: qsTr("Displayed when the file hash is being calculated")
}
}
-
- MyTextField {
- id: apiKey
- visible: !installed && isOnline
- Layout.topMargin: 20
- Layout.leftMargin: 20
- Layout.minimumWidth: 200
- Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
- wrapMode: Text.WrapAnywhere
- function showError() {
- messageToast.show(qsTr("ERROR: $API_KEY is empty."));
- apiKey.placeholderTextColor = theme.textErrorColor;
- }
- onTextChanged: {
- apiKey.placeholderTextColor = theme.mutedTextColor;
- }
- placeholderText: qsTr("enter $API_KEY")
- Accessible.role: Accessible.EditableText
- Accessible.name: placeholderText
- Accessible.description: qsTr("Whether the file hash is being calculated")
- }
-
- MyTextField {
- id: baseUrl
- visible: !installed && isOnline && isCompatibleApi
- Layout.topMargin: 20
- Layout.leftMargin: 20
- Layout.minimumWidth: 200
- Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
- wrapMode: Text.WrapAnywhere
- function showError() {
- messageToast.show(qsTr("ERROR: $BASE_URL is empty."));
- baseUrl.placeholderTextColor = theme.textErrorColor;
- }
- onTextChanged: {
- baseUrl.placeholderTextColor = theme.mutedTextColor;
- }
- placeholderText: qsTr("enter $BASE_URL")
- Accessible.role: Accessible.EditableText
- Accessible.name: placeholderText
- Accessible.description: qsTr("Whether the file hash is being calculated")
- }
-
- MyTextField {
- id: modelName
- visible: !installed && isOnline && isCompatibleApi
- Layout.topMargin: 20
- Layout.leftMargin: 20
- Layout.minimumWidth: 200
- Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
- wrapMode: Text.WrapAnywhere
- function showError() {
- messageToast.show(qsTr("ERROR: $MODEL_NAME is empty."))
- modelName.placeholderTextColor = theme.textErrorColor;
- }
- onTextChanged: {
- modelName.placeholderTextColor = theme.mutedTextColor;
- }
- placeholderText: qsTr("enter $MODEL_NAME")
- Accessible.role: Accessible.EditableText
- Accessible.name: placeholderText
- Accessible.description: qsTr("Whether the file hash is being calculated")
- }
}
}
}
diff --git a/gpt4all-chat/qml/AddModelView.qml b/gpt4all-chat/qml/AddModelView.qml
index 223d7037..716492d2 100644
--- a/gpt4all-chat/qml/AddModelView.qml
+++ b/gpt4all-chat/qml/AddModelView.qml
@@ -89,6 +89,13 @@ Rectangle {
gpt4AllModelView.show();
}
}
+ MyTabButton {
+ text: qsTr("Remote Providers")
+ isSelected: removeModelView.isShown()
+ onPressed: {
+ removeModelView.show();
+ }
+ }
MyTabButton {
text: qsTr("HuggingFace")
isSelected: huggingfaceModelView.isShown()
@@ -112,7 +119,20 @@ Rectangle {
stackLayout.currentIndex = 0;
}
function isShown() {
- return stackLayout.currentIndex === 0
+ return stackLayout.currentIndex === 0;
+ }
+ }
+
+ AddRemoteModelView {
+ id: removeModelView
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ function show() {
+ stackLayout.currentIndex = 1;
+ }
+ function isShown() {
+ return stackLayout.currentIndex === 1;
}
}
@@ -126,10 +146,10 @@ Rectangle {
anchors.fill: parent
function show() {
- stackLayout.currentIndex = 1;
+ stackLayout.currentIndex = 2;
}
function isShown() {
- return stackLayout.currentIndex === 1
+ return stackLayout.currentIndex === 2;
}
}
}
diff --git a/gpt4all-chat/qml/AddRemoteModelView.qml b/gpt4all-chat/qml/AddRemoteModelView.qml
new file mode 100644
index 00000000..20c71d6e
--- /dev/null
+++ b/gpt4all-chat/qml/AddRemoteModelView.qml
@@ -0,0 +1,86 @@
+import QtCore
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Controls.Basic
+import QtQuick.Layouts
+import QtQuick.Dialogs
+import Qt.labs.folderlistmodel
+import Qt5Compat.GraphicalEffects
+
+import llm
+import chatlistmodel
+import download
+import modellist
+import network
+import gpt4all
+import mysettings
+import localdocs
+
+ColumnLayout {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignTop
+ spacing: 5
+
+ Label {
+ Layout.topMargin: 0
+ Layout.bottomMargin: 25
+ Layout.rightMargin: 150 * theme.fontScale
+ Layout.alignment: Qt.AlignTop
+ Layout.fillWidth: true
+ verticalAlignment: Text.AlignTop
+ text: qsTr("Various remote model providers that use network resources for inference.")
+ font.pixelSize: theme.fontSizeLarger
+ color: theme.textColor
+ wrapMode: Text.WordWrap
+ }
+
+ ScrollView {
+ id: scrollView
+ ScrollBar.vertical.policy: ScrollBar.AsNeeded
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ clip: true
+ GridLayout {
+ rows: 2
+ columns: 3
+ rowSpacing: 20
+ columnSpacing: 20
+ RemoteModelCard {
+ Layout.preferredWidth: 600
+ Layout.minimumHeight: implicitHeight
+ Layout.alignment: Qt.AlignTop | Qt.AlignLeft
+ providerBaseUrl: "https://api.groq.com/openai/v1/"
+ providerName: qsTr("Groq")
+ providerImage: "qrc:/gpt4all/icons/groq.svg"
+ providerDesc: qsTr('Groq offers a high-performance AI inference engine designed for low-latency and efficient processing. Optimized for real-time applications, Groq’s technology is ideal for users who need fast responses from open large language models and other AI workloads. Get your API key: https://groq.com/ ')
+ }
+ RemoteModelCard {
+ Layout.preferredWidth: 600
+ Layout.minimumHeight: implicitHeight
+ Layout.alignment: Qt.AlignTop | Qt.AlignLeft
+ providerBaseUrl: "https://api.openai.com/v1/"
+ providerName: qsTr("OpenAI")
+ providerImage: "qrc:/gpt4all/icons/openai.svg"
+ providerDesc: qsTr('OpenAI provides access to advanced AI models, including GPT-4 supporting a wide range of applications, from conversational AI to content generation and code completion. Get your API key: https://openai.com/ ')
+ }
+ RemoteModelCard {
+ Layout.preferredWidth: 600
+ Layout.minimumHeight: implicitHeight
+ Layout.alignment: Qt.AlignTop | Qt.AlignLeft
+ providerBaseUrl: "https://api.mistral.ai/v1/"
+ providerName: qsTr("Mistral")
+ providerImage: "qrc:/gpt4all/icons/mistral.svg"
+ providerDesc: qsTr('Mistral AI specializes in efficient, open-weight language models optimized for various natural language processing tasks. Their models are designed for flexibility and performance, making them a solid option for applications requiring scalable AI solutions. Get your API key: https://mistral.ai/ ')
+ }
+ RemoteModelCard {
+ Layout.preferredWidth: 600
+ Layout.minimumHeight: implicitHeight
+ Layout.alignment: Qt.AlignTop | Qt.AlignLeft
+ providerIsCustom: true
+ providerName: qsTr("Custom")
+ providerImage: "qrc:/gpt4all/icons/antenna_3.svg"
+ providerDesc: qsTr("The custom provider option allows users to connect their own OpenAI-compatible AI models or third-party inference services. This is useful for organizations with proprietary models or those leveraging niche AI providers not listed here.")
+ }
+ }
+ }
+}
diff --git a/gpt4all-chat/qml/RemoteModelCard.qml b/gpt4all-chat/qml/RemoteModelCard.qml
new file mode 100644
index 00000000..a12811ed
--- /dev/null
+++ b/gpt4all-chat/qml/RemoteModelCard.qml
@@ -0,0 +1,202 @@
+import QtCore
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Controls.Basic
+import QtQuick.Layouts
+import QtQuick.Dialogs
+import Qt.labs.folderlistmodel
+import Qt5Compat.GraphicalEffects
+
+import llm
+import chatlistmodel
+import download
+import modellist
+import network
+import gpt4all
+import mysettings
+import localdocs
+
+
+Rectangle {
+ property alias providerName: providerNameLabel.text
+ property alias providerImage: myimage.source
+ property alias providerDesc: providerDescLabel.helpText
+ property string providerBaseUrl: ""
+ property bool providerIsCustom: false
+
+ color: theme.conversationBackground
+ radius: 10
+ border.width: 1
+ border.color: theme.controlBorder
+ implicitHeight: childrenRect.height + 40
+
+ ColumnLayout {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: 20
+ spacing: 30
+ RowLayout {
+ Layout.alignment: Qt.AlignTop
+ spacing: 10
+ Rectangle {
+ id: rec
+ color: "transparent"
+ Layout.preferredWidth: 48
+ Layout.preferredHeight: 48
+ Layout.alignment: Qt.AlignLeft
+ Image {
+ id: myimage
+ anchors.centerIn: parent
+ sourceSize.width: rec.width
+ sourceSize.height: rec.height
+ mipmap: true
+ visible: false
+ fillMode: Image.PreserveAspectFit
+
+ }
+
+ ColorOverlay {
+ anchors.fill: myimage
+ source: myimage
+ }
+ }
+
+ Label {
+ id: providerNameLabel
+ color: theme.textColor
+ font.pixelSize: theme.fontSizeBanner
+ }
+ }
+
+ MySettingsLabel {
+ id: providerDescLabel
+ onLinkActivated: function(link) { Qt.openUrlExternally(link) }
+ }
+
+ ColumnLayout {
+ MySettingsLabel {
+ text: qsTr("API Key")
+ font.bold: true
+ font.pixelSize: theme.fontSizeLarge
+ color: theme.settingsTitleTextColor
+ }
+
+ MyTextField {
+ id: apiKeyField
+ Layout.fillWidth: true
+ wrapMode: Text.WrapAnywhere
+ function showError() {
+ messageToast.show(qsTr("ERROR: $API_KEY is empty."));
+ apiKeyField.placeholderTextColor = theme.textErrorColor;
+ }
+ onTextChanged: {
+ apiKeyField.placeholderTextColor = theme.mutedTextColor;
+ if (!providerIsCustom) {
+ myModelList.model = ModelList.remoteModelList(apiKeyField.text, providerBaseUrl);
+ myModelList.currentIndex = -1;
+ }
+ }
+ placeholderText: qsTr("enter $API_KEY")
+ Accessible.role: Accessible.EditableText
+ Accessible.name: placeholderText
+ Accessible.description: qsTr("Whether the file hash is being calculated")
+ }
+ }
+
+ ColumnLayout {
+ visible: providerIsCustom
+ MySettingsLabel {
+ text: qsTr("Base Url")
+ font.bold: true
+ font.pixelSize: theme.fontSizeLarge
+ color: theme.settingsTitleTextColor
+ }
+ MyTextField {
+ id: baseUrl
+ Layout.fillWidth: true
+ wrapMode: Text.WrapAnywhere
+ function showError() {
+ messageToast.show(qsTr("ERROR: $BASE_URL is empty."));
+ baseUrl.placeholderTextColor = theme.textErrorColor;
+ }
+ onTextChanged: {
+ baseUrl.placeholderTextColor = theme.mutedTextColor;
+ }
+ placeholderText: qsTr("enter $BASE_URL")
+ Accessible.role: Accessible.EditableText
+ Accessible.name: placeholderText
+ }
+ }
+ ColumnLayout {
+ visible: providerIsCustom
+ MySettingsLabel {
+ text: qsTr("Model Name")
+ font.bold: true
+ font.pixelSize: theme.fontSizeLarge
+ color: theme.settingsTitleTextColor
+ }
+ MyTextField {
+ id: modelNameField
+ Layout.fillWidth: true
+ wrapMode: Text.WrapAnywhere
+ function showError() {
+ messageToast.show(qsTr("ERROR: $MODEL_NAME is empty."))
+ modelNameField.placeholderTextColor = theme.textErrorColor;
+ }
+ onTextChanged: {
+ modelNameField.placeholderTextColor = theme.mutedTextColor;
+ }
+ placeholderText: qsTr("enter $MODEL_NAME")
+ Accessible.role: Accessible.EditableText
+ Accessible.name: placeholderText
+ }
+ }
+
+ ColumnLayout {
+ visible: myModelList.count > 0 && !providerIsCustom
+
+ MySettingsLabel {
+ text: qsTr("Models")
+ font.bold: true
+ font.pixelSize: theme.fontSizeLarge
+ color: theme.settingsTitleTextColor
+ }
+
+ RowLayout {
+ spacing: 10
+
+ MyComboBox {
+ Layout.fillWidth: true
+ id: myModelList
+ currentIndex: -1;
+ }
+ }
+ }
+
+ MySettingsButton {
+ id: installButton
+ Layout.alignment: Qt.AlignRight
+ text: qsTr("Install")
+ font.pixelSize: theme.fontSizeLarge
+
+ property string apiKeyText: apiKeyField.text.trim()
+ property string baseUrlText: providerIsCustom ? baseUrlField.text.trim() : providerBaseUrl.trim()
+ property string modelNameText: providerIsCustom ? modelNameField.text.trim() : myModelList.currentText.trim()
+
+ enabled: apiKeyText !== "" && baseUrlText !== "" && modelNameText !== ""
+
+ onClicked: {
+ Download.installCompatibleModel(
+ modelNameText,
+ apiKeyText,
+ baseUrlText,
+ );
+ myModelList.currentIndex = -1;
+ }
+ Accessible.role: Accessible.Button
+ Accessible.name: qsTr("Install")
+ Accessible.description: qsTr("Install remote model")
+ }
+ }
+}
diff --git a/gpt4all-chat/src/modellist.cpp b/gpt4all-chat/src/modellist.cpp
index 6311e7e7..b24ffa1f 100644
--- a/gpt4all-chat/src/modellist.cpp
+++ b/gpt4all-chat/src/modellist.cpp
@@ -502,10 +502,12 @@ bool GPT4AllDownloadableModels::filterAcceptsRow(int sourceRow,
bool hasDescription = !description.isEmpty();
bool isClone = sourceModel()->data(index, ModelList::IsCloneRole).toBool();
bool isDiscovered = sourceModel()->data(index, ModelList::IsDiscoveredRole).toBool();
+ bool isOnline = sourceModel()->data(index, ModelList::OnlineRole).toBool();
+ bool isCompatibleApi = sourceModel()->data(index, ModelList::CompatibleApiRole).toBool();
bool satisfiesKeyword = m_keywords.isEmpty();
for (const QString &k : m_keywords)
satisfiesKeyword = description.contains(k) ? true : satisfiesKeyword;
- return !isDiscovered && hasDescription && !isClone && satisfiesKeyword;
+ return !isOnline && !isCompatibleApi && !isDiscovered && hasDescription && !isClone && satisfiesKeyword;
}
int GPT4AllDownloadableModels::count() const
@@ -2356,3 +2358,56 @@ void ModelList::handleDiscoveryItemErrorOccurred(QNetworkReply::NetworkError cod
qWarning() << u"ERROR: Discovery item failed with error code \"%1-%2\""_s
.arg(code).arg(reply->errorString()).toStdString();
}
+
+QStringList ModelList::remoteModelList(const QString &apiKey, const QUrl &baseUrl)
+{
+ QStringList modelList;
+
+ // Create the request
+ QNetworkRequest request;
+ request.setUrl(baseUrl.resolved(QUrl("models")));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+
+ // Add the Authorization header
+ const QString bearerToken = QString("Bearer %1").arg(apiKey);
+ request.setRawHeader("Authorization", bearerToken.toUtf8());
+
+ // Make the GET request
+ QNetworkReply *reply = m_networkManager.get(request);
+
+ // We use a local event loop to wait for the request to complete
+ QEventLoop loop;
+ connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
+ loop.exec();
+
+ // Check for errors
+ if (reply->error() == QNetworkReply::NoError) {
+ // Parse the JSON response
+ const QByteArray responseData = reply->readAll();
+ const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);
+
+ if (!jsonDoc.isNull() && jsonDoc.isObject()) {
+ QJsonObject rootObj = jsonDoc.object();
+ QJsonValue dataValue = rootObj.value("data");
+
+ if (dataValue.isArray()) {
+ QJsonArray dataArray = dataValue.toArray();
+ for (const QJsonValue &val : dataArray) {
+ if (val.isObject()) {
+ QJsonObject obj = val.toObject();
+ const QString modelId = obj.value("id").toString();
+ modelList.append(modelId);
+ }
+ }
+ }
+ }
+ } else {
+ // Handle network error (e.g. print it to qDebug)
+ qWarning() << "Error retrieving models:" << reply->errorString();
+ }
+
+ // Clean up
+ reply->deleteLater();
+
+ return modelList;
+}
diff --git a/gpt4all-chat/src/modellist.h b/gpt4all-chat/src/modellist.h
index 49eee8c4..756c6ffe 100644
--- a/gpt4all-chat/src/modellist.h
+++ b/gpt4all-chat/src/modellist.h
@@ -534,6 +534,8 @@ public:
Q_INVOKABLE void discoverSearch(const QString &discover);
+ Q_INVOKABLE QStringList remoteModelList(const QString &apiKey, const QUrl &baseUrl);
+
Q_SIGNALS:
void countChanged();
void installedModelsChanged();