chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Foundry Local Package (agent-framework-foundry-local)
Integration with Azure AI Foundry Local for local model inference.
## Main Classes
- **`FoundryLocalClient`** - Chat client for Foundry Local models
- **`FoundryLocalChatOptions`** - Options TypedDict for Foundry Local parameters
- **`FoundryLocalSettings`** - Pydantic settings for configuration
## Usage
```python
from agent_framework.foundry import FoundryLocalClient
client = FoundryLocalClient(model="your-local-model")
response = await client.get_response("Hello")
```
## Import Path
```python
from agent_framework.foundry import FoundryLocalClient
```
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
+13
View File
@@ -0,0 +1,13 @@
# Get Started with Microsoft Agent Framework Foundry Local
Please install this package as the extra for `agent-framework`:
```bash
pip install agent-framework-foundry-local --pre
```
and see the [README](https://github.com/microsoft/agent-framework/tree/main/python/README.md) for more information.
## Foundry Local Sample
See the [Foundry Local provider sample](../../samples/02-agents/providers/foundry/foundry_local_agent.py) for a runnable example.
@@ -0,0 +1,17 @@
# Copyright (c) Microsoft. All rights reserved.
import importlib.metadata
from ._foundry_local_client import FoundryLocalChatOptions, FoundryLocalClient, FoundryLocalSettings
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" # Fallback for development mode
__all__ = [
"FoundryLocalChatOptions",
"FoundryLocalClient",
"FoundryLocalSettings",
"__version__",
]
@@ -0,0 +1,348 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import sys
from collections.abc import Awaitable, Callable, Mapping, Sequence
from typing import Any, Generic, Literal, cast, overload
from agent_framework import (
ChatAndFunctionMiddlewareTypes,
ChatMiddlewareLayer,
ChatOptions,
ChatResponse,
ChatResponseUpdate,
CompactionStrategy,
FunctionInvocationConfiguration,
FunctionInvocationLayer,
Message,
ResponseStream,
TokenizerProtocol,
)
from agent_framework._settings import load_settings
from agent_framework.observability import ChatTelemetryLayer
from agent_framework_openai._chat_completion_client import RawOpenAIChatCompletionClient
from foundry_local import FoundryLocalManager
from foundry_local.models import DeviceType, FoundryModelInfo
from openai import AsyncOpenAI
from pydantic import BaseModel
if sys.version_info >= (3, 13):
from typing import TypeVar # pragma: no cover
else:
from typing_extensions import TypeVar # pragma: no cover
if sys.version_info >= (3, 11):
from typing import TypedDict # pragma: no cover
else:
from typing_extensions import TypedDict # pragma: no cover
__all__ = [
"FoundryLocalChatOptions",
"FoundryLocalClient",
"FoundryLocalSettings",
]
ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel | None, default=None)
# region Foundry Local Chat Options TypedDict
class FoundryLocalChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], total=False):
"""Azure Foundry Local (local model deployment) chat options dict.
Extends base ChatOptions for local model inference via Foundry Local.
Foundry Local provides an OpenAI-compatible API, so most standard
OpenAI chat completion options are supported.
See: https://github.com/Azure/azure-ai-foundry-model-inference
Keys:
# Inherited from ChatOptions (supported via OpenAI-compatible API):
model: The model identifier or alias (e.g., 'phi-4-mini').
temperature: Sampling temperature (0-2).
top_p: Nucleus sampling parameter.
max_tokens: Maximum tokens to generate.
stop: Stop sequences.
tools: List of tools available to the model.
tool_choice: How the model should use tools.
frequency_penalty: Frequency penalty (-2.0 to 2.0).
presence_penalty: Presence penalty (-2.0 to 2.0).
seed: Random seed for reproducibility.
# Options with limited support (depends on the model):
response_format: Response format specification.
Not all local models support JSON mode.
logit_bias: Token bias dictionary.
May not be supported by all models.
# Options not supported in Foundry Local:
user: Not used locally.
store: Not applicable for local inference.
metadata: Not applicable for local inference.
# Foundry Local-specific options:
extra_body: Additional request body parameters to pass to the model.
Can be used for model-specific options not covered by standard API.
Note:
The actual options supported depend on the specific model being used.
Some models (like Phi-4) may not support all OpenAI API features.
Options not supported by the model will typically be ignored.
"""
# Foundry Local-specific options
extra_body: dict[str, Any]
"""Additional request body parameters for model-specific options."""
# ChatOptions fields not applicable for local inference
user: None # type: ignore[misc]
"""Not used for local model inference."""
store: None # type: ignore[misc]
"""Not applicable for local inference."""
FoundryLocalChatOptionsT = TypeVar(
"FoundryLocalChatOptionsT",
bound=TypedDict, # type: ignore[valid-type]
default="FoundryLocalChatOptions",
covariant=True,
)
# endregion
class FoundryLocalSettings(TypedDict, total=False):
"""Foundry local model settings.
Settings are resolved in this order: explicit keyword arguments, values from an
explicitly provided .env file, then environment variables with the prefix
'FOUNDRY_LOCAL_'.
Keys:
model: The name of the model deployment to use.
(Env var FOUNDRY_LOCAL_MODEL)
"""
model: str | None
class FoundryLocalClient(
FunctionInvocationLayer[FoundryLocalChatOptionsT],
ChatMiddlewareLayer[FoundryLocalChatOptionsT],
ChatTelemetryLayer[FoundryLocalChatOptionsT],
RawOpenAIChatCompletionClient[FoundryLocalChatOptionsT],
Generic[FoundryLocalChatOptionsT],
):
"""Foundry Local Chat completion class with middleware, telemetry, and function invocation support."""
@overload
def get_response(
self,
messages: Sequence[Message],
*,
stream: Literal[False] = ...,
options: ChatOptions[ResponseModelT],
compaction_strategy: CompactionStrategy | None = None,
tokenizer: TokenizerProtocol | None = None,
function_invocation_kwargs: Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Any] | None = None,
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
) -> Awaitable[ChatResponse[ResponseModelT]]: ...
@overload
def get_response(
self,
messages: Sequence[Message],
*,
stream: Literal[False] = ...,
options: FoundryLocalChatOptionsT | ChatOptions[None] | None = None,
compaction_strategy: CompactionStrategy | None = None,
tokenizer: TokenizerProtocol | None = None,
function_invocation_kwargs: Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Any] | None = None,
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
) -> Awaitable[ChatResponse[Any]]: ...
@overload
def get_response(
self,
messages: Sequence[Message],
*,
stream: Literal[True],
options: FoundryLocalChatOptionsT | ChatOptions[Any] | None = None,
compaction_strategy: CompactionStrategy | None = None,
tokenizer: TokenizerProtocol | None = None,
function_invocation_kwargs: Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Any] | None = None,
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
) -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ...
def get_response(
self,
messages: Sequence[Message],
*,
stream: bool = False,
options: FoundryLocalChatOptionsT | ChatOptions[Any] | None = None,
compaction_strategy: CompactionStrategy | None = None,
tokenizer: TokenizerProtocol | None = None,
function_invocation_kwargs: Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Any] | None = None,
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
"""Get a response from the Foundry Local chat client with all standard layers enabled."""
super_get_response = cast(
"Callable[..., Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]]",
super().get_response,
)
effective_client_kwargs = dict(client_kwargs) if client_kwargs is not None else {}
if middleware is not None:
effective_client_kwargs["middleware"] = middleware
return super_get_response(
messages=messages,
stream=stream,
options=options,
compaction_strategy=compaction_strategy,
tokenizer=tokenizer,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=effective_client_kwargs,
)
def __init__(
self,
model: str | None = None,
*,
bootstrap: bool = True,
timeout: float | None = None,
prepare_model: bool = True,
device: DeviceType | None = None,
additional_properties: dict[str, Any] | None = None,
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
env_file_path: str | None = None,
env_file_encoding: str = "utf-8",
) -> None:
"""Initialize a FoundryLocalClient.
Keyword Args:
model: The Foundry Local model ID or alias to use. If not provided,
it will be loaded from the FoundryLocalSettings.
bootstrap: Whether to start the Foundry Local service if not already running.
Default is True.
timeout: Optional timeout for requests to Foundry Local.
This timeout is applied to any call to the Foundry Local service.
prepare_model: Whether to download the model into the cache, and load the model into
the inferencing service upon initialization. Default is True.
If false, the first call to generate a completion will load the model,
and might take a long time.
device: The device type to use for model inference.
The device is used to select the appropriate model variant.
If not provided, the default device for your system will be used.
The values are in the foundry_local.models.DeviceType enum.
additional_properties: Additional properties stored on the client instance.
middleware: Optional sequence of ChatAndFunctionMiddlewareTypes to apply to requests.
function_invocation_configuration: Optional configuration for function invocation support.
env_file_path: If provided, the .env settings are read from this file path location.
env_file_encoding: The encoding of the .env file, defaults to 'utf-8'.
Examples:
.. code-block:: python
# Create a FoundryLocalClient with a specific model ID:
from agent_framework.foundry import FoundryLocalClient
client = FoundryLocalClient(model="phi-4-mini")
agent = client.as_agent(
name="LocalAgent",
instructions="You are a helpful agent.",
tools=get_weather,
)
response = await agent.run("What's the weather like in Seattle?")
# Or you can set the model id in the environment:
os.environ["FOUNDRY_LOCAL_MODEL"] = "phi-4-mini"
client = FoundryLocalClient()
# A FoundryLocalManager is created and if set, the service is started.
# The FoundryLocalManager is available via the `manager` property.
# For instance to find out which models are available:
for model in client.manager.list_catalog_models():
print(f"- {model.alias} for {model.task} - id={model.id}")
# Other options include specifying the device type:
from foundry_local.models import DeviceType
client = FoundryLocalClient(
model="phi-4-mini",
device=DeviceType.GPU,
)
# and choosing if the model should be prepared on initialization:
client = FoundryLocalClient(
model="phi-4-mini",
prepare_model=False,
)
# Beware, in this case the first request to generate a completion
# will take a long time as the model is loaded then.
# Alternatively, you could call the `download_model` and `load_model` methods
# on the `manager` property manually.
client.manager.download_model("phi-4-mini", device=DeviceType.CPU)
client.manager.load_model("phi-4-mini", device=DeviceType.CPU)
# You can also use the CLI:
`foundry model load phi-4-mini --device Auto`
# Using custom ChatOptions with type safety:
from typing import TypedDict
from agent_framework.foundry import FoundryLocalChatOptions
class MyOptions(FoundryLocalChatOptions, total=False):
my_custom_option: str
client: FoundryLocalClient[MyOptions] = FoundryLocalClient(model="phi-4-mini")
response = await client.get_response("Hello", options={"my_custom_option": "value"})
Raises:
ValueError: If the specified model ID or alias is not found.
Sometimes a model might be available but if you have specified a device
type that is not supported by the model, it will not be found.
"""
settings = load_settings(
FoundryLocalSettings,
env_prefix="FOUNDRY_LOCAL_",
required_fields=["model"],
model=model,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
model_setting: str = settings["model"] # type: ignore[assignment]
manager = FoundryLocalManager(bootstrap=bootstrap, timeout=timeout)
model_info: FoundryModelInfo | None = manager.get_model_info(
model_setting,
device=device,
)
if model_info is None:
message = (
f"Model with ID or alias '{model_setting}:{device.value}' not found in Foundry Local."
if device
else (f"Model with ID or alias '{model_setting}' for your current device not found in Foundry Local.")
)
raise ValueError(message)
if prepare_model:
manager.download_model(model_info.id, device=device)
manager.load_model(model_info.id, device=device)
super().__init__(
model=model_info.id,
async_client=AsyncOpenAI(base_url=manager.endpoint, api_key=manager.api_key),
additional_properties=additional_properties,
middleware=middleware,
function_invocation_configuration=function_invocation_configuration,
)
self.manager = manager
@@ -0,0 +1,98 @@
[project]
name = "agent-framework-foundry-local"
description = "Foundry Local integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260521"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
urls.issues = "https://github.com/microsoft/agent-framework/issues"
classifiers = [
"License :: OSI Approved :: MIT License",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.6.0,<2",
"agent-framework-openai>=1.6.0,<2",
"foundry-local-sdk>=0.5.1,<0.5.2",
]
[tool.uv]
prerelease = "if-necessary-or-explicit"
environments = [
"sys_platform == 'darwin'",
"sys_platform == 'linux'",
"sys_platform == 'win32'"
]
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = []
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
[tool.coverage.run]
omit = [
"**/__init__.py"
]
[tool.pyright]
extends = "../../pyproject.toml"
include = ["agent_framework_foundry_local"]
exclude = ['tests']
[tool.mypy]
plugins = ['pydantic.mypy']
strict = true
python_version = "3.10"
ignore_missing_imports = true
disallow_untyped_defs = true
no_implicit_optional = true
check_untyped_defs = true
warn_return_any = true
show_error_codes = true
warn_unused_ignores = false
disallow_incomplete_defs = true
disallow_untyped_decorators = true
[tool.bandit]
targets = ["agent_framework_foundry_local"]
exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry_local"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_foundry_local --cov-report=term-missing:skip-covered tests'
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
build-backend = "flit_core.buildapi"
@@ -0,0 +1,55 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from unittest.mock import MagicMock
from pytest import fixture
@fixture
def exclude_list(request: Any) -> list[str]:
"""Fixture that returns a list of environment variables to exclude."""
return request.param if hasattr(request, "param") else []
@fixture
def override_env_param_dict(request: Any) -> dict[str, str]:
"""Fixture that returns a dict of environment variables to override."""
return request.param if hasattr(request, "param") else {}
@fixture()
def foundry_local_unit_test_env(monkeypatch: Any, exclude_list: list[str], override_env_param_dict: dict[str, str]):
"""Fixture to set environment variables for FoundryLocalSettings."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"FOUNDRY_LOCAL_MODEL": "test-model-id",
}
env_vars.update(override_env_param_dict)
for key, value in env_vars.items():
if key in exclude_list:
monkeypatch.delenv(key, raising=False)
continue
monkeypatch.setenv(key, value)
return env_vars
@fixture
def mock_foundry_local_manager() -> MagicMock:
"""Fixture that provides a mock FoundryLocalManager."""
mock_manager = MagicMock()
mock_manager.endpoint = "http://localhost:5272/v1"
mock_manager.api_key = "test-api-key"
mock_model_info = MagicMock()
mock_model_info.id = "test-model-id"
mock_manager.get_model_info.return_value = mock_model_info
return mock_manager
@@ -0,0 +1,227 @@
# Copyright (c) Microsoft. All rights reserved.
import inspect
from unittest.mock import MagicMock, patch
import pytest
from agent_framework import Agent, SupportsChatGetResponse
from agent_framework._settings import load_settings
from agent_framework.exceptions import SettingNotFoundError
from agent_framework.foundry import FoundryLocalClient
from agent_framework_foundry_local._foundry_local_client import FoundryLocalSettings
# Settings Tests
def test_foundry_local_settings_init_from_env(foundry_local_unit_test_env: dict[str, str]) -> None:
"""Test FoundryLocalSettings initialization from environment variables."""
settings = load_settings(FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_")
assert settings["model"] == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL"]
def test_foundry_local_settings_init_with_explicit_values() -> None:
"""Test FoundryLocalSettings initialization with explicit values."""
settings = load_settings(
FoundryLocalSettings,
env_prefix="FOUNDRY_LOCAL_",
model="custom-model-id",
)
assert settings["model"] == "custom-model-id"
@pytest.mark.parametrize("exclude_list", [["FOUNDRY_LOCAL_MODEL"]], indirect=True)
def test_foundry_local_settings_missing_model(foundry_local_unit_test_env: dict[str, str]) -> None:
"""Test FoundryLocalSettings when model is missing raises error."""
with pytest.raises(SettingNotFoundError, match="Required setting 'model'"):
load_settings(
FoundryLocalSettings,
env_prefix="FOUNDRY_LOCAL_",
required_fields=["model"],
)
def test_foundry_local_settings_explicit_overrides_env(foundry_local_unit_test_env: dict[str, str]) -> None:
"""Test that explicit values override environment variables."""
settings = load_settings(FoundryLocalSettings, env_prefix="FOUNDRY_LOCAL_", model="override-model-id")
assert settings["model"] == "override-model-id"
assert settings["model"] != foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL"]
# Client Initialization Tests
def test_foundry_local_client_init(mock_foundry_local_manager: MagicMock) -> None:
"""Test FoundryLocalClient initialization with mocked manager."""
with patch(
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
return_value=mock_foundry_local_manager,
):
client = FoundryLocalClient(model="test-model-id")
assert client.model == "test-model-id"
assert client.manager is mock_foundry_local_manager
assert isinstance(client, SupportsChatGetResponse)
def test_agent_accepts_foundry_local_client(mock_foundry_local_manager: MagicMock) -> None:
with patch(
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
return_value=mock_foundry_local_manager,
):
client = FoundryLocalClient(model="test-model-id")
agent = Agent(client=client, instructions="test agent")
assert agent.client is client
def test_foundry_local_client_get_response_uses_explicit_runtime_buckets() -> None:
"""Foundry Local should expose explicit runtime buckets instead of raw kwargs."""
signature = inspect.signature(FoundryLocalClient.get_response)
assert "client_kwargs" in signature.parameters
assert "function_invocation_kwargs" in signature.parameters
assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values())
def test_foundry_local_client_init_with_bootstrap_false(mock_foundry_local_manager: MagicMock) -> None:
"""Test FoundryLocalClient initialization with bootstrap=False."""
with patch(
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
return_value=mock_foundry_local_manager,
) as mock_manager_class:
FoundryLocalClient(model="test-model-id", bootstrap=False)
mock_manager_class.assert_called_once_with(
bootstrap=False,
timeout=None,
)
def test_foundry_local_client_init_with_timeout(mock_foundry_local_manager: MagicMock) -> None:
"""Test FoundryLocalClient initialization with custom timeout."""
with patch(
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
return_value=mock_foundry_local_manager,
) as mock_manager_class:
FoundryLocalClient(model="test-model-id", timeout=60.0)
mock_manager_class.assert_called_once_with(
bootstrap=True,
timeout=60.0,
)
def test_foundry_local_client_init_model_not_found(mock_foundry_local_manager: MagicMock) -> None:
"""Test FoundryLocalClient initialization when model is not found."""
mock_foundry_local_manager.get_model_info.return_value = None
with (
patch(
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
return_value=mock_foundry_local_manager,
),
pytest.raises(ValueError, match="not found in Foundry Local"),
):
FoundryLocalClient(model="unknown-model")
def test_foundry_local_client_uses_model_info_id(mock_foundry_local_manager: MagicMock) -> None:
"""Test that client uses the model ID from model_info, not the alias."""
mock_model_info = MagicMock()
mock_model_info.id = "resolved-model-id"
mock_foundry_local_manager.get_model_info.return_value = mock_model_info
with patch(
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
return_value=mock_foundry_local_manager,
):
client = FoundryLocalClient(model="model-alias")
assert client.model == "resolved-model-id"
def test_foundry_local_client_init_from_env(
foundry_local_unit_test_env: dict[str, str], mock_foundry_local_manager: MagicMock
) -> None:
"""Test FoundryLocalClient initialization using environment variables."""
with patch(
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
return_value=mock_foundry_local_manager,
):
client = FoundryLocalClient()
assert client.model == foundry_local_unit_test_env["FOUNDRY_LOCAL_MODEL"]
def test_foundry_local_client_init_with_device(mock_foundry_local_manager: MagicMock) -> None:
"""Test FoundryLocalClient initialization with device parameter."""
from foundry_local.models import DeviceType
with patch(
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
return_value=mock_foundry_local_manager,
):
FoundryLocalClient(model="test-model-id", device=DeviceType.CPU)
mock_foundry_local_manager.get_model_info.assert_called_once_with(
"test-model-id",
device=DeviceType.CPU,
)
mock_foundry_local_manager.download_model.assert_called_once_with(
"test-model-id",
device=DeviceType.CPU,
)
mock_foundry_local_manager.load_model.assert_called_once_with(
"test-model-id",
device=DeviceType.CPU,
)
def test_foundry_local_client_init_model_not_found_with_device(mock_foundry_local_manager: MagicMock) -> None:
"""Test FoundryLocalClient error message includes device when model not found with device specified."""
from foundry_local.models import DeviceType
mock_foundry_local_manager.get_model_info.return_value = None
with (
patch(
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
return_value=mock_foundry_local_manager,
),
pytest.raises(ValueError, match="unknown-model:GPU.*not found"),
):
FoundryLocalClient(model="unknown-model", device=DeviceType.GPU)
def test_foundry_local_client_init_with_prepare_model_false(mock_foundry_local_manager: MagicMock) -> None:
"""Test FoundryLocalClient initialization with prepare_model=False skips download and load."""
with patch(
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
return_value=mock_foundry_local_manager,
):
FoundryLocalClient(model="test-model-id", prepare_model=False)
mock_foundry_local_manager.download_model.assert_not_called()
mock_foundry_local_manager.load_model.assert_not_called()
def test_foundry_local_client_init_calls_download_and_load(mock_foundry_local_manager: MagicMock) -> None:
"""Test FoundryLocalClient initialization calls download_model and load_model by default."""
with patch(
"agent_framework_foundry_local._foundry_local_client.FoundryLocalManager",
return_value=mock_foundry_local_manager,
):
FoundryLocalClient(model="test-model-id")
mock_foundry_local_manager.download_model.assert_called_once_with(
"test-model-id",
device=None,
)
mock_foundry_local_manager.load_model.assert_called_once_with(
"test-model-id",
device=None,
)