mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-07-20 10:10:56 -04:00
refactor: Restructure project into sources/tests layout and dissolve PluginLLMCore
This commit is contained in:
373
sources/settings/ConfigurationManager.cpp
Normal file
373
sources/settings/ConfigurationManager.cpp
Normal file
@@ -0,0 +1,373 @@
|
||||
// Copyright (C) 2024-2026 Petr Mironychev
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE
|
||||
|
||||
#include "ConfigurationManager.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QUuid>
|
||||
|
||||
#include <coreplugin/icore.h>
|
||||
|
||||
#include "Logger.hpp"
|
||||
#include "ProviderNameMigration.hpp"
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
ConfigurationManager::ConfigurationManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
ConfigurationManager &ConfigurationManager::instance()
|
||||
{
|
||||
static ConfigurationManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
QVector<AIConfiguration> ConfigurationManager::getPredefinedConfigurations(
|
||||
ConfigurationType type)
|
||||
{
|
||||
QVector<AIConfiguration> presets;
|
||||
|
||||
AIConfiguration claudeOpus;
|
||||
claudeOpus.id = "preset_claude_opus";
|
||||
claudeOpus.name = "Claude Opus 4.7";
|
||||
claudeOpus.provider = "Claude";
|
||||
claudeOpus.model = "claude-opus-4-7";
|
||||
claudeOpus.url = "https://api.anthropic.com";
|
||||
claudeOpus.customEndpoint = "";
|
||||
claudeOpus.templateName = "Claude";
|
||||
claudeOpus.type = type;
|
||||
claudeOpus.isPredefined = true;
|
||||
|
||||
AIConfiguration claudeSonnet;
|
||||
claudeSonnet.id = "preset_claude_sonnet";
|
||||
claudeSonnet.name = "Claude Sonnet 4.6";
|
||||
claudeSonnet.provider = "Claude";
|
||||
claudeSonnet.model = "claude-sonnet-4-6";
|
||||
claudeSonnet.url = "https://api.anthropic.com";
|
||||
claudeSonnet.customEndpoint = "";
|
||||
claudeSonnet.templateName = "Claude";
|
||||
claudeSonnet.type = type;
|
||||
claudeSonnet.isPredefined = true;
|
||||
|
||||
AIConfiguration claudeHaiku;
|
||||
claudeHaiku.id = "preset_claude_haiku";
|
||||
claudeHaiku.name = "Claude Haiku 4.5";
|
||||
claudeHaiku.provider = "Claude";
|
||||
claudeHaiku.model = "claude-haiku-4-5-20251001";
|
||||
claudeHaiku.url = "https://api.anthropic.com";
|
||||
claudeHaiku.customEndpoint = "";
|
||||
claudeHaiku.templateName = "Claude";
|
||||
claudeHaiku.type = type;
|
||||
claudeHaiku.isPredefined = true;
|
||||
|
||||
AIConfiguration codestral;
|
||||
codestral.id = "preset_codestral";
|
||||
codestral.name = "Codestral";
|
||||
codestral.provider = "Codestral";
|
||||
codestral.model = "codestral-latest";
|
||||
codestral.url = "https://codestral.mistral.ai";
|
||||
codestral.customEndpoint = "";
|
||||
codestral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
|
||||
codestral.type = type;
|
||||
codestral.isPredefined = true;
|
||||
|
||||
AIConfiguration mistral;
|
||||
mistral.id = "preset_mistral";
|
||||
mistral.name = "Mistral";
|
||||
mistral.provider = "Mistral AI";
|
||||
mistral.model = type == ConfigurationType::CodeCompletion ? "codestral-latest" : "mistral-large-latest";
|
||||
mistral.url = "https://api.mistral.ai";
|
||||
mistral.customEndpoint = "";
|
||||
mistral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
|
||||
mistral.type = type;
|
||||
mistral.isPredefined = true;
|
||||
|
||||
AIConfiguration geminiFlash;
|
||||
geminiFlash.id = "preset_gemini_flash";
|
||||
geminiFlash.name = "Gemini 2.5 Flash";
|
||||
geminiFlash.provider = "Google AI";
|
||||
geminiFlash.model = "gemini-2.5-flash";
|
||||
geminiFlash.url = "https://generativelanguage.googleapis.com/v1beta";
|
||||
geminiFlash.customEndpoint = "";
|
||||
geminiFlash.templateName = "Google AI";
|
||||
geminiFlash.type = type;
|
||||
geminiFlash.isPredefined = true;
|
||||
|
||||
AIConfiguration qwenPlus;
|
||||
qwenPlus.id = "preset_qwen_plus";
|
||||
qwenPlus.name = "Qwen3.6 Plus";
|
||||
qwenPlus.provider = "Qwen (OpenAI Response)";
|
||||
qwenPlus.model = "qwen3.6-plus";
|
||||
qwenPlus.url = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||
qwenPlus.customEndpoint = "";
|
||||
qwenPlus.templateName = "OpenAI Responses";
|
||||
qwenPlus.type = type;
|
||||
qwenPlus.isPredefined = true;
|
||||
|
||||
AIConfiguration qwenMax;
|
||||
qwenMax.id = "preset_qwen_max";
|
||||
qwenMax.name = "Qwen3.7 Max";
|
||||
qwenMax.provider = "Qwen (OpenAI Response)";
|
||||
qwenMax.model = "qwen3.7-max";
|
||||
qwenMax.url = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
||||
qwenMax.customEndpoint = "";
|
||||
qwenMax.templateName = "OpenAI Responses";
|
||||
qwenMax.type = type;
|
||||
qwenMax.isPredefined = true;
|
||||
|
||||
AIConfiguration deepSeekFlash;
|
||||
deepSeekFlash.id = "preset_deepseek_flash";
|
||||
deepSeekFlash.name = "DeepSeek V4 Flash";
|
||||
deepSeekFlash.provider = "DeepSeek";
|
||||
deepSeekFlash.model = "deepseek-v4-flash";
|
||||
deepSeekFlash.url = "https://api.deepseek.com";
|
||||
deepSeekFlash.customEndpoint = "";
|
||||
deepSeekFlash.templateName = "OpenAI Compatible";
|
||||
deepSeekFlash.type = type;
|
||||
deepSeekFlash.isPredefined = true;
|
||||
|
||||
AIConfiguration deepSeekPro;
|
||||
deepSeekPro.id = "preset_deepseek_pro";
|
||||
deepSeekPro.name = "DeepSeek V4 Pro";
|
||||
deepSeekPro.provider = "DeepSeek";
|
||||
deepSeekPro.model = "deepseek-v4-pro";
|
||||
deepSeekPro.url = "https://api.deepseek.com";
|
||||
deepSeekPro.customEndpoint = "";
|
||||
deepSeekPro.templateName = "OpenAI Compatible";
|
||||
deepSeekPro.type = type;
|
||||
deepSeekPro.isPredefined = true;
|
||||
|
||||
AIConfiguration gpt;
|
||||
gpt.id = "preset_gpt";
|
||||
gpt.name = "gpt-5.5";
|
||||
gpt.provider = "OpenAI (Responses API)";
|
||||
gpt.model = "gpt-5.5";
|
||||
gpt.url = "https://api.openai.com/v1";
|
||||
gpt.customEndpoint = "";
|
||||
gpt.templateName = "OpenAI Responses";
|
||||
gpt.type = type;
|
||||
gpt.isPredefined = true;
|
||||
|
||||
presets.append(claudeSonnet);
|
||||
presets.append(claudeHaiku);
|
||||
presets.append(claudeOpus);
|
||||
presets.append(gpt);
|
||||
presets.append(codestral);
|
||||
presets.append(mistral);
|
||||
presets.append(geminiFlash);
|
||||
presets.append(qwenPlus);
|
||||
presets.append(qwenMax);
|
||||
presets.append(deepSeekFlash);
|
||||
presets.append(deepSeekPro);
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
QString ConfigurationManager::configurationTypeToString(ConfigurationType type) const
|
||||
{
|
||||
switch (type) {
|
||||
case ConfigurationType::CodeCompletion:
|
||||
return "code_completion";
|
||||
case ConfigurationType::Chat:
|
||||
return "chat";
|
||||
case ConfigurationType::QuickRefactor:
|
||||
return "quick_refactor";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
QString ConfigurationManager::getConfigurationDirectory(ConfigurationType type) const
|
||||
{
|
||||
QString path = QString("%1/qodeassist/configurations/%2")
|
||||
.arg(Core::ICore::userResourcePath().toFSPathString(),
|
||||
configurationTypeToString(type));
|
||||
return path;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::ensureDirectoryExists(ConfigurationType type) const
|
||||
{
|
||||
QDir dir(getConfigurationDirectory(type));
|
||||
if (!dir.exists()) {
|
||||
return dir.mkpath(".");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::loadConfigurations(ConfigurationType type)
|
||||
{
|
||||
QVector<AIConfiguration> *configs = nullptr;
|
||||
switch (type) {
|
||||
case ConfigurationType::CodeCompletion:
|
||||
configs = &m_ccConfigurations;
|
||||
break;
|
||||
case ConfigurationType::Chat:
|
||||
configs = &m_caConfigurations;
|
||||
break;
|
||||
case ConfigurationType::QuickRefactor:
|
||||
configs = &m_qrConfigurations;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!configs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
configs->clear();
|
||||
|
||||
QVector<AIConfiguration> predefinedConfigs = getPredefinedConfigurations(type);
|
||||
configs->append(predefinedConfigs);
|
||||
|
||||
if (!ensureDirectoryExists(type)) {
|
||||
LOG_MESSAGE("Failed to create configuration directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(getConfigurationDirectory(type));
|
||||
QStringList filters;
|
||||
filters << "*.json";
|
||||
QFileInfoList files = dir.entryInfoList(filters, QDir::Files);
|
||||
|
||||
for (const QFileInfo &fileInfo : files) {
|
||||
QFile file(fileInfo.absoluteFilePath());
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to open configuration file: %1").arg(fileInfo.fileName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
|
||||
file.close();
|
||||
|
||||
if (!doc.isObject()) {
|
||||
LOG_MESSAGE(QString("Invalid configuration file: %1").arg(fileInfo.fileName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
AIConfiguration config;
|
||||
config.id = obj["id"].toString();
|
||||
config.name = obj["name"].toString();
|
||||
config.provider = migrateProviderName(obj["provider"].toString());
|
||||
config.model = obj["model"].toString();
|
||||
config.templateName = obj["template"].toString();
|
||||
config.url = obj["url"].toString();
|
||||
config.customEndpoint = obj["customEndpoint"].toString();
|
||||
config.type = type;
|
||||
config.formatVersion = obj.value("formatVersion").toInt(1);
|
||||
|
||||
config.isPredefined = false;
|
||||
|
||||
if (config.id.isEmpty() || config.name.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Invalid configuration data in file: %1").arg(fileInfo.fileName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
configs->append(config);
|
||||
}
|
||||
|
||||
emit configurationsChanged(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::saveConfiguration(const AIConfiguration &config)
|
||||
{
|
||||
if (!ensureDirectoryExists(config.type)) {
|
||||
LOG_MESSAGE("Failed to create configuration directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject obj;
|
||||
obj["formatVersion"] = config.formatVersion;
|
||||
obj["id"] = config.id;
|
||||
obj["name"] = config.name;
|
||||
obj["provider"] = config.provider;
|
||||
obj["model"] = config.model;
|
||||
obj["template"] = config.templateName;
|
||||
obj["url"] = config.url;
|
||||
obj["customEndpoint"] = config.customEndpoint;
|
||||
|
||||
QString sanitizedName = config.name;
|
||||
sanitizedName.replace(" ", "_");
|
||||
sanitizedName.replace(QRegularExpression("[^a-zA-Z0-9_-]"), "");
|
||||
|
||||
QString fileName = QString("%1/%2_%3.json")
|
||||
.arg(getConfigurationDirectory(config.type), sanitizedName, config.id);
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to create configuration file: %1").arg(fileName));
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonDocument doc(obj);
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
|
||||
loadConfigurations(config.type);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigurationManager::deleteConfiguration(const QString &id, ConfigurationType type)
|
||||
{
|
||||
AIConfiguration config = getConfigurationById(id, type);
|
||||
if (config.isPredefined) {
|
||||
LOG_MESSAGE(QString("Cannot delete predefined configuration: %1").arg(id));
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(getConfigurationDirectory(type));
|
||||
QStringList filters;
|
||||
filters << QString("*_%1.json").arg(id);
|
||||
QFileInfoList files = dir.entryInfoList(filters, QDir::Files);
|
||||
|
||||
if (files.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Configuration file not found for id: %1").arg(id));
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const QFileInfo &fileInfo : files) {
|
||||
QFile file(fileInfo.absoluteFilePath());
|
||||
if (!file.remove()) {
|
||||
LOG_MESSAGE(QString("Failed to delete configuration file: %1")
|
||||
.arg(fileInfo.absoluteFilePath()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
loadConfigurations(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<AIConfiguration> ConfigurationManager::configurations(ConfigurationType type) const
|
||||
{
|
||||
switch (type) {
|
||||
case ConfigurationType::CodeCompletion:
|
||||
return m_ccConfigurations;
|
||||
case ConfigurationType::Chat:
|
||||
return m_caConfigurations;
|
||||
case ConfigurationType::QuickRefactor:
|
||||
return m_qrConfigurations;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
AIConfiguration ConfigurationManager::getConfigurationById(const QString &id,
|
||||
ConfigurationType type) const
|
||||
{
|
||||
const QVector<AIConfiguration> &configs = configurations(type);
|
||||
for (const AIConfiguration &config : configs) {
|
||||
if (config.id == id) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
return AIConfiguration();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
Reference in New Issue
Block a user