fix: Add checking model support for tool calling (#350)

This commit is contained in:
Petr Mironychev
2026-05-17 21:27:18 +02:00
committed by GitHub
parent 6addcedfd0
commit 74c899c8c3
16 changed files with 334 additions and 18 deletions

View File

@@ -15,6 +15,7 @@ public:
PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
QString name() const override { return "Claude"; }
QStringList stopWords() const override { return QStringList(); }
bool supportsToolHistory() const override { return true; }
void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
{
QJsonArray messages;
@@ -24,9 +25,48 @@ public:
}
if (context.history) {
int toolResultUserIdx = -1;
for (const auto &msg : context.history.value()) {
if (msg.role == "system") continue;
if (!msg.toolCalls.isEmpty()) {
toolResultUserIdx = -1;
QJsonArray content;
if (!msg.content.isEmpty()) {
content.append(QJsonObject{{"type", "text"}, {"text", msg.content}});
}
for (const auto &call : msg.toolCalls) {
content.append(QJsonObject{
{"type", "tool_use"},
{"id", call.id},
{"name", call.name},
{"input", call.arguments}});
}
messages.append(QJsonObject{{"role", "assistant"}, {"content", content}});
continue;
}
if (msg.role == "tool") {
QJsonObject resultBlock{
{"type", "tool_result"},
{"tool_use_id", msg.toolCallId},
{"content", msg.content}};
if (toolResultUserIdx >= 0) {
QJsonObject userMsg = messages[toolResultUserIdx].toObject();
QJsonArray content = userMsg["content"].toArray();
content.append(resultBlock);
userMsg["content"] = content;
messages[toolResultUserIdx] = userMsg;
} else {
messages.append(QJsonObject{
{"role", "user"}, {"content", QJsonArray{resultBlock}}});
toolResultUserIdx = messages.size() - 1;
}
continue;
}
toolResultUserIdx = -1;
if (msg.isThinking) {
// Claude API requires signature for thinking blocks
if (msg.signature.isEmpty()) {

View File

@@ -16,6 +16,7 @@ public:
PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
QString name() const override { return "Google AI"; }
QStringList stopWords() const override { return QStringList(); }
bool supportsToolHistory() const override { return true; }
void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
{
@@ -26,7 +27,45 @@ public:
{"parts", QJsonObject{{"text", context.systemPrompt.value()}}}};
}
int toolResultIdx = -1;
for (const auto &msg : context.history.value()) {
if (!msg.toolCalls.isEmpty()) {
toolResultIdx = -1;
QJsonArray callParts;
if (!msg.content.isEmpty()) {
callParts.append(QJsonObject{{"text", msg.content}});
}
for (const auto &call : msg.toolCalls) {
callParts.append(QJsonObject{
{"functionCall",
QJsonObject{{"name", call.name}, {"args", call.arguments}}}});
}
contents.append(QJsonObject{{"role", "model"}, {"parts", callParts}});
continue;
}
if (msg.role == "tool") {
QJsonObject responsePart{
{"functionResponse",
QJsonObject{
{"name", msg.toolName},
{"response", QJsonObject{{"result", msg.content}}}}}};
if (toolResultIdx >= 0) {
QJsonObject fnMsg = contents[toolResultIdx].toObject();
QJsonArray fnParts = fnMsg["parts"].toArray();
fnParts.append(responsePart);
fnMsg["parts"] = fnParts;
contents[toolResultIdx] = fnMsg;
} else {
contents.append(
QJsonObject{{"role", "function"}, {"parts", QJsonArray{responsePart}}});
toolResultIdx = contents.size() - 1;
}
continue;
}
toolResultIdx = -1;
QJsonObject content;
QJsonArray parts;

View File

@@ -6,6 +6,7 @@
#include <QJsonArray>
#include "pluginllmcore/PromptTemplate.hpp"
#include "templates/ToolMessages.hpp"
namespace QodeAssist::Templates {
@@ -47,6 +48,7 @@ public:
PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
QString name() const override { return "Mistral AI Chat"; }
QStringList stopWords() const override { return QStringList(); }
bool supportsToolHistory() const override { return true; }
void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
{
@@ -59,6 +61,9 @@ public:
if (context.history) {
for (const auto &msg : context.history.value()) {
if (appendOpenAIToolMessage(messages, msg)) {
continue;
}
if (msg.images && !msg.images->isEmpty()) {
QJsonArray content;

View File

@@ -49,6 +49,7 @@ public:
PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
QString name() const override { return "Ollama Chat"; }
QStringList stopWords() const override { return QStringList(); }
bool supportsToolHistory() const override { return true; }
void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
{
@@ -63,8 +64,28 @@ public:
for (const auto &msg : context.history.value()) {
QJsonObject messageObj;
messageObj["role"] = msg.role;
messageObj["content"] = msg.content;
if (!msg.toolCalls.isEmpty()) {
QJsonArray toolCalls;
for (const auto &call : msg.toolCalls) {
toolCalls.append(QJsonObject{
{"type", "function"},
{"function",
QJsonObject{{"name", call.name}, {"arguments", call.arguments}}}});
}
messageObj["tool_calls"] = toolCalls;
if (!msg.content.isEmpty()) {
messageObj["content"] = msg.content;
}
} else {
messageObj["content"] = msg.content;
// Ollama correlates a tool result to its originating
// call by tool_name; omitting it breaks multi-tool turns.
if (msg.role == QLatin1String("tool") && !msg.toolName.isEmpty()) {
messageObj["tool_name"] = msg.toolName;
}
}
if (msg.images && !msg.images->isEmpty()) {
QJsonArray images;
for (const auto &image : msg.images.value()) {

View File

@@ -6,6 +6,7 @@
#include <QJsonArray>
#include "pluginllmcore/PromptTemplate.hpp"
#include "templates/ToolMessages.hpp"
namespace QodeAssist::Templates {
@@ -15,6 +16,7 @@ public:
PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
QString name() const override { return "OpenAI"; }
QStringList stopWords() const override { return QStringList(); }
bool supportsToolHistory() const override { return true; }
void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
{
QJsonArray messages;
@@ -26,6 +28,9 @@ public:
if (context.history) {
for (const auto &msg : context.history.value()) {
if (appendOpenAIToolMessage(messages, msg)) {
continue;
}
if (msg.images && !msg.images->isEmpty()) {
QJsonArray content;

View File

@@ -6,6 +6,7 @@
#include <QJsonArray>
#include "pluginllmcore/PromptTemplate.hpp"
#include "templates/ToolMessages.hpp"
namespace QodeAssist::Templates {
@@ -15,6 +16,7 @@ public:
PluginLLMCore::TemplateType type() const override { return PluginLLMCore::TemplateType::Chat; }
QString name() const override { return "OpenAI Compatible"; }
QStringList stopWords() const override { return QStringList(); }
bool supportsToolHistory() const override { return true; }
void prepareRequest(QJsonObject &request, const PluginLLMCore::ContextData &context) const override
{
QJsonArray messages;
@@ -26,6 +28,9 @@ public:
if (context.history) {
for (const auto &msg : context.history.value()) {
if (appendOpenAIToolMessage(messages, msg)) {
continue;
}
if (msg.images && !msg.images->isEmpty()) {
QJsonArray content;

View File

@@ -6,6 +6,7 @@
#include "pluginllmcore/PromptTemplate.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace QodeAssist::Templates {
@@ -22,6 +23,8 @@ public:
QStringList stopWords() const override { return {}; }
bool supportsToolHistory() const override { return true; }
void prepareRequest(
QJsonObject &request, const PluginLLMCore::ContextData &context) const override
{
@@ -39,6 +42,30 @@ public:
continue;
}
if (!msg.toolCalls.isEmpty()) {
if (!msg.content.isEmpty()) {
input.append(QJsonObject{{"role", "assistant"}, {"content", msg.content}});
}
for (const auto &call : msg.toolCalls) {
input.append(QJsonObject{
{"type", "function_call"},
{"call_id", call.id},
{"name", call.name},
{"arguments",
QString::fromUtf8(
QJsonDocument(call.arguments).toJson(QJsonDocument::Compact))}});
}
continue;
}
if (msg.role == "tool") {
input.append(QJsonObject{
{"type", "function_call_output"},
{"call_id", msg.toolCallId},
{"output", msg.content}});
continue;
}
QJsonObject message;
message["role"] = msg.role;

View File

@@ -0,0 +1,45 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include "pluginllmcore/ContextData.hpp"
namespace QodeAssist::Templates {
inline bool appendOpenAIToolMessage(QJsonArray &messages, const PluginLLMCore::Message &msg)
{
if (!msg.toolCalls.isEmpty()) {
QJsonArray toolCalls;
for (const auto &call : msg.toolCalls) {
toolCalls.append(QJsonObject{
{"id", call.id},
{"type", "function"},
{"function",
QJsonObject{
{"name", call.name},
{"arguments",
QString::fromUtf8(
QJsonDocument(call.arguments).toJson(QJsonDocument::Compact))}}}});
}
QJsonObject toolMessage{{"role", "assistant"}, {"tool_calls", toolCalls}};
toolMessage["content"] = msg.content.isEmpty() ? QJsonValue() : QJsonValue(msg.content);
messages.append(toolMessage);
return true;
}
if (msg.role == QLatin1String("tool")) {
messages.append(QJsonObject{
{"role", "tool"}, {"tool_call_id", msg.toolCallId}, {"content", msg.content}});
return true;
}
return false;
}
} // namespace QodeAssist::Templates