fix: Add signature to chat history

This commit is contained in:
Petr Mironychev
2025-11-28 12:24:47 +01:00
parent 1f9c60ffb2
commit f6d647d5c8
5 changed files with 81 additions and 37 deletions

View File

@ -131,7 +131,9 @@ void ChatModel::addMessage(
ChatRole role, ChatRole role,
const QString &id, const QString &id,
const QList<Context::ContentFile> &attachments, const QList<Context::ContentFile> &attachments,
const QList<ImageAttachment> &images) const QList<ImageAttachment> &images,
bool isRedacted,
const QString &signature)
{ {
QString fullContent = content; QString fullContent = content;
if (!attachments.isEmpty()) { if (!attachments.isEmpty()) {
@ -148,12 +150,16 @@ void ChatModel::addMessage(
lastMessage.content = content; lastMessage.content = content;
lastMessage.attachments = attachments; lastMessage.attachments = attachments;
lastMessage.images = images; lastMessage.images = images;
lastMessage.isRedacted = isRedacted;
lastMessage.signature = signature;
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1)); emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
} else { } else {
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size()); beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message newMessage{role, content, id}; Message newMessage{role, content, id};
newMessage.attachments = attachments; newMessage.attachments = attachments;
newMessage.images = images; newMessage.images = images;
newMessage.isRedacted = isRedacted;
newMessage.signature = signature;
m_messages.append(newMessage); m_messages.append(newMessage);
endInsertRows(); endInsertRows();

View File

@ -73,7 +73,9 @@ public:
ChatRole role, ChatRole role,
const QString &id, const QString &id,
const QList<Context::ContentFile> &attachments = {}, const QList<Context::ContentFile> &attachments = {},
const QList<ImageAttachment> &images = {}); const QList<ImageAttachment> &images = {},
bool isRedacted = false,
const QString &signature = QString());
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const; Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;

View File

@ -94,7 +94,11 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
messageObj["role"] = static_cast<int>(message.role); messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content; messageObj["content"] = message.content;
messageObj["id"] = message.id; messageObj["id"] = message.id;
messageObj["isRedacted"] = message.isRedacted;
if (message.isRedacted) {
messageObj["isRedacted"] = true;
}
if (!message.signature.isEmpty()) { if (!message.signature.isEmpty()) {
messageObj["signature"] = message.signature; messageObj["signature"] = message.signature;
} }
@ -167,8 +171,11 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json,
model->setLoadingFromHistory(true); model->setLoadingFromHistory(true);
for (const auto &message : messages) { for (const auto &message : messages) {
model->addMessage(message.content, message.role, message.id, message.attachments, message.images); 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)").arg(message.images.size())); 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); model->setLoadingFromHistory(false);

View File

@ -44,6 +44,11 @@ public:
if (msg.role == "system") continue; if (msg.role == "system") continue;
if (msg.isThinking) { if (msg.isThinking) {
// Claude API requires signature for thinking blocks
if (msg.signature.isEmpty()) {
continue;
}
QJsonArray content; QJsonArray content;
QJsonObject thinkingBlock; QJsonObject thinkingBlock;
thinkingBlock["type"] = msg.isRedacted ? "redacted_thinking" : "thinking"; thinkingBlock["type"] = msg.isRedacted ? "redacted_thinking" : "thinking";
@ -57,9 +62,7 @@ public:
if (!msg.isRedacted) { if (!msg.isRedacted) {
thinkingBlock["thinking"] = thinkingText; thinkingBlock["thinking"] = thinkingText;
} }
if (!msg.signature.isEmpty()) { thinkingBlock["signature"] = msg.signature;
thinkingBlock["signature"] = msg.signature;
}
content.append(thinkingBlock); content.append(thinkingBlock);
messages.append(QJsonObject{{"role", "assistant"}, {"content", content}}); messages.append(QJsonObject{{"role", "assistant"}, {"content", content}});

View File

@ -46,36 +46,58 @@ public:
QJsonObject content; QJsonObject content;
QJsonArray parts; QJsonArray parts;
if (!msg.content.isEmpty()) { if (msg.isThinking) {
parts.append(QJsonObject{{"text", msg.content}}); if (!msg.content.isEmpty()) {
} QJsonObject thinkingPart;
thinkingPart["text"] = msg.content;
if (msg.images && !msg.images->isEmpty()) { thinkingPart["thought"] = true;
for (const auto &image : msg.images.value()) { parts.append(thinkingPart);
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.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; content["parts"] = parts;
contents.append(content); contents.append(content);
} }
@ -95,11 +117,15 @@ public:
" },\n" " },\n"
" {\n" " {\n"
" \"role\": \"model\",\n" " \"role\": \"model\",\n"
" \"parts\": [{\"text\": \"<assistant response>\"}]\n" " \"parts\": [\n"
" {\"text\": \"<thinking>\", \"thought\": true},\n"
" {\"thoughtSignature\": \"<signature>\"},\n"
" {\"text\": \"<assistant response>\"}\n"
" ]\n"
" }\n" " }\n"
" ]\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 bool isSupportProvider(LLMCore::ProviderID id) const override