From e6592b17f7c758174886207a510abc57d2d8a9bb Mon Sep 17 00:00:00 2001 From: Adam Treat Date: Wed, 19 Feb 2025 14:00:07 -0500 Subject: [PATCH] Add new remote model provider view. Signed-off-by: Adam Treat --- gpt4all-chat/CMakeLists.txt | 5 + gpt4all-chat/icons/groq.svg | 3 + gpt4all-chat/icons/mistral.svg | 1 + gpt4all-chat/icons/openai.svg | 2 + gpt4all-chat/qml/AddGPT4AllModelView.qml | 111 +------------ gpt4all-chat/qml/AddModelView.qml | 26 ++- gpt4all-chat/qml/AddRemoteModelView.qml | 86 ++++++++++ gpt4all-chat/qml/RemoteModelCard.qml | 202 +++++++++++++++++++++++ gpt4all-chat/src/modellist.cpp | 57 ++++++- gpt4all-chat/src/modellist.h | 2 + 10 files changed, 381 insertions(+), 114 deletions(-) create mode 100644 gpt4all-chat/icons/groq.svg create mode 100644 gpt4all-chat/icons/mistral.svg create mode 100644 gpt4all-chat/icons/openai.svg create mode 100644 gpt4all-chat/qml/AddRemoteModelView.qml create mode 100644 gpt4all-chat/qml/RemoteModelCard.qml 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();