diff --git a/ChatView/ChatModel.cpp b/ChatView/ChatModel.cpp index a0cc1ec..28f7f3f 100644 --- a/ChatView/ChatModel.cpp +++ b/ChatView/ChatModel.cpp @@ -131,7 +131,9 @@ void ChatModel::addMessage( ChatRole role, const QString &id, const QList &attachments, - const QList &images) + const QList &images, + bool isRedacted, + const QString &signature) { QString fullContent = content; if (!attachments.isEmpty()) { @@ -148,12 +150,16 @@ void ChatModel::addMessage( lastMessage.content = content; lastMessage.attachments = attachments; lastMessage.images = images; + lastMessage.isRedacted = isRedacted; + lastMessage.signature = signature; emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1)); } else { beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size()); Message newMessage{role, content, id}; newMessage.attachments = attachments; newMessage.images = images; + newMessage.isRedacted = isRedacted; + newMessage.signature = signature; m_messages.append(newMessage); endInsertRows(); diff --git a/ChatView/ChatModel.hpp b/ChatView/ChatModel.hpp index 1ad4331..66828f9 100644 --- a/ChatView/ChatModel.hpp +++ b/ChatView/ChatModel.hpp @@ -73,7 +73,9 @@ public: ChatRole role, const QString &id, const QList &attachments = {}, - const QList &images = {}); + const QList &images = {}, + bool isRedacted = false, + const QString &signature = QString()); Q_INVOKABLE void clear(); Q_INVOKABLE QList processMessageContent(const QString &content) const; diff --git a/ChatView/ChatSerializer.cpp b/ChatView/ChatSerializer.cpp index 820944a..91c458e 100644 --- a/ChatView/ChatSerializer.cpp +++ b/ChatView/ChatSerializer.cpp @@ -94,7 +94,11 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message, messageObj["role"] = static_cast(message.role); messageObj["content"] = message.content; messageObj["id"] = message.id; - messageObj["isRedacted"] = message.isRedacted; + + if (message.isRedacted) { + messageObj["isRedacted"] = true; + } + if (!message.signature.isEmpty()) { messageObj["signature"] = message.signature; } @@ -167,8 +171,11 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json, model->setLoadingFromHistory(true); for (const auto &message : messages) { - model->addMessage(message.content, message.role, message.id, message.attachments, message.images); - LOG_MESSAGE(QString("Loaded message with %1 image(s)").arg(message.images.size())); + model->addMessage(message.content, message.role, message.id, message.attachments, message.images, message.isRedacted, message.signature); + LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3") + .arg(message.images.size()) + .arg(message.isRedacted) + .arg(message.signature.length())); } model->setLoadingFromHistory(false); diff --git a/templates/Claude.hpp b/templates/Claude.hpp index c2818d1..53475ee 100644 --- a/templates/Claude.hpp +++ b/templates/Claude.hpp @@ -44,6 +44,11 @@ public: if (msg.role == "system") continue; if (msg.isThinking) { + // Claude API requires signature for thinking blocks + if (msg.signature.isEmpty()) { + continue; + } + QJsonArray content; QJsonObject thinkingBlock; thinkingBlock["type"] = msg.isRedacted ? "redacted_thinking" : "thinking"; @@ -57,9 +62,7 @@ public: if (!msg.isRedacted) { thinkingBlock["thinking"] = thinkingText; } - if (!msg.signature.isEmpty()) { - thinkingBlock["signature"] = msg.signature; - } + thinkingBlock["signature"] = msg.signature; content.append(thinkingBlock); messages.append(QJsonObject{{"role", "assistant"}, {"content", content}}); diff --git a/templates/GoogleAI.hpp b/templates/GoogleAI.hpp index 01d2bed..4a61ed0 100644 --- a/templates/GoogleAI.hpp +++ b/templates/GoogleAI.hpp @@ -46,36 +46,58 @@ public: QJsonObject content; QJsonArray parts; - if (!msg.content.isEmpty()) { - parts.append(QJsonObject{{"text", msg.content}}); - } - - if (msg.images && !msg.images->isEmpty()) { - for (const auto &image : msg.images.value()) { - QJsonObject imagePart; - - if (image.isUrl) { - QJsonObject fileData; - fileData["mime_type"] = image.mediaType; - fileData["file_uri"] = image.data; - imagePart["file_data"] = fileData; - } else { - QJsonObject inlineData; - inlineData["mime_type"] = image.mediaType; - inlineData["data"] = image.data; - imagePart["inline_data"] = inlineData; - } - - parts.append(imagePart); + if (msg.isThinking) { + if (!msg.content.isEmpty()) { + QJsonObject thinkingPart; + thinkingPart["text"] = msg.content; + thinkingPart["thought"] = true; + parts.append(thinkingPart); } + + if (!msg.signature.isEmpty()) { + QJsonObject signaturePart; + signaturePart["thoughtSignature"] = msg.signature; + parts.append(signaturePart); + } + + if (parts.isEmpty()) { + continue; + } + + content["role"] = "model"; + } else { + if (!msg.content.isEmpty()) { + parts.append(QJsonObject{{"text", msg.content}}); + } + + if (msg.images && !msg.images->isEmpty()) { + for (const auto &image : msg.images.value()) { + QJsonObject imagePart; + + if (image.isUrl) { + QJsonObject fileData; + fileData["mime_type"] = image.mediaType; + fileData["file_uri"] = image.data; + imagePart["file_data"] = fileData; + } else { + QJsonObject inlineData; + inlineData["mime_type"] = image.mediaType; + inlineData["data"] = image.data; + imagePart["inline_data"] = inlineData; + } + + parts.append(imagePart); + } + } + + QString role = msg.role; + if (role == "assistant") { + role = "model"; + } + + content["role"] = role; } - QString role = msg.role; - if (role == "assistant") { - role = "model"; - } - - content["role"] = role; content["parts"] = parts; contents.append(content); } @@ -95,11 +117,15 @@ public: " },\n" " {\n" " \"role\": \"model\",\n" - " \"parts\": [{\"text\": \"\"}]\n" + " \"parts\": [\n" + " {\"text\": \"\", \"thought\": true},\n" + " {\"thoughtSignature\": \"\"},\n" + " {\"text\": \"\"}\n" + " ]\n" " }\n" " ]\n" "}\n\n" - "Supports proper role mapping, including model/user roles."; + "Supports proper role mapping (model/user roles), images, and thinking blocks."; } bool isSupportProvider(LLMCore::ProviderID id) const override