Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3a6155b
Enable live catalog with region-aware fallback engine
baijumeswani Jun 9, 2026
cc1e22f
Add AzureAiStudio User-Agent
baijumeswani Jun 11, 2026
a827ec8
Address pr review comments
baijumeswani Jun 12, 2026
a75bb36
integrate get model versions into cpp core
selenayang888 Jun 15, 2026
fd865f3
Default registry region to centralus, httprequestoptions, and config …
baijumeswani Jun 15, 2026
c872980
Merge branch 'main' of https://github.com/microsoft/Foundry-Local int…
baijumeswani Jun 15, 2026
22f5b0f
Address pull-request review comments
baijumeswani Jun 16, 2026
d4b61f3
Address pull-request review comments
baijumeswani Jun 16, 2026
6739874
add max_version and continuation token
selenayang888 Jun 17, 2026
f0fb73c
Change all defaults to centralus
baijumeswani Jun 17, 2026
859d927
Address pull-request review comments
baijumeswani Jun 17, 2026
7a3d089
add a new test with empty alias and sort the results
selenayang888 Jun 17, 2026
b73c5e1
Merge remote-tracking branch 'origin/baijumeswani/catalog' into syang…
selenayang888 Jun 17, 2026
e7802c7
Merge remote-tracking branch 'origin/main' into syang/integrate-get-m…
selenayang888 Jun 18, 2026
2a37f65
Resolve all the comments
selenayang888 Jun 23, 2026
ffb7f46
put "CompareCaseInsensitive" in utils/string_utils.h
selenayang888 Jun 23, 2026
850358e
Merge remote-tracking branch 'origin/main' into syang/integrate-get-m…
selenayang888 Jun 24, 2026
1a31fc3
Resolved three comments
selenayang888 Jun 25, 2026
7164318
Resolve two comments
selenayang888 Jun 25, 2026
e9aa2f4
Resolved `GetModelVersions` , local copy and refactor `catalog_urls_ `
selenayang888 Jun 26, 2026
e4401ce
Addressing the comment for ordering of `CompareModelPointersForVersio…
selenayang888 Jun 26, 2026
94a1796
Merge remote-tracking branch 'origin/main' into syang/integrate-get-m…
selenayang888 Jun 26, 2026
bbfb6b4
Align with CompareModelsForSort
selenayang888 Jun 27, 2026
2f9d1d2
Resolved "Replacing on each call creates potential issues on the clie…
selenayang888 Jun 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions sdk_v2/cpp/include/foundry_local/foundry_local_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,20 @@ struct flCatalogApi {
FL_API_STATUS(GetCachedModels, _In_ const flCatalog* catalog, _Outptr_ flModelList** out_models);
FL_API_STATUS(GetLoadedModels, _In_ const flCatalog* catalog, _Outptr_ flModelList** out_models);

/// Get all versions of a model alias, optionally narrowed to a specific model name.
/// @param model_alias Alias of the model (e.g. "phi-4-mini"). Must be non-NULL and non-empty.
/// @param model_name Optional model name (ModelInfo.Name, e.g. "Phi-4-generic-gpu"). NULL returns
/// every model name.
/// @param max_versions Select latest X versions per model name. Pass 0 (or any
/// negative value) for no per-model-name cap.
/// Each call performs a fresh catalog query; results are not integrated into the
/// catalog's main lookup indices. Returned handles are owned by the catalog until
/// the next GetModelVersions call for the same alias or until the catalog is released.
/// Queries for different aliases do not invalidate each other's results.
/// Releasing the list does not invalidate the underlying model handles.
FL_API_STATUS(GetModelVersions, _In_ const flCatalog* catalog, _In_ const char* model_alias,
_In_opt_ const char* model_name, int32_t max_versions, _Outptr_ flModelList** out_models);
Comment on lines +958 to +970

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we note that (IIUC) these models will be returned by catalog operations in the current instance (e.g. list models) but are not saved in the model cache on disk so won't be present on restart? I think that behavior is fine but it could be a little surprising.

@selenayang888 selenayang888 Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is how design doc specified for caching strategy. get_model_versions results are not cached in BaseModelCatalog. Each call to get_model_versions does a fresh catalog query. However, to support download an older version", the resolved ModelInfo for that version is temporarily added to the catalog's modelIdToInfo index.

The link below is the part in the design doc for Caching Strategy for All-Versions Data:

model-versions-design.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we explain that behavior to the user in the comments here as it's a little opaque?

@selenayang888 selenayang888 Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the explanation of the behavior in the comments now.
Moreover, based on the design doc requirements for caching strategy, I updated the GetModelVersions in base_model_catalog.cc:475 no longer calls IntegrateVariants. It now stores the fetched models in transient per-catalog query storage and returns those handles without adding them to the main alias/id indices.


// End V1
};

Expand Down
15 changes: 15 additions & 0 deletions sdk_v2/cpp/include/foundry_local/foundry_local_cpp.h
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,18 @@ class ICatalog {
virtual std::unique_ptr<IModel> GetModel(const std::string& alias) const = 0;
virtual std::unique_ptr<IModel> GetModelVariant(const std::string& model_id) const = 0;
virtual std::unique_ptr<IModel> GetLatestVersion(const IModel& model) const = 0;

/// Get all versions of a model alias. `model_alias` must be non-empty.
/// `variant_name` optionally narrows the result to a single variant; empty
/// returns every variant. `max_versions` selects the latest X versions per
/// variant name (defaults to 50, matching the web service contract); pass 0
/// or a negative value for no per-variant cap. Each call performs a fresh
/// query and the returned model handles remain valid until the next
/// GetModelVersions call for the same alias or until the catalog is destroyed.
/// Queries for different aliases do not invalidate each other's results.
virtual ModelList GetModelVersions(const std::string& model_alias,
const std::string& variant_name = {},
int max_versions = 50) = 0;
};

// ===========================================================================
Expand All @@ -787,6 +799,9 @@ class Catalog final : public ICatalog {
std::unique_ptr<IModel> GetModel(const std::string& alias) const override;
std::unique_ptr<IModel> GetModelVariant(const std::string& model_id) const override;
std::unique_ptr<IModel> GetLatestVersion(const IModel& model) const override;
ModelList GetModelVersions(const std::string& model_alias,
const std::string& variant_name = {},
int max_versions = 50) override;

private:
detail::Base<flCatalog> handle_;
Expand Down
13 changes: 13 additions & 0 deletions sdk_v2/cpp/include/foundry_local/foundry_local_cpp.inline.h
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,19 @@ inline std::unique_ptr<IModel> Catalog::GetLatestVersion(const IModel& model) co
return std::make_unique<Model>(*m);
}

inline ModelList Catalog::GetModelVersions(const std::string& model_alias,
const std::string& variant_name,
int max_versions) {
flModelList* models = nullptr;
Check(detail::catalog_api()->GetModelVersions(
handle_.get(),
model_alias.c_str(),
variant_name.empty() ? nullptr : variant_name.c_str(),
max_versions,
&models));
return ModelList(*models);
}

// ===========================================================================
// Item
// ===========================================================================
Expand Down
29 changes: 29 additions & 0 deletions sdk_v2/cpp/src/c_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,34 @@ FL_API_STATUS_IMPL(Catalog_GetNameImpl, const flCatalog* catalog, const char** o
API_IMPL_END
}

FL_API_STATUS_IMPL(Catalog_GetModelVersionsImpl, const flCatalog* catalog,
const char* model_alias, const char* model_name,
int32_t max_versions, flModelList** out_models) {
API_IMPL_BEGIN
if (!catalog || !model_alias || !out_models) {
return MakeStatus(FOUNDRY_LOCAL_ERROR_INVALID_ARGUMENT, "null argument");
}

if (*model_alias == '\0') {
return MakeStatus(FOUNDRY_LOCAL_ERROR_INVALID_ARGUMENT, "model_alias must not be empty");
}

std::string alias = model_alias;
std::string model_name_filter = model_name ? model_name : std::string{};

auto models = catalog->impl.GetModelVersions(alias, model_name_filter, max_versions);
auto list = std::make_unique<flModelList>();
list->items.reserve(models.size());

for (auto* m : models) {
list->items.push_back(AsHandle<flModel>(m));
}

*out_models = list.release();
return nullptr;
API_IMPL_END
}

static const flCatalogApi g_catalog_api = {
Catalog_GetNameImpl,
Catalog_GetModelsImpl,
Expand All @@ -695,6 +723,7 @@ static const flCatalogApi g_catalog_api = {
Catalog_GetLatestVersionImpl,
Catalog_GetCachedModelsImpl,
Catalog_GetLoadedModelsImpl,
Catalog_GetModelVersionsImpl,
};

// ========================================================================
Expand Down
20 changes: 20 additions & 0 deletions sdk_v2/cpp/src/catalog.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,26 @@ class ICatalog {
/// Gets the latest version of a model. Returns nullptr if not found.
virtual Model* GetLatestVersion(const Model* model) const = 0;

/// Lists all known versions of a model (by alias), optionally filtered to a
/// specific variant name. Bypasses the "latest only" filter the regular
/// catalog refresh applies and performs a fresh source query on each call.
/// Results are not integrated into the catalog's main lookup indices.
/// Returned pointers are owned by the catalog until the next
/// GetModelVersions call for the same alias or until the catalog is destroyed.
/// Queries for different aliases do not invalidate each other's results.
///
/// `model_alias` is the alias of the model (e.g. "phi-4-mini") and must not
/// be empty.
/// `variant_name` optionally narrows results to a specific variant (e.g.
/// "Phi-4-generic-gpu"). Pass an empty string to return every variant.
/// `max_versions` selects the latest X versions per variant name for the
/// alias. 0 or negative means no per-variant cap.
///
/// Maps to C# `IModelCatalog.GetModelVersionsAsync`.
virtual std::vector<Model*> GetModelVersions(const std::string& model_alias,
const std::string& variant_name,
int max_versions = 0) = 0;

/// Lists only models that are cached locally.
virtual std::vector<Model*> GetCachedModels() const = 0;

Expand Down
59 changes: 52 additions & 7 deletions sdk_v2/cpp/src/catalog/azure_catalog_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#include <nlohmann/json.hpp>

#include <algorithm>
#include <cstdint>
#include <iterator>
#include <memory>
#include <optional>
Expand Down Expand Up @@ -183,18 +185,34 @@ std::vector<ModelInfo> ToModelInfos(const std::vector<CatalogLocalModel>& raw_mo
return infos;
}

std::vector<std::vector<CatalogFilter>> BuildSearchFilters(const IEpDetector& ep_detector,
const std::vector<std::string>& model_filter) {
/// Build per-device filter sets for catalog queries.
/// `latest_only` controls whether to include the `labels=latest` filter (default true for latest models).
/// `model_alias` scopes results to a specific alias when non-empty; when empty, no alias filter is applied.
/// `model_name` scopes results to a specific model name when non-empty for server-side filtering.
/// Each filter set queries for variants on a specific device/EP pair; the catalog API matches on the
/// (device, execution provider) pair.
std::vector<std::vector<CatalogFilter>> BuildSearchFilters(
const IEpDetector& ep_detector,
const std::vector<std::string>& model_filter,
bool latest_only = true,
const std::string& model_alias = "",
const std::string& model_name = "") {
std::vector<std::vector<CatalogFilter>> filter_sets;

// One filter set per detected device. The catalog API matches on the
// (device, execution provider) pair, so we keep the EPs grouped by device.
for (const auto& [device, eps] : ep_detector.GetAvailableDevicesToEPs()) {
std::vector<CatalogFilter> filters;
filters.push_back(MakeFilter("type", {"models"}));
filters.push_back(MakeFilter("kind", {"Versioned"}));
filters.push_back(MakeFilter("labels", {"latest"}));
if (latest_only) {
filters.push_back(MakeFilter("labels", {"latest"}));
}
filters.push_back(MakeFilter("annotations/tags/foundryLocal", model_filter));
if (!model_alias.empty()) {
filters.push_back(MakeFilter("annotations/tags/alias", {model_alias}));
}
if (!model_name.empty()) {
filters.push_back(MakeFilter("properties/name", {model_name}));
}
filters.push_back(MakeFilter("properties/variantInfo/variantMetadata/device", {ToLower(device)}));
filters.push_back(MakeFilter("properties/variantInfo/variantMetadata/executionProvider", eps));
filter_sets.push_back(std::move(filters));
Expand All @@ -203,6 +221,7 @@ std::vector<std::vector<CatalogFilter>> BuildSearchFilters(const IEpDetector& ep
return filter_sets;
}


std::vector<CatalogFilter> BuildModelIdFilters(const std::vector<std::string>& model_filter,
const std::vector<std::string>& model_ids) {
// Looking up specific IDs: no labels=latest (we want exact versions) and no
Expand Down Expand Up @@ -273,8 +292,7 @@ std::optional<AzureCatalogClient::FetchedFilterSet> AzureCatalogClient::FetchFil
if (regional && !pinned_region) {
// Page 1: run through region fallback starting from the sticky region (last known-good) or the active region.
// Exhaustion means every candidate had a retryable region-health failure, so fail just this filter set.
const std::string start =
region_fallback_.StickyRegion().value_or(region_);
const std::string start = region_fallback_.StickyRegion().value_or(region_);
try {
auto fallback_result = region_fallback_.Execute(start, [&](const std::string& r) {
return http_post_response_(BuildRegionalUrl(url_prefix_, url_suffix_, r), body);
Expand Down Expand Up @@ -384,6 +402,33 @@ std::vector<ModelInfo> AzureCatalogClient::FetchModelsByIds(
return ToModelInfos(result->models, result->region);
}

std::vector<ModelInfo> AzureCatalogClient::FetchAllVersionsByAlias(
const std::string& model_alias,
const std::string& model_name,
int /*max_versions*/) {
// Fetch all versions of the alias across per-device filter sets. Each filter set
// queries for variants matching the alias on a specific device/EP pair; the results
// are aggregated. The caller applies per-variant version caps (latest X per variant).
const auto filter_sets = BuildSearchFilters(ep_detector_, model_filter_, /*latest_only=*/false,
model_alias, model_name);

std::vector<ModelInfo> result;

for (const auto& filters : filter_sets) {
auto walk = FetchFilterSet(filters);
if (!walk) {
continue;
}

auto batch = ToModelInfos(walk->models, walk->region);
result.insert(result.end(),
std::make_move_iterator(batch.begin()),
std::make_move_iterator(batch.end()));
}

return result;
}

std::unique_ptr<ICatalogClient> MakeCatalogClient(
const std::string& base_url,
const std::string& filter_override,
Expand Down
10 changes: 10 additions & 0 deletions sdk_v2/cpp/src/catalog/azure_catalog_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ class AzureCatalogClient : public ICatalogClient {

std::vector<ModelInfo> FetchModelsByIds(const std::vector<std::string>& model_ids) override;

/// Fetch every version of `model_alias` from the live catalog by issuing the
/// per-device search with `labels=latest` removed and an alias filter added.
/// `model_alias` must be non-empty. Optionally filters by `model_name`.
/// The client fetches all matching versions, while the caller applies
/// `max_versions` semantics (latest X per variant).
std::vector<ModelInfo> FetchAllVersionsByAlias(const std::string& model_alias,
const std::string& model_name = "",
int max_versions = 0) override;

private:
struct FetchedFilterSet {
std::vector<CatalogLocalModel> models;
Expand All @@ -63,6 +72,7 @@ class AzureCatalogClient : public ICatalogClient {
std::optional<FetchedFilterSet> FetchFilterSet(const std::vector<CatalogFilter>& filters);

/// Fetch every device filter set, dropping the ones that failed their region-health checks.
/// Used for unbounded "latest only" / by-id queries.
std::vector<FetchedFilterSet> FetchAllFilterSets();

std::string base_url_;
Expand Down
Loading
Loading