From 08fa19f25a305aa6f1cd5df8d47be9427ddbf9cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:52:31 +0000 Subject: [PATCH 1/6] Python SDK: Update IModel with variant info, move get_latest_version to Catalog, use IModel return types Agent-Logs-Url: https://github.com/microsoft/Foundry-Local/sessions/8082508c-1338-48b2-bdd3-6c2c8e35e195 Co-authored-by: baijumeswani <12852605+baijumeswani@users.noreply.github.com> --- sdk/python/README.md | 8 +++- sdk/python/src/catalog.py | 69 ++++++++++++++++++++++++++------- sdk/python/src/imodel.py | 33 +++++++++++++++- sdk/python/src/model.py | 41 +++++++------------- sdk/python/src/model_variant.py | 22 +++++++++++ 5 files changed, 131 insertions(+), 42 deletions(-) diff --git a/sdk/python/README.md b/sdk/python/README.md index 7cc8b44c..83b0912f 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -140,6 +140,9 @@ cached = catalog.get_cached_models() # List currently loaded models loaded = catalog.get_loaded_models() + +# Check for a newer version of a model +latest = catalog.get_latest_version(model) ``` ### Loading and Running a Model @@ -200,9 +203,10 @@ manager.stop_web_service() |---|---| | `Configuration` | SDK configuration (app name, cache dir, log level, web service settings) | | `FoundryLocalManager` | Singleton entry point – initialization, catalog access, web service | -| `Catalog` | Model discovery – listing, lookup by alias/ID, cached/loaded queries | +| `Catalog` | Model discovery – listing, lookup by alias/ID, cached/loaded queries, latest version | +| `IModel` | Abstract interface for a model – shared by Model and ModelVariant | | `Model` | Groups variants under one alias – select, load, unload, create clients | -| `ModelVariant` | Specific model variant – download, cache, load/unload, create clients | +| `ModelVariant` | Specific model variant – download, cache, load/unload, create clients (implementation detail) | ### OpenAI Clients diff --git a/sdk/python/src/catalog.py b/sdk/python/src/catalog.py index 767a9f08..16d06ccd 100644 --- a/sdk/python/src/catalog.py +++ b/sdk/python/src/catalog.py @@ -11,6 +11,7 @@ from typing import List, Optional from pydantic import TypeAdapter +from .imodel import IModel from .model import Model from .model_variant import ModelVariant @@ -83,42 +84,42 @@ def _update_models(self): self._last_fetch = datetime.datetime.now() self._models = models - def list_models(self) -> List[Model]: + def list_models(self) -> List[IModel]: """ List the available models in the catalog. - :return: List of Model instances. + :return: List of IModel instances. """ self._update_models() return list(self._model_alias_to_model.values()) - def get_model(self, model_alias: str) -> Optional[Model]: + def get_model(self, model_alias: str) -> Optional[IModel]: """ Lookup a model by its alias. :param model_alias: Model alias. - :return: Model if found. + :return: IModel if found, or None. """ self._update_models() return self._model_alias_to_model.get(model_alias) - def get_model_variant(self, model_id: str) -> Optional[ModelVariant]: + def get_model_variant(self, model_id: str) -> Optional[IModel]: """ Lookup a model variant by its unique model id. :param model_id: Model id. - :return: Model variant if found. + :return: IModel if found, or None. """ self._update_models() return self._model_id_to_model_variant.get(model_id) - def get_cached_models(self) -> List[ModelVariant]: + def get_cached_models(self) -> List[IModel]: """ Get a list of currently downloaded models from the model cache. - :return: List of ModelVariant instances. + :return: List of IModel instances. """ self._update_models() cached_model_ids = get_cached_model_ids(self._core_interop) - cached_models = [] + cached_models: List[IModel] = [] for model_id in cached_model_ids: model_variant = self._model_id_to_model_variant.get(model_id) if model_variant is not None: @@ -126,19 +127,61 @@ def get_cached_models(self) -> List[ModelVariant]: return cached_models - def get_loaded_models(self) -> List[ModelVariant]: + def get_loaded_models(self) -> List[IModel]: """ Get a list of the currently loaded models. - :return: List of ModelVariant instances. + :return: List of IModel instances. """ self._update_models() loaded_model_ids = self._model_load_manager.list_loaded() - loaded_models = [] + loaded_models: List[IModel] = [] for model_id in loaded_model_ids: model_variant = self._model_id_to_model_variant.get(model_id) if model_variant is not None: loaded_models.append(model_variant) - return loaded_models \ No newline at end of file + return loaded_models + + def get_latest_version(self, model_or_variant: IModel) -> IModel: + """ + Get the latest version of a model. + This is used to check if a newer version of a model is available in the catalog for download. + + :param model_or_variant: The model to check for the latest version. + :return: The latest version of the model. Will match the input if it is the latest version. + :raises FoundryLocalException: If the model is not found in the catalog. + """ + self._update_models() + + if isinstance(model_or_variant, ModelVariant): + # For ModelVariant, resolve the owning Model via alias + model = self._model_alias_to_model.get(model_or_variant.alias) + elif isinstance(model_or_variant, Model): + model = model_or_variant + else: + # For other IModel implementations (e.g., test stubs), resolve via alias + model = self._model_alias_to_model.get(model_or_variant.alias) + + if model is None: + raise FoundryLocalException( + f"Model with alias '{model_or_variant.alias}' not found in catalog." + ) + + # variants are sorted by version, so the first one matching the name is the latest version. + latest = None + for v in model._variants: + if v.info.name == model_or_variant.info.name: + latest = v + break + + if latest is None: + raise FoundryLocalException( + f"Internal error. Mismatch between model (alias:{model.alias}) and " + f"model variant (alias:{model_or_variant.alias})." + ) + + # if input was the latest return the input (could be model or model variant) + # otherwise return the latest model variant + return model_or_variant if latest.id == model_or_variant.id else latest \ No newline at end of file diff --git a/sdk/python/src/imodel.py b/sdk/python/src/imodel.py index a092b98e..2a404f48 100644 --- a/sdk/python/src/imodel.py +++ b/sdk/python/src/imodel.py @@ -5,11 +5,14 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Callable, Optional +from typing import Callable, List, Optional, TYPE_CHECKING from .openai.chat_client import ChatClient from .openai.audio_client import AudioClient +if TYPE_CHECKING: + from .detail.model_data_types import ModelInfo + class IModel(ABC): """Abstract interface for a model that can be downloaded, loaded, and used for inference.""" @@ -25,6 +28,12 @@ def alias(self) -> str: """Model alias.""" pass + @property + @abstractmethod + def info(self) -> ModelInfo: + """Full catalog metadata for this model or variant.""" + pass + @property @abstractmethod def is_cached(self) -> bool: @@ -89,3 +98,25 @@ def get_audio_client(self) -> AudioClient: :return: AudioClient instance. """ pass + + @property + @abstractmethod + def variants(self) -> List[IModel]: + """Variants of the model that are available. Variants are optimized for different devices.""" + pass + + @property + @abstractmethod + def selected_variant(self) -> IModel: + """Currently selected model variant in use.""" + pass + + @abstractmethod + def select_variant(self, variant: IModel) -> None: + """ + Select a specific model variant from :attr:`variants` to use for IModel operations. + + :param variant: Model variant to select. Must be one of the variants in :attr:`variants`. + :raises FoundryLocalException: If variant is not valid for this model. + """ + pass diff --git a/sdk/python/src/model.py b/sdk/python/src/model.py index 4c8750ca..b0242f72 100644 --- a/sdk/python/src/model.py +++ b/sdk/python/src/model.py @@ -13,6 +13,7 @@ from .model_variant import ModelVariant from .exception import FoundryLocalException from .detail.core_interop import CoreInterop +from .detail.model_data_types import ModelInfo logger = logging.getLogger(__name__) @@ -27,7 +28,7 @@ def __init__(self, model_variant: ModelVariant, core_interop: CoreInterop): self._alias = model_variant.alias self._variants: List[ModelVariant] = [model_variant] # Variants are sorted by Core, so the first one added is the default - self._selected_variant = model_variant + self._selected_variant: ModelVariant = model_variant self._core_interop = core_interop def _add_variant(self, variant: ModelVariant) -> None: @@ -42,45 +43,28 @@ def _add_variant(self, variant: ModelVariant) -> None: if variant.info.cached and not self._selected_variant.info.cached: self._selected_variant = variant - def select_variant(self, variant: ModelVariant) -> None: + def select_variant(self, variant: IModel) -> None: """ - Select a specific model variant by its ModelVariant object. + Select a specific model variant from :attr:`variants`. The selected variant will be used for IModel operations. - :param variant: ModelVariant to select - :raises FoundryLocalException: If variant is not valid for this model + :param variant: Model variant to select. Must be one of the variants in :attr:`variants`. + :raises FoundryLocalException: If variant is not valid for this model. """ if variant not in self._variants: raise FoundryLocalException( - f"Model {self._alias} does not have a {variant.id} variant." + "Input variant was not found in Variants." ) self._selected_variant = variant - def get_latest_version(self, variant: ModelVariant) -> ModelVariant: - """ - Get the latest version of the specified model variant. - - :param variant: Model variant - :return: ModelVariant for latest version. Same as variant if that is the latest version - :raises FoundryLocalException: If variant is not valid for this model - """ - # Variants are sorted by version, so the first one matching the name is the latest version - for v in self._variants: - if v.info.name == variant.info.name: - return v - - raise FoundryLocalException( - f"Model {self._alias} does not have a {variant.id} variant." - ) - @property - def variants(self) -> List[ModelVariant]: + def variants(self) -> List[IModel]: """List of all variants for this model.""" - return self._variants.copy() # Return a copy to prevent external modification + return list(self._variants) # Return a copy to prevent external modification @property - def selected_variant(self) -> ModelVariant: + def selected_variant(self) -> IModel: """Currently selected variant.""" return self._selected_variant @@ -94,6 +78,11 @@ def alias(self) -> str: """Alias of this model.""" return self._alias + @property + def info(self) -> ModelInfo: + """Full catalog metadata for the currently selected variant.""" + return self._selected_variant.info + @property def is_cached(self) -> bool: """Is the currently selected variant cached locally?""" diff --git a/sdk/python/src/model_variant.py b/sdk/python/src/model_variant.py index f0d40109..f3ac4e3b 100644 --- a/sdk/python/src/model_variant.py +++ b/sdk/python/src/model_variant.py @@ -57,6 +57,28 @@ def info(self) -> ModelInfo: """Full catalog metadata for this variant.""" return self._model_info + @property + def variants(self) -> list[IModel]: + """Returns a list containing only this variant.""" + return [self] + + @property + def selected_variant(self) -> IModel: + """Returns this variant (a ModelVariant is its own selected variant).""" + return self + + def select_variant(self, variant: IModel) -> None: + """SelectVariant is not supported on a ModelVariant. + + Call ``Catalog.get_model(alias)`` to get a Model with all variants available. + + :raises FoundryLocalException: Always. + """ + raise FoundryLocalException( + f"select_variant is not supported on a ModelVariant. " + f"Call catalog.get_model(\"{self._alias}\") to get a Model with all variants available." + ) + @property def is_cached(self) -> bool: """``True`` if this variant is present in the local model cache.""" From 110801c4b0113666b87497e6a7873c110592791d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:02:59 +0000 Subject: [PATCH 2/6] JS SDK: Update IModel with variant info, add getLatestVersion to Catalog, use IModel return types Agent-Logs-Url: https://github.com/microsoft/Foundry-Local/sessions/8082508c-1338-48b2-bdd3-6c2c8e35e195 Co-authored-by: baijumeswani <12852605+baijumeswani@users.noreply.github.com> --- sdk/js/README.md | 4 +- sdk/js/src/catalog.ts | 68 +++++++++++++++++++++++++++------ sdk/js/src/imodel.ts | 21 ++++++++++ sdk/js/src/index.ts | 1 + sdk/js/src/model.ts | 78 +++++++++++++++++++++++--------------- sdk/js/src/modelVariant.ts | 27 +++++++++++++ 6 files changed, 156 insertions(+), 43 deletions(-) diff --git a/sdk/js/README.md b/sdk/js/README.md index 9b08f9ac..9755ab40 100644 --- a/sdk/js/README.md +++ b/sdk/js/README.md @@ -217,9 +217,9 @@ The SDK is configured via `FoundryLocalConfig` when creating the manager: Auto-generated class documentation lives in [`docs/classes/`](docs/classes/): - [FoundryLocalManager](docs/classes/FoundryLocalManager.md) — SDK entry point, web service management -- [Catalog](docs/classes/Catalog.md) — Model discovery and browsing +- [Catalog](docs/classes/Catalog.md) — Model discovery, browsing, and latest version checking - [Model](docs/classes/Model.md) — High-level model with variant selection -- [ModelVariant](docs/classes/ModelVariant.md) — Specific model variant: download, load, inference +- [IModel](docs/interfaces/IModel.md) — Interface shared by Model and ModelVariant - [ChatClient](docs/classes/ChatClient.md) — Chat completions (sync and streaming) - [AudioClient](docs/classes/AudioClient.md) — Audio transcription (sync and streaming) - [ModelLoadManager](docs/classes/ModelLoadManager.md) — Low-level model loading management diff --git a/sdk/js/src/catalog.ts b/sdk/js/src/catalog.ts index bf2ae5c9..675290d8 100644 --- a/sdk/js/src/catalog.ts +++ b/sdk/js/src/catalog.ts @@ -3,6 +3,7 @@ import { ModelLoadManager } from './detail/modelLoadManager.js'; import { Model } from './model.js'; import { ModelVariant } from './modelVariant.js'; import { ModelInfo } from './types.js'; +import { IModel } from './imodel.js'; /** * Represents a catalog of AI models available in the system. @@ -71,9 +72,9 @@ export class Catalog { /** * Lists all available models in the catalog. * This method is asynchronous as it may fetch the model list from a remote service or perform file I/O. - * @returns A Promise that resolves to an array of Model objects. + * @returns A Promise that resolves to an array of IModel objects. */ - public async getModels(): Promise { + public async getModels(): Promise { await this.updateModels(); return this._models; } @@ -82,10 +83,10 @@ export class Catalog { * Retrieves a model by its alias. * This method is asynchronous as it may ensure the catalog is up-to-date by fetching from a remote service. * @param alias - The alias of the model to retrieve. - * @returns A Promise that resolves to the Model object if found, otherwise throws an error. + * @returns A Promise that resolves to the IModel object if found, otherwise throws an error. * @throws Error - If alias is null, undefined, or empty. */ - public async getModel(alias: string): Promise { + public async getModel(alias: string): Promise { if (typeof alias !== 'string' || alias.trim() === '') { throw new Error('Model alias must be a non-empty string.'); } @@ -102,10 +103,10 @@ export class Catalog { * Retrieves a specific model variant by its ID. * This method is asynchronous as it may ensure the catalog is up-to-date by fetching from a remote service. * @param modelId - The unique identifier of the model variant. - * @returns A Promise that resolves to the ModelVariant object if found, otherwise throws an error. + * @returns A Promise that resolves to the IModel object if found, otherwise throws an error. * @throws Error - If modelId is null, undefined, or empty. */ - public async getModelVariant(modelId: string): Promise { + public async getModelVariant(modelId: string): Promise { if (typeof modelId !== 'string' || modelId.trim() === '') { throw new Error('Model ID must be a non-empty string.'); } @@ -121,9 +122,9 @@ export class Catalog { /** * Retrieves a list of all locally cached model variants. * This method is asynchronous as it may involve file I/O or querying the underlying core. - * @returns A Promise that resolves to an array of cached ModelVariant objects. + * @returns A Promise that resolves to an array of cached IModel objects. */ - public async getCachedModels(): Promise { + public async getCachedModels(): Promise { await this.updateModels(); const cachedModelListJson = this.coreInterop.executeCommand("get_cached_models"); let cachedModelIds: string[] = []; @@ -147,9 +148,9 @@ export class Catalog { * Retrieves a list of all currently loaded model variants. * This operation is asynchronous because checking the loaded status may involve querying * the underlying core or an external service, which can be an I/O bound operation. - * @returns A Promise that resolves to an array of loaded ModelVariant objects. + * @returns A Promise that resolves to an array of loaded IModel objects. */ - public async getLoadedModels(): Promise { + public async getLoadedModels(): Promise { await this.updateModels(); let loadedModelIds: string[] = []; try { @@ -157,7 +158,7 @@ export class Catalog { } catch (error) { throw new Error(`Failed to list loaded models: ${error}`); } - const loadedModels: ModelVariant[] = []; + const loadedModels: IModel[] = []; for (const modelId of loadedModelIds) { const variant = this.modelIdToModelVariant.get(modelId); @@ -167,4 +168,49 @@ export class Catalog { } return loadedModels; } + + /** + * Get the latest version of a model. + * This is used to check if a newer version of a model is available in the catalog for download. + * @param modelOrVariant - The model to check for the latest version. + * @returns The latest version of the model. Will match the input if it is the latest version. + * @throws Error - If the model is not found in the catalog. + */ + public async getLatestVersion(modelOrVariant: IModel): Promise { + await this.updateModels(); + + let model: Model | undefined; + + if (modelOrVariant instanceof ModelVariant) { + // For ModelVariant, resolve the owning Model via alias + model = this.modelAliasToModel.get(modelOrVariant.alias); + } else if (modelOrVariant instanceof Model) { + model = modelOrVariant; + } else { + // For other IModel implementations (e.g., test stubs), resolve via alias + model = this.modelAliasToModel.get(modelOrVariant.alias); + } + + if (!model) { + throw new Error( + `Model with alias '${modelOrVariant.alias}' not found in catalog.` + ); + } + + // variants are sorted by version, so the first one matching the name is the latest version. + const latest = model.variants.find( + v => v.modelInfo.name === modelOrVariant.modelInfo.name + ); + + if (!latest) { + throw new Error( + `Internal error. Mismatch between model (alias:${model.alias}) and ` + + `model variant (alias:${modelOrVariant.alias}).` + ); + } + + // if input was the latest return the input (could be model or model variant) + // otherwise return the latest model variant + return latest.id === modelOrVariant.id ? modelOrVariant : latest; + } } \ No newline at end of file diff --git a/sdk/js/src/imodel.ts b/sdk/js/src/imodel.ts index f5b72622..b5e743e6 100644 --- a/sdk/js/src/imodel.ts +++ b/sdk/js/src/imodel.ts @@ -1,6 +1,7 @@ import { ChatClient } from './openai/chatClient.js'; import { AudioClient } from './openai/audioClient.js'; import { ResponsesClient } from './openai/responsesClient.js'; +import { ModelInfo } from './types.js'; export interface IModel { get id(): string; @@ -8,6 +9,9 @@ export interface IModel { get isCached(): boolean; isLoaded(): Promise; + /** Full catalog metadata for this model or variant. */ + get modelInfo(): ModelInfo; + get contextLength(): number | null; get inputModalities(): string | null; get outputModalities(): string | null; @@ -29,4 +33,21 @@ export interface IModel { * @param baseUrl - The base URL of the Foundry Local web service. */ createResponsesClient(baseUrl: string): ResponsesClient; + + /** + * Variants of the model that are available. Variants are optimized for different devices. + */ + get variants(): IModel[]; + + /** + * Currently selected model variant in use. + */ + get selectedVariant(): IModel; + + /** + * Select a specific model variant from {@link variants} to use for IModel operations. + * @param variant - Model variant to select. Must be one of the variants in {@link variants}. + * @throws Error - If variant is not valid for this model. + */ + selectVariant(variant: IModel): void; } diff --git a/sdk/js/src/index.ts b/sdk/js/src/index.ts index 7d7ee17a..97a8f0f3 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -2,6 +2,7 @@ export { FoundryLocalManager } from './foundryLocalManager.js'; export type { FoundryLocalConfig } from './configuration.js'; export { Catalog } from './catalog.js'; export { Model } from './model.js'; +/** @internal */ export { ModelVariant } from './modelVariant.js'; export type { IModel } from './imodel.js'; export { ChatClient, ChatClientSettings } from './openai/chatClient.js'; diff --git a/sdk/js/src/model.ts b/sdk/js/src/model.ts index 155d5dd1..9e46a45e 100644 --- a/sdk/js/src/model.ts +++ b/sdk/js/src/model.ts @@ -2,6 +2,7 @@ import { ModelVariant } from './modelVariant.js'; import { ChatClient } from './openai/chatClient.js'; import { AudioClient } from './openai/audioClient.js'; import { ResponsesClient } from './openai/responsesClient.js'; +import { ModelInfo } from './types.js'; import { IModel } from './imodel.js'; /** @@ -12,21 +13,21 @@ export class Model implements IModel { private _alias: string; private _variants: ModelVariant[]; - private selectedVariant: ModelVariant; + private _selectedVariant: ModelVariant; constructor(variant: ModelVariant) { this._alias = variant.alias; this._variants = [variant]; - this.selectedVariant = variant; + this._selectedVariant = variant; } - private validateVariantInput(variant: ModelVariant, caller: string): void { + private validateVariantInput(variant: IModel, caller: string): void { if (variant === null || variant === undefined) { - throw new Error(`${caller}() requires a ModelVariant object but received ${variant}.`); + throw new Error(`${caller}() requires an IModel object but received ${variant}.`); } if (typeof variant !== 'object') { throw new Error( - `${caller}() requires a ModelVariant object but received ${typeof variant}.` + `${caller}() requires an IModel object but received ${typeof variant}.` ); } } @@ -36,6 +37,7 @@ export class Model implements IModel { * Automatically selects the new variant if it is cached and the current one is not. * @param variant - The model variant to add. * @throws Error - If the argument is not a ModelVariant object, or if the variant's alias does not match the model's alias. + * @internal */ public addVariant(variant: ModelVariant): void { this.validateVariantInput(variant, 'addVariant'); @@ -45,23 +47,23 @@ export class Model implements IModel { this._variants.push(variant); // prefer the highest priority locally cached variant - if (variant.isCached && !this.selectedVariant.isCached) { - this.selectedVariant = variant; + if (variant.isCached && !this._selectedVariant.isCached) { + this._selectedVariant = variant; } } /** * Selects a specific variant. - * @param variant - The model variant to select. - * @throws Error - If the argument is not a ModelVariant object, or if the variant does not belong to this model. + * @param variant - The model variant to select. Must be one of the variants in {@link variants}. + * @throws Error - If the variant does not belong to this model. */ - public selectVariant(variant: ModelVariant): void { + public selectVariant(variant: IModel): void { this.validateVariantInput(variant, 'selectVariant'); const matchingVariant = this._variants.find(v => v.id === variant.id); if (!variant.id || !matchingVariant) { - throw new Error(`Model variant with ID ${variant.id} does not belong to model "${this._alias}".`); + throw new Error(`Input variant was not found in Variants.`); } - this.selectedVariant = matchingVariant; + this._selectedVariant = matchingVariant; } /** @@ -69,7 +71,7 @@ export class Model implements IModel { * @returns The ID of the selected variant. */ public get id(): string { - return this.selectedVariant.id; + return this._selectedVariant.id; } /** @@ -80,12 +82,20 @@ export class Model implements IModel { return this._alias; } + /** + * Gets the detailed information about the currently selected variant. + * @returns The ModelInfo object. + */ + public get modelInfo(): ModelInfo { + return this._selectedVariant.modelInfo; + } + /** * Checks if the currently selected variant is cached locally. * @returns True if cached, false otherwise. */ public get isCached(): boolean { - return this.selectedVariant.isCached; + return this._selectedVariant.isCached; } /** @@ -93,35 +103,43 @@ export class Model implements IModel { * @returns True if loaded, false otherwise. */ public async isLoaded(): Promise { - return await this.selectedVariant.isLoaded(); + return await this._selectedVariant.isLoaded(); } /** * Gets all available variants for this model. - * @returns An array of ModelVariant objects. + * @returns An array of IModel objects. */ - public get variants(): ModelVariant[] { + public get variants(): IModel[] { return this._variants; } + /** + * Gets the currently selected variant. + * @returns The currently selected IModel. + */ + public get selectedVariant(): IModel { + return this._selectedVariant; + } + public get contextLength(): number | null { - return this.selectedVariant.contextLength; + return this._selectedVariant.contextLength; } public get inputModalities(): string | null { - return this.selectedVariant.inputModalities; + return this._selectedVariant.inputModalities; } public get outputModalities(): string | null { - return this.selectedVariant.outputModalities; + return this._selectedVariant.outputModalities; } public get capabilities(): string | null { - return this.selectedVariant.capabilities; + return this._selectedVariant.capabilities; } public get supportsToolCalling(): boolean | null { - return this.selectedVariant.supportsToolCalling; + return this._selectedVariant.supportsToolCalling; } /** @@ -129,7 +147,7 @@ export class Model implements IModel { * @param progressCallback - Optional callback to report download progress. */ public download(progressCallback?: (progress: number) => void): Promise { - return this.selectedVariant.download(progressCallback); + return this._selectedVariant.download(progressCallback); } /** @@ -137,7 +155,7 @@ export class Model implements IModel { * @returns The local file path. */ public get path(): string { - return this.selectedVariant.path; + return this._selectedVariant.path; } /** @@ -145,14 +163,14 @@ export class Model implements IModel { * @returns A promise that resolves when the model is loaded. */ public async load(): Promise { - await this.selectedVariant.load(); + await this._selectedVariant.load(); } /** * Removes the currently selected variant from the local cache. */ public removeFromCache(): void { - this.selectedVariant.removeFromCache(); + this._selectedVariant.removeFromCache(); } /** @@ -160,7 +178,7 @@ export class Model implements IModel { * @returns A promise that resolves when the model is unloaded. */ public async unload(): Promise { - await this.selectedVariant.unload(); + await this._selectedVariant.unload(); } /** @@ -168,7 +186,7 @@ export class Model implements IModel { * @returns A ChatClient instance. */ public createChatClient(): ChatClient { - return this.selectedVariant.createChatClient(); + return this._selectedVariant.createChatClient(); } /** @@ -176,7 +194,7 @@ export class Model implements IModel { * @returns An AudioClient instance. */ public createAudioClient(): AudioClient { - return this.selectedVariant.createAudioClient(); + return this._selectedVariant.createAudioClient(); } /** @@ -185,6 +203,6 @@ export class Model implements IModel { * @returns A ResponsesClient instance. */ public createResponsesClient(baseUrl: string): ResponsesClient { - return this.selectedVariant.createResponsesClient(baseUrl); + return this._selectedVariant.createResponsesClient(baseUrl); } } \ No newline at end of file diff --git a/sdk/js/src/modelVariant.ts b/sdk/js/src/modelVariant.ts index db06033a..4ea9bbf6 100644 --- a/sdk/js/src/modelVariant.ts +++ b/sdk/js/src/modelVariant.ts @@ -157,4 +157,31 @@ export class ModelVariant implements IModel { public createResponsesClient(baseUrl: string): ResponsesClient { return new ResponsesClient(baseUrl, this._modelInfo.id); } + + /** + * Returns an array containing only this variant. + * A ModelVariant is its own variant list. + */ + public get variants(): IModel[] { + return [this]; + } + + /** + * Returns this variant (a ModelVariant is its own selected variant). + */ + public get selectedVariant(): IModel { + return this; + } + + /** + * SelectVariant is not supported on a ModelVariant. + * Call `catalog.getModel(alias)` to get a Model with all variants available. + * @throws Error - Always. + */ + public selectVariant(_variant: IModel): void { + throw new Error( + `selectVariant is not supported on a ModelVariant. ` + + `Call catalog.getModel("${this._modelInfo.alias}") to get a Model with all variants available.` + ); + } } From d0b2d042597c3a5239c64bc0b9a11dd610b43610 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:05:20 +0000 Subject: [PATCH 3/6] Rust SDK: Add info() to Model, get_latest_version to Catalog, update docs Agent-Logs-Url: https://github.com/microsoft/Foundry-Local/sessions/8082508c-1338-48b2-bdd3-6c2c8e35e195 Co-authored-by: baijumeswani <12852605+baijumeswani@users.noreply.github.com> --- sdk/rust/README.md | 2 +- sdk/rust/docs/api.md | 2 ++ sdk/rust/src/catalog.rs | 39 +++++++++++++++++++++++++++++++++++++++ sdk/rust/src/model.rs | 6 ++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/sdk/rust/README.md b/sdk/rust/README.md index d76a7589..bc2711d9 100644 --- a/sdk/rust/README.md +++ b/sdk/rust/README.md @@ -127,7 +127,7 @@ let loaded = catalog.get_loaded_models().await?; ### Model Lifecycle -Each `Model` wraps one or more `ModelVariant` entries (different quantizations, hardware targets). The SDK auto-selects the best available variant, preferring cached versions. +Each `Model` wraps one or more variant entries (different quantizations, hardware targets). The SDK auto-selects the best available variant, preferring cached versions. ```rust let model = catalog.get_model("phi-3.5-mini").await?; diff --git a/sdk/rust/docs/api.md b/sdk/rust/docs/api.md index 278402fb..82648975 100644 --- a/sdk/rust/docs/api.md +++ b/sdk/rust/docs/api.md @@ -134,6 +134,7 @@ pub struct Catalog { /* private fields */ } | `get_model_variant` | `async fn get_model_variant(&self, id: &str) -> Result, FoundryLocalError>` | Look up a variant by unique id. | | `get_cached_models` | `async fn get_cached_models(&self) -> Result>, FoundryLocalError>` | Return only variants cached on disk. | | `get_loaded_models` | `async fn get_loaded_models(&self) -> Result>, FoundryLocalError>` | Return model variants currently loaded in memory. | +| `get_latest_version` | `async fn get_latest_version(&self, model: &Arc) -> Result, FoundryLocalError>` | Get the latest version of a model variant. Returns the latest variant matching the selected variant's name. | --- @@ -149,6 +150,7 @@ pub struct Model { /* private fields */ } |--------|-----------|-------------| | `alias` | `fn alias(&self) -> &str` | Alias shared by all variants. | | `id` | `fn id(&self) -> &str` | Unique identifier of the selected variant. | +| `info` | `fn info(&self) -> &ModelInfo` | Full metadata for the selected variant. | | `variants` | `fn variants(&self) -> &[Arc]` | All variants in this model. | | `selected_variant` | `fn selected_variant(&self) -> &ModelVariant` | Currently selected variant. | | `select_variant` | `fn select_variant(&self, id: &str) -> Result<(), FoundryLocalError>` | Select a variant by id. | diff --git a/sdk/rust/src/catalog.rs b/sdk/rust/src/catalog.rs index 9e04c943..92ccd300 100644 --- a/sdk/rust/src/catalog.rs +++ b/sdk/rust/src/catalog.rs @@ -188,6 +188,45 @@ impl Catalog { .collect()) } + /// Get the latest version of a model. + /// + /// This is used to check if a newer version of a model is available in the + /// catalog for download. + /// + /// Returns the same `Arc` if the model already references the latest + /// version. + pub async fn get_latest_version(&self, model: &Arc) -> Result> { + self.update_models().await?; + + let s = self.lock_state()?; + let catalog_model = s.models_by_alias.get(model.alias()).ok_or_else(|| { + FoundryLocalError::ModelOperation { + reason: format!( + "Model with alias '{}' not found in catalog.", + model.alias() + ), + } + })?; + + let current_name = &model.selected_variant().info().name; + + // variants are sorted by version, so the first one matching the name + // is the latest version for that variant. + let latest = catalog_model + .variants() + .iter() + .find(|v| &v.info().name == current_name) + .ok_or_else(|| FoundryLocalError::ModelOperation { + reason: format!( + "Internal error. Mismatch between model (alias:{}) and variant (name:{}).", + model.alias(), + current_name + ), + })?; + + Ok(Arc::clone(latest)) + } + async fn force_refresh(&self) -> Result<()> { let raw = self .core diff --git a/sdk/rust/src/model.rs b/sdk/rust/src/model.rs index 9d08f9a5..4ffe0e7e 100644 --- a/sdk/rust/src/model.rs +++ b/sdk/rust/src/model.rs @@ -11,6 +11,7 @@ use crate::error::{FoundryLocalError, Result}; use crate::model_variant::ModelVariant; use crate::openai::AudioClient; use crate::openai::ChatClient; +use crate::types::ModelInfo; /// A model groups one or more [`ModelVariant`]s that share the same alias. /// @@ -102,6 +103,11 @@ impl Model { &self.alias } + /// The full [`ModelInfo`] metadata of the selected variant. + pub fn info(&self) -> &ModelInfo { + self.selected_variant().info() + } + /// Unique identifier of the selected variant. pub fn id(&self) -> &str { self.selected_variant().id() From 97bee1e1091d752a1dcf3ad5404c252d36122eb1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:07:24 +0000 Subject: [PATCH 4/6] C# SDK: Update README, docs and samples for IModel API changes Agent-Logs-Url: https://github.com/microsoft/Foundry-Local/sessions/8082508c-1338-48b2-bdd3-6c2c8e35e195 Co-authored-by: baijumeswani <12852605+baijumeswani@users.noreply.github.com> --- .../src/AudioTranscriptionExample/Program.cs | 6 +++--- .../src/ModelManagementExample/Program.cs | 20 +++++++++---------- sdk/cs/README.md | 11 ++++++---- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/samples/cs/GettingStarted/src/AudioTranscriptionExample/Program.cs b/samples/cs/GettingStarted/src/AudioTranscriptionExample/Program.cs index be1db5db..74e6779a 100644 --- a/samples/cs/GettingStarted/src/AudioTranscriptionExample/Program.cs +++ b/samples/cs/GettingStarted/src/AudioTranscriptionExample/Program.cs @@ -23,10 +23,10 @@ var catalog = await mgr.GetCatalogAsync(); -// Get a model using an alias and select the CPU model variant +// Get a model using an alias and select the CPU variant var model = await catalog.GetModelAsync("whisper-tiny") ?? throw new System.Exception("Model not found"); -var modelVariant = model.Variants.First(v => v.Info.Runtime?.DeviceType == DeviceType.CPU); -model.SelectVariant(modelVariant); +var cpuVariant = model.Variants.First(v => v.Info.Runtime?.DeviceType == DeviceType.CPU); +model.SelectVariant(cpuVariant); // Download the model (the method skips download if already cached) diff --git a/samples/cs/GettingStarted/src/ModelManagementExample/Program.cs b/samples/cs/GettingStarted/src/ModelManagementExample/Program.cs index 2b6fe2e8..d4e2ea59 100644 --- a/samples/cs/GettingStarted/src/ModelManagementExample/Program.cs +++ b/samples/cs/GettingStarted/src/ModelManagementExample/Program.cs @@ -67,26 +67,26 @@ // OPTIONAL: `model` can be used directly and `model.SelectedVariant` will be used as the default. -// You can explicitly select or use a specific ModelVariant if you want more control +// You can explicitly select a specific variant if you want more control // over the device and/or execution provider used. -// Model and ModelVariant can be used interchangeably in methods such as +// Model and its variants can be used interchangeably in methods such as // DownloadAsync, LoadAsync, UnloadAsync and GetChatClientAsync. // // Choices: -// - Use a ModelVariant directly from the catalog if you know the variant Id -// - `var modelVariant = await catalog.GetModelVariantAsync("qwen2.5-0.5b-instruct-generic-gpu:3")` +// - Use a variant directly from the catalog if you know the variant Id +// - `var variant = await catalog.GetModelVariantAsync("qwen2.5-0.5b-instruct-generic-gpu:3")` // -// - Get the ModelVariant from Model.Variants -// - `var modelVariant = model.Variants.First(v => v.Id == "qwen2.5-0.5b-instruct-generic-cpu:4")` -// - `var modelVariant = model.Variants.First(v => v.Info.Runtime?.DeviceType == DeviceType.GPU)` -// - optional: update selected variant in `model` using `model.SelectVariant(modelVariant);` if you wish to use +// - Get the variant from Model.Variants +// - `var variant = model.Variants.First(v => v.Id == "qwen2.5-0.5b-instruct-generic-cpu:4")` +// - `var variant = model.Variants.First(v => v.Info.Runtime?.DeviceType == DeviceType.GPU)` +// - optional: update selected variant in `model` using `model.SelectVariant(variant);` if you wish to use // `model` in your code. // For this example we explicitly select the CPU variant, and call SelectVariant so all the following example code // uses the `model` instance. Console.WriteLine("Selecting CPU variant of model"); -var modelVariant = model.Variants.First(v => v.Info.Runtime?.DeviceType == DeviceType.CPU); -model.SelectVariant(modelVariant); +var cpuVariant = model.Variants.First(v => v.Info.Runtime?.DeviceType == DeviceType.CPU); +model.SelectVariant(cpuVariant); // Download the model (the method skips download if already cached) diff --git a/sdk/cs/README.md b/sdk/cs/README.md index f58e41e0..7e14e87a 100644 --- a/sdk/cs/README.md +++ b/sdk/cs/README.md @@ -9,7 +9,7 @@ The Foundry Local C# SDK provides a .NET interface for running AI models locally - **Chat completions** — synchronous and `IAsyncEnumerable` streaming via OpenAI-compatible types - **Audio transcription** — transcribe audio files with streaming support - **Download progress** — wire up an `Action` callback for real-time download percentage -- **Model variants** — select specific hardware/quantization variants per model alias +- **Model variants** — select specific hardware/quantization variants per model alias via the IModel interface - **Optional web service** — start an OpenAI-compatible REST endpoint (`/v1/chat_completions`, `/v1/models`) - **WinML acceleration** — opt-in Windows hardware acceleration with automatic EP download - **Full async/await** — every operation supports `CancellationToken` and async patterns @@ -138,11 +138,14 @@ var cached = await catalog.GetCachedModelsAsync(); // List models currently loaded in memory var loaded = await catalog.GetLoadedModelsAsync(); + +// Check for a newer version of a model +var latest = await catalog.GetLatestVersionAsync(model); ``` ### Model Lifecycle -Each `Model` wraps one or more `ModelVariant` entries (different quantizations, hardware targets). The SDK auto-selects the best variant, or you can pick one: +Each `Model` wraps one or more variant entries (different quantizations, hardware targets). The SDK auto-selects the best variant, or you can pick one: ```csharp // Check and select variants @@ -293,8 +296,8 @@ Key types: | [`FoundryLocalManager`](./docs/api/microsoft.ai.foundry.local.foundrylocalmanager.md) | Singleton entry point — create, catalog, web service | | [`Configuration`](./docs/api/microsoft.ai.foundry.local.configuration.md) | Initialization settings | | [`ICatalog`](./docs/api/microsoft.ai.foundry.local.icatalog.md) | Model catalog interface | -| [`Model`](./docs/api/microsoft.ai.foundry.local.model.md) | Model with variant selection | -| [`ModelVariant`](./docs/api/microsoft.ai.foundry.local.modelvariant.md) | Specific model variant (hardware/quantization) | +| [`IModel`](./docs/api/microsoft.ai.foundry.local.imodel.md) | Model interface — shared by Model and ModelVariant | +| [`Model`](./docs/api/microsoft.ai.foundry.local.model.md) | Model with variant selection (implementation detail) | | [`OpenAIChatClient`](./docs/api/microsoft.ai.foundry.local.openaichatclient.md) | Chat completions (sync + streaming) | | [`OpenAIAudioClient`](./docs/api/microsoft.ai.foundry.local.openaiaudioclient.md) | Audio transcription (sync + streaming) | | [`ModelInfo`](./docs/api/microsoft.ai.foundry.local.modelinfo.md) | Full model metadata record | From 4942e02f89fd811ec33fcb398f66a7ce8da58f3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:12:30 +0000 Subject: [PATCH 5/6] Update C# API docs to reflect IModel changes: ICatalog returns IModel, Model uses IModel variants Agent-Logs-Url: https://github.com/microsoft/Foundry-Local/sessions/8082508c-1338-48b2-bdd3-6c2c8e35e195 Co-authored-by: baijumeswani <12852605+baijumeswani@users.noreply.github.com> --- .../microsoft.ai.foundry.local.icatalog.md | 52 +++++++++++++------ .../api/microsoft.ai.foundry.local.imodel.md | 49 +++++++++++++++++ .../api/microsoft.ai.foundry.local.model.md | 37 +++---------- ...microsoft.ai.foundry.local.modelvariant.md | 4 +- 4 files changed, 96 insertions(+), 46 deletions(-) diff --git a/sdk/cs/docs/api/microsoft.ai.foundry.local.icatalog.md b/sdk/cs/docs/api/microsoft.ai.foundry.local.icatalog.md index dc68c173..ea3fed3b 100644 --- a/sdk/cs/docs/api/microsoft.ai.foundry.local.icatalog.md +++ b/sdk/cs/docs/api/microsoft.ai.foundry.local.icatalog.md @@ -29,7 +29,7 @@ public abstract string Name { get; } List the available models in the catalog. ```csharp -Task> ListModelsAsync(Nullable ct) +Task> ListModelsAsync(Nullable ct) ``` #### Parameters @@ -39,15 +39,15 @@ Optional CancellationToken. #### Returns -[Task<List<Model>>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
-List of Model instances. +[Task<List<IModel>>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
+List of IModel instances. ### **GetModelAsync(String, Nullable<CancellationToken>)** Lookup a model by its alias. ```csharp -Task GetModelAsync(string modelAlias, Nullable ct) +Task GetModelAsync(string modelAlias, Nullable ct) ``` #### Parameters @@ -60,15 +60,15 @@ Optional CancellationToken. #### Returns -[Task<Model>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
-The matching Model, or null if no model with the given alias exists. +[Task<IModel>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
+The matching IModel, or null if no model with the given alias exists. ### **GetModelVariantAsync(String, Nullable<CancellationToken>)** Lookup a model variant by its unique model id. ```csharp -Task GetModelVariantAsync(string modelId, Nullable ct) +Task GetModelVariantAsync(string modelId, Nullable ct) ``` #### Parameters @@ -81,15 +81,15 @@ Optional CancellationToken. #### Returns -[Task<ModelVariant>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
-The matching ModelVariant, or null if no variant with the given id exists. +[Task<IModel>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
+The matching IModel, or null if no variant with the given id exists. ### **GetCachedModelsAsync(Nullable<CancellationToken>)** Get a list of currently downloaded models from the model cache. ```csharp -Task> GetCachedModelsAsync(Nullable ct) +Task> GetCachedModelsAsync(Nullable ct) ``` #### Parameters @@ -99,15 +99,15 @@ Optional CancellationToken. #### Returns -[Task<List<ModelVariant>>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
-List of ModelVariant instances. +[Task<List<IModel>>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
+List of IModel instances. ### **GetLoadedModelsAsync(Nullable<CancellationToken>)** Get a list of the currently loaded models. ```csharp -Task> GetLoadedModelsAsync(Nullable ct) +Task> GetLoadedModelsAsync(Nullable ct) ``` #### Parameters @@ -117,5 +117,27 @@ Optional CancellationToken. #### Returns -[Task<List<ModelVariant>>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
-List of ModelVariant instances. +[Task<List<IModel>>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
+List of IModel instances. + +### **GetLatestVersionAsync(IModel, Nullable<CancellationToken>)** + +Get the latest version of a model. +This is used to check if a newer version of a model is available in the catalog for download. + +```csharp +Task GetLatestVersionAsync(IModel model, Nullable ct) +``` + +#### Parameters + +`model` [IModel](./microsoft.ai.foundry.local.imodel.md)
+The model to check for the latest version. + +`ct` [Nullable<CancellationToken>](https://docs.microsoft.com/en-us/dotnet/api/system.nullable-1)
+Optional CancellationToken. + +#### Returns + +[Task<IModel>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
+The latest version of the model. Will match the input if it is the latest version. diff --git a/sdk/cs/docs/api/microsoft.ai.foundry.local.imodel.md b/sdk/cs/docs/api/microsoft.ai.foundry.local.imodel.md index d5d2b437..a36e09be 100644 --- a/sdk/cs/docs/api/microsoft.ai.foundry.local.imodel.md +++ b/sdk/cs/docs/api/microsoft.ai.foundry.local.imodel.md @@ -30,6 +30,42 @@ public abstract string Alias { get; } [String](https://docs.microsoft.com/en-us/dotnet/api/system.string)
+### **Info** + +Full catalog metadata for this model or variant. + +```csharp +public abstract ModelInfo Info { get; } +``` + +#### Property Value + +[ModelInfo](./microsoft.ai.foundry.local.modelinfo.md)
+ +### **Variants** + +Variants of the model that are available. Variants are optimized for different devices. + +```csharp +public abstract IReadOnlyList Variants { get; } +``` + +#### Property Value + +[IReadOnlyList<IModel>](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.ireadonlylist-1)
+ +### **SelectedVariant** + +Currently selected model variant in use. + +```csharp +public abstract IModel SelectedVariant { get; } +``` + +#### Property Value + +[IModel](./microsoft.ai.foundry.local.imodel.md)
+ ## Methods ### **IsCachedAsync(Nullable<CancellationToken>)** @@ -185,3 +221,16 @@ Optional cancellation token. [Task<OpenAIAudioClient>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
OpenAI.AudioClient + +### **SelectVariant(IModel)** + +Select a specific model variant from Variants to use for IModel operations. + +```csharp +void SelectVariant(IModel variant) +``` + +#### Parameters + +`variant` [IModel](./microsoft.ai.foundry.local.imodel.md)
+Model variant to select. Must be one of the variants in Variants. diff --git a/sdk/cs/docs/api/microsoft.ai.foundry.local.model.md b/sdk/cs/docs/api/microsoft.ai.foundry.local.model.md index c63b78a4..6c40cde1 100644 --- a/sdk/cs/docs/api/microsoft.ai.foundry.local.model.md +++ b/sdk/cs/docs/api/microsoft.ai.foundry.local.model.md @@ -15,22 +15,22 @@ Attributes [NullableContextAttribute](https://docs.microsoft.com/en-us/dotnet/ap ### **Variants** ```csharp -public List Variants { get; internal set; } +public IReadOnlyList Variants { get; } ``` #### Property Value -[List<ModelVariant>](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1)
+[IReadOnlyList<IModel>](https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.ireadonlylist-1)
### **SelectedVariant** ```csharp -public ModelVariant SelectedVariant { get; internal set; } +public IModel SelectedVariant { get; internal set; } ``` #### Property Value -[ModelVariant](./microsoft.ai.foundry.local.modelvariant.md)
+[IModel](./microsoft.ai.foundry.local.imodel.md)
### **Alias** @@ -86,17 +86,17 @@ public Task IsLoadedAsync(Nullable ct) [Task<Boolean>](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1)
-### **SelectVariant(ModelVariant)** +### **SelectVariant(IModel)** Select a specific model variant from [Model.Variants](./microsoft.ai.foundry.local.model.md#variants) to use for [IModel](./microsoft.ai.foundry.local.imodel.md) operations. ```csharp -public void SelectVariant(ModelVariant variant) +public void SelectVariant(IModel variant) ``` #### Parameters -`variant` [ModelVariant](./microsoft.ai.foundry.local.modelvariant.md)
+`variant` [IModel](./microsoft.ai.foundry.local.imodel.md)
Model variant to select. Must be one of the variants in [Model.Variants](./microsoft.ai.foundry.local.model.md#variants). #### Exceptions @@ -104,29 +104,6 @@ Model variant to select. Must be one of the variants in [Model.Variants](./micro [FoundryLocalException](./microsoft.ai.foundry.local.foundrylocalexception.md)
If variant is not valid for this model. -### **GetLatestVersion(ModelVariant)** - -Get the latest version of the specified model variant. - -```csharp -public ModelVariant GetLatestVersion(ModelVariant variant) -``` - -#### Parameters - -`variant` [ModelVariant](./microsoft.ai.foundry.local.modelvariant.md)
-Model variant. - -#### Returns - -[ModelVariant](./microsoft.ai.foundry.local.modelvariant.md)
-ModelVariant for latest version. Same as `variant` if that is the latest version. - -#### Exceptions - -[FoundryLocalException](./microsoft.ai.foundry.local.foundrylocalexception.md)
-If variant is not valid for this model. - ### **GetPathAsync(Nullable<CancellationToken>)** ```csharp diff --git a/sdk/cs/docs/api/microsoft.ai.foundry.local.modelvariant.md b/sdk/cs/docs/api/microsoft.ai.foundry.local.modelvariant.md index 1f674511..a4ea6c6f 100644 --- a/sdk/cs/docs/api/microsoft.ai.foundry.local.modelvariant.md +++ b/sdk/cs/docs/api/microsoft.ai.foundry.local.modelvariant.md @@ -2,8 +2,10 @@ Namespace: Microsoft.AI.Foundry.Local +> **Note:** `ModelVariant` is an internal implementation detail. Use [`IModel`](./microsoft.ai.foundry.local.imodel.md) in public API code. + ```csharp -public class ModelVariant : IModel +internal class ModelVariant : IModel ``` Inheritance [Object](https://docs.microsoft.com/en-us/dotnet/api/system.object) → [ModelVariant](./microsoft.ai.foundry.local.modelvariant.md)
From 9b6ff02734ea19df26d2dc12337a955f02485269 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:14:08 +0000 Subject: [PATCH 6/6] Fix code review: use public variants property instead of private _variants in Catalog Agent-Logs-Url: https://github.com/microsoft/Foundry-Local/sessions/8082508c-1338-48b2-bdd3-6c2c8e35e195 Co-authored-by: baijumeswani <12852605+baijumeswani@users.noreply.github.com> --- sdk/python/src/catalog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/python/src/catalog.py b/sdk/python/src/catalog.py index 16d06ccd..e5506676 100644 --- a/sdk/python/src/catalog.py +++ b/sdk/python/src/catalog.py @@ -171,7 +171,7 @@ def get_latest_version(self, model_or_variant: IModel) -> IModel: # variants are sorted by version, so the first one matching the name is the latest version. latest = None - for v in model._variants: + for v in model.variants: if v.info.name == model_or_variant.info.name: latest = v break