diff --git a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp index da3e3e2c1..1dd81cb47 100644 --- a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp +++ b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.cpp @@ -210,6 +210,9 @@ AIAssistantPlugin::AIAssistantPlugin( PluginManager* pluginManager, bool sync ) AIAssistantPlugin::~AIAssistantPlugin() { { std::lock_guard lock( mModelCatalogMutex ); + mModelCatalogCancelled->store( true ); + if ( mModelCatalog ) + mModelCatalog->cancel(); mModelCatalog.reset(); } if ( SceneManager::existsSingleton() && !SceneManager::instance()->isShuttingDown() ) { @@ -287,9 +290,6 @@ void AIAssistantPlugin::load( PluginManager* pluginManager ) { userConfig["model_catalog_refresh_hours"].get(); } } - LLMModelCatalog catalog( catalogSettings ); - catalog.loadCached( mProviders ); - if ( !mConfigPath.empty() ) { try { loadAIAssistantConfig( mConfigPath, true ); @@ -375,7 +375,8 @@ void AIAssistantPlugin::load( PluginManager* pluginManager ) { void AIAssistantPlugin::refreshModelCatalogAsync() { LLMModelCatalog::Settings settings; Node* mainThreadNode; - LLMModelCatalog* catalog; + std::shared_ptr catalog; + std::shared_ptr cancelled; { std::lock_guard lock( mModelCatalogMutex ); if ( mModelCatalogRefreshStarted || !mModelCatalogSettings || @@ -386,23 +387,39 @@ void AIAssistantPlugin::refreshModelCatalogAsync() { if ( !mainThreadNode || !mainThreadNode->getSceneNode() ) return; settings = *mModelCatalogSettings; - mModelCatalog = std::make_unique( settings ); - catalog = mModelCatalog.get(); + mModelCatalog = std::make_shared( settings ); + catalog = mModelCatalog; + cancelled = mModelCatalogCancelled; mModelCatalogRefreshStarted = true; } auto* manager = mManager; - catalog->refreshAsync( mProviders, [manager, mainThreadNode]( LLMProviders providers ) mutable { - mainThreadNode->runOnMainThread( [providers = std::move( providers ), manager]() mutable { - if ( manager->isClosing() ) - return; - auto* plugin = manager->get( AIAssistantPlugin::Definition().id ); - if ( plugin ) - static_cast( plugin )->applyModelCatalog( - std::move( providers ) ); - else - Log::warning( - "Could not apply refreshed LLM model catalog: plugin is unavailable" ); - } ); + auto applyProviders = [manager, mainThreadNode, cancelled]( LLMProviders providers ) mutable { + if ( cancelled->load() ) + return; + mainThreadNode->runOnMainThread( + [providers = std::move( providers ), manager, cancelled]() mutable { + if ( cancelled->load() || manager->isClosing() ) + return; + auto* plugin = manager->get( AIAssistantPlugin::Definition().id ); + if ( plugin ) { + static_cast( plugin )->applyModelCatalog( + std::move( providers ) ); + } else { + Log::warning( + "Could not apply refreshed LLM model catalog: plugin is unavailable" ); + } + } ); + }; + LLMProviders providers = mProviders; + mThreadPool->run( [catalog = std::move( catalog ), providers = std::move( providers ), + applyProviders = std::move( applyProviders ), cancelled]() mutable { + if ( cancelled->load() ) + return; + if ( catalog->loadCached( providers ) && !cancelled->load() ) + applyProviders( LLMProviders( providers ) ); + if ( cancelled->load() ) + return; + catalog->refreshAsync( std::move( providers ), std::move( applyProviders ) ); } ); } diff --git a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp index a0fb32949..3e9a2cd19 100644 --- a/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp +++ b/src/tools/ecode/plugins/aiassistant/aiassistantplugin.hpp @@ -69,7 +69,9 @@ class AIAssistantPlugin : public PluginBase { std::string mConfigFileError; std::mutex mModelCatalogMutex; std::optional mModelCatalogSettings; - std::unique_ptr mModelCatalog; + std::shared_ptr mModelCatalog; + std::shared_ptr mModelCatalogCancelled{ + std::make_shared( false ) }; bool mModelCatalogRefreshStarted{ false }; AIAssistantPlugin( PluginManager* pluginManager, bool sync ); diff --git a/src/tools/ecode/plugins/aiassistant/llmmodelcatalog.cpp b/src/tools/ecode/plugins/aiassistant/llmmodelcatalog.cpp index d92c522b1..514291a7a 100644 --- a/src/tools/ecode/plugins/aiassistant/llmmodelcatalog.cpp +++ b/src/tools/ecode/plugins/aiassistant/llmmodelcatalog.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -32,9 +33,7 @@ static bool supportsTextChat( const json& model ) { } static json normalizeCatalog( const json& source ) { - json result = { { "version", CATALOG_VERSION }, - { "fetched_at", Sys::getUnixTimestamp() }, - { "providers", json::object() } }; + json result = { { "version", CATALOG_VERSION }, { "providers", json::object() } }; for ( const auto& [providerId, provider] : source.items() ) { if ( !provider.is_object() || !provider.contains( "models" ) || !provider["models"].is_object() ) @@ -124,6 +123,10 @@ static bool writeAtomically( const std::string& path, const std::string& data ) LLMModelCatalog::LLMModelCatalog( Settings settings ) : mSettings( std::move( settings ) ) {} LLMModelCatalog::~LLMModelCatalog() { + cancel(); +} + +void LLMModelCatalog::cancel() { mCancelled->store( true ); if ( mRequestId && Http::Pool::getGlobal().exists( mRequestURI, mProxyURI ) ) Http::Pool::getGlobal().get( mRequestURI, mProxyURI )->setCancelRequest( mRequestId ); @@ -161,7 +164,8 @@ LLMModelCatalog::parseReasoningConfiguration( const json& options ) { return fallback; } -bool LLMModelCatalog::applyCatalog( const std::string& data, LLMProviders& providers ) const { +bool LLMModelCatalog::applyCatalog( const std::string& data, LLMProviders& providers, + std::string* etag ) const { const auto catalog = json::parse( data, nullptr, false, true ); if ( !catalog.is_object() || !catalog.contains( "version" ) || !catalog["version"].is_number_integer() || @@ -243,15 +247,24 @@ bool LLMModelCatalog::applyCatalog( const std::string& data, LLMProviders& provi catalogProviders.insert_or_assign( providerId, std::move( provider ) ); if ( catalogProviders.empty() ) return false; + if ( etag ) + *etag = catalog.contains( "etag" ) && catalog["etag"].is_string() + ? catalog["etag"].get() + : ""; providers = std::move( catalogProviders ); return true; } -bool LLMModelCatalog::loadCached( LLMProviders& providers ) const { +bool LLMModelCatalog::loadCached( LLMProviders& providers ) { if ( !mSettings.enabled ) return false; std::string data; - return FileSystem::fileGet( mSettings.cachePath, data ) && applyCatalog( data, providers ); + if ( !FileSystem::fileGet( mSettings.cachePath, data ) || + !applyCatalog( data, providers, &mCachedETag ) ) + return false; + mCachedData = std::move( data ); + mCachedCatalogValid = true; + return true; } std::uint64_t @@ -260,36 +273,23 @@ LLMModelCatalog::refreshAsync( LLMProviders providers, if ( !mSettings.enabled || mSettings.url.empty() ) return 0; Clock clock; - std::string cached; - json cache; - if ( FileSystem::fileGet( mSettings.cachePath, cached ) ) - cache = json::parse( cached, nullptr, false, true ); - const bool validCache = cache.is_object() && cache.contains( "version" ) && - cache["version"].is_number_integer() && - cache["version"].get() == CATALOG_VERSION; - const std::int64_t fetchedAt = - validCache && cache.contains( "fetched_at" ) && cache["fetched_at"].is_number_integer() - ? cache["fetched_at"].get() - : 0; const std::int64_t maxAge = static_cast( mSettings.refreshIntervalHours ) * 60 * 60; - if ( fetchedAt > 0 && Sys::getUnixTimestamp() - fetchedAt < maxAge ) + const FileInfo cacheInfo( mSettings.cachePath ); + if ( mCachedCatalogValid && cacheInfo.exists() && cacheInfo.getModificationTime() > 0 && + Sys::getUnixTimestamp() - cacheInfo.getModificationTime() < maxAge ) return 0; Http::Request::FieldTable headers; - if ( validCache ) { - const std::string etag = cache.contains( "etag" ) && cache["etag"].is_string() - ? cache["etag"].get() - : ""; - if ( !etag.empty() ) - headers["If-None-Match"] = etag; - } + if ( !mCachedETag.empty() ) + headers["If-None-Match"] = mCachedETag; const Settings settings = mSettings; + std::string cachedData = std::move( mCachedData ); mCancelled->store( false ); mRequestURI = URI( mSettings.url ); mProxyURI = Http::getEnvProxyURI(); mRequestId = Http::getAsync( - [settings, cache = std::move( cache ), providers = std::move( providers ), + [settings, cachedData = std::move( cachedData ), providers = std::move( providers ), refreshedCallback = std::move( refreshedCallback ), clock, cancelled = mCancelled]( const Http&, Http::Request&, Http::Response& response ) mutable { if ( cancelled->load() ) @@ -297,8 +297,8 @@ LLMModelCatalog::refreshAsync( LLMProviders providers, if ( response.getStatus() == Http::Response::Status::NotModified ) { if ( cancelled->load() ) return; - cache["fetched_at"] = Sys::getUnixTimestamp(); - writeAtomically( settings.cachePath, cache.dump() ); + if ( !cachedData.empty() ) + writeAtomically( settings.cachePath, cachedData ); return; } if ( response.getStatus() != Http::Response::Status::Ok ) { diff --git a/src/tools/ecode/plugins/aiassistant/llmmodelcatalog.hpp b/src/tools/ecode/plugins/aiassistant/llmmodelcatalog.hpp index 8fb0e4e9e..a7f4c94f2 100644 --- a/src/tools/ecode/plugins/aiassistant/llmmodelcatalog.hpp +++ b/src/tools/ecode/plugins/aiassistant/llmmodelcatalog.hpp @@ -23,8 +23,9 @@ class LLMModelCatalog { explicit LLMModelCatalog( Settings settings ); ~LLMModelCatalog(); + void cancel(); - bool loadCached( LLMProviders& providers ) const; + bool loadCached( LLMProviders& providers ); std::uint64_t refreshAsync( LLMProviders providers, std::function refreshedCallback ); @@ -38,8 +39,12 @@ class LLMModelCatalog { EE::Network::URI mProxyURI; std::uint64_t mRequestId{ 0 }; std::shared_ptr mCancelled{ std::make_shared( false ) }; + std::string mCachedData; + std::string mCachedETag; + bool mCachedCatalogValid{ false }; - bool applyCatalog( const std::string& data, LLMProviders& providers ) const; + bool applyCatalog( const std::string& data, LLMProviders& providers, + std::string* etag = nullptr ) const; }; } // namespace ecode