feat(app): ✨ adding Preferences/Wingman for ollama, OpenRouter and OpenAI#1558
feat(app): ✨ adding Preferences/Wingman for ollama, OpenRouter and OpenAI#1558dvorka wants to merge 29 commits into
Conversation
🤖 Augment PR SummarySummary: Adds a new “Wingman2” Preferences tab for managing multiple LLM provider profiles (OpenAI/ollama).
🤖 Was this summary useful? React with 👍 or 👎 |
|
|
||
| #include <QtWidgets> | ||
|
|
||
| #include "../../lib/src/config/configuration.h" |
There was a problem hiding this comment.
The relative include ../../lib/src/config/configuration.h resolves to app/src/lib/... from this header, which doesn’t exist in the repo, so these new dialogs likely won’t compile. Same issue also appears in the OpenAI/ollama config dialog headers.
Severity: high
Other Locations
app/src/qt/dialogs/openai_config_dialog.h:25app/src/qt/dialogs/openai_config_dialog.h:26app/src/qt/dialogs/ollama_config_dialog.h:25app/src/qt/dialogs/ollama_config_dialog.h:26
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| QPushButton* addProviderButton; | ||
|
|
||
| QGroupBox* providerDetailsGroup; | ||
| QLabel* providerTypeLabel; |
There was a problem hiding this comment.
Wingman2Tab declares providerTypeLabel/modelLabel/statusLabel members, but the .cpp constructs local QLabel*s instead; these members remain uninitialized. This is easy to accidentally dereference later and crash.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| : QWidget(parent), | ||
| config(Configuration::getInstance()) | ||
| { | ||
| helpLabel = new QLabel( |
There was a problem hiding this comment.
The help text says Wingman2 config is “to be used by Wingman”, but the current runtime initialization path still appears to read only the legacy wingmanProvider/wingmanOpenAi*/wingmanOllama* fields. This may make the new tab misleading if Wingman doesn’t actually consume llmProviders yet.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| // populate providers combo | ||
| llmProvidersCombo->clear(); | ||
|
|
||
| vector<LlmProviderConfig>& providers = config.getLlmProviders(); |
There was a problem hiding this comment.
Wingman2Tab::refresh() populates from config.getLlmProviders() directly; since migrateFromLegacyWingmanConfig() isn’t invoked here, existing Wingman users may see an empty provider list even if legacy config is set.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| if (providerType == WINGMAN_PROVIDER_OPENAI) { | ||
| OpenAiConfigDialog configDialog(this); | ||
| if (configDialog.exec() == QDialog::Accepted) { | ||
| config.addLlmProvider(configDialog.getProviderConfig()); |
There was a problem hiding this comment.
Providers added via config.addLlmProvider() look to be stored only in-memory; MarkdownConfigurationRepresentation currently persists only the legacy Wingman fields. That means Wingman2 providers/activeLlmProviderId may be lost after restart.
Severity: high
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Pull request overview
Adds a new “Wingman2” preferences workflow to manage multiple LLM provider configurations (OpenAI + ollama), alongside groundwork in Configuration for storing providers, plus improvements to model discovery and a small ollama bug fix.
Changes:
- Introduces Wingman2 UI (tab + dialogs) to add/configure LLM providers and select an active provider.
- Extends
Configurationwith aLlmProviderConfigmodel and basic CRUD/probe/migration APIs. - Enhances OpenAI model listing via
/v1/modelsand fixes an ollama model-list parsing bug.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 22 comments.
Show a summary per file
| File | Description |
|---|---|
| vibe/designs/wingman2-llm-configuration.md | Design spec for Wingman2 provider configuration UX, storage, and probing. |
| lib/src/mind/ai/llm/openai_wingman.h | Adds declaration for HTTP model listing helper. |
| lib/src/mind/ai/llm/openai_wingman.cpp | Implements OpenAI /v1/models model fetching and fallback defaults. |
| lib/src/mind/ai/llm/ollama_wingman.cpp | Fixes model name push bug in model listing. |
| lib/src/config/configuration.h | Adds Wingman2 provider config struct, defaults, and new public APIs. |
| lib/src/config/configuration.cpp | Implements provider CRUD, active selection, probe stubs, and migration helper. |
| app/src/qt/dialogs/configuration_dialog.h | Adds Wingman2Tab declaration and members. |
| app/src/qt/dialogs/configuration_dialog.cpp | Adds Wingman2 tab UI/handlers and wires it into Preferences show/save. |
| app/src/qt/dialogs/openai_config_dialog.{h,cpp} | New OpenAI provider dialog (API key + model + refresh/probe/add). |
| app/src/qt/dialogs/ollama_config_dialog.{h,cpp} | New ollama provider dialog (URL + model + refresh/probe/add). |
| app/src/qt/dialogs/add_llm_provider_dialog.{h,cpp} | New dialog to choose provider type before configuring. |
| app/app.pro | Registers the new dialog sources/headers with qmake. |
| lib/src/app_info.h | Updates legal copyright year range. |
| build/ubuntu/debian/copyright | Updates Debian packaging copyright year range. |
| build/debian/debian/copyright | Updates Debian packaging copyright year range. |
| .github/copilot-instructions.md | Adds repository-specific Copilot guidance. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| MF_DEBUG( | ||
| "OpenAiWingman::listModelsHttpGet() parsed response:" << endl | ||
| << ">>>" | ||
| << httpResponseJson.dump(4) | ||
| << "<<<" | ||
| << endl); |
There was a problem hiding this comment.
listModelsHttpGet() logs the full parsed JSON response (httpResponseJson.dump(4)). The /v1/models payload can be large, which may bloat logs and add noticeable overhead. Consider logging only a summary (e.g., model count / first N IDs) or gating the full dump behind a more verbose debug flag.
| MF_DEBUG( | |
| "OpenAiWingman::listModelsHttpGet() parsed response:" << endl | |
| << ">>>" | |
| << httpResponseJson.dump(4) | |
| << "<<<" | |
| << endl); | |
| std::size_t modelsCount = 0; | |
| if (httpResponseJson.contains("data") && httpResponseJson["data"].is_array()) { | |
| modelsCount = httpResponseJson["data"].size(); | |
| } | |
| MF_DEBUG( | |
| "OpenAiWingman::listModelsHttpGet() parsed response: " | |
| << "data array size = " << modelsCount << endl); |
| AddLlmProviderDialog(const AddLlmProviderDialog&&) = delete; | ||
| AddLlmProviderDialog& operator=(const AddLlmProviderDialog&) = delete; | ||
| AddLlmProviderDialog& operator=(const AddLlmProviderDialog&&) = delete; |
There was a problem hiding this comment.
AddLlmProviderDialog also declares deleted move operations as const AddLlmProviderDialog&& / operator=(const AddLlmProviderDialog&&). This isn’t the normal move signature and can be misleading; use AddLlmProviderDialog&& (or omit move deletions if not needed).
| AddLlmProviderDialog(const AddLlmProviderDialog&&) = delete; | |
| AddLlmProviderDialog& operator=(const AddLlmProviderDialog&) = delete; | |
| AddLlmProviderDialog& operator=(const AddLlmProviderDialog&&) = delete; | |
| AddLlmProviderDialog(AddLlmProviderDialog&&) = delete; | |
| AddLlmProviderDialog& operator=(const AddLlmProviderDialog&) = delete; | |
| AddLlmProviderDialog& operator=(AddLlmProviderDialog&&) = delete; |
| string errorMessage; | ||
| bool success = false; | ||
|
|
||
| if (provider->providerType == WINGMAN_PROVIDER_OPENAI) { | ||
| success = config.probeOpenAiProvider( | ||
| provider->apiKey, provider->llmModel, errorMessage); | ||
| } else if (provider->providerType == WINGMAN_PROVIDER_OLLAMA) { | ||
| success = config.probeOllamaProvider( | ||
| provider->url, provider->llmModel, errorMessage); | ||
| } | ||
|
|
||
| if (success) { | ||
| QMessageBox::information( | ||
| this, | ||
| tr("Connection Test"), | ||
| tr("Provider configuration is valid.")); | ||
| } else { | ||
| QMessageBox::critical( | ||
| this, | ||
| tr("Connection Test"), | ||
| tr("Provider configuration test failed: %1") | ||
| .arg(QString::fromStdString(errorMessage))); | ||
| } |
There was a problem hiding this comment.
handleTestConnection() reports success/failure but never updates provider->isValid, so the status in the “Selected Provider Details” panel won’t reflect the latest test result. Consider setting provider->isValid = success (and persisting via updateLlmProvider if needed) and then refreshing the UI.
| // generate unique ID using timestamp | ||
| auto now = chrono::system_clock::now(); | ||
| auto timestamp = chrono::duration_cast<chrono::seconds>(now.time_since_epoch()).count(); | ||
|
|
||
| // extract host from URL for display name | ||
| string host = url; | ||
| size_t pos = url.find("://"); | ||
| if (pos != string::npos) { | ||
| host = url.substr(pos + 3); | ||
| } | ||
|
|
||
| providerConfig.id = "ollama-" + to_string(timestamp); | ||
| providerConfig.displayName = "ollama " + model + " @ " + host; | ||
| providerConfig.providerType = WINGMAN_PROVIDER_OLLAMA; | ||
| providerConfig.url = url; | ||
| providerConfig.llmModel = model; | ||
| providerConfig.isValid = configValid; |
There was a problem hiding this comment.
Provider IDs are generated using seconds-resolution timestamps (e.g., ollama-<seconds>), which can collide if the user adds providers quickly. Since IDs are used as stable keys for update/remove/active selection, consider generating IDs with higher entropy (ms + random/UUID) or checking for collisions and regenerating.
| return getLlmProviderById(activeLlmProviderId); | ||
| } | ||
|
|
||
| void Configuration::addLlmProvider(const LlmProviderConfig& provider) { |
There was a problem hiding this comment.
addLlmProvider() blindly appends the provider without enforcing unique IDs (despite id being documented as unique). This can create duplicate IDs (e.g., two providers added within the same second using timestamp IDs) and then getLlmProviderById()/setActiveLlmProvider() become ambiguous. Consider rejecting duplicates (and surfacing an error) or auto-renaming/regenerating the ID on collision.
| void Configuration::addLlmProvider(const LlmProviderConfig& provider) { | |
| void Configuration::addLlmProvider(const LlmProviderConfig& provider) { | |
| // Enforce unique provider IDs to avoid ambiguous lookups. | |
| if (!provider.id.empty() && getLlmProviderById(provider.id) != nullptr) { | |
| MF_DEBUG("Configuration::addLlmProvider() duplicate ID, provider not added: " << provider.id << endl); | |
| return; | |
| } |
| MF_DEBUG("Configuration::removeLlmProvider() removed: " << id << endl); | ||
| } | ||
|
|
||
| void Configuration::setActiveLlmProvider(const string& id) { |
There was a problem hiding this comment.
setActiveLlmProvider() sets activeLlmProviderId without verifying the ID exists in llmProviders. This can leave the config pointing at a non-existent provider (e.g., after update/remove sequences or malformed persisted data). Consider validating the ID (no-op with error/log, or clear active selection) to keep getActiveLlmProvider() consistent.
| void Configuration::setActiveLlmProvider(const string& id) { | |
| void Configuration::setActiveLlmProvider(const string& id) { | |
| // Only set active provider if the ID is known; keep configuration consistent. | |
| if (!id.empty() && getLlmProviderById(id) == nullptr) { | |
| MF_DEBUG("Configuration::setActiveLlmProvider() attempted to set unknown provider id: " << id << endl); | |
| return; | |
| } |
- Remove legacy per-provider config fields (wingmanOpenAiApiKey, etc.) and replace with std::vector<LlmProviderConfig> + activeLlmProviderId - OpenAiWingman/OpenRouterWingman now take explicit apiKey in constructor - Configuration dialog consolidated from 4 Wingman tabs to 1 WingmanTab - Config serializer/parser updated with backward-compatible migration - mind.cpp initWingman()/getWingman() updated to use new provider API - openai/ollama/openrouter config dialogs fixed to use new API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 28 changed files in this pull request and generated 17 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } else { | ||
| // check whether possible | ||
| if(config.canWingmanOllama()) { | ||
| if(!config.getLlmProviders().empty()) { |
| // generate unique ID using timestamp | ||
| auto now = chrono::system_clock::now(); | ||
| auto timestamp = chrono::duration_cast<chrono::seconds>(now.time_since_epoch()).count(); | ||
| providerConfig.id = "openai-" + to_string(timestamp); |
| // generate unique ID using timestamp | ||
| auto now = chrono::system_clock::now(); | ||
| auto timestamp = chrono::duration_cast<chrono::seconds>(now.time_since_epoch()).count(); | ||
|
|
||
| // extract host from URL for display name | ||
| string host = url; | ||
| size_t pos = url.find("://"); | ||
| if (pos != string::npos) { | ||
| host = url.substr(pos + 3); | ||
| } | ||
|
|
||
| providerConfig.id = "ollama-" + to_string(timestamp); |
| // generate unique ID using timestamp | ||
| auto now = chrono::system_clock::now(); | ||
| auto timestamp = chrono::duration_cast<chrono::seconds>(now.time_since_epoch()).count(); | ||
| providerConfig.id = "openrouter-" + to_string(timestamp); |
| string effectiveKey = apiKey.empty() ? std::string(envKey) : apiKey; | ||
| try { | ||
| OpenAiWingman wingman{effectiveKey}; | ||
| vector<string>& models = wingman.listModels(); |
| // parse JSon response (OpenAI-compatible format) | ||
| nlohmann::json httpResponseJSon; | ||
| try { | ||
| httpResponseJSon = nlohmann::json::parse(command.httpResponse); | ||
| } catch (...) { | ||
| MF_DEBUG( | ||
| "Error: unable to parse OpenRouter JSon response:" << endl << | ||
| "'" << command.httpResponse << "'" << endl | ||
| ); | ||
|
|
||
| command.status = WingmanStatusCode::WINGMAN_STATUS_CODE_ERROR; | ||
| command.errorMessage = "Error: unable to parse OpenRouter JSon response: '" + command.httpResponse + "'"; | ||
| command.answerMarkdown.clear(); | ||
| command.answerTokens = 0; | ||
| command.answerLlmModel = llmModel; | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| MF_DEBUG( | ||
| "OpenRouterWingman::curlGet() parsed response:" << endl | ||
| << ">>>" | ||
| << httpResponseJSon.dump(4) | ||
| << "<<<" | ||
| << endl); | ||
|
|
||
| MF_DEBUG("OpenRouterWingman::curlGet() fields:" << endl); | ||
| if(httpResponseJSon.contains("model")) { | ||
| httpResponseJSon["model"].get_to(command.answerLlmModel); | ||
| MF_DEBUG(" model: " << command.answerLlmModel << endl); | ||
| } | ||
| if(httpResponseJSon.contains("usage")) { | ||
| if(httpResponseJSon["usage"].contains("prompt_tokens")) { | ||
| httpResponseJSon["usage"]["prompt_tokens"].get_to(command.promptTokens); | ||
| MF_DEBUG(" prompt_tokens: " << command.promptTokens << endl); | ||
| } | ||
| if(httpResponseJSon["usage"].contains("completion_tokens")) { | ||
| httpResponseJSon["usage"]["completion_tokens"].get_to(command.answerTokens); | ||
| MF_DEBUG(" answer_tokens: " << command.answerTokens << endl); | ||
| } | ||
| } | ||
| if(httpResponseJSon.contains("choices") | ||
| && httpResponseJSon["choices"].size() > 0 | ||
| ) { | ||
| auto choice = httpResponseJSon["choices"][0]; | ||
| if(choice.contains("message") | ||
| && choice["message"].contains("content") | ||
| ) { | ||
| choice["message"]["content"].get_to(command.answerMarkdown); | ||
| m8r::replaceAll( | ||
| "\n", | ||
| "<br/>", | ||
| command.answerMarkdown); | ||
| MF_DEBUG(" answer (HTML): " << command.answerMarkdown << endl); | ||
| } | ||
| if(choice.contains("finish_reason")) { | ||
| string statusStr{}; | ||
| choice["finish_reason"].get_to(statusStr); | ||
| if(statusStr == "stop") { | ||
| command.status = m8r::WingmanStatusCode::WINGMAN_STATUS_CODE_OK; | ||
| } else { | ||
| command.status = m8r::WingmanStatusCode::WINGMAN_STATUS_CODE_ERROR; | ||
| command.errorMessage.assign( | ||
| "OpenRouter API HTTP request failed with finish_reason: " | ||
| + statusStr); | ||
| command.answerMarkdown.clear(); | ||
| command.answerTokens = 0; | ||
| command.answerLlmModel = llmModel; | ||
| } | ||
| MF_DEBUG(" status: " << command.status << endl); | ||
| } | ||
| } else { | ||
| command.status = m8r::WingmanStatusCode::WINGMAN_STATUS_CODE_ERROR; | ||
| command.answerMarkdown.clear(); | ||
| command.answerTokens = 0; | ||
| command.answerLlmModel = llmModel; | ||
| if( | ||
| httpResponseJSon.contains("error") | ||
| && httpResponseJSon["error"].contains("message") | ||
| ) { | ||
| httpResponseJSon["error"]["message"].get_to(command.errorMessage); | ||
| } else { | ||
| command.errorMessage.assign( | ||
| "No choices in the OpenRouter API HTTP response"); | ||
| } | ||
| } |
| "OpenRouterWingman::OpenRouterWingman() apiKey: '" | ||
| << this->apiKey << "'" << endl); |
| MF_DEBUG( | ||
| "OpenAiWingman::OpenAiWingman() apiKey: '" | ||
| << config.getWingmanOpenAiApiKey() << "'" << endl); | ||
| << this->apiKey << "'" << endl); |
| QMessageBox::information( | ||
| this, | ||
| tr("Models Refreshed"), | ||
| tr("Successfully fetched %1 models from OpenAI API.").arg(models.size())); |
…r Qt5+WebEngine macos-latest now maps to macOS 15 on Apple Silicon (ARM64) where Homebrew's qt@5 no longer includes QtWebEngine. Qt5 official builds with WebEngine only exist for x86_64, so switch to: - macos-13 (Intel x86_64) runner - jurplel/install-qt-action@v4 for Qt 5.15.2 with qtwebengine module - keep ccache via brew, remove the now-redundant qt@5 brew install Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
moc processes #ifdef guards with its own limited preprocessor pass. When DO_MF_DEBUG is defined, moc includes doActionMindHack() in the meta-object table unconditionally, but the #ifdef in the .cpp caused the function body to be absent in non-debug builds, yielding: undefined reference to MainWindowPresenter::doActionMindHack() Fix: always declare and define the slot; keep debug logic inside the body under #ifdef DO_MF_DEBUG so both moc and the linker are satisfied regardless of build configuration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two fixes: 1. doActionMindHack - restore proper #ifdef DO_MF_DEBUG guards around the slot declaration and implementation so the entire hook is compiled out in production builds (build-rc). All related code in main_menu_view.h/.cpp and main_menu_presenter.cpp was already correctly guarded; the slot itself is now also fully excluded. 2. actionMindSemanticSearch - remove incorrect #ifdef DO_MF_DEBUG guard from its field declaration in main_menu_view.h. This is a real production feature (Wingman semantic search), not a debug hook; the guard caused a build failure when DO_MF_DEBUG is not defined (e.g. build-rc). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 31 changed files in this pull request and generated 10 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } else { | ||
| // check whether possible | ||
| if(config.canWingmanOllama()) { | ||
| if(!config.getLlmProviders().empty()) { |
| if (configDialog.exec() == QDialog::Accepted) { | ||
| config.addLlmProvider(configDialog.getProviderConfig()); | ||
| refresh(); |
|
|
||
| return false; | ||
| bool Configuration::isWingman() { | ||
| return !llmProviders.empty() && !activeLlmProviderId.empty(); |
| bool Configuration::isWingman() { | ||
| return !llmProviders.empty() && !activeLlmProviderId.empty(); |
| // TODO: actually test the connection by calling OpenAI API | ||
| // For now, just validate the inputs | ||
| MF_DEBUG("Configuration::probeOpenAiProvider() validated: " << model << endl); |
| // TODO: actually test the connection by calling ollama API | ||
| // For now, just validate the inputs | ||
| MF_DEBUG("Configuration::probeOllamaProvider() validated: " << url << ", " << model << endl); |
| */ | ||
| bool Configuration::isWingman() { | ||
| return WingmanLlmProviders::WINGMAN_PROVIDER_NONE==wingmanProvider?false:true; | ||
| MF_DEBUG("Configuration::probeOpenRouterProvider() validated: " << model << endl); |
| "OpenRouterWingman::OpenRouterWingman() apiKey: '" | ||
| << this->apiKey << "'" << endl); |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| string answerDescriptor{ | ||
| "[provider: " + config.getWingmanLlmProviderAsString(config.getWingmanLlmProvider()) + | ||
| "[provider: " + config.getLlmProviderById(config.getActiveLlmProviderId())->displayName + | ||
| ", model: " + commandWingmanChat.answerLlmModel + |
|
|
||
| return false; | ||
| bool Configuration::isWingman() { | ||
| return !llmProviders.empty() && !activeLlmProviderId.empty(); |
| string effectiveKey = provider->apiKey; | ||
| if(provider->useEnvVar) { | ||
| const char* envKey = std::getenv(ENV_VAR_OPENAI_API_KEY); | ||
| if(envKey) { | ||
| effectiveKey = string(envKey); | ||
| MF_DEBUG(" MIND Wingman OpenAI: using env var key" << endl); | ||
| } | ||
| } | ||
| wingman = (Wingman*)new OpenAiWingman{effectiveKey}; | ||
| wingman->setLlmModel(provider->llmModel); |
| string effectiveKey = provider->apiKey; | ||
| if(provider->useEnvVar) { | ||
| const char* envKey = std::getenv(ENV_VAR_OPENROUTER_API_KEY); | ||
| if(envKey) { | ||
| effectiveKey = string(envKey); | ||
| MF_DEBUG(" MIND Wingman OpenRouter: using env var key" << endl); | ||
| } | ||
| } | ||
| wingman = (Wingman*)new OpenRouterWingman{effectiveKey}; | ||
| wingman->setLlmModel(provider->llmModel); |
| /** | ||
| * @brief Get embeddings (not supported by OpenRouter Wingman). | ||
| */ | ||
| virtual void embeddings(CommandWingmanEmbeddings& command) override { | ||
| UNUSED_ARG(command); | ||
| throw std::runtime_error("OpenRouter Wingman does not support embeddings"); | ||
| } |
| string errorMessage; | ||
| bool success = false; | ||
|
|
||
| if (provider->providerType == WINGMAN_PROVIDER_OPENAI) { | ||
| success = config.probeOpenAiProvider( | ||
| provider->apiKey, provider->llmModel, errorMessage); | ||
| } else if (provider->providerType == WINGMAN_PROVIDER_OLLAMA) { | ||
| success = config.probeOllamaProvider( | ||
| provider->url, provider->llmModel, errorMessage); | ||
| } else if (provider->providerType == WINGMAN_PROVIDER_OPENROUTER) { | ||
| success = config.probeOpenRouterProvider( | ||
| provider->apiKey, provider->llmModel, errorMessage); | ||
| } | ||
|
|
||
| /* | ||
| * Navigator tab | ||
| */ | ||
| if (success) { | ||
| provider->isValid = true; | ||
| statusValue->setText(tr("configured")); | ||
| statusValue->setStyleSheet("QLabel { color: green; }"); | ||
| QMessageBox::information( | ||
| this, | ||
| tr("Connection Test"), | ||
| tr("Provider configuration is valid.")); | ||
| } else { |
| s << CONFIG_SETTING_MIND_WINGMAN_PROVIDER_ITEM | ||
| << p.id << "|" | ||
| << p.displayName << "|" | ||
| << Configuration::getWingmanLlmProviderAsString(p.providerType) << "|" | ||
| << p.url << "|" | ||
| << p.apiKey << "|" | ||
| << p.llmModel << "|" | ||
| << (p.isValid ? "1" : "0") << "|" | ||
| << (p.useEnvVar ? "1" : "0") << endl; |
| // format: id|displayName|typeStr|url|apiKey|llmModel|isValid | ||
| std::vector<std::string> parts; |
| /** | ||
| * @brief LLM Provider Configuration | ||
| * | ||
| * Represents configuration for a single Large Language Model provider. | ||
| * Supports OpenAI and ollama providers with provider-specific fields. | ||
| */ | ||
| struct LlmProviderConfig { | ||
| std::string id; // unique identifier (e.g., "openai-1", "ollama-local") | ||
| std::string displayName; // user-friendly name (e.g., "OpenAI GPT-4", "Local Ollama") | ||
| WingmanLlmProviders providerType; // WINGMAN_PROVIDER_OPENAI, WINGMAN_PROVIDER_OLLAMA | ||
| std::string url; // for ollama: base URL, for OpenAI: empty | ||
| std::string apiKey; // for OpenAI/OpenRouter: stored API key (empty when useEnvVar is true) | ||
| std::string llmModel; // model name (e.g., "gpt-4", "llama2") | ||
| bool isValid; // whether configuration was validated/probed | ||
| bool useEnvVar; // if true, resolve API key from environment variable at runtime | ||
|
|
| } else if(line->find(CONFIG_SETTING_MIND_ACTIVE_WINGMAN_PROVIDER) != std::string::npos) { | ||
| string id = line->substr(strlen(CONFIG_SETTING_MIND_ACTIVE_WINGMAN_PROVIDER)); | ||
| c.setActiveLlmProvider(id); | ||
| } else if(line->find(CONFIG_SETTING_MIND_WINGMAN_PROVIDER_ITEM) != std::string::npos) { | ||
| string v = line->substr(strlen(CONFIG_SETTING_MIND_WINGMAN_PROVIDER_ITEM)); | ||
| // format: id|displayName|typeStr|url|apiKey|llmModel|isValid | ||
| std::vector<std::string> parts; | ||
| std::stringstream ss(v); | ||
| std::string part; | ||
| while(std::getline(ss, part, '|')) { | ||
| parts.push_back(part); | ||
| } | ||
| if(parts.size() >= 7) { | ||
| LlmProviderConfig p; | ||
| p.id = parts[0]; | ||
| p.displayName = parts[1]; | ||
| p.url = parts[3]; | ||
| p.apiKey = parts[4]; | ||
| p.llmModel = parts[5]; | ||
| p.isValid = (parts[6] == "1"); | ||
| p.useEnvVar = (parts.size() >= 8 && parts[7] == "1"); | ||
| string typeStr = parts[2]; | ||
| if(typeStr == Configuration::getWingmanLlmProviderAsString(WingmanLlmProviders::WINGMAN_PROVIDER_OPENAI)) { | ||
| p.providerType = WingmanLlmProviders::WINGMAN_PROVIDER_OPENAI; | ||
| } else if(typeStr == Configuration::getWingmanLlmProviderAsString(WingmanLlmProviders::WINGMAN_PROVIDER_OLLAMA)) { | ||
| p.providerType = WingmanLlmProviders::WINGMAN_PROVIDER_OLLAMA; | ||
| } else if(typeStr == Configuration::getWingmanLlmProviderAsString(WingmanLlmProviders::WINGMAN_PROVIDER_OPENROUTER)) { | ||
| p.providerType = WingmanLlmProviders::WINGMAN_PROVIDER_OPENROUTER; | ||
| } else { | ||
| p.providerType = WingmanLlmProviders::WINGMAN_PROVIDER_NONE; | ||
| } | ||
| c.addLlmProvider(p); | ||
| } | ||
| } |
…a/mindforger into enh-1539/ollama-llm-choice-ii-vibe
Tasks:
gtestbased testsThis PR brings:
ollamasupport & configuration.OpenRoutersupport & configuration.OpenAIconfiguration.Related: