Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions gpt4all-chat/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
49 changes: 37 additions & 12 deletions gpt4all-chat/deps/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
72 changes: 71 additions & 1 deletion gpt4all-chat/qml/RemoteModelCard.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 !== ""

Expand All @@ -210,7 +280,7 @@ Rectangle {
modelNameText,
apiKeyText,
baseUrlText,
);
customHeadersText);
myModelList.currentIndex = -1;
}
Accessible.role: Accessible.Button
Expand Down
24 changes: 21 additions & 3 deletions gpt4all-chat/src/chatapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions gpt4all-chat/src/chatapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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]]
Expand All @@ -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
Expand Down Expand Up @@ -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
11 changes: 11 additions & 0 deletions gpt4all-chat/src/chatllm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <QFile>
#include <QGlobalStatic>
#include <QIODevice> // IWYU pragma: keep
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
Expand Down Expand Up @@ -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);
Expand All @@ -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();
}
Expand All @@ -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
Expand Down
13 changes: 12 additions & 1 deletion gpt4all-chat/src/download.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion gpt4all-chat/src/download.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
18 changes: 17 additions & 1 deletion gpt4all-chat/src/modellist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion gpt4all-chat/src/modellist.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading