diff --git a/gpt4all-chat/CMakeLists.txt b/gpt4all-chat/CMakeLists.txt index af958afdd610..a9b067a433b7 100644 --- a/gpt4all-chat/CMakeLists.txt +++ b/gpt4all-chat/CMakeLists.txt @@ -457,6 +457,14 @@ else() endif() target_link_libraries(chat PRIVATE llmodel SingleApplication fmt::fmt duckx::duckx QXlsx) + +# Add QXlsx include path - system package uses QXlsxQt6, submodule uses local path +if (CMAKE_SYSTEM_NAME MATCHES Linux AND NOT DEFINED ENV{GPT4ALL_USE_SUBMODULE_QXLSX}) + target_include_directories(chat PRIVATE /usr/include/QXlsxQt6) +else() + target_include_directories(chat PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/deps/QXlsx/QXlsx/header) +endif() + target_include_directories(chat PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/deps/json/include) target_include_directories(chat PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/deps/json/include/nlohmann) target_include_directories(chat PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/deps/minja/include) diff --git a/gpt4all-chat/deps/CMakeLists.txt b/gpt4all-chat/deps/CMakeLists.txt index 6ef203bc06e3..6f723c5c01e0 100644 --- a/gpt4all-chat/deps/CMakeLists.txt +++ b/gpt4all-chat/deps/CMakeLists.txt @@ -3,18 +3,7 @@ include(FetchContent) set(BUILD_SHARED_LIBS OFF) -set(FMT_INSTALL OFF) -add_subdirectory(fmt) - -set(QAPPLICATION_CLASS QApplication) -add_subdirectory(SingleApplication) - -set(DUCKX_INSTALL OFF) -add_subdirectory(DuckX) - -set(QT_VERSION_MAJOR 6) -add_subdirectory(QXlsx/QXlsx) - +# PDFium needs to be fetched first (uses FetchContent) if (NOT GPT4ALL_USING_QTPDF) # If we do not use QtPDF, we need to get PDFium. set(GPT4ALL_PDFIUM_TAG "chromium/6996") @@ -49,3 +38,39 @@ if (NOT GPT4ALL_USING_QTPDF) FetchContent_MakeAvailable(pdfium) find_package(PDFium REQUIRED PATHS "${pdfium_SOURCE_DIR}" NO_DEFAULT_PATH) endif() + +set(FMT_INSTALL OFF) +add_subdirectory(fmt) + +set(QAPPLICATION_CLASS QApplication) +add_subdirectory(SingleApplication) + +set(DUCKX_INSTALL OFF) +add_subdirectory(DuckX) + +# Use system QXlsx on Linux (both submodule and system package are incompatible with Qt 6.8+ due to GuiPrivate) +# Manually create the target using system library and headers +if (CMAKE_SYSTEM_NAME MATCHES Linux AND NOT DEFINED ENV{GPT4ALL_USE_SUBMODULE_QXLSX}) + if(NOT TARGET QXlsx) + add_library(QXlsx SHARED IMPORTED) + set_target_properties(QXlsx PROPERTIES + IMPORTED_LOCATION "/usr/lib/libQXlsxQt6.so" + IMPORTED_NO_SONAME ON + INTERFACE_COMPILE_DEFINITIONS "QXlsx_SHAREDLIB" + INTERFACE_COMPILE_FEATURES "cxx_std_11" + INTERFACE_INCLUDE_DIRECTORIES "/usr/include/QXlsxQt6" + ) + endif() + + # Add the Qt dependencies to QXlsx since system package references removed GuiPrivate + if(TARGET QXlsx) + get_target_property(_existing_libs QXlsx INTERFACE_LINK_LIBRARIES) + if(NOT _existing_libs) + set_property(TARGET QXlsx APPEND PROPERTY INTERFACE_LINK_LIBRARIES Qt6::Core) + set_property(TARGET QXlsx APPEND PROPERTY INTERFACE_LINK_LIBRARIES Qt6::Gui) + endif() + endif() +else() + set(QT_VERSION_MAJOR 6) + add_subdirectory(QXlsx/QXlsx) +endif() diff --git a/gpt4all-chat/qml/RemoteModelCard.qml b/gpt4all-chat/qml/RemoteModelCard.qml index e8c637654810..9ed5f00c43ab 100644 --- a/gpt4all-chat/qml/RemoteModelCard.qml +++ b/gpt4all-chat/qml/RemoteModelCard.qml @@ -25,6 +25,11 @@ Rectangle { property bool providerIsCustom: false property var modelWhitelist: null + // Custom headers storage as ListModel with "key" and "value" roles + ListModel { + id: customHeadersModel + } + color: theme.conversationBackground radius: 10 border.width: 1 @@ -193,6 +198,61 @@ Rectangle { } } + // Custom Headers Section + ColumnLayout { + MySettingsLabel { + text: qsTr("Custom Headers") + font.bold: true + font.pixelSize: theme.fontSizeLarge + color: theme.settingsTitleTextColor + } + + ListView { + id: customHeadersView + Layout.fillWidth: true + implicitHeight: Math.max(model.count * 45, 30) + model: customHeadersModel + + delegate: RowLayout { + Layout.fillWidth: true + spacing: 5 + + MyTextField { + id: headerKeyField + Layout.fillWidth: true + font.pixelSize: theme.fontSizeLarge + placeholderText: qsTr("Header Name") + text: model.headerKey || "" + } + + MyTextField { + id: headerValueField + Layout.fillWidth: true + font.pixelSize: theme.fontSizeLarge + placeholderText: qsTr("Header Value") + text: model.headerValue || "" + } + + MyButton { + Layout.preferredWidth: contentItem.implicitWidth + text: qsTr("Remove") + font.pixelSize: theme.fontSizeLarge + onClicked: customHeadersModel.remove(index) + } + } + + ScrollIndicator.horizontal: ScrollIndicator {} + } + + MyButton { + id: addHeaderButton + Layout.alignment: Qt.AlignRight + text: qsTr("Add Header") + font.pixelSize: theme.fontSizeLarge + onClicked: customHeadersModel.append({headerKey: "", headerValue: ""}) + } + } + MySettingsButton { id: installButton Layout.alignment: Qt.AlignRight @@ -202,6 +262,16 @@ Rectangle { 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() + property string customHeadersText: { + var headersArray = [] + for (var i = 0; i < customHeadersModel.count; i++) { + var item = customHeadersModel.get(i) + if ((item.headerKey || "").trim() !== "") { + headersArray.push({key: (item.headerKey || "").trim(), value: (item.headerValue || "").trim()}) + } + } + return JSON.stringify(headersArray) + } enabled: apiKeyText !== "" && baseUrlText !== "" && modelNameText !== "" @@ -210,7 +280,7 @@ Rectangle { modelNameText, apiKeyText, baseUrlText, - ); + customHeadersText); myModelList.currentIndex = -1; } Accessible.role: Accessible.Button diff --git a/gpt4all-chat/src/chatapi.cpp b/gpt4all-chat/src/chatapi.cpp index aa2a7f6f2dd0..76674fb444e6 100644 --- a/gpt4all-chat/src/chatapi.cpp +++ b/gpt4all-chat/src/chatapi.cpp @@ -207,7 +207,7 @@ void ChatAPI::prompt( connect(&worker, &ChatAPIWorker::finished, &workerThread, &QThread::quit, Qt::DirectConnection); connect(this, &ChatAPI::request, &worker, &ChatAPIWorker::request, Qt::QueuedConnection); workerThread.start(); - emit request(m_apiKey, doc.toJson(QJsonDocument::Compact)); + emit request(m_apiKey, doc.toJson(QJsonDocument::Compact), m_customHeaders); workerThread.wait(); m_responseCallback = nullptr; @@ -227,17 +227,35 @@ bool ChatAPI::callResponse(int32_t token, const std::string& string) return m_responseCallback(token, string); } -void ChatAPIWorker::request(const QString &apiKey, const QByteArray &array) +void ChatAPIWorker::request(const QString &apiKey, const QByteArray &array, const QString &customHeaders) { QUrl apiUrl(m_chat->url()); const QString authorization = u"Bearer %1"_s.arg(apiKey).trimmed(); QNetworkRequest request(apiUrl); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Authorization", authorization.toUtf8()); + + // Apply custom headers + if (!customHeaders.isEmpty()) { + QJsonDocument headerDoc = QJsonDocument::fromJson(customHeaders.toUtf8()); + if (!headerDoc.isNull() && headerDoc.isArray()) { + QJsonArray headersArray = headerDoc.array(); + for (const QJsonValue &headerVal : headersArray) { + QJsonObject headerObj = headerVal.toObject(); + QString name = headerObj.value("key").toString(); + QString value = headerObj.value("value").toString(); + if (!name.isEmpty() && !value.isEmpty()) { + request.setRawHeader(name.toUtf8(), value.toUtf8()); + } + } + } + } + #if defined(DEBUG) qDebug() << "ChatAPI::request" << "API URL: " << apiUrl.toString() - << "Authorization: " << authorization.toUtf8(); + << "Authorization: " << authorization.toUtf8() + << "Custom Headers:" << customHeaders; #endif m_networkManager = new QNetworkAccessManager(this); QNetworkReply *reply = m_networkManager->post(request, array); diff --git a/gpt4all-chat/src/chatapi.h b/gpt4all-chat/src/chatapi.h index f937a20d55c3..dfe2e2c226ae 100644 --- a/gpt4all-chat/src/chatapi.h +++ b/gpt4all-chat/src/chatapi.h @@ -34,7 +34,7 @@ class ChatAPIWorker : public QObject { QString currentResponse() const { return m_currentResponse; } - void request(const QString &apiKey, const QByteArray &array); + void request(const QString &apiKey, const QByteArray &array, const QString &customHeaders = ""); Q_SIGNALS: void finished(); @@ -87,6 +87,8 @@ class ChatAPI : public QObject, public LLModel { void setRequestURL(const QString &requestURL) { m_requestURL = requestURL; } QString url() const { return m_requestURL; } + void setCustomHeaders(const QString &headersJson) { m_customHeaders = headersJson; } + bool callResponse(int32_t token, const std::string &string); [[noreturn]] @@ -97,7 +99,7 @@ class ChatAPI : public QObject, public LLModel { { return {}; } Q_SIGNALS: - void request(const QString &apiKey, const QByteArray &array); + void request(const QString &apiKey, const QByteArray &array, const QString &customHeaders = ""); protected: // We have to implement these as they are pure virtual in base class, but we don't actually use @@ -168,6 +170,7 @@ class ChatAPI : public QObject, public LLModel { QString m_modelName; QString m_apiKey; QString m_requestURL; + QString m_customHeaders; // JSON string containing array of header objects }; #endif // CHATAPI_H diff --git a/gpt4all-chat/src/chatllm.cpp b/gpt4all-chat/src/chatllm.cpp index 1fbcce8c6880..ced6a5633f41 100644 --- a/gpt4all-chat/src/chatllm.cpp +++ b/gpt4all-chat/src/chatllm.cpp @@ -21,6 +21,7 @@ #include #include #include // IWYU pragma: keep +#include #include #include #include @@ -440,6 +441,7 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo) QString apiKey; QString requestUrl; QString modelName; + QString customHeaders; { QFile file(filePath); bool success = file.open(QIODeviceBase::ReadOnly); @@ -459,6 +461,12 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo) QString suffixPath("%1/chat/completions"); apiUrl.setPath(suffixPath.arg(currentPath)); requestUrl = apiUrl.toString(); + + // Read custom headers from the .rmodel file + QJsonArray headersArray = obj["customHeaders"].toArray(); + if (!headersArray.isEmpty()) { + customHeaders = QJsonDocument(headersArray).toJson(QJsonDocument::Compact); + } } else { requestUrl = modelInfo.url(); } @@ -468,6 +476,9 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo) model->setModelName(modelName); model->setRequestURL(requestUrl); model->setAPIKey(apiKey); + if (!customHeaders.isEmpty()) { + model->setCustomHeaders(customHeaders); + } m_llModelInfo.resetModel(this, model); } else if (!loadNewModel(modelInfo, modelLoadProps)) { return false; // m_shouldBeLoaded became false diff --git a/gpt4all-chat/src/download.cpp b/gpt4all-chat/src/download.cpp index 6891feeb28ea..39a2f64ce68e 100644 --- a/gpt4all-chat/src/download.cpp +++ b/gpt4all-chat/src/download.cpp @@ -281,7 +281,7 @@ void Download::installModel(const QString &modelFile, const QString &apiKey) ModelList::globalInstance()->updateDataByFilename(modelFile, {{ ModelList::InstalledRole, true }}); } -void Download::installCompatibleModel(const QString &modelName, const QString &apiKey, const QString &baseUrl) +void Download::installCompatibleModel(const QString &modelName, const QString &apiKey, const QString &baseUrl, const QString &customHeaders) { Q_ASSERT(!modelName.isEmpty()); if (modelName.isEmpty()) { @@ -316,6 +316,17 @@ void Download::installCompatibleModel(const QString &modelName, const QString &a obj.insert("apiKey", apiKey); obj.insert("modelName", modelName); obj.insert("baseUrl", apiBaseUrl.toString()); + + // Parse and store custom headers + QJsonArray headersArray; + if (!customHeaders.isEmpty()) { + QJsonDocument headerDoc = QJsonDocument::fromJson(customHeaders.toUtf8()); + if (!headerDoc.isNull() && headerDoc.isArray()) { + headersArray = headerDoc.array(); + } + } + obj.insert("customHeaders", headersArray); + QJsonDocument doc(obj); QTextStream stream(&file); diff --git a/gpt4all-chat/src/download.h b/gpt4all-chat/src/download.h index 6db0c6ce5fbd..f5a913ea2028 100644 --- a/gpt4all-chat/src/download.h +++ b/gpt4all-chat/src/download.h @@ -68,7 +68,7 @@ class Download : public QObject Q_INVOKABLE void downloadModel(const QString &modelFile); Q_INVOKABLE void cancelDownload(const QString &modelFile); Q_INVOKABLE void installModel(const QString &modelFile, const QString &apiKey); - Q_INVOKABLE void installCompatibleModel(const QString &modelName, const QString &apiKey, const QString &baseUrl); + Q_INVOKABLE void installCompatibleModel(const QString &modelName, const QString &apiKey, const QString &baseUrl, const QString &customHeaders = ""); Q_INVOKABLE void removeModel(const QString &modelFile); Q_INVOKABLE bool isFirstStart(bool writeVersion = false) const; diff --git a/gpt4all-chat/src/modellist.cpp b/gpt4all-chat/src/modellist.cpp index 248f2f09f051..6cfd2ca7f9ab 100644 --- a/gpt4all-chat/src/modellist.cpp +++ b/gpt4all-chat/src/modellist.cpp @@ -2358,7 +2358,7 @@ void ModelList::handleDiscoveryItemErrorOccurred(QNetworkReply::NetworkError cod .arg(code).arg(reply->errorString()).toStdString(); } -QStringList ModelList::remoteModelList(const QString &apiKey, const QUrl &baseUrl) +QStringList ModelList::remoteModelList(const QString &apiKey, const QUrl &baseUrl, const QString &customHeaders) { QStringList modelList; @@ -2371,6 +2371,22 @@ QStringList ModelList::remoteModelList(const QString &apiKey, const QUrl &baseUr const QString bearerToken = QString("Bearer %1").arg(apiKey); request.setRawHeader("Authorization", bearerToken.toUtf8()); + // Apply custom headers + if (!customHeaders.isEmpty()) { + QJsonDocument headerDoc = QJsonDocument::fromJson(customHeaders.toUtf8()); + if (!headerDoc.isNull() && headerDoc.isArray()) { + QJsonArray headersArray = headerDoc.array(); + for (const QJsonValue &headerVal : headersArray) { + QJsonObject headerObj = headerVal.toObject(); + QString name = headerObj.value("key").toString(); + QString value = headerObj.value("value").toString(); + if (!name.isEmpty() && !value.isEmpty()) { + request.setRawHeader(name.toUtf8(), value.toUtf8()); + } + } + } + } + // Make the GET request QNetworkReply *reply = m_networkManager.get(request); diff --git a/gpt4all-chat/src/modellist.h b/gpt4all-chat/src/modellist.h index 756c6ffe4318..eed1cadd5ab6 100644 --- a/gpt4all-chat/src/modellist.h +++ b/gpt4all-chat/src/modellist.h @@ -534,7 +534,7 @@ class ModelList : public QAbstractListModel Q_INVOKABLE void discoverSearch(const QString &discover); - Q_INVOKABLE QStringList remoteModelList(const QString &apiKey, const QUrl &baseUrl); + Q_INVOKABLE QStringList remoteModelList(const QString &apiKey, const QUrl &baseUrl, const QString &customHeaders = ""); Q_SIGNALS: void countChanged(); diff --git a/gpt4all-chat/translations/gpt4all_en_US.ts b/gpt4all-chat/translations/gpt4all_en_US.ts index 50e39de3cc96..680e1c73df19 100644 --- a/gpt4all-chat/translations/gpt4all_en_US.ts +++ b/gpt4all-chat/translations/gpt4all_en_US.ts @@ -2685,6 +2685,31 @@ NOTE: By turning on this feature, you will be sending your data to the GPT4All O Install remote model + + + Custom Headers + + + + + Header Name + + + + + Header Value + + + + + Add Header + + + + + Remove + + SettingsView