diff --git a/sdk/cpp/CMakeLists.txt b/sdk/cpp/CMakeLists.txt index 41f12c27..8393fca2 100644 --- a/sdk/cpp/CMakeLists.txt +++ b/sdk/cpp/CMakeLists.txt @@ -12,33 +12,31 @@ endif() project(CppSdk LANGUAGES CXX) # ----------------------------- -# Windows-only + compiler guard +# Compiler guard # ----------------------------- -if (NOT WIN32) - message(FATAL_ERROR "CppSdk is Windows-only for now (uses Win32/WIL headers).") -endif() - -# Accept MSVC OR clang-cl (Clang in MSVC compatibility mode). -# VS CMake Open-Folder often uses clang-cl by default. -if (NOT (MSVC OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC"))) - message(STATUS "CMAKE_CXX_COMPILER_ID = ${CMAKE_CXX_COMPILER_ID}") - message(STATUS "CMAKE_CXX_COMPILER = ${CMAKE_CXX_COMPILER}") - message(STATUS "CMAKE_CXX_SIMULATE_ID = ${CMAKE_CXX_SIMULATE_ID}") - message(FATAL_ERROR "Need MSVC or clang-cl (MSVC-compatible toolchain).") +if (WIN32) + # Accept MSVC OR clang-cl (Clang in MSVC compatibility mode). + if (NOT (MSVC OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC"))) + message(STATUS "CMAKE_CXX_COMPILER_ID = ${CMAKE_CXX_COMPILER_ID}") + message(STATUS "CMAKE_CXX_COMPILER = ${CMAKE_CXX_COMPILER}") + message(STATUS "CMAKE_CXX_SIMULATE_ID = ${CMAKE_CXX_SIMULATE_ID}") + message(FATAL_ERROR "On Windows, need MSVC or clang-cl (MSVC-compatible toolchain).") + endif() endif() set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -# Optional: target Windows 10+ APIs (adjust if you need older) -add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00) +if (WIN32) + # Target Windows 10+ APIs + add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00) +endif() # ----------------------------- # Dependencies (installed via vcpkg) # ----------------------------- find_package(nlohmann_json CONFIG REQUIRED) -find_package(wil CONFIG REQUIRED) find_package(Microsoft.GSL CONFIG REQUIRED) option(BUILD_TESTING "Build unit and end-to-end tests" ON) if (BUILD_TESTING) @@ -70,9 +68,12 @@ target_link_libraries(CppSdk PUBLIC nlohmann_json::nlohmann_json Microsoft.GSL::GSL - WIL::WIL ) +if (UNIX) + target_link_libraries(CppSdk PUBLIC ${CMAKE_DL_LIBS}) +endif() + # ----------------------------- # Sample executable # ----------------------------- @@ -93,7 +94,6 @@ if (BUILD_TESTING) test/model_variant_test.cpp test/catalog_test.cpp test/client_test.cpp - test/live_audio_test.cpp ) target_include_directories(CppSdkTests diff --git a/sdk/cpp/CMakePresets.json b/sdk/cpp/CMakePresets.json index ddead1b2..6b654f30 100644 --- a/sdk/cpp/CMakePresets.json +++ b/sdk/cpp/CMakePresets.json @@ -51,6 +51,7 @@ "generator": "Ninja", "binaryDir": "${sourceDir}/out/build/${presetName}", "installDir": "${sourceDir}/out/install/${presetName}", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }, @@ -66,6 +67,7 @@ "generator": "Ninja", "binaryDir": "${sourceDir}/out/build/${presetName}", "installDir": "${sourceDir}/out/install/${presetName}", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }, diff --git a/sdk/cpp/include/catalog.h b/sdk/cpp/include/catalog.h index e4e5d17f..7a833a55 100644 --- a/sdk/cpp/include/catalog.h +++ b/sdk/cpp/include/catalog.h @@ -37,7 +37,7 @@ class Catalog final { } const std::string& GetName() const { return name_; } - std::vector ListModels() const; + std::vector GetModels() const; std::vector GetLoadedModels() const; std::vector GetCachedModels() const; diff --git a/sdk/cpp/include/foundry_local_manager.h b/sdk/cpp/include/foundry_local_manager.h index ce8725c6..074f5673 100644 --- a/sdk/cpp/include/foundry_local_manager.h +++ b/sdk/cpp/include/foundry_local_manager.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once + #include #include #include @@ -30,7 +31,7 @@ namespace foundry_local { /// Throws if an instance has already been created. Call Destroy() first to release the current instance. /// @param configuration Configuration to use. /// @param logger Optional application logger. Pass nullptr to suppress log output. - static void Create(Configuration configuration, ILogger* logger = nullptr); + static Manager& Create(Configuration configuration, ILogger* logger = nullptr); /// Get the singleton instance. /// Throws if Create() has not been called. @@ -47,17 +48,16 @@ namespace foundry_local { const Catalog& GetCatalog() const; Catalog& GetCatalog(); - /// Start the optional built-in web service. - /// Provides an OpenAI-compatible REST endpoint. - /// After startup, GetUrls() returns the actual bound URL/s. - /// Requires Configuration::Web to be set. + /// Start the embedded web service. + /// Requires Configuration::web to be set. void StartWebService(); - /// Stop the web service if started. + /// Stop the embedded web service. + /// Requires Configuration::web to be set. void StopWebService(); - /// Returns the bound URL/s after StartWebService(), or empty if not started. - gsl::span GetUrls() const noexcept; + /// Get the URLs the web service is bound to. Valid after StartWebService() and until StopWebService(). + gsl::span GetWebServiceEndpoints() const noexcept; /// Ensure execution providers are downloaded and registered. /// Once downloaded, EPs are not re-downloaded unless a new version is available. diff --git a/sdk/cpp/include/model.h b/sdk/cpp/include/model.h index 9238cf12..b52fae76 100644 --- a/sdk/cpp/include/model.h +++ b/sdk/cpp/include/model.h @@ -32,7 +32,7 @@ namespace foundry_local { } #endif - using DownloadProgressCallback = std::function; + using DownloadProgressCallback = std::function; class IModel { public: @@ -153,7 +153,7 @@ namespace foundry_local { public: explicit Model(gsl::not_null core, gsl::not_null logger); - gsl::span GetAllModelVariants() const; + gsl::span GetVariants() const; bool IsLoaded() const override { return SelectedVariant().IsLoaded(); } bool IsCached() const override { return SelectedVariant().IsCached(); } diff --git a/sdk/cpp/sample/main.cpp b/sdk/cpp/sample/main.cpp index 8ccc39d8..8926a6c7 100644 --- a/sdk/cpp/sample/main.cpp +++ b/sdk/cpp/sample/main.cpp @@ -7,6 +7,10 @@ #include #include +#ifdef _WIN32 +#include +#endif + using namespace foundry_local; // --------------------------------------------------------------------------- @@ -34,6 +38,21 @@ class StdLogger final : public ILogger { } }; +// --------------------------------------------------------------------------- +// Helper – Select the CPU variant for a model (if available) +// --------------------------------------------------------------------------- +void PreferCpuVariant(Model& model) { + for (const auto& variant : model.GetVariants()) { + const auto& info = variant.GetInfo(); + if (info.runtime && info.runtime->device_type == DeviceType::CPU) { + model.SelectVariant(variant); + std::cout << "Selected CPU variant: " << info.name << "\n"; + return; + } + } + std::cout << "No CPU variant found; using default variant.\n"; +} + // --------------------------------------------------------------------------- // Example 1 – Browse the catalog // --------------------------------------------------------------------------- @@ -43,7 +62,7 @@ void BrowseCatalog(Manager& manager) { auto& catalog = manager.GetCatalog(); std::cout << "Catalog: " << catalog.GetName() << "\n"; - auto models = catalog.ListModels(); + auto models = catalog.GetModels(); std::cout << "Models in catalog: " << models.size() << "\n"; for (const auto* model : models) { @@ -53,7 +72,7 @@ void BrowseCatalog(Manager& manager) { auto* concreteModel = dynamic_cast(model); if (!concreteModel) continue; - for (const auto& variant : concreteModel->GetAllModelVariants()) { + for (const auto& variant : concreteModel->GetVariants()) { const auto& info = variant.GetInfo(); std::cout << " variant: " << info.name << " v" << info.version << " cached=" << (variant.IsCached() ? "yes" : "no"); @@ -93,7 +112,12 @@ void ChatNonStreaming(Manager& manager, const std::string& alias) { return; } - model->Download([](float pct) { std::cout << "\rDownloading: " << pct << "% " << std::flush; }); + // Prefer CPU variant to avoid DML/GPU provider issues + if (auto* concreteModel = dynamic_cast(model)) { + PreferCpuVariant(*concreteModel); + } + + model->Download([](float pct) { std::cout << "\rDownloading: " << pct << "% " << std::flush; return true; }); std::cout << "\n"; model->Load(); @@ -138,6 +162,11 @@ void ChatStreaming(Manager& manager, const std::string& alias) { return; } + // Prefer CPU variant to avoid DML/GPU provider issues + if (auto* concreteModel = dynamic_cast(model)) { + PreferCpuVariant(*concreteModel); + } + model->Load(); OpenAIChatClient chat(*model); @@ -176,7 +205,12 @@ void TranscribeAudio(Manager& manager, const std::string& alias, const std::stri return; } - model->Download([](float pct) { std::cout << "\rDownloading: " << pct << "% " << std::flush; }); + // Prefer CPU variant to avoid DML/GPU provider issues + if (auto* concreteModel = dynamic_cast(model)) { + PreferCpuVariant(*concreteModel); + } + + model->Download([](float pct) { std::cout << "\rDownloading: " << pct << "% " << std::flush; return true; }); std::cout << "\n"; model->Load(); @@ -223,7 +257,12 @@ void ChatWithToolCalling(Manager& manager, const std::string& alias) { return; } - model->Download([](float pct) { std::cout << "\rDownloading: " << pct << "% " << std::flush; }); + // Prefer CPU variant to avoid DML/GPU provider issues + if (auto* concreteModel = dynamic_cast(model)) { + PreferCpuVariant(*concreteModel); + } + + model->Download([](float pct) { std::cout << "\rDownloading: " << pct << "% " << std::flush; return true; }); std::cout << "\n"; model->Load(); @@ -325,26 +364,62 @@ void ChatWithToolCalling(Manager& manager, const std::string& alias) { // --------------------------------------------------------------------------- // main // --------------------------------------------------------------------------- -int main() { +int main(int argc, char* argv[]) { +#ifdef _WIN32 + SetConsoleOutputCP(CP_UTF8); +#endif + + // Optional command-line args: + const std::string chatAlias = (argc > 1) ? argv[1] : "phi-3.5-mini"; + const std::string audioAlias = (argc > 2) ? argv[2] : "whisper-large-v3-turbo"; + const std::string audioPath = (argc > 3) ? argv[3] : ""; + try { StdLogger logger; Manager::Create({"SampleApp"}, &logger); auto& manager = Manager::Instance(); // 1. Browse the full catalog - BrowseCatalog(manager); - - // 2. Non-streaming chat (change alias to a model in your catalog) - ChatNonStreaming(manager, "phi-3.5-mini"); + try { + BrowseCatalog(manager); + } + catch (const std::exception& ex) { + std::cerr << "Example 1 failed: " << ex.what() << "\n"; + } + + // 2. Non-streaming chat + try { + ChatNonStreaming(manager, chatAlias); + } + catch (const std::exception& ex) { + std::cerr << "Example 2 failed: " << ex.what() << "\n"; + } // 3. Streaming chat - ChatStreaming(manager, "phi-3.5-mini"); + try { + ChatStreaming(manager, chatAlias); + } + catch (const std::exception& ex) { + std::cerr << "Example 3 failed: " << ex.what() << "\n"; + } - // 4. Audio transcription (uncomment and set a valid alias + wav path) - // TranscribeAudio(manager, "whisper-small", R"(C:\path\to\your\audio.wav)"); + // 4. Audio transcription (requires an audio file path) + if (!audioPath.empty()) { + try { + TranscribeAudio(manager, audioAlias, audioPath); + } + catch (const std::exception& ex) { + std::cerr << "Example 4 failed: " << ex.what() << "\n"; + } + } - // 5. Tool calling (define tools, let the model call them, feed results back) - ChatWithToolCalling(manager, "phi-3.5-mini"); + // 5. Tool calling + try { + ChatWithToolCalling(manager, chatAlias); + } + catch (const std::exception& ex) { + std::cerr << "Example 5 failed: " << ex.what() << "\n"; + } Manager::Destroy(); return 0; diff --git a/sdk/cpp/src/catalog.cpp b/sdk/cpp/src/catalog.cpp index 82aae3be..18340fa4 100644 --- a/sdk/cpp/src/catalog.cpp +++ b/sdk/cpp/src/catalog.cpp @@ -56,7 +56,7 @@ namespace foundry_local { return nullptr; } - std::vector Catalog::ListModels() const { + std::vector Catalog::GetModels() const { UpdateModels(); auto state = GetState(); @@ -159,7 +159,7 @@ namespace foundry_local { } const auto& targetName = it->second->GetInfo().name; - for (auto& v : model->GetAllModelVariants()) { + for (auto& v : model->GetVariants()) { // The variants returned by the catalog are sorted by version, so the first match should always be the // latest version. if (v.GetInfo().name == targetName) { diff --git a/sdk/cpp/src/core.h b/sdk/cpp/src/core.h index cc37ce9e..04c9e47c 100644 --- a/sdk/cpp/src/core.h +++ b/sdk/cpp/src/core.h @@ -1,18 +1,26 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // -// Core DLL interop � loads Microsoft.AI.Foundry.Local.Core.dll at runtime. +// Core shared library interop — loads the Foundry Local Core library at runtime. // Internal header, not part of the public API. #pragma once -#include #include #include #include #include -#include +#ifdef _WIN32 + #include +#else + #include + #include + #include + #ifdef __APPLE__ + #include + #endif +#endif #include "foundry_local_internal_core.h" #include "foundry_local_exception.h" @@ -22,16 +30,112 @@ namespace foundry_local { namespace { + + // RAII wrapper for a dynamically loaded shared library handle. + struct SharedLibHandle { + void* handle = nullptr; + + SharedLibHandle() = default; + explicit SharedLibHandle(void* h) : handle(h) {} + SharedLibHandle(const SharedLibHandle&) = delete; + SharedLibHandle& operator=(const SharedLibHandle&) = delete; + SharedLibHandle(SharedLibHandle&& o) noexcept : handle(o.handle) { o.handle = nullptr; } + SharedLibHandle& operator=(SharedLibHandle&& o) noexcept { + reset(); + handle = o.handle; + o.handle = nullptr; + return *this; + } + ~SharedLibHandle() { reset(); } + + void reset() noexcept { + if (!handle) return; +#ifdef _WIN32 + ::FreeLibrary(static_cast(handle)); +#else + ::dlclose(handle); +#endif + handle = nullptr; + } + + explicit operator bool() const noexcept { return handle != nullptr; } + }; + inline std::filesystem::path GetExecutableDir() { - auto exePath = wil::GetModuleFileNameW(nullptr); - return std::filesystem::path(exePath.get()).parent_path(); +#ifdef _WIN32 + std::wstring buf(MAX_PATH, L'\0'); + for (;;) { + DWORD len = ::GetModuleFileNameW(nullptr, buf.data(), static_cast(buf.size())); + if (len == 0) + throw std::runtime_error("GetModuleFileNameW failed"); + if (len < static_cast(buf.size())) + return std::filesystem::path(buf.c_str()).parent_path(); + buf.resize(buf.size() * 2); + } +#elif defined(__APPLE__) + char buf[PATH_MAX]; + uint32_t size = sizeof(buf); + if (_NSGetExecutablePath(buf, &size) != 0) + throw std::runtime_error("_NSGetExecutablePath failed"); + return std::filesystem::canonical(buf).parent_path(); +#else + return std::filesystem::read_symlink("/proc/self/exe").parent_path(); +#endif + } + + inline std::string GetLoaderError() { +#ifdef _WIN32 + DWORD err = ::GetLastError(); + if (err == 0) return {}; + LPSTR buf = nullptr; + DWORD len = ::FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&buf), 0, nullptr); + std::string msg(buf, len); + ::LocalFree(buf); + // Trim trailing newline + while (!msg.empty() && (msg.back() == '\n' || msg.back() == '\r')) + msg.pop_back(); + return msg; +#else + const char* err = ::dlerror(); + return err ? std::string(err) : std::string{}; +#endif } - inline void* RequireProc(HMODULE mod, const char* name) { - if (void* p = ::GetProcAddress(mod, name)) - return p; - throw std::runtime_error(std::string("GetProcAddress failed for ") + name); + inline void* LoadSharedLib(const std::filesystem::path& path) { +#ifdef _WIN32 + return static_cast(::LoadLibraryW(path.c_str())); +#else + return ::dlopen(path.c_str(), RTLD_NOW); +#endif } + + inline void* RequireProc(void* mod, const char* name) { +#ifdef _WIN32 + void* p = reinterpret_cast(::GetProcAddress(static_cast(mod), name)); +#else + void* p = ::dlsym(mod, name); +#endif + if (!p) { + std::string msg = std::string("Symbol not found: ") + name; + std::string detail = GetLoaderError(); + if (!detail.empty()) + msg += " (" + detail + ")"; + throw std::runtime_error(msg); + } + return p; + } + + inline void* OptionalProc(void* mod, const char* name) noexcept { +#ifdef _WIN32 + return reinterpret_cast(::GetProcAddress(static_cast(mod), name)); +#else + return ::dlsym(mod, name); +#endif + } + } // namespace struct Core : Internal::IFoundryLocalCore { @@ -40,7 +144,17 @@ namespace foundry_local { Core() = default; ~Core() = default; - void LoadEmbedded() { LoadFromPath(GetExecutableDir() / "Microsoft.AI.Foundry.Local.Core.dll"); } + void LoadEmbedded() { + constexpr const char* kCoreLibName = +#ifdef _WIN32 + "Microsoft.AI.Foundry.Local.Core.dll"; +#elif defined(__APPLE__) + "Microsoft.AI.Foundry.Local.Core.dylib"; +#else + "Microsoft.AI.Foundry.Local.Core.so"; +#endif + LoadFromPath(GetExecutableDir() / kCoreLibName); + } void unload() override { module_.reset(); @@ -52,7 +166,7 @@ namespace foundry_local { CoreResponse call(std::string_view command, ILogger& logger, const std::string* dataArgument = nullptr, NativeCallbackFn callback = nullptr, void* data = nullptr) const override { - if (!module_ || !execCmd_ || !execCbCmd_ || !freeResCmd_) { + if (!static_cast(module_) || !execCmd_ || !execCbCmd_ || !freeResCmd_) { throw Exception("Core is not loaded. Cannot call command: " + std::string(command), logger); } @@ -95,9 +209,12 @@ namespace foundry_local { CoreResponse callWithBinary(std::string_view command, ILogger& logger, const std::string* dataArgument, const uint8_t* binaryData, size_t binaryDataLength) const override { - if (!module_ || !execBinaryCmd_ || !freeResCmd_) { + if (!static_cast(module_) || !freeResCmd_) { throw Exception("Core is not loaded. Cannot call command: " + std::string(command), logger); } + if (!execBinaryCmd_) { + throw Exception("execute_command_with_binary is not available in this version of the Core library.", logger); + } StreamingRequestBuffer request{}; request.Command = command.empty() ? nullptr : command.data(); @@ -137,23 +254,28 @@ namespace foundry_local { } private: - wil::unique_hmodule module_; + SharedLibHandle module_; execute_command_fn execCmd_{}; execute_command_with_callback_fn execCbCmd_{}; execute_command_with_binary_fn execBinaryCmd_{}; free_response_fn freeResCmd_{}; void LoadFromPath(const std::filesystem::path& path) { - wil::unique_hmodule m(::LoadLibraryW(path.c_str())); - if (!m) - throw std::runtime_error("LoadLibraryW failed"); + SharedLibHandle m(LoadSharedLib(path)); + if (!m) { + std::string msg = "Failed to load shared library: " + path.string(); + std::string detail = GetLoaderError(); + if (!detail.empty()) + msg += " (" + detail + ")"; + throw std::runtime_error(msg); + } - execCmd_ = reinterpret_cast(RequireProc(m.get(), "execute_command")); + execCmd_ = reinterpret_cast(RequireProc(m.handle, "execute_command")); execCbCmd_ = reinterpret_cast( - RequireProc(m.get(), "execute_command_with_callback")); + RequireProc(m.handle, "execute_command_with_callback")); execBinaryCmd_ = reinterpret_cast( - RequireProc(m.get(), "execute_command_with_binary")); - freeResCmd_ = reinterpret_cast(RequireProc(m.get(), "free_response")); + OptionalProc(m.handle, "execute_command_with_binary")); + freeResCmd_ = reinterpret_cast(RequireProc(m.handle, "free_response")); module_ = std::move(m); } diff --git a/sdk/cpp/src/flcore_native.h b/sdk/cpp/src/flcore_native.h index d87baa09..b3d35d08 100644 --- a/sdk/cpp/src/flcore_native.h +++ b/sdk/cpp/src/flcore_native.h @@ -5,6 +5,12 @@ #include #include +#ifdef _WIN32 + #define FL_CDECL __cdecl +#else + #define FL_CDECL +#endif + extern "C" { // Layout must match C# structs exactly @@ -24,7 +30,7 @@ extern "C" }; // Callback signature: void(*)(void* data, int length, void* userData) - using UserCallbackFn = void(__cdecl*)(void*, int32_t, void*); + using UserCallbackFn = void(FL_CDECL*)(void*, int32_t, void*); struct StreamingRequestBuffer { const void* Command; @@ -36,11 +42,11 @@ extern "C" }; // Exported function pointer types - using execute_command_fn = void(__cdecl*)(RequestBuffer*, ResponseBuffer*); - using execute_command_with_callback_fn = void(__cdecl*)(RequestBuffer*, ResponseBuffer*, void* /*callback*/, + using execute_command_fn = void(FL_CDECL*)(RequestBuffer*, ResponseBuffer*); + using execute_command_with_callback_fn = void(FL_CDECL*)(RequestBuffer*, ResponseBuffer*, void* /*callback*/, void* /*userData*/); - using execute_command_with_binary_fn = void(__cdecl*)(StreamingRequestBuffer*, ResponseBuffer*); - using free_response_fn = void(__cdecl*)(ResponseBuffer*); + using execute_command_with_binary_fn = void(FL_CDECL*)(StreamingRequestBuffer*, ResponseBuffer*); + using free_response_fn = void(FL_CDECL*)(ResponseBuffer*); static_assert(std::is_standard_layout::value, "RequestBuffer must be standard layout"); static_assert(std::is_standard_layout::value, "ResponseBuffer must be standard layout"); diff --git a/sdk/cpp/src/foundry_local_manager.cpp b/sdk/cpp/src/foundry_local_manager.cpp index e24be049..dfaef291 100644 --- a/sdk/cpp/src/foundry_local_manager.cpp +++ b/sdk/cpp/src/foundry_local_manager.cpp @@ -19,7 +19,7 @@ namespace foundry_local { std::unique_ptr Manager::instance_; -void Manager::Create(Configuration configuration, ILogger* logger) { +Manager& Manager::Create(Configuration configuration, ILogger* logger) { if (instance_) { NullLogger fallback; ILogger& log = logger ? *logger : fallback; @@ -30,6 +30,7 @@ void Manager::Create(Configuration configuration, ILogger* logger) { std::unique_ptr manager( new Manager(std::move(configuration), logger)); instance_ = std::move(manager); + return *instance_; } Manager& Manager::Instance() { @@ -124,7 +125,7 @@ void Manager::Cleanup() noexcept { urls_.clear(); } - gsl::span Manager::GetUrls() const noexcept { + gsl::span Manager::GetWebServiceEndpoints() const noexcept { return urls_; } diff --git a/sdk/cpp/src/model.cpp b/sdk/cpp/src/model.cpp index d4240ecb..9b6cb1c5 100644 --- a/sdk/cpp/src/model.cpp +++ b/sdk/cpp/src/model.cpp @@ -160,7 +160,7 @@ namespace foundry_local { return *selectedVariant_; } - gsl::span Model::GetAllModelVariants() const { + gsl::span Model::GetVariants() const { return variants_; } diff --git a/sdk/cpp/test/catalog_test.cpp b/sdk/cpp/test/catalog_test.cpp index d93de49e..1531799d 100644 --- a/sdk/cpp/test/catalog_test.cpp +++ b/sdk/cpp/test/catalog_test.cpp @@ -51,14 +51,14 @@ TEST_F(CatalogTest, Create_ThrowsOnCoreError) { TEST_F(CatalogTest, ListModels_Empty) { core_.OnCall("get_model_list", "[]"); auto catalog = MakeCatalog(); - auto models = catalog->ListModels(); + auto models = catalog->GetModels(); EXPECT_TRUE(models.empty()); } TEST_F(CatalogTest, ListModels_SingleModel) { core_.OnCall("get_model_list", MakeModelListJson({{"model-1", "my-model"}})); auto catalog = MakeCatalog(); - auto models = catalog->ListModels(); + auto models = catalog->GetModels(); ASSERT_EQ(1u, models.size()); EXPECT_EQ("my-model", models[0]->GetAlias()); } @@ -71,24 +71,24 @@ TEST_F(CatalogTest, ListModels_MultipleVariantsSameAlias) { core_.OnCall("get_model_list", arr.dump()); auto catalog = MakeCatalog(); - auto models = catalog->ListModels(); + auto models = catalog->GetModels(); // Should be grouped into one Model ASSERT_EQ(1u, models.size()); - EXPECT_EQ(2u, dynamic_cast(models[0])->GetAllModelVariants().size()); + EXPECT_EQ(2u, dynamic_cast(models[0])->GetVariants().size()); } TEST_F(CatalogTest, ListModels_DifferentAliases) { core_.OnCall("get_model_list", MakeModelListJson({{"model-a", "alias-a"}, {"model-b", "alias-b"}})); auto catalog = MakeCatalog(); - auto models = catalog->ListModels(); + auto models = catalog->GetModels(); EXPECT_EQ(2u, models.size()); } TEST_F(CatalogTest, ListModels_IncludesOpenAIPrefix) { core_.OnCall("get_model_list", MakeModelListJson({{"model-a", "my-model"}, {"openai-model", "openai-stuff"}})); auto catalog = MakeCatalog(); - auto models = catalog->ListModels(); + auto models = catalog->GetModels(); ASSERT_EQ(2u, models.size()); } @@ -149,8 +149,8 @@ TEST_F(CatalogTest, ListModels_CachesResults) { core_.OnCall("get_model_list", MakeModelListJson({{"model-1", "my-model"}})); auto catalog = MakeCatalog(); - catalog->ListModels(); - catalog->ListModels(); + catalog->GetModels(); + catalog->GetModels(); // Should only call get_model_list once due to caching EXPECT_EQ(1, core_.GetCallCount("get_model_list")); @@ -166,7 +166,7 @@ TEST_F(CatalogTest, GetLatestVersion) { auto* model = dynamic_cast(catalog->GetModel("alias")); ASSERT_NE(nullptr, model); - const auto& first = model->GetAllModelVariants()[0]; + const auto& first = model->GetVariants()[0]; auto& latest = catalog->GetLatestVersion(first); // Should return the first one with matching name (which is variants_[0]) EXPECT_EQ(&first, &latest); @@ -187,7 +187,7 @@ TEST_F(FileBasedCatalogTest, RealModelsList) { auto core = FileBackedCore::FromModelList(TestDataPath("real_models_list.json")); auto catalog = Factory::CreateCatalog(&core, &logger_); - auto models = catalog->ListModels(); + auto models = catalog->GetModels(); ASSERT_EQ(2u, models.size()); int phi_models = 0, mistral_models = 0; @@ -196,11 +196,11 @@ TEST_F(FileBasedCatalogTest, RealModelsList) { for (const auto* model : models) { if (model->GetAlias() == "phi-4") { phi_models++; - phi_variants = dynamic_cast(model)->GetAllModelVariants().size(); + phi_variants = dynamic_cast(model)->GetVariants().size(); } else if (model->GetAlias() == "mistral-7b-v0.2") { mistral_models++; - mistral_variants = dynamic_cast(model)->GetAllModelVariants().size(); + mistral_variants = dynamic_cast(model)->GetVariants().size(); } } @@ -266,7 +266,7 @@ TEST_F(FileBasedCatalogTest, EmptyModelsList) { auto core = FileBackedCore::FromModelList(TestDataPath("empty_models_list.json")); auto catalog = Factory::CreateCatalog(&core, &logger_); - auto models = catalog->ListModels(); + auto models = catalog->GetModels(); EXPECT_TRUE(models.empty()); } @@ -274,7 +274,7 @@ TEST_F(FileBasedCatalogTest, MalformedJson) { auto core = FileBackedCore::FromModelList(TestDataPath("malformed_models_list.json")); auto catalog = Factory::CreateCatalog(&core, &logger_); - EXPECT_ANY_THROW(catalog->ListModels()); + EXPECT_ANY_THROW(catalog->GetModels()); } TEST_F(FileBasedCatalogTest, MissingNameField) { @@ -282,7 +282,7 @@ TEST_F(FileBasedCatalogTest, MissingNameField) { auto catalog = Factory::CreateCatalog(&core, &logger_); try { - catalog->ListModels(); + catalog->GetModels(); FAIL() << "Expected exception for missing 'name' field"; } catch (const std::exception& e) { @@ -312,14 +312,14 @@ TEST_F(FileBasedCatalogTest, CoreErrorOnModelList) { auto core = FileBackedCore::FromModelList("testdata/nonexistent_file.json"); auto catalog = Factory::CreateCatalog(&core, &logger_); - EXPECT_ANY_THROW(catalog->ListModels()); + EXPECT_ANY_THROW(catalog->GetModels()); } TEST_F(FileBasedCatalogTest, MixedOpenAIAndLocal_IncludesAll) { auto core = FileBackedCore::FromModelList(TestDataPath("mixed_openai_and_local.json")); auto catalog = Factory::CreateCatalog(&core, &logger_); - auto models = catalog->ListModels(); + auto models = catalog->GetModels(); ASSERT_EQ(3u, models.size()); } @@ -327,9 +327,9 @@ TEST_F(FileBasedCatalogTest, ThreeVariantsOneModel) { auto core = FileBackedCore::FromModelList(TestDataPath("three_variants_one_model.json")); auto catalog = Factory::CreateCatalog(&core, &logger_); - auto models = catalog->ListModels(); + auto models = catalog->GetModels(); ASSERT_EQ(1u, models.size()); - EXPECT_EQ(3u, dynamic_cast(models[0])->GetAllModelVariants().size()); + EXPECT_EQ(3u, dynamic_cast(models[0])->GetVariants().size()); } TEST_F(FileBasedCatalogTest, ThreeVariantsOneModel_CachedSubset) { @@ -349,7 +349,7 @@ TEST_F(FileBasedCatalogTest, GetModelByAlias) { const auto* model = catalog->GetModel("phi-4"); ASSERT_NE(nullptr, model); EXPECT_EQ("phi-4", model->GetAlias()); - EXPECT_EQ(2u, dynamic_cast(model)->GetAllModelVariants().size()); + EXPECT_EQ(2u, dynamic_cast(model)->GetVariants().size()); const auto* missing = catalog->GetModel("nonexistent-alias"); EXPECT_EQ(nullptr, missing); diff --git a/sdk/cpp/test/e2e_test.cpp b/sdk/cpp/test/e2e_test.cpp index 06bdc0ff..af8d526c 100644 --- a/sdk/cpp/test/e2e_test.cpp +++ b/sdk/cpp/test/e2e_test.cpp @@ -84,7 +84,7 @@ class EndToEndTest : public ::testing::Test { } if (!target) { - auto models = catalog.ListModels(); + auto models = catalog.GetModels(); for (auto* model : models) { if (!IsAudioModel(model->GetAlias())) { target = model; @@ -96,7 +96,7 @@ class EndToEndTest : public ::testing::Test { if (target) { auto* model = dynamic_cast(target); if (model) { - for (const auto& variant : model->GetAllModelVariants()) { + for (const auto& variant : model->GetVariants()) { if (variant.GetInfo().runtime.has_value() && variant.GetInfo().runtime->device_type == DeviceType::CPU) { model->SelectVariant(variant); @@ -142,16 +142,16 @@ TEST_F(EndToEndTest, BrowseCatalog_ListsModels) { auto& catalog = Manager::Instance().GetCatalog(); EXPECT_FALSE(catalog.GetName().empty()); - auto models = catalog.ListModels(); + auto models = catalog.GetModels(); EXPECT_GT(models.size(), 0u) << "Catalog should have at least one model"; for (const auto* model : models) { EXPECT_FALSE(model->GetAlias().empty()); auto* concreteModel = dynamic_cast(model); ASSERT_NE(nullptr, concreteModel); - EXPECT_FALSE(concreteModel->GetAllModelVariants().empty()); + EXPECT_FALSE(concreteModel->GetVariants().empty()); - for (const auto& variant : concreteModel->GetAllModelVariants()) { + for (const auto& variant : concreteModel->GetVariants()) { const auto& info = variant.GetInfo(); EXPECT_FALSE(info.id.empty()); EXPECT_FALSE(info.name.empty()); @@ -194,14 +194,14 @@ auto& catalog = Manager::Instance().GetCatalog(); TEST_F(EndToEndTest, GetModelVariant_Found) { auto& catalog = Manager::Instance().GetCatalog(); - auto models = catalog.ListModels(); + auto models = catalog.GetModels(); if (models.empty()) { GTEST_SKIP() << "No models in catalog"; } const auto* firstConcreteModel = dynamic_cast(models[0]); ASSERT_NE(nullptr, firstConcreteModel); - const auto& firstVariant = firstConcreteModel->GetAllModelVariants()[0]; + const auto& firstVariant = firstConcreteModel->GetVariants()[0]; auto* found = catalog.GetModelVariant(firstVariant.GetId()); ASSERT_NE(nullptr, found); EXPECT_EQ(firstVariant.GetId(), found->GetId()); @@ -209,7 +209,7 @@ auto& catalog = Manager::Instance().GetCatalog(); TEST_F(EndToEndTest, ModelVariantInfo_HasRequiredFields) { auto& catalog = Manager::Instance().GetCatalog(); - auto models = catalog.ListModels(); + auto models = catalog.GetModels(); if (models.empty()) { GTEST_SKIP() << "No models in catalog"; } @@ -217,7 +217,7 @@ auto& catalog = Manager::Instance().GetCatalog(); for (const auto* model : models) { auto* concreteModel = dynamic_cast(model); ASSERT_NE(nullptr, concreteModel); - for (const auto& variant : concreteModel->GetAllModelVariants()) { + for (const auto& variant : concreteModel->GetVariants()) { const auto& info = variant.GetInfo(); EXPECT_FALSE(info.id.empty()); EXPECT_FALSE(info.name.empty()); @@ -230,13 +230,13 @@ auto& catalog = Manager::Instance().GetCatalog(); TEST_F(EndToEndTest, ModelVariant_SelectVariant) { auto& catalog = Manager::Instance().GetCatalog(); - auto models = catalog.ListModels(); + auto models = catalog.GetModels(); // Find a model with multiple variants Model* multiVariantModel = nullptr; for (auto* model : models) { auto* concreteModel = dynamic_cast(model); - if (concreteModel && concreteModel->GetAllModelVariants().size() > 1) { + if (concreteModel && concreteModel->GetVariants().size() > 1) { multiVariantModel = concreteModel; break; } @@ -246,7 +246,7 @@ auto& catalog = Manager::Instance().GetCatalog(); GTEST_SKIP() << "No model with multiple variants found"; } - const auto& variants = multiVariantModel->GetAllModelVariants(); + const auto& variants = multiVariantModel->GetVariants(); const auto& secondVariant = variants[1]; multiVariantModel->SelectVariant(secondVariant); EXPECT_EQ(secondVariant.GetId(), multiVariantModel->GetId()); @@ -279,8 +279,8 @@ TEST_F(EndToEndTest, DISABLED_WebService_StartAndStop) { auto& manager = Manager::Instance(); - // GetUrls should be empty before starting - EXPECT_TRUE(manager.GetUrls().empty()); + // GetWebServiceEndpoints should be empty before starting + EXPECT_TRUE(manager.GetWebServiceEndpoints().empty()); // StartWebService without web config should throw // Note: the manager was created without web config, so this verifies the guard. @@ -309,6 +309,7 @@ auto& catalog = Manager::Instance().GetCatalog(); target->Download([&](float pct) { progressCallbackInvoked = true; std::cout << "\r[E2E] Download: " << pct << "% " << std::flush; + return true; }); std::cout << "\n"; @@ -418,7 +419,7 @@ TEST_F(EndToEndTest, DISABLED_ChatWithToolCalling) { bool supportsCalling = false; auto* targetModel = dynamic_cast(target); if (targetModel) { - for (const auto& v : targetModel->GetAllModelVariants()) { + for (const auto& v : targetModel->GetVariants()) { if (v.GetInfo().supports_tool_calling.has_value() && *v.GetInfo().supports_tool_calling) { supportsCalling = true; break; diff --git a/sdk/cpp/test/model_variant_test.cpp b/sdk/cpp/test/model_variant_test.cpp index c0ea9b39..6f36f218 100644 --- a/sdk/cpp/test/model_variant_test.cpp +++ b/sdk/cpp/test/model_variant_test.cpp @@ -124,7 +124,7 @@ TEST_F(ModelVariantTest, Download_WithCallback) { auto variant = MakeVariant("test-model"); float lastProgress = -1.0f; - variant.Download([&](float pct) { lastProgress = pct; }); + variant.Download([&](float pct) { lastProgress = pct; return true; }); EXPECT_NEAR(50.0f, lastProgress, 0.01f); } @@ -197,7 +197,7 @@ TEST_F(ModelTest, GetAllModelVariants) { Factory::AddVariantToModel(model, MakeVariant("v2", "alias", 2)); Factory::SelectFirstVariant(model); - auto variants = model.GetAllModelVariants(); + auto variants = model.GetVariants(); EXPECT_EQ(2u, variants.size()); } @@ -207,7 +207,7 @@ TEST_F(ModelTest, SelectVariant) { Factory::AddVariantToModel(model, MakeVariant("v2", "alias", 2)); Factory::SelectFirstVariant(model); - const auto& v2 = model.GetAllModelVariants()[1]; + const auto& v2 = model.GetVariants()[1]; model.SelectVariant(v2); EXPECT_EQ("v2:2", model.GetId()); } diff --git a/sdk/cpp/vcpkg.json b/sdk/cpp/vcpkg.json index ec08c349..459a72c1 100644 --- a/sdk/cpp/vcpkg.json +++ b/sdk/cpp/vcpkg.json @@ -3,7 +3,6 @@ "version-string": "0.1.0", "dependencies": [ "nlohmann-json", - "wil", "ms-gsl", "gtest" ]