8#include <unordered_set>
10#include "absl/strings/ascii.h"
11#include "absl/strings/match.h"
12#include "absl/time/clock.h"
13#include "absl/time/time.h"
36std::filesystem::path ExpandUserPath(
const std::string& input) {
40 if (input.front() !=
'~') {
41 return std::filesystem::path(input);
44 if (home_dir.empty() || home_dir ==
".") {
45 return std::filesystem::path(input);
47 if (input.size() == 1) {
50 if (input[1] ==
'/' || input[1] ==
'\\') {
51 return home_dir / input.substr(2);
53 return home_dir / input.substr(1);
57 const std::string ext = absl::AsciiStrToLower(path.extension().string());
58 return ext ==
".gguf" || ext ==
".ggml" || ext ==
".bin" ||
59 ext ==
".safetensors";
63 std::vector<std::string>* output,
64 std::unordered_set<std::string>* seen) {
65 if (!output || !seen || name.empty()) {
68 if (output->size() >= 512) {
71 if (seen->insert(name).second) {
72 output->push_back(name);
77 if (path.filename() !=
"models") {
80 return path.parent_path().filename() ==
".ollama";
84 std::vector<std::string>* output,
85 std::unordered_set<std::string>* seen) {
86 if (!output || !seen) {
90 const auto library_path =
91 models_root /
"manifests" /
"registry.ollama.ai" /
"library";
92 if (!std::filesystem::exists(library_path, ec)) {
95 std::filesystem::directory_options options =
96 std::filesystem::directory_options::skip_permission_denied;
97 for (std::filesystem::recursive_directory_iterator
98 it(library_path, options, ec),
100 it != end; it.increment(ec)) {
105 if (!it->is_regular_file(ec)) {
108 const auto rel = it->path().lexically_relative(library_path);
112 std::vector<std::string> parts;
113 for (
const auto& part : rel) {
115 parts.push_back(part.string());
121 std::string model = parts.front();
123 for (
size_t i = 1; i < parts.size(); ++i) {
129 const std::string name = tag.empty() ? model : model +
":" + tag;
131 if (output->size() >= 512) {
138 std::vector<std::string>* output,
139 std::unordered_set<std::string>* seen) {
140 if (!output || !seen) {
144 if (!std::filesystem::exists(base_path, ec)) {
147 std::filesystem::directory_options options =
148 std::filesystem::directory_options::skip_permission_denied;
149 constexpr int kMaxDepth = 4;
150 for (std::filesystem::recursive_directory_iterator it(base_path, options, ec),
152 it != end; it.increment(ec)) {
157 if (it->is_directory(ec)) {
158 if (it.depth() >= kMaxDepth) {
159 it.disable_recursion_pending();
163 if (!it->is_regular_file(ec)) {
169 std::filesystem::path rel = it->path().lexically_relative(base_path);
171 rel = it->path().filename();
173 rel.replace_extension();
175 if (output->size() >= 512) {
183 std::vector<std::string> results;
187 std::unordered_set<std::string> seen;
189 auto expanded = ExpandUserPath(raw_path);
190 if (expanded.empty()) {
194 if (!std::filesystem::exists(expanded, ec)) {
197 if (std::filesystem::is_regular_file(expanded, ec)) {
198 std::filesystem::path rel = expanded.filename();
199 rel.replace_extension();
203 if (!std::filesystem::is_directory(expanded, ec)) {
212 std::sort(results.begin(), results.end());
216bool ContainsText(
const std::string& haystack,
const std::string& needle) {
217 return haystack.find(needle) != std::string::npos;
221 return text.rfind(prefix, 0) == 0;
225 if (base_url.empty()) {
228 std::string lower = absl::AsciiStrToLower(base_url);
233 bool allow_insecure) {
234 if (allow_insecure) {
240 if (base_url.empty()) {
243 std::string lower = absl::AsciiStrToLower(base_url);
250#ifndef __EMSCRIPTEN__
252 if (base_url.empty()) {
255 httplib::Client client(base_url);
256 client.set_connection_timeout(0, 200000);
257 client.set_read_timeout(0, 250000);
258 client.set_write_timeout(0, 250000);
259 client.set_follow_location(
true);
260 auto response = client.Get(path);
264 return response->status > 0 && response->status < 500;
275bool ProbeOllamaHost(
const std::string&) {
279bool ProbeOpenAICompatible(
const std::string&) {
291 const auto& prefs = settings->
prefs();
292 if (prefs.ai_hosts.empty() && prefs.ai_profiles.empty()) {
295 bool applied =
false;
304 if (!prefs.ai_hosts.empty()) {
306 ? prefs.ai_hosts.front().id
307 : prefs.active_ai_host_id;
308 if (!active_id.empty()) {
309 for (
const auto& host : prefs.ai_hosts) {
310 if (host.id == active_id) {
318 if (!prefs.ai_profiles.empty()) {
320 if (!prefs.active_ai_profile.empty()) {
321 for (
const auto& profile : prefs.ai_profiles) {
322 if (profile.name == prefs.active_ai_profile) {
323 active_profile = &profile;
328 if (!active_profile) {
329 active_profile = &prefs.ai_profiles.front();
332 if (!active_profile->
model.empty()) {
342 !prefs.openai_api_key.empty()) {
347 !prefs.gemini_api_key.empty()) {
352 !prefs.anthropic_api_key.empty()) {
369 bool profile_updated =
false;
370 auto env_value = [](
const char* key) -> std::string {
371 const char* value = std::getenv(key);
372 return value ? std::string(value) : std::string();
375 std::string env_openai_base = env_value(
"OPENAI_BASE_URL");
376 if (env_openai_base.empty()) {
377 env_openai_base = env_value(
"OPENAI_API_BASE");
379 std::string env_openai_model = env_value(
"OPENAI_MODEL");
380 std::string env_ollama_host = env_value(
"OLLAMA_HOST");
381 std::string env_ollama_model = env_value(
"OLLAMA_MODEL");
382 std::string env_gemini_model = env_value(
"GEMINI_MODEL");
383 std::string env_anthropic_model = env_value(
"ANTHROPIC_MODEL");
385 if (!env_ollama_host.empty() &&
389 profile_updated =
true;
391 if (!env_openai_base.empty()) {
395 "https://api.openai.com") {
398 profile_updated =
true;
402 if (
const char* gemini_key = std::getenv(
"GEMINI_API_KEY")) {
405 profile_updated =
true;
408 if (
const char* anthropic_key = std::getenv(
"ANTHROPIC_API_KEY")) {
411 profile_updated =
true;
414 if (
const char* openai_key = std::getenv(
"OPENAI_API_KEY")) {
417 profile_updated =
true;
420 const bool has_openai_endpoint_hint =
422 kDefaultOpenAiBaseUrl;
423 const bool provider_is_default =
427 if (provider_is_default) {
433 env_gemini_model.empty() ?
"gemini-2.5-flash" : env_gemini_model;
436 profile_updated =
true;
437 }
else if (has_openai_endpoint_hint) {
444 profile_updated =
true;
450 ?
"claude-3-5-sonnet-20241022"
451 : env_anthropic_model;
454 profile_updated =
true;
459 if (!env_openai_model.empty()) {
466 profile_updated =
true;
467 }
else if (!env_ollama_host.empty() || !env_ollama_model.empty()) {
474 profile_updated =
true;
482 profile_updated =
true;
488 profile_updated =
true;
494 profile_updated =
true;
500 profile_updated =
true;
502 if (profile_updated) {
507 profile_updated =
true;
511 agent_chat_->Initialize(toast_manager, proposal_drawer);
522 harness_telemetry_bridge_.SetAgentChat(
agent_chat_.get());
543 if (model_cache.loading) {
546 if (!force && model_cache.last_refresh != absl::InfinitePast()) {
547 const absl::Duration since_refresh = absl::Now() - model_cache.
last_refresh;
548 if (since_refresh < absl::Seconds(15)) {
553 model_cache.loading =
true;
554 model_cache.auto_refresh_requested =
true;
555 model_cache.available_models.clear();
556 model_cache.model_names.clear();
559 bool needs_local_refresh = force;
560 if (!needs_local_refresh) {
562 needs_local_refresh =
true;
565 needs_local_refresh =
true;
568 if (needs_local_refresh) {
569 model_cache.local_model_names = CollectLocalModelNames(&prefs);
574 model_cache.local_model_names.clear();
581 next_key.
model = config.ai_model;
588 next_key.
verbose = config.verbose;
600 model_cache.loading =
false;
601 model_cache.model_names = model_cache.local_model_names;
602 model_cache.last_refresh = absl::Now();
617 if (
rom_ !=
nullptr) {
622 if (!service_or.ok()) {
624 model_cache.loading =
false;
625 model_cache.model_names = model_cache.local_model_names;
626 model_cache.last_refresh = absl::Now();
638 if (!models_or.ok()) {
639 model_cache.loading =
false;
640 model_cache.model_names = model_cache.local_model_names;
641 model_cache.last_refresh = absl::Now();
649 model_cache.available_models = models_or.value();
650 std::unordered_set<std::string> seen;
651 for (
const auto& info : model_cache.available_models) {
652 if (!info.name.empty()) {
653 AddUniqueModelName(info.name, &model_cache.model_names, &seen);
656 std::sort(model_cache.model_names.begin(), model_cache.model_names.end());
659 std::string selected;
660 for (
const auto& info : model_cache.available_models) {
661 if (ctx_config.ai_provider.empty() ||
662 info.provider == ctx_config.ai_provider) {
663 selected = info.name;
667 if (selected.empty() && !model_cache.model_names.empty()) {
668 selected = model_cache.model_names.front();
670 if (!selected.empty()) {
673 ctx_config.model_buffer);
676 model_cache.last_refresh = absl::Now();
677 model_cache.loading =
false;
689 if (!preset.
model.empty()) {
690 config.ai_model = preset.
model;
692 if (!preset.
host.empty()) {
694 config.ollama_host = preset.
host;
700 for (
auto& entry : config.model_presets) {
701 if (entry.name == preset.
name) {
702 entry.last_used = absl::Now();
711 config.openai_base_url_buffer);
726 const auto& prefs = settings->
prefs();
727 if (prefs.ai_hosts.empty()) {
736 auto resolved = host;
737 if (resolved.api_key.empty()) {
755 bool probe_only_local) {
756 std::string api_type =
761 auto resolved = build_host(host);
762 const bool has_key = !resolved.api_key.empty();
765 if (resolved.base_url.empty()) {
768 if (probe_only_local && !IsLocalOrTrustedEndpoint(
769 resolved.base_url, resolved.allow_insecure)) {
772 if (!ProbeOllamaHost(resolved.base_url)) {
775 return select_host(resolved);
779 if (resolved.base_url.empty()) {
783 IsLocalOrTrustedEndpoint(resolved.base_url, resolved.allow_insecure);
784 if (probe_only_local && !trusted) {
787 if (trusted && ProbeOpenAICompatible(resolved.base_url)) {
788 return select_host(resolved);
790 if (!probe_only_local && has_key) {
791 return select_host(resolved);
798 return has_key ? select_host(resolved) :
false;
804 std::vector<const UserSettings::Preferences::AiHost*> candidates;
805 if (!prefs.active_ai_host_id.empty()) {
806 for (
const auto& host : prefs.ai_hosts) {
807 if (host.
id == prefs.active_ai_host_id) {
808 candidates.push_back(&host);
813 for (
const auto& host : prefs.ai_hosts) {
814 if (!candidates.empty() && candidates.front()->id == host.
id) {
817 candidates.push_back(&host);
820 for (
const auto* host : candidates) {
821 if (try_host(*host,
true)) {
825 for (
const auto* host : candidates) {
826 if (try_host(*host,
false)) {
856 if (
rom_ !=
nullptr) {
860 auto status = service->ConfigureProvider(provider_config);
865 auto agent_cfg = service->GetConfig();
868 agent_cfg.verbose = config.
verbose;
870 service->SetConfig(agent_cfg);
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
void RefreshModelCache(bool force)
std::unique_ptr< cli::AIService > model_service_
void ApplyConfig(const AgentConfig &config)
void InitializeWithDependencies(ToastManager *toast_manager, ProposalDrawer *proposal_drawer, Rom *rom)
void SetRomContext(Rom *rom)
void SetupAutomationCallbacks()
void SyncContextFromProfile()
void SetupMultimodalCallbacks()
void ApplyModelPreset(const ModelPreset &preset)
absl::Time last_local_model_scan_
void SyncConfigFromProfile()
ProposalDrawer * proposal_drawer_
AgentConfig current_config_
ModelServiceKey last_model_service_key_
AgentUIContext * context_
bool MaybeAutoDetectLocalProviders()
ToastManager * toast_manager_
AgentConfig GetCurrentConfig() const
BotProfile current_profile_
std::vector< std::string > last_local_model_paths_
void ApplyConfigFromContext(const AgentConfigState &config)
void MarkProfileUiDirty()
std::unique_ptr< AgentChat > agent_chat_
void ApplyUserSettingsDefaults(bool force=false)
ModelCache & model_cache()
AgentConfigState & agent_config()
EditorDependencies dependencies_
ImGui drawer for displaying and managing agent proposals.
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
static TestManager & Get()
constexpr char kProviderGemini[]
constexpr char kProviderAnthropic[]
absl::StatusOr< std::unique_ptr< AIService > > CreateAIServiceStrict(const AIServiceConfig &config)
constexpr char kProviderAuto[]
std::string NormalizeOpenAiBaseUrl(std::string base)
constexpr char kProviderMock[]
constexpr char kProviderOpenAi[]
constexpr char kProviderOllama[]
constexpr char kProviderLmStudio[]
bool IsOllamaModelsPath(const std::filesystem::path &path)
void CollectOllamaManifestModels(const std::filesystem::path &models_root, std::vector< std::string > *output, std::unordered_set< std::string > *seen)
constexpr char kDefaultOpenAiBaseUrl[]
bool HasModelExtension(const std::filesystem::path &path)
bool IsLocalOrTrustedEndpoint(const std::string &base_url, bool allow_insecure)
bool ProbeOllamaHost(const std::string &base_url)
bool IsTailscaleEndpoint(const std::string &base_url)
void CollectModelFiles(const std::filesystem::path &base_path, std::vector< std::string > *output, std::unordered_set< std::string > *seen)
bool ProbeOpenAICompatible(const std::string &base_url)
bool ContainsText(const std::string &haystack, const std::string &needle)
bool StartsWithText(const std::string &text, const std::string &prefix)
std::vector< std::string > CollectLocalModelNames(const UserSettings::Preferences *prefs)
void AddUniqueModelName(const std::string &name, std::vector< std::string > *output, std::unordered_set< std::string > *seen)
bool ProbeHttpEndpoint(const std::string &base_url, const char *path)
std::string ResolveHostApiKey(const UserSettings::Preferences *prefs, const UserSettings::Preferences::AiHost &host)
void CopyStringToBuffer(const std::string &src, char(&dest)[N])
void ApplyHostPresetToProfile(AgentEditor::BotProfile *profile, const UserSettings::Preferences::AiHost &host, const UserSettings::Preferences *prefs)
std::string rom_path_hint
std::string gemini_api_key
std::string openai_base_url
std::string anthropic_api_key
std::string openai_api_key
std::string anthropic_api_key
std::string gemini_api_key
std::string openai_api_key
std::string openai_base_url
std::string anthropic_api_key
std::string openai_api_key
std::string openai_base_url
std::string gemini_api_key
std::string openai_api_key
std::string anthropic_api_key
std::string openai_base_url
std::string gemini_api_key
UserSettings * user_settings
Model preset for quick switching.
std::string active_ai_host_id
std::vector< std::string > ai_model_paths