From cccdb1cab974e8c49985318d84175931cb5ad9cc Mon Sep 17 00:00:00 2001 From: Selena Yang <179177246+selenayang888@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:00:57 -0700 Subject: [PATCH 1/2] Derive and enforce BYOM registration metadata --- sdk_v2/cpp/src/catalog/local_model_catalog.cc | 108 +++++++++++++++++- sdk_v2/cpp/src/catalog/local_model_catalog.h | 4 +- .../cpp/src/inferencing/execution_provider.h | 6 +- sdk_v2/cpp/test/internal_api/c_api_test.cc | 12 +- .../internal_api/execution_provider_test.cc | 17 +++ .../internal_api/local_model_catalog_test.cc | 102 ++++++++++++++++- 6 files changed, 239 insertions(+), 10 deletions(-) diff --git a/sdk_v2/cpp/src/catalog/local_model_catalog.cc b/sdk_v2/cpp/src/catalog/local_model_catalog.cc index bd5e93d3a..9ade1697d 100644 --- a/sdk_v2/cpp/src/catalog/local_model_catalog.cc +++ b/sdk_v2/cpp/src/catalog/local_model_catalog.cc @@ -3,6 +3,7 @@ #include "catalog/local_model_catalog.h" #include "exception.h" +#include "inferencing/execution_provider.h" #include "inferencing/generative/genai_config.h" #include "util/file_lock.h" #include "util/time_utils.h" @@ -50,6 +51,62 @@ struct ParsedModelId { int version; }; +std::optional ConfigExecutionProvider(const GenAIConfig& config) { + const auto config_provider = config.DefaultProvider(); + if (config_provider.empty()) { + return std::nullopt; + } + + auto provider = EPUtils::StringtoEP(config_provider); + if (provider == ExecutionProvider::kUnknown) { + return std::nullopt; + } + + return std::string(EPUtils::EPtoRegistrationName(provider)); +} + +std::optional DeviceTypeForExecutionProvider(std::string_view provider_name) { + const auto provider = EPUtils::StringtoEP(provider_name); + switch (provider) { + case ExecutionProvider::kCPU: + return "CPU"; + case ExecutionProvider::kCUDA: + case ExecutionProvider::kWebGPU: + case ExecutionProvider::kTensorRT_RTX: + return "GPU"; + case ExecutionProvider::kVitisAI: + case ExecutionProvider::kRyzenAI: + return "NPU"; + default: + return std::nullopt; + } +} + +void ApplyTaskDefaults(ModelInfo& info) { + if (info.task == "chat-completion") { + if (!info.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR)) { + info.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR, "text"); + } + if (!info.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR)) { + info.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, "text"); + } + } else if (info.task == "vision-language-chat") { + if (!info.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR)) { + info.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR, "text,image"); + } + if (!info.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR)) { + info.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, "text"); + } + } else if (info.task == "automatic-speech-recognition") { + if (!info.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR)) { + info.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR, "audio"); + } + if (!info.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR)) { + info.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, "text"); + } + } +} + ParsedModelId ParseModelId(const std::string& model_id) { const auto separator = model_id.find(':'); if (separator == std::string::npos || separator == 0 || separator + 1 == model_id.size() || @@ -118,6 +175,25 @@ void RemoveLegacyRegistrationProperties(ModelInfo& info) { info.int_properties.erase(kLegacyVersionProperty); } +void RemoveSdkOwnedProperty(ModelInfo& info, std::string_view key) { + if (const auto string_property = info.string_properties.find(key); + string_property != info.string_properties.end()) { + info.string_properties.erase(string_property); + } + if (const auto int_property = info.int_properties.find(key); int_property != info.int_properties.end()) { + info.int_properties.erase(int_property); + } +} + +void RemoveSdkOwnedProperties(ModelInfo& info) { + RemoveSdkOwnedProperty(info, FOUNDRY_LOCAL_MODEL_PROP_MODEL_PROVIDER_STR); + RemoveSdkOwnedProperty(info, FOUNDRY_LOCAL_MODEL_PROP_ENTITY_TYPE_STR); + RemoveSdkOwnedProperty(info, FOUNDRY_LOCAL_MODEL_PROP_MODEL_TYPE_STR); + RemoveSdkOwnedProperty(info, FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT); + RemoveSdkOwnedProperty(info, FOUNDRY_LOCAL_MODEL_PROP_CREATED_AT_UNIX_INT); + RemoveSdkOwnedProperty(info, FOUNDRY_LOCAL_MODEL_PROP_CREATION_TIME_STR); +} + bool IsLegacyIntegerProperty(std::string_view key) { return key == FOUNDRY_LOCAL_MODEL_PROP_SUPPORTS_TOOL_CALLING_INT || key == FOUNDRY_LOCAL_MODEL_PROP_SUPPORTS_REASONING_INT || @@ -217,7 +293,7 @@ Model* LocalModelCatalog::RegisterModel(const std::string& model_path_value, con FL_THROW(FOUNDRY_LOCAL_ERROR_INVALID_ARGUMENT, "model_path must contain a regular genai_config.json file"); } - GenAIConfig::LoadFromFile(config_path.string()); + const auto genai_config = GenAIConfig::LoadFromFile(config_path.string()); ListModels(); Registration registration; @@ -232,7 +308,8 @@ Model* LocalModelCatalog::RegisterModel(const std::string& model_path_value, con FL_THROW(FOUNDRY_LOCAL_ERROR_INVALID_ARGUMENT, "model_id is already registered: " + model_id); } - registration = {ResolveMetadata(metadata, model_id, parsed_id.name, parsed_id.version), model_path.string()}; + registration = { + ResolveMetadata(metadata, model_id, parsed_id.name, parsed_id.version, genai_config), model_path.string()}; registrations.push_back(registration); SaveRegistrations(registrations); @@ -316,22 +393,47 @@ void LocalModelCatalog::UnregisterModel(const std::string& alias_or_model_id) { } ModelInfo LocalModelCatalog::ResolveMetadata(const ModelInfo& metadata, const std::string& model_id, - const std::string& name, int version) const { + const std::string& name, int version, + const GenAIConfig& genai_config) const { auto resolved = metadata; RemoveLegacyRegistrationProperties(resolved); + RemoveSdkOwnedProperties(resolved); + resolved.detected_region.clear(); + resolved.prompt_templates = {}; + resolved.model_settings = {}; resolved.alias = DeriveAlias(name); resolved.name = name; resolved.version = version; resolved.model_id = model_id; resolved.uri.clear(); + if (!resolved.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_DISPLAY_NAME_STR)) { + resolved.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_DISPLAY_NAME_STR, name); + } if (!resolved.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_PUBLISHER_STR)) { resolved.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_PUBLISHER_STR, "local"); } + if (!resolved.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_EP_STR)) { + if (const auto provider = ConfigExecutionProvider(genai_config)) { + resolved.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_EP_STR, *provider); + } + } + if (!resolved.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_DEVICE_TYPE_STR)) { + if (const auto* provider = resolved.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_EP_STR)) { + if (const auto device_type = DeviceTypeForExecutionProvider(*provider)) { + resolved.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_DEVICE_TYPE_STR, std::string(*device_type)); + } + } + } + ApplyTaskDefaults(resolved); resolved.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_MODEL_PROVIDER_STR, "LocalRegistration"); resolved.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_ENTITY_TYPE_STR, "Model"); resolved.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_MODEL_TYPE_STR, "ONNX"); + if (genai_config.model && genai_config.model->context_length > 0) { + resolved.SetPropertyInt(FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT, genai_config.model->context_length); + } + const auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); resolved.SetPropertyInt(FOUNDRY_LOCAL_MODEL_PROP_CREATED_AT_UNIX_INT, now); resolved.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_CREATION_TIME_STR, FormatUtcTimestamp(now)); diff --git a/sdk_v2/cpp/src/catalog/local_model_catalog.h b/sdk_v2/cpp/src/catalog/local_model_catalog.h index 51d663238..273a6283d 100644 --- a/sdk_v2/cpp/src/catalog/local_model_catalog.h +++ b/sdk_v2/cpp/src/catalog/local_model_catalog.h @@ -10,6 +10,8 @@ namespace fl { +struct GenAIConfig; + /// Mutable, persistent catalog for models registered from arbitrary local directories. class LocalModelCatalog final : public BaseModelCatalog { public: @@ -32,7 +34,7 @@ class LocalModelCatalog final : public BaseModelCatalog { private: ModelInfo ResolveMetadata(const ModelInfo& metadata, const std::string& model_id, - const std::string& name, int version) const; + const std::string& name, int version, const GenAIConfig& genai_config) const; std::vector LoadRegistrations() const; void SaveRegistrations(const std::vector& registrations) const; Model CreateModel(const Registration& registration) const; diff --git a/sdk_v2/cpp/src/inferencing/execution_provider.h b/sdk_v2/cpp/src/inferencing/execution_provider.h index b1e06295c..02a46f9b2 100644 --- a/sdk_v2/cpp/src/inferencing/execution_provider.h +++ b/sdk_v2/cpp/src/inferencing/execution_provider.h @@ -36,7 +36,8 @@ struct EPUtils { return ExecutionProvider::kCUDA; } else if (ep == "webgpu" || ep == "WebGPU" || - ep == "WebGPUExecutionProvider") { + ep == "WebGPUExecutionProvider" || + ep == "WebGpuExecutionProvider") { return ExecutionProvider::kWebGPU; } else if (ep == "openvino" || ep == "OpenVINO" || @@ -50,7 +51,8 @@ struct EPUtils { ep == "VitisAI" || ep == "VitisAIExecutionProvider") { return ExecutionProvider::kVitisAI; - } else if (ep == "RyzenAI") { + } else if (ep == "RyzenAI" || + ep == "RyzenAIExecutionProvider") { return ExecutionProvider::kRyzenAI; } else if (ep == "qnn" || ep == "QNN" || diff --git a/sdk_v2/cpp/test/internal_api/c_api_test.cc b/sdk_v2/cpp/test/internal_api/c_api_test.cc index ab1f8445d..0e5bf0e6b 100644 --- a/sdk_v2/cpp/test/internal_api/c_api_test.cc +++ b/sdk_v2/cpp/test/internal_api/c_api_test.cc @@ -338,7 +338,8 @@ TEST(CApiTest, LocalCatalogRegistersListsAndUnregistersWithoutOwningAssets) { const auto app_data_path = root.path() / "appdata"; const auto model_cache_path = root.path() / "cache" / "models"; std::filesystem::create_directories(model_path); - std::ofstream(model_path / "genai_config.json") << R"({"model":{"type":"phi3"}})"; + std::ofstream(model_path / "genai_config.json") + << R"({"model":{"type":"phi3","context_length":4096}})"; const flApi* api = GetApi(); const flConfigurationApi* config_api = api->GetConfigurationApi(); @@ -373,6 +374,7 @@ TEST(CApiTest, LocalCatalogRegistersListsAndUnregistersWithoutOwningAssets) { ASSERT_FL_OK(api, model_api->Info_SetStringProperty(metadata, FOUNDRY_LOCAL_MODEL_PROP_TASK_STR, "chat-completion")); ASSERT_FL_OK(api, model_api->Info_SetIntProperty(metadata, FOUNDRY_LOCAL_MODEL_PROP_FILESIZE_MB_INT, 17)); + ASSERT_FL_OK(api, model_api->Info_SetIntProperty(metadata, FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT, 17)); flModel* registered = nullptr; ASSERT_FL_OK(api, catalog_api->RegisterModel(catalog, model_path.string().c_str(), "c-api-model:3", metadata, @@ -387,6 +389,14 @@ TEST(CApiTest, LocalCatalogRegistersListsAndUnregistersWithoutOwningAssets) { EXPECT_EQ(model_api->Info_GetVersion(registered_info), 3); EXPECT_STREQ(model_api->Info_GetTask(registered_info), "chat-completion"); EXPECT_EQ(model_api->Info_GetIntProperty(registered_info, FOUNDRY_LOCAL_MODEL_PROP_FILESIZE_MB_INT, -1), 17); + EXPECT_EQ(model_api->Info_GetIntProperty(registered_info, FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT, -1), 4096); + EXPECT_STREQ(model_api->Info_GetStringProperty(registered_info, FOUNDRY_LOCAL_MODEL_PROP_DISPLAY_NAME_STR), + "c-api-model"); + EXPECT_STREQ(model_api->Info_GetStringProperty(registered_info, FOUNDRY_LOCAL_MODEL_PROP_PUBLISHER_STR), "local"); + EXPECT_STREQ(model_api->Info_GetStringProperty(registered_info, FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR), + "text"); + EXPECT_STREQ(model_api->Info_GetStringProperty(registered_info, FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR), + "text"); ModelListGuard models_guard(nullptr, [api](flModelList* value) { api->ModelList_Release(value); }); flModelList* models = nullptr; diff --git a/sdk_v2/cpp/test/internal_api/execution_provider_test.cc b/sdk_v2/cpp/test/internal_api/execution_provider_test.cc index c31f0139f..041a8b736 100644 --- a/sdk_v2/cpp/test/internal_api/execution_provider_test.cc +++ b/sdk_v2/cpp/test/internal_api/execution_provider_test.cc @@ -53,3 +53,20 @@ TEST(ExecutionProviderTest, RoundTripPreservesSupportedNonDefaultProviders) { EXPECT_EQ(EPUtils::StringtoEP(EPUtils::EPtoGenAI(provider)), provider); } } + +TEST(ExecutionProviderTest, RegistrationNameRoundTripPreservesSupportedProviders) { + const auto providers = { + ExecutionProvider::kCPU, + ExecutionProvider::kCUDA, + ExecutionProvider::kWebGPU, + ExecutionProvider::kOpenVINO, + ExecutionProvider::kTensorRT_RTX, + ExecutionProvider::kVitisAI, + ExecutionProvider::kRyzenAI, + ExecutionProvider::kQNN, + }; + + for (auto provider : providers) { + EXPECT_EQ(EPUtils::StringtoEP(EPUtils::EPtoRegistrationName(provider)), provider); + } +} diff --git a/sdk_v2/cpp/test/internal_api/local_model_catalog_test.cc b/sdk_v2/cpp/test/internal_api/local_model_catalog_test.cc index 39c23ee42..4db6a0f2a 100644 --- a/sdk_v2/cpp/test/internal_api/local_model_catalog_test.cc +++ b/sdk_v2/cpp/test/internal_api/local_model_catalog_test.cc @@ -58,15 +58,17 @@ class LocalModelCatalogTest : public ::testing::Test { LocalModelCatalog catalog_; }; -TEST_F(LocalModelCatalogTest, RegisterPreservesCallerMetadataAndWritesLocalModelInfoCache) { +TEST_F(LocalModelCatalogTest, RegisterResolvesMetadataAndWritesLocalModelInfoCache) { auto info = MakeMetadata(); info.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR, "text,image"); info.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, "text"); info.SetPropertyInt(FOUNDRY_LOCAL_MODEL_PROP_FILESIZE_MB_INT, 321); + info.SetPropertyInt(FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT, 123); info.SetPropertyStr("custom_metadata", "preserved"); info.SetPropertyInt("custom_count", 42); info.prompt_templates.Add("user", "<|user|>{Content}<|end|>"); info.model_settings.Add("temperature", "0.5"); + info.detected_region = "caller-region"; info.SetPropertyStr("model_path", "ignored"); info.SetPropertyStr("alias", "ignored"); info.SetPropertyStr("version", "ignored"); @@ -83,11 +85,14 @@ TEST_F(LocalModelCatalogTest, RegisterPreservesCallerMetadataAndWritesLocalModel EXPECT_EQ(model->Info().alias, "my-model"); EXPECT_EQ(model->Info().version, 7); EXPECT_EQ(model->Info().task, "chat-completion"); + EXPECT_EQ(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_DISPLAY_NAME_STR, std::string{}), + "my-model-generic-cpu"); EXPECT_EQ(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR, std::string{}), "text,image"); EXPECT_EQ(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, std::string{}), "text"); EXPECT_EQ(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_FILESIZE_MB_INT, int64_t{-1}), 321); + EXPECT_EQ(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT, int64_t{-1}), 4096); EXPECT_EQ(model->Info().GetPropertyStr("model_path"), nullptr); EXPECT_EQ(model->Info().GetPropertyStr("alias"), nullptr); EXPECT_EQ(model->Info().GetPropertyStr("version"), nullptr); @@ -95,6 +100,9 @@ TEST_F(LocalModelCatalogTest, RegisterPreservesCallerMetadataAndWritesLocalModel EXPECT_EQ(model->Info().GetPropertyInt("alias"), nullptr); EXPECT_EQ(model->Info().GetPropertyInt("_local_registration_id"), nullptr); EXPECT_EQ(model->Info().GetPropertyInt("version"), nullptr); + EXPECT_TRUE(model->Info().prompt_templates.empty()); + EXPECT_TRUE(model->Info().model_settings.empty()); + EXPECT_TRUE(model->Info().detected_region.empty()); EXPECT_TRUE(model->IsCached()); EXPECT_FALSE(std::filesystem::exists(model_dir_ / "model_metadata.yml")); @@ -124,8 +132,96 @@ TEST_F(LocalModelCatalogTest, RegisterPreservesCallerMetadataAndWritesLocalModel ASSERT_NE(restored_model, nullptr); EXPECT_EQ(restored_model->Info().GetPropertyWithDefault("custom_metadata", std::string{}), "preserved"); EXPECT_EQ(restored_model->Info().GetPropertyWithDefault("custom_count", int64_t{-1}), 42); - EXPECT_STREQ(restored_model->Info().prompt_templates.Find("user"), "<|user|>{Content}<|end|>"); - EXPECT_STREQ(restored_model->Info().model_settings.Find("temperature"), "0.5"); + EXPECT_EQ(restored_model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT, int64_t{-1}), + 4096); + EXPECT_TRUE(restored_model->Info().prompt_templates.empty()); + EXPECT_TRUE(restored_model->Info().model_settings.empty()); + EXPECT_TRUE(restored_model->Info().detected_region.empty()); +} + +TEST_F(LocalModelCatalogTest, RegistrationDerivesDefaultsAndPreservesApplicationOverrides) { + std::ofstream(model_dir_ / "genai_config.json") + << R"({"model":{"type":"phi3","context_length":8192,"decoder":{"session_options":{"provider_options":[{"cuda":{}}]}}}})"; + + auto* derived = catalog_.RegisterModel(model_dir_.string(), "derived-model:1", MakeMetadata()); + + ASSERT_NE(derived, nullptr); + EXPECT_EQ(derived->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_DISPLAY_NAME_STR, std::string{}), + "derived-model"); + EXPECT_EQ(derived->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_PUBLISHER_STR, std::string{}), "local"); + EXPECT_EQ(derived->Info().execution_provider, "CUDAExecutionProvider"); + EXPECT_EQ(derived->Info().device_type, DeviceType::kGPU); + EXPECT_EQ(derived->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT, int64_t{-1}), 8192); + EXPECT_EQ(derived->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR, std::string{}), + "text"); + EXPECT_EQ(derived->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, std::string{}), + "text"); + + auto overrides = MakeMetadata(); + overrides.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_DISPLAY_NAME_STR, "Custom display name"); + overrides.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_PUBLISHER_STR, "Contoso"); + overrides.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_EP_STR, "CPUExecutionProvider"); + overrides.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_DEVICE_TYPE_STR, "NPU"); + overrides.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR, "custom-input"); + overrides.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, "custom-output"); + auto* overridden = catalog_.RegisterModel(model_dir_.string(), "overridden-model:1", overrides); + + ASSERT_NE(overridden, nullptr); + EXPECT_EQ(overridden->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_DISPLAY_NAME_STR, std::string{}), + "Custom display name"); + EXPECT_EQ(overridden->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_PUBLISHER_STR, std::string{}), + "Contoso"); + EXPECT_EQ(overridden->Info().execution_provider, "CPUExecutionProvider"); + EXPECT_EQ(overridden->Info().device_type, DeviceType::kNPU); + EXPECT_EQ(overridden->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR, std::string{}), + "custom-input"); + EXPECT_EQ(overridden->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, std::string{}), + "custom-output"); +} + +TEST_F(LocalModelCatalogTest, RegistrationOverwritesSdkOwnedMetadata) { + auto metadata = MakeMetadata(); + metadata.model_id = "caller-id:99"; + metadata.name = "caller-name"; + metadata.version = 99; + metadata.alias = "caller-alias"; + metadata.uri = "caller://uri"; + metadata.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_MODEL_PROVIDER_STR, "CallerProvider"); + metadata.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_ENTITY_TYPE_STR, "CallerEntity"); + metadata.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_MODEL_TYPE_STR, "CallerType"); + metadata.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_CREATION_TIME_STR, "2000-01-01T00:00:00Z"); + metadata.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT, "caller-context"); + metadata.SetPropertyInt(FOUNDRY_LOCAL_MODEL_PROP_CREATED_AT_UNIX_INT, 1); + metadata.SetPropertyInt(FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT, 123); + + auto* model = catalog_.RegisterModel(model_dir_.string(), "sdk-owned-generic-cpu:4", metadata); + + ASSERT_NE(model, nullptr); + EXPECT_EQ(model->Info().model_id, "sdk-owned-generic-cpu:4"); + EXPECT_EQ(model->Info().name, "sdk-owned-generic-cpu"); + EXPECT_EQ(model->Info().version, 4); + EXPECT_EQ(model->Info().alias, "sdk-owned"); + EXPECT_TRUE(model->Info().uri.empty()); + EXPECT_EQ(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_MODEL_PROVIDER_STR, std::string{}), + "LocalRegistration"); + EXPECT_EQ(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_ENTITY_TYPE_STR, std::string{}), "Model"); + EXPECT_EQ(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_MODEL_TYPE_STR, std::string{}), "ONNX"); + EXPECT_NE(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_CREATION_TIME_STR, std::string{}), + "2000-01-01T00:00:00Z"); + EXPECT_NE(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_CREATED_AT_UNIX_INT, int64_t{0}), 1); + EXPECT_EQ(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT, int64_t{-1}), 4096); + EXPECT_EQ(model->Info().GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT), nullptr); +} + +TEST_F(LocalModelCatalogTest, RegistrationDoesNotUseCallerContextLengthWhenConfigOmitsIt) { + std::ofstream(model_dir_ / "genai_config.json") << R"({"model":{"type":"phi3"}})"; + auto metadata = MakeMetadata(); + metadata.SetPropertyInt(FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT, 123); + + auto* model = catalog_.RegisterModel(model_dir_.string(), "no-context:1", metadata); + + ASSERT_NE(model, nullptr); + EXPECT_EQ(model->Info().GetPropertyInt(FOUNDRY_LOCAL_MODEL_PROP_CONTEXT_LENGTH_INT), nullptr); } TEST_F(LocalModelCatalogTest, RegistrationRequiresExistingDirectoryAndParseableConfig) { From 7f219db1692e7c78bb0f2085c204487212df6964 Mon Sep 17 00:00:00 2001 From: Selena Yang <179177246+selenayang888@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:16:58 -0700 Subject: [PATCH 2/2] Resolve Copilot comments --- .../include/foundry_local/foundry_local_c.h | 7 ++++- .../include/foundry_local/foundry_local_cpp.h | 5 ++- sdk_v2/cpp/src/catalog/local_model_catalog.cc | 11 +++++++ .../internal_api/local_model_catalog_test.cc | 31 ++++++++++++++++++- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/sdk_v2/cpp/include/foundry_local/foundry_local_c.h b/sdk_v2/cpp/include/foundry_local/foundry_local_c.h index 4bce94459..2e402db70 100644 --- a/sdk_v2/cpp/include/foundry_local/foundry_local_c.h +++ b/sdk_v2/cpp/include/foundry_local/foundry_local_c.h @@ -1036,7 +1036,12 @@ struct flCatalogApi { /// Register a model in a local catalog without taking ownership of its assets. /// `model_path` must identify a model directory containing genai_config.json. /// `model_id` must use the canonical `:` format and be unique in the local catalog. - /// The metadata is copied; model identity and location are taken only from the explicit arguments. + /// `metadata.task` is required. Application-owned properties, including display name, publisher, runtime, + /// modalities, and arbitrary string or integer properties, are preserved; missing values receive authoritative + /// defaults when available. + /// Identity, alias, type, timestamps, context length, and prompt templates are derived by the SDK from the + /// arguments, registration time, and genai_config.json. Caller-supplied location and internal catalog metadata are + /// ignored. FL_API_STATUS(RegisterModel, _In_ flCatalog* catalog, _In_ const char* model_path, _In_ const char* model_id, _In_ const flModelInfo* metadata, _Outptr_ flModel** out_model); diff --git a/sdk_v2/cpp/include/foundry_local/foundry_local_cpp.h b/sdk_v2/cpp/include/foundry_local/foundry_local_cpp.h index 3a0e0c16c..c047a4142 100644 --- a/sdk_v2/cpp/include/foundry_local/foundry_local_cpp.h +++ b/sdk_v2/cpp/include/foundry_local/foundry_local_cpp.h @@ -808,7 +808,10 @@ class ICatalog { const std::string& variant_name = {}, int max_versions = 50) = 0; - /// Register existing local model assets. `model_id` must use `:`; metadata is copied. + /// Register existing local model assets. `model_id` must use `:` and `metadata.Task()` is required. + /// Application-owned properties are preserved and may receive authoritative defaults. Identity, alias, type, + /// timestamps, context length, and prompt templates are SDK-derived; caller location and internal metadata are + /// ignored. /// The catalog does not take ownership of `model_path` and never deletes its contents. virtual std::unique_ptr RegisterModel(const std::string& model_path, const std::string& model_id, const ModelInfo& metadata) = 0; diff --git a/sdk_v2/cpp/src/catalog/local_model_catalog.cc b/sdk_v2/cpp/src/catalog/local_model_catalog.cc index 9ade1697d..9c67130f6 100644 --- a/sdk_v2/cpp/src/catalog/local_model_catalog.cc +++ b/sdk_v2/cpp/src/catalog/local_model_catalog.cc @@ -76,6 +76,7 @@ std::optional DeviceTypeForExecutionProvider(std::string_view return "GPU"; case ExecutionProvider::kVitisAI: case ExecutionProvider::kRyzenAI: + case ExecutionProvider::kQNN: return "NPU"; default: return std::nullopt; @@ -104,6 +105,13 @@ void ApplyTaskDefaults(ModelInfo& info) { if (!info.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR)) { info.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, "text"); } + } else if (info.task == "embeddings") { + if (!info.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR)) { + info.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR, "text"); + } + if (!info.GetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR)) { + info.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, "embeddings"); + } } } @@ -402,6 +410,9 @@ ModelInfo LocalModelCatalog::ResolveMetadata(const ModelInfo& metadata, const st resolved.detected_region.clear(); resolved.prompt_templates = {}; resolved.model_settings = {}; + if (genai_config.model) { + resolved.prompt_templates.CopyFromMap(genai_config.model->prompt_templates); + } resolved.alias = DeriveAlias(name); resolved.name = name; resolved.version = version; diff --git a/sdk_v2/cpp/test/internal_api/local_model_catalog_test.cc b/sdk_v2/cpp/test/internal_api/local_model_catalog_test.cc index 4db6a0f2a..9ba14eec7 100644 --- a/sdk_v2/cpp/test/internal_api/local_model_catalog_test.cc +++ b/sdk_v2/cpp/test/internal_api/local_model_catalog_test.cc @@ -141,7 +141,9 @@ TEST_F(LocalModelCatalogTest, RegisterResolvesMetadataAndWritesLocalModelInfoCac TEST_F(LocalModelCatalogTest, RegistrationDerivesDefaultsAndPreservesApplicationOverrides) { std::ofstream(model_dir_ / "genai_config.json") - << R"({"model":{"type":"phi3","context_length":8192,"decoder":{"session_options":{"provider_options":[{"cuda":{}}]}}}})"; + << R"({"model":{"type":"phi3","context_length":8192,)" + R"("prompt_templates":{"user":"<|user|>{Content}<|end|>"},)" + R"("decoder":{"session_options":{"provider_options":[{"cuda":{}}]}}}})"; auto* derived = catalog_.RegisterModel(model_dir_.string(), "derived-model:1", MakeMetadata()); @@ -156,6 +158,12 @@ TEST_F(LocalModelCatalogTest, RegistrationDerivesDefaultsAndPreservesApplication "text"); EXPECT_EQ(derived->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, std::string{}), "text"); + EXPECT_STREQ(derived->Info().prompt_templates.Find("user"), "<|user|>{Content}<|end|>"); + + auto restored = MakeCatalog(); + auto* restored_derived = restored.GetModelVariant("derived-model:1"); + ASSERT_NE(restored_derived, nullptr); + EXPECT_STREQ(restored_derived->Info().prompt_templates.Find("user"), "<|user|>{Content}<|end|>"); auto overrides = MakeMetadata(); overrides.SetPropertyStr(FOUNDRY_LOCAL_MODEL_PROP_DISPLAY_NAME_STR, "Custom display name"); @@ -179,6 +187,27 @@ TEST_F(LocalModelCatalogTest, RegistrationDerivesDefaultsAndPreservesApplication "custom-output"); } +TEST_F(LocalModelCatalogTest, RegistrationDerivesQnnDeviceType) { + std::ofstream(model_dir_ / "genai_config.json") + << R"({"model":{"type":"phi3","decoder":{"session_options":{"provider_options":[{"qnn":{}}]}}}})"; + + auto* model = catalog_.RegisterModel(model_dir_.string(), "qnn-model:1", MakeMetadata()); + + ASSERT_NE(model, nullptr); + EXPECT_EQ(model->Info().execution_provider, "QNNExecutionProvider"); + EXPECT_EQ(model->Info().device_type, DeviceType::kNPU); +} + +TEST_F(LocalModelCatalogTest, RegistrationDerivesEmbeddingModalities) { + auto* model = catalog_.RegisterModel(model_dir_.string(), "embedding-model:1", MakeMetadata("embeddings")); + + ASSERT_NE(model, nullptr); + EXPECT_EQ(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_INPUT_MODALITIES_STR, std::string{}), + "text"); + EXPECT_EQ(model->Info().GetPropertyWithDefault(FOUNDRY_LOCAL_MODEL_PROP_OUTPUT_MODALITIES_STR, std::string{}), + "embeddings"); +} + TEST_F(LocalModelCatalogTest, RegistrationOverwritesSdkOwnedMetadata) { auto metadata = MakeMetadata(); metadata.model_id = "caller-id:99";